Skip to content

Commit

Permalink
Use Server/Client Manifests from Singleton in encryption-utils (#70485)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sebmarkbage authored Sep 26, 2024
1 parent 0b926ff commit 80974df
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 28 deletions.
20 changes: 14 additions & 6 deletions packages/next/src/server/app-render/encryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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({
Expand All @@ -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,
},
}
)
Expand Down
71 changes: 49 additions & 22 deletions packages/next/src/server/use-cache/use-cache-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand All @@ -81,28 +81,39 @@ async function generateCacheEntry(
fn: any
): Promise<ReadableStream> {
const temporaryReferences = createServerTemporaryReferenceSet()
const [, args] = await decodeReply<any[]>(encodedArguments, serverManifest, {
temporaryReferences,
})

const [, args] = await decodeReply<any[]>(
encodedArguments,
getServerModuleMap(),
{
temporaryReferences,
}
)

// Invoke the inner function to load a new result.
const result = fn.apply(null, args)

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()

Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 80974df

Please sign in to comment.