@rangojs/router 0.27.0 → 0.29.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 +1 -1
- package/dist/types/__internal.d.ts +7 -1
- package/dist/types/browser/action-fence.d.ts +2 -1
- package/dist/types/browser/event-controller.d.ts +8 -2
- package/dist/types/browser/history-state.d.ts +3 -9
- package/dist/types/browser/navigation-store.d.ts +8 -6
- package/dist/types/browser/partial-update.d.ts +11 -0
- package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
- package/dist/types/browser/react/use-action.d.ts +7 -2
- package/dist/types/browser/react/use-handle.d.ts +19 -16
- package/dist/types/browser/react/use-link-status.d.ts +5 -2
- package/dist/types/browser/segment-reconciler.d.ts +32 -4
- package/dist/types/browser/types.d.ts +24 -5
- package/dist/types/browser/wire-protocol.d.ts +26 -0
- package/dist/types/cache/cache-key-utils.d.ts +11 -5
- package/dist/types/cache/cache-scope.d.ts +34 -22
- package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
- package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
- package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
- package/dist/types/cache/handle-snapshot.d.ts +9 -4
- package/dist/types/cache/segment-codec.d.ts +14 -0
- package/dist/types/cache/shell-snapshot.d.ts +1 -1
- package/dist/types/cache/taint.d.ts +5 -3
- package/dist/types/cache/types.d.ts +7 -3
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +8 -6
- package/dist/types/prefetch-markers.d.ts +5 -14
- package/dist/types/prerender/store.d.ts +31 -0
- package/dist/types/prerender/warm-request.d.ts +3 -3
- package/dist/types/prerender.d.ts +2 -2
- package/dist/types/route-name.d.ts +5 -0
- package/dist/types/router/error-handling.d.ts +11 -5
- package/dist/types/router/instrument.d.ts +4 -3
- package/dist/types/router/intercept-resolution.d.ts +25 -9
- package/dist/types/router/match-context.d.ts +6 -11
- package/dist/types/router/match-handlers.d.ts +2 -6
- package/dist/types/router/match-middleware/background-revalidation.d.ts +13 -22
- package/dist/types/router/match-middleware/cache-lookup.d.ts +34 -86
- package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
- package/dist/types/router/navigation-snapshot.d.ts +7 -0
- package/dist/types/router/prerender-match.d.ts +21 -6
- package/dist/types/router/preview-match.d.ts +2 -7
- package/dist/types/router/request-classification.d.ts +5 -0
- package/dist/types/router/router-context.d.ts +7 -46
- package/dist/types/router/router-interfaces.d.ts +8 -14
- package/dist/types/router/segment-resolution/helpers.d.ts +19 -3
- package/dist/types/router/segment-resolution/loader-cache.d.ts +3 -3
- package/dist/types/router/segment-resolution/loader-mask.d.ts +4 -4
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +20 -18
- package/dist/types/router/segment-resolution/resolution-pass.d.ts +95 -0
- package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
- package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +2 -0
- package/dist/types/router/segment-resolution.d.ts +1 -2
- package/dist/types/router/segment-wrappers.d.ts +11 -48
- package/dist/types/router/telemetry.d.ts +2 -0
- package/dist/types/router/types.d.ts +11 -1
- package/dist/types/rsc/full-payload.d.ts +2 -2
- package/dist/types/rsc/handler-context.d.ts +6 -6
- package/dist/types/rsc/json-route-result.d.ts +5 -9
- package/dist/types/rsc/request-stages.d.ts +203 -0
- package/dist/types/rsc/response-cache-serve.d.ts +7 -12
- package/dist/types/rsc/response-route-handler.d.ts +2 -2
- package/dist/types/rsc/routine-plan.d.ts +1 -1
- package/dist/types/rsc/server-action.d.ts +2 -2
- package/dist/types/rsc/shell-capture-constants.d.ts +1 -1
- package/dist/types/rsc/shell-capture.d.ts +132 -15
- package/dist/types/rsc/shell-serve.d.ts +37 -12
- package/dist/types/server/context.d.ts +9 -8
- package/dist/types/server/handle-store.d.ts +16 -0
- package/dist/types/server/render-pass.d.ts +72 -0
- package/dist/types/server/request-context.d.ts +53 -104
- package/dist/types/server/rewrite-trail.d.ts +2 -2
- package/dist/types/testing/dispatch.d.ts +35 -18
- package/dist/types/testing/render-route.d.ts +43 -8
- package/dist/types/testing/serve-shell-request.d.ts +8 -1
- package/dist/types/types/cache-types.d.ts +6 -3
- package/dist/types/types/error-types.d.ts +30 -3
- package/dist/types/types/handler-context.d.ts +9 -1
- package/dist/types/types/segments.d.ts +1 -1
- package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
- package/dist/types/vite/discovery/build-env.d.ts +22 -0
- package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
- package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
- package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
- package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
- package/dist/types/vite/discovery/state.d.ts +12 -0
- package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
- package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
- package/dist/types/vite/router-discovery.d.ts +8 -47
- package/dist/types/vite/temp-rsc-server.d.ts +39 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
- package/dist/types/wire-params.d.ts +39 -0
- package/dist/vite/index.js +3822 -3727
- package/package.json +1 -1
- package/skills/cache-guide/SKILL.md +8 -5
- package/skills/caching/SKILL.md +37 -18
- package/skills/client-urls/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +13 -1
- package/skills/handler-use/SKILL.md +1 -1
- package/skills/hooks/SKILL.md +1 -1
- package/skills/hooks/data.md +8 -1
- package/skills/hooks/handle-and-actions.md +33 -2
- package/skills/hooks/navigation.md +121 -15
- package/skills/hooks/state.md +6 -4
- package/skills/hooks/urls.md +16 -5
- package/skills/intercept/SKILL.md +23 -7
- package/skills/middleware/SKILL.md +18 -0
- package/skills/observability/SKILL.md +30 -0
- package/skills/ppr/SKILL.md +18 -3
- package/skills/prerender/SKILL.md +39 -15
- package/skills/rango/SKILL.md +5 -1
- package/skills/server-actions/SKILL.md +15 -1
- package/skills/testing/client-components.md +25 -18
- package/skills/testing/response-routes.md +6 -4
- package/skills/testing/serve-shell-request.md +7 -7
- package/skills/use-cache/SKILL.md +46 -8
- package/skills/view-transitions/SKILL.md +1 -1
- package/src/__internal.ts +7 -1
- package/src/browser/action-fence.ts +2 -1
- package/src/browser/connection-warmup.ts +3 -1
- package/src/browser/event-controller.ts +37 -13
- package/src/browser/history-state.ts +6 -12
- package/src/browser/navigation-bridge.ts +59 -26
- package/src/browser/navigation-client.ts +20 -25
- package/src/browser/navigation-store.ts +25 -27
- package/src/browser/navigation-transaction.ts +4 -1
- package/src/browser/network-error-handler.ts +4 -0
- package/src/browser/partial-update.ts +33 -6
- package/src/browser/prefetch/cache.ts +2 -1
- package/src/browser/prefetch/fetch.ts +8 -16
- package/src/browser/prefetch/queue.ts +4 -4
- package/src/browser/react/NavigationProvider.tsx +2 -1
- package/src/browser/react/filter-segment-order.ts +2 -2
- package/src/browser/react/use-action.ts +24 -48
- package/src/browser/react/use-handle.ts +22 -18
- package/src/browser/react/use-link-status.ts +48 -60
- package/src/browser/react/use-navigation.ts +92 -41
- package/src/browser/rsc-router.tsx +1 -0
- package/src/browser/segment-reconciler.ts +109 -53
- package/src/browser/server-action-bridge.ts +81 -61
- package/src/browser/types.ts +25 -5
- package/src/browser/wire-protocol.ts +43 -0
- package/src/cache/cache-key-utils.ts +11 -5
- package/src/cache/cache-runtime.ts +132 -75
- package/src/cache/cache-scope.ts +110 -64
- package/src/cache/cf/cf-cache-constants.ts +2 -2
- package/src/cache/cf/cf-cache-store.ts +486 -874
- package/src/cache/cf/cf-kv-utils.ts +1 -1
- package/src/cache/document-cache.ts +64 -46
- package/src/cache/handle-snapshot.ts +19 -7
- package/src/cache/read-through-swr.ts +20 -18
- package/src/cache/segment-codec.ts +55 -0
- package/src/cache/shell-snapshot.ts +2 -2
- package/src/cache/tag-invalidation.ts +6 -2
- package/src/cache/taint.ts +5 -3
- package/src/cache/types.ts +7 -3
- package/src/cache/vercel/vercel-cache-store.ts +243 -310
- package/src/handles/pushed-by.ts +1 -1
- package/src/prefetch-markers.ts +5 -16
- package/src/prerender/store.ts +76 -0
- package/src/prerender/warm-request.ts +8 -4
- package/src/prerender.ts +2 -2
- package/src/route-name.ts +8 -0
- package/src/router/error-handling.ts +22 -10
- package/src/router/instrument.ts +4 -3
- package/src/router/intercept-resolution.ts +82 -75
- package/src/router/loader-resolution.ts +5 -4
- package/src/router/match-api.ts +5 -5
- package/src/router/match-context.ts +23 -12
- package/src/router/match-handlers.ts +8 -20
- package/src/router/match-middleware/background-revalidation.ts +46 -57
- package/src/router/match-middleware/cache-lookup.ts +677 -726
- package/src/router/match-middleware/cache-store.ts +4 -8
- package/src/router/match-middleware/intercept-resolution.ts +3 -23
- package/src/router/match-middleware/segment-resolution.ts +34 -61
- package/src/router/middleware.ts +69 -11
- package/src/router/navigation-snapshot.ts +14 -4
- package/src/router/prerender-match.ts +100 -44
- package/src/router/preview-match.ts +6 -41
- package/src/router/request-classification.ts +13 -7
- package/src/router/route-snapshot.ts +2 -5
- package/src/router/router-context.ts +10 -95
- package/src/router/router-interfaces.ts +8 -14
- package/src/router/segment-resolution/helpers.ts +49 -13
- package/src/router/segment-resolution/loader-cache.ts +15 -9
- package/src/router/segment-resolution/loader-mask.ts +5 -5
- package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
- package/src/router/segment-resolution/resolution-pass.ts +139 -0
- package/src/router/segment-resolution/revalidation.ts +939 -789
- package/src/router/segment-resolution/streamed-handler-boundary.ts +3 -0
- package/src/router/segment-resolution.ts +4 -13
- package/src/router/segment-wrappers.ts +51 -269
- package/src/router/telemetry.ts +5 -0
- package/src/router/types.ts +11 -1
- package/src/router.ts +19 -17
- package/src/rsc/full-payload.ts +6 -5
- package/src/rsc/handler-context.ts +8 -14
- package/src/rsc/handler.ts +126 -507
- package/src/rsc/json-route-result.ts +5 -9
- package/src/rsc/loader-fetch.ts +6 -5
- package/src/rsc/progressive-enhancement.ts +5 -0
- package/src/rsc/request-stages.ts +759 -0
- package/src/rsc/response-cache-serve.ts +10 -14
- package/src/rsc/response-route-handler.ts +13 -5
- package/src/rsc/rewrite-passes.ts +2 -1
- package/src/rsc/routine-plan.ts +1 -1
- package/src/rsc/rsc-rendering.ts +164 -349
- package/src/rsc/server-action.ts +87 -113
- package/src/rsc/shell-capture-constants.ts +1 -1
- package/src/rsc/shell-capture.ts +275 -227
- package/src/rsc/shell-serve.ts +121 -18
- package/src/rsc/ssr-setup.ts +5 -4
- package/src/segment-system.tsx +3 -3
- package/src/server/context.ts +22 -25
- package/src/server/handle-store.ts +55 -0
- package/src/server/render-pass.ts +291 -0
- package/src/server/request-context.ts +75 -115
- package/src/server/rewrite-trail.ts +6 -4
- package/src/ssr/index.tsx +1 -1
- package/src/testing/dispatch.ts +226 -710
- package/src/testing/e2e/page-helpers.ts +2 -2
- package/src/testing/render-route.tsx +199 -13
- package/src/testing/serve-shell-request.ts +36 -40
- package/src/types/cache-types.ts +6 -3
- package/src/types/error-types.ts +30 -3
- package/src/types/handler-context.ts +12 -1
- package/src/types/segments.ts +1 -1
- package/src/vite/discovery/build-discovery-phase.ts +204 -0
- package/src/vite/discovery/build-env.ts +148 -0
- package/src/vite/discovery/bundle-scan-phase.ts +128 -0
- package/src/vite/discovery/dev-prerender-cache.ts +8 -3
- package/src/vite/discovery/dev-server-phase.ts +1776 -0
- package/src/vite/discovery/discover-routers.ts +1 -1
- package/src/vite/discovery/discovery-errors.ts +1 -1
- package/src/vite/discovery/prerender-collection.ts +24 -39
- package/src/vite/discovery/state.ts +15 -0
- package/src/vite/plugins/version-plugin.ts +1 -1
- package/src/vite/plugins/virtual-entries.ts +1 -1
- package/src/vite/router-discovery.ts +29 -2428
- package/src/vite/temp-rsc-server.ts +223 -0
- package/src/vite/utils/prerender-utils.ts +47 -0
- package/src/wire-params.ts +40 -0
- package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
- package/src/router/segment-resolution/fresh.ts +0 -945
|
@@ -277,6 +277,36 @@ Events include `request.start/end/error`, `loader.start/end/error`,
|
|
|
277
277
|
`handler.error`, `cache.decision`, `revalidation.decision`, `request.timeout`,
|
|
278
278
|
and `request.origin-rejected`.
|
|
279
279
|
|
|
280
|
+
## Errors reported to onError
|
|
281
|
+
|
|
282
|
+
`createRouter({ onError })` sees every server error, including errors from
|
|
283
|
+
work no visitor waits on. A stale `cache()` record, document cache entry, PPR
|
|
284
|
+
shell, `"use cache"` function or cached loader is served stale and refreshed
|
|
285
|
+
beside the request. That refresh's error carries `background: true`, with the
|
|
286
|
+
starting request's `request` and `url`, and `handledByBoundary: false`: the
|
|
287
|
+
failed render is not stored, so no one sees its fallback. The stale entry
|
|
288
|
+
keeps serving.
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
const router = createRouter({
|
|
292
|
+
onError: ({ error, phase, background, routeKey, duration }) => {
|
|
293
|
+
errorTracker.capture(error, {
|
|
294
|
+
// A failed refresh is not a failed page view.
|
|
295
|
+
level: background ? "warning" : "error",
|
|
296
|
+
tags: { phase, route: routeKey },
|
|
297
|
+
extra: { duration },
|
|
298
|
+
});
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
`duration` is the time from the request's start to the error; for a
|
|
304
|
+
`background` error, from the start of the request that started the refresh.
|
|
305
|
+
`segmentId` and `segmentType` name the segment that threw: an intercept's or
|
|
306
|
+
a parallel slot's error reports the slot (`segmentType: "parallel"`), an
|
|
307
|
+
orphan layout's error the orphan layout, even when the owning layout's
|
|
308
|
+
boundary renders the fallback.
|
|
309
|
+
|
|
280
310
|
## Debugging revalidation and stale data
|
|
281
311
|
|
|
282
312
|
When stale UI or unexpected partial renders are the question, use all three
|
package/skills/ppr/SKILL.md
CHANGED
|
@@ -450,7 +450,7 @@ non-claiming read option.
|
|
|
450
450
|
|
|
451
451
|
The first cold partial request reports `BYPASS; reason=no-entry` because no
|
|
452
452
|
artifact could supply that response; its successful render schedules
|
|
453
|
-
`
|
|
453
|
+
`planShellCapture` with a navigation-only marker. The next request reports
|
|
454
454
|
`HIT` when the capture produced an eligible snapshot. Navigation snapshots use
|
|
455
455
|
a separate shell key, so they cannot overwrite or be served as a document shell.
|
|
456
456
|
The capture runs with the stripped target document URL and loads SSR support in
|
|
@@ -509,7 +509,9 @@ In handlers it is
|
|
|
509
509
|
too late to prevent a MISS render from already happening, but it still prevents
|
|
510
510
|
the follow-up shell capture. A `ctx.dynamic()` call that only happens during the
|
|
511
511
|
capture itself (for example inside a handler promise, after an await) refuses
|
|
512
|
-
that capture: a dynamic render has no shell.
|
|
512
|
+
that capture: a dynamic render has no shell. A cached handler that calls it in
|
|
513
|
+
the background refresh of a stale `cache()` record does not: the capture
|
|
514
|
+
replayed the record, and the refresh renders the record only.
|
|
513
515
|
|
|
514
516
|
During `Prerender` + `ppr` build-shell capture, middleware is replayed with
|
|
515
517
|
`ctx.build === true`, `ctx.waitUntil()` inert, and the same `ctx.dynamic()`
|
|
@@ -1177,7 +1179,7 @@ their data should bake vs stay live. Your levers, in order of preference:
|
|
|
1177
1179
|
3. **Guaranteed-fresh widgets: live loaders with a widget-sized boundary.**
|
|
1178
1180
|
An unflagged loader read under an inline `<Suspense>` in the widget is
|
|
1179
1181
|
enough. Alternatively, a parallel slot with its OWN `loading()`:
|
|
1180
|
-
parallel-owned loaders get their own per-slot boundary (`
|
|
1182
|
+
parallel-owned loaders get their own per-slot boundary (`resolveParallelSegments` tags
|
|
1181
1183
|
them with the slot's loading; `segment-system.tsx` builds a per-slot
|
|
1182
1184
|
LoaderBoundary), so the chrome bakes into the shell and each widget is an
|
|
1183
1185
|
independent, widget-sized hole:
|
|
@@ -1633,6 +1635,19 @@ evicted by tag at all — move always-fresh data into a live-lane loader (no
|
|
|
1633
1635
|
capture is served as before. Only a throw during the capture counts: a
|
|
1634
1636
|
component that failed in the live render and succeeds when captured is
|
|
1635
1637
|
stored.
|
|
1638
|
+
- **A handler that throws during capture refuses it too**: when a route,
|
|
1639
|
+
layout or parallel slot handler throws (or throws `notFound()`) on the
|
|
1640
|
+
capture's own run while the request that scheduled it rendered fine, for
|
|
1641
|
+
example on a flaky upstream, its boundary (a declared `errorBoundary()` or
|
|
1642
|
+
`notFoundBoundary()`, or the default error page) would become the shell.
|
|
1643
|
+
The capture stores nothing, logs `[ShellCache] capture`, reports to
|
|
1644
|
+
`onError` as `cache-write`, and backs the key off; the next capture after
|
|
1645
|
+
the window stores normally. A request whose own render fails answers
|
|
1646
|
+
500/404 and schedules no capture, but a recapture has no visitor render
|
|
1647
|
+
behind it: a stale shell whose recapture fails keeps serving until its
|
|
1648
|
+
`ttl` + `swr` ends (a build-time shell, until a recapture stores), as a
|
|
1649
|
+
failed `cache()` refresh does, logging and calling `onError` once per
|
|
1650
|
+
backoff window. When you delete a page, invalidate its tag.
|
|
1636
1651
|
|
|
1637
1652
|
## Related
|
|
1638
1653
|
|
|
@@ -309,16 +309,16 @@ layout(DocsNav, () => [
|
|
|
309
309
|
|
|
310
310
|
## Interaction with DSL Items
|
|
311
311
|
|
|
312
|
-
| DSL item | Behavior with Prerender
|
|
313
|
-
| -------------- |
|
|
314
|
-
| `loader()` | Live at runtime, bundled normally. Use `cache()` for caching.
|
|
315
|
-
| `revalidate()` | Without Passthrough: no
|
|
316
|
-
| `cache()` | Orthogonal -- prerendered segments bypass the runtime cache. Use `cache()` on parent layouts and loaders.
|
|
317
|
-
| `layout()` | Child layouts inside path are pre-rendered. Parent layouts are live.
|
|
318
|
-
| `parallel()` | Parallel slots inside path are pre-rendered.
|
|
319
|
-
| `middleware()` | Skipped while collecting build-time Flight payloads (no request). For `Prerender` + `ppr`, the build-shell step replays global and route middleware with `ctx.build === true`; `ctx.dynamic()` skips that shell. At request time middleware runs normally on every request.
|
|
320
|
-
| `loading()` | Ignored without Passthrough. Works for live fallback with Passthrough.
|
|
321
|
-
| `intercept()` | Pre-rendered at build time. Intercept variant stored under `/i` key alongside main segments. At runtime, the correct variant is served based on `ctx.isIntercept`. `when` config conditions are skipped at build time (all intercepts are pre-rendered unconditionally).
|
|
312
|
+
| DSL item | Behavior with Prerender |
|
|
313
|
+
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
314
|
+
| `loader()` | Live at runtime, bundled normally. Use `cache()` for caching. |
|
|
315
|
+
| `revalidate()` | Without Passthrough: re-runs no prerendered handler (segments are re-served from the build artifact); when the params change, a held parent layout whose `revalidate()` asks for it gets its baked copy. Loader revalidation still works. With Passthrough: re-renders through the live handler. |
|
|
316
|
+
| `cache()` | Orthogonal -- prerendered segments bypass the runtime cache. Use `cache()` on parent layouts and loaders. |
|
|
317
|
+
| `layout()` | Child layouts inside path are pre-rendered. Parent layouts are pre-rendered too: every hit, intercept variant included, serves them from the build artifact and runs no layout handler. On a navigation a parent layout the client holds stays held, even when the params change, as on a live route. |
|
|
318
|
+
| `parallel()` | Parallel slots inside path are pre-rendered. A parent layout's slots are pre-rendered too: a hit serves them from the build artifact with the layout, and one the client holds stays held, as on a live route. |
|
|
319
|
+
| `middleware()` | Skipped while collecting build-time Flight payloads (no request). For `Prerender` + `ppr`, the build-shell step replays global and route middleware with `ctx.build === true`; `ctx.dynamic()` skips that shell. At request time middleware runs normally on every request. |
|
|
320
|
+
| `loading()` | Ignored without Passthrough. Works for live fallback with Passthrough. |
|
|
321
|
+
| `intercept()` | Pre-rendered at build time. Intercept variant stored under `/i` key alongside the main entry, without the route's own segments. At runtime, the correct variant is served based on `ctx.isIntercept`. `when` config conditions are skipped at build time (all intercepts are pre-rendered unconditionally); a navigation serves only the intercept it selected. |
|
|
322
322
|
|
|
323
323
|
When Passthrough revalidation is enabled, remember that revalidation is
|
|
324
324
|
still partial: opting a child segment into revalidation does not
|
|
@@ -1426,19 +1426,24 @@ export const urlpatterns = urls(({ path, include }) => [
|
|
|
1426
1426
|
## Interaction with intercept()
|
|
1427
1427
|
|
|
1428
1428
|
When a pre-rendered route is also the target of an `intercept()`, the build system
|
|
1429
|
-
resolves the intercept handler at build time and stores
|
|
1430
|
-
|
|
1429
|
+
resolves the intercept handler at build time and stores an intercept variant
|
|
1430
|
+
under an `/i`-suffixed key alongside the main entry. The variant holds what an
|
|
1431
|
+
intercept navigation renders: the main segments without the route's own (its
|
|
1432
|
+
handler, layouts and slots, which a render under an intercept skips), then the
|
|
1433
|
+
intercept segments, with the handle values of exactly those segments:
|
|
1431
1434
|
|
|
1432
1435
|
```
|
|
1433
1436
|
prerender store keys:
|
|
1434
1437
|
"blog.post/a1b2c3" -> main segments (full page)
|
|
1435
|
-
"blog.post/a1b2c3/i" -> main segments + intercept segments
|
|
1438
|
+
"blog.post/a1b2c3/i" -> main segments without the route's own + intercept segments
|
|
1436
1439
|
```
|
|
1437
1440
|
|
|
1438
1441
|
At runtime, the cache-lookup middleware checks `ctx.isIntercept`:
|
|
1439
1442
|
|
|
1440
1443
|
- **Intercept navigation**: looks up `paramHash/i` first. If found, yields
|
|
1441
|
-
the
|
|
1444
|
+
the variant's segments with only the slot of the intercept the navigation
|
|
1445
|
+
selected (`selectInterceptVariant`); a variant without that slot is a miss.
|
|
1446
|
+
`handleCacheHitIntercept()` extracts intercept segments
|
|
1442
1447
|
(filtered by `namespace?.startsWith("intercept:")`) and sets up slots.
|
|
1443
1448
|
- **Direct navigation**: looks up `paramHash` (no suffix). Standard prerender path.
|
|
1444
1449
|
- **Intercept miss (no `/i` entry)**: falls through to the normal pipeline so
|
|
@@ -1447,7 +1452,26 @@ At runtime, the cache-lookup middleware checks `ctx.isIntercept`:
|
|
|
1447
1452
|
|
|
1448
1453
|
The `when` config selector receives an `InterceptSelectorContext` whose `from`
|
|
1449
1454
|
location (the page being left) is unknown at build time. All intercepts are pre-rendered unconditionally;
|
|
1450
|
-
`when` is evaluated at runtime by the intercept-resolution middleware
|
|
1455
|
+
`when` is evaluated at runtime by the intercept-resolution middleware, and a
|
|
1456
|
+
navigation shows only the intercept it selected: the innermost whose `when`
|
|
1457
|
+
passes, as on a route that is not pre-rendered. Two intercepts on one layout
|
|
1458
|
+
that fill the same slot are not pre-rendered: the variant could not tell them
|
|
1459
|
+
apart, so they render live. Intercepts declared in the route's own layouts are
|
|
1460
|
+
never selected and are not pre-rendered.
|
|
1461
|
+
|
|
1462
|
+
The build warns once per route and slot for an intercept a navigation can
|
|
1463
|
+
select that is not pre-rendered: two intercepts that fill one slot, a handler
|
|
1464
|
+
or layout that returned a `Response` at build time, or an intercept that
|
|
1465
|
+
targets the route by its name inside an `include()` (`"item"` for the route
|
|
1466
|
+
`"shop.item"`, or for a route named `"item"` in an unnamed `include()`), which
|
|
1467
|
+
only the local-name fallback selects (name the `include()` and target the
|
|
1468
|
+
full name, or declare the intercept inside it with `".item"`, to pre-render
|
|
1469
|
+
it). That intercept renders live, and its handler runs per request without
|
|
1470
|
+
`BuildContext`; a route in an unnamed `include()` is named by its URL:
|
|
1471
|
+
|
|
1472
|
+
```
|
|
1473
|
+
[rango] Prerender route "item": intercept @modal is not pre-rendered: two intercepts on one layout fill @modal. A navigation that selects it renders it live, and its handler runs per request without BuildContext. To pre-render it, give each intercept its own slot.
|
|
1474
|
+
```
|
|
1451
1475
|
|
|
1452
1476
|
### Example: Pre-rendered route with intercept
|
|
1453
1477
|
|
package/skills/rango/SKILL.md
CHANGED
|
@@ -249,7 +249,11 @@ identity".
|
|
|
249
249
|
The predicate arg carries the action's full context, not just its identity. Match
|
|
250
250
|
_which_ action with `ctx.isAction(addToCart)` (rename-safe); branch on _what it
|
|
251
251
|
returned_ with `ctx.actionResult` — the value your `"use server"` function
|
|
252
|
-
returned, for outcome-conditional revalidation.
|
|
252
|
+
returned, for outcome-conditional revalidation. When the action threw into an
|
|
253
|
+
`errorBoundary()`, a predicate above the boundary still runs, with
|
|
254
|
+
`actionResult` set to the thrown value: check a field's shape before reading
|
|
255
|
+
it (`Array.isArray(actionResult?.items)`). A `redirect()` it throws then redirects, as
|
|
256
|
+
after a success. The arg also exposes `actionId`
|
|
253
257
|
(raw `path#export`), `actionUrl`, `formData` (form-based actions only), `method`,
|
|
254
258
|
and `stale` (the `_rsc_stale` signal that an action ran in this or another tab;
|
|
255
259
|
that Back/Forward revalidation takes an action's defaults, but `isAction()` is
|
|
@@ -569,6 +569,10 @@ A few details:
|
|
|
569
569
|
until a request re-runs them.
|
|
570
570
|
- It holds no segments, so every loader on the page re-runs whatever its
|
|
571
571
|
`revalidate()` says, and the document cache never answers it.
|
|
572
|
+
- If a later action on that page threw into its `errorBoundary()`, the
|
|
573
|
+
revalidation keeps the boundary on screen: everything above it re-runs,
|
|
574
|
+
nothing at or below it. Under the router's default boundary at the root,
|
|
575
|
+
nothing runs. A link from the page re-runs what is at or below the boundary.
|
|
572
576
|
- Going Back or Forward out of an intercept does not abort an action sent from
|
|
573
577
|
the intercept. `useAction()` and your `await` get its real result.
|
|
574
578
|
|
|
@@ -704,7 +708,17 @@ is usually too aggressive.
|
|
|
704
708
|
Throw for genuinely exceptional conditions (network failure, DB outage,
|
|
705
709
|
auth violation). The nearest `errorBoundary()` above the page the action was
|
|
706
710
|
called from catches them, and the boundary renders inside route middleware like
|
|
707
|
-
a normal render.
|
|
711
|
+
a normal render. Above the boundary the response is the action's revalidation,
|
|
712
|
+
as after a success: the layouts' loaders re-run as their `revalidate()`
|
|
713
|
+
decides, so a cart count shows what the action wrote before it threw. A
|
|
714
|
+
route's `errorBoundary()` replaces the route: its handler and loaders do not
|
|
715
|
+
run. A layout's `errorBoundary()` replaces what the layout renders in its
|
|
716
|
+
outlet: the layout's own loaders, slots and orphan layouts revalidate as on a
|
|
717
|
+
success, and nothing inside the outlet runs. A link from the page re-runs
|
|
718
|
+
what the boundary replaced and everything under it; under the router's
|
|
719
|
+
default boundary at the root, the whole page. Back to the page later
|
|
720
|
+
restores it and revalidates it, which replaces the boundary with the route. An action sent from an intercept
|
|
721
|
+
gets the boundary alone.
|
|
708
722
|
|
|
709
723
|
```typescript
|
|
710
724
|
// urls.tsx — inside the urls() callback
|
|
@@ -66,9 +66,13 @@ type RenderRouteResult = RenderResult & {
|
|
|
66
66
|
url: string,
|
|
67
67
|
options?: {
|
|
68
68
|
loaders?: ReadonlyArray<readonly [LoaderDefinition<any>, unknown]>;
|
|
69
|
+
handles?: ReadonlyArray<readonly [Handle<any, any>, unknown[]]>;
|
|
69
70
|
state?: HistoryState;
|
|
70
71
|
replace?: boolean;
|
|
71
72
|
transition?: boolean;
|
|
73
|
+
// the response's stream: opens a task after the call, before the
|
|
74
|
+
// commit, and stays open until the promise settles (isStreaming)
|
|
75
|
+
streamUntil?: Promise<unknown>;
|
|
72
76
|
},
|
|
73
77
|
): Promise<void>;
|
|
74
78
|
// re-render the current location, as router.refresh() does:
|
|
@@ -203,13 +207,14 @@ it("lists no product twice while the next page loads", async () => {
|
|
|
203
207
|
});
|
|
204
208
|
```
|
|
205
209
|
|
|
206
|
-
| To bring state to a reader
|
|
207
|
-
|
|
|
208
|
-
| a push / replace, the component's own
|
|
209
|
-
| a push / replace whose loader the test holds
|
|
210
|
-
| back/forward onto
|
|
211
|
-
|
|
|
212
|
-
| a
|
|
210
|
+
| To bring state to a reader | Do |
|
|
211
|
+
| ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
212
|
+
| a push / replace, the component's own | click its `<Link state>` or the control that calls `router.push(url, { state })`, then `waitFor` |
|
|
213
|
+
| a push / replace whose loader the test holds | `router.navigate(url, { state, replace?, loaders: [[Loader, pendingPromise]] })` on a `transition` spec |
|
|
214
|
+
| back/forward onto the mounted entry or one a navigation wrote | `window.history.back()` inside `await act(async () => { ... })`: the entry's page is restored with its state and handle values, as production's history-cache restore is |
|
|
215
|
+
| back/forward onto an entry the test wrote | `history.replaceState(entry, "")`, then `window.dispatchEvent(new Event("popstate"))` inside `act`: readers take the entry as history holds it (also how a `Def.write()` becomes visible) |
|
|
216
|
+
| a reader that mounts while a navigation is pending | hold the navigation as in the row above, then mount the reader inside `await act(async () => { ... })`: it reads the entry on screen |
|
|
217
|
+
| a server action's state, a redirect's, an intercept | e2e |
|
|
213
218
|
|
|
214
219
|
### Definitions with `clearOnReload`
|
|
215
220
|
|
|
@@ -390,17 +395,19 @@ it("dims the held price while the next product streams", async () => {
|
|
|
390
395
|
});
|
|
391
396
|
```
|
|
392
397
|
|
|
393
|
-
| Pending state | Under `renderRoute`
|
|
394
|
-
| ---------------------------------------------------------------------------------------------- |
|
|
395
|
-
| `useLoader().isLoading` on the reader a `transition` navigate() holds, pending seed | Modeled: `true` on the OLD data until the promise settles, then `false` with the new data in the same commit.
|
|
396
|
-
| `transition` navigate(), settled seed (a plain value) | `false`; the data swaps in the commit.
|
|
397
|
-
| navigate() without `transition`, pending seed | Urgent commit (production's cold-navigation lane): the read suspends to the nearest `<Suspense>` fallback (with none, React keeps the previous tree). No flag.
|
|
398
|
-
| `transition: { when }` returning false, or `navigate(url, { transition: false })` | Same as without `transition`: urgent, with production's decision (`kind: "push"`; `refresh()` is `"revalidate"`). `transition: false` calls no predicate.
|
|
399
|
-
| Production's other hold lanes (same-structure search/filter nav, fully-prefetched, optimistic) | NOT modeled — without `transition` navigate() stays urgent. Assert those at e2e.
|
|
400
|
-
| `useNavigation().state`, `useLinkStatus().pending` | Modeled: navigate() starts the navigation as a click does. `loading` from the call, `idle` in the commit, held with it; `pending` true for the destination's link until
|
|
401
|
-
| `useAction().state` | Stays `idle`: navigate() runs no action. e2e.
|
|
402
|
-
| `useHandle()` on a held navigation, `navigate(url, { handles, loaders })` | Modeled: `handles` arrive on the response's handle stream, so readers keep the page being left's values while React holds the page and take the new ones with its commit.
|
|
403
|
-
|
|
|
398
|
+
| Pending state | Under `renderRoute` |
|
|
399
|
+
| ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
400
|
+
| `useLoader().isLoading` on the reader a `transition` navigate() holds, pending seed | Modeled: `true` on the OLD data until the promise settles, then `false` with the new data in the same commit. |
|
|
401
|
+
| `transition` navigate(), settled seed (a plain value) | `false`; the data swaps in the commit. |
|
|
402
|
+
| navigate() without `transition`, pending seed | Urgent commit (production's cold-navigation lane): the read suspends to the nearest `<Suspense>` fallback (with none, React keeps the previous tree). No flag. |
|
|
403
|
+
| `transition: { when }` returning false, or `navigate(url, { transition: false })` | Same as without `transition`: urgent, with production's decision (`kind: "push"`; `refresh()` is `"revalidate"`). `transition: false` calls no predicate. |
|
|
404
|
+
| Production's other hold lanes (same-structure search/filter nav, fully-prefetched, optimistic) | NOT modeled — without `transition` navigate() stays urgent. Assert those at e2e. |
|
|
405
|
+
| `useNavigation().state`, `useLinkStatus().pending` | Modeled: navigate() starts the navigation as a click does. `loading` from the call, `idle` in the commit, held with it; `pending` true for the destination's link until that commit, held with it too. |
|
|
406
|
+
| `useAction().state` | Stays `idle`: navigate() runs no action. e2e. |
|
|
407
|
+
| `useHandle()` on a held navigation, `navigate(url, { handles, loaders })` | Modeled: `handles` arrive on the response's handle stream, so readers keep the page being left's values while React holds the page and take the new ones with its commit. |
|
|
408
|
+
| `useNavigation().isStreaming` past the commit, `navigate(url, { streamUntil })` | Modeled: the stream opens a task after the call (`pendingUrl` clears) and ends when the promise settles or a newer navigation aborts it; settle it inside `await act(async () => ...)`. |
|
|
409
|
+
| A navigation started while React holds a page (a newer click, a `transition: false` commit) | Call it inside `await act(async () => { ... })`: its first update makes React retry the held page, and a synchronous `act()` drops that retry, so a later urgent commit never renders. |
|
|
410
|
+
| A back/forward restored from the history cache (`pop`) | Modeled for the mounted entry and one a navigate() wrote: the page and its handle values come back, decided by `transition({ when })` as kind `"pop"`. A stale entry's revalidation and a restore that fetches: e2e. |
|
|
404
411
|
|
|
405
412
|
- Per-navigation seeds merge over the render-time `loaders`/`loaderData` for THAT navigation only; a later `navigate()` without `loaders` falls back to the render-time data.
|
|
406
413
|
- Pass settled data as a plain value, not `Promise.resolve(value)`: React has not observed a plain promise yet, so it counts as pending and renders one `stale` frame. (A Flight chunk that has already settled carries its status and is skipped, which a plain value mirrors.)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Layer:** integration (node) · **Import:** `@rangojs/router/testing` · **DSL it tests:** response routes (json/text/html/xml/md), redirects, 404 (see `/response-routes`, `/mime-routes`)
|
|
4
4
|
|
|
5
|
-
`dispatch` runs the
|
|
5
|
+
`dispatch` runs the production handler's REAL request stages — classify (matching + content negotiation), the terminal plans (reloads, the rewritten-action rule), and the real global + route-level middleware chain — with no RSC render, so redirects, 404s, reloads, response routes, content negotiation, and middleware short-circuits behave exactly as in production. You SEED the request and `env`; everything else (matching, middleware, header/cookie merge) is real machinery.
|
|
6
6
|
|
|
7
7
|
Matching goes through the route trie, as every request in `vite dev` and production does.
|
|
8
8
|
|
|
@@ -83,8 +83,9 @@ describe("api routes via dispatch", () => {
|
|
|
83
83
|
- trailing-slash redirects from the trie — a route with `trailingSlash: "never"` or `"always"` answers the other form with a 308 and the `Location` (query preserved). A route with no `trailingSlash` serves its URL with and without a trailing slash, with no redirect, as production does;
|
|
84
84
|
- the open-redirect guard on browser-followed redirects (a cross-origin `Location` is rewritten to the basename root unless `redirect(url, { external: true })`);
|
|
85
85
|
- content-negotiated routes (`Vary: Accept` appended);
|
|
86
|
+
- the reloads production answers inside the global middleware chain: a request whose `_rsc_v` names another deploy's document version (`setBuildVersions`, `./serve-shell-request.md`), and a `_rsc_partial` request whose `_rsc_rid` names another router (a cross-app navigation), get an `X-RSC-Reload` at the visitor's URL — on any route, a component route or an unmatched path included;
|
|
86
87
|
- cached response routes (`cache({ ... })` around a `path.json` etc.): hit, SWR, and tag writes. The cache write is scheduled via `waitUntil` (a microtask in tests), so flush microtasks between the seeding `dispatch` and the one that asserts the hit;
|
|
87
|
-
- `createRouter({ telemetry })` `request.start` / `request.end` / `request.error` events, and `createRouter({ onError })` for cache-read/write failures.
|
|
88
|
+
- `createRouter({ telemetry })` `request.start` / `request.end` / `request.error` events, all with one `requestId`, which a response route's `request.timeout` carries too (a rewritten pass's included), and `createRouter({ onError })` for cache-read/write failures.
|
|
88
89
|
|
|
89
90
|
## Caveats
|
|
90
91
|
|
|
@@ -94,9 +95,10 @@ describe("api routes via dispatch", () => {
|
|
|
94
95
|
- Setup: needs the preset (alias + virtual stubs) or a Vite-RSC env (see `./setup.md`); a bare router import throws on Vite virtuals.
|
|
95
96
|
- Building the trie runs route discovery: every `include()`d `urls()` handler runs once more, and every async include provider (`() => import(...)`) is called. Count provider calls or handler runs after the first `dispatch` on that router, not from router construction. A provider that rejects fails the `dispatch`, as it fails `vite dev` and the build.
|
|
96
97
|
- A router using `Prerender()`/`createLoader()`/`Static()` now constructs in a bare test (each assigns a runtime fallback `$$id`). Importing the whole router _file_ may still need the plugin (its page modules pull app deps / `virtual:` modules) — build from a focused include (your API routes) for whole-router dispatch.
|
|
97
|
-
- A `throw rewrite(target)` from global or route middleware re-dispatches as the router does: global middleware matched on the target's path, the target's route middleware and handler, the same request context, `x-rango-rewrite` on the response, 508 on a loop. The visitor's URL is matched before middleware runs, so it must be a response route (or a redirect or 404) too; a rewrite target that is a component route throws the same error as a direct request.
|
|
98
|
+
- A `throw rewrite(target)` from global or route middleware re-dispatches as the router does: global middleware matched on the target's path, the target's route middleware and handler, the same request context, `x-rango-rewrite` on the response, 508 on a loop. The visitor's URL is matched before middleware runs, so it must be a response route (or a redirect or 404) too; a rewrite target that is a component route throws the same error as a direct request. An action request (`?_rsc_action`) a rewrite moves to a response route, a redirect or an unmatched path rejects with production's error, `A rewrite of a server action or form post must target a page route; …`.
|
|
99
|
+
- The route's params and name are set inside the global middleware chain, as production sets them: a global middleware reads `ctx.routeName` (and `getRequestContext().params`) after `await next()`, not before it. A trailing-slash redirect sets none.
|
|
98
100
|
- A `_rsc_partial` request to a response route runs global middleware first (an auth gate can still 401/redirect), then returns `X-RSC-Reload` — route-level middleware is skipped, exactly like production.
|
|
99
|
-
- `dispatch` does NOT execute server actions (`?_rsc_action`), but it DOES run the global middleware chain on an action request — middleware can still 401/redirect it,
|
|
101
|
+
- `dispatch` does NOT execute server actions (`?_rsc_action`), but it DOES run the global middleware chain on an action request — middleware can still 401/redirect it. When a global middleware matches the path, any 3xx that leaves the chain on a partial OR action request (a global or route middleware's redirect, a response route's, a trailing-slash redirect) becomes a `204` + `X-RSC-Redirect` (fetch-safe interception), the raw `Location` dropped. Without one, an action request's 3xx goes out raw (through the open-redirect guard), as production sends it; a trailing-slash redirect on a partial request is a `204` either way. Location state carried on such a redirect is dropped too (production embeds it in a Flight payload); cover that at e2e.
|
|
100
102
|
- `createRouter({ onError })` is NOT invoked for a thrown non-Response HANDLER error here (the typed error Response still matches production), and `cache.decision` / `loader.*` telemetry never fires (no RSC render runs). Cover both at e2e.
|
|
101
103
|
|
|
102
104
|
## See also
|
|
@@ -20,13 +20,13 @@ A `Prerender` route is served as in production: from the artifact `router.matchF
|
|
|
20
20
|
|
|
21
21
|
### Options — `ServeShellRequestOptions`
|
|
22
22
|
|
|
23
|
-
| Field | Type
|
|
24
|
-
| ------------ |
|
|
25
|
-
| `cacheStore` | `SegmentCacheStore`
|
|
26
|
-
| `env` | `TEnv`
|
|
27
|
-
| `headers` | `HeadersInit`
|
|
28
|
-
| `partial` | `true \| { from?: string; rewriteTarget?: string; segments?: string[]; prefetch?: boolean; fill?: boolean; interceptSource?: string; stale?: boolean }` | Serve the navigation request the browser sends instead of a document GET: from `from` (default the site root) with `segments` mounted (default none). `prefetch: true` sends it as a `<Link>` prefetch (`X-Rango-Prefetch`), so `prefetch: false` work is deferred. `fill: true` sends the follow-up request (`_rsc_fill=1`) the browser makes after adopting a prefetch; pass the prefetch's delivered ids, without the deferred ones, as `segments`, and the same URL as `from`. `interceptSource` sends the intercept source (`X-RSC-Router-Intercept-Source`) the browser sends with the fill of an intercept, whose `from` is that source too. `rewriteTarget` sends what the page at `from` carried as its payload's `metadata.rewriteTarget` when middleware rewrote it (`_rsc_rt`), as the browser does, so the server diffs against the route that rendered that page. `stale: true` sends a stale request (`_rsc_stale`), as the browser sends it: every `revalidate()` sees `stale: true` (see `revalidate()`'s `stale`), segments take an action's revalidation defaults, and it renders live, as an action does: neither the route cache (`cache()`) nor the document cache serves or stores it, and a `ppr` route's `replayStatus` is `{ outcome: "BYPASS", reason: "stale" }`. |
|
|
29
|
-
| `action` | `{ ref: Function; id: string; args?: unknown[]; segments?: string[]; rewriteTarget?: string }`
|
|
23
|
+
| Field | Type | Meaning |
|
|
24
|
+
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
25
|
+
| `cacheStore` | `SegmentCacheStore` | The store for this request, in place of the store your `createRouter({ cache })` config returns. The rest of that config (`enabled`, `searchParams`) still applies. Omit it to use the router's own store. |
|
|
26
|
+
| `env` | `TEnv` | Bindings, as `router.fetch(request, { env })` passes them. A function `cache` config receives them. |
|
|
27
|
+
| `headers` | `HeadersInit` | Request headers (e.g. `cookie`). A document request defaults `accept` to `text/html`. |
|
|
28
|
+
| `partial` | `true \| { from?: string; rewriteTarget?: string; segments?: string[]; prefetch?: boolean; fill?: boolean; interceptSource?: string; stale?: boolean; boundary?: string }` | Serve the navigation request the browser sends instead of a document GET: from `from` (default the site root) with `segments` mounted (default none). `prefetch: true` sends it as a `<Link>` prefetch (`X-Rango-Prefetch`), so `prefetch: false` work is deferred. `fill: true` sends the follow-up request (`_rsc_fill=1`) the browser makes after adopting a prefetch; pass the prefetch's delivered ids, without the deferred ones, as `segments`, and the same URL as `from`. `interceptSource` sends the intercept source (`X-RSC-Router-Intercept-Source`) the browser sends with the fill of an intercept, whose `from` is that source too. `rewriteTarget` sends what the page at `from` carried as its payload's `metadata.rewriteTarget` when middleware rewrote it (`_rsc_rt`), as the browser does, so the server diffs against the route that rendered that page. `stale: true` sends a stale request (`_rsc_stale`), as the browser sends it: every `revalidate()` sees `stale: true` (see `revalidate()`'s `stale`), segments take an action's revalidation defaults, and it renders live, as an action does: neither the route cache (`cache()`) nor the document cache serves or stores it, and a `ppr` route's `replayStatus` is `{ outcome: "BYPASS", reason: "stale" }`. `boundary` names the error segment of a thrown action's boundary the page at `from` shows (`_rsc_boundary`), as the background revalidation an earlier action owes that page sends it, with `stale: true` and no `segments`: the response renders every segment above the boundary and nothing at or below it, claims the boundary in `matched` and sends no segment for it. The server reads it only beside `stale`. |
|
|
29
|
+
| `action` | `{ ref: Function; id: string; args?: unknown[]; segments?: string[]; rewriteTarget?: string }` | Send the request a JS server action call makes from the page at `url`: a POST of `args` (encoded as the browser encodes them) with `?_rsc_action`, the `rsc-action` header, `X-RSC-Router-Client-Path` and `_rsc_segments` (the page's mounted segment ids, default none), plus the page's `rewriteTarget` as for `partial`. Registers `ref` under `id` (`<module id>#<export name>`). Not with `partial` or `form`. |
|
|
30
30
|
|
|
31
31
|
### Returns — `ServeShellRequestResult`
|
|
32
32
|
|
|
@@ -68,10 +68,17 @@ export async function ProductCard({ id }: { id: string }) {
|
|
|
68
68
|
A result Flight cannot encode cleanly is not cached: an async child component
|
|
69
69
|
that throws, a promise in the result or in a handle push that rejects, a
|
|
70
70
|
function or a class instance. Storing it would bake the failure into every hit
|
|
71
|
-
until expiry. Instead the write is skipped and reported to `onError`
|
|
72
|
-
`"cache"`, category `cache-write`, or `stale-revalidation` for a
|
|
73
|
-
refresh):
|
|
74
|
-
|
|
71
|
+
until expiry. Instead the write is skipped, logged and reported to `onError`
|
|
72
|
+
(phase `"cache"`, category `cache-write`, or `stale-revalidation` for a
|
|
73
|
+
background refresh): the next call runs the function again, and a stale entry
|
|
74
|
+
keeps serving.
|
|
75
|
+
|
|
76
|
+
A miss renders a decoded copy of what it encodes, as a hit renders the stored
|
|
77
|
+
entry, so a child that throws or a promise that rejects throws where the copy
|
|
78
|
+
renders, as it would on a hit. A value Flight cannot encode at all (a
|
|
79
|
+
function, a class instance) in the data the function returns is the
|
|
80
|
+
exception: the call returns the live value, uncached. One inside a promise in
|
|
81
|
+
that data rejects that promise instead, as it would on a hit.
|
|
75
82
|
|
|
76
83
|
## Named Cache Profiles
|
|
77
84
|
|
|
@@ -233,6 +240,35 @@ export const ProductLoader = createLoader(async (ctx) => getProduct(ctx));
|
|
|
233
240
|
per-request values (cookies, headers) are not reflected in the cache key. Without
|
|
234
241
|
this guard, one user's data would be served to another.
|
|
235
242
|
|
|
243
|
+
That includes the server components the function returns. The value is
|
|
244
|
+
encoded once, inside the same guard, and the page renders the decoded copy, as
|
|
245
|
+
on a hit, so the guard refuses there every read and write it refuses in the
|
|
246
|
+
body, and a refused read fails the request as it would in the body: at the
|
|
247
|
+
value's root the call throws, and below the root that part renders as an
|
|
248
|
+
error, to its nearest error boundary. Nothing is stored. The same holds for a
|
|
249
|
+
`ctx.use(Loader)` in the returned tree (the loader runs inside the guard, and
|
|
250
|
+
the request's memo of it rejects, so a later read of it in the request throws
|
|
251
|
+
too) and for a `React.lazy` or a memoizing thenable created in the body.
|
|
252
|
+
|
|
253
|
+
A handle value the function pushes (`ctx.use(Handle)`) is encoded inside the
|
|
254
|
+
guard as well, and one that reads the request is not stored. One handle case
|
|
255
|
+
is not covered yet: a handle value that holds a lazy component or a memoized
|
|
256
|
+
promise. Don't push lazy state that reads the request from a cached function.
|
|
257
|
+
|
|
258
|
+
Two memo gaps remain, where the read happened before the guard could see it:
|
|
259
|
+
|
|
260
|
+
- In the body: a `React.cache`-memoized read of `cookies()` or `headers()` (a
|
|
261
|
+
`getSession = cache(() => cookies()...)` auth helper) that the page called
|
|
262
|
+
before the cached function runs returns the memo without reading again, so
|
|
263
|
+
the guard never sees it and the entry stores that visitor's value.
|
|
264
|
+
- In the body or the returned tree: a per-request memo of your own, keyed by
|
|
265
|
+
`getRequestContext()`, that the page filled and the cached function or a
|
|
266
|
+
component it returns reads. `React.cache` itself is safe in the returned
|
|
267
|
+
tree, which renders in its own Flight request.
|
|
268
|
+
|
|
269
|
+
Pass the value in as an argument instead of reading a memo inside the cached
|
|
270
|
+
function or the tree it returns.
|
|
271
|
+
|
|
236
272
|
The raw reads throw the same way: `ctx.request.headers` on any `ctx` passed in
|
|
237
273
|
(handler, loader, middleware; it is one `Request` object, also
|
|
238
274
|
`getRequestContext().request`), and `getRequestContext().cookie()` /
|
|
@@ -397,9 +433,11 @@ export const Page = Prerender(
|
|
|
397
433
|
|
|
398
434
|
## Performance: waitUntil
|
|
399
435
|
|
|
400
|
-
On cache miss, the function executes and
|
|
401
|
-
|
|
402
|
-
the
|
|
436
|
+
On cache miss, the function executes and its result is encoded; the call
|
|
437
|
+
returns once the decoded copy's root has arrived, so a server component at the
|
|
438
|
+
root of the value renders before the call resolves. The rest of the encode and
|
|
439
|
+
the cache **store write** (`setItem`) are deferred to `waitUntil` and do NOT
|
|
440
|
+
block the response.
|
|
403
441
|
|
|
404
442
|
On stale hit, stale data is returned immediately. Background revalidation
|
|
405
443
|
(re-execute + store write) runs entirely inside `waitUntil`.
|
|
@@ -407,7 +445,7 @@ On stale hit, stale data is returned immediately. Background revalidation
|
|
|
407
445
|
| Phase | Blocks response? |
|
|
408
446
|
| ------------------------------------ | ---------------- |
|
|
409
447
|
| Function execution (miss) | Yes |
|
|
410
|
-
| Result
|
|
448
|
+
| Result encode, to its root (miss) | Yes |
|
|
411
449
|
| Cache store write (miss) | No (waitUntil) |
|
|
412
450
|
| Stale value return (stale hit) | No (immediate) |
|
|
413
451
|
| Background revalidation (stale) | No (waitUntil) |
|
|
@@ -154,7 +154,7 @@ Every commit the router runs in a transition adds its `transition({ when })` kin
|
|
|
154
154
|
- Tagging alone animates nothing: React starts a view transition only where a `<ViewTransition>` resolves a class other than `"none"` for the commit's types.
|
|
155
155
|
- A commit outside a transition carries no type: a cold click that mounts new segments, a gated-off commit (`when` returned `false`), and a back/forward restored from the history cache on a route without `transition()` (it commits inside the `popstate` event). Under `transition()` that restore carries `"pop"` and `"navigation-back"`.
|
|
156
156
|
- Under `transition()`, a stale entry's revalidation carries `"revalidate"` and `"action"`. An `action`-keyed map that must skip it also keys `revalidate: "none"` (`"none"` wins over any other match).
|
|
157
|
-
- The types reach the commit that brings the page. React keeps one queue of transition types per root, and the first transition commit takes the whole queue. No router hook update takes them first: `
|
|
157
|
+
- The types reach the commit that brings the page. React keeps one queue of transition types per root, and the first transition commit takes the whole queue. No router hook update takes them first: `useSegments()` commits with the page and `useHandle()` right after it, and a `useNavigation()` value that moves while neither the state nor the URL does (`pendingUrl` as the response starts streaming, `isStreaming` as it ends) commits with the page while the reader has a held value, urgently otherwise, so it starts no view transition of its own. `useAction()` values commit urgently. `useLinkStatus()` commits `pending` urgently and its end with the page, and a newer navigation or an abort ends it urgently, outside a held commit; neither starts a transition. A transition your own code starts while React holds a navigation's page can still take them. Handle values on a held page land in the commit right after the page, by design; see [hooks](../hooks/handle-and-actions.md#when-handle-values-change-by-design). Use a kind map to choose an animation, never for correctness.
|
|
158
158
|
|
|
159
159
|
### Back and Forward
|
|
160
160
|
|
package/src/__internal.ts
CHANGED
|
@@ -85,10 +85,16 @@ export type {
|
|
|
85
85
|
*/
|
|
86
86
|
export type {
|
|
87
87
|
RouterContext,
|
|
88
|
-
RevalidationContext,
|
|
89
88
|
InterceptResult,
|
|
90
89
|
} from "./router/router-context.js";
|
|
91
90
|
|
|
91
|
+
/**
|
|
92
|
+
* @internal
|
|
93
|
+
* The request side of a revalidating resolution pass (RouterContext's
|
|
94
|
+
* revalidating resolvers take it).
|
|
95
|
+
*/
|
|
96
|
+
export type { ResolutionPassInput } from "./router/segment-resolution/resolution-pass.js";
|
|
97
|
+
|
|
92
98
|
/**
|
|
93
99
|
* @internal
|
|
94
100
|
* Route match context during pipeline processing.
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
*
|
|
21
21
|
* The drop runs the batch's follow-up: when an action of the batch did not
|
|
22
22
|
* land on the page on screen, the action bridge refetches that page once,
|
|
23
|
-
* whole
|
|
23
|
+
* whole (or whole above an action's error boundary on screen, which it
|
|
24
|
+
* holds), after the navigation the user started, unless an intercept is on
|
|
24
25
|
* screen (server-action-bridge.ts followUp; the rules are R1-R7 in
|
|
25
26
|
* docs/internal/execution-model.md).
|
|
26
27
|
*
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
* state regardless of listener ordering.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
import { WIRE } from "../wire-params.js";
|
|
21
|
+
|
|
20
22
|
const IDLE_TIMEOUT = 60_000;
|
|
21
23
|
const DEBOUNCE_DELAY = 150;
|
|
22
24
|
|
|
@@ -61,7 +63,7 @@ export function startConnectionWarmup(
|
|
|
61
63
|
|
|
62
64
|
function sendWarmup() {
|
|
63
65
|
coldLatch = false;
|
|
64
|
-
env.fetch(
|
|
66
|
+
env.fetch(`/?${WIRE.warmup}`, { method: "HEAD" }).catch(() => {});
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
function triggerWarmup() {
|