@rangojs/router 0.6.0 → 0.8.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/types/cache/cf/cf-cache-store.d.ts +30 -16
- package/dist/types/cache/shell-snapshot.d.ts +2 -2
- package/dist/types/cache/types.d.ts +26 -7
- package/dist/types/client-urls/server-projection.d.ts +10 -1
- package/dist/types/client-urls/types.d.ts +5 -2
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -9
- package/dist/types/rsc/shell-capture.d.ts +5 -4
- package/dist/types/urls/path-helper-types.d.ts +4 -1
- package/dist/types/vite/plugins/client-ref-dedup.d.ts +0 -11
- package/dist/vite/index.js +360 -108
- package/package.json +25 -25
- package/skills/client-urls/SKILL.md +9 -9
- package/skills/cloudflare/SKILL.md +5 -3
- package/skills/ppr/SKILL.md +11 -7
- package/src/cache/cf/cf-cache-store.ts +118 -62
- package/src/cache/shell-snapshot.ts +2 -2
- package/src/cache/types.ts +27 -7
- package/src/client-urls/client-urls.ts +78 -0
- package/src/client-urls/server-projection.ts +95 -4
- package/src/client-urls/types.ts +8 -2
- package/src/router/segment-resolution/fresh.ts +8 -2
- package/src/router/segment-resolution/loader-cache.ts +34 -13
- package/src/router/segment-resolution/loader-mask.ts +9 -9
- package/src/router/segment-resolution/revalidation.ts +3 -1
- package/src/rsc/shell-build-manifest.ts +13 -6
- package/src/rsc/shell-capture.ts +22 -5
- package/src/urls/path-helper-types.ts +4 -1
- package/src/vite/plugins/client-ref-dedup.ts +281 -19
- package/src/vite/plugins/expose-action-id.ts +45 -23
|
@@ -7,7 +7,11 @@ import {
|
|
|
7
7
|
type InterceptSelectorContext,
|
|
8
8
|
} from "../server/context.js";
|
|
9
9
|
import type { LoaderDefinition, TrailingSlashMode } from "../types.js";
|
|
10
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
PartialPrerenderProps,
|
|
12
|
+
PathOptions,
|
|
13
|
+
UrlPatterns,
|
|
14
|
+
} from "../urls/pattern-types.js";
|
|
11
15
|
import type { PathHelpers } from "../urls/path-helper-types.js";
|
|
12
16
|
import type { AllUseItems } from "../route-types.js";
|
|
13
17
|
import { urls } from "../urls/urls-function.js";
|
|
@@ -45,6 +49,14 @@ const PROJECTION_OPTIONS = new Set<PropertyKey>([
|
|
|
45
49
|
"name",
|
|
46
50
|
"search",
|
|
47
51
|
"trailingSlash",
|
|
52
|
+
"ppr",
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
const PPR_NUMBER_KEYS = new Set<PropertyKey>([
|
|
56
|
+
"ttl",
|
|
57
|
+
"swr",
|
|
58
|
+
"captureTimeout",
|
|
59
|
+
"maxSnapshotBytes",
|
|
48
60
|
]);
|
|
49
61
|
|
|
50
62
|
const clientUrlProjections = new Map<string, ClientUrlProjection>();
|
|
@@ -59,6 +71,15 @@ export type ClientUrlDefinitionSource = ClientUrlReference | ClientUrlPatterns;
|
|
|
59
71
|
export interface ClientUrlProjectionOptions {
|
|
60
72
|
readonly search?: Readonly<Record<string, SearchSchemaValue>>;
|
|
61
73
|
readonly trailingSlash?: TrailingSlashMode;
|
|
74
|
+
/**
|
|
75
|
+
* The `ppr` path option, JSON-projected. Materialization passes it onto
|
|
76
|
+
* the group route's server `path()` verbatim, so the manifest entry
|
|
77
|
+
* carries it and the runtime shell capture/serve lanes engage exactly as
|
|
78
|
+
* for a hand-written server route (search is part of shell identity —
|
|
79
|
+
* the capture render seeds the key's own search, so group static parts
|
|
80
|
+
* may read useSearchParams).
|
|
81
|
+
*/
|
|
82
|
+
readonly ppr?: true | Readonly<PartialPrerenderProps>;
|
|
62
83
|
}
|
|
63
84
|
|
|
64
85
|
export interface ClientUrlProjectionRoute {
|
|
@@ -138,9 +159,6 @@ function serializeOptions(
|
|
|
138
159
|
if (!source) return Object.freeze({});
|
|
139
160
|
|
|
140
161
|
for (const key of Reflect.ownKeys(source)) {
|
|
141
|
-
if (key === "ppr") {
|
|
142
|
-
throw projectionError(route, 'path option "ppr" is not supported');
|
|
143
|
-
}
|
|
144
162
|
if (!PROJECTION_OPTIONS.has(key)) {
|
|
145
163
|
throw projectionError(
|
|
146
164
|
route,
|
|
@@ -152,6 +170,7 @@ function serializeOptions(
|
|
|
152
170
|
const options: {
|
|
153
171
|
search?: Readonly<Record<string, SearchSchemaValue>>;
|
|
154
172
|
trailingSlash?: TrailingSlashMode;
|
|
173
|
+
ppr?: true | Readonly<PartialPrerenderProps>;
|
|
155
174
|
} = {};
|
|
156
175
|
if (source.search !== undefined) {
|
|
157
176
|
options.search = serializeSearchSchema(route, source.search);
|
|
@@ -159,9 +178,72 @@ function serializeOptions(
|
|
|
159
178
|
if (source.trailingSlash !== undefined) {
|
|
160
179
|
options.trailingSlash = source.trailingSlash;
|
|
161
180
|
}
|
|
181
|
+
if (source.ppr !== undefined) {
|
|
182
|
+
options.ppr = serializePpr(route, source.ppr);
|
|
183
|
+
}
|
|
162
184
|
return Object.freeze(options);
|
|
163
185
|
}
|
|
164
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Serialize the `ppr` path option for the projection. The DSL already
|
|
189
|
+
* validated shape in the authoring module (validateClientPpr in
|
|
190
|
+
* client-urls.ts); this re-validates independently and re-copies
|
|
191
|
+
* defensively — the projection must carry only JSON, and the copy keeps a
|
|
192
|
+
* later mutation of the consumer's options object from leaking into the
|
|
193
|
+
* frozen projection.
|
|
194
|
+
*/
|
|
195
|
+
function serializePpr(
|
|
196
|
+
route: ClientUrlRouteRecord,
|
|
197
|
+
source: unknown,
|
|
198
|
+
): true | Readonly<PartialPrerenderProps> {
|
|
199
|
+
if (source === true) return true;
|
|
200
|
+
if (typeof source !== "object" || source === null || Array.isArray(source)) {
|
|
201
|
+
throw projectionError(
|
|
202
|
+
route,
|
|
203
|
+
'path option "ppr" must be true or a PartialPrerenderProps object',
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
const out: {
|
|
207
|
+
ttl?: number;
|
|
208
|
+
swr?: number;
|
|
209
|
+
captureTimeout?: number;
|
|
210
|
+
maxSnapshotBytes?: number;
|
|
211
|
+
tags?: readonly string[];
|
|
212
|
+
} = {};
|
|
213
|
+
for (const key of Reflect.ownKeys(source)) {
|
|
214
|
+
if (PPR_NUMBER_KEYS.has(key)) {
|
|
215
|
+
const value = (source as Record<PropertyKey, unknown>)[key];
|
|
216
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
217
|
+
throw projectionError(
|
|
218
|
+
route,
|
|
219
|
+
`path option "ppr.${String(key)}" must be a finite number`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
out[key as "ttl" | "swr" | "captureTimeout" | "maxSnapshotBytes"] = value;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (key === "tags") {
|
|
226
|
+
const tags = (source as { tags: unknown }).tags;
|
|
227
|
+
if (
|
|
228
|
+
!Array.isArray(tags) ||
|
|
229
|
+
tags.some((tag) => typeof tag !== "string" || tag === "")
|
|
230
|
+
) {
|
|
231
|
+
throw projectionError(
|
|
232
|
+
route,
|
|
233
|
+
'path option "ppr.tags" must be an array of non-empty strings',
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
out.tags = Object.freeze([...tags]);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
throw projectionError(
|
|
240
|
+
route,
|
|
241
|
+
`path option "ppr.${String(key)}" is not supported`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
return Object.freeze(out) as Readonly<PartialPrerenderProps>;
|
|
245
|
+
}
|
|
246
|
+
|
|
165
247
|
function serializeTransition(
|
|
166
248
|
route: ClientUrlRouteRecord,
|
|
167
249
|
): Readonly<ClientTransitionConfig> | undefined {
|
|
@@ -393,6 +475,15 @@ function materializedPathOptions(route: ClientUrlProjectionRoute): PathOptions {
|
|
|
393
475
|
...(route.options.trailingSlash
|
|
394
476
|
? { trailingSlash: route.options.trailingSlash }
|
|
395
477
|
: {}),
|
|
478
|
+
// ppr rides onto the materialized server path() verbatim: path-helper
|
|
479
|
+
// stamps it on the manifest entry, so resolvePprConfig classifies the
|
|
480
|
+
// group route exactly like a hand-written ppr page. Shallow-copy the
|
|
481
|
+
// object form — path-helper receives a mutable PathOptions.
|
|
482
|
+
...(route.options.ppr !== undefined
|
|
483
|
+
? {
|
|
484
|
+
ppr: route.options.ppr === true ? true : { ...route.options.ppr },
|
|
485
|
+
}
|
|
486
|
+
: {}),
|
|
396
487
|
};
|
|
397
488
|
}
|
|
398
489
|
|
package/src/client-urls/types.ts
CHANGED
|
@@ -28,7 +28,10 @@ export type ClientUrlUse = () => ClientUrlItems;
|
|
|
28
28
|
export type ClientPathOptions<
|
|
29
29
|
TName extends string = string,
|
|
30
30
|
TSearch extends SearchSchema = SearchSchema,
|
|
31
|
-
> = Pick<
|
|
31
|
+
> = Pick<
|
|
32
|
+
PathOptions<TName, TSearch>,
|
|
33
|
+
"name" | "search" | "trailingSlash" | "ppr"
|
|
34
|
+
>;
|
|
32
35
|
|
|
33
36
|
export type ClientPathFn = <
|
|
34
37
|
const TPattern extends string,
|
|
@@ -141,7 +144,10 @@ export interface ClientUrlHelpers {
|
|
|
141
144
|
* Pass `{ stream: "navigation" }` to await this loader before first flush
|
|
142
145
|
* on DOCUMENT requests (see {@link LoaderOptions}) — the opt-in for loaders
|
|
143
146
|
* whose data, handle pushes, or thrown notFound()/redirect() must be in the
|
|
144
|
-
* SSR'd HTML. Per-loader: a dynamic sibling keeps streaming.
|
|
147
|
+
* SSR'd HTML. Per-loader: a dynamic sibling keeps streaming. Under a
|
|
148
|
+
* `ppr` group route the flag BAKES: the loader executes at shell capture
|
|
149
|
+
* and its settled return freezes into the shell (nested promises stay
|
|
150
|
+
* live holes).
|
|
145
151
|
*/
|
|
146
152
|
readonly loader: <TData>(
|
|
147
153
|
definition: LoaderDefinition<TData>,
|
|
@@ -148,7 +148,13 @@ export async function resolveLoaders<TEnv>(
|
|
|
148
148
|
loaderEntry,
|
|
149
149
|
ctx,
|
|
150
150
|
ctx.pathname,
|
|
151
|
-
|
|
151
|
+
// The bake key rides for flagged loaders too: stream:"navigation"
|
|
152
|
+
// bakes at capture regardless of the entry's loading() lane
|
|
153
|
+
// (loader-cache.ts capture branch) and its HIT-tail seed
|
|
154
|
+
// overlay needs the same key.
|
|
155
|
+
bakeLane || loaderEntry.awaitBeforeFlush === true
|
|
156
|
+
? segmentId
|
|
157
|
+
: null,
|
|
152
158
|
),
|
|
153
159
|
),
|
|
154
160
|
entry,
|
|
@@ -191,7 +197,7 @@ export async function resolveLoaders<TEnv>(
|
|
|
191
197
|
loaderEntry,
|
|
192
198
|
ctx,
|
|
193
199
|
ctx.pathname,
|
|
194
|
-
bakeLane ? segmentId : null,
|
|
200
|
+
bakeLane || loaderEntry.awaitBeforeFlush === true ? segmentId : null,
|
|
195
201
|
),
|
|
196
202
|
),
|
|
197
203
|
entry,
|
|
@@ -157,19 +157,40 @@ export function resolveLoaderData<TEnv>(
|
|
|
157
157
|
// shell HIT the recorded container is overlaid onto the fresh run so the
|
|
158
158
|
// payload matches the frozen prelude byte-for-byte.
|
|
159
159
|
if (isShellCaptureActive(reqCtx)) {
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
// the
|
|
160
|
+
// Capture lane, per LOADER (not per entry):
|
|
161
|
+
//
|
|
162
|
+
// - `stream: "navigation"` (awaitBeforeFlush) — the BAKE lane. The flag's
|
|
163
|
+
// document promise is "this loader's data is in the HTML before first
|
|
164
|
+
// flush"; under ppr the pre-flush HTML IS the frozen prelude, so the
|
|
165
|
+
// loader executes at capture and its SETTLED return bakes into the
|
|
166
|
+
// shell. Nested-promise SHAPE stays the liveness declaration: nested
|
|
167
|
+
// thenables are masked so their subtrees postpone as holes (per-request
|
|
168
|
+
// material must be promise-shaped — the #692 cross-session scar). The
|
|
169
|
+
// masked container registers on _shellCaptureLoaderRecords: it holds
|
|
170
|
+
// the capture gate (bounded by ppr.captureTimeout) and pins into the
|
|
171
|
+
// shell snapshot, so a HIT tail replays the baked value and the
|
|
172
|
+
// hydration payload matches the frozen prelude byte-for-byte.
|
|
173
|
+
// - Every other loader — LIVE unconditionally: masked with a
|
|
174
|
+
// never-resolving promise, postponed at its boundary (loading() or an
|
|
175
|
+
// inline Suspense), streamed fresh per request. A masked reader with NO
|
|
176
|
+
// boundary above it root-postpones and the <body> sanity gate refuses
|
|
177
|
+
// the shell (eternal-MISS warning) — the degrade for boundary-less ppr
|
|
178
|
+
// routes. A route whose loaders are ALL flagged therefore needs no
|
|
179
|
+
// loading() at all: nothing masks, the shell captures complete.
|
|
180
|
+
if (loaderEntry.awaitBeforeFlush && bakeSegmentKey) {
|
|
181
|
+
const containerPromise = executeLoaderData(loaderEntry, ctx, pathname);
|
|
182
|
+
// Pre-attach a no-op catch: a bake-lane rejection during capture must
|
|
183
|
+
// surface through the drain's refusal (and the wrapper's error
|
|
184
|
+
// boundary), never as an unhandled rejection that can kill the worker
|
|
185
|
+
// before the drain probes this record.
|
|
186
|
+
containerPromise.catch(() => {});
|
|
187
|
+
const maskedPromise = containerPromise.then((container: unknown) =>
|
|
188
|
+
maskNestedContainerThenables(container),
|
|
189
|
+
);
|
|
190
|
+
maskedPromise.catch(() => {});
|
|
191
|
+
reqCtx?._shellCaptureLoaderRecords?.set(bakeSegmentKey, maskedPromise);
|
|
192
|
+
return maskedPromise;
|
|
193
|
+
}
|
|
173
194
|
return createMaskedLoaderPromise();
|
|
174
195
|
}
|
|
175
196
|
|
|
@@ -42,15 +42,15 @@ export function isShellCaptureActive(
|
|
|
42
42
|
export { createMaskedLoaderPromise } from "./mask-nested.js";
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
46
|
-
* docs/design/loader-container-bake.md)
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
45
|
+
* Entry-level lane input for an entry's loaders under PPR (the loading()
|
|
46
|
+
* value; docs/design/loader-container-bake.md). The CAPTURE decision itself
|
|
47
|
+
* is per LOADER in loader-cache.ts: a `stream: "navigation"`
|
|
48
|
+
* (awaitBeforeFlush) loader BAKES at capture regardless of this value — the
|
|
49
|
+
* flag's document promise ("data in the HTML before first flush") maps to
|
|
50
|
+
* the frozen prelude — while every other loader is LIVE (masked at capture,
|
|
51
|
+
* fresh on every serve), postponing at loading() or an inline Suspense.
|
|
52
|
+
* This helper still gates which entries pass a bake segment key for the
|
|
53
|
+
* legacy loading()-less shape and the HIT-tail seed overlay.
|
|
54
54
|
*
|
|
55
55
|
* Mirrors segment-system's isRenderableLoading so the mask decision and the
|
|
56
56
|
* tree's boundary placement can never disagree.
|
|
@@ -306,20 +306,27 @@ export async function lookupBuildShell(
|
|
|
306
306
|
const entry = record.entry;
|
|
307
307
|
if (record.tags && record.tags.length > 0) {
|
|
308
308
|
const check = store.isTagsInvalidatedSince;
|
|
309
|
-
if (typeof check !== "function") {
|
|
309
|
+
if (typeof check !== "function" || store.tagHistoryInert) {
|
|
310
310
|
// A tagged build entry on a store that cannot answer "was this tag
|
|
311
311
|
// invalidated since the build" must not serve: updateTag() could
|
|
312
|
-
// never evict it.
|
|
312
|
+
// never evict it. That covers a store missing the method AND one
|
|
313
|
+
// whose answers have no durable history behind them
|
|
314
|
+
// (SegmentCacheStore.tagHistoryInert — a KV-less CFCacheStore's
|
|
315
|
+
// memo-only answers would let the immutable asset resurrect on the
|
|
316
|
+
// next request). Declared intent that cannot be honored deserves a
|
|
313
317
|
// diagnostic; the route keeps runtime-capture semantics.
|
|
314
318
|
const key = buildShellManifestKey(url.pathname);
|
|
315
319
|
if (!warnedTagCheckUnsupported.has(key)) {
|
|
316
320
|
warnedTagCheckUnsupported.add(key);
|
|
317
321
|
console.warn(
|
|
318
322
|
`[rango] Build-time shell for "${url.pathname}" carries cache tags, but ` +
|
|
319
|
-
"the app cache store
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
+
"the app cache store cannot answer tag-invalidation history durably " +
|
|
324
|
+
(typeof check !== "function"
|
|
325
|
+
? "(isTagsInvalidatedSince() is not implemented), "
|
|
326
|
+
: "(no durable tag history — a CFCacheStore without a KV namespace), ") +
|
|
327
|
+
"so updateTag() could not evict it. The entry is not served; the route " +
|
|
328
|
+
"keeps runtime shell capture. Use MemorySegmentCacheStore, a KV-backed " +
|
|
329
|
+
"CFCacheStore, or VercelCacheStore (or add the method to your custom store).",
|
|
323
330
|
);
|
|
324
331
|
}
|
|
325
332
|
return null;
|
package/src/rsc/shell-capture.ts
CHANGED
|
@@ -509,9 +509,10 @@ export interface ShellCaptureDebugEvent {
|
|
|
509
509
|
* the capture was not attempted
|
|
510
510
|
* - skip-capacity: the isolate capture queue is full; a later request may retry
|
|
511
511
|
* - skip-inert-store: the resolved store's shell family is missing or
|
|
512
|
-
* declared inert (SegmentCacheStore.shellFamilyInert — a
|
|
513
|
-
*
|
|
514
|
-
* background render was not
|
|
512
|
+
* declared inert (SegmentCacheStore.shellFamilyInert — a custom store
|
|
513
|
+
* whose backing tier is unavailable; built-in stores never declare it);
|
|
514
|
+
* nothing could store the result, so the background render was not
|
|
515
|
+
* scheduled at all
|
|
515
516
|
* - skip-queue-timeout: the capture waited past CAPTURE_QUEUE_WAIT_BUDGET_MS
|
|
516
517
|
* behind other captures and was dropped unrun (no backoff — the route is
|
|
517
518
|
* not doomed, the isolate was busy; a later request re-probes). Carries
|
|
@@ -554,7 +555,7 @@ export interface ShellCaptureDebugEvent {
|
|
|
554
555
|
/** A bake-lane loader settled into a shell that uses TTL/SWR-only invalidation. */
|
|
555
556
|
untaggedBake?: true;
|
|
556
557
|
/** Outcome reported by a store that supports shell-write acknowledgements. */
|
|
557
|
-
storeWrite?: "stored" | "invalidated";
|
|
558
|
+
storeWrite?: "stored" | "invalidated" | "uncacheable";
|
|
558
559
|
/** Consecutive failure count in the key's backoff entry, when one exists. */
|
|
559
560
|
backoffFailures?: number;
|
|
560
561
|
/** Ms remaining in the key's backoff window, when one exists. */
|
|
@@ -1005,7 +1006,8 @@ export function scheduleShellCapture(
|
|
|
1005
1006
|
// serialized queue (a promise-heavy route bakes for seconds per MISS),
|
|
1006
1007
|
// starving captures that CAN store. Skip scheduling entirely when the
|
|
1007
1008
|
// resolved store (same defaulting as captureAndStoreShell) has no shell
|
|
1008
|
-
// family or declared it inert (
|
|
1009
|
+
// family or declared it inert (a custom-store escape hatch; built-in
|
|
1010
|
+
// stores never declare it — a KV-less CFCacheStore stores L1-only).
|
|
1009
1011
|
const scheduledStore = descriptor.store ?? reqCtx._cacheStore;
|
|
1010
1012
|
if (!scheduledStore?.putShell || scheduledStore.shellFamilyInert) {
|
|
1011
1013
|
publishCaptureDebugEvent(descriptor, { key, outcome: "skip-inert-store" });
|
|
@@ -2028,6 +2030,21 @@ async function captureAndStoreShell(
|
|
|
2028
2030
|
);
|
|
2029
2031
|
return "refused";
|
|
2030
2032
|
}
|
|
2033
|
+
if (storeWrite === "uncacheable") {
|
|
2034
|
+
// The store declared the entry unstorable under its current
|
|
2035
|
+
// configuration — every retry would refuse identically, so back
|
|
2036
|
+
// the key off like any refused capture instead of burning a full
|
|
2037
|
+
// serialized render per MISS. Store-neutral on purpose: the
|
|
2038
|
+
// acknowledging store emits its own diagnostic naming the specific
|
|
2039
|
+
// limit (CFCacheStore warns about the over-limit Cache-Tag set).
|
|
2040
|
+
warnCaptureRefusedOnce(
|
|
2041
|
+
capture.key,
|
|
2042
|
+
"the store cannot cache this shell under its current configuration and acknowledged " +
|
|
2043
|
+
"the write as permanently refusable, so the key is backed off. See the cache store's " +
|
|
2044
|
+
"own warning for the specific limit and remedy.",
|
|
2045
|
+
);
|
|
2046
|
+
return "refused";
|
|
2047
|
+
}
|
|
2031
2048
|
} catch (error) {
|
|
2032
2049
|
// Best-effort: a failed put must never throw out of the background task.
|
|
2033
2050
|
reportCacheError(
|
|
@@ -350,7 +350,10 @@ export type PathHelpers<TEnv> = {
|
|
|
350
350
|
* Pass `{ stream: "navigation" }` to await this loader before first flush
|
|
351
351
|
* on DOCUMENT requests (see {@link LoaderOptions}) — the opt-in for loaders
|
|
352
352
|
* whose data, handle pushes, or thrown notFound()/redirect() must be in the
|
|
353
|
-
* SSR'd HTML. Per-loader: a dynamic sibling keeps streaming.
|
|
353
|
+
* SSR'd HTML. Per-loader: a dynamic sibling keeps streaming. Under a
|
|
354
|
+
* `ppr` route the flag BAKES: the loader executes at shell capture and
|
|
355
|
+
* its settled return freezes into the shell (nested promises stay live
|
|
356
|
+
* holes) — the pre-flush promise applied to the prelude.
|
|
354
357
|
*/
|
|
355
358
|
loader: <TData>(
|
|
356
359
|
loaderDef: LoaderDefinition<TData>,
|