diff --git a/src/app/(components)/cache/cache-provider.tsx b/src/app/(components)/cache/cache-provider.tsx new file mode 100644 index 00000000..f60240e6 --- /dev/null +++ b/src/app/(components)/cache/cache-provider.tsx @@ -0,0 +1,26 @@ +"use client"; +import * as React from "react"; + +export function RSCCacheProvider({ children }: { children: React.ReactNode }) { + const [cache] = React.useState(() => new Map()); + return ( + {children} + ); +} + +export type RSCCacheContextType = Map< + string, + Promise +> | null; + +export const CacheContext = React.createContext(null); + +export function useRSCCacheContext() { + const cache = React.use(CacheContext); + + if (!cache) { + throw new Error("Cache is null, this should never arrives"); + } + + return cache; +} diff --git a/src/app/(components)/cache/cache.client.tsx b/src/app/(components)/cache/cache.client.tsx index d40e4d93..0b26ff4a 100644 --- a/src/app/(components)/cache/cache.client.tsx +++ b/src/app/(components)/cache/cache.client.tsx @@ -5,6 +5,7 @@ import * as RSDW from "react-server-dom-webpack/client"; import { getSSRManifest } from "~/app/(components)/cache/manifest"; import { ErrorBoundary } from "react-error-boundary"; +import { useRSCCacheContext } from "~/app/(components)/cache/cache-provider"; function transformStringToStream(input: string) { // Using Flight to deserialize the args from the string. @@ -21,7 +22,13 @@ function transformStringToStream(input: string) { }); } -export function CacheClient({ payload }: { payload: string }) { +export function CacheClient({ + payload, + cacheKey +}: { + payload: string; + cacheKey: string; +}) { if (typeof window === "undefined") { console.time("running cache client for SSR..."); console.log("[SSR] before `use`"); @@ -29,7 +36,29 @@ export function CacheClient({ payload }: { payload: string }) { console.log("[CSR] before `use`"); console.time("running cache client for CSR..."); } - const element = React.use(resolveElementCached(payload)); + + let rscPromise: Promise | null = null; + const rscCache = useRSCCacheContext(); + if (rscCache.has(cacheKey)) { + rscPromise = rscCache.get(cacheKey)!; + } else { + const rscStream = transformStringToStream(payload); + // Render to HTML + if (typeof window === "undefined") { + // the SSR manifest contains all the client components that will be SSR'ed + // And also how to import them + rscPromise = RSDWSSr.createFromReadableStream( + rscStream, + getSSRManifest() + ); + } else { + // Hydrate or CSR + rscPromise = RSDW.createFromReadableStream(rscStream, {}); + } + rscCache.set(cacheKey, rscPromise); + } + + const element = React.use(rscPromise); if (typeof window === "undefined") { console.log("[SSR] after `use`"); console.timeEnd("running cache client for SSR..."); @@ -40,47 +69,6 @@ export function CacheClient({ payload }: { payload: string }) { return element; } -/** - * Custom `cache` function as `React.cache` doesn't work in the client - * @param fn - * @returns - */ -function fnCache any>(fn: T): T { - const cache = new Map(); - - return function cachedFn(...args: Parameters): ReturnType { - const key = JSON.stringify(args); - if (cache.has(key)) { - return cache.get(key); - } - - const result = fn(...args); - cache.set(key, result); - return result; - } as T; -} - -const resolveElementCached = fnCache(async function resolveElementCached( - payload: string -) { - const rscStream = transformStringToStream(payload); - let rscPromise: Promise | null = null; - - // Render to HTML - if (typeof window === "undefined") { - // the SSR manifest contains all the client components that will be SSR'ed - // And also how to import them - rscPromise = RSDWSSr.createFromReadableStream(rscStream, getSSRManifest()); - } - - // Hydrate or CSR - if (rscPromise === null) { - rscPromise = RSDW.createFromReadableStream(rscStream, {}); - } - - return await rscPromise; -}); - export function CacheErrorBoundary({ children, fallback diff --git a/src/app/(components)/cache/cache.server.tsx b/src/app/(components)/cache/cache.server.tsx index 2f8968ed..e9862b6b 100644 --- a/src/app/(components)/cache/cache.server.tsx +++ b/src/app/(components)/cache/cache.server.tsx @@ -94,12 +94,14 @@ export async function Cache({ } if (debug) { - return
{cachedPayload.rsc}
; + return ( +
{cachedPayload.rsc}
+ ); } return ( }> - + ); } catch (error) { diff --git a/src/app/(components)/markdown/markdown-h.tsx b/src/app/(components)/markdown/markdown-h.tsx index 30195eeb..b6d249ba 100644 --- a/src/app/(components)/markdown/markdown-h.tsx +++ b/src/app/(components)/markdown/markdown-h.tsx @@ -30,7 +30,9 @@ export function MarkdownH({ as, showLink, ...props }: MarkdownHProps) { href={`#${props.id}`} className={clsx( "absolute -left-6 -top-1.5 opacity-100 transition duration-150", - "md:opacity-0 md:group-hover:opacity-100" + "md:opacity-0 md:group-hover:opacity-100", + "focus:opacity-100 focus:ring-2 focus:ring-accent focus:outline-none", + "rounded-md" )} > diff --git a/src/app/(components)/providers.tsx b/src/app/(components)/providers.tsx index fd25b5a8..2af2bb8d 100644 --- a/src/app/(components)/providers.tsx +++ b/src/app/(components)/providers.tsx @@ -4,6 +4,7 @@ import * as React from "react"; // components import { RouterProvider } from "react-aria-components"; import { ReactQueryProvider } from "~/app/(components)/react-query-provider"; +import { RSCCacheProvider } from "~/app/(components)/cache/cache-provider"; // utils import { useRouter } from "next/navigation"; @@ -13,7 +14,9 @@ export function ClientProviders({ children }: { children: React.ReactNode }) { return ( - {children} + + {children} + ); } diff --git a/src/lib/server/rsc-utils.server.ts b/src/lib/server/rsc-utils.server.ts index c0237859..763b5972 100644 --- a/src/lib/server/rsc-utils.server.ts +++ b/src/lib/server/rsc-utils.server.ts @@ -17,6 +17,9 @@ export function nextCache( return cache(unstable_cache(cb, options.tags, options)); } +/** + * This function is only used in `DEV` because fetch-cache is bypassed by nextjs on DEV + */ function cacheForDev( cb: T, options: { @@ -30,16 +33,12 @@ function cacheForDev( cached: ReturnType; }>(key); - if (!cachedValue) { - cachedValue = await cb(...args); - await kv.set( - key, - { - cached: cachedValue - }, - options.revalidate - ); + if (!cachedValue?.cached) { + cachedValue = { + cached: await cb(...args) + }; + await kv.set(key, cachedValue, options.revalidate); } - return cachedValue!.cached; + return cachedValue.cached; }; }