@rangojs/router 0.5.2 → 0.6.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 +62 -0
- package/dist/types/client-urls/types.d.ts +144 -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/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 +10 -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/vite/index.js +5313 -2365
- package/package.json +1 -1
- 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 +698 -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 +579 -0
- package/src/client-urls/types.ts +195 -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 +47 -0
- package/src/router/segment-resolution/helpers.ts +9 -11
- package/src/router/segment-resolution/loader-cache.ts +14 -24
- package/src/router/segment-resolution/revalidation.ts +20 -1
- 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 +14 -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/virtual-entries.ts +12 -3
- package/src/vite/router-discovery.ts +163 -12
|
@@ -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
|
};
|
|
@@ -618,6 +618,17 @@ export interface RequestContext<
|
|
|
618
618
|
*/
|
|
619
619
|
_handlerLoaderDeps?: Set<string>;
|
|
620
620
|
|
|
621
|
+
/**
|
|
622
|
+
* @internal Loader IDs ($$id) whose entries carry `awaitBeforeFlush`
|
|
623
|
+
* (loader(Def, { stream: "navigation" })): segment resolution awaits these
|
|
624
|
+
* before returning, so the render barrier cannot resolve until they settle.
|
|
625
|
+
* rendered() checks this set to fail fast — a flagged loader awaiting the
|
|
626
|
+
* barrier is a guaranteed cycle, not a race. Registered by resolveLoaders
|
|
627
|
+
* (fresh.ts) before loader kickoff; only ever populated on document renders
|
|
628
|
+
* (the flag is stamped per-isSSR at DSL evaluation).
|
|
629
|
+
*/
|
|
630
|
+
_awaitBeforeFlushLoaderIds?: Set<string>;
|
|
631
|
+
|
|
621
632
|
/**
|
|
622
633
|
* @internal Cached HandleData snapshot built at barrier resolution time.
|
|
623
634
|
* Avoids rebuilding the snapshot on every loader ctx.use(handle) call.
|
|
@@ -765,6 +776,7 @@ export type PublicRequestContext<
|
|
|
765
776
|
| "_treeHasStreaming"
|
|
766
777
|
| "_renderBarrierWaiters"
|
|
767
778
|
| "_handlerLoaderDeps"
|
|
779
|
+
| "_awaitBeforeFlushLoaderIds"
|
|
768
780
|
| "_renderBarrierHandleSnapshot"
|
|
769
781
|
| "_renderBarrierGuardClosed"
|
|
770
782
|
| "_reportBackgroundError"
|
|
@@ -1384,6 +1396,7 @@ export function wireRenderBarrier(
|
|
|
1384
1396
|
ctx._renderBarrierHandleSnapshot = undefined;
|
|
1385
1397
|
ctx._renderBarrierGuardClosed = undefined;
|
|
1386
1398
|
ctx._handlerLoaderDeps = undefined;
|
|
1399
|
+
ctx._awaitBeforeFlushLoaderIds = undefined;
|
|
1387
1400
|
ctx._treeHasStreaming = undefined;
|
|
1388
1401
|
|
|
1389
1402
|
// Lazy allocation: only create the Promise when a loader calls rendered().
|
|
@@ -1626,12 +1639,23 @@ export function createUseFunction<TEnv>(
|
|
|
1626
1639
|
env: ctx.env as any,
|
|
1627
1640
|
waitUntil: ctx.waitUntil.bind(ctx),
|
|
1628
1641
|
executionContext: ctx.executionContext,
|
|
1629
|
-
get:
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1642
|
+
get: ((keyOrVar: any) => {
|
|
1643
|
+
// Handle READS need the rendered() barrier, which this lane
|
|
1644
|
+
// (action/dispatch-invoked loaders) does not run. Fail with guidance
|
|
1645
|
+
// instead of returning a misleading empty collect.
|
|
1646
|
+
if (isHandle(keyOrVar)) {
|
|
1647
|
+
throw new Error(
|
|
1648
|
+
`ctx.get(handle) is only available in DSL loaders after ` +
|
|
1649
|
+
`"await ctx.rendered()". It cannot be used from request-context ` +
|
|
1650
|
+
`loaders or server actions.`,
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
return (ctx.get as any)(keyOrVar);
|
|
1654
|
+
}) as any,
|
|
1655
|
+
// Pass items through: loaders delegate to the request ctx's loader
|
|
1656
|
+
// executor; a handle yields the ctx's push function (write parity —
|
|
1657
|
+
// ctx.use(Meta)({...}) works in this lane exactly like in a handler).
|
|
1658
|
+
use: ((item: any) => ctx.use(item)) as LoaderContext["use"],
|
|
1635
1659
|
method: "GET",
|
|
1636
1660
|
body: undefined,
|
|
1637
1661
|
reverse: createReverseFunction(
|
package/src/server.ts
CHANGED
|
@@ -47,3 +47,10 @@ export {
|
|
|
47
47
|
|
|
48
48
|
// Component utilities (used internally for server/client boundary checks)
|
|
49
49
|
export { isClientComponent, assertClientComponent } from "./component-utils.js";
|
|
50
|
+
|
|
51
|
+
// Client URL projection registry (Vite discovery/bootstrap bridge)
|
|
52
|
+
export {
|
|
53
|
+
clearClientUrlProjections,
|
|
54
|
+
setClientUrlProjection,
|
|
55
|
+
type ClientUrlProjection,
|
|
56
|
+
} from "./client-urls/server-projection.js";
|
package/src/ssr/index.tsx
CHANGED
|
@@ -105,6 +105,15 @@ export interface SSRRenderOptions {
|
|
|
105
105
|
* - `"allReady"` — await `stream.allReady` before returning.
|
|
106
106
|
*/
|
|
107
107
|
streamMode?: import("../router/router-options.js").SSRStreamMode;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The live request's query string (`?`-prefixed or empty). Seeds the SSR
|
|
111
|
+
* navigation store location so `useSearchParams` carries real values
|
|
112
|
+
* during document renders. Absent on the build-time prerender pass —
|
|
113
|
+
* build shells capture bare pathnames and serve search-less requests
|
|
114
|
+
* only (runtime captures own the search variants, seeded per key).
|
|
115
|
+
*/
|
|
116
|
+
search?: string;
|
|
108
117
|
}
|
|
109
118
|
|
|
110
119
|
/**
|
|
@@ -347,6 +356,13 @@ interface ShellCaptureOptions {
|
|
|
347
356
|
quiesce: Promise<void>;
|
|
348
357
|
/** Upper bound on how long to wait for `quiesce`. Default SHELL_CAPTURE_MAX_WAIT_MS. */
|
|
349
358
|
maxWaitMs?: number;
|
|
359
|
+
/**
|
|
360
|
+
* The SHELL KEY's search string (`?`-prefixed, sorted, cache.searchParams
|
|
361
|
+
* filter applied — shellSearchSeed in rsc/shell-serve.ts), seeding the SSR
|
|
362
|
+
* store so static-part `useSearchParams` reads bake markup consistent with
|
|
363
|
+
* the shell's own key. MUST equal the resume pass's seed for the same key.
|
|
364
|
+
*/
|
|
365
|
+
search?: string;
|
|
350
366
|
}
|
|
351
367
|
|
|
352
368
|
/**
|
|
@@ -368,6 +384,13 @@ interface ShellResumeOptions {
|
|
|
368
384
|
postponed: string | null;
|
|
369
385
|
/** Nonce for CSP. */
|
|
370
386
|
nonce?: string;
|
|
387
|
+
/**
|
|
388
|
+
* The SHELL KEY's search string — same derivation as the capture pass
|
|
389
|
+
* (shellSearchSeed). A HIT shares the capture's key, so seeding the same
|
|
390
|
+
* string keeps the resume tree identical to the captured tree above the
|
|
391
|
+
* postponed holes.
|
|
392
|
+
*/
|
|
393
|
+
search?: string;
|
|
371
394
|
}
|
|
372
395
|
|
|
373
396
|
/**
|
|
@@ -449,7 +472,7 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
|
|
|
449
472
|
rscStream: ReadableStream<Uint8Array>,
|
|
450
473
|
options?: SSRRenderOptions,
|
|
451
474
|
): Promise<ReadableStream<Uint8Array>> {
|
|
452
|
-
const { nonce, formState, streamMode } = options ?? {};
|
|
475
|
+
const { nonce, formState, streamMode, search } = options ?? {};
|
|
453
476
|
|
|
454
477
|
try {
|
|
455
478
|
// Tee the stream:
|
|
@@ -461,6 +484,12 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
|
|
|
461
484
|
createFromReadableStream,
|
|
462
485
|
rscStream: rscStream1,
|
|
463
486
|
nonce,
|
|
487
|
+
// Live fizz seeds the request's RAW search. The shell capture pass
|
|
488
|
+
// below and its resume twin seed the SHELL KEY's search instead
|
|
489
|
+
// (sorted, cache.searchParams filter applied — shellSearchSeed):
|
|
490
|
+
// same key => same seed, so the resume tree matches the captured
|
|
491
|
+
// tree while static-part search reads render what the key names.
|
|
492
|
+
search,
|
|
464
493
|
});
|
|
465
494
|
|
|
466
495
|
// Get bootstrap script content
|
|
@@ -562,6 +591,9 @@ export function createShellCaptureHandler<TEnv = unknown>(
|
|
|
562
591
|
createFromReadableStream,
|
|
563
592
|
rscStream,
|
|
564
593
|
onPayloadSettled: settlePayload,
|
|
594
|
+
// The shell key's own search: static-part search reads render what
|
|
595
|
+
// the key names, and the resume pass seeds the identical string.
|
|
596
|
+
search: opts.search,
|
|
565
597
|
});
|
|
566
598
|
|
|
567
599
|
// Bootstrap load raced against the deadline. A load that never resolves
|
|
@@ -735,7 +767,7 @@ export function createShellResumeHandler<TEnv = unknown>(
|
|
|
735
767
|
rscStream: ReadableStream<Uint8Array>,
|
|
736
768
|
opts: ShellResumeOptions,
|
|
737
769
|
): Promise<ReadableStream<Uint8Array>> {
|
|
738
|
-
const { postponed, nonce } = opts;
|
|
770
|
+
const { postponed, nonce, search } = opts;
|
|
739
771
|
|
|
740
772
|
try {
|
|
741
773
|
if (postponed === null) {
|
|
@@ -765,6 +797,9 @@ export function createShellResumeHandler<TEnv = unknown>(
|
|
|
765
797
|
createFromReadableStream,
|
|
766
798
|
rscStream: rscStream1,
|
|
767
799
|
nonce,
|
|
800
|
+
// Same seed as the capture pass for this key: the resume tree must
|
|
801
|
+
// match the captured tree above the holes, search reads included.
|
|
802
|
+
search,
|
|
768
803
|
});
|
|
769
804
|
|
|
770
805
|
// EAGER injection (resume-only): the stored prelude — a complete document
|
package/src/ssr/ssr-root.tsx
CHANGED
|
@@ -63,11 +63,21 @@ async function consumeAsyncGenerator(
|
|
|
63
63
|
*/
|
|
64
64
|
function createSsrEventController(opts: {
|
|
65
65
|
pathname: string;
|
|
66
|
+
/**
|
|
67
|
+
* Query string seeding the store location. Live fizz passes the request's
|
|
68
|
+
* raw search; ppr capture/resume pass the SHELL KEY's search (sorted,
|
|
69
|
+
* cache.searchParams filter applied) so the shell renders what its key
|
|
70
|
+
* names and both passes agree byte-for-byte.
|
|
71
|
+
*/
|
|
72
|
+
search?: string;
|
|
66
73
|
params?: Record<string, string>;
|
|
67
74
|
handleData?: HandleData;
|
|
68
75
|
matched?: string[];
|
|
69
76
|
}): EventController {
|
|
70
|
-
const location = new URL(
|
|
77
|
+
const location = new URL(
|
|
78
|
+
`${opts.pathname}${opts.search ?? ""}`,
|
|
79
|
+
"http://localhost",
|
|
80
|
+
);
|
|
71
81
|
let params = opts.params ?? {};
|
|
72
82
|
const rawMatched = opts.matched ?? [];
|
|
73
83
|
const handleState = {
|
|
@@ -142,6 +152,16 @@ export interface SsrRootOptions {
|
|
|
142
152
|
* they postpone below the root.
|
|
143
153
|
*/
|
|
144
154
|
onPayloadSettled?: () => void;
|
|
155
|
+
/**
|
|
156
|
+
* The query string seeding the SSR store location (`?`-prefixed or
|
|
157
|
+
* empty), so `useSearchParams`/`useNavigation` carry real values during
|
|
158
|
+
* document renders. Live fizz passes the request's raw search. The shell
|
|
159
|
+
* capture and resume passes pass the SHELL KEY's search (sorted,
|
|
160
|
+
* cache.searchParams filter applied — search is part of shell identity):
|
|
161
|
+
* a HIT shares the capture's key, so both passes seed the SAME string and
|
|
162
|
+
* the resume tree matches the captured tree above the postponed holes.
|
|
163
|
+
*/
|
|
164
|
+
search?: string;
|
|
145
165
|
}
|
|
146
166
|
|
|
147
167
|
/**
|
|
@@ -162,7 +182,13 @@ export interface SsrRootOptions {
|
|
|
162
182
|
* re-running the whole segment-tree build unless the promise is memoized.
|
|
163
183
|
*/
|
|
164
184
|
export function createSsrRootComponent(opts: SsrRootOptions): React.FC {
|
|
165
|
-
const {
|
|
185
|
+
const {
|
|
186
|
+
createFromReadableStream,
|
|
187
|
+
rscStream,
|
|
188
|
+
nonce,
|
|
189
|
+
onPayloadSettled,
|
|
190
|
+
search,
|
|
191
|
+
} = opts;
|
|
166
192
|
|
|
167
193
|
let payload: Promise<RscPayload> | undefined;
|
|
168
194
|
let handlesPromise: Promise<HandleData> | undefined;
|
|
@@ -211,6 +237,7 @@ export function createSsrRootComponent(opts: SsrRootOptions): React.FC {
|
|
|
211
237
|
store: null as any,
|
|
212
238
|
eventController: createSsrEventController({
|
|
213
239
|
pathname,
|
|
240
|
+
search,
|
|
214
241
|
params: resolved.metadata?.params,
|
|
215
242
|
handleData,
|
|
216
243
|
matched: resolved.metadata?.matched,
|
package/src/testing/dom.entry.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Component-render testing: `renderRoute`, the React-Testing-Library-style stub
|
|
5
5
|
* for client components that read router context (useParams / useReverse /
|
|
6
|
-
* Outlet / useNavigation / useLoader).
|
|
6
|
+
* Outlet / useOutlet / useNavigation / useLoader).
|
|
7
7
|
*
|
|
8
8
|
* Separate from the main `@rangojs/router/testing` barrel so unit suites that
|
|
9
9
|
* only test loaders, middleware, or `dispatch` never reference React, the
|