@rangojs/router 0.19.1 → 0.21.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 +26 -14
- package/dist/types/browser/event-controller.d.ts +32 -0
- package/dist/types/browser/history-state.d.ts +16 -4
- package/dist/types/browser/navigation-transaction.d.ts +7 -0
- package/dist/types/browser/rango-state.d.ts +3 -1
- package/dist/types/browser/react/context.d.ts +11 -0
- package/dist/types/browser/react/location-state-shared.d.ts +84 -10
- package/dist/types/browser/react/location-state.d.ts +7 -2
- package/dist/types/browser/react/subscribe-to-nothing.d.ts +7 -0
- package/dist/types/cache/cache-scope.d.ts +4 -5
- package/dist/types/cache/cf/cf-cache-store.d.ts +27 -6
- package/dist/types/cache/cf/cf-cache-types.d.ts +12 -4
- package/dist/types/cache/handle-snapshot.d.ts +36 -50
- package/dist/types/cache/index.d.ts +2 -0
- package/dist/types/cache/shell-snapshot.d.ts +19 -3
- package/dist/types/cache/types.d.ts +21 -10
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +28 -16
- package/dist/types/client-urls/navigation.d.ts +7 -1
- package/dist/types/client-urls/optimistic-location.d.ts +10 -4
- package/dist/types/handles/deferred-resolution.d.ts +11 -1
- package/dist/types/prerender/build-shell-capture.d.ts +8 -6
- package/dist/types/router/match-middleware/cache-lookup.d.ts +1 -1
- package/dist/types/router/navigation-snapshot.d.ts +8 -1
- package/dist/types/router/router-interfaces.d.ts +2 -1
- package/dist/types/router/router-options.d.ts +17 -7
- package/dist/types/router/segment-resolution/loader-cache.d.ts +21 -9
- package/dist/types/router-versions.d.ts +39 -0
- package/dist/types/rsc/handler-context.d.ts +5 -0
- package/dist/types/rsc/shell-build-manifest.d.ts +3 -2
- package/dist/types/rsc/types.d.ts +4 -6
- package/dist/types/server/build-version-table.d.ts +44 -0
- package/dist/types/server/handle-store.d.ts +87 -46
- package/dist/types/server/request-context.d.ts +22 -8
- package/dist/types/ssr/entry-preload-priority.d.ts +69 -0
- package/dist/types/ssr/index.d.ts +1 -0
- package/dist/types/ssr/preinit-client-references.d.ts +17 -5
- package/dist/types/testing/build-versions.d.ts +32 -0
- package/dist/types/testing/dom.entry.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +2 -0
- package/dist/types/testing/internal/seed-vars.d.ts +7 -2
- package/dist/types/testing/location-state-key.d.ts +5 -1
- package/dist/types/testing/render-route.d.ts +77 -6
- package/dist/types/testing/serve-shell-request.d.ts +21 -0
- package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
- package/dist/types/types/handler-context.d.ts +8 -5
- package/dist/types/vite/discovery/build-versions.d.ts +393 -0
- package/dist/types/vite/discovery/installed-packages.d.ts +26 -0
- package/dist/types/vite/discovery/router-versions-phase.d.ts +52 -0
- package/dist/types/vite/discovery/shell-prerender-phase.d.ts +7 -17
- package/dist/types/vite/discovery/state.d.ts +14 -0
- package/dist/types/vite/encryption-key.d.ts +18 -0
- package/dist/types/vite/plugin-types.d.ts +47 -6
- package/dist/types/vite/plugins/{version-injector.d.ts → entry-bootstrap-injector.d.ts} +4 -6
- package/dist/types/vite/plugins/expose-ids/export-analysis.d.ts +1 -0
- package/dist/types/vite/plugins/expose-ids/router-transform.d.ts +14 -0
- package/dist/types/vite/plugins/stable-rsc-output.d.ts +51 -0
- package/dist/types/vite/plugins/version-plugin.d.ts +9 -11
- package/dist/types/vite/plugins/virtual-entries.d.ts +21 -2
- package/dist/types/vite/router-discovery.d.ts +6 -0
- package/dist/types/vite/utils/client-chunks.d.ts +24 -2
- package/dist/types/vite/utils/compare-strings.d.ts +7 -0
- package/dist/types/vite/utils/package-resolution.d.ts +8 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +2 -0
- package/dist/vite/index.js +1544 -420
- package/package.json +1 -2
- package/skills/bundle-analysis/SKILL.md +1 -1
- package/skills/cache-guide/SKILL.md +84 -20
- package/skills/caching/SKILL.md +16 -5
- package/skills/client-urls/SKILL.md +16 -0
- package/skills/cloudflare/SKILL.md +2 -1
- package/skills/comparison/references/framework-comparison.md +16 -12
- package/skills/hooks/SKILL.md +3 -0
- package/skills/hooks/handle-and-actions.md +6 -0
- package/skills/hooks/navigation.md +28 -0
- package/skills/hooks/state.md +209 -8
- package/skills/hooks/urls.md +40 -1
- package/skills/host-router/SKILL.md +13 -0
- package/skills/intercept/SKILL.md +6 -0
- package/skills/loader/SKILL.md +19 -1
- package/skills/middleware/SKILL.md +2 -0
- package/skills/ppr/SKILL.md +119 -40
- package/skills/prerender/SKILL.md +7 -3
- package/skills/rango/SKILL.md +8 -7
- package/skills/router-setup/SKILL.md +23 -3
- package/skills/testing/SKILL.md +30 -26
- package/skills/testing/client-components.md +193 -8
- package/skills/testing/handles.md +2 -0
- package/skills/testing/loader.md +1 -1
- package/skills/testing/middleware.md +1 -1
- package/skills/testing/render-handler.md +18 -18
- package/skills/testing/serve-shell-request.md +63 -14
- package/skills/testing/server-actions.md +17 -17
- package/skills/testing/server-tree.md +1 -1
- package/skills/typesafety/generated-files-and-cli.md +1 -1
- package/skills/use-cache/SKILL.md +16 -5
- package/skills/vercel/SKILL.md +3 -3
- package/src/bin/rango.ts +1 -1
- package/src/browser/event-controller.ts +75 -2
- package/src/browser/history-state.ts +39 -17
- package/src/browser/navigation-bridge.ts +41 -18
- package/src/browser/navigation-transaction.ts +15 -10
- package/src/browser/partial-update.ts +10 -2
- package/src/browser/rango-state.ts +3 -1
- package/src/browser/react/Link.tsx +2 -3
- package/src/browser/react/NavigationProvider.tsx +25 -1
- package/src/browser/react/context.ts +13 -0
- package/src/browser/react/location-state-shared.ts +200 -34
- package/src/browser/react/location-state.ts +40 -75
- package/src/browser/react/subscribe-to-nothing.ts +7 -0
- package/src/browser/react/use-handle.ts +20 -1
- package/src/browser/rsc-router.tsx +33 -11
- package/src/browser/server-action-bridge.ts +17 -1
- package/src/cache/cache-scope.ts +18 -8
- package/src/cache/cf/cf-cache-store.ts +75 -55
- package/src/cache/cf/cf-cache-types.ts +12 -4
- package/src/cache/handle-snapshot.ts +69 -87
- package/src/cache/index.ts +4 -0
- package/src/cache/shell-snapshot.ts +35 -5
- package/src/cache/types.ts +21 -10
- package/src/cache/vercel/vercel-cache-store.ts +48 -20
- package/src/client-urls/client-root.tsx +1 -0
- package/src/client-urls/navigation.ts +10 -0
- package/src/client-urls/optimistic-location.ts +10 -4
- package/src/handles/deferred-resolution.ts +31 -0
- package/src/prerender/build-shell-capture.ts +11 -9
- package/src/router/manifest.ts +2 -0
- package/src/router/match-middleware/cache-lookup.ts +11 -18
- package/src/router/navigation-snapshot.ts +25 -6
- package/src/router/request-classification.ts +15 -12
- package/src/router/router-interfaces.ts +2 -1
- package/src/router/router-options.ts +17 -7
- package/src/router/segment-resolution/loader-cache.ts +95 -60
- package/src/router-versions.ts +60 -0
- package/src/rsc/full-payload.ts +6 -2
- package/src/rsc/handler-context.ts +5 -0
- package/src/rsc/handler.ts +14 -4
- package/src/rsc/rsc-rendering.ts +12 -1
- package/src/rsc/shell-build-manifest.ts +10 -7
- package/src/rsc/shell-capture.ts +36 -26
- package/src/rsc/types.ts +4 -6
- package/src/server/build-version-table.ts +115 -0
- package/src/server/handle-store.ts +344 -127
- package/src/server/request-context.ts +62 -11
- package/src/ssr/entry-preload-priority.ts +308 -0
- package/src/ssr/index.tsx +46 -9
- package/src/ssr/preinit-client-references.ts +17 -5
- package/src/ssr/ssr-root.tsx +16 -4
- package/src/testing/build-versions.ts +64 -0
- package/src/testing/dispatch.ts +6 -0
- package/src/testing/dom.entry.ts +1 -0
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +3 -0
- package/src/testing/internal/context.ts +2 -1
- package/src/testing/internal/seed-vars.ts +14 -2
- package/src/testing/location-state-key.ts +18 -8
- package/src/testing/render-handler.ts +2 -1
- package/src/testing/render-route.tsx +277 -43
- package/src/testing/serve-shell-request.ts +80 -11
- package/src/testing/vitest-stubs/version.ts +4 -1
- package/src/types/handler-context.ts +8 -5
- package/src/vite/discovery/build-versions.ts +1281 -0
- package/src/vite/discovery/installed-packages.ts +159 -0
- package/src/vite/discovery/prerender-collection.ts +38 -1
- package/src/vite/discovery/router-versions-phase.ts +336 -0
- package/src/vite/discovery/shell-prerender-phase.ts +28 -38
- package/src/vite/discovery/state.ts +19 -0
- package/src/vite/discovery/virtual-module-codegen.ts +7 -2
- package/src/vite/encryption-key.ts +101 -9
- package/src/vite/plugin-types.ts +48 -6
- package/src/vite/plugins/{version-injector.ts → entry-bootstrap-injector.ts} +7 -22
- package/src/vite/plugins/expose-ids/export-analysis.ts +1 -1
- package/src/vite/plugins/expose-ids/router-transform.ts +36 -15
- package/src/vite/plugins/expose-internal-ids.ts +12 -3
- package/src/vite/plugins/stable-rsc-output.ts +147 -0
- package/src/vite/plugins/version-plugin.ts +19 -24
- package/src/vite/plugins/version.d.ts +17 -2
- package/src/vite/plugins/virtual-entries.ts +40 -7
- package/src/vite/rango.ts +30 -3
- package/src/vite/router-discovery.ts +82 -13
- package/src/vite/utils/client-chunks.ts +84 -14
- package/src/vite/utils/compare-strings.ts +9 -0
- package/src/vite/utils/package-resolution.ts +9 -0
- package/src/vite/utils/prerender-utils.ts +4 -1
- package/src/vite/utils/shared-utils.ts +1 -1
|
@@ -55,13 +55,23 @@ function createLateHandlePushError(
|
|
|
55
55
|
interface SlotTag {
|
|
56
56
|
/** Pushed inside a DSL loader scope; captureHandles drops it. */
|
|
57
57
|
loader?: boolean;
|
|
58
|
-
/** A
|
|
58
|
+
/** A cached unit's replay of this loader's recorded push (pushReplayed). */
|
|
59
59
|
replayOf?: string;
|
|
60
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* A record's copy for a loader that record does not supply the value of
|
|
62
|
+
* (pushPlaceholder): it stands in until the loader's own source delivers.
|
|
63
|
+
*/
|
|
64
|
+
placeholderOf?: string;
|
|
65
|
+
/** Pushed by this loader's live run after it replaced its copies. */
|
|
61
66
|
liveOf?: string;
|
|
67
|
+
/**
|
|
68
|
+
* The loaders around that run's body whose runs replaced copies too: their
|
|
69
|
+
* next push follows this one, so a run's pushes keep their order.
|
|
70
|
+
*/
|
|
71
|
+
under?: readonly string[];
|
|
62
72
|
/** Pushed by this loader's body and kept in records (push `owner`). */
|
|
63
73
|
owner?: string;
|
|
64
|
-
/** A
|
|
74
|
+
/** A record's copy for a loader served from that record's pin (pushRestored). */
|
|
65
75
|
restoredOf?: string;
|
|
66
76
|
/** A restored slot a dropped settled push of its loader stood in for. */
|
|
67
77
|
matched?: true;
|
|
@@ -70,6 +80,26 @@ interface SlotTag {
|
|
|
70
80
|
// Shared by every plain DSL-loader push; tags are never mutated in place.
|
|
71
81
|
const LOADER_SLOT: SlotTag = Object.freeze({ loader: true });
|
|
72
82
|
|
|
83
|
+
/**
|
|
84
|
+
* What a restored record is to one loader's handle pushes on this request
|
|
85
|
+
* (loader-cache.ts loaderPins; docs/design/handle-push-ownership.md). Two
|
|
86
|
+
* answers let the record's copies stand, two make them placeholders, and in
|
|
87
|
+
* each pair one also speaks for the loader's other pushes:
|
|
88
|
+
* - "pin": the request serves the loader's value from the pin stored with
|
|
89
|
+
* the record, and the record lists every settled push of that run. Its
|
|
90
|
+
* copies stand, and any other settled push of the loader is dropped.
|
|
91
|
+
* - "copies": its copies stand and nothing else is known: a dependency of
|
|
92
|
+
* pinned loaders, or a pin written before captures recorded every push.
|
|
93
|
+
* - "hole": the route runs the loader on this request (it registers it and
|
|
94
|
+
* the record does not pin it). Its copies are placeholders and a push made
|
|
95
|
+
* in its body is its own, with or without copies.
|
|
96
|
+
* - "placeholders": its copies are placeholders and nothing else is known: a
|
|
97
|
+
* dependency the route does not register while a pin is missing (the
|
|
98
|
+
* record does not name the loader that ran it). Without copies its push
|
|
99
|
+
* counts for the loader that awaits it.
|
|
100
|
+
*/
|
|
101
|
+
export type RecordAuthority = "pin" | "copies" | "hole" | "placeholders";
|
|
102
|
+
|
|
73
103
|
function cloneHandleData(data: HandleData): HandleData {
|
|
74
104
|
const clone: HandleData = {};
|
|
75
105
|
for (const handleName in data) {
|
|
@@ -158,15 +188,17 @@ export interface HandleStore {
|
|
|
158
188
|
): void;
|
|
159
189
|
|
|
160
190
|
/**
|
|
161
|
-
* push() a
|
|
162
|
-
*
|
|
163
|
-
* in this request, its first push removes every
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
191
|
+
* push() a cached unit's copy of a value `loaderId`'s body pushed
|
|
192
|
+
* (handle-snapshot.ts appendHandles: a loader-cache or "use cache" HIT). If
|
|
193
|
+
* that loader then runs live in this request, its first push removes every
|
|
194
|
+
* copy pushed for it (replays and placeholders) and takes the first one's
|
|
195
|
+
* position, and the pushes of that run follow in push order, a dependency's
|
|
196
|
+
* included, so the handle output stays live without a duplicate. A push
|
|
197
|
+
* counts as that loader's when it is made in its body, or in a body it
|
|
198
|
+
* entered that has no copies of its own (the capture credits a dependency's
|
|
199
|
+
* push under a live-lane loader to that loader). A run that ends without a
|
|
200
|
+
* push leaves a replay in place, unless the loader is a hole
|
|
201
|
+
* (settleLoaderRun).
|
|
170
202
|
*/
|
|
171
203
|
pushReplayed(
|
|
172
204
|
handleName: string,
|
|
@@ -176,16 +208,27 @@ export interface HandleStore {
|
|
|
176
208
|
): void;
|
|
177
209
|
|
|
178
210
|
/**
|
|
179
|
-
* push() a
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
211
|
+
* push() a record's copy of `loaderId`'s push as a placeholder
|
|
212
|
+
* (handle-snapshot.ts restoreHandles): the record does not supply that
|
|
213
|
+
* loader's value on this request, so its own source decides its pushes. A
|
|
214
|
+
* live run replaces it as it does a replay; a run that ends without a push
|
|
215
|
+
* drops it (settleLoaderRun); a cached unit that claims the loader delivers
|
|
216
|
+
* in its place (replacePlaceholders).
|
|
217
|
+
*/
|
|
218
|
+
pushPlaceholder(
|
|
219
|
+
handleName: string,
|
|
220
|
+
segmentId: string,
|
|
221
|
+
data: unknown,
|
|
222
|
+
loaderId: string,
|
|
223
|
+
): void;
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* push() a record's copy of `loaderId`'s settled push that stands
|
|
227
|
+
* (restoreHandles): the record supplies the loader's value too. From then
|
|
228
|
+
* on this request drops every settled, thenable-free push the loader makes
|
|
229
|
+
* (live, or replayed from a cache entry, anywhere inside its body), and
|
|
230
|
+
* keeps the ones holding a thenable (holdsThenable: what the record could
|
|
231
|
+
* not keep), placed among the restored copies in the loader's push order.
|
|
189
232
|
*/
|
|
190
233
|
pushRestored(
|
|
191
234
|
handleName: string,
|
|
@@ -195,41 +238,34 @@ export interface HandleStore {
|
|
|
195
238
|
): void;
|
|
196
239
|
|
|
197
240
|
/**
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
* never takes a push made inside a hole it awaited, while a body inside the
|
|
205
|
-
* hole that restored copies of its own (a dependency the capture ran under
|
|
206
|
-
* a bake-lane loader) still takes its pushes.
|
|
241
|
+
* What the record this request restored is to each loader (RecordAuthority;
|
|
242
|
+
* CacheScope.lookupRouteDetailed sets it on a hit). A "pin" loader counts
|
|
243
|
+
* as restored with or without copies. A hole is a "hole" loader or one with
|
|
244
|
+
* placeholders: the walk out through enclosing loader bodies that finds the
|
|
245
|
+
* loader a push counts for stops at its body, so a pinned loader never
|
|
246
|
+
* takes a push made inside a hole it awaited.
|
|
207
247
|
*/
|
|
208
|
-
|
|
248
|
+
setRecordAuthority(authority: (loaderId: string) => RecordAuthority): void;
|
|
209
249
|
|
|
210
250
|
/**
|
|
211
251
|
* `loaderId`'s run in this request settled (resolved or rejected), its
|
|
212
|
-
* pushes made (loader-resolution.ts createLoaderExecutor).
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
* its settled pushes, as its pin is for its data. Restored copies are not
|
|
218
|
-
* replays and stand.
|
|
252
|
+
* pushes made (loader-resolution.ts createLoaderExecutor). Its placeholders
|
|
253
|
+
* that no push of the run replaced are dropped: the run's output decides,
|
|
254
|
+
* so a loader that throws or skips its push shows none rather than the
|
|
255
|
+
* record's. A hole's replays go with them. Any other loader's replays stay
|
|
256
|
+
* (a dependency another entry's HIT replayed), and so do restored copies.
|
|
219
257
|
*/
|
|
220
258
|
settleLoaderRun(loaderId: string): void;
|
|
221
259
|
|
|
222
260
|
/**
|
|
223
|
-
* A
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
* place in each array. Any other loader, or a hole with nothing replayed,
|
|
230
|
-
* delivers as a plain replay.
|
|
261
|
+
* A cached unit's HIT delivering the pushes it recorded for `loaderIds`,
|
|
262
|
+
* the loaders it claimed (appendHandles). Their placeholders give way to
|
|
263
|
+
* what `deliver` pushes for them, nothing when it pushes nothing, so the
|
|
264
|
+
* page shows the pushes of the run that produced the unit's value. The
|
|
265
|
+
* pushReplayed calls `deliver` makes take the removed values' place in each
|
|
266
|
+
* array, in order. Without placeholders it is a plain replay.
|
|
231
267
|
*/
|
|
232
|
-
|
|
268
|
+
replacePlaceholders(loaderIds: readonly string[], deliver: () => void): void;
|
|
233
269
|
|
|
234
270
|
/**
|
|
235
271
|
* Get all collected handle data after all handlers have settled.
|
|
@@ -237,6 +273,24 @@ export interface HandleStore {
|
|
|
237
273
|
*/
|
|
238
274
|
getData(): Promise<HandleData>;
|
|
239
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Fix the document lane at the store's current state. From then on
|
|
278
|
+
* `stream("settled")` yields this state and nothing newer, and
|
|
279
|
+
* `streamLate()` delivers every change made after it, whether or not it
|
|
280
|
+
* beat the handler barrier. The first call wins. It must run before the
|
|
281
|
+
* document stream is first read: a stream that started on the live state
|
|
282
|
+
* stays on it (reported through `onError` in development).
|
|
283
|
+
*
|
|
284
|
+
* A PPR shell HIT calls it once its record is replayed and before a loader
|
|
285
|
+
* runs (rsc-rendering.ts serveShellHit). The prelude was rendered from the
|
|
286
|
+
* record's handle data (resolvedHandleStream `recordedOnly`), so that is
|
|
287
|
+
* what the client must hydrate with. What the request's loaders then push,
|
|
288
|
+
* replace or drop is theirs, not the shell's: in the document snapshot it
|
|
289
|
+
* rendered elements the prelude never had, or left out ones it has (issue
|
|
290
|
+
* #1035), and it got there or not by a race with the lane's first read.
|
|
291
|
+
*/
|
|
292
|
+
freezeDocumentSnapshot(): void;
|
|
293
|
+
|
|
240
294
|
/**
|
|
241
295
|
* Get an async iterator that yields handle data on each push.
|
|
242
296
|
* Completes at the chosen settlement barrier: "fullySettled" (default —
|
|
@@ -245,6 +299,9 @@ export interface HandleStore {
|
|
|
245
299
|
* blocking positions and must not wait on loader bodies).
|
|
246
300
|
* Each yield contains the full accumulated state (not just the delta).
|
|
247
301
|
* Safe for concurrent consumers (per-consumer version cursor).
|
|
302
|
+
* The "settled" lane of a frozen store (freezeDocumentSnapshot) yields the
|
|
303
|
+
* frozen state once, at once, and ends. That yield is the store's own
|
|
304
|
+
* frozen object: a consumer must not write to it.
|
|
248
305
|
*/
|
|
249
306
|
stream(
|
|
250
307
|
until?: "settled" | "fullySettled",
|
|
@@ -256,7 +313,9 @@ export interface HandleStore {
|
|
|
256
313
|
* fullySettled. Returns without yielding when the auxiliary lane is empty
|
|
257
314
|
* by the time `settled` resolves. The client consumes this post-hydration
|
|
258
315
|
* (rsc-router.tsx) and merges via the same application path as nav-lane
|
|
259
|
-
* progressive handle updates.
|
|
316
|
+
* progressive handle updates. On a frozen store (freezeDocumentSnapshot)
|
|
317
|
+
* it starts from the frozen state instead: a change made before the
|
|
318
|
+
* handler barrier is late too.
|
|
260
319
|
*/
|
|
261
320
|
streamLate(): AsyncGenerator<HandleData, void, unknown>;
|
|
262
321
|
|
|
@@ -317,45 +376,51 @@ export function createHandleStore(): HandleStore {
|
|
|
317
376
|
// (not value identity) so primitive values are covered too.
|
|
318
377
|
// replaySegmentData installs fresh arrays, so restored values are untagged.
|
|
319
378
|
const slotTags = new WeakMap<unknown[], (SlotTag | undefined)[]>();
|
|
320
|
-
//
|
|
321
|
-
// holding
|
|
379
|
+
// Copies standing in for a loader that has not pushed live yet: loaderId ->
|
|
380
|
+
// the arrays holding them. Kept apart by kind: only a placeholder gives way
|
|
381
|
+
// to a run that ends without a push, or to a cached unit's delivery.
|
|
322
382
|
const pendingReplays = new Map<string, Set<unknown[]>>();
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
383
|
+
const pendingPlaceholders = new Map<string, Set<unknown[]>>();
|
|
384
|
+
// Loaders whose live run already replaced its copies.
|
|
385
|
+
const replaced = new Set<string>();
|
|
386
|
+
// Loaders a record restored standing copies for (pushRestored), and those
|
|
387
|
+
// it restored placeholders for (pushPlaceholder).
|
|
326
388
|
const restoredLoaders = new Set<string>();
|
|
389
|
+
const placeholderLoaders = new Set<string>();
|
|
390
|
+
let authority: ((loaderId: string) => RecordAuthority) | undefined;
|
|
327
391
|
const isRestored = (loaderId: string): boolean =>
|
|
328
|
-
restoredLoaders.has(loaderId);
|
|
329
|
-
// The route's live-lane loaders (markLiveLane): holes a restore never
|
|
330
|
-
// reaches into.
|
|
331
|
-
let liveLane: ReadonlySet<string> | undefined;
|
|
392
|
+
restoredLoaders.has(loaderId) || authority?.(loaderId) === "pin";
|
|
332
393
|
const isHole = (loaderId: string): boolean =>
|
|
333
|
-
|
|
394
|
+
placeholderLoaders.has(loaderId) || authority?.(loaderId) === "hole";
|
|
334
395
|
|
|
335
|
-
const
|
|
336
|
-
pendingReplays.has(loaderId) ||
|
|
396
|
+
const hasCopies = (loaderId: string): boolean =>
|
|
397
|
+
pendingReplays.has(loaderId) ||
|
|
398
|
+
pendingPlaceholders.has(loaderId) ||
|
|
399
|
+
replaced.has(loaderId);
|
|
337
400
|
|
|
338
401
|
// The restored loader a push counts for, as the capture credits it
|
|
339
402
|
// (shell-capture.ts deriveShellCaptureContext): the replayed loader when it
|
|
340
|
-
// restored
|
|
403
|
+
// is restored, else the innermost enclosing loader body that is. A
|
|
341
404
|
// "use cache" hit inside a restored body replays a dependency's push the
|
|
342
405
|
// capture recorded under that body. A hole's body, or a replay of a hole's
|
|
343
406
|
// push, ends the search: a live loader a restored loader awaits is still
|
|
344
407
|
// live.
|
|
345
408
|
function restoringLoader(replayOf: string | undefined): string | undefined {
|
|
346
409
|
if (replayOf !== undefined) {
|
|
347
|
-
if (
|
|
410
|
+
if (isRestored(replayOf)) return replayOf;
|
|
348
411
|
// Defensive: a hole's own cache() replay runs outside any loader body,
|
|
349
412
|
// so the walk below finds no restored loader either.
|
|
350
413
|
if (isHole(replayOf)) return undefined;
|
|
351
414
|
}
|
|
352
415
|
return findEnclosingLoaderBody(isRestored, isHole);
|
|
353
416
|
}
|
|
354
|
-
// The
|
|
355
|
-
//
|
|
356
|
-
// push
|
|
357
|
-
let replaying:
|
|
358
|
-
|
|
417
|
+
// The copy whose this.push() is in flight, as the tag its slot takes. Set
|
|
418
|
+
// around the call, not passed, so a capture wrapping push() sees it as a
|
|
419
|
+
// push.
|
|
420
|
+
let replaying:
|
|
421
|
+
| Pick<SlotTag, "replayOf" | "placeholderOf" | "restoredOf">
|
|
422
|
+
| undefined;
|
|
423
|
+
// A replacePlaceholders delivery in flight: per array, where its next
|
|
359
424
|
// pushReplayed value goes (the removed values' place).
|
|
360
425
|
let redelivery: Map<unknown[], number> | undefined;
|
|
361
426
|
|
|
@@ -383,22 +448,23 @@ export function createHandleStore(): HandleStore {
|
|
|
383
448
|
tags.splice(at, 0, tag);
|
|
384
449
|
}
|
|
385
450
|
|
|
386
|
-
// Remove `
|
|
387
|
-
function
|
|
451
|
+
// Remove the slots `drops` accepts; returns the first one's position.
|
|
452
|
+
function removeSlots(
|
|
388
453
|
values: unknown[],
|
|
389
|
-
|
|
454
|
+
drops: (tag: SlotTag) => boolean,
|
|
390
455
|
): number | undefined {
|
|
391
456
|
const tags = slotTags.get(values);
|
|
392
457
|
if (!tags) return undefined;
|
|
393
458
|
let first: number | undefined;
|
|
394
459
|
let kept = 0;
|
|
395
460
|
for (let i = 0; i < values.length; i++) {
|
|
396
|
-
|
|
461
|
+
const tag = tags[i];
|
|
462
|
+
if (tag && drops(tag)) {
|
|
397
463
|
first ??= kept;
|
|
398
464
|
continue;
|
|
399
465
|
}
|
|
400
466
|
values[kept] = values[i];
|
|
401
|
-
tags[kept] =
|
|
467
|
+
tags[kept] = tag;
|
|
402
468
|
kept++;
|
|
403
469
|
}
|
|
404
470
|
values.length = kept;
|
|
@@ -406,32 +472,80 @@ export function createHandleStore(): HandleStore {
|
|
|
406
472
|
return first;
|
|
407
473
|
}
|
|
408
474
|
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
|
|
412
|
-
function liveReplacementIndex(
|
|
413
|
-
loaderId: string,
|
|
414
|
-
values: unknown[],
|
|
415
|
-
): number | undefined {
|
|
416
|
-
const pending = pendingReplays.get(loaderId);
|
|
417
|
-
if (pending) {
|
|
418
|
-
pendingReplays.delete(loaderId);
|
|
419
|
-
replacedReplays.add(loaderId);
|
|
420
|
-
let at: number | undefined;
|
|
421
|
-
for (const replayed of pending) {
|
|
422
|
-
const first = removeReplayedSlots(replayed, loaderId);
|
|
423
|
-
if (replayed === values) at = first;
|
|
424
|
-
}
|
|
425
|
-
return at;
|
|
426
|
-
}
|
|
475
|
+
// After the last push of `loaderId`'s run in `values`: its own, or one made
|
|
476
|
+
// in a body it entered. undefined: none yet.
|
|
477
|
+
function runCursor(loaderId: string, values: unknown[]): number | undefined {
|
|
427
478
|
const tags = slotTags.get(values);
|
|
428
479
|
if (!tags) return undefined;
|
|
429
480
|
for (let i = tags.length - 1; i >= 0; i--) {
|
|
430
|
-
|
|
481
|
+
const tag = tags[i];
|
|
482
|
+
if (tag?.liveOf === loaderId || tag?.under?.includes(loaderId)) {
|
|
483
|
+
return i + 1;
|
|
484
|
+
}
|
|
431
485
|
}
|
|
432
486
|
return undefined;
|
|
433
487
|
}
|
|
434
488
|
|
|
489
|
+
// The loaders around `pusher`'s body, innermost first, whose runs replaced
|
|
490
|
+
// copies (SlotTag.under).
|
|
491
|
+
function replacedAround(pusher: string): string[] | undefined {
|
|
492
|
+
let around: string[] | undefined;
|
|
493
|
+
let outside = false;
|
|
494
|
+
findEnclosingLoaderBody((loaderId) => {
|
|
495
|
+
if (outside && replaced.has(loaderId)) (around ??= []).push(loaderId);
|
|
496
|
+
if (loaderId === pusher) outside = true;
|
|
497
|
+
return false;
|
|
498
|
+
});
|
|
499
|
+
return around;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Remove `loaderId`'s pending placeholders, and its replays with `replays`.
|
|
503
|
+
// Returns the first position removed from `values`.
|
|
504
|
+
function dropCopies(
|
|
505
|
+
loaderId: string,
|
|
506
|
+
replays: boolean,
|
|
507
|
+
values?: unknown[],
|
|
508
|
+
): number | undefined {
|
|
509
|
+
const placeholderArrays = pendingPlaceholders.get(loaderId);
|
|
510
|
+
const replayArrays = replays ? pendingReplays.get(loaderId) : undefined;
|
|
511
|
+
if (!placeholderArrays && !replayArrays) return undefined;
|
|
512
|
+
pendingPlaceholders.delete(loaderId);
|
|
513
|
+
if (replayArrays) pendingReplays.delete(loaderId);
|
|
514
|
+
replaced.add(loaderId);
|
|
515
|
+
const own = (tag: SlotTag): boolean =>
|
|
516
|
+
tag.placeholderOf === loaderId || (replays && tag.replayOf === loaderId);
|
|
517
|
+
let at: number | undefined;
|
|
518
|
+
for (const arrays of [placeholderArrays, replayArrays]) {
|
|
519
|
+
if (!arrays) continue;
|
|
520
|
+
for (const copied of arrays) {
|
|
521
|
+
const first = removeSlots(copied, own);
|
|
522
|
+
if (copied === values) at ??= first;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return at;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Where a live push by a loader with copies lands in `values`: its first
|
|
529
|
+
// push removes all of its copies and takes the first one's position in this
|
|
530
|
+
// array; later pushes follow the last push of its run. undefined: no place
|
|
531
|
+
// of its own here.
|
|
532
|
+
function liveReplacementIndex(
|
|
533
|
+
loaderId: string,
|
|
534
|
+
values: unknown[],
|
|
535
|
+
): number | undefined {
|
|
536
|
+
return dropCopies(loaderId, true, values) ?? runCursor(loaderId, values);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function addPending(
|
|
540
|
+
pending: Map<string, Set<unknown[]>>,
|
|
541
|
+
loaderId: string,
|
|
542
|
+
values: unknown[],
|
|
543
|
+
): void {
|
|
544
|
+
let arrays = pending.get(loaderId);
|
|
545
|
+
if (!arrays) pending.set(loaderId, (arrays = new Set()));
|
|
546
|
+
arrays.add(values);
|
|
547
|
+
}
|
|
548
|
+
|
|
435
549
|
// A restored loader's settled push: the first of its restored slots here
|
|
436
550
|
// not yet matched stands in for it. Returns nothing; the push is dropped.
|
|
437
551
|
function matchRestoredSlot(values: unknown[], loaderId: string): void {
|
|
@@ -561,6 +675,12 @@ export function createHandleStore(): HandleStore {
|
|
|
561
675
|
let emissionWaiters: (() => void)[] = [];
|
|
562
676
|
let completed = false;
|
|
563
677
|
let streamConsumed = false;
|
|
678
|
+
// freezeDocumentSnapshot: the state the document lane serves, and the
|
|
679
|
+
// version the late channel starts after.
|
|
680
|
+
let documentSnapshot: { version: number; data: HandleData } | undefined;
|
|
681
|
+
// A stream("settled") consumer started on the live state: a freeze after
|
|
682
|
+
// that does not reach it.
|
|
683
|
+
let documentStreamRead = false;
|
|
564
684
|
|
|
565
685
|
// Wake every waiting consumer (new push or a settlement barrier fired).
|
|
566
686
|
function signalEmission() {
|
|
@@ -631,21 +751,27 @@ export function createHandleStore(): HandleStore {
|
|
|
631
751
|
throw error;
|
|
632
752
|
}
|
|
633
753
|
|
|
634
|
-
const { replayOf, restoredOf } = replaying ?? {};
|
|
635
|
-
// A
|
|
636
|
-
//
|
|
754
|
+
const { replayOf, placeholderOf, restoredOf } = replaying ?? {};
|
|
755
|
+
// A record's copy is no loader's push. Any other push, while a loader
|
|
756
|
+
// has copies or a restore to reconcile, is attributed as the capture
|
|
637
757
|
// credits it: to the innermost loader body it is made in that has
|
|
638
|
-
//
|
|
639
|
-
//
|
|
640
|
-
//
|
|
641
|
-
//
|
|
758
|
+
// copies, up to a hole's body, so a replay made in a body replaces
|
|
759
|
+
// that body's copies and a dependency's push replaces those of the
|
|
760
|
+
// live-lane loader it ran under. Its record copy stands when a
|
|
761
|
+
// restored loader counts it (restoringLoader).
|
|
642
762
|
let pusher: string | undefined;
|
|
643
763
|
let restoredBy: string | undefined;
|
|
644
|
-
if (
|
|
645
|
-
if (
|
|
646
|
-
|
|
764
|
+
if (restoredOf === undefined && placeholderOf === undefined) {
|
|
765
|
+
if (
|
|
766
|
+
pendingReplays.size > 0 ||
|
|
767
|
+
pendingPlaceholders.size > 0 ||
|
|
768
|
+
replaced.size > 0
|
|
769
|
+
) {
|
|
770
|
+
pusher = findEnclosingLoaderBody(hasCopies, isHole);
|
|
771
|
+
}
|
|
772
|
+
if (restoredLoaders.size > 0 || authority) {
|
|
773
|
+
restoredBy = restoringLoader(replayOf);
|
|
647
774
|
}
|
|
648
|
-
if (restoredLoaders.size > 0) restoredBy = restoringLoader(replayOf);
|
|
649
775
|
}
|
|
650
776
|
if (restoredBy !== undefined && !holdsThenable(value)) {
|
|
651
777
|
const existing = data[handleName]?.[segmentId];
|
|
@@ -663,32 +789,52 @@ export function createHandleStore(): HandleStore {
|
|
|
663
789
|
const loader = (loaderPush ?? isInsideLoaderScope()) || undefined;
|
|
664
790
|
let at: number | undefined;
|
|
665
791
|
let liveOf: string | undefined;
|
|
666
|
-
|
|
792
|
+
let under: string[] | undefined;
|
|
793
|
+
if (restoredOf !== undefined) {
|
|
667
794
|
restoredLoaders.add(restoredOf);
|
|
795
|
+
} else if (placeholderOf !== undefined) {
|
|
796
|
+
placeholderLoaders.add(placeholderOf);
|
|
668
797
|
} else if (replayOf !== undefined && redelivery?.has(values)) {
|
|
669
798
|
at = redelivery.get(values)!;
|
|
670
799
|
redelivery.set(values, at + 1);
|
|
671
800
|
} else if (pusher !== undefined) {
|
|
801
|
+
under = replacedAround(pusher);
|
|
672
802
|
at = liveReplacementIndex(pusher, values);
|
|
803
|
+
// No place of its own here: it follows the run it was made in.
|
|
804
|
+
for (let i = 0; at === undefined && under && i < under.length; i++) {
|
|
805
|
+
at = runCursor(under[i]!, values);
|
|
806
|
+
}
|
|
673
807
|
liveOf = pusher;
|
|
674
808
|
} else if (restoredBy !== undefined) {
|
|
675
809
|
at = restoredInsertIndex(values, restoredBy);
|
|
676
|
-
if (
|
|
810
|
+
if (replayOf === undefined) liveOf = restoredBy;
|
|
677
811
|
}
|
|
678
812
|
insertSlot(
|
|
679
813
|
values,
|
|
680
814
|
at,
|
|
681
815
|
value,
|
|
682
|
-
replayOf
|
|
683
|
-
|
|
816
|
+
replayOf !== undefined ||
|
|
817
|
+
placeholderOf !== undefined ||
|
|
818
|
+
restoredOf !== undefined ||
|
|
819
|
+
liveOf !== undefined ||
|
|
820
|
+
owner !== undefined
|
|
821
|
+
? {
|
|
822
|
+
loader,
|
|
823
|
+
replayOf,
|
|
824
|
+
placeholderOf,
|
|
825
|
+
restoredOf,
|
|
826
|
+
liveOf,
|
|
827
|
+
under,
|
|
828
|
+
owner,
|
|
829
|
+
}
|
|
684
830
|
: loader
|
|
685
831
|
? LOADER_SLOT
|
|
686
832
|
: undefined,
|
|
687
833
|
);
|
|
688
|
-
if (replayOf) {
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
834
|
+
if (replayOf !== undefined) {
|
|
835
|
+
addPending(pendingReplays, replayOf, values);
|
|
836
|
+
} else if (placeholderOf !== undefined) {
|
|
837
|
+
addPending(pendingPlaceholders, placeholderOf, values);
|
|
692
838
|
}
|
|
693
839
|
|
|
694
840
|
// Bump the version; each consumer's cursor decides when to clone+yield.
|
|
@@ -696,7 +842,7 @@ export function createHandleStore(): HandleStore {
|
|
|
696
842
|
signalEmission();
|
|
697
843
|
},
|
|
698
844
|
|
|
699
|
-
//
|
|
845
|
+
// All three go through this.push: active captures see a copy like a live
|
|
700
846
|
// push, and a diverting capture keeps it out of the store.
|
|
701
847
|
pushReplayed(
|
|
702
848
|
handleName: string,
|
|
@@ -707,6 +853,21 @@ export function createHandleStore(): HandleStore {
|
|
|
707
853
|
replayPush(this, { replayOf: loaderId }, handleName, segmentId, value);
|
|
708
854
|
},
|
|
709
855
|
|
|
856
|
+
pushPlaceholder(
|
|
857
|
+
handleName: string,
|
|
858
|
+
segmentId: string,
|
|
859
|
+
value: unknown,
|
|
860
|
+
loaderId: string,
|
|
861
|
+
): void {
|
|
862
|
+
replayPush(
|
|
863
|
+
this,
|
|
864
|
+
{ placeholderOf: loaderId },
|
|
865
|
+
handleName,
|
|
866
|
+
segmentId,
|
|
867
|
+
value,
|
|
868
|
+
);
|
|
869
|
+
},
|
|
870
|
+
|
|
710
871
|
pushRestored(
|
|
711
872
|
handleName: string,
|
|
712
873
|
segmentId: string,
|
|
@@ -716,30 +877,52 @@ export function createHandleStore(): HandleStore {
|
|
|
716
877
|
replayPush(this, { restoredOf: loaderId }, handleName, segmentId, value);
|
|
717
878
|
},
|
|
718
879
|
|
|
719
|
-
|
|
720
|
-
|
|
880
|
+
setRecordAuthority(of: (loaderId: string) => RecordAuthority): void {
|
|
881
|
+
authority = of;
|
|
721
882
|
},
|
|
722
883
|
|
|
723
884
|
settleLoaderRun(loaderId: string): void {
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
885
|
+
if (completed) return;
|
|
886
|
+
const hole = isHole(loaderId);
|
|
887
|
+
if (
|
|
888
|
+
!pendingPlaceholders.has(loaderId) &&
|
|
889
|
+
!(hole && pendingReplays.has(loaderId))
|
|
890
|
+
) {
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
dropCopies(loaderId, hole);
|
|
729
894
|
version++;
|
|
730
895
|
signalEmission();
|
|
731
896
|
},
|
|
732
897
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
898
|
+
replacePlaceholders(
|
|
899
|
+
loaderIds: readonly string[],
|
|
900
|
+
deliver: () => void,
|
|
901
|
+
): void {
|
|
902
|
+
// One pass per array over every loader's placeholders: an array two of
|
|
903
|
+
// them share gets one anchor, the first removed position.
|
|
904
|
+
let given: Set<string> | undefined;
|
|
905
|
+
let arrays: Set<unknown[]> | undefined;
|
|
906
|
+
if (!completed) {
|
|
907
|
+
for (const loaderId of loaderIds) {
|
|
908
|
+
const pending = pendingPlaceholders.get(loaderId);
|
|
909
|
+
if (!pending) continue;
|
|
910
|
+
pendingPlaceholders.delete(loaderId);
|
|
911
|
+
(given ??= new Set()).add(loaderId);
|
|
912
|
+
arrays ??= new Set();
|
|
913
|
+
for (const values of pending) arrays.add(values);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
if (!given || !arrays) {
|
|
736
917
|
deliver();
|
|
737
918
|
return;
|
|
738
919
|
}
|
|
739
|
-
|
|
920
|
+
const owners = given;
|
|
921
|
+
const ofGiven = (tag: SlotTag): boolean =>
|
|
922
|
+
tag.placeholderOf !== undefined && owners.has(tag.placeholderOf);
|
|
740
923
|
const anchors = new Map<unknown[], number>();
|
|
741
|
-
for (const values of
|
|
742
|
-
const first =
|
|
924
|
+
for (const values of arrays) {
|
|
925
|
+
const first = removeSlots(values, ofGiven);
|
|
743
926
|
if (first !== undefined) anchors.set(values, first);
|
|
744
927
|
}
|
|
745
928
|
const outer = redelivery;
|
|
@@ -758,6 +941,23 @@ export function createHandleStore(): HandleStore {
|
|
|
758
941
|
return this.settled.then(() => cloneHandleData(data));
|
|
759
942
|
},
|
|
760
943
|
|
|
944
|
+
freezeDocumentSnapshot(): void {
|
|
945
|
+
if (documentSnapshot) return;
|
|
946
|
+
if (process.env.NODE_ENV !== "production" && documentStreamRead) {
|
|
947
|
+
const error = new Error(
|
|
948
|
+
"HandleStore.freezeDocumentSnapshot() ran after the document's " +
|
|
949
|
+
'handle stream (stream("settled")) was first read. That stream ' +
|
|
950
|
+
"keeps reading the live store, so the document hydrates with " +
|
|
951
|
+
"whatever a loader pushed by then instead of the frozen state " +
|
|
952
|
+
"(issue #1035). Freeze before the payload that carries the " +
|
|
953
|
+
"stream starts rendering.",
|
|
954
|
+
);
|
|
955
|
+
if (this.onError) this.onError(error);
|
|
956
|
+
else console.error(error);
|
|
957
|
+
}
|
|
958
|
+
documentSnapshot = { version, data: cloneHandleData(data) };
|
|
959
|
+
},
|
|
960
|
+
|
|
761
961
|
async *stream(
|
|
762
962
|
until: "settled" | "fullySettled" = "fullySettled",
|
|
763
963
|
): AsyncGenerator<HandleData, void, unknown> {
|
|
@@ -768,6 +968,22 @@ export function createHandleStore(): HandleStore {
|
|
|
768
968
|
// then, so the completed flag must be armed here.
|
|
769
969
|
notifyDrain();
|
|
770
970
|
|
|
971
|
+
if (until === "settled") {
|
|
972
|
+
// A frozen document lane cannot change, so it is served at once:
|
|
973
|
+
// SSR and the pre-hydration drain block on this stream, and the
|
|
974
|
+
// batching hop below would cost every shell HIT a timer tick. It is
|
|
975
|
+
// yielded as the frozen object itself, already a copy: the lane's
|
|
976
|
+
// one consumer (resolvedHandleStream) builds a new object from a
|
|
977
|
+
// yield and never writes to it.
|
|
978
|
+
if (documentSnapshot) {
|
|
979
|
+
if (Object.keys(documentSnapshot.data).length > 0) {
|
|
980
|
+
yield documentSnapshot.data;
|
|
981
|
+
}
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
documentStreamRead = true;
|
|
985
|
+
}
|
|
986
|
+
|
|
771
987
|
// Per-consumer termination flag driven by the chosen barrier. The
|
|
772
988
|
// global `completed` (late-push guard) always keys on FULL drain via
|
|
773
989
|
// notifyDrain — a "settled"-scoped consumer finishing does not make
|
|
@@ -806,8 +1022,9 @@ export function createHandleStore(): HandleStore {
|
|
|
806
1022
|
// state. This channel exists only for pushes that land AFTER `settled`
|
|
807
1023
|
// — streaming loader bodies writing meta/breadcrumbs mid-body.
|
|
808
1024
|
await this.settled;
|
|
809
|
-
|
|
810
|
-
|
|
1025
|
+
// A frozen document snapshot carried nothing newer than its version.
|
|
1026
|
+
let seen = documentSnapshot?.version ?? version;
|
|
1027
|
+
if (auxInflightCount === 0 && version === seen) {
|
|
811
1028
|
// Auxiliary lane already drained: every loader push (if any) beat the
|
|
812
1029
|
// handler barrier and rode the normal snapshot. Nothing late follows —
|
|
813
1030
|
// handler pushes after settled are illegal by contract.
|