-
-
Notifications
You must be signed in to change notification settings - Fork 268
Expand file tree
/
Copy pathuse-cache-runtime.tsx
More file actions
117 lines (103 loc) · 3.74 KB
/
Copy pathuse-cache-runtime.tsx
File metadata and controls
117 lines (103 loc) · 3.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import {
createClientTemporaryReferenceSet,
createFromReadableStream,
encodeReply,
} from '@vitejs/plugin-rsc/rsc/client'
import {
createTemporaryReferenceSet,
decodeReply,
renderToReadableStream,
} from '@vitejs/plugin-rsc/rsc/server'
// based on
// https://github.com/vercel/next.js/pull/70435
// https://github.com/vercel/next.js/blob/09a2167b0a970757606b7f91ff2d470f77f13f8c/packages/next/src/server/use-cache/use-cache-wrapper.ts
const cachedFnMap = new WeakMap<Function, unknown>()
const cachedFnCacheEntries = new WeakMap<
Function,
Record<string, Promise<StreamCacher>>
>()
export default function cacheWrapper(fn: (...args: any[]) => Promise<unknown>) {
if (cachedFnMap.has(fn)) {
return cachedFnMap.get(fn)!
}
async function cachedFn(...args: any[]): Promise<unknown> {
let cacheEntries = cachedFnCacheEntries.get(cachedFn)
if (!cacheEntries) {
cacheEntries = {}
cachedFnCacheEntries.set(cachedFn, cacheEntries)
}
// Serialize arguments to a cache key via `encodeReply` from `react-server-dom/client`.
// NOTE: using `renderToReadableStream` here for arguments serialization would end up
// serializing react elements (e.g. children props), which causes
// those arguments to be included as a cache key and it doesn't achieve
// "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(args, {
temporaryReferences: clientTemporaryReferences,
})
const serializedCacheKey = await replyToCacheKey(encodedArguments)
// cache `fn` result as stream
// (cache value is promise so that it dedupes concurrent async calls)
const entryPromise = (cacheEntries[serializedCacheKey] ??= (async () => {
const temporaryReferences = createTemporaryReferenceSet()
const decodedArgs = await decodeReply(encodedArguments, {
temporaryReferences,
})
// run the original function
const result = await fn(...decodedArgs)
// serialize result to a ReadableStream
const stream = renderToReadableStream(result, {
environmentName: 'Cache',
temporaryReferences,
})
return new StreamCacher(stream)
})())
// deserialized cached stream
const stream = (await entryPromise).get()
const result = createFromReadableStream(stream, {
environmentName: 'Cache',
replayConsoleLogs: true,
temporaryReferences: clientTemporaryReferences,
})
return result
}
cachedFnMap.set(fn, cachedFn)
return cachedFn
}
export function revalidateCache(cachedFn: Function) {
cachedFnCacheEntries.delete(cachedFn)
}
class StreamCacher {
constructor(private stream: ReadableStream<Uint8Array>) {}
get(): ReadableStream<Uint8Array> {
const [returnStream, savedStream] = this.stream.tee()
this.stream = savedStream
return returnStream
}
}
async function replyToCacheKey(reply: string | FormData) {
if (typeof reply === 'string') {
return reply
}
// `new Response(reply).arrayBuffer()` would serialize FormData with a random
// multipart boundary, so encode entries directly to keep cache keys stable.
const parts: BlobPart[] = []
for (const [name, value] of reply) {
if (typeof value === 'string') {
parts.push(JSON.stringify([name, 'string', value]), '\0')
} else {
parts.push(
JSON.stringify([name, 'file']),
'\0',
await value.arrayBuffer(),
'\0',
)
}
}
const buffer = await crypto.subtle.digest(
'SHA-256',
await new Blob(parts).arrayBuffer(),
)
return btoa(String.fromCharCode(...new Uint8Array(buffer)))
}