@rangojs/router 0.0.0-experimental.145 → 0.0.0-experimental.146
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/dist/bin/rango.js +1 -40
- package/dist/vite/index.js +34 -9
- package/package.json +1 -1
- package/src/cache/cache-scope.ts +30 -1
- package/src/cache/handle-snapshot.ts +22 -1
- package/src/cache/shell-snapshot.ts +47 -0
- package/src/cache/types.ts +13 -0
- package/src/deps/ssr.ts +4 -1
- package/src/router/loader-resolution.ts +16 -0
- package/src/router/match-api.ts +9 -2
- package/src/router/match-handlers.ts +13 -0
- package/src/router/segment-resolution/mask-nested.ts +19 -3
- package/src/rsc/rsc-rendering.ts +54 -2
- package/src/rsc/shell-capture.ts +94 -4
- package/src/segment-system.tsx +49 -19
- package/src/server/request-context.ts +47 -0
- package/src/ssr/index.tsx +104 -13
- package/src/ssr/inject-rsc-eager.ts +2 -2
- package/src/ssr/preinit-client-references.ts +106 -0
- package/src/vite/index.ts +1 -0
- package/src/vite/plugin-types.ts +33 -0
- package/src/vite/plugins/virtual-entries.ts +37 -4
- package/src/vite/rango.ts +10 -2
- package/src/vite/utils/shared-utils.ts +4 -2
package/dist/bin/rango.js
CHANGED
|
@@ -1394,7 +1394,7 @@ var init_generate_route_types = __esm({
|
|
|
1394
1394
|
function getVirtualVersionContent(version) {
|
|
1395
1395
|
return `export const VERSION = ${JSON.stringify(version)};`;
|
|
1396
1396
|
}
|
|
1397
|
-
var VIRTUAL_ENTRY_BROWSER,
|
|
1397
|
+
var VIRTUAL_ENTRY_BROWSER, VIRTUAL_IDS;
|
|
1398
1398
|
var init_virtual_entries = __esm({
|
|
1399
1399
|
"src/vite/plugins/virtual-entries.ts"() {
|
|
1400
1400
|
"use strict";
|
|
@@ -1434,45 +1434,6 @@ async function initializeApp() {
|
|
|
1434
1434
|
}
|
|
1435
1435
|
|
|
1436
1436
|
initializeApp().catch(console.error);
|
|
1437
|
-
`.trim();
|
|
1438
|
-
VIRTUAL_ENTRY_SSR = `
|
|
1439
|
-
import { createFromReadableStream } from "@rangojs/router/internal/deps/ssr";
|
|
1440
|
-
import { renderToReadableStream, resume } from "react-dom/server.edge";
|
|
1441
|
-
import { prerender } from "react-dom/static.edge";
|
|
1442
|
-
import { injectRSCPayload } from "@rangojs/router/internal/deps/html-stream-server";
|
|
1443
|
-
import {
|
|
1444
|
-
createSSRHandler,
|
|
1445
|
-
createShellCaptureHandler,
|
|
1446
|
-
createShellResumeHandler,
|
|
1447
|
-
} from "@rangojs/router/ssr";
|
|
1448
|
-
|
|
1449
|
-
export const renderHTML = createSSRHandler({
|
|
1450
|
-
createFromReadableStream,
|
|
1451
|
-
renderToReadableStream,
|
|
1452
|
-
injectRSCPayload,
|
|
1453
|
-
loadBootstrapScriptContent: () =>
|
|
1454
|
-
import.meta.viteRsc.loadBootstrapScriptContent("index"),
|
|
1455
|
-
});
|
|
1456
|
-
|
|
1457
|
-
export const captureShellHTML = createShellCaptureHandler({
|
|
1458
|
-
createFromReadableStream,
|
|
1459
|
-
renderToReadableStream,
|
|
1460
|
-
injectRSCPayload,
|
|
1461
|
-
prerender,
|
|
1462
|
-
resume,
|
|
1463
|
-
loadBootstrapScriptContent: () =>
|
|
1464
|
-
import.meta.viteRsc.loadBootstrapScriptContent("index"),
|
|
1465
|
-
});
|
|
1466
|
-
|
|
1467
|
-
export const resumeShellHTML = createShellResumeHandler({
|
|
1468
|
-
createFromReadableStream,
|
|
1469
|
-
renderToReadableStream,
|
|
1470
|
-
injectRSCPayload,
|
|
1471
|
-
prerender,
|
|
1472
|
-
resume,
|
|
1473
|
-
loadBootstrapScriptContent: () =>
|
|
1474
|
-
import.meta.viteRsc.loadBootstrapScriptContent("index"),
|
|
1475
|
-
});
|
|
1476
1437
|
`.trim();
|
|
1477
1438
|
VIRTUAL_IDS = {
|
|
1478
1439
|
browser: "virtual:rsc-router/entry.browser.js",
|
package/dist/vite/index.js
CHANGED
|
@@ -2210,21 +2210,35 @@ async function initializeApp() {
|
|
|
2210
2210
|
|
|
2211
2211
|
initializeApp().catch(console.error);
|
|
2212
2212
|
`.trim();
|
|
2213
|
-
|
|
2214
|
-
|
|
2213
|
+
function getVirtualEntrySSR(headScripts = "preinit") {
|
|
2214
|
+
const preinit = headScripts !== "preload";
|
|
2215
|
+
const depsImportNames = preinit ? "createFromReadableStream,\n setOnClientReference," : "createFromReadableStream,";
|
|
2216
|
+
const ssrImportNames = preinit ? "\n installClientReferencePreinit," : "";
|
|
2217
|
+
const install = preinit ? `
|
|
2218
|
+
// Upgrade client-reference modulepreload hints to executing module scripts in
|
|
2219
|
+
// the document head, for every render pass (live SSR, shell capture, resume).
|
|
2220
|
+
// See src/ssr/preinit-client-references.ts for the full rationale.
|
|
2221
|
+
installClientReferencePreinit(setOnClientReference);
|
|
2222
|
+
` : "";
|
|
2223
|
+
const hs = JSON.stringify(headScripts);
|
|
2224
|
+
return `
|
|
2225
|
+
import {
|
|
2226
|
+
${depsImportNames}
|
|
2227
|
+
} from "@rangojs/router/internal/deps/ssr";
|
|
2215
2228
|
import { renderToReadableStream, resume } from "react-dom/server.edge";
|
|
2216
2229
|
import { prerender } from "react-dom/static.edge";
|
|
2217
2230
|
import { injectRSCPayload } from "@rangojs/router/internal/deps/html-stream-server";
|
|
2218
2231
|
import {
|
|
2219
2232
|
createSSRHandler,
|
|
2220
2233
|
createShellCaptureHandler,
|
|
2221
|
-
createShellResumeHandler
|
|
2234
|
+
createShellResumeHandler,${ssrImportNames}
|
|
2222
2235
|
} from "@rangojs/router/ssr";
|
|
2223
|
-
|
|
2236
|
+
${install}
|
|
2224
2237
|
export const renderHTML = createSSRHandler({
|
|
2225
2238
|
createFromReadableStream,
|
|
2226
2239
|
renderToReadableStream,
|
|
2227
2240
|
injectRSCPayload,
|
|
2241
|
+
headScripts: ${hs},
|
|
2228
2242
|
loadBootstrapScriptContent: () =>
|
|
2229
2243
|
import.meta.viteRsc.loadBootstrapScriptContent("index"),
|
|
2230
2244
|
});
|
|
@@ -2235,6 +2249,7 @@ export const captureShellHTML = createShellCaptureHandler({
|
|
|
2235
2249
|
injectRSCPayload,
|
|
2236
2250
|
prerender,
|
|
2237
2251
|
resume,
|
|
2252
|
+
headScripts: ${hs},
|
|
2238
2253
|
loadBootstrapScriptContent: () =>
|
|
2239
2254
|
import.meta.viteRsc.loadBootstrapScriptContent("index"),
|
|
2240
2255
|
});
|
|
@@ -2245,10 +2260,12 @@ export const resumeShellHTML = createShellResumeHandler({
|
|
|
2245
2260
|
injectRSCPayload,
|
|
2246
2261
|
prerender,
|
|
2247
2262
|
resume,
|
|
2263
|
+
headScripts: ${hs},
|
|
2248
2264
|
loadBootstrapScriptContent: () =>
|
|
2249
2265
|
import.meta.viteRsc.loadBootstrapScriptContent("index"),
|
|
2250
2266
|
});
|
|
2251
2267
|
`.trim();
|
|
2268
|
+
}
|
|
2252
2269
|
var RSC_ENTRY_BOOTSTRAP_IMPORTS = [
|
|
2253
2270
|
"virtual:rsc-router/routes-manifest",
|
|
2254
2271
|
"virtual:rsc-router/loader-manifest"
|
|
@@ -2393,7 +2410,7 @@ import { resolve } from "node:path";
|
|
|
2393
2410
|
// package.json
|
|
2394
2411
|
var package_default = {
|
|
2395
2412
|
name: "@rangojs/router",
|
|
2396
|
-
version: "0.0.0-experimental.
|
|
2413
|
+
version: "0.0.0-experimental.146",
|
|
2397
2414
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
2398
2415
|
keywords: [
|
|
2399
2416
|
"react",
|
|
@@ -4017,13 +4034,13 @@ function normalizeHostRouterEntry(rawInput, root, exists) {
|
|
|
4017
4034
|
}
|
|
4018
4035
|
return exists(resolve4(root, raw)) ? "./" + raw : raw;
|
|
4019
4036
|
}
|
|
4020
|
-
function createVirtualEntriesPlugin(entries, routerPathRef) {
|
|
4037
|
+
function createVirtualEntriesPlugin(entries, routerPathRef, options) {
|
|
4021
4038
|
const virtualModules = {};
|
|
4022
4039
|
if (entries.client === VIRTUAL_IDS.browser) {
|
|
4023
4040
|
virtualModules[VIRTUAL_IDS.browser] = VIRTUAL_ENTRY_BROWSER;
|
|
4024
4041
|
}
|
|
4025
4042
|
if (entries.ssr === VIRTUAL_IDS.ssr) {
|
|
4026
|
-
virtualModules[VIRTUAL_IDS.ssr] =
|
|
4043
|
+
virtualModules[VIRTUAL_IDS.ssr] = getVirtualEntrySSR(options?.headScripts);
|
|
4027
4044
|
}
|
|
4028
4045
|
const knownIds = new Set(Object.keys(virtualModules));
|
|
4029
4046
|
if (entries.rsc === VIRTUAL_IDS.rsc) {
|
|
@@ -7893,7 +7910,11 @@ async function rango(options) {
|
|
|
7893
7910
|
}
|
|
7894
7911
|
}
|
|
7895
7912
|
});
|
|
7896
|
-
plugins.push(
|
|
7913
|
+
plugins.push(
|
|
7914
|
+
createVirtualEntriesPlugin(finalEntries, void 0, {
|
|
7915
|
+
headScripts: resolvedOptions.headScripts
|
|
7916
|
+
})
|
|
7917
|
+
);
|
|
7897
7918
|
plugins.push(performanceTracksPlugin());
|
|
7898
7919
|
plugins.push(
|
|
7899
7920
|
rsc({
|
|
@@ -8073,7 +8094,11 @@ If this is a multi-app host router, export a createHostRouter() instance and set
|
|
|
8073
8094
|
}
|
|
8074
8095
|
}
|
|
8075
8096
|
});
|
|
8076
|
-
plugins.push(
|
|
8097
|
+
plugins.push(
|
|
8098
|
+
createVirtualEntriesPlugin(finalEntries, routerRef, {
|
|
8099
|
+
headScripts: resolvedOptions.headScripts
|
|
8100
|
+
})
|
|
8101
|
+
);
|
|
8077
8102
|
plugins.push(performanceTracksPlugin());
|
|
8078
8103
|
plugins.push(
|
|
8079
8104
|
rsc({
|
package/package.json
CHANGED
package/src/cache/cache-scope.ts
CHANGED
|
@@ -452,7 +452,11 @@ export class CacheScope {
|
|
|
452
452
|
}
|
|
453
453
|
|
|
454
454
|
// Collect handle data for non-loader segments only
|
|
455
|
-
const handles = captureHandles(
|
|
455
|
+
const handles = captureHandles(
|
|
456
|
+
nonLoaderSegments,
|
|
457
|
+
handleStore,
|
|
458
|
+
requestCtx._shellCaptureLoaderHandleValues,
|
|
459
|
+
);
|
|
456
460
|
|
|
457
461
|
try {
|
|
458
462
|
if (INTERNAL_RANGO_DEBUG) {
|
|
@@ -512,3 +516,28 @@ export function createCacheScope(
|
|
|
512
516
|
if (!config) return parent; // No config, inherit parent
|
|
513
517
|
return new CacheScope(config.options, parent);
|
|
514
518
|
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Shell fast path: when the route tree derived NO cache scope and the current
|
|
522
|
+
* request context carries the `_shellImplicitCache` marker (a shell capture,
|
|
523
|
+
* or a HIT tail serving an eligible entry), substitute an implicit doc-level
|
|
524
|
+
* scope so withCacheLookup/withCacheStore treat the WHOLE matched route as a
|
|
525
|
+
* cache() boundary — the shell entry IS a cache() of the handler layer, with
|
|
526
|
+
* loaders as the live carve-outs (resolveFreshLoadersAndYield).
|
|
527
|
+
*
|
|
528
|
+
* An existing scope — including an explicit cache(false) opt-out — always
|
|
529
|
+
* wins: the consumer's cache() semantics (their ttl/swr/store/condition) are
|
|
530
|
+
* never overridden, and cache(false) keeps the tail on the full handler
|
|
531
|
+
* re-run path.
|
|
532
|
+
*/
|
|
533
|
+
export function resolveShellImplicitCacheScope(
|
|
534
|
+
scope: CacheScope | null,
|
|
535
|
+
): CacheScope | null {
|
|
536
|
+
if (scope) return scope;
|
|
537
|
+
const marker = getRequestContext()?._shellImplicitCache;
|
|
538
|
+
if (!marker) return null;
|
|
539
|
+
return new CacheScope(
|
|
540
|
+
{ ttl: marker.ttl, swr: marker.swr, store: marker.store },
|
|
541
|
+
null,
|
|
542
|
+
);
|
|
543
|
+
}
|
|
@@ -83,14 +83,35 @@ export async function decodeHandleValue<T>(encoded: string): Promise<T | null> {
|
|
|
83
83
|
/**
|
|
84
84
|
* Capture handle data for a set of segments from the handle store.
|
|
85
85
|
* Used when caching segments to preserve their handle data.
|
|
86
|
+
*
|
|
87
|
+
* `exclude` (shell captures: RequestContext._shellCaptureLoaderHandleValues)
|
|
88
|
+
* drops DSL-loader-scoped push values from the CACHE WRITE only: loaders
|
|
89
|
+
* re-run fresh on every HIT, so replaying their captured values would
|
|
90
|
+
* duplicate the fresh push — and their masked nested promises would stall the
|
|
91
|
+
* Flight handle encode to its timeout. Threaded as an explicit argument so
|
|
92
|
+
* every other getDataForSegment consumer (the render-barrier snapshot,
|
|
93
|
+
* prerender) provably sees every push.
|
|
86
94
|
*/
|
|
87
95
|
export function captureHandles(
|
|
88
96
|
segments: ResolvedSegment[],
|
|
89
97
|
handleStore: HandleStore,
|
|
98
|
+
exclude?: WeakSet<object>,
|
|
90
99
|
): Record<string, SegmentHandleData> {
|
|
91
100
|
const handles: Record<string, SegmentHandleData> = {};
|
|
92
101
|
for (const seg of segments) {
|
|
93
|
-
|
|
102
|
+
const data = handleStore.getDataForSegment(seg.id);
|
|
103
|
+
if (!exclude) {
|
|
104
|
+
handles[seg.id] = data;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const filtered: SegmentHandleData = {};
|
|
108
|
+
for (const [handleName, values] of Object.entries(data)) {
|
|
109
|
+
const kept = values.filter(
|
|
110
|
+
(v) => typeof v !== "object" || v === null || !exclude.has(v),
|
|
111
|
+
);
|
|
112
|
+
if (kept.length > 0) filtered[handleName] = kept;
|
|
113
|
+
}
|
|
114
|
+
handles[seg.id] = filtered;
|
|
94
115
|
}
|
|
95
116
|
return handles;
|
|
96
117
|
}
|
|
@@ -110,6 +110,19 @@ export class RecordingShellStore<
|
|
|
110
110
|
this.writes.push(p);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Record a segment-family write into the snapshot WITHOUT touching the inner
|
|
115
|
+
* store. The shell fast path's implicit doc-cache scope writes through this
|
|
116
|
+
* (via {@link SnapshotOnlySegmentStore}): the recorded doc entry must ride
|
|
117
|
+
* ONLY inside the shell entry — a passthrough write would leave a doc-keyed
|
|
118
|
+
* entry in the real store that the NEXT capture's lookup would hit, replaying
|
|
119
|
+
* the previous generation's segments instead of re-running handlers (breaking
|
|
120
|
+
* SWR recapture freshness).
|
|
121
|
+
*/
|
|
122
|
+
recordSegmentWrite(key: string, data: CachedEntryData): void {
|
|
123
|
+
this.record("segment", key, data);
|
|
124
|
+
}
|
|
125
|
+
|
|
113
126
|
/**
|
|
114
127
|
* Await the tracked deferred writes so their records are present before drain.
|
|
115
128
|
* Drains ITERATIVELY: a write task can schedule a NESTED write (the ring-3
|
|
@@ -245,6 +258,40 @@ export function getRecordingStore<TEnv>(
|
|
|
245
258
|
return store instanceof RecordingShellStore ? store : undefined;
|
|
246
259
|
}
|
|
247
260
|
|
|
261
|
+
/**
|
|
262
|
+
* The store the shell fast path's IMPLICIT doc-cache scope resolves during a
|
|
263
|
+
* capture: reads pass through the recording store (a real-store hit is
|
|
264
|
+
* recorded, exactly like any capture read), but segment WRITES are recorded
|
|
265
|
+
* into the snapshot only — see {@link RecordingShellStore.recordSegmentWrite}
|
|
266
|
+
* for why passthrough would break SWR recapture. Routes with their OWN
|
|
267
|
+
* cache() config never see this store (their scope resolves the app-level
|
|
268
|
+
* recording store and keeps today's record-and-write behavior).
|
|
269
|
+
*/
|
|
270
|
+
export class SnapshotOnlySegmentStore<
|
|
271
|
+
TEnv = unknown,
|
|
272
|
+
> implements SegmentCacheStore<TEnv> {
|
|
273
|
+
constructor(private readonly recording: RecordingShellStore<TEnv>) {}
|
|
274
|
+
|
|
275
|
+
get defaults(): SegmentCacheStore<TEnv>["defaults"] {
|
|
276
|
+
return this.recording.defaults;
|
|
277
|
+
}
|
|
278
|
+
get keyGenerator(): SegmentCacheStore<TEnv>["keyGenerator"] {
|
|
279
|
+
return this.recording.keyGenerator;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async get(key: string): Promise<CacheGetResult | null> {
|
|
283
|
+
return this.recording.get(key);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async set(key: string, data: CachedEntryData): Promise<void> {
|
|
287
|
+
this.recording.recordSegmentWrite(key, data);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async delete(key: string): Promise<boolean> {
|
|
291
|
+
return this.recording.delete(key);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
248
295
|
/**
|
|
249
296
|
* Materialize the loader-family seed from a shell snapshot for a HIT's tail
|
|
250
297
|
* render: Flight-deserialize each recorded (promise-elided) bake-lane
|
package/src/cache/types.ts
CHANGED
|
@@ -280,6 +280,19 @@ export interface ShellCacheEntry {
|
|
|
280
280
|
* heals it. See docs/design/ppr-shell-resume.md ("the capture data snapshot").
|
|
281
281
|
*/
|
|
282
282
|
snapshot?: ShellSnapshotRecord[];
|
|
283
|
+
/**
|
|
284
|
+
* True when the capture's HANDLER layer declared per-request liveness: a
|
|
285
|
+
* handle pushed OUTSIDE a DSL loader scope carried a nested thenable (the
|
|
286
|
+
* capture mask turns it into a never-filling hole), such a push was still
|
|
287
|
+
* pending when the entry was written, or a handler-invoked loader
|
|
288
|
+
* (ctx.use(loader) from a handler body — the consumption lane, #672)
|
|
289
|
+
* executed during the capture. The serve tail then must NOT take the
|
|
290
|
+
* handler-free fast path (the implicit doc-cache hit): only a handler
|
|
291
|
+
* re-run can mint that hole's live promise or refresh that consumed value.
|
|
292
|
+
* DSL-loader pushes never set this — loaders re-run fresh on every HIT, so
|
|
293
|
+
* their holes always fill.
|
|
294
|
+
*/
|
|
295
|
+
handlerLiveHoles?: boolean;
|
|
283
296
|
/** Epoch ms when the shell was captured. */
|
|
284
297
|
createdAt: number;
|
|
285
298
|
}
|
package/src/deps/ssr.ts
CHANGED
|
@@ -430,6 +430,22 @@ function createLoaderExecutor<TEnv>(
|
|
|
430
430
|
// nested deps inherit isDslLoader=false only when the CHAIN started in a
|
|
431
431
|
// handler; a chain started by the segment funnel stays DSL (the loader
|
|
432
432
|
// scope ALS survives the body's awaits).
|
|
433
|
+
// Shell fast path eligibility: a HANDLER-invoked loader executing during a
|
|
434
|
+
// capture is handler-layer dynamism — on a handler-free (replayed) HIT it
|
|
435
|
+
// would never re-run, freezing its consumption-lane value (#672's "fresh
|
|
436
|
+
// per serve" slot shape). Mark the capture; the entry declines the fast
|
|
437
|
+
// path and keeps the full tail. DSL loaders re-run on every HIT and never
|
|
438
|
+
// set this.
|
|
439
|
+
if (!isDslLoader) {
|
|
440
|
+
const captureCtx = _getRequestContext();
|
|
441
|
+
if (
|
|
442
|
+
captureCtx?._shellCaptureRun &&
|
|
443
|
+
captureCtx._shellCaptureHandleLiveness
|
|
444
|
+
) {
|
|
445
|
+
captureCtx._shellCaptureHandleLiveness.handlerInvokedLoader = true;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
433
449
|
const promise = observePhase(PHASES.loader(loader.$$id), () =>
|
|
434
450
|
Promise.resolve(
|
|
435
451
|
runInsideLoaderBodyScope(
|
package/src/router/match-api.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
CacheScope,
|
|
3
|
+
createCacheScope,
|
|
4
|
+
resolveShellImplicitCacheScope,
|
|
5
|
+
} from "../cache/cache-scope.js";
|
|
2
6
|
import { RouteNotFoundError } from "../errors";
|
|
3
7
|
import {
|
|
4
8
|
createErrorInfo,
|
|
@@ -180,7 +184,10 @@ export async function createMatchContextForFull<TEnv>(
|
|
|
180
184
|
},
|
|
181
185
|
isSameRouteNavigation: false,
|
|
182
186
|
interceptResult: null,
|
|
183
|
-
|
|
187
|
+
// Shell fast path: a capture or an eligible HIT tail may substitute an
|
|
188
|
+
// implicit doc-level scope (marker-gated; a route-derived scope wins).
|
|
189
|
+
// Full matches only — partial navigations never serve a shell.
|
|
190
|
+
cacheScope: resolveShellImplicitCacheScope(snapshot.cacheScope),
|
|
184
191
|
isIntercept: false,
|
|
185
192
|
actionContext: undefined,
|
|
186
193
|
isAction: false,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
+
import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
|
|
2
3
|
import { sanitizeError } from "../errors";
|
|
3
4
|
import type { ErrorInfo, ErrorPhase, MatchResult } from "../types";
|
|
4
5
|
import type {
|
|
@@ -291,7 +292,13 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
291
292
|
});
|
|
292
293
|
emitter.start();
|
|
293
294
|
|
|
295
|
+
const ctxBuildStart = INTERNAL_RANGO_DEBUG ? performance.now() : 0;
|
|
294
296
|
const result = await createMatchContextForFull(request, env);
|
|
297
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
298
|
+
console.log(
|
|
299
|
+
`[Server][match] context built +${Math.round(performance.now() - ctxBuildStart)}ms (abs ${Math.round(performance.now())})`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
295
302
|
|
|
296
303
|
if ("type" in result && result.type === "redirect") {
|
|
297
304
|
emitter.end(0, false);
|
|
@@ -310,7 +317,13 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
310
317
|
try {
|
|
311
318
|
const state = createPipelineState();
|
|
312
319
|
const pipeline = createMatchPartialPipeline(ctx, state);
|
|
320
|
+
const pipeStart = INTERNAL_RANGO_DEBUG ? performance.now() : 0;
|
|
313
321
|
const matchResult = await collectMatchResult(pipeline, ctx, state);
|
|
322
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
323
|
+
console.log(
|
|
324
|
+
`[Server][match] pipeline collected +${Math.round(performance.now() - pipeStart)}ms (abs ${Math.round(performance.now())})`,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
314
327
|
if (hasTelemetry || cacheSignalEnabled) {
|
|
315
328
|
const signalSegments = buildSignal(ctx.routeKey, state);
|
|
316
329
|
recordSignalIfEnabled(signalSegments);
|
|
@@ -51,11 +51,27 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
51
51
|
* rule, semantic-matrix PPR3) shares the raw container and must keep real
|
|
52
52
|
* values. Cycles are preserved as cycles in the copy.
|
|
53
53
|
*/
|
|
54
|
+
/**
|
|
55
|
+
* Optional single-walk report for maskNestedContainerThenables: `thenable`
|
|
56
|
+
* flips true when the walk masked at least one thenable. The shell fast path
|
|
57
|
+
* reads it at handle-push time — a pushed container with a nested thenable
|
|
58
|
+
* declares per-request data, and a shell entry whose HANDLER layer made such
|
|
59
|
+
* a declaration cannot be served handler-free (the hole would never fill).
|
|
60
|
+
* Same single-pass shape as elideLoaderContainer's `hasHole`.
|
|
61
|
+
*/
|
|
62
|
+
export interface MaskReport {
|
|
63
|
+
thenable: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
54
66
|
export function maskNestedContainerThenables(
|
|
55
67
|
value: unknown,
|
|
56
68
|
seen: Map<object, unknown> = new Map(),
|
|
69
|
+
report?: MaskReport,
|
|
57
70
|
): unknown {
|
|
58
|
-
if (isThenable(value))
|
|
71
|
+
if (isThenable(value)) {
|
|
72
|
+
if (report) report.thenable = true;
|
|
73
|
+
return createMaskedLoaderPromise();
|
|
74
|
+
}
|
|
59
75
|
|
|
60
76
|
if (Array.isArray(value)) {
|
|
61
77
|
const cached = seen.get(value);
|
|
@@ -63,7 +79,7 @@ export function maskNestedContainerThenables(
|
|
|
63
79
|
const out: unknown[] = new Array(value.length);
|
|
64
80
|
seen.set(value, out);
|
|
65
81
|
for (let i = 0; i < value.length; i++) {
|
|
66
|
-
out[i] = maskNestedContainerThenables(value[i], seen);
|
|
82
|
+
out[i] = maskNestedContainerThenables(value[i], seen, report);
|
|
67
83
|
}
|
|
68
84
|
return out;
|
|
69
85
|
}
|
|
@@ -74,7 +90,7 @@ export function maskNestedContainerThenables(
|
|
|
74
90
|
const out: Record<string, unknown> = {};
|
|
75
91
|
seen.set(value, out);
|
|
76
92
|
for (const key of Object.keys(value)) {
|
|
77
|
-
out[key] = maskNestedContainerThenables(value[key], seen);
|
|
93
|
+
out[key] = maskNestedContainerThenables(value[key], seen, report);
|
|
78
94
|
}
|
|
79
95
|
return out;
|
|
80
96
|
}
|
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -493,7 +493,13 @@ function serveShellHit(
|
|
|
493
493
|
const renderTail = async (
|
|
494
494
|
activeCtx: RequestContext<any>,
|
|
495
495
|
): Promise<ReadableStream<Uint8Array> | { redirect: string }> => {
|
|
496
|
+
const matchStart = INTERNAL_RANGO_DEBUG ? performance.now() : 0;
|
|
496
497
|
const match = await ctx.router.match(request, { env });
|
|
498
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
499
|
+
console.log(
|
|
500
|
+
`[Server][ppr] shell HIT: tail match done +${Math.round(performance.now() - matchStart)}ms (abs ${Math.round(performance.now())}, started ${Math.round(matchStart)})`,
|
|
501
|
+
);
|
|
502
|
+
}
|
|
497
503
|
if (match.redirect) return { redirect: match.redirect };
|
|
498
504
|
setRequestContextParams(match.params, match.routeName);
|
|
499
505
|
const payload = buildFullPayload(match, ctx, url, activeCtx, handleStore);
|
|
@@ -530,7 +536,7 @@ function serveShellHit(
|
|
|
530
536
|
if (!first) {
|
|
531
537
|
first = true;
|
|
532
538
|
console.log(
|
|
533
|
-
`[Server][ppr] flight render: first chunk +${Math.round(performance.now() - tapStart)}ms`,
|
|
539
|
+
`[Server][ppr] flight render: first chunk +${Math.round(performance.now() - tapStart)}ms (abs ${Math.round(performance.now())})`,
|
|
534
540
|
);
|
|
535
541
|
}
|
|
536
542
|
controller.enqueue(chunk);
|
|
@@ -571,8 +577,38 @@ function serveShellHit(
|
|
|
571
577
|
// decode into a seed Map for the resolveLoaderData overlay, so the
|
|
572
578
|
// payload's baked container bytes match the frozen prelude while the
|
|
573
579
|
// hole-marker paths keep the fresh run's live nested promises.
|
|
580
|
+
const seedStart = INTERNAL_RANGO_DEBUG ? performance.now() : 0;
|
|
574
581
|
const loaderSeed = await buildShellLoaderSeed(entry.snapshot);
|
|
582
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
583
|
+
console.log(
|
|
584
|
+
`[Server][ppr] shell HIT: loader seed built +${Math.round(performance.now() - seedStart)}ms (abs ${Math.round(performance.now())})`,
|
|
585
|
+
);
|
|
586
|
+
}
|
|
575
587
|
if (loaderSeed) seededCtx._shellLoaderSeed = loaderSeed;
|
|
588
|
+
// Shell fast path (serve side): when the capture recorded the implicit
|
|
589
|
+
// doc segment record and the handler layer declared no liveness, arm the
|
|
590
|
+
// implicit scope on the seeded context — the tail match's cache lookup
|
|
591
|
+
// then HITs the SeededShellStore's doc entry and the whole handler layer
|
|
592
|
+
// is REPLAYED, not re-executed (loaders still run fresh via
|
|
593
|
+
// resolveFreshLoadersAndYield; per-request payload metadata is rebuilt
|
|
594
|
+
// by buildFullPayload as always). A route with handler-live holes, a
|
|
595
|
+
// route-derived cache scope, or a missing/corrupt record degrades to
|
|
596
|
+
// the full tail (handler re-run — today's behavior) automatically.
|
|
597
|
+
if (!entry.handlerLiveHoles) {
|
|
598
|
+
seededCtx._shellImplicitCache = {
|
|
599
|
+
ttl: descriptor.ttl,
|
|
600
|
+
swr: descriptor.swr,
|
|
601
|
+
};
|
|
602
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
603
|
+
console.log(
|
|
604
|
+
`[Server][ppr] shell HIT: fast path armed (implicit doc cache) (abs ${Math.round(performance.now())})`,
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
} else if (INTERNAL_RANGO_DEBUG) {
|
|
608
|
+
console.log(
|
|
609
|
+
`[Server][ppr] shell HIT: fast path declined — handler-live holes; tail re-runs handlers (abs ${Math.round(performance.now())})`,
|
|
610
|
+
);
|
|
611
|
+
}
|
|
576
612
|
return runWithRequestContext(seededCtx, () => renderTail(seededCtx));
|
|
577
613
|
}
|
|
578
614
|
return renderTail(reqCtx);
|
|
@@ -592,9 +628,15 @@ function serveShellHit(
|
|
|
592
628
|
}
|
|
593
629
|
try {
|
|
594
630
|
const tail = await tailPromise;
|
|
631
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
632
|
+
console.log(
|
|
633
|
+
`[Server][ppr] shell HIT: tail stream handed over +${Math.round(performance.now() - serveStart)}ms (abs ${Math.round(performance.now())})`,
|
|
634
|
+
);
|
|
635
|
+
}
|
|
595
636
|
if (tail instanceof ReadableStream) {
|
|
596
637
|
const reader = tail.getReader();
|
|
597
638
|
let firstTailChunk = true;
|
|
639
|
+
let tailBytes = 0;
|
|
598
640
|
try {
|
|
599
641
|
for (;;) {
|
|
600
642
|
const { done, value } = await reader.read();
|
|
@@ -602,14 +644,24 @@ function serveShellHit(
|
|
|
602
644
|
if (INTERNAL_RANGO_DEBUG && firstTailChunk) {
|
|
603
645
|
firstTailChunk = false;
|
|
604
646
|
console.log(
|
|
605
|
-
`[Server][ppr] shell HIT: first tail chunk on the wire +${Math.round(performance.now() - serveStart)}ms`,
|
|
647
|
+
`[Server][ppr] shell HIT: first tail chunk on the wire +${Math.round(performance.now() - serveStart)}ms (abs ${Math.round(performance.now())})`,
|
|
606
648
|
);
|
|
607
649
|
}
|
|
650
|
+
if (INTERNAL_RANGO_DEBUG) tailBytes += value.length;
|
|
608
651
|
controller.enqueue(value);
|
|
609
652
|
}
|
|
610
653
|
} finally {
|
|
611
654
|
reader.releaseLock();
|
|
612
655
|
}
|
|
656
|
+
// Bounds the post-header work Server-Timing structurally cannot see:
|
|
657
|
+
// the HIT commits headers at the flush, so ALL live-tail time (match,
|
|
658
|
+
// loaders, Flight, resume) happens inside the response body. This
|
|
659
|
+
// line plus the [Server][segments] build logs narrate that window.
|
|
660
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
661
|
+
console.log(
|
|
662
|
+
`[Server][ppr] shell HIT: tail complete +${Math.round(performance.now() - serveStart)}ms (${tailBytes}b)`,
|
|
663
|
+
);
|
|
664
|
+
}
|
|
613
665
|
} else {
|
|
614
666
|
// Defensive, near-unreachable: a redirecting match cannot have captured
|
|
615
667
|
// a shell (capture bails on redirects), so a HIT on a redirecting URL
|