@thomas.f/prnext 0.1.17-alpha → 0.1.19-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -868
- package/build/actions.mjs +27 -15
- package/build/analyze.mjs +180 -24
- package/build/app-config.mjs +13 -0
- package/build/app-static.mjs +34 -12
- package/build/app-styles.mjs +79 -0
- package/build/app.mjs +122 -46
- package/build/bundle-runtime.mjs +22 -9
- package/build/cache-components.mjs +1 -1
- package/build/cache-handlers.mjs +1 -1
- package/build/compact-css.mjs +53 -0
- package/build/compilation-cache.mjs +269 -0
- package/build/compilation-store.mjs +142 -0
- package/build/compiler.mjs +34 -10
- package/build/config.mjs +34 -3
- package/build/create.mjs +239 -0
- package/build/css-modules.mjs +56 -12
- package/build/deployment-assets.mjs +89 -0
- package/build/deployment-config.mjs +18 -0
- package/build/dev-css-assets.mjs +173 -0
- package/build/dev-css-update.mjs +18 -14
- package/build/dev-dependencies.mjs +1 -1
- package/build/dev-discovery.mjs +3 -3
- package/build/dev-lazy-build.mjs +77 -0
- package/build/dev.mjs +15 -8
- package/build/dynamic.mjs +14 -7
- package/build/edge.mjs +3 -3
- package/build/fonts.mjs +1 -1
- package/build/framework-imports.mjs +15 -5
- package/build/html-only-config.mjs +53 -0
- package/build/html-only-graph.mjs +67 -0
- package/build/incremental.mjs +112 -18
- package/build/index.mjs +90 -28
- package/build/instrumentation.mjs +30 -0
- package/build/lazy-dynamic-imports.mjs +137 -0
- package/build/metadata-routes.mjs +1 -1
- package/build/migrate.mjs +4 -4
- package/build/migration-audit.mjs +19 -31
- package/build/migration-imports.mjs +245 -0
- package/build/migration-scenarios-runner.mjs +73 -0
- package/build/migration-scenarios.mjs +72 -0
- package/build/native-discovery.mjs +65 -5
- package/build/native-packages.mjs +2 -0
- package/build/native-type-config.mjs +37 -0
- package/build/npm-packages.mjs +1 -0
- package/build/persistent-cache.mjs +18 -3
- package/build/precompress.mjs +1 -1
- package/build/prerender-cache.mjs +70 -0
- package/build/prerender-config.mjs +29 -0
- package/build/prerender-pages.mjs +19 -9
- package/build/prerender-pool.mjs +165 -0
- package/build/prerender-process-worker.mjs +58 -0
- package/build/prerender-worker.mjs +5 -5
- package/build/production-builder.mjs +59 -10
- package/build/react-compiler-report.mjs +225 -0
- package/build/react-compiler.mjs +139 -0
- package/build/runtime-contract.mjs +2 -2
- package/build/standalone.mjs +21 -3
- package/build/static-export.mjs +4 -4
- package/build/styles.mjs +6 -4
- package/build/transform.mjs +1 -1
- package/build/typed-routes.mjs +4 -0
- package/build/typescript-dev.mjs +35 -8
- package/build/typescript-native-worker.mjs +55 -0
- package/build/typescript-native.mjs +18 -0
- package/build/typescript-toolchain.mjs +19 -0
- package/build/typescript.mjs +6 -4
- package/build/webpack-graph.mjs +19 -6
- package/build/worker-memory.mjs +9 -0
- package/cli.mjs +70 -5
- package/compat/activity-cache.cjs +26 -0
- package/compat/after.cjs +6 -4
- package/compat/app-dynamic.cjs +12 -1
- package/compat/app-layout-context.cjs +52 -2
- package/compat/base64.cjs +10 -0
- package/compat/cache-handlers.cjs +15 -3
- package/compat/cache-wire.cjs +28 -0
- package/compat/cache.cjs +2 -1
- package/compat/cache.d.cts +4 -0
- package/compat/cookies.cjs +21 -5
- package/compat/data-cache.cjs +12 -8
- package/compat/flight-delta.cjs +152 -0
- package/compat/headers.cjs +4 -2
- package/compat/incremental-cache.cjs +20 -7
- package/compat/index.d.ts +69 -4
- package/compat/link.cjs +13 -10
- package/compat/navigation-authorization.cjs +9 -0
- package/compat/navigation-stages.cjs +19 -0
- package/compat/offline-store.cjs +11 -0
- package/compat/offline.cjs +5 -0
- package/compat/offline.d.cts +2 -0
- package/compat/redis-cache.cjs +201 -0
- package/compat/redis-cache.d.cts +37 -0
- package/compat/server-primitives.cjs +31 -11
- package/compat/server.d.cts +15 -2
- package/compat/static-generation.cjs +7 -0
- package/compat/use-cache.cjs +3 -0
- package/docs/ai/compatibility.md +248 -2
- package/docs/ai/data.md +165 -5
- package/docs/ai/deployment.md +158 -0
- package/docs/ai/quickstart.md +29 -4
- package/docs/ai/routing.md +93 -0
- package/docs/ai/troubleshooting.md +116 -2
- package/package.json +40 -9
- package/runtime/action-client.mjs +10 -4
- package/runtime/action-server.mjs +2 -0
- package/runtime/app-client.mjs +316 -92
- package/runtime/app-content.mjs +4 -3
- package/runtime/app-partial-client.mjs +3 -0
- package/runtime/app-partial.mjs +1 -1
- package/runtime/app-prefetch-constants.mjs +4 -0
- package/runtime/app-prefetch.mjs +2 -3
- package/runtime/app-render-key.mjs +5 -0
- package/runtime/app-render.mjs +88 -28
- package/runtime/app-routing.mjs +22 -9
- package/runtime/app-static.mjs +18 -8
- package/runtime/app-stream-html.mjs +10 -53
- package/runtime/app-style-selection.mjs +34 -0
- package/runtime/app-styles.mjs +154 -0
- package/runtime/atomic-file.mjs +27 -0
- package/runtime/cache-components-rsc.mjs +7 -2
- package/runtime/cache-request.mjs +2 -1
- package/runtime/deployment-client.mjs +19 -0
- package/runtime/dev-client.mjs +13 -3
- package/runtime/dev-compiler-broker.mjs +42 -0
- package/runtime/dev-file-changes.mjs +123 -8
- package/runtime/dev-indicator.mjs +8 -3
- package/runtime/dev-lazy-imports.mjs +121 -0
- package/runtime/dev-lazy-staging.mjs +14 -0
- package/runtime/dev-lazy-tool.mjs +16 -0
- package/runtime/dev-style-retention.mjs +15 -0
- package/runtime/development.mjs +83 -25
- package/runtime/document.mjs +1 -1
- package/runtime/edge.mjs +6 -5
- package/runtime/fetch-cache.mjs +10 -8
- package/runtime/flight-delta.mjs +74 -0
- package/runtime/flight-tap.mjs +28 -0
- package/runtime/fs.mjs +15 -8
- package/runtime/html-only.mjs +27 -0
- package/runtime/http.mjs +2 -0
- package/runtime/incremental-cache.mjs +25 -4
- package/runtime/instrumentation.mjs +49 -0
- package/runtime/lazy-import-client.mjs +38 -0
- package/runtime/navigation-recovery.mjs +36 -0
- package/runtime/navigation.mjs +7 -0
- package/runtime/node-html-stream.mjs +47 -0
- package/runtime/pages-client.mjs +14 -7
- package/runtime/pages-navigation.mjs +48 -63
- package/runtime/prefetch-scheduler.mjs +110 -0
- package/runtime/profile.mjs +14 -0
- package/runtime/profiles.json +2 -2
- package/runtime/render.mjs +3 -0
- package/runtime/route-pattern.mjs +44 -0
- package/runtime/rsc-worker.mjs +34 -12
- package/runtime/trace.mjs +78 -0
- package/runtime/watch-project.mjs +19 -15
- package/runtime/worker.mjs +70 -26
- package/vendor/nft/LICENSE +7 -0
- package/vendor/nft/README.md +16 -0
- package/vendor/nft/out/analyze.d.ts +8 -0
- package/vendor/nft/out/analyze.js +1216 -0
- package/vendor/nft/out/analyze.js.map +1 -0
- package/vendor/nft/out/cli.d.ts +2 -0
- package/vendor/nft/out/cli.js +94 -0
- package/vendor/nft/out/cli.js.map +1 -0
- package/vendor/nft/out/fs.d.ts +22 -0
- package/vendor/nft/out/fs.js +106 -0
- package/vendor/nft/out/fs.js.map +1 -0
- package/vendor/nft/out/index.d.ts +4 -0
- package/vendor/nft/out/index.js +25 -0
- package/vendor/nft/out/index.js.map +1 -0
- package/vendor/nft/out/node-file-trace.d.ts +41 -0
- package/vendor/nft/out/node-file-trace.js +380 -0
- package/vendor/nft/out/node-file-trace.js.map +1 -0
- package/vendor/nft/out/resolve-dependency.d.ts +6 -0
- package/vendor/nft/out/resolve-dependency.js +427 -0
- package/vendor/nft/out/resolve-dependency.js.map +1 -0
- package/vendor/nft/out/types.d.ts +66 -0
- package/vendor/nft/out/types.js +2 -0
- package/vendor/nft/out/types.js.map +1 -0
- package/vendor/nft/out/utils/ast-helpers.js +49 -0
- package/vendor/nft/out/utils/ast-helpers.js.map +1 -0
- package/vendor/nft/out/utils/binary-locators.js +88 -0
- package/vendor/nft/out/utils/binary-locators.js.map +1 -0
- package/vendor/nft/out/utils/get-package-base.js +30 -0
- package/vendor/nft/out/utils/get-package-base.js.map +1 -0
- package/vendor/nft/out/utils/interop-require.js +24 -0
- package/vendor/nft/out/utils/interop-require.js.map +1 -0
- package/vendor/nft/out/utils/node-version.js +10 -0
- package/vendor/nft/out/utils/node-version.js.map +1 -0
- package/vendor/nft/out/utils/sharedlib-emit.js +33 -0
- package/vendor/nft/out/utils/sharedlib-emit.js.map +1 -0
- package/vendor/nft/out/utils/special-cases.js +347 -0
- package/vendor/nft/out/utils/special-cases.js.map +1 -0
- package/vendor/nft/out/utils/static-eval.js +579 -0
- package/vendor/nft/out/utils/static-eval.js.map +1 -0
- package/vendor/nft/out/utils/types.js +2 -0
- package/vendor/nft/out/utils/types.js.map +1 -0
- package/vendor/nft/out/utils/wrappers.js +659 -0
- package/vendor/nft/out/utils/wrappers.js.map +1 -0
- package/vendor/nft/package.json +4 -0
- package/vendor/nft/upstream.json +51 -0
|
@@ -1,8 +1,53 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
'use strict';
|
|
3
3
|
const React = require('react');
|
|
4
|
+
const { AppRouterContext } = require('./app-context.cjs');
|
|
4
5
|
const LayoutContext = React.createContext({ children: [] });
|
|
5
6
|
const LayoutCacheContext = React.createContext(null);
|
|
7
|
+
const ActivityCacheContext = React.createContext(null);
|
|
8
|
+
|
|
9
|
+
function ActivitySlot({ identity, children, authorizationKey }) {
|
|
10
|
+
const retention = React.useContext(ActivityCacheContext);
|
|
11
|
+
authorizationKey ??= retention?.authorizationKey;
|
|
12
|
+
const router = React.useContext(AppRouterContext);
|
|
13
|
+
const epoch = retention?.epoch ?? 0;
|
|
14
|
+
const authorizationEpoch = retention?.authorizationEpoch ?? 0;
|
|
15
|
+
const registrations = React.useRef(new Set());
|
|
16
|
+
const serial = React.useRef(0);
|
|
17
|
+
const [saved, setSaved] = React.useState(() => ({ epoch, authorizationEpoch, active: identity, entries: new Map() }));
|
|
18
|
+
let entries = saved.entries;
|
|
19
|
+
const current = entries.get(identity);
|
|
20
|
+
if (saved.epoch !== epoch || saved.authorizationEpoch !== authorizationEpoch || current?.children !== children || current?.router !== router || current?.authorizationKey !== authorizationKey) {
|
|
21
|
+
const sameAuthorization = saved.authorizationEpoch === authorizationEpoch && !(current?.authorizationKey && authorizationKey && current.authorizationKey !== authorizationKey);
|
|
22
|
+
entries = saved.epoch === epoch && sameAuthorization ? new Map(entries) : new Map();
|
|
23
|
+
const preserve = current && saved.authorizationEpoch === authorizationEpoch && (sameAuthorization && (saved.epoch === epoch || saved.active === identity)
|
|
24
|
+
|| !retention?.strictAuthorization && saved.active === identity);
|
|
25
|
+
entries.set(identity, { children, router, authorizationKey, token: preserve ? current.token : {}, reactKey: preserve ? current.reactKey : ++serial.current });
|
|
26
|
+
setSaved({ epoch, authorizationEpoch, active: identity, entries });
|
|
27
|
+
}
|
|
28
|
+
React.useLayoutEffect(() => {
|
|
29
|
+
const cache = retention?.cache;
|
|
30
|
+
if (!cache) return;
|
|
31
|
+
const inactive = new Set([...entries].filter(([key]) => key !== identity).map(([, entry]) => entry.token));
|
|
32
|
+
for (const token of registrations.current) if (!inactive.has(token)) cache.release(token);
|
|
33
|
+
for (const [key, entry] of entries) {
|
|
34
|
+
if (key === identity) cache.release(entry.token);
|
|
35
|
+
else cache.retain(entry.token, () => setSaved(previous => {
|
|
36
|
+
if (!previous.entries.has(key)) return previous;
|
|
37
|
+
const next = new Map(previous.entries); next.delete(key); return { ...previous, entries: next };
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
registrations.current = inactive;
|
|
41
|
+
}, [entries, identity, retention?.cache]);
|
|
42
|
+
React.useLayoutEffect(() => () => {
|
|
43
|
+
for (const token of registrations.current) retention?.cache.release(token);
|
|
44
|
+
registrations.current.clear();
|
|
45
|
+
}, [retention?.cache]);
|
|
46
|
+
// Identical boundary structure during SSR and hydration. Hidden branches get
|
|
47
|
+
// their original router context and their effects are disconnected by React.
|
|
48
|
+
return [...entries].map(([key, entry]) => React.createElement(React.Activity, { key: entry.reactKey, mode: key === identity ? 'visible' : 'hidden' },
|
|
49
|
+
React.createElement(AppRouterContext.Provider, { value: key === identity ? router : entry.router }, entry.children)));
|
|
50
|
+
}
|
|
6
51
|
|
|
7
52
|
function LayoutProvider({ id, segments, children }) {
|
|
8
53
|
const cache = React.useContext(LayoutCacheContext);
|
|
@@ -13,15 +58,20 @@ function LayoutProvider({ id, segments, children }) {
|
|
|
13
58
|
return React.createElement(LayoutContext.Provider, { value: current }, children);
|
|
14
59
|
}
|
|
15
60
|
|
|
16
|
-
function LayoutSlot({ id, preserve, children }) {
|
|
61
|
+
function LayoutSlot({ id, preserve, children, activityKey, activityPreserve, authorizationKey }) {
|
|
17
62
|
const cache = React.useContext(LayoutCacheContext);
|
|
18
63
|
const previous = React.useRef(children);
|
|
19
64
|
const current = preserve && cache && Object.hasOwn(cache.slots, id) ? cache.slots[id] : preserve ? previous.current : children;
|
|
20
65
|
React.useLayoutEffect(() => { previous.current = current; if (cache && id) cache.slots[id] = current; }, [cache, id, current]);
|
|
21
|
-
|
|
66
|
+
const previousKey = React.useRef(activityKey);
|
|
67
|
+
const identity = preserve || activityPreserve ? previousKey.current : activityKey;
|
|
68
|
+
React.useLayoutEffect(() => { previousKey.current = identity; }, [identity]);
|
|
69
|
+
return activityKey === undefined ? current ?? null : React.createElement(ActivitySlot, { identity, authorizationKey }, current);
|
|
22
70
|
}
|
|
23
71
|
|
|
24
72
|
module.exports.LayoutContext = LayoutContext;
|
|
25
73
|
module.exports.LayoutCacheContext = LayoutCacheContext;
|
|
26
74
|
module.exports.LayoutProvider = LayoutProvider;
|
|
27
75
|
module.exports.LayoutSlot = LayoutSlot;
|
|
76
|
+
module.exports.ActivitySlot = ActivitySlot;
|
|
77
|
+
module.exports.ActivityCacheContext = ActivityCacheContext;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Native decoding avoids a JavaScript callback for each Flight byte. The loop
|
|
3
|
+
// remains available on browsers that do not implement Uint8Array.fromBase64.
|
|
4
|
+
function decodeBase64(value) {
|
|
5
|
+
if(typeof Uint8Array.fromBase64==='function')return Uint8Array.fromBase64(value);
|
|
6
|
+
const binary=atob(value),bytes=new Uint8Array(binary.length);
|
|
7
|
+
for(let n=0;n<binary.length;n++)bytes[n]=binary.charCodeAt(n);
|
|
8
|
+
return bytes;
|
|
9
|
+
}
|
|
10
|
+
module.exports={decodeBase64};
|
|
@@ -80,7 +80,9 @@ async function read(entry, context) {
|
|
|
80
80
|
if (size > MAX_BYTES) throw new Error('CacheEntry exceeds 2 MiB');
|
|
81
81
|
chunks.push(Buffer.from(value));
|
|
82
82
|
}
|
|
83
|
-
|
|
83
|
+
// Buffer.from above owns each chunk even when a custom producer reuses its
|
|
84
|
+
// Uint8Array on the next pull. Keep that copy, but do not copy it twice.
|
|
85
|
+
return chunks.length===1?chunks[0]:Buffer.concat(chunks, size);
|
|
84
86
|
} finally {
|
|
85
87
|
if (!complete) void reader.cancel().catch(() => {});
|
|
86
88
|
reader.releaseLock();
|
|
@@ -139,10 +141,19 @@ async function customCachedValue(key, kind, producer, options) {
|
|
|
139
141
|
const timestamp = Date.now();
|
|
140
142
|
const record = { revision, timestamp, work: undefined, metadata: undefined };
|
|
141
143
|
work = (async () => {
|
|
144
|
+
let lease,canStore=true;
|
|
145
|
+
try {
|
|
146
|
+
if(typeof handler.acquire==='function'){
|
|
147
|
+
try{
|
|
148
|
+
lease=await call(handler,'acquire',[key,{signal:context.signal}],context);
|
|
149
|
+
const shared=await call(handler,'get',[key,tags],context);
|
|
150
|
+
if(shared){const sharedInfo=metadata(shared);if(Date.now()-sharedInfo.timestamp<Math.min(sharedInfo.expire,sharedInfo.revalidate)*1000){record.metadata=sharedInfo;options.onReadMetadata?.(sharedInfo);return await read(shared,context);}void shared.value.cancel().catch(()=>{});}
|
|
151
|
+
}catch{context.signal?.throwIfAborted();canStore=false;}
|
|
152
|
+
}
|
|
142
153
|
const result = await producer();
|
|
143
154
|
const info = resolveMetadata();
|
|
144
155
|
record.metadata = info;
|
|
145
|
-
if (result.byteLength <= MAX_BYTES && revision === (revisions.get(handler) || 0)) {
|
|
156
|
+
if (canStore && result.byteLength <= MAX_BYTES && revision === (revisions.get(handler) || 0)) {
|
|
146
157
|
try {
|
|
147
158
|
const changed = await expiration(handler, [...new Set([...tags, ...info.tags])], context);
|
|
148
159
|
// Many backends record Date.now() millisecond timestamps. Treat the
|
|
@@ -150,12 +161,13 @@ async function customCachedValue(key, kind, producer, options) {
|
|
|
150
161
|
// hide an invalidation which actually happened after the read started.
|
|
151
162
|
if ((changed === Infinity || changed < Math.floor(timestamp)) && revision === (revisions.get(handler) || 0)) {
|
|
152
163
|
const value = stream(result);
|
|
153
|
-
try { await call(handler, 'set', [key, Promise.resolve({ value, tags: info.tags, stale: info.stale, revalidate: info.revalidate, expire: info.expire, timestamp })], context); }
|
|
164
|
+
try { await call(handler, 'set', [key, Promise.resolve({ value, tags: info.tags, stale: info.stale, revalidate: info.revalidate, expire: info.expire, timestamp }),{lease,softTags:tags}], context); }
|
|
154
165
|
catch { if (!value.locked) void value.cancel().catch(() => {}); }
|
|
155
166
|
}
|
|
156
167
|
} catch { context.signal?.throwIfAborted(); }
|
|
157
168
|
}
|
|
158
169
|
return result;
|
|
170
|
+
}finally{try{await lease?.release();}catch{/* A failed cache cleanup does not replay application work. */}}
|
|
159
171
|
})();
|
|
160
172
|
record.work = work;
|
|
161
173
|
if (pendingCount < 64) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Versioned private framing: bounded JSON metadata followed by uninterpreted bytes.
|
|
3
|
+
const TYPE = 'application/vnd.prnext.cache-v2';
|
|
4
|
+
const MAX_VALUE = 2 * 1024 * 1024;
|
|
5
|
+
const MAX_META = 2 * 1024 * 1024;
|
|
6
|
+
function encode(operation) {
|
|
7
|
+
const { value, ...metadata } = operation;
|
|
8
|
+
if (value !== undefined && !Buffer.isBuffer(value) && !(value instanceof Uint8Array)) throw new TypeError('Cache wire values must be bytes');
|
|
9
|
+
const bytes = value === undefined ? undefined : Buffer.isBuffer(value) ? value : Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
10
|
+
if (bytes && (metadata.op !== 'commit' || bytes.length > MAX_VALUE)) throw new Error('Invalid cache wire value');
|
|
11
|
+
if (metadata.op === 'commit' && !bytes) throw new Error('Cache commit requires bytes');
|
|
12
|
+
const json = Buffer.from(JSON.stringify(metadata));
|
|
13
|
+
if (json.length > MAX_META) throw new Error('Cache wire metadata exceeds limit');
|
|
14
|
+
const prefix = Buffer.allocUnsafe(4); prefix.writeUInt32BE(json.length);
|
|
15
|
+
return bytes ? [prefix, json, bytes] : [prefix, json];
|
|
16
|
+
}
|
|
17
|
+
function decode(bytes) {
|
|
18
|
+
if (bytes.length < 4) throw new Error('Truncated cache wire frame');
|
|
19
|
+
const length = bytes.readUInt32BE(0);
|
|
20
|
+
if (length > MAX_META || length > bytes.length - 4) throw new Error('Invalid cache wire metadata length');
|
|
21
|
+
const value = JSON.parse(bytes.subarray(4, 4 + length).toString('utf8'));
|
|
22
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid cache wire metadata');
|
|
23
|
+
const raw = bytes.subarray(4 + length);
|
|
24
|
+
if (raw.length > MAX_VALUE || (value.value !== true && raw.length) || (value.value !== undefined && value.value !== true)) throw new Error('Invalid cache wire payload');
|
|
25
|
+
if (value.value === true) value.value = raw;
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
module.exports = { TYPE, MAX_VALUE, MAX_META, encode, decode };
|
package/compat/cache.cjs
CHANGED
|
@@ -96,4 +96,5 @@ function unstable_noStore() {
|
|
|
96
96
|
}
|
|
97
97
|
function refresh() { cacheState(mutationContext('refresh', true)).refresh = true; }
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
const { navigation, prefetch } = require('./navigation-stages.cjs');
|
|
100
|
+
module.exports = { unstable_cache, revalidateTag, updateTag, revalidatePath, unstable_noStore, refresh, cacheTag, cacheLife, navigation, prefetch };
|
package/compat/cache.d.cts
CHANGED
|
@@ -34,3 +34,7 @@ export function updateTag(tag: string): void;
|
|
|
34
34
|
export function revalidatePath(path: string, type?: 'page' | 'layout'): void;
|
|
35
35
|
export function unstable_noStore(): void;
|
|
36
36
|
export function refresh(): void;
|
|
37
|
+
/** Defer runtime work until navigation, while allowing static prerenders. */
|
|
38
|
+
export function navigation(): Promise<void>;
|
|
39
|
+
/** Defer runtime work until a per-link prefetch or navigation. */
|
|
40
|
+
export function prefetch(): Promise<void>;
|
package/compat/cookies.cjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
function parseCookieHeader(value = '') {
|
|
3
|
+
function parseCookieHeader(value = '', { ignoreMalformed = false } = {}) {
|
|
4
4
|
const values = new Map();
|
|
5
5
|
for (const part of String(value).split(';')) {
|
|
6
6
|
const separator = part.indexOf('=');
|
|
7
7
|
if (separator < 1) continue;
|
|
8
8
|
const name = part.slice(0, separator).trim();
|
|
9
9
|
let value = part.slice(separator + 1).trim();
|
|
10
|
-
try { value = decodeURIComponent(value); } catch {
|
|
10
|
+
try { value = decodeURIComponent(value); } catch { if (ignoreMalformed) continue; }
|
|
11
11
|
values.set(name, { name, value });
|
|
12
12
|
}
|
|
13
13
|
return values;
|
|
@@ -50,7 +50,7 @@ function serializeCookie(cookie) {
|
|
|
50
50
|
|
|
51
51
|
function parseResponseCookie(header) {
|
|
52
52
|
const [pair, ...attributes] = header.split(';');
|
|
53
|
-
const cookie = parseCookieHeader(pair).values().next().value;
|
|
53
|
+
const cookie = parseCookieHeader(pair, { ignoreMalformed: true }).values().next().value;
|
|
54
54
|
if (!cookie) return;
|
|
55
55
|
for (const attribute of attributes) {
|
|
56
56
|
const separator = attribute.indexOf('=');
|
|
@@ -71,7 +71,7 @@ function parseResponseCookie(header) {
|
|
|
71
71
|
|
|
72
72
|
class CookieStore {
|
|
73
73
|
constructor(header, { mutable = false, onChange } = {}) {
|
|
74
|
-
this._cookies = parseCookieHeader(header);
|
|
74
|
+
this._cookies = parseCookieHeader(header, { ignoreMalformed: true });
|
|
75
75
|
this._mutable = mutable;
|
|
76
76
|
this._onChange = onChange;
|
|
77
77
|
}
|
|
@@ -99,6 +99,22 @@ class CookieStore {
|
|
|
99
99
|
*[Symbol.iterator]() { for (const [name, cookie] of this._cookies) yield [name, { ...cookie }]; }
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
class RequestCookieStore extends CookieStore {
|
|
103
|
+
constructor(headers) {
|
|
104
|
+
super(headers.get('cookie') || '', { mutable: true, onChange: () => headers.set('cookie', this.toString()) });
|
|
105
|
+
}
|
|
106
|
+
set(name, value) {
|
|
107
|
+
const cookie = typeof name === 'object' ? { name: name.name, value: name.value } : { name, value };
|
|
108
|
+
return super.set(cookie);
|
|
109
|
+
}
|
|
110
|
+
delete(names) {
|
|
111
|
+
const result = Array.isArray(names) ? names.map(name => this._cookies.delete(name)) : this._cookies.delete(names);
|
|
112
|
+
this._onChange?.();
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
clear() { this.delete([...this._cookies.keys()]); return this; }
|
|
116
|
+
}
|
|
117
|
+
|
|
102
118
|
class ResponseCookieStore extends CookieStore {
|
|
103
119
|
constructor(headers) {
|
|
104
120
|
super('', { mutable: true });
|
|
@@ -126,4 +142,4 @@ class ResponseCookieStore extends CookieStore {
|
|
|
126
142
|
toString() { return [...this._cookies.values()].map(serializeCookie).join('; '); }
|
|
127
143
|
}
|
|
128
144
|
|
|
129
|
-
module.exports = { CookieStore, ResponseCookieStore, parseCookieHeader, serializeCookie };
|
|
145
|
+
module.exports = { CookieStore, RequestCookieStore, ResponseCookieStore, parseCookieHeader, serializeCookie };
|
package/compat/data-cache.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
const { AsyncLocalStorage, AsyncResource } = require('node:async_hooks');
|
|
3
3
|
const http = require('node:http');
|
|
4
|
+
const cacheWire = require('./cache-wire.cjs');
|
|
4
5
|
const { setTimeout: delay } = require('node:timers/promises');
|
|
5
6
|
const { trackStaticDependency } = require('./static-generation.cjs');
|
|
6
7
|
const diagnostics = require('./diagnostics.cjs');
|
|
@@ -21,7 +22,8 @@ const rpcScope = new AsyncResource('prnext-cache-transport');
|
|
|
21
22
|
// idle sockets churns ephemeral ports under concurrent PPR traffic. Idle
|
|
22
23
|
// connections expire after 30 seconds and retain no request context.
|
|
23
24
|
const rpcAgent = new http.Agent({ keepAlive: true, maxSockets: 32, maxTotalSockets: 32, maxFreeSockets: 32, timeout: 30_000 });
|
|
24
|
-
function localRpc(target,
|
|
25
|
+
function localRpc(target, parts, signal, binary) {
|
|
26
|
+
const length = parts.reduce((sum,part)=>sum+Buffer.byteLength(part),0);
|
|
25
27
|
return rpcScope.runInAsyncScope(() => new Promise((resolve, reject) => {
|
|
26
28
|
let settled = false;
|
|
27
29
|
const finish = (error, value) => {
|
|
@@ -31,7 +33,7 @@ function localRpc(target, body, signal) {
|
|
|
31
33
|
if (error) reject(error); else resolve(value);
|
|
32
34
|
};
|
|
33
35
|
const request = http.request(target.url, { method: 'POST', agent: rpcAgent,
|
|
34
|
-
headers: { 'content-type': 'application/json', authorization: `Bearer ${target.token}`, 'content-length':
|
|
36
|
+
headers: { 'content-type': binary ? cacheWire.TYPE : 'application/json', authorization: `Bearer ${target.token}`, 'content-length': length },
|
|
35
37
|
}, response => {
|
|
36
38
|
let size = 0;
|
|
37
39
|
const chunks = [];
|
|
@@ -43,14 +45,14 @@ function localRpc(target, body, signal) {
|
|
|
43
45
|
});
|
|
44
46
|
response.on('end', () => {
|
|
45
47
|
const ok = response.statusCode >= 200 && response.statusCode < 300;
|
|
46
|
-
try { finish(null, { ok, status: response.statusCode, value: ok ? JSON.parse(Buffer.concat(chunks, size).toString()) : undefined }); }
|
|
48
|
+
try { finish(null, { ok, status: response.statusCode, value: ok ? (binary ? (response.headers['content-type'] === cacheWire.TYPE ? cacheWire.decode(Buffer.concat(chunks,size)) : (()=>{throw new Error('Cache protocol mismatch')})()) : JSON.parse(Buffer.concat(chunks, size).toString())) : undefined }); }
|
|
47
49
|
catch (error) { finish(error); }
|
|
48
50
|
});
|
|
49
51
|
});
|
|
50
52
|
const abort = () => request.destroy(signal.reason instanceof Error ? signal.reason : new Error('Cache RPC aborted'));
|
|
51
53
|
request.on('error', error => finish(error));
|
|
52
54
|
signal?.addEventListener('abort', abort, { once: true });
|
|
53
|
-
if (signal?.aborted) abort(); else request.end(
|
|
55
|
+
if (signal?.aborted) abort(); else { request.cork(); for(const part of parts) request.write(part); request.end(); request.uncork(); }
|
|
54
56
|
}));
|
|
55
57
|
}
|
|
56
58
|
|
|
@@ -149,9 +151,10 @@ async function acquireExternalCacheLease(key, signal) {
|
|
|
149
151
|
throw new Error('Timed out waiting for a native incremental cache lease');
|
|
150
152
|
}
|
|
151
153
|
async function rpc(target, operation, signal) {
|
|
152
|
-
const body = JSON.stringify(operation);
|
|
153
154
|
const url = new URL(target.url);
|
|
154
155
|
const local = url.protocol === 'http:' && ['127.0.0.1', '[::1]', 'localhost'].includes(url.hostname);
|
|
156
|
+
const binary = local && process.env.PRNEXT_CACHE_PROTOCOL === '2';
|
|
157
|
+
const body = binary ? cacheWire.encode(operation) : [JSON.stringify(operation.value === undefined ? operation : { ...operation, value: bufferValue(operation.value).toString('base64') })];
|
|
155
158
|
const controller = new AbortController();
|
|
156
159
|
const timer = setTimeout(() => controller.abort(new DOMException('The operation was aborted due to timeout', 'TimeoutError')), 5000);
|
|
157
160
|
timer.unref();
|
|
@@ -159,10 +162,10 @@ async function rpc(target, operation, signal) {
|
|
|
159
162
|
const operationSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
|
|
160
163
|
let retryMs = 10;
|
|
161
164
|
while (true) {
|
|
162
|
-
const response = local ? await localRpc(target, body, operationSignal) : await nativeFetch(target.url, {
|
|
165
|
+
const response = local ? await localRpc(target, body, operationSignal, binary) : await nativeFetch(target.url, {
|
|
163
166
|
method: 'POST',
|
|
164
167
|
headers: { 'content-type': 'application/json', authorization: `Bearer ${target.token}` },
|
|
165
|
-
body,
|
|
168
|
+
body: body[0],
|
|
166
169
|
signal: operationSignal,
|
|
167
170
|
});
|
|
168
171
|
// The deadline covers the JSON body as well as response headers. A
|
|
@@ -198,6 +201,7 @@ function bufferValue(value) {
|
|
|
198
201
|
return Buffer.isBuffer(value) ? value : Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
199
202
|
}
|
|
200
203
|
function decodeValue(value) {
|
|
204
|
+
if (Buffer.isBuffer(value)) { if(value.length > MAX_CACHE_VALUE_BYTES) throw new Error('Invalid PRNext data cache value'); return value; }
|
|
201
205
|
if (typeof value !== 'string' || value.length > Math.ceil(MAX_CACHE_VALUE_BYTES / 3) * 4) throw new Error('Invalid PRNext data cache value');
|
|
202
206
|
const bytes = Buffer.from(value, 'base64');
|
|
203
207
|
if (bytes.length > MAX_CACHE_VALUE_BYTES) throw new Error('Invalid PRNext data cache value');
|
|
@@ -247,7 +251,7 @@ async function compute(target, key, lease, producer, options) {
|
|
|
247
251
|
try {
|
|
248
252
|
const metadata = options.resolveMetadata?.();
|
|
249
253
|
if (metadata?.externalCache) { await release(target, key, lease); return; }
|
|
250
|
-
const result = await rpc(target, { op: 'commit', key, lease, value: bytes
|
|
254
|
+
const result = await rpc(target, { op: 'commit', key, lease, value: bytes, revalidate: options.revalidate,
|
|
251
255
|
...(metadata ? { tags: validateTags(metadata.tags), paths: metadata.paths || [], revalidate: validateRevalidate(metadata.revalidate, { allowZero: true }), expire: metadata.expire } : {}) }, signal);
|
|
252
256
|
if (!result.stored) await release(target, key, lease);
|
|
253
257
|
} catch {
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const WINDOW = 32, MAX_VALUE = 2 * 1024 * 1024, MAX_RESPONSE = 16 * 1024 * 1024;
|
|
4
|
+
const TYPE = 'application/x-prnext-flight-delta';
|
|
5
|
+
const MAGIC = new Uint8Array([80,82,68,49]);
|
|
6
|
+
const BLOCK_BYTES = 32 * 1024, MAX_COMMANDS_PER_CHUNK = 256, MAX_COMMANDS = 2048;
|
|
7
|
+
let POWER = 1; for(let n=1;n<WINDOW;n++) POWER = Math.imul(POWER,31) >>> 0;
|
|
8
|
+
function hash(bytes,offset) { let result=0; for(let n=0;n<WINDOW;n++) result=(Math.imul(result,31)+bytes[offset+n])>>>0; return result; }
|
|
9
|
+
function createIndex(base) {
|
|
10
|
+
const index=new Map();
|
|
11
|
+
for(let offset=0;offset+WINDOW<=base.length;offset+=WINDOW) {
|
|
12
|
+
const key=hash(base,offset), candidates=index.get(key)||[];
|
|
13
|
+
if(candidates.length<4)candidates.push(offset); index.set(key,candidates);
|
|
14
|
+
}
|
|
15
|
+
return index;
|
|
16
|
+
}
|
|
17
|
+
function frame(tag,a,b) { const value=new Uint8Array(tag===1?9:5), view=new DataView(value.buffer); value[0]=tag;view.setUint32(1,a);if(tag===1)view.setUint32(5,b);return value; }
|
|
18
|
+
function encodeChunk(bytes,base,index) {
|
|
19
|
+
const output=[];let cursor=0,literal=0,key=bytes.length>=WINDOW?hash(bytes,0):0,work=0,commands=0;
|
|
20
|
+
const budget=bytes.length*8+32768;
|
|
21
|
+
const emit=end=>{if(end>literal){commands++;output.push(frame(0,end-literal),bytes.subarray(literal,end));}};
|
|
22
|
+
while(cursor+WINDOW<=bytes.length) {
|
|
23
|
+
if(work>budget||commands>=MAX_COMMANDS_PER_CHUNK)return null;
|
|
24
|
+
let match=-1;
|
|
25
|
+
for(const offset of index.get(key)||[]) {let n=0;while(n<WINDOW&&bytes[cursor+n]===base[offset+n]){n++;work++;}if(n===WINDOW){match=offset;break;}}
|
|
26
|
+
if(match!==-1) {
|
|
27
|
+
emit(cursor);let length=WINDOW;
|
|
28
|
+
while(cursor+length<bytes.length&&match+length<base.length&&bytes[cursor+length]===base[match+length]){length++;work++;}
|
|
29
|
+
commands++;output.push(frame(1,match,length));cursor+=length;literal=cursor;
|
|
30
|
+
if(cursor+WINDOW<=bytes.length)key=hash(bytes,cursor);
|
|
31
|
+
} else {
|
|
32
|
+
key=(Math.imul((key-Math.imul(bytes[cursor],POWER))>>>0,31)+bytes[cursor+WINDOW])>>>0;cursor++;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
emit(bytes.length);if(commands>MAX_COMMANDS_PER_CHUNK)return null;output.commands=commands;return output;
|
|
36
|
+
}
|
|
37
|
+
function join(parts,size) { const result=new Uint8Array(size);let offset=0;for(const part of parts){result.set(part,offset);offset+=part.length;}return result; }
|
|
38
|
+
// Commands are a wire format, not scheduling units. Thousands of tiny writes
|
|
39
|
+
// defeat gzip and multiply worker IPC/socket work even when the raw delta saves
|
|
40
|
+
// bytes. Bound each outgoing block without reading ahead into a delayed chunk.
|
|
41
|
+
function blocks(parts) {
|
|
42
|
+
const output=[];let pending=[],size=0;
|
|
43
|
+
const flush=()=>{if(size)output.push(pending.length===1?pending[0]:join(pending,size));pending=[];size=0;};
|
|
44
|
+
for(const part of parts)for(let offset=0;offset<part.length;){const n=Math.min(BLOCK_BYTES-size,part.length-offset);pending.push(part.subarray(offset,offset+n));size+=n;offset+=n;if(size===BLOCK_BYTES)flush();}
|
|
45
|
+
flush();return output;
|
|
46
|
+
}
|
|
47
|
+
function collectStream(source,{onComplete,onCancel,onFinally,capture=true}={}) {
|
|
48
|
+
const reader=source.getReader(), parts=[];let size=0,complete=false,finished=false;
|
|
49
|
+
const finish=()=>{if(!finished){finished=true;onFinally?.();}};
|
|
50
|
+
return new ReadableStream({
|
|
51
|
+
async pull(controller) {try{const next=await reader.read();if(next.done){complete=true;if(capture&&size<=MAX_VALUE)onComplete?.(join(parts,size));parts.length=0;finish();controller.close();return;}if(!(next.value instanceof Uint8Array))throw new Error('Invalid Flight bytes');size+=next.value.length;if(size>MAX_RESPONSE)throw new Error('Flight exceeds transport budget');if(capture&&size<=MAX_VALUE)parts.push(next.value.slice());else parts.length=0;controller.enqueue(next.value);}catch(error){parts.length=0;finish();controller.error(error);await reader.cancel(error).catch(()=>{});}},
|
|
52
|
+
async cancel(reason){parts.length=0;finish();if(!complete)onCancel?.();await reader.cancel(reason);},
|
|
53
|
+
},{highWaterMark:0});
|
|
54
|
+
}
|
|
55
|
+
async function deltaStream(source,base,{assess}={}) {
|
|
56
|
+
const reader=source.getReader(), first=await reader.read();
|
|
57
|
+
if(first.done)return {delta:false,body:new ReadableStream({start(c){c.close();}})};
|
|
58
|
+
// A short first chunk always selects ordinary Flight. Do not build a base
|
|
59
|
+
// index that this response cannot use, or wait for a later streaming chunk.
|
|
60
|
+
let index=first.value.length>=WINDOW&&base instanceof Uint8Array&&base.length<=MAX_VALUE?createIndex(base):null;
|
|
61
|
+
const profitable=(raw,frames,initial)=>frames&&frames.reduce((n,b)=>n+b.length,0)+(initial?5:1)<raw.length*0.9&&(!assess||assess(raw,join(initial?[MAGIC,...frames,new Uint8Array([2])]:frames,frames.reduce((n,b)=>n+b.length,0)+(initial?5:0)))===true);
|
|
62
|
+
const firstFrames=index&&first.value.length>=WINDOW?encodeChunk(first.value,base,index):null;
|
|
63
|
+
let useDelta;
|
|
64
|
+
try{useDelta=!!profitable(first.value,firstFrames,true);}catch(error){await reader.cancel(error).catch(()=>{});throw error;}
|
|
65
|
+
let pending=useDelta?blocks([MAGIC,...firstFrames]):[first.value], pendingAt=0,ended=false,tail=false,commands=firstFrames?.commands||0;
|
|
66
|
+
if(!useDelta){base=undefined;index=undefined;}
|
|
67
|
+
return {delta:!!useDelta,body:new ReadableStream({
|
|
68
|
+
async pull(controller){try{
|
|
69
|
+
if(pendingAt===pending.length&&!ended){pending=[];pendingAt=0;const next=await reader.read();
|
|
70
|
+
if(next.done){ended=true;if(useDelta&&!tail)pending=[new Uint8Array([2])];}
|
|
71
|
+
else if(useDelta&&!tail){const encoded=encodeChunk(next.value,base,index);commands+=encoded?.commands||0;
|
|
72
|
+
if(commands>MAX_COMMANDS||!profitable(next.value,encoded,false)){tail=true;index=undefined;base=undefined;pending=blocks([new Uint8Array([3]),next.value]);}
|
|
73
|
+
else pending=blocks(encoded);
|
|
74
|
+
}else pending=[next.value];
|
|
75
|
+
}
|
|
76
|
+
if(pendingAt<pending.length)controller.enqueue(pending[pendingAt++]);else if(ended)controller.close();
|
|
77
|
+
}catch(error){pending=[];base=undefined;index=undefined;controller.error(error);await reader.cancel(error).catch(()=>{});}},
|
|
78
|
+
cancel(reason){pending=[];base=undefined;index=undefined;ended=true;return reader.cancel(reason);},
|
|
79
|
+
},{highWaterMark:0})};
|
|
80
|
+
}
|
|
81
|
+
function decodeStream(source,base) {
|
|
82
|
+
const reader=source.getReader();let chunks=[],head=0,available=0,offset=0,total=0,wireTotal=0,started=false,ended=false,tail=false,commands=0;
|
|
83
|
+
function consume(size){
|
|
84
|
+
const first=chunks[head];
|
|
85
|
+
if(first&&first.length-offset>=size){const result=first.subarray(offset,offset+size);offset+=size;available-=size;if(offset===first.length){head++;offset=0;}return result;}
|
|
86
|
+
const result=new Uint8Array(size);let written=0;
|
|
87
|
+
while(written<size){const value=chunks[head],count=Math.min(size-written,value.length-offset);result.set(value.subarray(offset,offset+count),written);written+=count;offset+=count;available-=count;if(offset===value.length){head++;offset=0;}}
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
async function take(size) {
|
|
91
|
+
if(size>MAX_RESPONSE)throw new Error('Invalid Flight delta frame size');
|
|
92
|
+
while(available<size){const next=await reader.read();if(next.done)throw new Error('Truncated Flight delta');if(!(next.value instanceof Uint8Array))throw new Error('Invalid Flight delta bytes');wireTotal+=next.value.length;if(wireTotal>MAX_RESPONSE+1024*1024)throw new Error('Flight delta wire budget exceeded');if(next.value.length){if(head){chunks=chunks.slice(head);head=0;}chunks.push(next.value);available+=next.value.length;}}
|
|
93
|
+
return consume(size);
|
|
94
|
+
}
|
|
95
|
+
function peek(at){let index=head,position=offset+at;while(index<chunks.length){if(position<chunks[index].length)return chunks[index][position];position-=chunks[index++].length;}}
|
|
96
|
+
function bufferedCommand(){
|
|
97
|
+
if(!available)return false;
|
|
98
|
+
const tag=peek(0);if(tag!==0&&tag!==1)return true;
|
|
99
|
+
if(available<(tag===1?9:5))return false;
|
|
100
|
+
const length=tag===1?0:((peek(1)*0x1000000)+(peek(2)<<16)+(peek(3)<<8)+peek(4));
|
|
101
|
+
return length>MAX_RESPONSE||available>=5+length;
|
|
102
|
+
}
|
|
103
|
+
async function pullTail(controller){
|
|
104
|
+
const next=available?{value:await take(available),done:false}:await reader.read();
|
|
105
|
+
if(next.done){ended=true;controller.close();return;}
|
|
106
|
+
if(!(next.value instanceof Uint8Array))throw new Error('Invalid Flight delta bytes');
|
|
107
|
+
total+=next.value.length;if(total>MAX_RESPONSE)throw new Error('Decoded Flight exceeds transport budget');controller.enqueue(next.value);
|
|
108
|
+
}
|
|
109
|
+
return new ReadableStream({
|
|
110
|
+
async pull(controller){try{
|
|
111
|
+
if(tail)return await pullTail(controller);
|
|
112
|
+
if(!started){const magic=await take(4);if(!magic.every((value,n)=>value===MAGIC[n]))throw new Error('Unsupported Flight delta version');started=true;}
|
|
113
|
+
if(ended)return controller.close();
|
|
114
|
+
const output=[];let size=0;
|
|
115
|
+
do {
|
|
116
|
+
// Publish decoded data before waiting for another source chunk or the
|
|
117
|
+
// final EOF. This preserves Suspense flushes and downstream backpressure.
|
|
118
|
+
if(output.length&&(!bufferedCommand()||peek(0)===2||peek(0)===3))break;
|
|
119
|
+
if(++commands>65536)throw new Error('Flight delta command budget exceeded');
|
|
120
|
+
const [tag]=await take(1);let value;
|
|
121
|
+
if(tag===3){tail=true;return await pullTail(controller);}
|
|
122
|
+
if(tag===2){ended=true;if(available)throw new Error('Trailing Flight delta data');const rest=await reader.read();if(!rest.done)throw new Error('Trailing Flight delta data');controller.close();return;}
|
|
123
|
+
if(tag===0){const header=await take(4),length=new DataView(header.buffer,header.byteOffset,header.byteLength).getUint32(0);if(!length)throw new Error('Empty Flight delta literal');value=await take(length);}
|
|
124
|
+
else if(tag===1){const header=await take(8),view=new DataView(header.buffer,header.byteOffset,header.byteLength),start=view.getUint32(0),length=view.getUint32(4);if(!length||start+length>base.length)throw new Error('Invalid Flight delta reference');value=base.subarray(start,start+length);}
|
|
125
|
+
else throw new Error('Invalid Flight delta command');
|
|
126
|
+
total+=value.length;if(total>MAX_RESPONSE)throw new Error('Decoded Flight exceeds transport budget');output.push(value);size+=value.length;
|
|
127
|
+
}while(size<BLOCK_BYTES);
|
|
128
|
+
controller.enqueue(output.length===1?output[0]:join(output,size));
|
|
129
|
+
}catch(error){chunks=[];controller.error(error);await reader.cancel(error).catch(()=>{});}},
|
|
130
|
+
cancel:reason=>{chunks=[];return reader.cancel(reason);},
|
|
131
|
+
},{highWaterMark:0});
|
|
132
|
+
}
|
|
133
|
+
function createFlightTransport() {
|
|
134
|
+
let current,serial=0;
|
|
135
|
+
return {
|
|
136
|
+
snapshot(){return current;},
|
|
137
|
+
async response(response,base) {
|
|
138
|
+
const version=++serial;
|
|
139
|
+
const token=response.headers.get('x-prnext-flight-token'), headers=new Headers(response.headers);
|
|
140
|
+
let source=response.body;
|
|
141
|
+
if(!source)return response;
|
|
142
|
+
if(headers.get('content-type')?.startsWith(TYPE)) {
|
|
143
|
+
if(!base||headers.get('x-prnext-flight-base')!==base.token){await source.cancel();throw new Error('Missing Flight delta base');}
|
|
144
|
+
source=decodeStream(source,base.bytes);
|
|
145
|
+
}
|
|
146
|
+
headers.set('content-type','text/x-component; charset=utf-8');headers.delete('content-length');headers.delete('content-encoding');
|
|
147
|
+
return new Response(collectStream(source,{capture:!!token&&/^[a-f0-9]{48}$/.test(token),onComplete:bytes=>{if(version===serial&&token&&/^[a-f0-9]{48}$/.test(token))current={token,bytes};}}),{status:response.status,statusText:response.statusText,headers});
|
|
148
|
+
},
|
|
149
|
+
clear(){serial++;current=undefined;},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
module.exports={TYPE,MAX_VALUE,createIndex,encodeChunk,deltaStream,decodeStream,collectStream,createFlightTransport};
|
package/compat/headers.cjs
CHANGED
|
@@ -63,12 +63,14 @@ function runRequestContext(input, callback) {
|
|
|
63
63
|
return requestStorage.run(context, callback);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
function assertAfterRequestData(name) { const context = currentRequest(); if (context.phase === 'after' && context.afterRestricted) throw new Error(`${name}() cannot be called inside after() registered by a Server Component; read it before registering the callback`); }
|
|
66
67
|
function assertUncached(name) {
|
|
67
68
|
if (inCacheScope()) throw new Error(`${name}() cannot be accessed inside unstable_cache; read it outside and pass the required values as arguments`);
|
|
68
69
|
}
|
|
69
|
-
async function headers() { if (currentRequest().instantValidation && currentRequest().instantSample) { assertUncached('headers'); return sampleMethods(currentRequest(), 'headers', currentRequest().headers); } if (currentCacheScope()?.kind !== 'private') assertUncached('headers'); dynamicUsage('headers()'); return currentRequest().headers; }
|
|
70
|
-
async function cookies() { if (currentRequest().instantValidation && currentRequest().instantSample) { assertUncached('cookies'); return sampleMethods(currentRequest(), 'cookies', currentRequest().cookies); } if (currentCacheScope()?.kind !== 'private') assertUncached('cookies'); dynamicUsage('cookies()'); return currentRequest().cookies; }
|
|
70
|
+
async function headers() { assertAfterRequestData('headers'); if (currentRequest().instantValidation && currentRequest().instantSample) { assertUncached('headers'); return sampleMethods(currentRequest(), 'headers', currentRequest().headers); } if (currentCacheScope()?.kind !== 'private') assertUncached('headers'); dynamicUsage('headers()'); return currentRequest().headers; }
|
|
71
|
+
async function cookies() { assertAfterRequestData('cookies'); if (currentRequest().instantValidation && currentRequest().instantSample) { assertUncached('cookies'); return sampleMethods(currentRequest(), 'cookies', currentRequest().cookies); } if (currentCacheScope()?.kind !== 'private') assertUncached('cookies'); dynamicUsage('cookies()'); return currentRequest().cookies; }
|
|
71
72
|
async function draftMode() {
|
|
73
|
+
assertAfterRequestData('draftMode');
|
|
72
74
|
const context = currentRequest();
|
|
73
75
|
if (!context.draftProvider) context.draftProvider = {
|
|
74
76
|
get isEnabled() { return context.draftMode; },
|
|
@@ -78,6 +78,7 @@ async function invalidate(context, operation) {
|
|
|
78
78
|
await invoke(handler, 'revalidateTag', [[...new Set(tags)], durations], context);
|
|
79
79
|
}
|
|
80
80
|
function fresh(entry, revalidate) {
|
|
81
|
+
if (entry?.isStale === true) return false;
|
|
81
82
|
const timestamp = entry?.lastModified;
|
|
82
83
|
if (!Number.isFinite(timestamp) || timestamp < 0 || timestamp > Date.now() + 60_000) return false;
|
|
83
84
|
const stored = entry.value?.revalidate;
|
|
@@ -124,14 +125,18 @@ async function cachedIncrementalValue(key, producer, options) {
|
|
|
124
125
|
let complete;
|
|
125
126
|
const owner = { revision, done: new Promise(resolve => { complete = resolve; }) };
|
|
126
127
|
if (ownersCount < 64) { record.owners.set(key, owner); ownersCount++; }
|
|
127
|
-
let lease;
|
|
128
|
+
let lease, sharedLease;
|
|
128
129
|
const cleanup = async () => {
|
|
129
130
|
if (owner.finished) return;
|
|
130
131
|
owner.finished = true;
|
|
131
|
-
try {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
try {
|
|
133
|
+
if (sharedLease && handler.prnextLeaseVersion === 1 && typeof handler.release === 'function') await invoke(handler, 'release', [key, sharedLease], { ...context, signal: undefined });
|
|
134
|
+
} finally {
|
|
135
|
+
try { await lease?.release(); } finally {
|
|
136
|
+
if (record.owners.get(key) === owner) record.owners.delete(key);
|
|
137
|
+
if (owner.tracked) ownersCount--;
|
|
138
|
+
complete();
|
|
139
|
+
}
|
|
135
140
|
}
|
|
136
141
|
};
|
|
137
142
|
owner.tracked = record.owners.get(key) === owner;
|
|
@@ -140,7 +145,7 @@ async function cachedIncrementalValue(key, producer, options) {
|
|
|
140
145
|
if (!(value instanceof Uint8Array) || value.byteLength > MAX_BYTES) return;
|
|
141
146
|
if (lease && !await lease.valid()) return;
|
|
142
147
|
const data = incremental.encode(Buffer.from(value));
|
|
143
|
-
const setContext = { ...getContext, tags: [...new Set([...tags, ...implicit])], cacheControl: { revalidate } };
|
|
148
|
+
const setContext = { ...getContext, tags: [...new Set([...tags, ...implicit])], cacheControl: { revalidate }, prnextLease: sharedLease };
|
|
144
149
|
try {
|
|
145
150
|
await invoke(handler, 'set', [key, { kind: 'FETCH', data, revalidate: revalidate === false ? 31536000 : revalidate }, setContext], context);
|
|
146
151
|
// A mutation in any local worker can revoke the native lease while the
|
|
@@ -152,7 +157,15 @@ async function cachedIncrementalValue(key, producer, options) {
|
|
|
152
157
|
const work = (async () => {
|
|
153
158
|
try {
|
|
154
159
|
lease = await cache.acquireExternalCacheLease(key, signal);
|
|
155
|
-
if (
|
|
160
|
+
if (handler.prnextLeaseVersion === 1 && typeof handler.acquire === 'function') {
|
|
161
|
+
const until = Date.now() + 25000;
|
|
162
|
+
for (;;) {
|
|
163
|
+
signal?.throwIfAborted();
|
|
164
|
+
try { sharedLease = await invoke(handler, 'acquire', [key, { signal }], context); break; }
|
|
165
|
+
catch (error) { if (error.code !== 'PRNEXT_REDIS_BUSY' || Date.now() >= until) throw error; }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (lease || sharedLease) {
|
|
156
169
|
// Even an immediately granted lease may follow a producer that finished
|
|
157
170
|
// between our first backend read and this native admission.
|
|
158
171
|
let shared;
|