From 80974dfa95016f2c317e934991a7c18eb8a5fbaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 26 Sep 2024 11:49:55 -0400 Subject: [PATCH] Use Server/Client Manifests from Singleton in encryption-utils (#70485) The closure encryption utilities need the same module maps as use-cache-wrapper. We can share the same singletons. Using singletons for this is sketchy because if concurrent requests switches to another page with a different manifest, it would potentially have missing entries. We should ideally use AsyncLocalStorage but this is not making anything worse and any solution should apply to both. This doesn't actually make anything new work. Because you can't pass a Server References into these functions since React doesn't yet allow Server References inside the RSC layer to be encoded through encodeReply. We could. One thing to note there is that if we do allow that then the id of the Server Reference ideally includes the hash of its implementation (not just Cache IDs) because if the implementation can change without the id then passing a Server Reference as an argument to "use cache" and calling it within the cache should not reuse results if the implementation changes. It also doesn't yet work to pass Client References out of these functions because the SSR manifest is missing. That is already missing for encryption too and we should pass the same thing into both there. This clarifies that in either case we should always pass null for moduleLoading because that's only used for SSR and would lead to unnecessary preloads to be added if we replayed the preloads that are already covered by client references. --- .../next/src/server/app-render/encryption.ts | 20 ++++-- .../src/server/use-cache/use-cache-wrapper.ts | 71 +++++++++++++------ 2 files changed, 63 insertions(+), 28 deletions(-) diff --git a/packages/next/src/server/app-render/encryption.ts b/packages/next/src/server/app-render/encryption.ts index 59ffb87369d35..70f864c8ba415 100644 --- a/packages/next/src/server/app-render/encryption.ts +++ b/packages/next/src/server/app-render/encryption.ts @@ -23,6 +23,8 @@ import { stringToUint8Array, } from './encryption-utils' +import type { ManifestNode } from '../../build/webpack/plugins/flight-manifest-plugin' + const textEncoder = new TextEncoder() const textDecoder = new TextDecoder() @@ -97,6 +99,13 @@ export async function decryptActionBoundArgs( // Decrypt the serialized string with the action id as the salt. const decryped = await decodeActionBoundArg(actionId, await encrypted) + // TODO: We can't use the client reference manifest to resolve the modules + // on the server side - instead they need to be recovered as the module + // references (proxies) again. + // For now, we'll just use an empty module map. + const ssrModuleMap: { + [moduleExport: string]: ManifestNode + } = {} // Using Flight to deserialize the args from the string. const deserialized = await createFromReadableStream( new ReadableStream({ @@ -107,12 +116,11 @@ export async function decryptActionBoundArgs( }), { ssrManifest: { - // TODO: We can't use the client reference manifest to resolve the modules - // on the server side - instead they need to be recovered as the module - // references (proxies) again. - // For now, we'll just use an empty module map. - moduleLoading: {}, - moduleMap: {}, + // moduleLoading must be null because we don't want to trigger preloads of ClientReferences + // to be added to the current execution. Instead, we'll wait for any ClientReference + // to be emitted which themselves will handle the preloading. + moduleLoading: null, + moduleMap: ssrModuleMap, }, } ) diff --git a/packages/next/src/server/use-cache/use-cache-wrapper.ts b/packages/next/src/server/use-cache/use-cache-wrapper.ts index ecfe5c7675cbf..5cec14e907507 100644 --- a/packages/next/src/server/use-cache/use-cache-wrapper.ts +++ b/packages/next/src/server/use-cache/use-cache-wrapper.ts @@ -15,6 +15,13 @@ import { import type { StaticGenerationStore } from '../../client/components/static-generation-async-storage.external' import { staticGenerationAsyncStorage } from '../../client/components/static-generation-async-storage.external' +import { + getClientReferenceManifestSingleton, + getServerModuleMap, +} from '../app-render/encryption-utils' + +import type { ManifestNode } from '../../build/webpack/plugins/flight-manifest-plugin' + type CacheEntry = { value: ReadableStream stale: boolean @@ -63,13 +70,6 @@ cacheHandlerMap.set('default', { shouldRevalidateStale: false, }) -const serverManifest: any = null // TODO -const clientManifest: any = null // TODO -const ssrManifest: any = { - moduleMap: {}, - moduleLoading: null, -} // TODO - // TODO: Consider moving this another module that is guaranteed to be required in a safe scope. const runInCleanSnapshot = createSnapshot() @@ -81,9 +81,14 @@ async function generateCacheEntry( fn: any ): Promise { const temporaryReferences = createServerTemporaryReferenceSet() - const [, args] = await decodeReply(encodedArguments, serverManifest, { - temporaryReferences, - }) + + const [, args] = await decodeReply( + encodedArguments, + getServerModuleMap(), + { + temporaryReferences, + } + ) // Invoke the inner function to load a new result. const result = fn.apply(null, args) @@ -91,18 +96,24 @@ async function generateCacheEntry( let didError = false let firstError: any = null - const stream = renderToReadableStream(result, clientManifest, { - environmentName: 'Cache', - temporaryReferences, - onError(error: any) { - // Report the error. - console.error(error) - if (!didError) { - didError = true - firstError = error - } - }, - }) + const clientReferenceManifestSingleton = getClientReferenceManifestSingleton() + + const stream = renderToReadableStream( + result, + clientReferenceManifestSingleton.clientModules, + { + environmentName: 'Cache', + temporaryReferences, + onError(error: any) { + // Report the error. + console.error(error) + if (!didError) { + didError = true + firstError = error + } + }, + } + ) const [returnStream, savedStream] = stream.tee() @@ -235,6 +246,22 @@ export function cache(kind: string, id: string, fn: any) { // server terminal. Once while generating the cache entry and once when replaying it on // the server, which is required to pick it up for replaying again on the client. const replayConsoleLogs = true + + // TODO: We can't use the client reference manifest to resolve the modules + // on the server side - instead they need to be recovered as the module + // references (proxies) again. + // For now, we'll just use an empty module map. + const ssrModuleMap: { + [moduleExport: string]: ManifestNode + } = {} + + const ssrManifest = { + // moduleLoading must be null because we don't want to trigger preloads of ClientReferences + // to be added to the consumer. Instead, we'll wait for any ClientReference to be emitted + // which themselves will handle the preloading. + moduleLoading: null, + moduleMap: ssrModuleMap, + } return createFromReadableStream(stream, { ssrManifest, temporaryReferences,