@rangojs/router 0.22.1 → 0.24.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 +10 -4
- package/dist/testing/vitest.js +31 -1
- package/dist/types/browser/defer.d.ts +48 -0
- package/dist/types/browser/document-revealed.d.ts +17 -0
- package/dist/types/browser/event-controller.d.ts +7 -1
- package/dist/types/browser/merge-segment-loaders.d.ts +5 -2
- package/dist/types/browser/navigation-transaction.d.ts +21 -7
- package/dist/types/browser/partial-update.d.ts +39 -8
- package/dist/types/browser/react/NavigationProvider.d.ts +19 -5
- package/dist/types/browser/react/context.d.ts +3 -2
- package/dist/types/browser/react/filter-segment-order.d.ts +7 -0
- package/dist/types/browser/response-adapter.d.ts +7 -0
- package/dist/types/browser/types.d.ts +24 -10
- package/dist/types/browser/validate-redirect-origin.d.ts +11 -0
- package/dist/types/build/generate-route-types.d.ts +1 -1
- package/dist/types/build/route-types/router-processing.d.ts +1 -1
- package/dist/types/build/route-types/scan-filter.d.ts +22 -2
- package/dist/types/cache/cf/cf-cache-store.d.ts +16 -1
- package/dist/types/cache/cf/cf-cache-types.d.ts +23 -2
- package/dist/types/cache/document-cache-policy.d.ts +24 -0
- package/dist/types/cache/document-cache.d.ts +2 -10
- package/dist/types/client-urls/client-urls.d.ts +1 -1
- package/dist/types/client-urls/hoisted-revalidate.d.ts +13 -0
- package/dist/types/client-urls/navigation.d.ts +0 -37
- package/dist/types/client-urls/revalidate-registry.d.ts +4 -0
- package/dist/types/client-urls/server-projection.d.ts +29 -1
- package/dist/types/client-urls/types.d.ts +34 -69
- package/dist/types/client.d.ts +0 -1
- package/dist/types/context-var.d.ts +12 -0
- package/dist/types/errors.d.ts +15 -0
- package/dist/types/handles/deferred-resolution.d.ts +2 -1
- package/dist/types/handles/pushed-by.d.ts +31 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.rsc.d.ts +2 -0
- package/dist/types/loader-store.d.ts +8 -0
- package/dist/types/prefetch-markers.d.ts +11 -0
- package/dist/types/response-utils.d.ts +7 -0
- package/dist/types/reverse.d.ts +8 -8
- package/dist/types/route-definition/helpers-types.d.ts +27 -0
- package/dist/types/route-definition/rewrite.d.ts +56 -0
- package/dist/types/route-map-builder.d.ts +7 -2
- package/dist/types/router/basename.d.ts +7 -0
- package/dist/types/router/handler-context.d.ts +1 -5
- package/dist/types/router/internal-params.d.ts +8 -0
- package/dist/types/router/is-action.d.ts +2 -1
- package/dist/types/router/middleware-types.d.ts +15 -0
- package/dist/types/router/revalidation.d.ts +1 -2
- package/dist/types/router/route-trie-builder.d.ts +10 -0
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
- package/dist/types/router/segment-resolution/mask-nested.d.ts +4 -2
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +264 -0
- package/dist/types/rsc/full-payload.d.ts +11 -0
- package/dist/types/rsc/handler.d.ts +33 -0
- package/dist/types/rsc/helpers.d.ts +8 -0
- package/dist/types/rsc/loader-fetch.d.ts +16 -0
- package/dist/types/rsc/rewrite-passes.d.ts +74 -0
- package/dist/types/rsc/ssr-setup.d.ts +3 -1
- package/dist/types/server/context.d.ts +21 -1
- package/dist/types/server/handle-store.d.ts +9 -1
- package/dist/types/server/request-context.d.ts +52 -2
- package/dist/types/server/rewrite-trail.d.ts +49 -0
- package/dist/types/server/slot-accessors.d.ts +29 -0
- package/dist/types/server.d.ts +1 -0
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +4 -4
- package/dist/types/testing/internal/action-id.d.ts +10 -1
- package/dist/types/testing/internal/client-urls-projection.d.ts +16 -0
- package/dist/types/testing/internal/test-action.d.ts +8 -0
- package/dist/types/testing/render-route.d.ts +8 -0
- package/dist/types/testing/run-loader.d.ts +2 -1
- package/dist/types/testing/run-middleware.d.ts +20 -0
- package/dist/types/testing/run-revalidate.d.ts +74 -0
- package/dist/types/testing/run-transition-when.d.ts +2 -3
- package/dist/types/testing/serve-shell-request.d.ts +40 -1
- package/dist/types/testing/vitest.d.ts +11 -0
- package/dist/types/thenable-status.d.ts +6 -0
- package/dist/types/types/boundaries.d.ts +9 -0
- package/dist/types/types/handler-context.d.ts +14 -0
- package/dist/types/types/loader-types.d.ts +30 -0
- package/dist/types/types/request-scope.d.ts +7 -0
- package/dist/types/types/segments.d.ts +21 -4
- package/dist/types/urls/path-helper-types.d.ts +25 -4
- package/dist/types/urls/type-extraction.d.ts +6 -2
- package/dist/types/vite/discovery/discover-routers.d.ts +9 -0
- package/dist/types/vite/discovery/state.d.ts +14 -4
- package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +54 -0
- package/dist/types/vite/plugins/hoist/literal.d.ts +83 -0
- package/dist/types/vite/plugins/hoist/scope.d.ts +40 -0
- package/dist/types/vite/plugins/hoist/source.d.ts +41 -0
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +8 -11
- package/dist/types/vite/router-discovery.d.ts +10 -0
- package/dist/vite/index.js +974 -379
- package/package.json +1 -1
- package/skills/caching/SKILL.md +54 -5
- package/skills/catalog.json +3 -3
- package/skills/client-urls/SKILL.md +197 -24
- package/skills/cloudflare/SKILL.md +14 -9
- package/skills/hooks/SKILL.md +16 -3
- package/skills/hooks/data.md +3 -1
- package/skills/links/SKILL.md +24 -0
- package/skills/loader/SKILL.md +199 -2
- package/skills/middleware/SKILL.md +183 -2
- package/skills/migrate-nextjs/SKILL.md +2 -2
- package/skills/migrate-react-router/SKILL.md +10 -7
- package/skills/migrate-react-router/data-and-actions.md +1 -1
- package/skills/ppr/SKILL.md +22 -0
- package/skills/prerender/SKILL.md +22 -8
- package/skills/rango/SKILL.md +13 -7
- package/skills/route/SKILL.md +139 -0
- package/skills/server-actions/SKILL.md +27 -0
- package/skills/testing/SKILL.md +3 -2
- package/skills/testing/client-components.md +2 -1
- package/skills/testing/middleware.md +31 -10
- package/skills/testing/predicates.md +67 -32
- package/skills/testing/response-routes.md +1 -0
- package/skills/testing/serve-shell-request.md +107 -19
- package/skills/testing/setup.md +2 -2
- package/skills/typesafety/route-types.md +2 -2
- package/skills/use-cache/SKILL.md +4 -2
- package/skills/view-transitions/SKILL.md +26 -0
- package/src/__augment-tests__/augmented.check.ts +20 -0
- package/src/browser/defer.ts +82 -0
- package/src/browser/document-revealed.ts +52 -0
- package/src/browser/event-controller.ts +46 -32
- package/src/browser/merge-segment-loaders.ts +31 -18
- package/src/browser/navigation-bridge.ts +82 -16
- package/src/browser/navigation-client.ts +48 -36
- package/src/browser/navigation-store.ts +13 -0
- package/src/browser/navigation-transaction.ts +25 -8
- package/src/browser/partial-update.ts +554 -146
- package/src/browser/prefetch/fetch.ts +2 -1
- package/src/browser/react/Link.tsx +7 -9
- package/src/browser/react/NavigationProvider.tsx +88 -49
- package/src/browser/react/context.ts +3 -2
- package/src/browser/react/filter-segment-order.ts +13 -2
- package/src/browser/react/use-router.ts +2 -4
- package/src/browser/response-adapter.ts +20 -0
- package/src/browser/rsc-router.tsx +17 -45
- package/src/browser/server-action-bridge.ts +25 -63
- package/src/browser/types.ts +24 -10
- package/src/browser/validate-redirect-origin.ts +16 -0
- package/src/build/generate-route-types.ts +1 -0
- package/src/build/route-types/router-processing.ts +3 -6
- package/src/build/route-types/scan-filter.ts +45 -11
- package/src/cache/cf/cf-cache-store.ts +59 -21
- package/src/cache/cf/cf-cache-types.ts +24 -2
- package/src/cache/document-cache-policy.ts +131 -0
- package/src/cache/document-cache.ts +21 -107
- package/src/client-urls/client-urls.ts +22 -6
- package/src/client-urls/hoisted-revalidate.ts +25 -0
- package/src/client-urls/navigation.ts +0 -96
- package/src/client-urls/revalidate-registry.ts +30 -0
- package/src/client-urls/server-projection.ts +151 -65
- package/src/client-urls/types.ts +39 -71
- package/src/client.tsx +9 -5
- package/src/context-var.ts +32 -2
- package/src/errors.ts +34 -0
- package/src/handles/deferred-resolution.ts +28 -5
- package/src/handles/pushed-by.ts +148 -0
- package/src/index.rsc.ts +8 -0
- package/src/index.ts +7 -0
- package/src/loader-store.ts +18 -7
- package/src/prefetch-markers.ts +13 -0
- package/src/response-utils.ts +19 -0
- package/src/reverse.ts +17 -14
- package/src/route-content-wrapper.tsx +18 -4
- package/src/route-definition/dsl-helpers.ts +27 -4
- package/src/route-definition/helpers-types.ts +27 -1
- package/src/route-definition/redirect.ts +2 -15
- package/src/route-definition/rewrite.ts +146 -0
- package/src/route-map-builder.ts +14 -11
- package/src/router/basename.ts +20 -0
- package/src/router/handler-context.ts +3 -13
- package/src/router/intercept-resolution.ts +35 -16
- package/src/router/internal-params.ts +16 -0
- package/src/router/is-action.ts +2 -1
- package/src/router/loader-resolution.ts +85 -4
- package/src/router/match-api.ts +36 -1
- package/src/router/match-middleware/background-revalidation.ts +16 -4
- package/src/router/match-middleware/cache-lookup.ts +35 -7
- package/src/router/match-middleware/cache-store.ts +15 -1
- package/src/router/match-middleware/intercept-resolution.ts +4 -2
- package/src/router/match-result.ts +14 -2
- package/src/router/middleware-types.ts +16 -0
- package/src/router/middleware.ts +127 -49
- package/src/router/navigation-snapshot.ts +2 -1
- package/src/router/revalidation.ts +1 -1
- package/src/router/route-trie-builder.ts +28 -1
- package/src/router/segment-resolution/fresh.ts +22 -18
- package/src/router/segment-resolution/loader-cache.ts +2 -1
- package/src/router/segment-resolution/loader-mask.ts +5 -3
- package/src/router/segment-resolution/loader-snapshot.ts +18 -4
- package/src/router/segment-resolution/mask-nested.ts +8 -4
- package/src/router/segment-resolution/prefetch-deferral.ts +583 -0
- package/src/router/segment-resolution/revalidation.ts +215 -82
- package/src/rsc/full-payload.ts +37 -5
- package/src/rsc/handler.ts +82 -55
- package/src/rsc/helpers.ts +24 -0
- package/src/rsc/loader-fetch.ts +79 -24
- package/src/rsc/response-cache-serve.ts +18 -2
- package/src/rsc/response-route-handler.ts +4 -7
- package/src/rsc/rewrite-passes.ts +203 -0
- package/src/rsc/rsc-rendering.ts +54 -9
- package/src/rsc/shell-capture.ts +2 -1
- package/src/rsc/ssr-setup.ts +11 -5
- package/src/segment-loader-promise.ts +23 -20
- package/src/segment-system.tsx +60 -26
- package/src/server/context.ts +39 -5
- package/src/server/cookie-parse.ts +36 -12
- package/src/server/cookie-store.ts +7 -3
- package/src/server/handle-store.ts +152 -25
- package/src/server/request-context.ts +175 -64
- package/src/server/request-headers.ts +4 -14
- package/src/server/rewrite-trail.ts +190 -0
- package/src/server/slot-accessors.ts +57 -0
- package/src/server.ts +4 -0
- package/src/ssr/index.tsx +118 -16
- package/src/testing/action-form.ts +2 -11
- package/src/testing/dispatch.ts +138 -67
- package/src/testing/e2e/page-helpers.ts +3 -1
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +7 -3
- package/src/testing/internal/action-id.ts +16 -1
- package/src/testing/internal/client-urls-projection.ts +70 -0
- package/src/testing/internal/test-action.ts +25 -0
- package/src/testing/render-route.tsx +16 -3
- package/src/testing/run-loader.ts +4 -1
- package/src/testing/run-middleware.ts +56 -11
- package/src/testing/run-revalidate.ts +142 -0
- package/src/testing/run-transition-when.ts +7 -8
- package/src/testing/serve-shell-request.ts +131 -8
- package/src/testing/vitest.ts +67 -1
- package/src/thenable-status.ts +11 -0
- package/src/types/boundaries.ts +12 -0
- package/src/types/handler-context.ts +14 -0
- package/src/types/loader-types.ts +30 -0
- package/src/types/request-scope.ts +8 -0
- package/src/types/segments.ts +26 -4
- package/src/urls/path-helper-types.ts +27 -5
- package/src/urls/type-extraction.ts +30 -10
- package/src/use-loader.tsx +18 -13
- package/src/vite/discovery/client-urls-projection.ts +20 -21
- package/src/vite/discovery/discover-routers.ts +94 -26
- package/src/vite/discovery/state.ts +28 -4
- package/src/vite/discovery/virtual-module-codegen.ts +22 -17
- package/src/vite/plugins/client-urls-revalidate-hoist.ts +564 -0
- package/src/vite/plugins/expose-internal-ids.ts +2 -3
- package/src/vite/plugins/hoist/literal.ts +417 -0
- package/src/vite/plugins/hoist/scope.ts +370 -0
- package/src/vite/plugins/hoist/source.ts +84 -0
- package/src/vite/plugins/transition-when-hoist.ts +113 -779
- package/src/vite/rango.ts +2 -0
- package/src/vite/router-discovery.ts +116 -53
- package/dist/types/client-urls/revalidate-chain.d.ts +0 -33
- package/dist/types/client-urls/revalidation-protocol.d.ts +0 -25
- package/dist/types/testing/run-client-revalidate.d.ts +0 -43
- package/src/client-urls/revalidate-chain.ts +0 -83
- package/src/client-urls/revalidation-protocol.ts +0 -56
- package/src/testing/run-client-revalidate.ts +0 -92
|
@@ -35,6 +35,7 @@ import { shouldPrefetch } from "./policy.js";
|
|
|
35
35
|
import { debugLog, IS_BROWSER_DEBUG } from "../logging.js";
|
|
36
36
|
import { teeWithCompletion, isForeignRouterId } from "../response-adapter.js";
|
|
37
37
|
import { SEGMENT_FRAGMENT_CAPABILITY_HEADER } from "../../segment-fragments.js";
|
|
38
|
+
import { PREFETCH_HEADER } from "../../prefetch-markers.js";
|
|
38
39
|
import type { RscPayload } from "../types.js";
|
|
39
40
|
|
|
40
41
|
/**
|
|
@@ -263,7 +264,7 @@ function executePrefetchFetch(
|
|
|
263
264
|
headers: {
|
|
264
265
|
"X-Rango-State": rangoState,
|
|
265
266
|
"X-RSC-Router-Client-Path": window.location.href,
|
|
266
|
-
|
|
267
|
+
[PREFETCH_HEADER]: "1",
|
|
267
268
|
...(isFragmentPassthroughEnabled() && {
|
|
268
269
|
[SEGMENT_FRAGMENT_CAPABILITY_HEADER]: "1",
|
|
269
270
|
}),
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
subscribeToAdaptiveStrategyChange,
|
|
43
43
|
} from "../prefetch/default-strategy.js";
|
|
44
44
|
import { getAppVersion } from "../app-version.js";
|
|
45
|
+
import { prefixBasename } from "../../router/basename.js";
|
|
45
46
|
import type { PrefetchStrategy } from "../../router/prefetch-default.js";
|
|
46
47
|
|
|
47
48
|
// The PrefetchStrategy union is defined in router/prefetch-default.ts (both
|
|
@@ -244,15 +245,12 @@ export const Link: ForwardRefExoticComponent<
|
|
|
244
245
|
// store and no origin: links stay internal.
|
|
245
246
|
const isExternal = isExternalUrl(to, ctx?.origin);
|
|
246
247
|
|
|
247
|
-
// Auto-prefix with basename for app-local paths
|
|
248
|
-
//
|
|
249
|
-
const resolvedTo = useMemo(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
return to;
|
|
254
|
-
return to === "/" ? bn : bn + to;
|
|
255
|
-
}, [to, isExternal, ctx?.basename]);
|
|
248
|
+
// Auto-prefix with basename for app-local paths (prefixBasename: not when
|
|
249
|
+
// external, already prefixed, or not root-relative).
|
|
250
|
+
const resolvedTo = useMemo(
|
|
251
|
+
() => (isExternal ? to : prefixBasename(to, ctx?.basename)),
|
|
252
|
+
[to, isExternal, ctx?.basename],
|
|
253
|
+
);
|
|
256
254
|
|
|
257
255
|
// No explicit `prefetch` prop: fall back to the router-wide default from
|
|
258
256
|
// the router store. Adaptive is resolved against the input capability when a
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
NavigationUpdate,
|
|
23
23
|
NavigateOptionsInternal,
|
|
24
24
|
NavigationBridge,
|
|
25
|
+
RscMetadata,
|
|
25
26
|
} from "../types.js";
|
|
26
27
|
import type { EventController } from "../event-controller.js";
|
|
27
28
|
import { RootErrorBoundary } from "../../root-error-boundary.js";
|
|
@@ -211,6 +212,30 @@ async function processHandles(
|
|
|
211
212
|
}
|
|
212
213
|
}
|
|
213
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Read the handle stream of `metadata` into the event controller and the
|
|
217
|
+
* entry on screen. The provider's own call for every payload it is handed,
|
|
218
|
+
* and the one write for a response that hands React nothing (a fill,
|
|
219
|
+
* browser/partial-update.ts fillHoles).
|
|
220
|
+
*/
|
|
221
|
+
export function consumeHandles(
|
|
222
|
+
metadata: RscMetadata,
|
|
223
|
+
eventController: EventController,
|
|
224
|
+
store: NavigationStore,
|
|
225
|
+
): void {
|
|
226
|
+
processHandles(metadata.handles!, {
|
|
227
|
+
eventController,
|
|
228
|
+
store,
|
|
229
|
+
matched: metadata.matched,
|
|
230
|
+
isPartial: metadata.isPartial,
|
|
231
|
+
resolvedIds: metadata.resolvedIds,
|
|
232
|
+
// Now: by the time the stream ends the user may be elsewhere.
|
|
233
|
+
historyKey: store.getHistoryKey(),
|
|
234
|
+
}).catch((err) =>
|
|
235
|
+
console.error("[NavigationProvider] Error consuming handles:", err),
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
214
239
|
/**
|
|
215
240
|
* Props for NavigationProvider
|
|
216
241
|
*/
|
|
@@ -286,10 +311,17 @@ export interface NavigationProviderProps {
|
|
|
286
311
|
* The document this provider hydrates (initBrowserApp; renderRoute with
|
|
287
312
|
* `hydrate: true`), whose store EventController.lockHydration locked.
|
|
288
313
|
* `settled` resolves once the stream has ended and React has revealed every
|
|
289
|
-
* boundary.
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
314
|
+
* boundary. Then a transition changes HydrationBarrierContext alone, and
|
|
315
|
+
* React commits it once every boundary has hydrated, one still suspended on
|
|
316
|
+
* the client (a lazy chunk, a deferred-hydration gate) included. Until that
|
|
317
|
+
* commit location state is `undefined`, as SSR rendered it, through any
|
|
318
|
+
* commit no payload follows (a shallow navigation), and the store is
|
|
319
|
+
* locked; after it the entry's state lands in a transition of its own
|
|
320
|
+
* and the store is released (handle data lands in that release
|
|
321
|
+
* notification). A navigation committed earlier releases the store before
|
|
322
|
+
* its update, for the whole hold when the navigation is held: an old-page
|
|
323
|
+
* boundary hydrating meanwhile reads the destination's values and can
|
|
324
|
+
* mismatch.
|
|
293
325
|
* Absent for a provider mounted on the client.
|
|
294
326
|
*/
|
|
295
327
|
hydration?: { settled: Promise<void> };
|
|
@@ -368,6 +400,11 @@ export function NavigationProvider({
|
|
|
368
400
|
}
|
|
369
401
|
const shellRef = appShellRef ?? fallbackShellRef.current;
|
|
370
402
|
|
|
403
|
+
// Fixed for the document: read once on mount.
|
|
404
|
+
const [origin] = useState(
|
|
405
|
+
() => (eventController.getState().location as URL | undefined)?.origin,
|
|
406
|
+
);
|
|
407
|
+
|
|
371
408
|
const contextValue = useMemo<NavigationStoreContextValue>(() => {
|
|
372
409
|
const value = {
|
|
373
410
|
store,
|
|
@@ -375,8 +412,7 @@ export function NavigationProvider({
|
|
|
375
412
|
navigate,
|
|
376
413
|
refresh,
|
|
377
414
|
defaultPrefetch: initialPayload.metadata.defaultPrefetch,
|
|
378
|
-
|
|
379
|
-
origin: (eventController.getState().location as URL | undefined)?.origin,
|
|
415
|
+
origin,
|
|
380
416
|
} as NavigationStoreContextValue;
|
|
381
417
|
Object.defineProperty(value, "basename", {
|
|
382
418
|
configurable: true,
|
|
@@ -400,18 +436,18 @@ export function NavigationProvider({
|
|
|
400
436
|
return startConnectionWarmup();
|
|
401
437
|
}, [warmupEnabled]);
|
|
402
438
|
|
|
403
|
-
// The document has been revealed
|
|
404
|
-
//
|
|
405
|
-
// dehydrated
|
|
439
|
+
// The document has been revealed. The barrier must be the only update in
|
|
440
|
+
// its transition lane: then React holds the commit until every boundary
|
|
441
|
+
// still dehydrated beneath it has hydrated. Any other update in the lane
|
|
442
|
+
// (location state in its own startTransition here included) can let React
|
|
443
|
+
// commit around a boundary still suspended on the client. Why:
|
|
444
|
+
// docs/internal/execution-model.md, "The reveal barrier is alone in its lane".
|
|
406
445
|
useEffect(() => {
|
|
407
446
|
if (!hydration) return;
|
|
408
447
|
let cancelled = false;
|
|
409
448
|
void hydration.settled.then(() => {
|
|
410
449
|
if (cancelled || !hydrationOpen.current) return;
|
|
411
|
-
startTransition(() =>
|
|
412
|
-
setLocationState(eventController.getLocationState());
|
|
413
|
-
setHydrationBarrier((n) => n + 1);
|
|
414
|
-
});
|
|
450
|
+
startTransition(() => setHydrationBarrier((n) => n + 1));
|
|
415
451
|
});
|
|
416
452
|
return () => {
|
|
417
453
|
cancelled = true;
|
|
@@ -419,20 +455,31 @@ export function NavigationProvider({
|
|
|
419
455
|
}, []);
|
|
420
456
|
|
|
421
457
|
// After the barrier's commit no boundary under this provider is dehydrated:
|
|
422
|
-
//
|
|
458
|
+
// the entry's location state lands in a transition of its own, and the
|
|
459
|
+
// store is released so its handle writes reach their readers. A navigation
|
|
460
|
+
// that committed first has done both (store onUpdate below).
|
|
423
461
|
useEffect(() => {
|
|
424
|
-
if (hydrationBarrier === 0) return;
|
|
462
|
+
if (hydrationBarrier === 0 || !hydrationOpen.current) return;
|
|
425
463
|
hydrationOpen.current = false;
|
|
464
|
+
startTransition(() => setLocationState(eventController.getLocationState()));
|
|
426
465
|
eventController.releaseHydrationLock();
|
|
427
466
|
}, [hydrationBarrier]);
|
|
428
467
|
|
|
429
468
|
// A commit no payload follows (a shallow navigation, a commit that keeps
|
|
430
469
|
// every segment, merged server-set state): the tree on screen is already
|
|
431
|
-
// the entry's, so its state is taken with the commit's notification.
|
|
470
|
+
// the entry's, so its state is taken with the commit's notification. Not
|
|
471
|
+
// while the hydration window is open: a boundary of the document may still
|
|
472
|
+
// be dehydrated, an update a reader outside it takes commits around it, and
|
|
473
|
+
// it then hydrates against the new state. The barrier's effect gives the
|
|
474
|
+
// controller's state, this commit's included. The window stays open: this
|
|
475
|
+
// commit mounts nothing that must read the live store.
|
|
432
476
|
useEffect(
|
|
433
477
|
() =>
|
|
434
478
|
eventController.subscribe(() => {
|
|
435
|
-
if (
|
|
479
|
+
if (
|
|
480
|
+
eventController.takeTreelessLocationState() &&
|
|
481
|
+
!hydrationOpen.current
|
|
482
|
+
) {
|
|
436
483
|
setLocationState(eventController.getLocationState());
|
|
437
484
|
}
|
|
438
485
|
}),
|
|
@@ -456,7 +503,7 @@ export function NavigationProvider({
|
|
|
456
503
|
return unsub;
|
|
457
504
|
}, [eventController]);
|
|
458
505
|
|
|
459
|
-
//
|
|
506
|
+
// The last navigation's scroll decision, until React commits its update.
|
|
460
507
|
const pendingScrollRef = useRef<NavigationUpdate["scroll"]>(undefined);
|
|
461
508
|
|
|
462
509
|
// Apply scroll after React commits the new content to the DOM
|
|
@@ -478,11 +525,14 @@ export function NavigationProvider({
|
|
|
478
525
|
useEffect(() => {
|
|
479
526
|
const unsubscribe = store.onUpdate((update) => {
|
|
480
527
|
if (process.env.NODE_ENV !== "production") auditTreeUpdate(update.root);
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
//
|
|
485
|
-
|
|
528
|
+
// Scroll belongs to the navigation transaction: only an update that
|
|
529
|
+
// carries one's decision sets the pending action, and the layout effect
|
|
530
|
+
// above consumes it once, after the commit that follows. An update with
|
|
531
|
+
// no decision (server action, error, prefetch: false fill) leaves a
|
|
532
|
+
// pending one alone. React can hold a navigation's commit, and whatever
|
|
533
|
+
// lands meanwhile must not cost the navigation its scroll. Nothing
|
|
534
|
+
// stale is replayed: every update commits, and that commit consumes.
|
|
535
|
+
if (update.scroll) pendingScrollRef.current = update.scroll;
|
|
486
536
|
|
|
487
537
|
// Committed before the document finished hydrating: end the window
|
|
488
538
|
// first, so the page this update mounts reads the live store. A
|
|
@@ -494,10 +544,7 @@ export function NavigationProvider({
|
|
|
494
544
|
eventController.releaseHydrationLock();
|
|
495
545
|
}
|
|
496
546
|
|
|
497
|
-
setPayload({
|
|
498
|
-
root: update.root,
|
|
499
|
-
metadata: update.metadata,
|
|
500
|
-
});
|
|
547
|
+
setPayload({ root: update.root, metadata: update.metadata });
|
|
501
548
|
// The state of the entry this payload renders, as its commit recorded
|
|
502
549
|
// it: one update, so one lane, so one React commit with the tree.
|
|
503
550
|
setLocationState(eventController.getLocationState());
|
|
@@ -509,31 +556,23 @@ export function NavigationProvider({
|
|
|
509
556
|
eventController.setParams(update.metadata.params);
|
|
510
557
|
}
|
|
511
558
|
|
|
559
|
+
// A page rendered from a history-cache entry (back/forward, or a link
|
|
560
|
+
// to it) starts from the entry's pushes, and its response's handles
|
|
561
|
+
// merge onto them: the page on screen's pushes are not its segments'.
|
|
562
|
+
const { matched, cachedHandleData } = update.metadata;
|
|
563
|
+
if (matched && cachedHandleData) {
|
|
564
|
+
eventController.setHandleData(cachedHandleData, matched);
|
|
565
|
+
}
|
|
512
566
|
// Update handle data progressively as it streams in
|
|
513
567
|
if (update.metadata.handles) {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
processHandles(update.metadata.handles, {
|
|
519
|
-
eventController,
|
|
520
|
-
store,
|
|
521
|
-
matched: update.metadata.matched,
|
|
522
|
-
isPartial: update.metadata.isPartial,
|
|
523
|
-
resolvedIds: update.metadata.resolvedIds,
|
|
524
|
-
historyKey,
|
|
525
|
-
}).catch((err) =>
|
|
526
|
-
console.error("[NavigationProvider] Error consuming handles:", err),
|
|
527
|
-
);
|
|
528
|
-
} else if (update.metadata.matched) {
|
|
529
|
-
// cachedHandleData present -> full restore (back/forward); absent ->
|
|
530
|
-
// partial cleanup of segments no longer matched.
|
|
531
|
-
const cached = update.metadata.cachedHandleData;
|
|
568
|
+
consumeHandles(update.metadata, eventController, store);
|
|
569
|
+
} else if (matched && !cachedHandleData) {
|
|
570
|
+
// Partial cleanup of segments no longer matched.
|
|
532
571
|
eventController.setHandleData(
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
572
|
+
{},
|
|
573
|
+
matched,
|
|
574
|
+
true,
|
|
575
|
+
update.metadata.resolvedIds,
|
|
537
576
|
);
|
|
538
577
|
}
|
|
539
578
|
|
|
@@ -91,8 +91,9 @@ export const LocationStateContext: Context<LocationStateSnapshot> =
|
|
|
91
91
|
* Read by no component. NavigationProvider changes its value in a transition so
|
|
92
92
|
* that React hydrates every boundary still dehydrated beneath it before that
|
|
93
93
|
* commit: React hydrates a dehydrated boundary first when a provider above it
|
|
94
|
-
* changes, and nothing re-renders for a value nobody reads.
|
|
95
|
-
*
|
|
94
|
+
* changes, and nothing re-renders for a value nobody reads. It must stay
|
|
95
|
+
* unread and alone in its lane (docs/internal/execution-model.md, "The reveal
|
|
96
|
+
* barrier is alone in its lane"). See NavigationProviderProps.hydration.
|
|
96
97
|
*/
|
|
97
98
|
export const HydrationBarrierContext: Context<number> =
|
|
98
99
|
createContext<number>(0);
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A loader sub-id carries a `D${index}.` marker ("M0L0D1.user"). Non-global,
|
|
3
|
+
* so `.test` and `.replace` are stateless.
|
|
4
|
+
*/
|
|
5
|
+
export const LOADER_SEGMENT_ID: RegExp = /D\d+\./;
|
|
6
|
+
|
|
7
|
+
/** True for a loader sub-id ("M0L0D1.user"), false for a layout/route/slot id. */
|
|
8
|
+
export function isLoaderSegmentId(id: string): boolean {
|
|
9
|
+
return LOADER_SEGMENT_ID.test(id);
|
|
10
|
+
}
|
|
11
|
+
|
|
1
12
|
/**
|
|
2
13
|
* Build the handle-collection segment order from a raw `matched` list.
|
|
3
14
|
*
|
|
@@ -24,7 +35,7 @@ export function filterSegmentOrder(matched: string[]): string[] {
|
|
|
24
35
|
const nonSlotSet = new Set<string>();
|
|
25
36
|
|
|
26
37
|
for (const id of matched) {
|
|
27
|
-
if (
|
|
38
|
+
if (isLoaderSegmentId(id)) continue;
|
|
28
39
|
const slotIdx = id.indexOf(".@");
|
|
29
40
|
if (slotIdx >= 0) {
|
|
30
41
|
const parent = id.slice(0, slotIdx);
|
|
@@ -66,5 +77,5 @@ export function filterSegmentOrder(matched: string[]): string[] {
|
|
|
66
77
|
* and React reports a hydration mismatch.
|
|
67
78
|
*/
|
|
68
79
|
export function filterRouteSegmentIds(matched: string[]): string[] {
|
|
69
|
-
return matched.filter((id) => !id.includes(".@") &&
|
|
80
|
+
return matched.filter((id) => !id.includes(".@") && !isLoaderSegmentId(id));
|
|
70
81
|
}
|
|
@@ -5,6 +5,7 @@ import { NavigationStoreContext } from "./context.js";
|
|
|
5
5
|
import { useMount } from "./use-mount.js";
|
|
6
6
|
import { prefetchDirect } from "../prefetch/loader.js";
|
|
7
7
|
import { getAppVersion } from "../app-version.js";
|
|
8
|
+
import { prefixBasename } from "../../router/basename.js";
|
|
8
9
|
import type { RouterInstance, RouterNavigateOptions } from "../types.js";
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -51,10 +52,7 @@ export function useRouter(): RouterInstance {
|
|
|
51
52
|
return useMemo<RouterInstance>(() => {
|
|
52
53
|
/** Prefix a root-relative path with basename if not already prefixed. */
|
|
53
54
|
function withBasename(url: string): string {
|
|
54
|
-
|
|
55
|
-
if (!bn || !url.startsWith("/") || url.startsWith(bn + "/") || url === bn)
|
|
56
|
-
return url;
|
|
57
|
-
return url === "/" ? bn : bn + url;
|
|
55
|
+
return prefixBasename(url, ctx!.basename);
|
|
58
56
|
}
|
|
59
57
|
|
|
60
58
|
/**
|
|
@@ -26,6 +26,26 @@ export function extractRscHeaderUrl(
|
|
|
26
26
|
return url ? { url } : "blocked";
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Run `request` with a signal that `signal` aborts until the request settles,
|
|
31
|
+
* and only until then. A response that has arrived streams to its end:
|
|
32
|
+
* aborting a Flight body mid-read rejects every chunk a mounted tree may be
|
|
33
|
+
* reading. Aborted, the request rejects with `signal.reason`.
|
|
34
|
+
*/
|
|
35
|
+
export function abortUntilResponse<T>(
|
|
36
|
+
signal: AbortSignal | undefined,
|
|
37
|
+
request: (signal: AbortSignal | undefined) => Promise<T>,
|
|
38
|
+
): Promise<T> {
|
|
39
|
+
if (!signal) return request(undefined);
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
const cancel = (): void => controller.abort(signal.reason);
|
|
42
|
+
if (signal.aborted) cancel();
|
|
43
|
+
else signal.addEventListener("abort", cancel, { once: true });
|
|
44
|
+
return request(controller.signal).finally(() =>
|
|
45
|
+
signal.removeEventListener("abort", cancel),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
29
49
|
/**
|
|
30
50
|
* Empty 200 response that won't choke Flight parsing.
|
|
31
51
|
* Used when a header URL is blocked by origin validation.
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { createEventController } from "./event-controller.js";
|
|
12
12
|
import { resolveDeferredHandleValues } from "../handles/deferred-resolution.js";
|
|
13
13
|
import { validateRedirectOrigin } from "./validate-redirect-origin.js";
|
|
14
|
+
import { awaitDocumentRevealed } from "./document-revealed.js";
|
|
14
15
|
import { createNavigationClient } from "./navigation-client.js";
|
|
15
16
|
import { createServerActionBridge } from "./server-action-bridge.js";
|
|
16
17
|
import { createNavigationBridge } from "./navigation-bridge.js";
|
|
@@ -331,14 +332,23 @@ export async function initBrowserApp(
|
|
|
331
332
|
// deferred values here exactly like the nav lane's processHandles
|
|
332
333
|
// does before applying.
|
|
333
334
|
const lateData = await resolveDeferredHandleValues(rawLateData);
|
|
334
|
-
|
|
335
|
+
// Live store only while the document's page is on screen: a full
|
|
336
|
+
// replace with its `matched` would overwrite another page's handles.
|
|
337
|
+
const pageKeys = store.getPageHistoryKeys(initialHistoryKey);
|
|
338
|
+
const onScreen = pageKeys.includes(store.getHistoryKey());
|
|
339
|
+
bootLog("handles: late push", {
|
|
335
340
|
segments: Object.keys(lateData),
|
|
341
|
+
onScreen,
|
|
336
342
|
});
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
343
|
+
if (onScreen) {
|
|
344
|
+
eventController.setHandleData(
|
|
345
|
+
lateData,
|
|
346
|
+
initialPayload.metadata?.matched,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
for (const key of pageKeys) {
|
|
350
|
+
store.updateCacheHandleData(key, lateData);
|
|
351
|
+
}
|
|
342
352
|
}
|
|
343
353
|
} catch (err) {
|
|
344
354
|
console.error("[rsc-router] Error consuming late handles:", err);
|
|
@@ -641,28 +651,7 @@ export async function initBrowserApp(
|
|
|
641
651
|
linkInterceptionEnabled: linkInterception,
|
|
642
652
|
version,
|
|
643
653
|
appShellRef,
|
|
644
|
-
documentRevealed:
|
|
645
|
-
// Parsing ends with the stream, but React reveals a streamed boundary
|
|
646
|
-
// in batches up to 300 ms later (Fizz's $RC marks it `$~` and reveals
|
|
647
|
-
// it from a timer), so the boundary is still pending at
|
|
648
|
-
// DOMContentLoaded. A transition then would make React client-render
|
|
649
|
-
// it over its server HTML.
|
|
650
|
-
const start = (): void => {
|
|
651
|
-
// A reveal rewrites the same comment's data, or React removes the
|
|
652
|
-
// node; a boundary nested in revealed content joins the document
|
|
653
|
-
// only then, so resolve only once a full walk finds none.
|
|
654
|
-
let pending = unrevealedBoundaries();
|
|
655
|
-
const check = (): void => {
|
|
656
|
-
pending = pending.filter(isUnrevealed);
|
|
657
|
-
if (pending.length === 0) pending = unrevealedBoundaries();
|
|
658
|
-
if (pending.length > 0) requestAnimationFrame(check);
|
|
659
|
-
else resolve();
|
|
660
|
-
};
|
|
661
|
-
check();
|
|
662
|
-
};
|
|
663
|
-
if (document.readyState !== "loading") start();
|
|
664
|
-
else document.addEventListener("DOMContentLoaded", start, { once: true });
|
|
665
|
-
}),
|
|
654
|
+
documentRevealed: awaitDocumentRevealed(),
|
|
666
655
|
};
|
|
667
656
|
browserAppContext = context;
|
|
668
657
|
|
|
@@ -670,23 +659,6 @@ export async function initBrowserApp(
|
|
|
670
659
|
return context;
|
|
671
660
|
}
|
|
672
661
|
|
|
673
|
-
/**
|
|
674
|
-
* A Suspense boundary of the document React has not revealed yet: Fizz marks
|
|
675
|
-
* one `$?` while its content streams and `$~` once its reveal is queued.
|
|
676
|
-
*/
|
|
677
|
-
function isUnrevealed(node: Comment): boolean {
|
|
678
|
-
return node.isConnected && (node.data === "$?" || node.data === "$~");
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
function unrevealedBoundaries(): Comment[] {
|
|
682
|
-
const found: Comment[] = [];
|
|
683
|
-
const walker = document.createTreeWalker(document, NodeFilter.SHOW_COMMENT);
|
|
684
|
-
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
685
|
-
if (isUnrevealed(node as Comment)) found.push(node as Comment);
|
|
686
|
-
}
|
|
687
|
-
return found;
|
|
688
|
-
}
|
|
689
|
-
|
|
690
662
|
// Once-flag so the hydration-commit boot log fires a single time (StrictMode
|
|
691
663
|
// re-runs the root effect; the second flush is not a second hydration).
|
|
692
664
|
let hydrationCommitLogged = false;
|
|
@@ -26,8 +26,8 @@ import {
|
|
|
26
26
|
startBrowserTransaction,
|
|
27
27
|
} from "./logging.js";
|
|
28
28
|
import {
|
|
29
|
+
validatePayloadRedirect,
|
|
29
30
|
validateRedirectOrigin,
|
|
30
|
-
validateExternalRedirect,
|
|
31
31
|
} from "./validate-redirect-origin.js";
|
|
32
32
|
import {
|
|
33
33
|
extractRscHeaderUrl,
|
|
@@ -40,13 +40,11 @@ import { mergeLocationState } from "./history-state.js";
|
|
|
40
40
|
import { stampLocationState } from "./react/location-state-shared.js";
|
|
41
41
|
import { classifyActionOutcome } from "./action-coordinator.js";
|
|
42
42
|
import { getAppVersion } from "./app-version.js";
|
|
43
|
-
import { collectClientRevalidationDecisions } from "../client-urls/navigation.js";
|
|
44
43
|
import {
|
|
45
44
|
decideCommitGatedOff,
|
|
46
45
|
findActionFormData,
|
|
47
46
|
type TransitionWhenActionInput,
|
|
48
47
|
} from "./transition-when.js";
|
|
49
|
-
import { CLIENT_REVALIDATION_HEADER } from "../client-urls/revalidation-protocol.js";
|
|
50
48
|
import { auditTreeCause } from "../suspense-audit.js";
|
|
51
49
|
|
|
52
50
|
// Polyfill Symbol.dispose/asyncDispose for Safari and older browsers
|
|
@@ -335,39 +333,12 @@ export function createServerActionBridge(
|
|
|
335
333
|
const onHandleAbort = () => fetchAbort.abort();
|
|
336
334
|
handle.signal.addEventListener("abort", onHandleAbort, { once: true });
|
|
337
335
|
|
|
338
|
-
// Client-run per-loader revalidation for the action follow-up render:
|
|
339
|
-
// run the held clientUrls route's revalidate() predicates with the
|
|
340
|
-
// action context and ship their decisions. Fails soft to null (locked
|
|
341
|
-
// server defaults).
|
|
342
|
-
let clientRevalidation: string | null = null;
|
|
343
|
-
try {
|
|
344
|
-
const actionPageUrl = new URL(
|
|
345
|
-
segmentState.currentUrl,
|
|
346
|
-
window.location.origin,
|
|
347
|
-
);
|
|
348
|
-
clientRevalidation = collectClientRevalidationDecisions({
|
|
349
|
-
currentUrl: actionPageUrl,
|
|
350
|
-
nextUrl: actionPageUrl,
|
|
351
|
-
// Decisions ride the action POST itself — the server evaluates it
|
|
352
|
-
// with actionContext, so the locked default is the action default.
|
|
353
|
-
actionRequest: true,
|
|
354
|
-
isAction: true,
|
|
355
|
-
actionId: id,
|
|
356
|
-
stale: false,
|
|
357
|
-
});
|
|
358
|
-
} catch {
|
|
359
|
-
clientRevalidation = null;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
336
|
// Send action request with stream tracking
|
|
363
337
|
const responsePromise = fetch(url, {
|
|
364
338
|
method: "POST",
|
|
365
339
|
headers: {
|
|
366
340
|
"rsc-action": id,
|
|
367
341
|
"X-RSC-Router-Client-Path": segmentState.currentUrl,
|
|
368
|
-
...(clientRevalidation && {
|
|
369
|
-
[CLIENT_REVALIDATION_HEADER]: clientRevalidation,
|
|
370
|
-
}),
|
|
371
342
|
...(tx && { "X-RSC-Router-Request-Id": tx.requestId }),
|
|
372
343
|
...(interceptSourceUrl && {
|
|
373
344
|
"X-RSC-Router-Intercept-Source": interceptSourceUrl,
|
|
@@ -518,41 +489,26 @@ export function createServerActionBridge(
|
|
|
518
489
|
// Check handle.signal.aborted to avoid redirecting from a stale action
|
|
519
490
|
// when the user has already navigated away.
|
|
520
491
|
if (metadata?.redirect && !handle.signal.aborted) {
|
|
521
|
-
//
|
|
522
|
-
//
|
|
523
|
-
//
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
492
|
+
// An explicit off-host redirect is a hard navigation, still
|
|
493
|
+
// scheme-validated: a forged payload carrying a javascript:/data: URL
|
|
494
|
+
// cannot script via location.assign (validatePayloadRedirect).
|
|
495
|
+
const { redirect } = metadata;
|
|
496
|
+
const target = validatePayloadRedirect(redirect);
|
|
497
|
+
handle.complete(returnValue?.data);
|
|
498
|
+
if (!target) {
|
|
499
|
+
log(
|
|
500
|
+
redirect.external
|
|
501
|
+
? "blocked external action redirect payload"
|
|
502
|
+
: "blocked action redirect payload",
|
|
503
|
+
{ url: redirect.url },
|
|
529
504
|
);
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
}
|
|
537
|
-
log("external action redirect", { url: externalUrl });
|
|
538
|
-
handle.complete(returnValue?.data);
|
|
539
|
-
window.location.assign(externalUrl);
|
|
540
|
-
return returnValue?.data;
|
|
505
|
+
} else if (redirect.external) {
|
|
506
|
+
log("external action redirect", { url: target });
|
|
507
|
+
window.location.assign(target);
|
|
508
|
+
} else {
|
|
509
|
+
log("action redirect", { url: target });
|
|
510
|
+
await dispatchRedirect(target, metadata.locationState);
|
|
541
511
|
}
|
|
542
|
-
const redirectUrl = validateRedirectOrigin(
|
|
543
|
-
metadata.redirect.url,
|
|
544
|
-
window.location.origin,
|
|
545
|
-
);
|
|
546
|
-
if (!redirectUrl) {
|
|
547
|
-
log("blocked action redirect payload", {
|
|
548
|
-
url: metadata.redirect.url,
|
|
549
|
-
});
|
|
550
|
-
handle.complete(returnValue?.data);
|
|
551
|
-
return returnValue?.data;
|
|
552
|
-
}
|
|
553
|
-
log("action redirect", { url: redirectUrl });
|
|
554
|
-
handle.complete(returnValue?.data);
|
|
555
|
-
await dispatchRedirect(redirectUrl, metadata.locationState);
|
|
556
512
|
return returnValue?.data;
|
|
557
513
|
}
|
|
558
514
|
|
|
@@ -950,6 +906,12 @@ export function createServerActionBridge(
|
|
|
950
906
|
|
|
951
907
|
handle.complete(returnData);
|
|
952
908
|
return returnData;
|
|
909
|
+
} catch (error) {
|
|
910
|
+
// What the call rejects with (a revalidation render that threw, a
|
|
911
|
+
// failed decode) is the action's error, for useAction as for the
|
|
912
|
+
// caller. A no-op for an aborted or already-settled action.
|
|
913
|
+
handle.fail(error);
|
|
914
|
+
throw error;
|
|
953
915
|
} finally {
|
|
954
916
|
// The single deferred invalidation + fence release for this action. Runs
|
|
955
917
|
// for every terminal that settles (normal, navigated-away, error, abort,
|