Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
8732f56
feat(rsc): demonstrate encrypted cache captures
hi-ogawa Aug 5, 2026
b42a63c
refactor(rsc): split protected cache captures example
hi-ogawa Aug 5, 2026
b63b4eb
Merge origin/main into fix/issue-1393
hi-ogawa Aug 5, 2026
cfcc4d2
chore(rsc): order protected captures route last
hi-ogawa Aug 5, 2026
38d2d81
refactor(rsc): render capture selector on server
hi-ogawa Aug 5, 2026
f572f1a
test(rsc): verify protected captures across reload
hi-ogawa Aug 5, 2026
c152ca9
nit
hi-ogawa Aug 5, 2026
7612a59
docs(rsc): explain protected capture argument adapter
hi-ogawa Aug 5, 2026
77d4c32
nit
hi-ogawa Aug 5, 2026
144a2b7
Merge origin/main into fix/issue-1393
hi-ogawa Aug 5, 2026
9e66ba2
fix(rsc): stabilize encrypted form cache keys
hi-ogawa Aug 5, 2026
56bdff7
docs(rsc): explain form cache key divergence
hi-ogawa Aug 5, 2026
da14545
test(rsc): document encrypted form reload miss
hi-ogawa Aug 5, 2026
2ed49ab
refactor(rsc): keep cache capture envelope synchronous
hi-ogawa Aug 5, 2026
cd72fec
refactor(rsc): clarify protected cache key encoding
hi-ogawa Aug 5, 2026
add3c5c
nit
hi-ogawa Aug 5, 2026
9b799ad
refactor(rsc): name cache capture encryption explicitly
hi-ogawa Aug 5, 2026
329abcb
nit
hi-ogawa Aug 5, 2026
5c0734e
refactor(rsc): remove cache capture key marker
hi-ogawa Aug 5, 2026
f45d329
nit
hi-ogawa Aug 5, 2026
09400ad
docs(rsc): note duplicate capture decryption
hi-ogawa Aug 5, 2026
2f70fe9
refactor(rsc): decrypt cache captures once
hi-ogawa Aug 5, 2026
8e36cae
Merge origin/main into refactor/use-cache-single-decode
hi-ogawa Aug 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ export function callableCachePlugin(): Plugin {
hoistRuntime: true,
runtime: (value, name) => runtime(value, name, {}),
encode: (value) => `$$encryptCacheCaptures(${value})`,
decode: (value) => `await $$decryptCacheCaptures(${value})`,
// The cache runtime replaces the envelope with decoded captures
// before invoking this private implementation.
decode: (value) => value,
})
if (!result.output.hasChanged()) {
manager.serverReferences.deleteClaim(pluginName, id)
Expand All @@ -71,7 +73,7 @@ export function callableCachePlugin(): Plugin {
exportNames: 'names' in result ? result.names : result.exportNames,
})
result.output.prepend(
`import $$cacheWrapper, { encryptCacheCaptures as $$encryptCacheCaptures, decryptCacheCaptures as $$decryptCacheCaptures } from "/src/framework/use-cache-runtime";\n` +
`import $$cacheWrapper, { encryptCacheCaptures as $$encryptCacheCaptures } from "/src/framework/use-cache-runtime";\n` +
`import * as $$ReactServer from "@vitejs/plugin-rsc/react/rsc/server";\n`,
)
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,22 @@ export default function cacheWrapper(
// "use cache static shell + dynamic children props" pattern.
// cf. https://nextjs.org/docs/app/api-reference/directives/use-cache#non-serializable-arguments
const clientTemporaryReferences = createClientTemporaryReferenceSet()
const encodedArguments = await encodeReply(admittedArgs, {
let executionArguments = admittedArgs
let cacheArguments = admittedArgs
const firstArgument = admittedArgs[0]
if (isCacheCaptureEnvelope(firstArgument)) {
const captures = await decryptCacheCaptures(firstArgument)
const invocationArguments = admittedArgs.slice(1)
// The private implementation receives the decoded capture array, while the
// cache key retains the same flattened logical argument shape as direct captures.
executionArguments = [captures, ...invocationArguments]
cacheArguments = [...captures, ...invocationArguments]
}
const encodedArguments = await encodeReply(executionArguments, {
temporaryReferences: clientTemporaryReferences,
})
let encodedCacheArguments = encodedArguments
// Re-encode decrypted captures so cache identity reflects their logical values
// rather than the randomized ciphertext used by the transport arguments.
const firstArgument = admittedArgs[0]
if (isCacheCaptureEnvelope(firstArgument)) {
// TODO: On a cache miss, the hoister-generated implementation decrypts the
// original envelope again. A tighter adapter could reuse these captures.
const cacheArguments = [
...(await decryptCacheCaptures(firstArgument)),
...admittedArgs.slice(1),
]
if (cacheArguments !== executionArguments) {
encodedCacheArguments = await encodeReply(cacheArguments, {
temporaryReferences: createClientTemporaryReferenceSet(),
})
Expand Down Expand Up @@ -174,7 +176,7 @@ export function encryptCacheCaptures(
}
}

export async function decryptCacheCaptures(
async function decryptCacheCaptures(
envelope: CacheCaptureEnvelope,
): Promise<unknown[]> {
const { encrypted } = envelope
Expand Down
Loading