@rangojs/router 0.17.0 → 0.18.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/README.md +3 -2
- package/dist/types/browser/cookie-name.d.ts +6 -0
- package/dist/types/browser/history-state.d.ts +16 -0
- package/dist/types/cache/cache-key-utils.d.ts +17 -2
- package/dist/types/cache/cache-policy.d.ts +17 -0
- package/dist/types/cache/cache-scope.d.ts +98 -14
- package/dist/types/cache/cache-tag.d.ts +92 -7
- package/dist/types/cache/cf/cf-base64.d.ts +6 -0
- package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
- package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
- package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
- package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
- package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
- package/dist/types/cache/handle-snapshot.d.ts +4 -3
- package/dist/types/cache/index.d.ts +1 -0
- package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
- package/dist/types/cache/shell-memo.d.ts +178 -0
- package/dist/types/cache/shell-snapshot.d.ts +110 -21
- package/dist/types/cache/tag-invalidation.d.ts +30 -3
- package/dist/types/cache/types.d.ts +207 -43
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
- package/dist/types/prerender/build-shell-capture.d.ts +11 -4
- package/dist/types/router/instrument.d.ts +8 -0
- package/dist/types/router/middleware-types.d.ts +6 -1
- package/dist/types/router/segment-resolution/loader-cache.d.ts +40 -11
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +21 -10
- package/dist/types/router/segment-resolution/mask-nested.d.ts +65 -24
- package/dist/types/rsc/full-payload.d.ts +14 -0
- package/dist/types/rsc/handler.d.ts +1 -1
- package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +53 -6
- package/dist/types/rsc/shell-capture.d.ts +146 -15
- package/dist/types/rsc/shell-serve.d.ts +140 -17
- package/dist/types/rsc/types.d.ts +7 -0
- package/dist/types/server/context.d.ts +109 -15
- package/dist/types/server/cookie-store.d.ts +21 -0
- package/dist/types/server/request-context.d.ts +159 -33
- package/dist/types/ssr/index.d.ts +9 -0
- package/dist/types/ssr/ssr-root.d.ts +9 -8
- package/dist/types/testing/flight.entry.d.ts +7 -0
- package/dist/types/testing/run-loader.d.ts +18 -3
- package/dist/types/testing/serve-shell-request.d.ts +132 -0
- package/dist/types/testing/shell-status.d.ts +23 -10
- package/dist/types/types/handler-context.d.ts +14 -1
- package/dist/types/urls/pattern-types.d.ts +22 -18
- package/dist/types/vite/router-discovery.d.ts +29 -2
- package/dist/vite/index.js +46 -9
- package/package.json +1 -1
- package/skills/breadcrumbs/SKILL.md +17 -7
- package/skills/cache-guide/SKILL.md +123 -51
- package/skills/caching/SKILL.md +253 -23
- package/skills/catalog.json +1 -1
- package/skills/client-urls/SKILL.md +11 -6
- package/skills/cloudflare/SKILL.md +15 -0
- package/skills/comparison/references/framework-comparison.md +44 -32
- package/skills/defer-hydration/SKILL.md +50 -30
- package/skills/deployment-caching/SKILL.md +5 -2
- package/skills/document-cache/SKILL.md +8 -2
- package/skills/handler-use/SKILL.md +2 -0
- package/skills/hooks/handle-and-actions.md +3 -1
- package/skills/hooks/state.md +3 -1
- package/skills/loader/SKILL.md +161 -39
- package/skills/middleware/SKILL.md +16 -9
- package/skills/migrate-nextjs/SKILL.md +67 -36
- package/skills/migrate-react-router/SKILL.md +2 -1
- package/skills/migrate-react-router/component-migration.md +3 -1
- package/skills/migrate-react-router/data-and-actions.md +3 -1
- package/skills/migrate-react-router/route-mapping.md +7 -4
- package/skills/observability/SKILL.md +64 -0
- package/skills/parallel/SKILL.md +13 -8
- package/skills/ppr/SKILL.md +655 -363
- package/skills/prerender/SKILL.md +27 -14
- package/skills/rango/SKILL.md +52 -33
- package/skills/route/SKILL.md +12 -7
- package/skills/router-setup/SKILL.md +4 -2
- package/skills/scripts/SKILL.md +8 -1
- package/skills/server-actions/SKILL.md +11 -0
- package/skills/shell-manifest/SKILL.md +21 -12
- package/skills/testing/SKILL.md +25 -21
- package/skills/testing/cache-prerender.md +15 -14
- package/skills/testing/loader.md +18 -1
- package/skills/testing/serve-shell-request.md +188 -0
- package/skills/theme/SKILL.md +52 -4
- package/skills/typesafety/env-and-bindings.md +10 -2
- package/skills/use-cache/SKILL.md +39 -16
- package/skills/vercel/SKILL.md +14 -2
- package/skills/view-transitions/SKILL.md +1 -1
- package/src/browser/cookie-name.ts +23 -0
- package/src/browser/history-state.ts +23 -0
- package/src/browser/rsc-router.tsx +5 -0
- package/src/cache/cache-key-utils.ts +22 -2
- package/src/cache/cache-policy.ts +22 -0
- package/src/cache/cache-runtime.ts +34 -7
- package/src/cache/cache-scope.ts +345 -33
- package/src/cache/cache-tag.ts +375 -11
- package/src/cache/cf/cf-base64.ts +36 -4
- package/src/cache/cf/cf-cache-constants.ts +12 -0
- package/src/cache/cf/cf-cache-store.ts +990 -348
- package/src/cache/cf/cf-cache-types.ts +32 -4
- package/src/cache/cf/cf-shell-frame.ts +258 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
- package/src/cache/handle-snapshot.ts +4 -3
- package/src/cache/index.ts +2 -0
- package/src/cache/isolate-tag-memo.ts +308 -0
- package/src/cache/shell-memo.ts +341 -0
- package/src/cache/shell-snapshot.ts +217 -27
- package/src/cache/tag-invalidation.ts +104 -19
- package/src/cache/types.ts +211 -43
- package/src/cache/vercel/vercel-cache-store.ts +545 -61
- package/src/prerender/build-shell-capture.ts +65 -11
- package/src/route-definition/dsl-helpers.ts +4 -3
- package/src/router/handler-context.ts +38 -4
- package/src/router/instrument.ts +54 -2
- package/src/router/intercept-resolution.ts +19 -7
- package/src/router/loader-resolution.ts +63 -32
- package/src/router/match-api.ts +28 -9
- package/src/router/match-handlers.ts +5 -0
- package/src/router/match-middleware/cache-lookup.ts +17 -0
- package/src/router/match-middleware/cache-store.ts +31 -11
- package/src/router/middleware-types.ts +6 -1
- package/src/router/middleware.ts +23 -4
- package/src/router/prerender-match.ts +6 -0
- package/src/router/segment-resolution/fresh.ts +18 -9
- package/src/router/segment-resolution/helpers.ts +4 -2
- package/src/router/segment-resolution/loader-cache.ts +227 -56
- package/src/router/segment-resolution/loader-snapshot.ts +203 -51
- package/src/router/segment-resolution/mask-nested.ts +136 -42
- package/src/router/segment-resolution/revalidation.ts +27 -10
- package/src/rsc/full-payload.ts +25 -1
- package/src/rsc/handler.ts +14 -2
- package/src/rsc/progressive-enhancement.ts +3 -2
- package/src/rsc/response-cache-serve.ts +9 -6
- package/src/rsc/rsc-rendering.ts +601 -181
- package/src/rsc/shell-build-manifest.ts +27 -9
- package/src/rsc/shell-capture-constants.ts +60 -6
- package/src/rsc/shell-capture.ts +1001 -250
- package/src/rsc/shell-serve.ts +316 -34
- package/src/rsc/types.ts +7 -0
- package/src/server/context.ts +219 -28
- package/src/server/cookie-store.ts +87 -62
- package/src/server/request-context.ts +263 -38
- package/src/ssr/index.tsx +96 -37
- package/src/ssr/ssr-root.tsx +9 -8
- package/src/testing/flight.entry.ts +14 -0
- package/src/testing/run-loader.ts +63 -5
- package/src/testing/serve-shell-request.ts +378 -0
- package/src/testing/shell-status.ts +34 -12
- package/src/theme/ThemeProvider.tsx +7 -6
- package/src/types/handler-context.ts +14 -1
- package/src/urls/pattern-types.ts +22 -18
- package/src/vite/discovery/shell-prerender-phase.ts +9 -0
- package/src/vite/router-discovery.ts +14 -4
|
@@ -17,6 +17,7 @@ import type { CookieOptions } from "../router/middleware.js";
|
|
|
17
17
|
import {
|
|
18
18
|
KEEP_CACHE_HEADER,
|
|
19
19
|
getRawCookieValue,
|
|
20
|
+
freshReadsCookieName,
|
|
20
21
|
mintStateValue,
|
|
21
22
|
serializeStateCookie,
|
|
22
23
|
} from "../browser/cookie-name.js";
|
|
@@ -64,6 +65,7 @@ import {
|
|
|
64
65
|
assertNonCacheableReadAllowed,
|
|
65
66
|
clearPprHeaderScope,
|
|
66
67
|
} from "./context.js";
|
|
68
|
+
import { assertThemeReadAllowed } from "./cookie-store.js";
|
|
67
69
|
import {
|
|
68
70
|
createReverseFunction,
|
|
69
71
|
stripInternalParams,
|
|
@@ -165,6 +167,40 @@ export interface RequestContext<
|
|
|
165
167
|
_rotateStateCookie(): void;
|
|
166
168
|
/** @internal Set the keepClientCache() directive header on the response. */
|
|
167
169
|
_setKeepCacheDirective(): void;
|
|
170
|
+
/**
|
|
171
|
+
* @internal This request carries the fresh-reads cookie: it follows an
|
|
172
|
+
* updateTag()/revalidateTag() by the same user, so the stores read tag
|
|
173
|
+
* markers and shells from the store, not from an isolate memo.
|
|
174
|
+
*/
|
|
175
|
+
_freshReads?: boolean;
|
|
176
|
+
/**
|
|
177
|
+
* @internal Set the fresh-reads cookie on this response for `maxAgeMs`
|
|
178
|
+
* (rounded up to whole seconds; the longest stays). Called by
|
|
179
|
+
* updateTag()/revalidateTag().
|
|
180
|
+
*/
|
|
181
|
+
_setFreshReadsCookie(maxAgeMs: number): void;
|
|
182
|
+
/**
|
|
183
|
+
* @internal The context createRequestContext() returned. A context derived
|
|
184
|
+
* from it with Object.create (a PPR HIT tail, a shell capture, a background
|
|
185
|
+
* revalidation render) inherits it, so per-request invalidation state
|
|
186
|
+
* (tag-invalidation.ts, the stores' masks) is keyed by the request, not by
|
|
187
|
+
* whichever derived object called or read (#973).
|
|
188
|
+
*/
|
|
189
|
+
_requestRoot?: RequestContext<any, any>;
|
|
190
|
+
/**
|
|
191
|
+
* @internal The tags this request invalidated with
|
|
192
|
+
* updateTag()/revalidateTag(), each with its position in the invalidation
|
|
193
|
+
* order (tag-invalidation.ts currentCacheSeq). Kept on _requestRoot. A
|
|
194
|
+
* "use cache" execution that started before one of its own tags was
|
|
195
|
+
* invalidated is neither joined nor written (cache-runtime.ts, #973).
|
|
196
|
+
*/
|
|
197
|
+
_tagInvalidations?: Map<string, number>;
|
|
198
|
+
/**
|
|
199
|
+
* @internal The handler has handed the response to the host (rsc/handler.ts
|
|
200
|
+
* `rango.response`): header and cookie writes after this are lost, as from
|
|
201
|
+
* a streaming loader or render.
|
|
202
|
+
*/
|
|
203
|
+
_responseSent?: boolean;
|
|
168
204
|
|
|
169
205
|
/**
|
|
170
206
|
* Access loader data or push handle data.
|
|
@@ -264,6 +300,49 @@ export interface RequestContext<
|
|
|
264
300
|
*/
|
|
265
301
|
_shellCaptureLoaderRecords?: Map<string, Promise<unknown>>;
|
|
266
302
|
|
|
303
|
+
/**
|
|
304
|
+
* @internal Set on a capture's derived context when a loader pushed a
|
|
305
|
+
* value the record cannot keep (a deferred push, masked nested promises,
|
|
306
|
+
* or a push outside a loader body): the loader records then carry
|
|
307
|
+
* `runs: 1`, so a HIT still runs their bodies for those pushes.
|
|
308
|
+
*/
|
|
309
|
+
_shellCaptureUnrecordedLoaderPush?: true;
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* @internal Record keys resolved for this request (CacheScope
|
|
313
|
+
* resolveKeyFrom): by the scope's `key()` (one result) or its store's
|
|
314
|
+
* keyGenerator (one per default key). A shell capture's derived context
|
|
315
|
+
* reads it through the prototype chain, so the capture reuses the
|
|
316
|
+
* partition its request resolved instead of running `key()` again.
|
|
317
|
+
*/
|
|
318
|
+
_resolvedCacheKeys?: Map<unknown, Map<string, Promise<string>>>;
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* @internal The shell store key the PPR serve path resolved for this
|
|
322
|
+
* document request (rsc-rendering.ts shellServePlan), request partition
|
|
323
|
+
* included. Read by the testing helper serveShellRequest.
|
|
324
|
+
*/
|
|
325
|
+
_shellKey?: string;
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* @internal The request carried the forced-MISS reload marker
|
|
329
|
+
* (shell-serve.ts SHELL_MISS_PARAM). The handler strips the marker from the
|
|
330
|
+
* request on entry and sets this; the shell gate renders the request like a
|
|
331
|
+
* cache miss.
|
|
332
|
+
*/
|
|
333
|
+
_shellForcedMiss?: true;
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* @internal The freshness window of the route cache() records this
|
|
337
|
+
* request read or wrote (cache-scope.ts noteRouteRecordWindow), as ms
|
|
338
|
+
* timestamps: `freshUntil` is the record's `expiresAt`, `staleUntil` adds
|
|
339
|
+
* its scope's swr. The narrowest record wins. A shell capture caps its
|
|
340
|
+
* entry's ttl and ttl+swr to it (shell-capture.ts capShellWindow), so a
|
|
341
|
+
* shell is never fresher, or served longer, than the route cache() entry
|
|
342
|
+
* it replayed or wrote.
|
|
343
|
+
*/
|
|
344
|
+
_routeRecordWindow?: RouteRecordWindow;
|
|
345
|
+
|
|
267
346
|
/**
|
|
268
347
|
* @internal Loader-family snapshot seed for a shell HIT's tail render:
|
|
269
348
|
* segment-key -> the capture's elided container (already Flight-deserialized
|
|
@@ -271,8 +350,10 @@ export interface RequestContext<
|
|
|
271
350
|
* overlays it onto the fresh run's container (recorded paths pinned,
|
|
272
351
|
* hole-marker paths keep the fresh nested promises) so the payload's baked
|
|
273
352
|
* bytes match the frozen prelude; a hole-free entry resolves pin-first
|
|
274
|
-
*
|
|
275
|
-
* context
|
|
353
|
+
* and does not run the loader. Two owners: serveShellHit sets it on the
|
|
354
|
+
* HIT tail's derived context, and matchPartialWithPprReplay sets it on the
|
|
355
|
+
* request context of a PPR partial replay once its doc record hits, and
|
|
356
|
+
* restores the previous value after the match.
|
|
276
357
|
*/
|
|
277
358
|
_shellLoaderSeed?: Map<
|
|
278
359
|
string,
|
|
@@ -280,16 +361,17 @@ export interface RequestContext<
|
|
|
280
361
|
>;
|
|
281
362
|
|
|
282
363
|
/**
|
|
283
|
-
* @internal
|
|
284
|
-
*
|
|
364
|
+
* @internal The doc record marker: makes the next match treat the whole
|
|
365
|
+
* matched route as an implicit doc-level cache() boundary (see
|
|
285
366
|
* resolveShellImplicitCacheScope in cache/cache-scope.ts). Set ONLY on
|
|
286
367
|
* (a) the capture's derived context — with a record-only store so the
|
|
287
368
|
* capture's cacheRoute write lands in the snapshot, never the real store —
|
|
288
|
-
* (b) a HIT tail's seeded context, and (c) an eligible
|
|
289
|
-
* replay, where `store` is a request-local segment
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
369
|
+
* (b) a document HIT tail's seeded context (`docTail`), and (c) an eligible
|
|
370
|
+
* normal-route partial replay, where `store` is a request-local segment
|
|
371
|
+
* overlay.
|
|
372
|
+
* A document HIT tail replays the record even for a route with its own
|
|
373
|
+
* cache() config (the gate before the commit already evaluated that
|
|
374
|
+
* scope's opt-outs). Elsewhere the route's scope stays authoritative. On the
|
|
293
375
|
* navigation-replay serve path (`onExplicitHit` set) the marker COMPOSES with
|
|
294
376
|
* such a scope instead of being ignored: the seeded doc record supplies the match
|
|
295
377
|
* only when the explicit tier misses (withCacheLookup). cache(false) and a
|
|
@@ -306,12 +388,17 @@ export interface RequestContext<
|
|
|
306
388
|
* segment record even when the triggering request is partial.
|
|
307
389
|
*/
|
|
308
390
|
keyPrefix?: "doc";
|
|
309
|
-
/**
|
|
310
|
-
|
|
391
|
+
/**
|
|
392
|
+
* @internal Called only after the implicit cache hit decodes
|
|
393
|
+
* successfully; the lookup awaits it before returning the hit, so work
|
|
394
|
+
* it starts lands before the route's loaders resolve.
|
|
395
|
+
*/
|
|
396
|
+
onHit?: () => void | Promise<void>;
|
|
311
397
|
/**
|
|
312
398
|
* @internal Called when the seeded document record fails server-side
|
|
313
399
|
* deserialization. Partial replay uses it to replace the enclosing shell
|
|
314
|
-
* snapshot after serving the fresh fallback.
|
|
400
|
+
* snapshot after serving the fresh fallback. A document HIT tail does not
|
|
401
|
+
* set it: withCacheLookup throws ShellRecordUnavailableError instead.
|
|
315
402
|
*/
|
|
316
403
|
onCorrupt?: () => void;
|
|
317
404
|
/**
|
|
@@ -322,6 +409,21 @@ export interface RequestContext<
|
|
|
322
409
|
* require the exact consumable record instead of "any segment record".
|
|
323
410
|
*/
|
|
324
411
|
docKey?: string;
|
|
412
|
+
/**
|
|
413
|
+
* @internal Set on a document HIT tail (serveShellHit). The implicit doc
|
|
414
|
+
* scope replaces any route-derived cache() scope for the tail's match
|
|
415
|
+
* (resolveShellImplicitCacheScope), and a lookup that does not hit throws
|
|
416
|
+
* ShellRecordUnavailableError instead of resolving segments
|
|
417
|
+
* (withCacheLookup): a HIT never runs a handler.
|
|
418
|
+
*/
|
|
419
|
+
docTail?: true;
|
|
420
|
+
/**
|
|
421
|
+
* @internal The key a document HIT tail looks the doc record up by: the
|
|
422
|
+
* entry's own `docKey`. Recomputing it could name another key (a store
|
|
423
|
+
* keyGenerator folding request data, a build-time capture's synthetic
|
|
424
|
+
* host) and miss the record.
|
|
425
|
+
*/
|
|
426
|
+
fixedDocKey?: string;
|
|
325
427
|
/**
|
|
326
428
|
* @internal Set ONLY by matchPartialWithPprReplay on the navigation-replay
|
|
327
429
|
* serve path. Its presence arms the explicit-scope composition in
|
|
@@ -365,31 +467,16 @@ export interface RequestContext<
|
|
|
365
467
|
_shellFragmentPayload?: boolean;
|
|
366
468
|
|
|
367
469
|
/**
|
|
368
|
-
* @internal
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
* pushes (liveness unknowable at the barrier); (c) a handler-invoked loader
|
|
373
|
-
* executing during the capture (loader-resolution.ts — its consumption-lane
|
|
374
|
-
* value would freeze on a handler-free HIT). captureAndStoreShell folds it
|
|
375
|
-
* into ShellCacheEntry.handlerLiveHoles at the putShell barrier. Own
|
|
376
|
-
* property of the capture's derived context only.
|
|
377
|
-
*/
|
|
378
|
-
_shellCaptureHandleLiveness?: {
|
|
379
|
-
holes: boolean;
|
|
380
|
-
pendingPushes: number;
|
|
381
|
-
handlerInvokedLoader: boolean;
|
|
382
|
-
};
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* @internal Set (to the offending fn name) by the cookies()/headers()
|
|
386
|
-
* capture guard when it throws DURING a capture render. Load-bearing for the
|
|
470
|
+
* @internal Set by the capture guard (server/context.ts
|
|
471
|
+
* tripShellCaptureGuard) when it throws DURING a capture render: the read
|
|
472
|
+
* ("cookies()", "headers()", "ctx.get()", "ctx.theme") and the fix the
|
|
473
|
+
* refusal warning gives for it. Load-bearing for the
|
|
387
474
|
* bake lane: a guard throw inside an executing loader is swallowed by
|
|
388
475
|
* wrapLoaderPromise into per-loader error UI, which would otherwise bake
|
|
389
476
|
* silently into the shared shell — the capture checks this flag after the
|
|
390
477
|
* render and refuses instead. Deterministic, so the capture does not retry.
|
|
391
478
|
*/
|
|
392
|
-
_shellCaptureGuardTripped?: string;
|
|
479
|
+
_shellCaptureGuardTripped?: { surface: string; fix: string };
|
|
393
480
|
|
|
394
481
|
/**
|
|
395
482
|
* @internal The loader $$id whose BODY was executing when the capture guard
|
|
@@ -420,6 +507,23 @@ export interface RequestContext<
|
|
|
420
507
|
*/
|
|
421
508
|
_requestTags: Set<string>;
|
|
422
509
|
|
|
510
|
+
/**
|
|
511
|
+
* @internal This request can write a route cache() record: its match
|
|
512
|
+
* resolved a cache scope (match-api.ts). Arms the record tag owners
|
|
513
|
+
* (cache-tag.ts armRecordTagOwners); off, the segment scopes, loader links
|
|
514
|
+
* and owned tag sets are skipped entirely. A capture's or background
|
|
515
|
+
* re-render's derived context (Object.create) inherits it.
|
|
516
|
+
*/
|
|
517
|
+
_recordTagOwners?: true;
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* @internal This request resolves a loader bound with its own cache()
|
|
521
|
+
* (match-api.ts, or the binding's funnel). Arms the per-execution loader
|
|
522
|
+
* tag sets (cache-tag.ts armLoaderTagSets); off, a loader execution
|
|
523
|
+
* allocates no set and a read links nothing. Derived contexts inherit it.
|
|
524
|
+
*/
|
|
525
|
+
_recordLoaderTags?: true;
|
|
526
|
+
|
|
423
527
|
/** @internal Cache profiles for "use cache" profile resolution (per-router) */
|
|
424
528
|
_cacheProfiles?: Record<
|
|
425
529
|
string,
|
|
@@ -467,6 +571,11 @@ export interface RequestContext<
|
|
|
467
571
|
* Returns the theme value from the cookie, or the default theme if not set.
|
|
468
572
|
* This is the user's preference ("light", "dark", or "system"), not the resolved value.
|
|
469
573
|
*
|
|
574
|
+
* The visitor's cookie, so it is guarded like `cookies()`: reading it
|
|
575
|
+
* refuses a ppr shell capture and throws inside a `cache()` boundary or a
|
|
576
|
+
* `"use cache"` function (#971). A non-enumerable getter: a spread or
|
|
577
|
+
* `Object.assign` copy of the context does not carry it.
|
|
578
|
+
*
|
|
470
579
|
* @example
|
|
471
580
|
* ```typescript
|
|
472
581
|
* route("settings", (ctx) => {
|
|
@@ -475,7 +584,7 @@ export interface RequestContext<
|
|
|
475
584
|
* });
|
|
476
585
|
* ```
|
|
477
586
|
*/
|
|
478
|
-
theme?: Theme;
|
|
587
|
+
readonly theme?: Theme;
|
|
479
588
|
|
|
480
589
|
/**
|
|
481
590
|
* Set the theme (only available when theme is enabled in router config)
|
|
@@ -499,6 +608,15 @@ export interface RequestContext<
|
|
|
499
608
|
/** @internal Theme configuration (null if theme not enabled) */
|
|
500
609
|
_themeConfig?: ResolvedThemeConfig | null;
|
|
501
610
|
|
|
611
|
+
/**
|
|
612
|
+
* @internal The theme cookie (or the default) read with no identity guard.
|
|
613
|
+
* The guarded reads (`theme` here, the handler and middleware `ctx.theme`)
|
|
614
|
+
* call it after assertThemeReadAllowed; the router's own payload read goes
|
|
615
|
+
* through payloadInitialTheme (rsc/full-payload.ts). Undefined without
|
|
616
|
+
* theme config.
|
|
617
|
+
*/
|
|
618
|
+
_readTheme(): Theme | undefined;
|
|
619
|
+
|
|
502
620
|
/**
|
|
503
621
|
* Attach location state entries to the current response.
|
|
504
622
|
*
|
|
@@ -726,6 +844,21 @@ export interface RequestContext<
|
|
|
726
844
|
_cacheSignal?: import("../router/telemetry.js").CacheSegmentSignal[];
|
|
727
845
|
}
|
|
728
846
|
|
|
847
|
+
/**
|
|
848
|
+
* @internal The freshness window of a route cache() record, as ms
|
|
849
|
+
* timestamps: fresh until `freshUntil` (its `expiresAt`), stale until
|
|
850
|
+
* `staleUntil` (plus its scope's swr). `ttl`, `swr` and `written` describe
|
|
851
|
+
* the record that sets `staleUntil`: its cache() config, and whether this
|
|
852
|
+
* request wrote it (cacheRoute) rather than read it.
|
|
853
|
+
*/
|
|
854
|
+
export interface RouteRecordWindow {
|
|
855
|
+
freshUntil: number;
|
|
856
|
+
staleUntil: number;
|
|
857
|
+
ttl: number;
|
|
858
|
+
swr: number;
|
|
859
|
+
written: boolean;
|
|
860
|
+
}
|
|
861
|
+
|
|
729
862
|
/**
|
|
730
863
|
* Public view of RequestContext, without internal methods and fields.
|
|
731
864
|
*
|
|
@@ -751,16 +884,23 @@ export type PublicRequestContext<
|
|
|
751
884
|
| "_shellImplicitCache"
|
|
752
885
|
| "_shellLoaderSeed"
|
|
753
886
|
| "_shellCaptureLoaderRecords"
|
|
754
|
-
| "
|
|
887
|
+
| "_shellCaptureUnrecordedLoaderPush"
|
|
888
|
+
| "_resolvedCacheKeys"
|
|
889
|
+
| "_shellKey"
|
|
890
|
+
| "_shellForcedMiss"
|
|
891
|
+
| "_routeRecordWindow"
|
|
755
892
|
| "_shellFragmentPayload"
|
|
756
893
|
| "_shellCaptureGuardTripped"
|
|
757
894
|
| "_shellCaptureGuardTrippedLoaderId"
|
|
758
895
|
| "_explicitTaggedStores"
|
|
759
896
|
| "_requestTags"
|
|
897
|
+
| "_recordTagOwners"
|
|
898
|
+
| "_recordLoaderTags"
|
|
760
899
|
| "_cacheProfiles"
|
|
761
900
|
| "_onResponseCallbacks"
|
|
762
901
|
| "_pendingBackgroundTasks"
|
|
763
902
|
| "_themeConfig"
|
|
903
|
+
| "_readTheme"
|
|
764
904
|
| "_locationState"
|
|
765
905
|
| "_routeName"
|
|
766
906
|
| "_prevRouteKey"
|
|
@@ -795,6 +935,11 @@ export type PublicRequestContext<
|
|
|
795
935
|
| "_setStatus"
|
|
796
936
|
| "_rotateStateCookie"
|
|
797
937
|
| "_setKeepCacheDirective"
|
|
938
|
+
| "_freshReads"
|
|
939
|
+
| "_setFreshReadsCookie"
|
|
940
|
+
| "_requestRoot"
|
|
941
|
+
| "_tagInvalidations"
|
|
942
|
+
| "_responseSent"
|
|
798
943
|
| "_variables"
|
|
799
944
|
| "_classifiedRoute"
|
|
800
945
|
| "_requestMode"
|
|
@@ -991,6 +1136,10 @@ export function createRequestContext<TEnv>(
|
|
|
991
1136
|
} = options;
|
|
992
1137
|
const cookieHeader = request.headers.get("Cookie");
|
|
993
1138
|
let rangoStateRotated = false;
|
|
1139
|
+
const freshCookie = stateCookieName
|
|
1140
|
+
? freshReadsCookieName(stateCookieName)
|
|
1141
|
+
: undefined;
|
|
1142
|
+
let freshCookieMaxAge = 0;
|
|
994
1143
|
let parsedCookies: Record<string, string> | null = null;
|
|
995
1144
|
|
|
996
1145
|
let stubResponse = initialResponse
|
|
@@ -1144,7 +1293,7 @@ export function createRequestContext<TEnv>(
|
|
|
1144
1293
|
_dynamic: false,
|
|
1145
1294
|
get: ((keyOrVar: any) => {
|
|
1146
1295
|
if (isNonCacheable(variables, keyOrVar)) {
|
|
1147
|
-
assertNonCacheableReadAllowed(keyOrVar);
|
|
1296
|
+
assertNonCacheableReadAllowed(keyOrVar, _getRequestContext());
|
|
1148
1297
|
}
|
|
1149
1298
|
return contextGet(variables, keyOrVar);
|
|
1150
1299
|
}) as RequestContext<TEnv>["get"],
|
|
@@ -1254,6 +1403,48 @@ export function createRequestContext<TEnv>(
|
|
|
1254
1403
|
rawStubHeaders.set(KEEP_CACHE_HEADER, "1");
|
|
1255
1404
|
},
|
|
1256
1405
|
|
|
1406
|
+
// The fresh-reads cookie (browser/cookie-name.ts): one Set-Cookie per
|
|
1407
|
+
// response, the longest Max-Age any invalidation asked for. HttpOnly (the
|
|
1408
|
+
// client never reads it), SameSite=Lax, Path=/. rawStubHeaders: an
|
|
1409
|
+
// internal writer, allowed wherever updateTag()/revalidateTag() are. A
|
|
1410
|
+
// shared cache never stores it (Set-Cookie is a per-client signal).
|
|
1411
|
+
_setFreshReadsCookie(maxAgeMs: number): void {
|
|
1412
|
+
if (!freshCookie) return;
|
|
1413
|
+
const maxAge = Math.ceil(maxAgeMs / 1000);
|
|
1414
|
+
// Already set for at least this long (an earlier invalidation in this
|
|
1415
|
+
// request): nothing to add, sent or not.
|
|
1416
|
+
if (maxAge <= freshCookieMaxAge) return;
|
|
1417
|
+
if (ctx._responseSent) {
|
|
1418
|
+
// A streaming loader or render: the headers already left. The
|
|
1419
|
+
// invalidation itself still ran; only the mutating user's bypass of
|
|
1420
|
+
// the stores' isolate memos is lost.
|
|
1421
|
+
if (process.env.NODE_ENV !== "production") {
|
|
1422
|
+
console.warn(
|
|
1423
|
+
`[rango] updateTag()/revalidateTag() ran after the response ` +
|
|
1424
|
+
`headers were sent (from a streaming loader or render), so the ` +
|
|
1425
|
+
`${freshCookie} cookie was not set: this user's next requests ` +
|
|
1426
|
+
`can be served from a store's isolate memo for a few seconds. ` +
|
|
1427
|
+
`Invalidate in a server action, a route handler, or middleware.`,
|
|
1428
|
+
);
|
|
1429
|
+
}
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
const serialized = (age: number) =>
|
|
1433
|
+
`${freshCookie}=1; Max-Age=${age}; Path=/; HttpOnly; SameSite=Lax` +
|
|
1434
|
+
(url.protocol === "https:" ? "; Secure" : "");
|
|
1435
|
+
if (freshCookieMaxAge > 0) {
|
|
1436
|
+
const prior = serialized(freshCookieMaxAge);
|
|
1437
|
+
const kept = rawStubHeaders
|
|
1438
|
+
.getSetCookie()
|
|
1439
|
+
.filter((cookie) => cookie !== prior);
|
|
1440
|
+
rawStubHeaders.delete("Set-Cookie");
|
|
1441
|
+
for (const cookie of kept) rawStubHeaders.append("Set-Cookie", cookie);
|
|
1442
|
+
}
|
|
1443
|
+
freshCookieMaxAge = maxAge;
|
|
1444
|
+
rawStubHeaders.append("Set-Cookie", serialized(maxAge));
|
|
1445
|
+
invalidateResponseCookieCache();
|
|
1446
|
+
},
|
|
1447
|
+
|
|
1257
1448
|
setStatus(status: number): void {
|
|
1258
1449
|
assertNotInsideCacheExec(ctx, "setStatus");
|
|
1259
1450
|
assertResponseWriteAllowed("setStatus");
|
|
@@ -1275,6 +1466,9 @@ export function createRequestContext<TEnv>(
|
|
|
1275
1466
|
_explicitTaggedStores: explicitTaggedStores,
|
|
1276
1467
|
_requestTags: new Set<string>(),
|
|
1277
1468
|
_cacheProfiles: cacheProfiles,
|
|
1469
|
+
_freshReads:
|
|
1470
|
+
freshCookie !== undefined &&
|
|
1471
|
+
getRawCookieValue(cookieHeader, freshCookie) !== null,
|
|
1278
1472
|
|
|
1279
1473
|
waitUntil(fn: () => Promise<void>): void {
|
|
1280
1474
|
if (ctx.build) return;
|
|
@@ -1316,9 +1510,8 @@ export function createRequestContext<TEnv>(
|
|
|
1316
1510
|
this._onResponseCallbacks.push(callback);
|
|
1317
1511
|
},
|
|
1318
1512
|
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
},
|
|
1513
|
+
// theme: a guarded, non-enumerable getter defined after this literal.
|
|
1514
|
+
_readTheme: getTheme,
|
|
1322
1515
|
setTheme: themeConfig
|
|
1323
1516
|
? (theme: Theme) => {
|
|
1324
1517
|
assertNotInsideCacheExec(ctx, "setTheme");
|
|
@@ -1359,7 +1552,39 @@ export function createRequestContext<TEnv>(
|
|
|
1359
1552
|
getContext: () => ctx,
|
|
1360
1553
|
});
|
|
1361
1554
|
|
|
1555
|
+
// getRequestContext().theme is the visitor's cookie, guarded like cookies()
|
|
1556
|
+
// (#971); before it was a plain read that a ppr shell, a cache() entry or a
|
|
1557
|
+
// "use cache" entry stored for every visitor. Non-enumerable like the
|
|
1558
|
+
// handler ctx.theme (handler-context.ts THEME_DESCRIPTOR): serializing the
|
|
1559
|
+
// context (React dev debug info) must not count as a read. The guard reads
|
|
1560
|
+
// the ambient context, as cookies() does, and falls back to the receiver.
|
|
1561
|
+
// _readTheme is the unguarded read: non-enumerable too, so a spread copy of
|
|
1562
|
+
// the context (the fetchable-loader ctx, loader-fetch.ts) cannot carry it.
|
|
1563
|
+
Object.defineProperties(ctx, {
|
|
1564
|
+
theme: {
|
|
1565
|
+
get(this: RequestContext<TEnv>): Theme | undefined {
|
|
1566
|
+
if (!themeConfig) return undefined;
|
|
1567
|
+
assertThemeReadAllowed(
|
|
1568
|
+
_getRequestContext() ?? this,
|
|
1569
|
+
"getRequestContext().theme",
|
|
1570
|
+
);
|
|
1571
|
+
return getTheme();
|
|
1572
|
+
},
|
|
1573
|
+
enumerable: false,
|
|
1574
|
+
configurable: true,
|
|
1575
|
+
},
|
|
1576
|
+
_readTheme: { enumerable: false },
|
|
1577
|
+
});
|
|
1578
|
+
|
|
1362
1579
|
(ctx as any)[NOCACHE_SYMBOL] = true;
|
|
1580
|
+
// Non-enumerable: a self-reference would make the context circular for
|
|
1581
|
+
// JSON.stringify, spreads and React's dev debug-info serialization of props.
|
|
1582
|
+
Object.defineProperty(ctx, "_requestRoot", {
|
|
1583
|
+
value: ctx,
|
|
1584
|
+
enumerable: false,
|
|
1585
|
+
configurable: true,
|
|
1586
|
+
writable: true,
|
|
1587
|
+
});
|
|
1363
1588
|
return ctx;
|
|
1364
1589
|
}
|
|
1365
1590
|
|
package/src/ssr/index.tsx
CHANGED
|
@@ -2,7 +2,11 @@ import React from "react";
|
|
|
2
2
|
import { createSsrRootComponent, deserializeSsrPayload } from "./ssr-root.js";
|
|
3
3
|
import { injectRSCPayloadEager } from "./inject-rsc-eager.js";
|
|
4
4
|
import { runWithPreinitNonce } from "./preinit-client-references.js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
POST_QUIESCE_TASK_HOPS,
|
|
7
|
+
SHELL_CAPTURE_MAX_WAIT_MS,
|
|
8
|
+
} from "../rsc/shell-capture-constants.js";
|
|
9
|
+
import { isThenable } from "../handles/is-thenable.js";
|
|
6
10
|
import type { ErrorPhase } from "../types.js";
|
|
7
11
|
import type { HeadScriptsOption } from "../vite/plugin-types.js";
|
|
8
12
|
|
|
@@ -237,28 +241,64 @@ export interface SSRDependencies<TEnv = unknown> {
|
|
|
237
241
|
}
|
|
238
242
|
|
|
239
243
|
/**
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
* real module-runner I/O in dev (100ms+ cold), which no fixed count of near-
|
|
255
|
-
* zero-cost task hops can buy. captureShellHTML therefore awaits the payload-
|
|
256
|
-
* settled signal (SsrRootOptions.onPayloadSettled, deadline-bounded) between
|
|
257
|
-
* quiesce and these hops; the hops then only flush the settled tree and mark
|
|
258
|
-
* pending boundaries POSTPONED. Still task-based (masked loaders never emit,
|
|
259
|
-
* so more hops never lets a hole settle). Bounded by maxWaitMs end to end.
|
|
244
|
+
* One set per running capture, collecting the client-reference module loads
|
|
245
|
+
* requested while it runs (captureClientLoads). The Flight client resolves a
|
|
246
|
+
* client component used as an element type lazily, so the payload settles
|
|
247
|
+
* while its module is still loading, and a shell that renders it outside any
|
|
248
|
+
* Suspense boundary cannot complete until the load does. On the
|
|
249
|
+
* cloudflare-basic build a cold load (the module, its imports, its CSS
|
|
250
|
+
* virtual) settled at +890.1ms, after the payload (+873.4ms) and 1ms before
|
|
251
|
+
* the abort, and uninstrumented builds lost that race (issue #949). The
|
|
252
|
+
* Flight client asks the loader again for every import row, even for a
|
|
253
|
+
* module another render is already loading, so a capture sees each load it
|
|
254
|
+
* needs; one a concurrent render requests meanwhile is collected too and is
|
|
255
|
+
* the same bounded I/O. The sets are per capture, their contents isolate-wide:
|
|
256
|
+
* a load that never settles can only hold the captures running when it was
|
|
257
|
+
* requested, each until its deadline.
|
|
260
258
|
*/
|
|
261
|
-
const
|
|
259
|
+
const activeCaptureClientLoads = new Set<Set<PromiseLike<unknown>>>();
|
|
260
|
+
|
|
261
|
+
/** The loader wrapper {@link captureClientLoads} last installed. */
|
|
262
|
+
let recordingClientReferenceLoader: ((id: string) => unknown) | undefined;
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Start collecting the client-reference loads requested during a capture;
|
|
266
|
+
* call the returned `stop` when the capture ends. Wraps plugin-rsc's SSR
|
|
267
|
+
* loader (`globalThis.__vite_rsc_client_require__`, which
|
|
268
|
+
* `__vite_rsc_require__` reads per call) on the first capture, so an isolate
|
|
269
|
+
* that never captures keeps the bare loader. Wraps again when another wrapper
|
|
270
|
+
* replaced this one (the build shell phase bridges hashed ids the same way,
|
|
271
|
+
* vite/discovery/shell-prerender-phase.ts). Collects nothing without the
|
|
272
|
+
* loader.
|
|
273
|
+
*/
|
|
274
|
+
function captureClientLoads(): {
|
|
275
|
+
loads: Set<PromiseLike<unknown>>;
|
|
276
|
+
stop: () => void;
|
|
277
|
+
} {
|
|
278
|
+
const g = globalThis as {
|
|
279
|
+
__vite_rsc_client_require__?: (id: string) => unknown;
|
|
280
|
+
};
|
|
281
|
+
const load = g.__vite_rsc_client_require__;
|
|
282
|
+
if (typeof load === "function" && load !== recordingClientReferenceLoader) {
|
|
283
|
+
recordingClientReferenceLoader = (id) => {
|
|
284
|
+
const loading = load(id);
|
|
285
|
+
// plugin-rsc memoizes one promise per id and the Flight client stamps
|
|
286
|
+
// status "fulfilled" on it once loaded: a warm module is not collected.
|
|
287
|
+
if (
|
|
288
|
+
activeCaptureClientLoads.size > 0 &&
|
|
289
|
+
isThenable(loading) &&
|
|
290
|
+
(loading as { status?: string }).status !== "fulfilled"
|
|
291
|
+
) {
|
|
292
|
+
for (const loads of activeCaptureClientLoads) loads.add(loading);
|
|
293
|
+
}
|
|
294
|
+
return loading;
|
|
295
|
+
};
|
|
296
|
+
g.__vite_rsc_client_require__ = recordingClientReferenceLoader;
|
|
297
|
+
}
|
|
298
|
+
const loads = new Set<PromiseLike<unknown>>();
|
|
299
|
+
activeCaptureClientLoads.add(loads);
|
|
300
|
+
return { loads, stop: () => activeCaptureClientLoads.delete(loads) };
|
|
301
|
+
}
|
|
262
302
|
|
|
263
303
|
/**
|
|
264
304
|
* Route an SSR error through the deps.onError notification callback with the
|
|
@@ -421,6 +461,13 @@ interface ShellCaptureOptions {
|
|
|
421
461
|
* refuses to store the shell (issue #915).
|
|
422
462
|
*/
|
|
423
463
|
onError?: (error: unknown) => void;
|
|
464
|
+
/**
|
|
465
|
+
* Called with React's errorInfo for each task still pending when the
|
|
466
|
+
* capture's own abort froze the prelude; its componentStack is computed
|
|
467
|
+
* when read. The dev no-shell warning prints the stacks: one of them is what
|
|
468
|
+
* suspended above <body> (issue #941).
|
|
469
|
+
*/
|
|
470
|
+
onAbortedTask?: (errorInfo: { componentStack?: string } | undefined) => void;
|
|
424
471
|
}
|
|
425
472
|
|
|
426
473
|
/**
|
|
@@ -761,12 +808,13 @@ export function createShellCaptureHandler<TEnv = unknown>(
|
|
|
761
808
|
const deadline = (await isDebuggerAttached())
|
|
762
809
|
? { promise: new Promise<void>(() => {}), cancel: () => {} }
|
|
763
810
|
: createCancelableTimeout(maxWaitMs);
|
|
811
|
+
const clientLoads = captureClientLoads();
|
|
764
812
|
try {
|
|
765
813
|
// No nonce (nonce'd requests never reach capture); no formState.
|
|
766
|
-
// payloadSettled: fires when the Flight payload root settles
|
|
767
|
-
//
|
|
768
|
-
//
|
|
769
|
-
//
|
|
814
|
+
// payloadSettled: fires when the Flight payload root settles and fizz
|
|
815
|
+
// can emit the tree. The abort below gates on it and on the
|
|
816
|
+
// client-reference loads in flight in the isolate (bounded by the same
|
|
817
|
+
// deadline): Flight byte-quiet alone is NOT render-readiness.
|
|
770
818
|
let settlePayload!: () => void;
|
|
771
819
|
const payloadSettled = new Promise<void>((resolve) => {
|
|
772
820
|
settlePayload = resolve;
|
|
@@ -823,8 +871,9 @@ export function createShellCaptureHandler<TEnv = unknown>(
|
|
|
823
871
|
// render errors are NOT our sentinel and still surface through
|
|
824
872
|
// deps.onError, the same channel renderHTML uses. See
|
|
825
873
|
// docs/design/ppr-shell-resume.md.
|
|
826
|
-
onError: (error: unknown) => {
|
|
874
|
+
onError: (error: unknown, errorInfo?: { componentStack?: string }) => {
|
|
827
875
|
if (error === abortReason) {
|
|
876
|
+
opts.onAbortedTask?.(errorInfo);
|
|
828
877
|
return;
|
|
829
878
|
}
|
|
830
879
|
opts.onError?.(error);
|
|
@@ -846,25 +895,34 @@ export function createShellCaptureHandler<TEnv = unknown>(
|
|
|
846
895
|
// shell that never goes quiet (a root postpone / hung handle).
|
|
847
896
|
await Promise.race([opts.quiesce, deadline.promise]);
|
|
848
897
|
// Then wait for fizz RENDER-READINESS, bounded by the same deadline:
|
|
849
|
-
// the payload root settles
|
|
850
|
-
//
|
|
851
|
-
//
|
|
898
|
+
// the payload root settles, then the client-reference module loads the
|
|
899
|
+
// capture started settle — a client component used as an element type
|
|
900
|
+
// resolves lazily, after the payload (real module-runner I/O in dev;
|
|
901
|
+
// 100ms+ on a cold graph). Flight byte-quiet does NOT imply this — a fully
|
|
852
902
|
// REPLAYED (prerendered) route's Flight stream finishes in ~1-3ms and
|
|
853
903
|
// an abort taken on quiet-plus-task-hops alone landed BEFORE fizz could
|
|
854
904
|
// emit <html>, freezing a zero-byte prelude: the eternal-MISS shape
|
|
855
905
|
// this route class showed on every cold graph (dev cold boots, GH
|
|
856
906
|
// runners) while ordinary routes — whose live handler execution keeps
|
|
857
|
-
// Flight noisy long enough — never hit it.
|
|
858
|
-
// block
|
|
859
|
-
//
|
|
860
|
-
//
|
|
861
|
-
//
|
|
862
|
-
//
|
|
907
|
+
// Flight noisy long enough — never hit it. Holes the payload carries
|
|
908
|
+
// block neither wait (masked live loaders and loader-nested promises postpone
|
|
909
|
+
// below the root and load no module; the Flight input stays frozen),
|
|
910
|
+
// so this await costs a genuinely hole-y shell nothing; a payload that
|
|
911
|
+
// NEVER settles (hung handles) degrades at the deadline exactly as
|
|
912
|
+
// before. A prerender that SETTLES first (early success or a hard
|
|
913
|
+
// rejection) ends the wait immediately — fizz is already done either
|
|
914
|
+
// way.
|
|
863
915
|
const prerenderSettled = prerenderPromise.then(
|
|
864
916
|
() => {},
|
|
865
917
|
() => {},
|
|
866
918
|
);
|
|
867
|
-
|
|
919
|
+
// The capture's loads all started when their import rows were parsed,
|
|
920
|
+
// before the Flight input froze.
|
|
921
|
+
await Promise.race([
|
|
922
|
+
payloadSettled.then(() => Promise.allSettled(clientLoads.loads)),
|
|
923
|
+
prerenderSettled,
|
|
924
|
+
deadline.promise,
|
|
925
|
+
]);
|
|
868
926
|
// Fixed task hops before the abort: give React's fizz worker turns to flush
|
|
869
927
|
// the now-complete shell and mark the still-pending boundaries as POSTPONED
|
|
870
928
|
// rather than errored. Deterministic (the byte set is already frozen and
|
|
@@ -923,6 +981,7 @@ export function createShellCaptureHandler<TEnv = unknown>(
|
|
|
923
981
|
};
|
|
924
982
|
} finally {
|
|
925
983
|
deadline.cancel();
|
|
984
|
+
clientLoads.stop();
|
|
926
985
|
}
|
|
927
986
|
};
|
|
928
987
|
}
|