@thomas.f/prnext 0.1.17-alpha → 0.1.18-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 +26 -869
- package/build/actions.mjs +1 -1
- 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 +81 -26
- 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 +31 -8
- package/build/config.mjs +30 -1
- 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-update.mjs +4 -2
- package/build/dev-dependencies.mjs +1 -1
- package/build/dev-discovery.mjs +3 -3
- package/build/dev.mjs +15 -8
- package/build/dynamic.mjs +1 -1
- 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/index.mjs +59 -26
- package/build/instrumentation.mjs +30 -0
- package/build/metadata-routes.mjs +1 -1
- package/build/migration-audit.mjs +1 -1
- package/build/native-type-config.mjs +37 -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 +58 -9
- package/build/react-compiler-report.mjs +225 -0
- package/build/react-compiler.mjs +139 -0
- package/build/runtime-contract.mjs +2 -2
- 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 +6 -3
- package/build/worker-memory.mjs +9 -0
- package/cli.mjs +60 -4
- package/compat/activity-cache.cjs +26 -0
- package/compat/after.cjs +6 -4
- package/compat/app-layout-context.cjs +52 -2
- package/compat/base64.cjs +10 -0
- package/compat/cache-handlers.cjs +12 -2
- 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 +150 -0
- package/compat/headers.cjs +4 -2
- package/compat/incremental-cache.cjs +20 -7
- package/compat/index.d.ts +68 -3
- package/compat/link.cjs +12 -9
- 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 +171 -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 +82 -0
- package/docs/ai/troubleshooting.md +37 -2
- package/package.json +26 -7
- package/runtime/action-client.mjs +10 -4
- package/runtime/action-server.mjs +2 -0
- package/runtime/app-client.mjs +212 -65
- 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/cache-components-rsc.mjs +4 -1
- package/runtime/cache-request.mjs +2 -1
- package/runtime/deployment-client.mjs +19 -0
- package/runtime/dev-client.mjs +7 -2
- package/runtime/dev-compiler-broker.mjs +42 -0
- package/runtime/dev-indicator.mjs +8 -3
- package/runtime/dev-lazy-tool.mjs +16 -0
- package/runtime/development.mjs +29 -12
- package/runtime/document.mjs +1 -1
- package/runtime/edge.mjs +6 -5
- package/runtime/fetch-cache.mjs +10 -8
- package/runtime/flight-delta.mjs +50 -0
- package/runtime/flight-tap.mjs +28 -0
- 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/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
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {readFile,stat} from 'node:fs/promises';
|
|
2
|
+
import {createRequire} from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
// Select from the application's installation, never from the framework's own
|
|
6
|
+
// development dependencies. Native TypeScript deliberately lacks the old API.
|
|
7
|
+
export async function resolveTypeScript(root) {
|
|
8
|
+
const require=createRequire(path.join(root,'package.json'));
|
|
9
|
+
const manifest=require.resolve('typescript/package.json');
|
|
10
|
+
const metadata=JSON.parse(await readFile(manifest,'utf8'));
|
|
11
|
+
const major=Number(/^([0-9]+)\./.exec(metadata.version)?.[1]);
|
|
12
|
+
if(!Number.isInteger(major))throw new Error('The installed TypeScript package has no valid version.');
|
|
13
|
+
if(major<7)return {native:false,compiler:require.resolve('typescript'),version:metadata.version};
|
|
14
|
+
const entry=typeof metadata.bin==='string'?metadata.bin:metadata.bin?.tsc;
|
|
15
|
+
if(typeof entry!=='string'||!entry)throw new Error('The installed TypeScript package has no tsc command. Reinstall TypeScript with its optional native dependencies.');
|
|
16
|
+
const directory=path.dirname(manifest),bin=path.resolve(directory,entry);
|
|
17
|
+
if(!bin.startsWith(directory+path.sep)||!(await stat(bin)).isFile())throw new Error('The installed TypeScript tsc command is invalid.');
|
|
18
|
+
return {native:true,compiler:bin,version:metadata.version};
|
|
19
|
+
}
|
package/build/typescript.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { stat } from 'node:fs/promises';
|
|
2
|
-
import { createRequire } from 'node:module';
|
|
3
2
|
import { execFile } from 'node:child_process';
|
|
4
3
|
import { promisify } from 'node:util';
|
|
5
4
|
import { fileURLToPath } from 'node:url';
|
|
6
5
|
import path from 'node:path';
|
|
6
|
+
import {resolveTypeScript} from './typescript-toolchain.mjs';
|
|
7
|
+
import {checkNativeTypeScript} from './typescript-native.mjs';
|
|
7
8
|
|
|
8
9
|
const execute = promisify(execFile);
|
|
9
10
|
|
|
@@ -37,11 +38,12 @@ export async function checkTypeScript(root, config, { dev = false, tsconfig, for
|
|
|
37
38
|
if (!force && (dev || !config.typescript || config.typescript.ignoreBuildErrors)) return;
|
|
38
39
|
tsconfig ??= await projectTsconfig(root, config);
|
|
39
40
|
if (!tsconfig) throw new Error('TypeScript checking was explicitly enabled, but tsconfig.json does not exist. Create a TypeScript configuration, set typescript.tsconfigPath to an existing configuration, or explicitly set typescript.ignoreBuildErrors: true if types are checked separately.');
|
|
40
|
-
let
|
|
41
|
-
try {
|
|
41
|
+
let toolchain;
|
|
42
|
+
try { toolchain = await resolveTypeScript(root); }
|
|
42
43
|
catch (error) { throw new Error('TypeScript checking requires typescript in the project. Install it as a dev dependency, or explicitly set typescript.ignoreBuildErrors: true if a separate process checks types.', { cause: error }); }
|
|
43
44
|
try {
|
|
44
|
-
|
|
45
|
+
if(toolchain.native){await checkNativeTypeScript(toolchain.compiler,tsconfig,root);return;}
|
|
46
|
+
await execute(process.execPath, [fileURLToPath(new URL('./typescript-check.mjs', import.meta.url)), toolchain.compiler, tsconfig], {
|
|
45
47
|
cwd: root, windowsHide: true, maxBuffer: 4 * 1024 * 1024,
|
|
46
48
|
});
|
|
47
49
|
} catch (error) {
|
package/build/webpack-graph.mjs
CHANGED
|
@@ -233,7 +233,7 @@ async function createGraphSession(root,{webpack,reusable,stage}) {
|
|
|
233
233
|
cssChunkFilename:'style-[contenthash:16].css',module:true,library:{type:'module'},chunkFormat:'module',chunkLoading:'import',
|
|
234
234
|
publicPath:target==='node'?'./':state.assetBase+'/',environment:{module:true,dynamicImport:true},compareBeforeEmit:false,clean:false},
|
|
235
235
|
performance:false,
|
|
236
|
-
optimization:{minimize:!!options.minify,minimizer:[{apply(compiler){
|
|
236
|
+
optimization:{minimize:!!options.minify,mangleExports:options.minify?'size':false,minimizer:[{apply(compiler){
|
|
237
237
|
compiler.hooks.compilation.tap('PRNextMinify',compilation=>{
|
|
238
238
|
compilation.hooks.processAssets.tapPromise({name:'PRNextMinify',stage:webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE},async()=>{
|
|
239
239
|
// Sequential native esbuild transforms avoid a Terser worker pool.
|
|
@@ -247,7 +247,7 @@ async function createGraphSession(root,{webpack,reusable,stage}) {
|
|
|
247
247
|
});
|
|
248
248
|
});
|
|
249
249
|
}}],runtimeChunk:options.splitting?{name:()=>runtime}:false,
|
|
250
|
-
splitChunks:options.splitting?{chunks:'all',minSize:0,cacheGroups:{default:false,defaultVendors:false,shared:{minChunks:2,test:module=>!module.type.startsWith('css'),name:false}}}:false},
|
|
250
|
+
splitChunks:options.splitting?{chunks:'all',minSize:0,cacheGroups:{default:false,defaultVendors:false,shared:{minChunks:2,reuseExistingChunk:true,test:module=>!module.type.startsWith('css'),name:false}}}:false},
|
|
251
251
|
resolve:{alias:config.resolution.resolveAlias || {},conditionNames:[...(options.conditions || []),'...'],extensions:config.resolution.resolveExtensions || ['.tsx','.ts','.jsx','.js','.mjs','.json'],fullySpecified:false},
|
|
252
252
|
module:{unsafeCache:false,rules:[{test:/./,type:'javascript/auto',enforce:'post',use:[{loader:loaderPath,options:{bridge:bridgeId}}]},
|
|
253
253
|
{test:/\.json$/,type:'json'},{test:/\.(?:css|scss|sass)$/,type:'css'},
|
|
@@ -379,7 +379,10 @@ async function createGraphSession(root,{webpack,reusable,stage}) {
|
|
|
379
379
|
for(const name of previousOutputs)if(!currentOutputs.has(name))await discardPrivateFile(path.join(actualOut,name),actualOut);
|
|
380
380
|
previousOutputs.clear();for(const name of currentOutputs)previousOutputs.add(name);
|
|
381
381
|
}
|
|
382
|
-
|
|
382
|
+
// This compatibility metafile does not yet encode webpack's runtime and
|
|
383
|
+
// async chunk-group edges. Consumers must not mistake empty import lists
|
|
384
|
+
// for a complete initial browser graph.
|
|
385
|
+
return {errors:[],warnings:[],metafile:{inputs,outputs,initialGraphComplete:false},...(options.write===false?{outputFiles}:{})};
|
|
383
386
|
} finally {
|
|
384
387
|
active.delete(bridgeId);
|
|
385
388
|
for(const file of virtualFiles)if(!usedVirtualFiles.has(file)){virtualFiles.delete(file);await discardPrivateFile(file,home);}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function validateWorkerMemory(value) {
|
|
2
|
+
if (value === undefined || value === false) return undefined;
|
|
3
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) ||
|
|
4
|
+
Object.keys(value).some(key => key !== 'maxRssMb') ||
|
|
5
|
+
!Number.isSafeInteger(value.maxRssMb) || value.maxRssMb < 64 || value.maxRssMb > 65536) {
|
|
6
|
+
throw new TypeError('workerMemory must be false or { maxRssMb: 64..65536 }');
|
|
7
|
+
}
|
|
8
|
+
return { maxRssMb: value.maxRssMb };
|
|
9
|
+
}
|
package/cli.mjs
CHANGED
|
@@ -11,6 +11,9 @@ const help = `PRNext ${packageManifest.version}
|
|
|
11
11
|
|
|
12
12
|
Use prnext for portable scripts; prn is a shortcut on compatible shells.
|
|
13
13
|
|
|
14
|
+
prnext create <directory> Create a project and install dependencies
|
|
15
|
+
prnext create <directory> --no-install Prepare project files without installing
|
|
16
|
+
prnext create --help TypeScript, JavaScript and creation options
|
|
14
17
|
prnext build [directory] Build React/TypeScript and static HTML
|
|
15
18
|
prnext build [directory] --watch Incremental production rebuilds on changes
|
|
16
19
|
prnext build [directory] --adapter static [--from dist] [--spa]
|
|
@@ -40,6 +43,7 @@ Use prnext for portable scripts; prn is a shortcut on compatible shells.
|
|
|
40
43
|
prnext typecheck [directory] [--json] Generate types and check TypeScript without building
|
|
41
44
|
prnext inspect [directory] [--json|--otlp] Explain routes, health and sampled timings
|
|
42
45
|
prnext analyze [directory] [--json] [--max-js 150] Browser bundles and budget (kB)
|
|
46
|
+
prnext compiler [directory] [--json] React Compiler results from the last successful build
|
|
43
47
|
prnext host [prnext.host.json] Host built apps by hostname; wake on demand
|
|
44
48
|
prnext host [config] --check Validate hosting configuration and builds
|
|
45
49
|
prnext host [config] --status [--json] Show memory, process and idle status
|
|
@@ -69,10 +73,12 @@ Analysis options: --metric raw|gzip|brotli (default raw), --baseline analysis.js
|
|
|
69
73
|
--max-increase-bytes 1024 --max-increase-percent 5
|
|
70
74
|
--budgets budgets.json Exact route keys with maxJs (kB),
|
|
71
75
|
maxIncreaseBytes and/or maxIncreasePercent
|
|
72
|
-
Analysis exit codes: 0 passed, 1 budget exceeded
|
|
76
|
+
Analysis exit codes: 0 passed, 1 budget exceeded or initial total unverifiable,
|
|
77
|
+
2 invalid input or missing build. Request-dependent totals are lower bounds.
|
|
73
78
|
|
|
74
79
|
Server options: --port 3000 --hostname 127.0.0.1 --workers 1 --inspect
|
|
75
80
|
Production profiles: --profile balanced|speed|memory|classic
|
|
81
|
+
Render readiness: --prewarm --readiness-file /private/path/ready.json
|
|
76
82
|
Worker retirement: --worker-idle-seconds 300 --lane-idle-seconds 30 --keep-warm [true|false]
|
|
77
83
|
Memory favors lower RAM under concurrency, accepting more waiting and CPU per response.
|
|
78
84
|
Without an option: PRNEXT_PROFILE, legacy PRNEXT_MEMORY_PROFILE=compact, then balanced.
|
|
@@ -101,6 +107,30 @@ function stop(child) {
|
|
|
101
107
|
async function main() {
|
|
102
108
|
if (['help', '--help', '-h'].includes(command)) { console.log(help); return; }
|
|
103
109
|
if (['--version', '-v'].includes(command)) { console.log(packageManifest.version); return; }
|
|
110
|
+
if (command === 'create') {
|
|
111
|
+
const { createHelp, parseCreateArgs, createProject } = await import('./build/create.mjs');
|
|
112
|
+
if (argv.length === 1 && ['--help', '-h'].includes(argv[0])) { console.log(createHelp); return; }
|
|
113
|
+
const { directory, options } = parseCreateArgs(argv);
|
|
114
|
+
const controller = new AbortController();
|
|
115
|
+
let interrupted;
|
|
116
|
+
const interrupt = signal => { interrupted = signal; controller.abort(); };
|
|
117
|
+
const onInt = () => interrupt('SIGINT'), onTerm = () => interrupt('SIGTERM');
|
|
118
|
+
process.on('SIGINT', onInt); process.on('SIGTERM', onTerm);
|
|
119
|
+
try {
|
|
120
|
+
const report = await createProject(directory, { ...options, signal: controller.signal,
|
|
121
|
+
onOutput: options.json ? () => {} : (chunk, stream) => process[stream].write(chunk) });
|
|
122
|
+
if (options.json) console.log(JSON.stringify(report, null, 2));
|
|
123
|
+
else {
|
|
124
|
+
console.log(`PRNext project ${report.status}: ${report.root}`);
|
|
125
|
+
if (report.error) console.error(report.error);
|
|
126
|
+
if (!report.ok) console.log('Project files were preserved. Retry installation from this directory.');
|
|
127
|
+
if (path.resolve(process.cwd()) !== directory) console.log(`\nOpen ${report.root} in your terminal, then run:`);
|
|
128
|
+
for (const next of report.next) console.log(` ${next}`);
|
|
129
|
+
}
|
|
130
|
+
if (!report.ok) process.exitCode = interrupted === 'SIGINT' ? 130 : interrupted === 'SIGTERM' ? 143 : 1;
|
|
131
|
+
} finally { process.removeListener('SIGINT', onInt); process.removeListener('SIGTERM', onTerm); }
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
104
134
|
if (command === 'docs') {
|
|
105
135
|
const { executeAiDocs } = await import('./runtime/ai-docs.mjs');
|
|
106
136
|
const result = await executeAiDocs(argv);
|
|
@@ -185,10 +215,28 @@ async function main() {
|
|
|
185
215
|
if (!report.ok) process.exitCode = 1;
|
|
186
216
|
return;
|
|
187
217
|
}
|
|
188
|
-
if (!['build', 'start', 'dev', 'routes', 'check', 'typegen', 'typecheck', 'inspect', 'analyze', 'output'].includes(command)) throw new Error(`Unknown command: ${command}\n${help}`);
|
|
218
|
+
if (!['build', 'start', 'dev', 'routes', 'check', 'typegen', 'typecheck', 'inspect', 'analyze', 'compiler', 'output'].includes(command)) throw new Error(`Unknown command: ${command}\n${help}`);
|
|
189
219
|
const args = [...argv];
|
|
190
220
|
const root = path.resolve(args[0] && !args[0].startsWith('-') ? args.shift() : '.');
|
|
191
|
-
if (['check','typegen','typecheck','analyze'].includes(command) && args.some(arg => ['--help','-h'].includes(arg))) { console.log(help); return; }
|
|
221
|
+
if (['check','typegen','typecheck','analyze','compiler'].includes(command) && args.some(arg => ['--help','-h'].includes(arg))) { console.log(help); return; }
|
|
222
|
+
if(command==='compiler'){
|
|
223
|
+
try {
|
|
224
|
+
if(args.some(arg=>arg!=='--json') || args.filter(arg=>arg==='--json').length>1)throw new Error('compiler accepts only [directory] and --json');
|
|
225
|
+
const {readReactCompilerReport,printReactCompilerReport}=await import('./build/react-compiler-report.mjs');
|
|
226
|
+
const {readBuildOutput}=await import('./build/output-contract.mjs');
|
|
227
|
+
const directory=path.join(root,await readBuildDirectory(root));
|
|
228
|
+
const output=await readBuildOutput(directory);
|
|
229
|
+
if(output.adapter!=='react')throw new Error('React Compiler reports apply to React builds. This application uses the static adapter.');
|
|
230
|
+
const report=await readReactCompilerReport(directory,{buildId:output.buildId});
|
|
231
|
+
if(!report)throw new Error('No React Compiler report is available. Rebuild this application with the installed PRNext version.');
|
|
232
|
+
if(args.includes('--json'))console.log(JSON.stringify(report,null,2));else printReactCompilerReport(report,{details:true});
|
|
233
|
+
} catch(error) {
|
|
234
|
+
if(args.includes('--json'))console.log(JSON.stringify({ok:false,error:error.message}));
|
|
235
|
+
else console.error(`PRNext compiler: ${error.message}`);
|
|
236
|
+
process.exitCode=2;
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
192
240
|
if(command==='analyze'){
|
|
193
241
|
try {
|
|
194
242
|
const {analyzeBuild,printAnalysis,parseAnalysisArgs}=await import('./build/analyze.mjs');
|
|
@@ -307,7 +355,15 @@ async function main() {
|
|
|
307
355
|
await buildStatic(root, options);
|
|
308
356
|
} else {
|
|
309
357
|
const { build } = await import('./build/index.mjs');
|
|
310
|
-
await build(root, { ...options, dev, envMode });
|
|
358
|
+
const result=await build(root, { ...options, dev, envMode });
|
|
359
|
+
if(!dev&&result.compilationCache?.hits){
|
|
360
|
+
const {hits,reusedBytes}=result.compilationCache;
|
|
361
|
+
console.log(`Compilation cache: ${hits} ${hits===1?'graph':'graphs'} reused (${Math.ceil(reusedBytes/1024)} KiB).`);
|
|
362
|
+
}
|
|
363
|
+
if(result.reactCompilerReport?.enabled){
|
|
364
|
+
const {printReactCompilerReport}=await import('./build/react-compiler-report.mjs');
|
|
365
|
+
printReactCompilerReport(result.reactCompilerReport,{details:false});
|
|
366
|
+
}
|
|
311
367
|
}
|
|
312
368
|
console.log(`PRNext built ${root} in ${((performance.now() - started) / 1000).toFixed(2)}s`);
|
|
313
369
|
return;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// A shared budget across layout slots, rather than a separate page budget at
|
|
4
|
+
// every nesting level. Only inactive branches enter this registry.
|
|
5
|
+
function createActivityCache({ limit = 4, ttl = 120000, now = Date.now, schedule = setTimeout, cancel = clearTimeout } = {}) {
|
|
6
|
+
const entries = new Map(); let timer;
|
|
7
|
+
function arm() {
|
|
8
|
+
cancel(timer); timer = undefined;
|
|
9
|
+
if (entries.size) {
|
|
10
|
+
timer = schedule(prune, Math.max(1, Math.min(...[...entries.values()].map(entry => entry.expires)) - now()));
|
|
11
|
+
timer?.unref?.();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function prune() {
|
|
15
|
+
for (const [key, entry] of entries) if (entry.expires <= now()) { entries.delete(key); entry.evict(); }
|
|
16
|
+
while (entries.size > limit) { const [key, entry] = entries.entries().next().value; entries.delete(key); entry.evict(); }
|
|
17
|
+
arm();
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
retain(key, evict) { if (!entries.has(key)) entries.set(key, { evict, expires: now() + ttl }); prune(); },
|
|
21
|
+
release(key) { entries.delete(key); arm(); },
|
|
22
|
+
clear() { const previous = [...entries.values()]; entries.clear(); cancel(timer); timer = undefined; for (const entry of previous) entry.evict(); },
|
|
23
|
+
get size() { return entries.size; },
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
module.exports = { createActivityCache };
|
package/compat/after.cjs
CHANGED
|
@@ -4,11 +4,11 @@ const { AsyncLocalStorage } = require('node:async_hooks');
|
|
|
4
4
|
function after(task) {
|
|
5
5
|
const context = require('./headers.cjs').currentRequest();
|
|
6
6
|
if (require('./data-cache.cjs').inCacheScope()) throw new Error('after() cannot be registered inside a cache scope');
|
|
7
|
-
if (!context.afterScope || !['route', 'action', 'after'].includes(context.phase)) throw new Error('after()
|
|
7
|
+
if (!context.afterScope || !['route', 'action', 'render', 'after'].includes(context.phase)) throw new Error('after() requires a Node Server Component, Route Handler or Server Action. Edge after() is not implemented.');
|
|
8
8
|
context.afterScope.add(task, context, AsyncLocalStorage.snapshot());
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
function createAfterScope({ signal, timeoutMs = 25000, onTimeout = () => {}, onComplete, report = error => console.error('[prnext] after() failed:', error?.stack || error) } = {}) {
|
|
11
|
+
function createAfterScope({ signal, timeoutMs = 25000, onTimeout = () => {}, onComplete, onRegistered, report = error => console.error('[prnext] after() failed:', error?.stack || error) } = {}) {
|
|
12
12
|
const queue = []; let count = 0, closed = false, finishing;
|
|
13
13
|
const deadline = Date.now() + timeoutMs;
|
|
14
14
|
const track = promise => signal?.[Symbol.for('prnext.request-work')]?.(promise, { timeoutMs: Math.max(1, deadline - Date.now()) });
|
|
@@ -18,6 +18,7 @@ function createAfterScope({ signal, timeoutMs = 25000, onTimeout = () => {}, onC
|
|
|
18
18
|
if (closed) throw new Error('after() request has already completed');
|
|
19
19
|
if (typeof task !== 'function' && (!task || typeof task.then !== 'function')) throw new TypeError('after() expects a callback or a Promise');
|
|
20
20
|
if (++count > 128) throw new Error('after() exceeds 128 tasks per request');
|
|
21
|
+
if (count === 1) onRegistered?.();
|
|
21
22
|
const pending = typeof task === 'function' ? null : observe(task);
|
|
22
23
|
if (pending) track(pending);
|
|
23
24
|
queue.push({ task: pending ? null : task, pending, context, run });
|
|
@@ -27,8 +28,9 @@ function createAfterScope({ signal, timeoutMs = 25000, onTimeout = () => {}, onC
|
|
|
27
28
|
try {
|
|
28
29
|
while (queue.length) {
|
|
29
30
|
const item = queue.shift(), remaining = deadline - Date.now();
|
|
30
|
-
if (remaining <= 0) { const error = new Error('after() exceeded the request maxDuration'); report(error); onTimeout(error); break; }
|
|
31
|
+
if (remaining <= 0) { const error = new Error('after() exceeded the request maxDuration'); await item.run(()=>report(error)); onTimeout(error); break; }
|
|
31
32
|
const abort = new AbortController();
|
|
33
|
+
item.context.afterRestricted ||= item.context.phase === 'render';
|
|
32
34
|
item.context.cookies._mutable = false;
|
|
33
35
|
item.context.phase = 'after';
|
|
34
36
|
item.context.signal = abort.signal;
|
|
@@ -38,7 +40,7 @@ function createAfterScope({ signal, timeoutMs = 25000, onTimeout = () => {}, onC
|
|
|
38
40
|
let timer;
|
|
39
41
|
try {
|
|
40
42
|
const outcome = await Promise.race([work, new Promise(resolve => { timer = setTimeout(() => { const error = new Error('after() exceeded the request maxDuration'); abort.abort(error); resolve({ error }); }, remaining); })]);
|
|
41
|
-
if ('error' in outcome) report(outcome.error);
|
|
43
|
+
if ('error' in outcome) await item.run(()=>report(outcome.error));
|
|
42
44
|
if (abort.signal.aborted) { onTimeout(outcome.error); break; }
|
|
43
45
|
} finally { clearTimeout(timer); item.context.cacheState.closed = true; await onComplete?.(item.context); }
|
|
44
46
|
}
|
|
@@ -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};
|
|
@@ -139,10 +139,19 @@ async function customCachedValue(key, kind, producer, options) {
|
|
|
139
139
|
const timestamp = Date.now();
|
|
140
140
|
const record = { revision, timestamp, work: undefined, metadata: undefined };
|
|
141
141
|
work = (async () => {
|
|
142
|
+
let lease,canStore=true;
|
|
143
|
+
try {
|
|
144
|
+
if(typeof handler.acquire==='function'){
|
|
145
|
+
try{
|
|
146
|
+
lease=await call(handler,'acquire',[key,{signal:context.signal}],context);
|
|
147
|
+
const shared=await call(handler,'get',[key,tags],context);
|
|
148
|
+
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(()=>{});}
|
|
149
|
+
}catch{context.signal?.throwIfAborted();canStore=false;}
|
|
150
|
+
}
|
|
142
151
|
const result = await producer();
|
|
143
152
|
const info = resolveMetadata();
|
|
144
153
|
record.metadata = info;
|
|
145
|
-
if (result.byteLength <= MAX_BYTES && revision === (revisions.get(handler) || 0)) {
|
|
154
|
+
if (canStore && result.byteLength <= MAX_BYTES && revision === (revisions.get(handler) || 0)) {
|
|
146
155
|
try {
|
|
147
156
|
const changed = await expiration(handler, [...new Set([...tags, ...info.tags])], context);
|
|
148
157
|
// Many backends record Date.now() millisecond timestamps. Treat the
|
|
@@ -150,12 +159,13 @@ async function customCachedValue(key, kind, producer, options) {
|
|
|
150
159
|
// hide an invalidation which actually happened after the read started.
|
|
151
160
|
if ((changed === Infinity || changed < Math.floor(timestamp)) && revision === (revisions.get(handler) || 0)) {
|
|
152
161
|
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); }
|
|
162
|
+
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
163
|
catch { if (!value.locked) void value.cancel().catch(() => {}); }
|
|
155
164
|
}
|
|
156
165
|
} catch { context.signal?.throwIfAborted(); }
|
|
157
166
|
}
|
|
158
167
|
return result;
|
|
168
|
+
}finally{try{await lease?.release();}catch{/* A failed cache cleanup does not replay application work. */}}
|
|
159
169
|
})();
|
|
160
170
|
record.work = work;
|
|
161
171
|
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 {
|