@rangojs/router 0.5.2 → 0.7.0
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 +343 -125
- package/dist/types/browser/react/use-router.d.ts +10 -3
- package/dist/types/browser/react/use-search-params.d.ts +57 -10
- package/dist/types/browser/types.d.ts +22 -0
- package/dist/types/build/merge-full-manifests.d.ts +3 -0
- package/dist/types/build/route-trie.d.ts +4 -73
- package/dist/types/build/route-types/per-module-writer.d.ts +6 -4
- package/dist/types/build/route-types/router-processing.d.ts +2 -3
- package/dist/types/cache/cache-exec-scope.d.ts +31 -0
- package/dist/types/cache/taint.d.ts +12 -6
- package/dist/types/client-urls/client-root.d.ts +38 -0
- package/dist/types/client-urls/client-urls.d.ts +5 -0
- package/dist/types/client-urls/navigation.d.ts +38 -0
- package/dist/types/client-urls/revalidation-protocol.d.ts +25 -0
- package/dist/types/client-urls/server-projection.d.ts +71 -0
- package/dist/types/client-urls/types.d.ts +147 -0
- package/dist/types/client.d.ts +12 -4
- package/dist/types/client.rsc.d.ts +4 -1
- package/dist/types/decode-loader-results.d.ts +37 -0
- package/dist/types/errors.d.ts +1 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/loader-redirect.d.ts +27 -0
- package/dist/types/outlet-context.d.ts +12 -0
- package/dist/types/outlet-provider.d.ts +3 -1
- package/dist/types/redirect-origin.d.ts +4 -0
- package/dist/types/route-content-wrapper.d.ts +42 -1
- package/dist/types/route-definition/helpers-types.d.ts +13 -2
- package/dist/types/router/error-handling.d.ts +35 -1
- package/dist/types/router/intercept-resolution.d.ts +12 -0
- package/dist/types/router/loader-resolution.d.ts +24 -2
- package/dist/types/router/revalidation.d.ts +7 -0
- package/dist/types/router/route-trie-builder.d.ts +77 -0
- package/dist/types/router/router-interfaces.d.ts +20 -0
- package/dist/types/router/segment-resolution/helpers.d.ts +1 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -9
- package/dist/types/router/trie-matching.d.ts +1 -1
- package/dist/types/rsc/manifest-init.d.ts +5 -5
- package/dist/types/rsc/shell-capture.d.ts +9 -0
- package/dist/types/rsc/shell-serve.d.ts +11 -0
- package/dist/types/rsc/types.d.ts +30 -0
- package/dist/types/segment-system.d.ts +2 -0
- package/dist/types/server/context.d.ts +10 -0
- package/dist/types/server/handle-store.d.ts +34 -3
- package/dist/types/server/request-context.d.ts +11 -1
- package/dist/types/server.d.ts +1 -0
- package/dist/types/ssr/index.d.ts +22 -0
- package/dist/types/ssr/ssr-root.d.ts +10 -0
- package/dist/types/testing/dom.entry.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +16 -6
- package/dist/types/testing/run-loader.d.ts +9 -0
- package/dist/types/types/boundaries.d.ts +22 -0
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/loader-types.d.ts +57 -5
- package/dist/types/types/segments.d.ts +7 -0
- package/dist/types/urls/path-helper-types.d.ts +13 -4
- package/dist/types/vite/discovery/client-urls-projection.d.ts +53 -0
- package/dist/types/vite/discovery/discover-routers.d.ts +1 -1
- package/dist/types/vite/discovery/state.d.ts +8 -1
- package/dist/types/vite/plugins/client-ref-dedup.d.ts +0 -11
- package/dist/vite/index.js +6159 -2959
- package/package.json +6 -5
- package/skills/breadcrumbs/SKILL.md +39 -9
- package/skills/catalog.json +7 -1
- package/skills/client-urls/SKILL.md +338 -0
- package/skills/comparison/references/framework-comparison.md +23 -9
- package/skills/hooks/SKILL.md +2 -2
- package/skills/hooks/data.md +11 -2
- package/skills/hooks/handle-and-actions.md +7 -0
- package/skills/hooks/outlets.md +26 -5
- package/skills/hooks/urls.md +40 -3
- package/skills/loader/SKILL.md +132 -20
- package/skills/migrate-nextjs/SKILL.md +70 -10
- package/skills/migrate-react-router/SKILL.md +49 -13
- package/skills/migrate-react-router/component-migration.md +18 -13
- package/skills/migrate-react-router/data-and-actions.md +14 -3
- package/skills/migrate-react-router/route-mapping.md +15 -2
- package/skills/parallel/SKILL.md +32 -1
- package/skills/ppr/SKILL.md +16 -6
- package/skills/prerender/SKILL.md +8 -4
- package/skills/rango/SKILL.md +21 -17
- package/skills/react-compiler/SKILL.md +3 -3
- package/skills/route/SKILL.md +5 -2
- package/skills/router-setup/SKILL.md +16 -2
- package/skills/scripts/SKILL.md +16 -6
- package/skills/shell-manifest/SKILL.md +16 -7
- package/skills/testing/SKILL.md +2 -2
- package/skills/testing/client-components.md +6 -0
- package/skills/testing/handles.md +30 -8
- package/skills/testing/loader.md +51 -49
- package/skills/testing/middleware.md +1 -1
- package/skills/theme/SKILL.md +8 -5
- package/src/bin/rango.ts +7 -3
- package/src/browser/navigation-bridge.ts +6 -0
- package/src/browser/navigation-client.ts +5 -0
- package/src/browser/partial-update.ts +65 -13
- package/src/browser/react/use-router.ts +40 -11
- package/src/browser/react/use-search-params.ts +140 -17
- package/src/browser/rsc-router.tsx +59 -0
- package/src/browser/server-action-bridge.ts +26 -0
- package/src/browser/types.ts +22 -0
- package/src/build/merge-full-manifests.ts +161 -0
- package/src/build/route-trie.ts +9 -332
- package/src/build/route-types/include-resolution.ts +66 -11
- package/src/build/route-types/per-module-writer.ts +11 -6
- package/src/build/route-types/router-processing.ts +184 -153
- package/src/build/runtime-discovery.ts +23 -12
- package/src/cache/cache-exec-scope.ts +47 -0
- package/src/cache/cache-runtime.ts +24 -25
- package/src/cache/taint.ts +28 -9
- package/src/client-urls/client-root.tsx +168 -0
- package/src/client-urls/client-urls.ts +776 -0
- package/src/client-urls/navigation.ts +237 -0
- package/src/client-urls/revalidation-protocol.ts +56 -0
- package/src/client-urls/server-projection.ts +670 -0
- package/src/client-urls/types.ts +201 -0
- package/src/client.rsc.tsx +12 -0
- package/src/client.tsx +49 -6
- package/src/decode-loader-results.ts +113 -0
- package/src/errors.ts +14 -0
- package/src/handles/deferred-resolution.ts +14 -7
- package/src/index.ts +1 -0
- package/src/loader-redirect.tsx +64 -0
- package/src/outlet-context.ts +12 -0
- package/src/outlet-provider.tsx +15 -1
- package/src/redirect-origin.ts +29 -0
- package/src/route-content-wrapper.tsx +96 -3
- package/src/route-definition/dsl-helpers.ts +28 -3
- package/src/route-definition/helpers-types.ts +13 -0
- package/src/route-definition/redirect.ts +17 -18
- package/src/router/error-handling.ts +65 -11
- package/src/router/intercept-resolution.ts +29 -0
- package/src/router/loader-resolution.ts +261 -28
- package/src/router/match-result.ts +7 -0
- package/src/router/revalidation.ts +24 -11
- package/src/router/route-trie-builder.ts +334 -0
- package/src/router/router-interfaces.ts +38 -0
- package/src/router/segment-resolution/fresh.ts +55 -2
- package/src/router/segment-resolution/helpers.ts +9 -11
- package/src/router/segment-resolution/loader-cache.ts +34 -23
- package/src/router/segment-resolution/loader-mask.ts +9 -9
- package/src/router/segment-resolution/revalidation.ts +23 -2
- package/src/router/trie-matching.ts +3 -3
- package/src/router.ts +46 -1
- package/src/rsc/full-payload.ts +6 -0
- package/src/rsc/handler.ts +10 -7
- package/src/rsc/loader-fetch.ts +2 -2
- package/src/rsc/manifest-init.ts +28 -9
- package/src/rsc/rsc-rendering.ts +15 -1
- package/src/rsc/shell-capture.ts +12 -0
- package/src/rsc/shell-serve.ts +15 -2
- package/src/rsc/ssr-setup.ts +10 -1
- package/src/rsc/types.ts +31 -2
- package/src/segment-system.tsx +83 -26
- package/src/server/context.ts +10 -0
- package/src/server/cookie-store.ts +19 -19
- package/src/server/handle-store.ts +185 -48
- package/src/server/request-context.ts +30 -6
- package/src/server.ts +7 -0
- package/src/ssr/index.tsx +37 -2
- package/src/ssr/ssr-root.tsx +29 -2
- package/src/testing/dom.entry.ts +1 -1
- package/src/testing/render-route.tsx +22 -8
- package/src/testing/run-loader.ts +51 -13
- package/src/types/boundaries.ts +19 -0
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +60 -5
- package/src/types/segments.ts +7 -0
- package/src/urls/include-helper.ts +22 -4
- package/src/urls/path-helper-types.ts +17 -1
- package/src/use-loader.tsx +67 -6
- package/src/vite/discovery/client-urls-projection.ts +322 -0
- package/src/vite/discovery/discover-routers.ts +43 -17
- package/src/vite/discovery/state.ts +11 -1
- package/src/vite/discovery/virtual-module-codegen.ts +20 -0
- package/src/vite/plugins/client-ref-dedup.ts +281 -19
- package/src/vite/plugins/expose-action-id.ts +45 -23
- package/src/vite/plugins/virtual-entries.ts +12 -3
- package/src/vite/router-discovery.ts +163 -12
package/src/segment-system.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import { invariant } from "./errors.js";
|
|
|
8
8
|
import {
|
|
9
9
|
RouteContentWrapper,
|
|
10
10
|
LoaderBoundary,
|
|
11
|
+
StreamedLoaderErrorBoundary,
|
|
11
12
|
} from "./route-content-wrapper.js";
|
|
12
13
|
import { RootErrorBoundary } from "./root-error-boundary.js";
|
|
13
14
|
import { INTERNAL_RANGO_DEBUG } from "./internal-debug.js";
|
|
@@ -113,6 +114,9 @@ export interface RenderSegmentsOptions {
|
|
|
113
114
|
*/
|
|
114
115
|
forceAwait?: boolean;
|
|
115
116
|
|
|
117
|
+
/** Seeded descendant client-route pending state for testing. */
|
|
118
|
+
outletPending?: boolean;
|
|
119
|
+
|
|
116
120
|
/**
|
|
117
121
|
* Intercept segments to inject into the tree.
|
|
118
122
|
* These are parallel segments from intercept routes that need to be
|
|
@@ -231,6 +235,7 @@ export async function renderSegments(
|
|
|
231
235
|
isAction,
|
|
232
236
|
interceptSegments,
|
|
233
237
|
forceAwait,
|
|
238
|
+
outletPending = false,
|
|
234
239
|
rootLayout: RootLayout,
|
|
235
240
|
} = options || {};
|
|
236
241
|
|
|
@@ -448,9 +453,30 @@ export async function renderSegments(
|
|
|
448
453
|
// Prepare loader data if there are loaders
|
|
449
454
|
const loaderIds = loaderEntries.map((loader) => loader.loaderId!);
|
|
450
455
|
|
|
456
|
+
// Loader-bearing segments get the router-owned error boundary around
|
|
457
|
+
// their children so a read-site loader error renders its errorBoundary()
|
|
458
|
+
// fallback (LOADER_ERROR_FALLBACK marker on the thrown error) instead of
|
|
459
|
+
// escaping to app boundaries. Wrapped UNCONDITIONALLY on loader presence
|
|
460
|
+
// — streams and forceAwait lanes must produce the same tree shape or
|
|
461
|
+
// lane changes remount the subtree (docs/tree-structure.md).
|
|
462
|
+
if (loaderEntries.length > 0) {
|
|
463
|
+
nodeContent = createElement(StreamedLoaderErrorBoundary, {
|
|
464
|
+
children: nodeContent,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
451
468
|
if (loading !== undefined && loading !== null) {
|
|
452
469
|
const loaderDataPromise = getMemoizedLoaderPromise(loaderEntries);
|
|
453
470
|
let boundaryLoaderData: Promise<any[]> | any[] = loaderDataPromise;
|
|
471
|
+
// SPIKE (streaming useLoader): per-loader streams for the boundary's
|
|
472
|
+
// readers. MUST come from the individual loader refs — splitting the
|
|
473
|
+
// aggregate client-side is wrong (Promise.all resolves at the slowest
|
|
474
|
+
// loader, erasing per-loader timing; measured 400ms data held to a
|
|
475
|
+
// 2000ms sibling). Zero-loader boundaries pass NO streams so the
|
|
476
|
+
// resolver keeps the fresh-promise resolve-above suspension — that
|
|
477
|
+
// microtask suspension is what makes SSR emit the loading() fallback
|
|
478
|
+
// for content-suspending routes (segment-loader-promise.ts).
|
|
479
|
+
let boundaryLoaderStreams: Record<string, unknown> | undefined;
|
|
454
480
|
if (forceAwait || isAction) {
|
|
455
481
|
const awaitStart = segDebug ? performance.now() : 0;
|
|
456
482
|
boundaryLoaderData = await loaderDataPromise;
|
|
@@ -460,16 +486,23 @@ export async function renderSegments(
|
|
|
460
486
|
ms: Math.round(performance.now() - awaitStart),
|
|
461
487
|
});
|
|
462
488
|
}
|
|
463
|
-
} else if (
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
489
|
+
} else if (loaderEntries.length > 0) {
|
|
490
|
+
boundaryLoaderStreams = {};
|
|
491
|
+
for (const l of loaderEntries) {
|
|
492
|
+
boundaryLoaderStreams[l.loaderId!] = l.loaderData;
|
|
493
|
+
}
|
|
494
|
+
if (segDebug) {
|
|
495
|
+
segDebugLog(
|
|
496
|
+
`segment ${id}: per-loader streams via LoaderBoundary (read-site suspense)`,
|
|
497
|
+
{ loaderIds },
|
|
498
|
+
);
|
|
499
|
+
}
|
|
468
500
|
}
|
|
469
501
|
content = createElement(LoaderBoundary, {
|
|
470
502
|
key: `loader-boundary-${key}`,
|
|
471
503
|
loaderDataPromise: boundaryLoaderData,
|
|
472
504
|
loaderIds,
|
|
505
|
+
loaderStreams: boundaryLoaderStreams,
|
|
473
506
|
fallback: loading,
|
|
474
507
|
outletKey: key,
|
|
475
508
|
outletContent,
|
|
@@ -483,6 +516,7 @@ export async function renderSegments(
|
|
|
483
516
|
content: outletContent,
|
|
484
517
|
segment: node.segment,
|
|
485
518
|
parallel: node.parallel,
|
|
519
|
+
pending: outletPending,
|
|
486
520
|
children: nodeContent,
|
|
487
521
|
});
|
|
488
522
|
} else {
|
|
@@ -492,29 +526,50 @@ export async function renderSegments(
|
|
|
492
526
|
);
|
|
493
527
|
|
|
494
528
|
const layoutLoaderIds = layoutLoaders.map((l) => l.loaderId!);
|
|
495
|
-
//
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
resolvedData
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
if (decodeMs > 0) {
|
|
514
|
-
segDebugLog(`segment ${id}: loader results decoded`, {
|
|
515
|
-
ms: decodeMs,
|
|
529
|
+
// SPIKE (streaming useLoader): a segment without loading() used to BLOCK
|
|
530
|
+
// the tree build here (await buildLoaderPromise) so its data was always
|
|
531
|
+
// resolved before render. Streaming lanes now skip the await and pass
|
|
532
|
+
// per-loader UNDECODED results (possibly pending promises) through
|
|
533
|
+
// OutletProvider.loaderStreams — useLoader suspends at the read site to
|
|
534
|
+
// the nearest CONSUMER Suspense boundary. forceAwait (popstate, stale
|
|
535
|
+
// revalidation, fully-prefetched) and action lanes keep the blocking
|
|
536
|
+
// await so those commits stay whole with no fallback flash.
|
|
537
|
+
let loaderData: Record<string, any> = {};
|
|
538
|
+
let errorFallback: ReactNode = null;
|
|
539
|
+
let loaderStreams: Record<string, unknown> | undefined;
|
|
540
|
+
if (forceAwait || isAction) {
|
|
541
|
+
const layoutAwaitStart = segDebug ? performance.now() : 0;
|
|
542
|
+
const resolvedData = await buildLoaderPromise(layoutLoaders);
|
|
543
|
+
if (segDebug) {
|
|
544
|
+
segDebugLog(`segment ${id}: layout loaders awaited (blocking)`, {
|
|
545
|
+
loaderIds: layoutLoaderIds,
|
|
546
|
+
ms: Math.round(performance.now() - layoutAwaitStart),
|
|
516
547
|
});
|
|
517
548
|
}
|
|
549
|
+
const decodeStart = segDebug ? performance.now() : 0;
|
|
550
|
+
({ loaderData, errorFallback } = decodeLoaderResults(
|
|
551
|
+
resolvedData,
|
|
552
|
+
layoutLoaderIds,
|
|
553
|
+
));
|
|
554
|
+
if (segDebug) {
|
|
555
|
+
const decodeMs = Math.round(performance.now() - decodeStart);
|
|
556
|
+
if (decodeMs > 0) {
|
|
557
|
+
segDebugLog(`segment ${id}: loader results decoded`, {
|
|
558
|
+
ms: decodeMs,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
} else if (layoutLoaders.length > 0) {
|
|
563
|
+
loaderStreams = {};
|
|
564
|
+
for (const l of layoutLoaders) {
|
|
565
|
+
loaderStreams[l.loaderId!] = l.loaderData;
|
|
566
|
+
}
|
|
567
|
+
if (segDebug) {
|
|
568
|
+
segDebugLog(
|
|
569
|
+
`segment ${id}: layout loaders streaming to read sites (no loading())`,
|
|
570
|
+
{ loaderIds: layoutLoaderIds },
|
|
571
|
+
);
|
|
572
|
+
}
|
|
518
573
|
}
|
|
519
574
|
|
|
520
575
|
if (parallelOwnedLoaders.length > 0) {
|
|
@@ -574,6 +629,8 @@ export async function renderSegments(
|
|
|
574
629
|
segment: node.segment,
|
|
575
630
|
parallel: node.parallel,
|
|
576
631
|
loaderData: Object.keys(loaderData).length > 0 ? loaderData : undefined,
|
|
632
|
+
loaderStreams,
|
|
633
|
+
pending: outletPending,
|
|
577
634
|
children: errorFallback ?? nodeContent,
|
|
578
635
|
});
|
|
579
636
|
}
|
package/src/server/context.ts
CHANGED
|
@@ -106,6 +106,16 @@ export type LoaderEntry = {
|
|
|
106
106
|
revalidate: ShouldRevalidateFn<any, any>[];
|
|
107
107
|
/** Cache config for this specific loader (loaders are NOT cached by default) */
|
|
108
108
|
cache?: EntryCacheConfig;
|
|
109
|
+
/**
|
|
110
|
+
* Document renders await this loader before segment resolution returns
|
|
111
|
+
* (loader(Def, { stream: "navigation" })), so its data, handle pushes, and
|
|
112
|
+
* thrown notFound()/redirect() deterministically precede first flush.
|
|
113
|
+
* Resolved at DSL-evaluation time from ctx.isSSR — entries are cached
|
|
114
|
+
* per-isSSR (router/manifest.ts cache key), so the flag is already
|
|
115
|
+
* request-mode-correct when resolveLoaders (fresh.ts) reads it and never
|
|
116
|
+
* appears on navigation-lane entries.
|
|
117
|
+
*/
|
|
118
|
+
awaitBeforeFlush?: true;
|
|
109
119
|
};
|
|
110
120
|
|
|
111
121
|
/**
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
getCurrentLoaderBodyId,
|
|
15
15
|
isInsideHandlerInvokedLoaderBody,
|
|
16
16
|
} from "./context.js";
|
|
17
|
-
import {
|
|
17
|
+
import { isInsideCacheExecScope } from "../cache/cache-exec-scope.js";
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* A single cookie entry returned by get() and getAll().
|
|
@@ -65,7 +65,7 @@ export interface CookieStore {
|
|
|
65
65
|
*/
|
|
66
66
|
export function cookies(): CookieStore {
|
|
67
67
|
const ctx = getRequestContext();
|
|
68
|
-
assertNotInsideCacheContext(
|
|
68
|
+
assertNotInsideCacheContext("cookies");
|
|
69
69
|
assertNotInsideShellCapture(ctx, "cookies");
|
|
70
70
|
return createCookieStore(ctx);
|
|
71
71
|
}
|
|
@@ -91,9 +91,10 @@ type HeadersIterator<T> = IterableIterator<T>;
|
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
93
|
* Throw if called inside a cache boundary — either a "use cache" function
|
|
94
|
-
* (`
|
|
95
|
-
*
|
|
96
|
-
*
|
|
94
|
+
* (`isInsideCacheExecScope()` — the AsyncLocalStorage scope the cache runtime
|
|
95
|
+
* enters around the cached body) or a `cache()` DSL boundary
|
|
96
|
+
* (`isInsideCacheScope()` — the render-store flag set while resolving a
|
|
97
|
+
* `type: "cache"` route entry).
|
|
97
98
|
*
|
|
98
99
|
* Reading request-scoped data (cookies, headers) inside a cached scope
|
|
99
100
|
* produces per-request values that are NOT reflected in the cache key, so
|
|
@@ -103,18 +104,17 @@ type HeadersIterator<T> = IterableIterator<T>;
|
|
|
103
104
|
* here is baked into the shell exactly like a `"use cache"` return value is
|
|
104
105
|
* baked into its cache entry.
|
|
105
106
|
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
107
|
+
* Both checks are scoped so loaders stay exempt — loaders are the dynamic
|
|
108
|
+
* "holes" of a cached document and always run fresh. `isInsideCacheScope()`
|
|
109
|
+
* returns false inside loaders by construction; `isInsideCacheExecScope()`
|
|
110
|
+
* follows the cached body's own async chain, so a loader running in PARALLEL
|
|
111
|
+
* with a slow "use cache" fetch on the same request reads cookies() freely.
|
|
112
|
+
* (Scar: the previous INSIDE_CACHE_EXEC stamp on the shared RequestContext
|
|
113
|
+
* made exactly that parallel read throw for the cached fetch's whole
|
|
114
|
+
* execution window — see cache-exec-scope.ts.)
|
|
110
115
|
*/
|
|
111
|
-
function assertNotInsideCacheContext(
|
|
112
|
-
if (
|
|
113
|
-
ctx !== null &&
|
|
114
|
-
ctx !== undefined &&
|
|
115
|
-
typeof ctx === "object" &&
|
|
116
|
-
(INSIDE_CACHE_EXEC as symbol) in (ctx as Record<symbol, unknown>)
|
|
117
|
-
) {
|
|
116
|
+
function assertNotInsideCacheContext(fnName: string): void {
|
|
117
|
+
if (isInsideCacheExecScope()) {
|
|
118
118
|
throw new Error(
|
|
119
119
|
`${fnName}() cannot be called inside a "use cache" function. ` +
|
|
120
120
|
`Request-scoped data (cookies, headers) varies per request but is not ` +
|
|
@@ -222,7 +222,7 @@ const HEADERS_MUTATION_METHODS = new Set(["set", "append", "delete"]);
|
|
|
222
222
|
*/
|
|
223
223
|
export function headers(): ReadonlyHeaders {
|
|
224
224
|
const ctx = getRequestContext();
|
|
225
|
-
assertNotInsideCacheContext(
|
|
225
|
+
assertNotInsideCacheContext("headers");
|
|
226
226
|
assertNotInsideShellCapture(ctx, "headers");
|
|
227
227
|
return new Proxy(ctx.request.headers, {
|
|
228
228
|
get(target, prop, receiver) {
|
|
@@ -262,7 +262,7 @@ export function invalidateClientCache(): void {
|
|
|
262
262
|
}
|
|
263
263
|
return;
|
|
264
264
|
}
|
|
265
|
-
assertNotInsideCacheContext(
|
|
265
|
+
assertNotInsideCacheContext("invalidateClientCache");
|
|
266
266
|
ctx._rotateStateCookie();
|
|
267
267
|
}
|
|
268
268
|
|
|
@@ -287,7 +287,7 @@ export function keepClientCache(): void {
|
|
|
287
287
|
}
|
|
288
288
|
return;
|
|
289
289
|
}
|
|
290
|
-
assertNotInsideCacheContext(
|
|
290
|
+
assertNotInsideCacheContext("keepClientCache");
|
|
291
291
|
ctx._setKeepCacheDirective();
|
|
292
292
|
}
|
|
293
293
|
|
|
@@ -92,6 +92,26 @@ export interface HandleStore {
|
|
|
92
92
|
*/
|
|
93
93
|
onError?: (error: Error) => void;
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Track a promise on the AUXILIARY (loader) settlement lane. Aux tracking
|
|
97
|
+
* keeps the store OPEN for pushes (a mid-body `ctx.handle(H)(...)` from a
|
|
98
|
+
* streaming loader is legal until this lane drains) WITHOUT joining
|
|
99
|
+
* `settled` — the handler barrier that rendered()-readers, the document
|
|
100
|
+
* handle snapshot, and prerender/cache collection wait on. Two lanes exist
|
|
101
|
+
* because loader bodies stream: joining them to `settled` would block SSR
|
|
102
|
+
* markup and hydration on the slowest loader (ssr-root.tsx and
|
|
103
|
+
* rsc-router.tsx both drain the document handle stream in blocking
|
|
104
|
+
* positions), and would self-deadlock any loader awaiting ctx.rendered().
|
|
105
|
+
*/
|
|
106
|
+
trackAuxiliary<T>(promise: Promise<T>): Promise<T>;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Promise that resolves when the store is sealed AND BOTH lanes (handler +
|
|
110
|
+
* auxiliary) have drained. Late-push legality and default stream completion
|
|
111
|
+
* key on this, not on `settled`.
|
|
112
|
+
*/
|
|
113
|
+
readonly fullySettled: Promise<void>;
|
|
114
|
+
|
|
95
115
|
/**
|
|
96
116
|
* Push handle data for a specific handle and segment.
|
|
97
117
|
* Multiple pushes to the same handle/segment accumulate in an array.
|
|
@@ -101,16 +121,32 @@ export interface HandleStore {
|
|
|
101
121
|
|
|
102
122
|
/**
|
|
103
123
|
* Get all collected handle data after all handlers have settled.
|
|
104
|
-
* Waits for `settled
|
|
124
|
+
* Waits for `settled` (handler lane), then returns the data at that point.
|
|
105
125
|
*/
|
|
106
126
|
getData(): Promise<HandleData>;
|
|
107
127
|
|
|
108
128
|
/**
|
|
109
129
|
* Get an async iterator that yields handle data on each push.
|
|
110
|
-
*
|
|
130
|
+
* Completes at the chosen settlement barrier: "fullySettled" (default —
|
|
131
|
+
* nav/action payloads, whose consumer applies each yield progressively) or
|
|
132
|
+
* "settled" (the document lane, whose consumers drain to completion in
|
|
133
|
+
* blocking positions and must not wait on loader bodies).
|
|
111
134
|
* Each yield contains the full accumulated state (not just the delta).
|
|
135
|
+
* Safe for concurrent consumers (per-consumer version cursor).
|
|
112
136
|
*/
|
|
113
|
-
stream(
|
|
137
|
+
stream(
|
|
138
|
+
until?: "settled" | "fullySettled",
|
|
139
|
+
): AsyncGenerator<HandleData, void, unknown>;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Document-lane late channel: yields full-state updates for pushes that
|
|
143
|
+
* land AFTER the handler barrier (streaming loader bodies), completing at
|
|
144
|
+
* fullySettled. Returns without yielding when the auxiliary lane is empty
|
|
145
|
+
* by the time `settled` resolves. The client consumes this post-hydration
|
|
146
|
+
* (rsc-router.tsx) and merges via the same application path as nav-lane
|
|
147
|
+
* progressive handle updates.
|
|
148
|
+
*/
|
|
149
|
+
streamLate(): AsyncGenerator<HandleData, void, unknown>;
|
|
114
150
|
|
|
115
151
|
/**
|
|
116
152
|
* Get handle data for a specific segment (for caching).
|
|
@@ -151,20 +187,64 @@ export interface HandleStore {
|
|
|
151
187
|
export function createHandleStore(): HandleStore {
|
|
152
188
|
const data: HandleData = {};
|
|
153
189
|
|
|
154
|
-
// Settlement
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
190
|
+
// Settlement barriers: `settled` (handler lane) resolves when sealed AND
|
|
191
|
+
// handler inflight === 0. `fullySettled` additionally waits for the
|
|
192
|
+
// auxiliary (loader) lane. seal() signals "no more track() calls"; each
|
|
193
|
+
// track/trackAuxiliary increments its lane's count, each promise settle
|
|
194
|
+
// decrements. Barriers resolve once their conditions are met — even if
|
|
195
|
+
// tracks are added while earlier ones are still in flight.
|
|
159
196
|
let sealed = false;
|
|
160
197
|
let inflightCount = 0;
|
|
198
|
+
let auxInflightCount = 0;
|
|
161
199
|
let drainWaiters: (() => void)[] = [];
|
|
200
|
+
let fullDrainWaiters: (() => void)[] = [];
|
|
201
|
+
|
|
202
|
+
// Swap the waiter list out before resolving: a resolver may synchronously
|
|
203
|
+
// re-await and push onto the fresh list, which must not be drained by this
|
|
204
|
+
// pass. All three waiter lists (drain, full-drain, emission) flush through
|
|
205
|
+
// here so that ordering rule cannot drift between them.
|
|
206
|
+
function flushWaiters(waiters: (() => void)[]): void {
|
|
207
|
+
for (const resolve of waiters) resolve();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Both lanes settle identically — the handler lane (`track`) gates `settled`,
|
|
211
|
+
// the auxiliary loader lane (`trackAuxiliary`) gates `fullySettled`. Shared so
|
|
212
|
+
// neither copy can lose this rule: .then() rather than .finally(), because
|
|
213
|
+
// .finally() re-throws on a NEW branch, producing an unhandled rejection that
|
|
214
|
+
// can crash the process when the tracked promise rejects.
|
|
215
|
+
function trackSettlement<T>(
|
|
216
|
+
promise: Promise<T>,
|
|
217
|
+
release: () => void,
|
|
218
|
+
): Promise<T> {
|
|
219
|
+
const onSettle = () => {
|
|
220
|
+
release();
|
|
221
|
+
notifyDrain();
|
|
222
|
+
};
|
|
223
|
+
promise.then(onSettle, onSettle);
|
|
224
|
+
return promise;
|
|
225
|
+
}
|
|
162
226
|
|
|
163
227
|
function notifyDrain() {
|
|
164
228
|
if (sealed && inflightCount === 0 && drainWaiters.length > 0) {
|
|
165
229
|
const waiters = drainWaiters;
|
|
166
230
|
drainWaiters = [];
|
|
167
|
-
|
|
231
|
+
flushWaiters(waiters);
|
|
232
|
+
}
|
|
233
|
+
if (sealed && inflightCount === 0 && auxInflightCount === 0) {
|
|
234
|
+
if (streamConsumed && !completed) {
|
|
235
|
+
// Late-push guard + stream termination key on FULL drain, so a
|
|
236
|
+
// streaming loader body's ctx.handle() push stays legal until the
|
|
237
|
+
// auxiliary lane empties. Armed only once a stream consumer exists —
|
|
238
|
+
// getData()-only paths (prerender collection) keep their historical
|
|
239
|
+
// no-guard behavior.
|
|
240
|
+
completed = true;
|
|
241
|
+
signalEmission();
|
|
242
|
+
}
|
|
243
|
+
if (fullDrainWaiters.length > 0) {
|
|
244
|
+
const waiters = fullDrainWaiters;
|
|
245
|
+
fullDrainWaiters = [];
|
|
246
|
+
flushWaiters(waiters);
|
|
247
|
+
}
|
|
168
248
|
}
|
|
169
249
|
}
|
|
170
250
|
|
|
@@ -174,44 +254,50 @@ export function createHandleStore(): HandleStore {
|
|
|
174
254
|
notifyDrain();
|
|
175
255
|
}
|
|
176
256
|
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
257
|
+
// Emission versioning. Each push bumps `version`; every stream consumer
|
|
258
|
+
// keeps its own cursor and yields whenever the version advanced past it.
|
|
259
|
+
// Multi-consumer safe (the document "settled" stream and the late channel
|
|
260
|
+
// overlap until the handler barrier): a single dirty bit + single resolver
|
|
261
|
+
// slot would drop wakeups for all but one consumer.
|
|
262
|
+
let version = 0;
|
|
263
|
+
let emissionWaiters: (() => void)[] = [];
|
|
182
264
|
let completed = false;
|
|
265
|
+
let streamConsumed = false;
|
|
183
266
|
|
|
184
|
-
//
|
|
267
|
+
// Wake every waiting consumer (new push or a settlement barrier fired).
|
|
185
268
|
function signalEmission() {
|
|
186
|
-
if (
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
269
|
+
if (emissionWaiters.length > 0) {
|
|
270
|
+
const waiters = emissionWaiters;
|
|
271
|
+
emissionWaiters = [];
|
|
272
|
+
flushWaiters(waiters);
|
|
190
273
|
}
|
|
191
274
|
}
|
|
192
275
|
|
|
193
276
|
// Wait for the next emission or completion
|
|
194
|
-
|
|
195
|
-
|
|
277
|
+
// Resolve when the consumer's cursor is behind the current version, the
|
|
278
|
+
// given done-flag reader reports termination, or a new signal arrives.
|
|
279
|
+
function waitForEmission(seen: number, isDone: () => boolean): Promise<void> {
|
|
280
|
+
if (version > seen || isDone()) {
|
|
196
281
|
return Promise.resolve();
|
|
197
282
|
}
|
|
198
283
|
return new Promise((resolve) => {
|
|
199
|
-
|
|
284
|
+
emissionWaiters.push(resolve);
|
|
200
285
|
});
|
|
201
286
|
}
|
|
202
287
|
|
|
203
288
|
return {
|
|
204
289
|
track<T>(promise: Promise<T>): Promise<T> {
|
|
205
290
|
inflightCount++;
|
|
206
|
-
|
|
207
|
-
// branch when the promise rejects. .finally() re-throws on a new branch that
|
|
208
|
-
// can crash the process if not caught.
|
|
209
|
-
const onSettle = () => {
|
|
291
|
+
return trackSettlement(promise, () => {
|
|
210
292
|
inflightCount--;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
293
|
+
});
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
trackAuxiliary<T>(promise: Promise<T>): Promise<T> {
|
|
297
|
+
auxInflightCount++;
|
|
298
|
+
return trackSettlement(promise, () => {
|
|
299
|
+
auxInflightCount--;
|
|
300
|
+
});
|
|
215
301
|
},
|
|
216
302
|
|
|
217
303
|
seal() {
|
|
@@ -225,6 +311,15 @@ export function createHandleStore(): HandleStore {
|
|
|
225
311
|
});
|
|
226
312
|
},
|
|
227
313
|
|
|
314
|
+
get fullySettled(): Promise<void> {
|
|
315
|
+
if (sealed && inflightCount === 0 && auxInflightCount === 0) {
|
|
316
|
+
return Promise.resolve();
|
|
317
|
+
}
|
|
318
|
+
return new Promise<void>((resolve) => {
|
|
319
|
+
fullDrainWaiters.push(resolve);
|
|
320
|
+
});
|
|
321
|
+
},
|
|
322
|
+
|
|
228
323
|
push(handleName: string, segmentId: string, value: unknown): void {
|
|
229
324
|
if (completed) {
|
|
230
325
|
const error = createLateHandlePushError(handleName, segmentId);
|
|
@@ -240,8 +335,8 @@ export function createHandleStore(): HandleStore {
|
|
|
240
335
|
}
|
|
241
336
|
data[handleName][segmentId].push(value);
|
|
242
337
|
|
|
243
|
-
//
|
|
244
|
-
|
|
338
|
+
// Bump the version; each consumer's cursor decides when to clone+yield.
|
|
339
|
+
version++;
|
|
245
340
|
signalEmission();
|
|
246
341
|
},
|
|
247
342
|
|
|
@@ -250,33 +345,75 @@ export function createHandleStore(): HandleStore {
|
|
|
250
345
|
return this.settled.then(() => cloneHandleData(data));
|
|
251
346
|
},
|
|
252
347
|
|
|
253
|
-
async *stream(
|
|
348
|
+
async *stream(
|
|
349
|
+
until: "settled" | "fullySettled" = "fullySettled",
|
|
350
|
+
): AsyncGenerator<HandleData, void, unknown> {
|
|
351
|
+
streamConsumed = true;
|
|
254
352
|
sealInternal();
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
353
|
+
// Re-evaluate: the store may have been sealed and drained BEFORE this
|
|
354
|
+
// consumer existed (seal() then stream()); sealInternal early-returns
|
|
355
|
+
// then, so the completed flag must be armed here.
|
|
356
|
+
notifyDrain();
|
|
357
|
+
|
|
358
|
+
// Per-consumer termination flag driven by the chosen barrier. The
|
|
359
|
+
// global `completed` (late-push guard) always keys on FULL drain via
|
|
360
|
+
// notifyDrain — a "settled"-scoped consumer finishing does not make
|
|
361
|
+
// later loader pushes illegal.
|
|
362
|
+
let done = false;
|
|
363
|
+
const barrier = until === "settled" ? this.settled : this.fullySettled;
|
|
364
|
+
barrier.then(() => {
|
|
365
|
+
done = true;
|
|
258
366
|
signalEmission();
|
|
259
367
|
});
|
|
260
368
|
|
|
261
369
|
// Batch rapid synchronous pushes with initial delay
|
|
262
370
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
263
371
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
372
|
+
let seen = 0;
|
|
373
|
+
while (true) {
|
|
374
|
+
if (version > seen && Object.keys(data).length > 0) {
|
|
375
|
+
seen = version;
|
|
376
|
+
yield cloneHandleData(data);
|
|
377
|
+
}
|
|
378
|
+
if (done) break;
|
|
379
|
+
await waitForEmission(seen, () => done);
|
|
267
380
|
}
|
|
381
|
+
},
|
|
268
382
|
|
|
269
|
-
|
|
270
|
-
|
|
383
|
+
async *streamLate(): AsyncGenerator<HandleData, void, unknown> {
|
|
384
|
+
streamConsumed = true;
|
|
385
|
+
sealInternal();
|
|
386
|
+
// Re-evaluate: the store may have been sealed and drained BEFORE this
|
|
387
|
+
// consumer existed (seal() then stream()); sealInternal early-returns
|
|
388
|
+
// then, so the completed flag must be armed here.
|
|
389
|
+
notifyDrain();
|
|
390
|
+
|
|
391
|
+
// Skip everything the handler-barrier snapshot already carried; the
|
|
392
|
+
// document consumers (SSR seed + pre-hydration drain) deliver that
|
|
393
|
+
// state. This channel exists only for pushes that land AFTER `settled`
|
|
394
|
+
// — streaming loader bodies writing meta/breadcrumbs mid-body.
|
|
395
|
+
await this.settled;
|
|
396
|
+
let seen = version;
|
|
397
|
+
if (auxInflightCount === 0) {
|
|
398
|
+
// Auxiliary lane already drained: every loader push (if any) beat the
|
|
399
|
+
// handler barrier and rode the normal snapshot. Nothing late follows —
|
|
400
|
+
// handler pushes after settled are illegal by contract.
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
271
403
|
|
|
272
|
-
|
|
273
|
-
|
|
404
|
+
let done = false;
|
|
405
|
+
this.fullySettled.then(() => {
|
|
406
|
+
done = true;
|
|
407
|
+
signalEmission();
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
while (true) {
|
|
411
|
+
if (version > seen) {
|
|
412
|
+
seen = version;
|
|
274
413
|
yield cloneHandleData(data);
|
|
275
414
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
if (hasPendingEmission) {
|
|
279
|
-
yield cloneHandleData(data);
|
|
415
|
+
if (done) break;
|
|
416
|
+
await waitForEmission(seen, () => done);
|
|
280
417
|
}
|
|
281
418
|
},
|
|
282
419
|
|
|
@@ -303,7 +440,7 @@ export function createHandleStore(): HandleStore {
|
|
|
303
440
|
// segment, not accumulate on top of data from a different route.
|
|
304
441
|
data[handleName][segmentId] = [...segmentHandles[handleName]];
|
|
305
442
|
}
|
|
306
|
-
|
|
443
|
+
version++;
|
|
307
444
|
signalEmission();
|
|
308
445
|
},
|
|
309
446
|
};
|