@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
|
@@ -4,7 +4,10 @@ import { rewriteQuery } from '../compat/rewrite.cjs';
|
|
|
4
4
|
import { normalizeBasePath, removeBasePath } from '../compat/paths.cjs';
|
|
5
5
|
import { publicNavigationURL, isApplicationURL } from './client-navigation.mjs';
|
|
6
6
|
import DefaultError from '../compat/error.cjs';
|
|
7
|
+
import { createPrefetchScheduler, prefetchCacheVictim } from './prefetch-scheduler.mjs';
|
|
7
8
|
import { applicationProps, createInitialPropsRunner } from './pages-initial-props.mjs';
|
|
9
|
+
import { matchPagePattern } from './route-pattern.mjs';
|
|
10
|
+
export { matchPagePattern } from './route-pattern.mjs';
|
|
8
11
|
|
|
9
12
|
const PREFETCH_LIMIT = 8;
|
|
10
13
|
const PREFETCH_TTL = 30_000;
|
|
@@ -51,33 +54,6 @@ export function pageRouterSnapshot(router, requestURL, visibleURL, rewrite) {
|
|
|
51
54
|
...(rewrite ? { rewrite } : {}) };
|
|
52
55
|
}
|
|
53
56
|
|
|
54
|
-
export function matchPagePattern(pattern, pathname) {
|
|
55
|
-
let segments;
|
|
56
|
-
try { segments = pathname.replace(/\/+$/, '').split('/').slice(1).map(decodeURIComponent); }
|
|
57
|
-
catch { return null; }
|
|
58
|
-
if (segments.length === 1 && !segments[0]) segments = [];
|
|
59
|
-
if (segments.some(segment => !segment || /[/\\\0]/.test(segment) || segment === '.' || segment === '..')) return null;
|
|
60
|
-
const parts = pattern === '/' ? [] : pattern.split('/').slice(1), params = Object.create(null), rank = [];
|
|
61
|
-
let index = 0;
|
|
62
|
-
for (const part of parts) {
|
|
63
|
-
const optional = /^\[\[\.\.\.([^\]]+)\]\]$/.exec(part);
|
|
64
|
-
const catchall = /^\[\.\.\.([^\]]+)\]$/.exec(part);
|
|
65
|
-
const parameter = /^\[([^\]]+)\]$/.exec(part);
|
|
66
|
-
if (optional || catchall) {
|
|
67
|
-
if (catchall && index === segments.length) return null;
|
|
68
|
-
if (index !== segments.length) params[(optional || catchall)[1]] = segments.slice(index);
|
|
69
|
-
index = segments.length; rank.push(optional ? 0 : 1);
|
|
70
|
-
} else if (parameter) {
|
|
71
|
-
if (index === segments.length) return null;
|
|
72
|
-
params[parameter[1]] = segments[index++]; rank.push(2);
|
|
73
|
-
} else {
|
|
74
|
-
if (segments[index++] !== part) return null;
|
|
75
|
-
rank.push(3);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return index === segments.length ? { params, rank } : null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
57
|
export function resolvePageRoute(manifest, pathname) {
|
|
82
58
|
let winner;
|
|
83
59
|
for (const [routes, pages] of [[manifest.routes, true], [manifest.nonPagesRoutes || [], false]]) {
|
|
@@ -129,7 +105,8 @@ export function createPagesNavigation({ initial, initialRoute, manifestUrl, read
|
|
|
129
105
|
initial = undefined;
|
|
130
106
|
let manifestPromise, active, renderingError, sequence = 0, beforePop = () => true;
|
|
131
107
|
let currentKey = win.history.state?.__prnextPages?.key || 0, nextKey = currentKey + 1;
|
|
132
|
-
const prefetches = new Map(),
|
|
108
|
+
const prefetches = new Map(), scheduler = createPrefetchScheduler({ window: win, document: doc });
|
|
109
|
+
let prefetchBytes = 0;
|
|
133
110
|
|
|
134
111
|
function assetURL(value) {
|
|
135
112
|
const url = new URL(value, win.location.href);
|
|
@@ -147,7 +124,7 @@ export function createPagesNavigation({ initial, initialRoute, manifestUrl, read
|
|
|
147
124
|
const abort = new AbortController(), timer = setTimeout(() => abort.abort(), 30_000);
|
|
148
125
|
try {
|
|
149
126
|
const value = JSON.parse(await consume(await fetchAsset(manifestUrl, abort.signal), ASSET_LIMIT));
|
|
150
|
-
if (value.buildId !== buildId || !Array.isArray(value.routes) || value.routes.length > 10_000) throw new Error('Invalid Pages manifest');
|
|
127
|
+
if (value.buildId !== buildId || current.deploymentId && value.deploymentId !== current.deploymentId || !Array.isArray(value.routes) || value.routes.length > 10_000) throw new Error('Invalid Pages manifest');
|
|
151
128
|
for (const route of [...value.routes, ...Object.values(value.errors || {}).filter(Boolean)]) {
|
|
152
129
|
if (typeof route.pattern !== 'string' || !Array.isArray(route.css)) throw new Error('Invalid page route');
|
|
153
130
|
assetURL(route.client); for (const css of route.css) assetURL(css);
|
|
@@ -278,44 +255,45 @@ export function createPagesNavigation({ initial, initialRoute, manifestUrl, read
|
|
|
278
255
|
prefetchBytes -= entry.bytes || 0; entry.bytes = 0;
|
|
279
256
|
if (abort) entry.abort.abort(); else entry.consumed = true;
|
|
280
257
|
}
|
|
281
|
-
function
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
entry.bytes = decoded.bytes || 0; prefetchBytes += entry.bytes;
|
|
301
|
-
while (prefetchBytes > PREFETCH_BYTES) dropPrefetch(prefetches.keys().next().value);
|
|
302
|
-
}
|
|
303
|
-
})().catch(() => { dropPrefetch(entry.key); }).finally(() => { prefetchRunning--; entry.resolve(); pumpPrefetch(); });
|
|
258
|
+
async function runPrefetch(entry) {
|
|
259
|
+
const map = await abortable(manifest(), entry.abort.signal), matched = resolvePageRoute(map, internalPath(entry.request));
|
|
260
|
+
if (!matched?.pages) return;
|
|
261
|
+
// Fetch rather than evaluate speculative modules: fetch can be cancelled,
|
|
262
|
+
// whereas import() can leave an uncancellable module request in flight.
|
|
263
|
+
await consume(await fetchAsset(matched.route.client, entry.abort.signal), ASSET_LIMIT);
|
|
264
|
+
if (entry.abort.signal.aborted) return;
|
|
265
|
+
for (const css of matched.route.css) await consume(await fetchAsset(css, entry.abort.signal), ASSET_LIMIT);
|
|
266
|
+
if (matched.route.ssg) {
|
|
267
|
+
const decoded = await requestData(entry.request, entry.abort.signal, true);
|
|
268
|
+
if (entry.abort.signal.aborted || (prefetches.get(entry.key) !== entry && !entry.consumed) || decoded.cacheable === false) return;
|
|
269
|
+
entry.data = decoded;
|
|
270
|
+
if (entry.consumed) return;
|
|
271
|
+
entry.bytes = decoded.bytes || 0; prefetchBytes += entry.bytes;
|
|
272
|
+
while (prefetchBytes > PREFETCH_BYTES) {
|
|
273
|
+
const victim = prefetchCacheVictim([...prefetches].filter(([, value]) => value.bytes > 0), 1);
|
|
274
|
+
if (victim === undefined) break;
|
|
275
|
+
dropPrefetch(victim);
|
|
276
|
+
}
|
|
304
277
|
}
|
|
305
278
|
}
|
|
306
|
-
function prefetch(url, as, options) {
|
|
279
|
+
function prefetch(url, as, options = {}) {
|
|
307
280
|
let request;
|
|
308
281
|
try { ({ request } = targets(url, as, options)); } catch { return Promise.resolve(); }
|
|
309
282
|
if (!isApplicationURL(request, win.location.origin, basePath) || process.env.NODE_ENV !== 'production') return Promise.resolve();
|
|
310
283
|
request.hash = ''; const key = request.href;
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const
|
|
316
|
-
|
|
284
|
+
const priority = options.priority === 'viewport' ? 0 : 1;
|
|
285
|
+
const previous = prefetches.get(key);
|
|
286
|
+
if (previous && !previous.abort.signal.aborted) { previous.priority = Math.max(previous.priority, priority); previous.task.promote(options.priority); return previous.promise; }
|
|
287
|
+
if (previous) dropPrefetch(key);
|
|
288
|
+
const victim = prefetches.size >= PREFETCH_LIMIT ? prefetchCacheVictim(prefetches, priority) : undefined;
|
|
289
|
+
if (prefetches.size >= PREFETCH_LIMIT && victim === undefined) return Promise.resolve();
|
|
290
|
+
const entry = { key, request, priority, abort: new AbortController() };
|
|
291
|
+
entry.task = scheduler.schedule(() => runPrefetch(entry), { abort: entry.abort, priority: options.priority });
|
|
292
|
+
if (!entry.task) return Promise.resolve();
|
|
293
|
+
if (victim !== undefined) dropPrefetch(victim);
|
|
294
|
+
entry.promise = entry.task.promise.catch(() => { if (prefetches.get(key) === entry) dropPrefetch(key); });
|
|
317
295
|
entry.timer = setTimeout(() => dropPrefetch(key), PREFETCH_TTL);
|
|
318
|
-
prefetches.set(key, entry);
|
|
296
|
+
prefetches.set(key, entry); return entry.promise;
|
|
319
297
|
}
|
|
320
298
|
function cancelActive() {
|
|
321
299
|
if (!active) return;
|
|
@@ -332,6 +310,9 @@ export function createPagesNavigation({ initial, initialRoute, manifestUrl, read
|
|
|
332
310
|
cancelActive(); renderingError?.abort.abort(); renderingError = undefined; saveScroll();
|
|
333
311
|
const job = { id: ++sequence, abort: new AbortController(), asPath: destination.pathname + destination.search + destination.hash, shallow: !!options.shallow };
|
|
334
312
|
active = job;
|
|
313
|
+
const prefetchKey = new URL(request); prefetchKey.hash = '';
|
|
314
|
+
const releaseNavigation = scheduler.navigation(prefetches.get(prefetchKey.href)?.task);
|
|
315
|
+
job.abort.signal.addEventListener('abort', releaseNavigation, { once: true });
|
|
335
316
|
const timer = setTimeout(() => job.abort.abort(new Error('Page navigation timed out')), 30_000);
|
|
336
317
|
const hashOnly = destination.pathname === currentURL.pathname && destination.search === currentURL.search &&
|
|
337
318
|
(destination.hash !== currentURL.hash || !!destination.hash) && !options.shallow;
|
|
@@ -459,7 +440,7 @@ export function createPagesNavigation({ initial, initialRoute, manifestUrl, read
|
|
|
459
440
|
job.finished = true; active = undefined;
|
|
460
441
|
events.emit('routeChangeError', error, job.asPath, { shallow: job.shallow });
|
|
461
442
|
hard(destination, options.replace || options.pop); return false;
|
|
462
|
-
} finally { clearTimeout(timer); }
|
|
443
|
+
} finally { clearTimeout(timer); job.abort.signal.removeEventListener('abort', releaseNavigation); releaseNavigation(); }
|
|
463
444
|
}
|
|
464
445
|
async function reportRenderError(error, failedView) {
|
|
465
446
|
if (failedView !== current && (failedView.clientSnapshot || failedView.Page !== current.Page || failedView.props !== current.props)) return false;
|
|
@@ -550,7 +531,11 @@ export function createPagesNavigation({ initial, initialRoute, manifestUrl, read
|
|
|
550
531
|
};
|
|
551
532
|
let frame;
|
|
552
533
|
const onScroll = () => { if (frame !== undefined) return; frame = win.requestAnimationFrame(() => { frame = undefined; saveScroll(); }); };
|
|
553
|
-
const pagehide = () => {
|
|
534
|
+
const pagehide = () => {
|
|
535
|
+
saveScroll(); cancelActive(); active = undefined;
|
|
536
|
+
renderingError?.abort.abort(); renderingError = undefined;
|
|
537
|
+
for (const key of prefetches.keys()) dropPrefetch(key);
|
|
538
|
+
};
|
|
554
539
|
win.history.replaceState(historyState(currentURL, currentRequest, currentKey, {}, [win.scrollX, win.scrollY]), '', currentURL.href);
|
|
555
540
|
win.history.scrollRestoration = 'manual';
|
|
556
541
|
win.addEventListener('popstate', popstate); win.addEventListener('scroll', onScroll, { passive: true }); win.addEventListener('pagehide', pagehide);
|
|
@@ -565,6 +550,6 @@ export function createPagesNavigation({ initial, initialRoute, manifestUrl, read
|
|
|
565
550
|
await navigate(currentRequest, currentURL, { replace: true, scroll: false, devRefresh:true });
|
|
566
551
|
} } : {}),
|
|
567
552
|
refreshFallback,
|
|
568
|
-
dispose() { pagehide(); win.removeEventListener('popstate', popstate); win.removeEventListener('scroll', onScroll); win.removeEventListener('pagehide', pagehide); if (frame !== undefined) win.cancelAnimationFrame(frame); },
|
|
553
|
+
dispose() { pagehide(); scheduler.dispose(); win.removeEventListener('popstate', popstate); win.removeEventListener('scroll', onScroll); win.removeEventListener('pagehide', pagehide); if (frame !== undefined) win.cancelAnimationFrame(frame); },
|
|
569
554
|
};
|
|
570
555
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const INTENT = 1;
|
|
2
|
+
const abortError = () => Object.assign(new Error('Speculative navigation cancelled'), { name: 'AbortError' });
|
|
3
|
+
|
|
4
|
+
// The completed-result cache obeys the same priority as the producer queue.
|
|
5
|
+
// Otherwise a viewport scan could evict the hovered route immediately after
|
|
6
|
+
// the scheduler had correctly protected its request.
|
|
7
|
+
export function prefetchCacheVictim(cache, priority) {
|
|
8
|
+
let candidate;
|
|
9
|
+
for (const [key, entry] of cache) {
|
|
10
|
+
if (entry.abort.signal.aborted) return key;
|
|
11
|
+
if (!candidate || entry.priority < candidate[1].priority) candidate = [key, entry];
|
|
12
|
+
}
|
|
13
|
+
return candidate && candidate[1].priority <= priority ? candidate[0] : undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Speculative work never competes with a committed navigation. The same policy
|
|
17
|
+
// is used by both routers; data and authorization remain in their own caches.
|
|
18
|
+
export function createPrefetchScheduler({ window: win = globalThis.window, document: doc = globalThis.document,
|
|
19
|
+
concurrency = 2, limit = 8 } = {}) {
|
|
20
|
+
const tasks = new Set();
|
|
21
|
+
let serial = 0, active = 0, navigating = 0, disposed = false, scheduled = false;
|
|
22
|
+
const connection = win?.navigator?.connection;
|
|
23
|
+
const priorityOf = value => value === 'viewport' ? 0 : INTENT;
|
|
24
|
+
function policy(priority) {
|
|
25
|
+
const network = win?.navigator?.connection;
|
|
26
|
+
if (doc?.visibilityState === 'hidden' || win?.navigator?.onLine === false || network?.saveData || /^(slow-)?2g$/.test(network?.effectiveType || '')) return 0;
|
|
27
|
+
const slow = network?.effectiveType === '3g' || (network?.downlink > 0 && network.downlink < 1.5);
|
|
28
|
+
return slow ? (priority < INTENT ? 0 : 1) : concurrency;
|
|
29
|
+
}
|
|
30
|
+
function finish(task, error, value) {
|
|
31
|
+
if (!tasks.delete(task)) return;
|
|
32
|
+
if (task.running) active--;
|
|
33
|
+
task.abort.signal.removeEventListener('abort', task.cancelled);
|
|
34
|
+
const { resolve, reject } = task;
|
|
35
|
+
task.run = task.resolve = task.reject = task.cancelled = undefined;
|
|
36
|
+
error ? reject(error) : resolve(value);
|
|
37
|
+
wake();
|
|
38
|
+
}
|
|
39
|
+
function start(task) {
|
|
40
|
+
task.running = true; active++;
|
|
41
|
+
let result;
|
|
42
|
+
try { result = task.run(task.abort.signal); } catch (error) { finish(task, error); return; }
|
|
43
|
+
Promise.resolve(result).then(value => finish(task, null, value), error => finish(task, error));
|
|
44
|
+
}
|
|
45
|
+
function pump() {
|
|
46
|
+
scheduled = false;
|
|
47
|
+
if (disposed) return;
|
|
48
|
+
const queued = [...tasks].filter(task => !task.running).sort((a, b) => b.priority - a.priority || a.order - b.order);
|
|
49
|
+
for (const task of queued) {
|
|
50
|
+
if (!tasks.has(task)) continue;
|
|
51
|
+
if (task.priority === 2) { start(task); continue; }
|
|
52
|
+
if (navigating) break;
|
|
53
|
+
const slots = policy(task.priority);
|
|
54
|
+
if (!slots) { task.abort.abort(abortError()); continue; }
|
|
55
|
+
if (active >= slots) continue;
|
|
56
|
+
start(task);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function wake() {
|
|
60
|
+
if (scheduled || disposed) return;
|
|
61
|
+
scheduled = true; queueMicrotask(pump);
|
|
62
|
+
}
|
|
63
|
+
function changed() {
|
|
64
|
+
for (const task of tasks) if (task.priority !== 2 && !policy(task.priority)) task.abort.abort(abortError());
|
|
65
|
+
wake();
|
|
66
|
+
}
|
|
67
|
+
doc?.addEventListener?.('visibilitychange', changed);
|
|
68
|
+
win?.addEventListener?.('online', changed);
|
|
69
|
+
win?.addEventListener?.('offline', changed);
|
|
70
|
+
connection?.addEventListener?.('change', changed);
|
|
71
|
+
return {
|
|
72
|
+
schedule(run, { priority, abort = new AbortController() } = {}) {
|
|
73
|
+
const rank = priorityOf(priority);
|
|
74
|
+
if (disposed || abort.signal.aborted || !policy(rank)) return undefined;
|
|
75
|
+
if (tasks.size >= limit) {
|
|
76
|
+
// Keep intent ahead of broad viewport scans. Never evict demand work.
|
|
77
|
+
const oldest = [...tasks].filter(task => task.priority < 2).sort((a, b) => a.priority - b.priority || a.order - b.order)[0];
|
|
78
|
+
if (!oldest || oldest.priority > rank) return undefined;
|
|
79
|
+
oldest.abort.abort(abortError());
|
|
80
|
+
}
|
|
81
|
+
const task = { run, abort, priority: rank, order: serial++ };
|
|
82
|
+
const promise = new Promise((resolve, reject) => { task.resolve = resolve; task.reject = reject; });
|
|
83
|
+
// Cancellation also settles a task whose producer ignores AbortSignal.
|
|
84
|
+
// A late result cannot regain a queue slot or resurrect an invalid cache.
|
|
85
|
+
task.cancelled = () => finish(task, abort.signal.reason || abortError());
|
|
86
|
+
tasks.add(task); abort.signal.addEventListener('abort', task.cancelled, { once: true });
|
|
87
|
+
wake();
|
|
88
|
+
return { abort, promise,
|
|
89
|
+
promote(priority = 'intent') { if (tasks.has(task)) { task.priority = Math.max(task.priority, priorityOf(priority)); wake(); } },
|
|
90
|
+
demand() { if (tasks.has(task)) { task.priority = 2; wake(); } },
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
navigation(target) {
|
|
94
|
+
navigating++;
|
|
95
|
+
target?.demand();
|
|
96
|
+
for (const task of tasks) if (task.abort !== target?.abort && task.priority !== 2) task.abort.abort(abortError());
|
|
97
|
+
let released = false;
|
|
98
|
+
return () => { if (!released) { released = true; navigating--; wake(); } };
|
|
99
|
+
},
|
|
100
|
+
dispose() {
|
|
101
|
+
if (disposed) return;
|
|
102
|
+
disposed = true;
|
|
103
|
+
doc?.removeEventListener?.('visibilitychange', changed);
|
|
104
|
+
win?.removeEventListener?.('online', changed);
|
|
105
|
+
win?.removeEventListener?.('offline', changed);
|
|
106
|
+
connection?.removeEventListener?.('change', changed);
|
|
107
|
+
for (const task of tasks) task.abort.abort(abortError());
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
package/runtime/profile.mjs
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import presets from './profiles.json' with { type: 'json' };
|
|
2
2
|
|
|
3
|
+
export function rscResourceLimits(environment=process.env,production=environment.NODE_ENV!=='development'){
|
|
4
|
+
if(!production||/--max[-_]semi[-_]space[-_]size(?:[=\s]|$)/.test(environment.NODE_OPTIONS||''))return undefined;
|
|
5
|
+
return {maxYoungGenerationSizeMb:runtimeProfile(environment,production).youngGenerationMiB};
|
|
6
|
+
}
|
|
7
|
+
|
|
3
8
|
export function runtimeProfile(environment = process.env, production = environment.NODE_ENV !== 'development') {
|
|
4
9
|
const requested = environment.PRNEXT_PROFILE || (environment.PRNEXT_MEMORY_PROFILE === 'compact' ? 'compact' : 'balanced');
|
|
5
10
|
const name = requested === 'standard' ? 'classic' : requested;
|
|
6
11
|
if (!Object.hasOwn(presets, name)) throw new Error(`Unknown runtime profile ${JSON.stringify(name)}; use ${Object.keys(presets).join(', ')}`);
|
|
7
12
|
const effective = production ? name : 'classic';
|
|
13
|
+
if(production&&environment.PRNEXT_EFFECTIVE_PROFILE){
|
|
14
|
+
let value;
|
|
15
|
+
try{value=JSON.parse(environment.PRNEXT_EFFECTIVE_PROFILE);}catch{throw new Error('Invalid effective runtime profile');}
|
|
16
|
+
const settings=value?.[1],original=presets[effective];
|
|
17
|
+
if(!Array.isArray(value)||value.length!==2||value[0]!==effective||!settings||
|
|
18
|
+
Object.keys(settings).length!==Object.keys(original).length||Object.keys(original).some(key=>!Object.hasOwn(settings,key)||
|
|
19
|
+
(original[key]===null?settings[key]!==null&&(!Number.isSafeInteger(settings[key])||settings[key]<1):typeof original[key]==='boolean'?typeof settings[key]!=='boolean':!Number.isSafeInteger(settings[key])||settings[key]<1||settings[key]>Math.max(512,original[key]))))throw new Error('Invalid effective runtime profile');
|
|
20
|
+
return {name:effective,...settings};
|
|
21
|
+
}
|
|
8
22
|
return { name: effective, ...presets[effective] };
|
|
9
23
|
}
|
package/runtime/profiles.json
CHANGED
|
@@ -40,9 +40,9 @@
|
|
|
40
40
|
"renderIdleSeconds": null
|
|
41
41
|
},
|
|
42
42
|
"memory": {
|
|
43
|
-
"optimizeForSize":
|
|
43
|
+
"optimizeForSize": false,
|
|
44
44
|
"semiSpaceMiB": 2,
|
|
45
|
-
"youngGenerationMiB":
|
|
45
|
+
"youngGenerationMiB": 8,
|
|
46
46
|
"renderStarts": 8,
|
|
47
47
|
"liveResponses": 28,
|
|
48
48
|
"apiConcurrency": 64,
|
package/runtime/render.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {reportRequestError} from './instrumentation.mjs';
|
|
1
2
|
import { localePath } from '../compat/locale.cjs';
|
|
2
3
|
import React from 'react';
|
|
3
4
|
import DefaultApp, { loadGetInitialProps } from '../compat/app.cjs';
|
|
@@ -94,6 +95,7 @@ export async function renderPageRequest(options) {
|
|
|
94
95
|
try { result = await renderPage({ ...options, capturedResponse }); }
|
|
95
96
|
catch (error) {
|
|
96
97
|
if ([400, 504].includes(error?.statusCode) || options.signal?.aborted) throw error;
|
|
98
|
+
reportRequestError(error);
|
|
97
99
|
console.error('[prnext]', error?.stack || error);
|
|
98
100
|
return renderPageError(options, { statusCode: 500, error, responseHeaders: capturedResponse.getHeaders() });
|
|
99
101
|
}
|
|
@@ -266,6 +268,7 @@ export async function renderIsrPage(options) {
|
|
|
266
268
|
catch (error) {
|
|
267
269
|
if (!options.capturePageFailure) throw error;
|
|
268
270
|
const pageFailure = summarizePageFailure(error);
|
|
271
|
+
reportRequestError(error);
|
|
269
272
|
console.error('[prnext] Page generation failed:', pageFailure.stack || pageFailure.message);
|
|
270
273
|
return { status: 500, headers: {}, body: Buffer.alloc(0), pageFailure };
|
|
271
274
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Shared browser route matching; importing this never loads either router. */
|
|
2
|
+
export function matchPagePattern(pattern, pathname) {
|
|
3
|
+
let segments;
|
|
4
|
+
try { segments = pathname.replace(/\/+$/, '').split('/').slice(1).map(decodeURIComponent); }
|
|
5
|
+
catch { return null; }
|
|
6
|
+
if (segments.length === 1 && !segments[0]) segments = [];
|
|
7
|
+
if (segments.some(segment => !segment || /[/\\\0]/.test(segment) || segment === '.' || segment === '..')) return null;
|
|
8
|
+
const parts = pattern === '/' ? [] : pattern.split('/').slice(1), params = Object.create(null), rank = [];
|
|
9
|
+
let index = 0;
|
|
10
|
+
for (const part of parts) {
|
|
11
|
+
const optional = /^\[\[\.\.\.([^\]]+)\]\]$/.exec(part);
|
|
12
|
+
const catchall = /^\[\.\.\.([^\]]+)\]$/.exec(part);
|
|
13
|
+
const parameter = /^\[([^\]]+)\]$/.exec(part);
|
|
14
|
+
if (optional || catchall) {
|
|
15
|
+
if (catchall && index === segments.length) return null;
|
|
16
|
+
if (index !== segments.length) params[(optional || catchall)[1]] = segments.slice(index);
|
|
17
|
+
index = segments.length; rank.push(optional ? 0 : 1);
|
|
18
|
+
} else if (parameter) {
|
|
19
|
+
if (index === segments.length) return null;
|
|
20
|
+
params[parameter[1]] = segments[index++]; rank.push(2);
|
|
21
|
+
} else {
|
|
22
|
+
if (segments[index++] !== part) return null;
|
|
23
|
+
rank.push(3);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return index === segments.length ? { params, rank } : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isHtmlDocumentRoute(routes, pathname) {
|
|
30
|
+
let winner;
|
|
31
|
+
for (const route of routes || []) {
|
|
32
|
+
const match = matchPagePattern(route.pattern, pathname);
|
|
33
|
+
if (!match) continue;
|
|
34
|
+
const rank = winner && (() => {
|
|
35
|
+
for (let index = 0; index < Math.max(match.rank.length, winner.rank.length); index++) {
|
|
36
|
+
const difference = (match.rank[index] ?? 4) - (winner.rank[index] ?? 4);
|
|
37
|
+
if (difference) return difference;
|
|
38
|
+
}
|
|
39
|
+
return 0;
|
|
40
|
+
})();
|
|
41
|
+
if (!winner || rank > 0) winner = { ...match, route };
|
|
42
|
+
}
|
|
43
|
+
return winner?.route.htmlOnly === true;
|
|
44
|
+
}
|
package/runtime/rsc-worker.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import {drainTraces} from './trace.mjs';
|
|
2
|
+
import {withInstrumentation,reportRequestError,wasRequestErrorReported} from './instrumentation.mjs';
|
|
1
3
|
import React from 'react';
|
|
2
4
|
import {scheduleCompileCacheFlush} from './compile-cache.mjs';
|
|
3
5
|
import {preloadModules} from './preload.mjs';
|
|
@@ -8,6 +10,7 @@ import { createFromReadableStream } from 'react-server-dom-webpack/client.edge';
|
|
|
8
10
|
import { parentPort } from 'node:worker_threads';
|
|
9
11
|
import { pathToFileURL } from 'node:url';
|
|
10
12
|
import { createHash } from 'node:crypto';
|
|
13
|
+
import { navigationAuthorization } from '../compat/navigation-authorization.cjs';
|
|
11
14
|
import { runRequestContext, currentRequest } from '../compat/headers.cjs';
|
|
12
15
|
import { Metadata } from './app-metadata.mjs';
|
|
13
16
|
import { appTimeoutError } from './app-errors.mjs';
|
|
@@ -27,6 +30,8 @@ import { flightConsumer } from './cache-components-rsc.mjs';
|
|
|
27
30
|
import { partialModel, isPartialHole, boundedPartialFlight } from './app-partial-model.mjs';
|
|
28
31
|
import { PartialTemplates, canResumePartialDirectly, partialTemplateReuse } from './partial-template.mjs';
|
|
29
32
|
import { createAfterScope } from '../compat/after.cjs';
|
|
33
|
+
import {renderKeyFor} from './app-render-key.mjs';
|
|
34
|
+
import { selectAppStyles } from './app-style-selection.mjs';
|
|
30
35
|
|
|
31
36
|
installFetchCache();
|
|
32
37
|
if (process.env.PRNEXT_INSPECT_DIR) parentPort.postMessage({type:'rsc-ready'});
|
|
@@ -71,6 +76,9 @@ function queryObject(url) {
|
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
function createTree(entry, request, fallback = null) {
|
|
79
|
+
function RetainedBranch(props) {
|
|
80
|
+
return React.createElement(entry.ActivitySlot, { ...props, authorizationKey: navigationAuthorization(currentRequest(), request.authorizationSecret || request.actionKey, request.navigationCache?.authCookies) });
|
|
81
|
+
}
|
|
74
82
|
const params = staticParams(request.params || {});
|
|
75
83
|
const searchParams = staticSearchParams(queryObject(new URL(request.url)), currentRequest(), entry.page?.default?.$$typeof === Symbol.for('react.client.reference'));
|
|
76
84
|
if (entry.routing && !fallback) return createAdvancedTree(entry, request, searchParams);
|
|
@@ -100,7 +108,7 @@ function createTree(entry, request, fallback = null) {
|
|
|
100
108
|
const scopedParams = paramsBySegment(segments, request.params || {});
|
|
101
109
|
for (const segment of segments) {
|
|
102
110
|
scopes.set(segment, `${segment.path || ''}:${JSON.stringify(scopedParams.get(segment))}`);
|
|
103
|
-
if (request.partialParams?.length) for (const prefix of ['', 'error:', 'navigation:', 'layout:']) keyScope(`${prefix}${segment.path || ''}:`, scopedParams.get(segment), prefix === 'layout:' ? ['segments'] : undefined);
|
|
111
|
+
if (request.partialParams?.length) for (const prefix of ['', 'error:', 'navigation:', 'layout:', 'activity:']) keyScope(`${prefix}${segment.path || ''}:`, scopedParams.get(segment), prefix === 'layout:' ? ['segments'] : prefix === 'activity:' ? ['identity', 'authorizationKey'] : ['error:', 'navigation:'].includes(prefix) ? ['resetKey'] : undefined);
|
|
104
112
|
}
|
|
105
113
|
let instantConfig = request.instantValidation ? entry.pageConfig?.instant : undefined;
|
|
106
114
|
for (let index = fallback ? Math.min(fallback.index, segments.length - 1) : segments.length - 1; index >= 0; index--) {
|
|
@@ -128,6 +136,10 @@ function createTree(entry, request, fallback = null) {
|
|
|
128
136
|
tree = React.createElement(component(segment.template, 'template'), { key: child ? scopes.get(child) : pageKey }, tree);
|
|
129
137
|
}
|
|
130
138
|
if (segment.layout) {
|
|
139
|
+
if (entry.ActivitySlot) tree = React.createElement(RetainedBranch, {
|
|
140
|
+
identity: segments.slice(index + 1).some(child => child.layout) ? scopes.get(segments[index + 1]) : pageKey + new URL(request.originalUrl || request.url).search,
|
|
141
|
+
key: `activity:${scopes.get(segment)}`,
|
|
142
|
+
}, tree);
|
|
131
143
|
if (request.cacheComponents && segment === rootLayout) tree = React.createElement(React.Fragment, null, head, tree);
|
|
132
144
|
tree = React.createElement(component(segment.layout, 'layout'), { params: staticParams(scopedParams.get(segment)), key: scopes.get(segment) }, tree);
|
|
133
145
|
if (entry.LayoutProvider) tree = React.createElement(entry.LayoutProvider, { key: `layout:${scopes.get(segment)}`,
|
|
@@ -196,8 +208,9 @@ async function renderFlight(request, fallback = null, channel = null) {
|
|
|
196
208
|
try {
|
|
197
209
|
if (request.routingContexts?.size) request.renderRoutingBranch = routingBranchRenderer(cacheContext, bundlerConfig,
|
|
198
210
|
renderSignal, error => publicError(error, request.production).digest);
|
|
199
|
-
let model = request.controlModel || { ...(allowedRouting ? createTree(entry, request, fallback) : { tree: null, routingInvalid: true }), router, css: request.css || [], fonts: request.fonts || [], ...(request.instantDiagnostics?.length ? { instantDiagnostics: request.instantDiagnostics } : {}), ...request.actionModel };
|
|
211
|
+
let model = request.controlModel || { ...(allowedRouting ? createTree(entry, request, fallback) : { tree: null, routingInvalid: true }), router, styleRoute: request.routePattern, css: request.css || [], ...(request.cssScopes?.length ? { cssScopes: request.cssScopes } : {}), fonts: request.fonts || [], ...(request.instantDiagnostics?.length ? { instantDiagnostics: request.instantDiagnostics } : {}), ...request.actionModel };
|
|
200
212
|
if (model.routing?.params) model.router.params = model.routing.params;
|
|
213
|
+
selectAppStyles(model);
|
|
201
214
|
if (request.partialFlight) {
|
|
202
215
|
const consumer = { serverConsumerManifest: flightConsumer(request), replayConsoleLogs: false };
|
|
203
216
|
const decode = () => {
|
|
@@ -214,14 +227,16 @@ async function renderFlight(request, fallback = null, channel = null) {
|
|
|
214
227
|
const keyMap = request.partialKeys ? new Map(request.partialKeys) : undefined;
|
|
215
228
|
model = partialModel(stored, { live, keyMap, reuse: partialTemplateReuse(stored, keyMap) });
|
|
216
229
|
}
|
|
230
|
+
model.authorizationKey = navigationAuthorization(cacheContext, request.authorizationSecret || request.actionKey, request.navigationCache?.authCookies);
|
|
217
231
|
const stream = renderToReadableStream(model, bundlerConfig, {
|
|
218
232
|
signal: renderSignal,
|
|
219
233
|
temporaryReferences: request.temporaryReferences,
|
|
220
234
|
onError(error) {
|
|
221
235
|
const failure = error && typeof error === 'object' ? error : new Error(String(error));
|
|
222
|
-
if (cacheContext.staticState?.partial && isPartialHole(failure)) return 'PRNEXT_PPR_DYNAMIC';
|
|
236
|
+
if ((cacheContext.staticState?.partial || request.navigationStage) && isPartialHole(failure)) return 'PRNEXT_PPR_DYNAMIC';
|
|
223
237
|
renderError ??= failure;
|
|
224
238
|
const digest = publicError(failure, request.production).digest;
|
|
239
|
+
if(ordinaryRenderError(failure)&&!abort.signal.aborted&&!channel?.signal.aborted){if(!failure.digest&&Object.isExtensible(failure))failure.digest=digest;const reporting=reportRequestError(failure);if(reporting&&digest.length<=1024&&request.instrumentationId!==undefined)parentPort.postMessage({id:request.instrumentationId,type:'instrumentation-error',value:digest});}
|
|
225
240
|
if (!abort.signal.aborted && !channel?.signal.aborted && ordinaryRenderError(failure)) console.error(`[prnext] ${digest}`, failure?.stack || failure);
|
|
226
241
|
return digest;
|
|
227
242
|
},
|
|
@@ -342,7 +357,12 @@ async function renderFlight(request, fallback = null, channel = null) {
|
|
|
342
357
|
});
|
|
343
358
|
}
|
|
344
359
|
|
|
345
|
-
async function handleRequest(request,
|
|
360
|
+
async function handleRequest(request,channel=null){
|
|
361
|
+
process.env.NEXT_RUNTIME='nodejs';
|
|
362
|
+
if(!request.runtimeInstrumentation&&(request.operation==='static-params'||request.staticGeneration||request.staticState))return handleRequestInner(request,channel);
|
|
363
|
+
return withInstrumentation({...request,signal:channel?.signal,manifest:{instrumentation:request.instrumentation},route:{router:'app',kind:'page',pattern:request.routePattern,cacheConfig:request.cacheConfig}},()=>handleRequestInner(request,channel),{realm:'rsc'});
|
|
364
|
+
}
|
|
365
|
+
async function handleRequestInner(request, channel = null) {
|
|
346
366
|
if (request.operation === 'static-params') {
|
|
347
367
|
return runRequestContext({ ...request, phase: 'render' }, async () => {
|
|
348
368
|
const context = currentRequest();
|
|
@@ -390,6 +410,7 @@ const responseCompletions = new Map();
|
|
|
390
410
|
const manifests = new Map();
|
|
391
411
|
const artifacts = new Map();
|
|
392
412
|
parentPort.on('message', async ({ id, request, type, value, token }) => {
|
|
413
|
+
if(type==='telemetry-flush'){await drainTraces();parentPort.postMessage({type:'telemetry-flushed',token});return;}
|
|
393
414
|
if(type==='preload'){
|
|
394
415
|
try{await preloadModules(value.files,value.options,file=>routeEntry({modulePath:file,production:true}));parentPort.postMessage({type:'preloaded',id});}
|
|
395
416
|
catch(error){parentPort.postMessage({type:'preloaded',id,error:error.message});}return;
|
|
@@ -410,17 +431,18 @@ parentPort.on('message', async ({ id, request, type, value, token }) => {
|
|
|
410
431
|
for (const field of ['clientModules', 'actions']) {
|
|
411
432
|
if (request[field + 'Id'] !== undefined) request[field] = manifests.get(request[field + 'Id']);
|
|
412
433
|
}
|
|
434
|
+
request.instrumentationId=id;
|
|
413
435
|
const channel = request.stream && (!request.action || request.action.id) ? workerStreamChannel(parentPort, id) : null;
|
|
414
436
|
let responseDone, responseTimer;
|
|
415
|
-
if (request.
|
|
437
|
+
if (request.cacheConfig?.runtime !== 'edge') {
|
|
416
438
|
const timeoutMs = request.renderTimeoutMs ?? 25000;
|
|
417
|
-
request.afterScope = createAfterScope({ timeoutMs, onComplete: flushCacheWork,
|
|
418
|
-
onTimeout: error => parentPort.postMessage({ id, error: { message: error.message, retireWorker: true } })
|
|
419
|
-
|
|
439
|
+
request.afterScope = createAfterScope({report:error=>{console.error('[prnext] after() failed:',error?.stack||error);return reportRequestError(error);}, timeoutMs, onComplete: flushCacheWork,
|
|
440
|
+
onTimeout: error => parentPort.postMessage({ id, error: { message: error.message, retireWorker: true } }),
|
|
441
|
+
onRegistered() { if (request.waitForResponse) responseDone = new Promise(resolve => {
|
|
420
442
|
const finish = () => { clearTimeout(responseTimer); resolve(); };
|
|
421
443
|
responseCompletions.set(id, finish);
|
|
422
444
|
responseTimer = setTimeout(finish, timeoutMs);
|
|
423
|
-
});
|
|
445
|
+
}); } });
|
|
424
446
|
}
|
|
425
447
|
activeRequests.add(id);
|
|
426
448
|
if (channel) streamChannels.set(id, channel);
|
|
@@ -432,14 +454,14 @@ parentPort.on('message', async ({ id, request, type, value, token }) => {
|
|
|
432
454
|
delete request.partialFlightId;
|
|
433
455
|
}
|
|
434
456
|
if (channel) {
|
|
435
|
-
await handleRequest({ ...request, renderKey: id }, channel);
|
|
457
|
+
await handleRequest({ ...request, renderKey: renderKeyFor(request, id) }, channel);
|
|
436
458
|
return;
|
|
437
459
|
}
|
|
438
|
-
const result = await handleRequest({ ...request, renderKey: id });
|
|
460
|
+
const result = await handleRequest({ ...request, renderKey: renderKeyFor(request, id) });
|
|
439
461
|
parentPort.postMessage({ id, ...result }, result.body instanceof ArrayBuffer ? [result.body] : []);
|
|
440
462
|
} catch (error) {
|
|
441
463
|
parentPort.postMessage({ id, error: {
|
|
442
|
-
message: error?.message || String(error), stack: error?.stack, digest: error?.digest, statusCode: error?.statusCode,
|
|
464
|
+
instrumentationReported:wasRequestErrorReported(error), message: error?.message || String(error), stack: error?.stack, digest: error?.digest, statusCode: error?.statusCode,
|
|
443
465
|
retireWorker: error?.retireWorker, responseHeaders: error?.responseHeaders,
|
|
444
466
|
code: error?.code, staticMode: error?.staticMode, dynamicReason: error?.dynamicReason,
|
|
445
467
|
} });
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {isControlError} from './navigation.mjs';
|
|
2
|
+
import {AsyncLocalStorage} from 'node:async_hooks';
|
|
3
|
+
import {randomBytes} from 'node:crypto';
|
|
4
|
+
import {createRequire} from 'node:module';
|
|
5
|
+
const scope=new AsyncLocalStorage(),rawFetch=globalThis.fetch;
|
|
6
|
+
let api;
|
|
7
|
+
try{api=createRequire(import.meta.url)('@opentelemetry/api');}catch{}
|
|
8
|
+
const endpoint=process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT||(process.env.OTEL_EXPORTER_OTLP_ENDPOINT?process.env.OTEL_EXPORTER_OTLP_ENDPOINT.replace(/\/$/,'')+'/v1/traces':undefined);
|
|
9
|
+
const protocol=process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL||process.env.OTEL_EXPORTER_OTLP_PROTOCOL;
|
|
10
|
+
const enabled=process.env.OTEL_SDK_DISABLED!=='true'&&endpoint&&(!protocol||protocol==='http/json');
|
|
11
|
+
const queue=[];let timer,flushing;
|
|
12
|
+
const epoch=BigInt(Date.now())*1000000n-process.hrtime.bigint();
|
|
13
|
+
const nanos=()=>String(epoch+process.hrtime.bigint());
|
|
14
|
+
export function parseTraceparent(value){
|
|
15
|
+
const match=/^00-([a-f0-9]{32})-([a-f0-9]{16})-([a-f0-9]{2})$/.exec(value||'');
|
|
16
|
+
return match&&!/^0+$/.test(match[1])&&!/^0+$/.test(match[2])?{traceId:match[1],spanId:match[2],traceFlags:parseInt(match[3],16)&1}:undefined;
|
|
17
|
+
}
|
|
18
|
+
export function currentTraceparent(){const value=scope.getStore();return value?`00-${value.traceId}-${value.spanId}-${value.traceFlags?'01':'00'}`:undefined;}
|
|
19
|
+
export function propagatedFetch(input,init){
|
|
20
|
+
const parent=currentTraceparent();if(!parent)return [input,init];
|
|
21
|
+
const headers=new Headers(init?.headers??(input instanceof Request?input.headers:undefined));
|
|
22
|
+
if(!headers.has('traceparent'))headers.set('traceparent',parent);
|
|
23
|
+
return [input,{...init,headers}];
|
|
24
|
+
}
|
|
25
|
+
function enqueue(span){
|
|
26
|
+
if(!enabled||!span.flags||queue.length>=1024)return;
|
|
27
|
+
queue.push(span);if(!timer)timer=setTimeout(()=>{timer=undefined;void flushTraces();},200).unref();
|
|
28
|
+
}
|
|
29
|
+
export async function flushTraces(){
|
|
30
|
+
if(flushing)return flushing;
|
|
31
|
+
if(!queue.length)return;
|
|
32
|
+
const headers={'content-type':'application/json'};
|
|
33
|
+
for(const part of (process.env.OTEL_EXPORTER_OTLP_TRACES_HEADERS||process.env.OTEL_EXPORTER_OTLP_HEADERS||'').split(',')){
|
|
34
|
+
const split=part.indexOf('=');if(split>0)try{headers[part.slice(0,split).trim()]=decodeURIComponent(part.slice(split+1).trim());}catch{}
|
|
35
|
+
}
|
|
36
|
+
const spans=queue.splice(0,128);
|
|
37
|
+
flushing=(async()=>{
|
|
38
|
+
const body=JSON.stringify({resourceSpans:[{resource:{attributes:[{key:'service.name',value:{stringValue:process.env.OTEL_SERVICE_NAME||'prnext'}}]},scopeSpans:[{scope:{name:'prnext.runtime'},spans}]}]});
|
|
39
|
+
for(let attempt=0;attempt<3;attempt++){
|
|
40
|
+
let retry=false;
|
|
41
|
+
try{
|
|
42
|
+
const response=await rawFetch(endpoint,{method:'POST',redirect:'error',headers,body,signal:AbortSignal.timeout(1000)});
|
|
43
|
+
retry=[429,502,503,504].includes(response.status);await response.body?.cancel();
|
|
44
|
+
}catch{retry=true;} // Telemetry failure never changes an application response.
|
|
45
|
+
if(!retry)break;
|
|
46
|
+
if(attempt<2)await new Promise(resolve=>setTimeout(resolve,100*2**attempt).unref());
|
|
47
|
+
}
|
|
48
|
+
})().finally(()=>{flushing=undefined;if(queue.length&&!timer)timer=setTimeout(()=>{timer=undefined;void flushTraces();},200).unref();});
|
|
49
|
+
return flushing;
|
|
50
|
+
}
|
|
51
|
+
export async function drainTraces(){
|
|
52
|
+
const deadline=Date.now()+2500;let timeout;
|
|
53
|
+
try{
|
|
54
|
+
await Promise.race([(async()=>{while((flushing||queue.length)&&Date.now()<deadline)await(flushing||flushTraces());})(),new Promise(resolve=>{timeout=setTimeout(resolve,2500);})]);
|
|
55
|
+
}finally{clearTimeout(timeout);clearTimeout(timer);timer=undefined;}
|
|
56
|
+
}
|
|
57
|
+
export function markTraceError(){const state=scope.getStore();if(state?.span)state.span.status={code:2};state?.sdkSpan?.setStatus({code:2});}
|
|
58
|
+
export async function withTrace(options,name,callback){
|
|
59
|
+
const parent=scope.getStore()||parseTraceparent(options.traceparent||new Headers(options.headers).get('traceparent'));
|
|
60
|
+
if(!enabled&&!api&&!parent)return callback();
|
|
61
|
+
let sdkSpan;
|
|
62
|
+
if(!enabled&&api){
|
|
63
|
+
const remote=parent?api.trace.setSpanContext(api.context.active(),{traceId:parent.traceId,spanId:parent.spanId,traceFlags:parent.traceFlags,isRemote:true}):api.context.active();
|
|
64
|
+
sdkSpan=api.trace.getTracer('prnext.runtime').startSpan(name,{kind:options.kind||1},remote);
|
|
65
|
+
const context=sdkSpan.spanContext();
|
|
66
|
+
if(!api.trace.isSpanContextValid(context)){sdkSpan.end();return callback();}
|
|
67
|
+
const run=()=>scope.run({...context,sdkSpan},callback);
|
|
68
|
+
try{return await api.context.with(api.trace.setSpan(remote,sdkSpan),run);}catch(error){if(!isControlError(error))sdkSpan.setStatus({code:2});throw error;}finally{sdkSpan.end();}
|
|
69
|
+
}
|
|
70
|
+
const span={traceId:parent?.traceId||randomBytes(16).toString('hex'),spanId:randomBytes(8).toString('hex'),parentSpanId:parent?.spanId,
|
|
71
|
+
flags:parent?.traceFlags??1,name,kind:options.kind||1,startTimeUnixNano:nanos(),attributes:[]};
|
|
72
|
+
if(options.route?.pattern)span.attributes.push({key:'http.route',value:{stringValue:options.route.pattern}});
|
|
73
|
+
const state={traceId:span.traceId,spanId:span.spanId,traceFlags:span.flags,span};
|
|
74
|
+
const run=()=>scope.run(state,async()=>{
|
|
75
|
+
try{const result=await callback();if(result?.status>=500)span.status={code:2};return result;}catch(error){if(!isControlError(error))span.status={code:2};throw error;}finally{span.endTimeUnixNano=nanos();enqueue(span);}
|
|
76
|
+
});
|
|
77
|
+
return api?api.context.with(api.trace.setSpanContext(api.context.active(),{traceId:state.traceId,spanId:state.spanId,traceFlags:state.traceFlags}),run):run();
|
|
78
|
+
}
|