@rangojs/router 0.23.0 → 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 +4 -3
- package/dist/types/browser/merge-segment-loaders.d.ts +5 -2
- package/dist/types/browser/partial-update.d.ts +6 -4
- package/dist/types/browser/react/NavigationProvider.d.ts +11 -7
- package/dist/types/browser/react/context.d.ts +3 -2
- package/dist/types/browser/types.d.ts +5 -1
- package/dist/types/build/route-types/scan-filter.d.ts +7 -1
- package/dist/types/cache/document-cache-policy.d.ts +24 -0
- package/dist/types/cache/document-cache.d.ts +2 -10
- package/dist/types/context-var.d.ts +12 -0
- package/dist/types/errors.d.ts +10 -0
- package/dist/types/handles/pushed-by.d.ts +8 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.rsc.d.ts +2 -0
- package/dist/types/response-utils.d.ts +7 -0
- package/dist/types/reverse.d.ts +8 -8
- 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/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/mask-nested.d.ts +4 -2
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +36 -13
- 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/request-context.d.ts +39 -2
- package/dist/types/server/rewrite-trail.d.ts +49 -0
- package/dist/types/server/slot-accessors.d.ts +29 -0
- package/dist/types/testing/index.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +8 -0
- package/dist/types/testing/run-middleware.d.ts +20 -0
- package/dist/types/testing/serve-shell-request.d.ts +2 -1
- package/dist/types/thenable-status.d.ts +6 -0
- package/dist/types/types/request-scope.d.ts +7 -0
- package/dist/types/vite/discovery/state.d.ts +12 -9
- package/dist/vite/index.js +164 -103
- package/package.json +1 -1
- package/skills/caching/SKILL.md +12 -0
- package/skills/catalog.json +1 -1
- package/skills/client-urls/SKILL.md +84 -0
- package/skills/hooks/SKILL.md +11 -6
- package/skills/hooks/data.md +3 -1
- package/skills/loader/SKILL.md +7 -1
- package/skills/middleware/SKILL.md +183 -2
- package/skills/ppr/SKILL.md +9 -0
- package/skills/rango/SKILL.md +1 -1
- package/skills/server-actions/SKILL.md +27 -0
- package/skills/testing/client-components.md +1 -0
- package/skills/testing/middleware.md +31 -10
- package/skills/testing/response-routes.md +1 -0
- package/skills/testing/serve-shell-request.md +19 -1
- package/skills/use-cache/SKILL.md +4 -2
- package/src/__augment-tests__/augmented.check.ts +20 -0
- package/src/browser/event-controller.ts +3 -0
- package/src/browser/merge-segment-loaders.ts +31 -18
- package/src/browser/navigation-bridge.ts +19 -3
- package/src/browser/partial-update.ts +27 -12
- package/src/browser/react/Link.tsx +7 -9
- package/src/browser/react/NavigationProvider.tsx +46 -26
- package/src/browser/react/context.ts +3 -2
- package/src/browser/react/use-router.ts +2 -4
- package/src/browser/server-action-bridge.ts +6 -0
- package/src/browser/types.ts +5 -1
- package/src/build/route-types/scan-filter.ts +11 -4
- package/src/cache/document-cache-policy.ts +131 -0
- package/src/cache/document-cache.ts +12 -119
- package/src/context-var.ts +32 -2
- package/src/errors.ts +19 -0
- package/src/handles/pushed-by.ts +26 -0
- package/src/index.rsc.ts +8 -0
- package/src/index.ts +7 -0
- package/src/response-utils.ts +19 -0
- package/src/reverse.ts +17 -14
- 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 +29 -12
- package/src/router/internal-params.ts +16 -0
- package/src/router/loader-resolution.ts +2 -0
- package/src/router/match-middleware/background-revalidation.ts +11 -9
- package/src/router/match-middleware/cache-lookup.ts +20 -15
- package/src/router/match-middleware/cache-store.ts +9 -11
- package/src/router/match-middleware/intercept-resolution.ts +4 -2
- package/src/router/middleware-types.ts +16 -0
- package/src/router/middleware.ts +127 -49
- package/src/router/revalidation.ts +1 -1
- package/src/router/route-trie-builder.ts +28 -1
- 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 +57 -17
- package/src/router/segment-resolution/revalidation.ts +57 -33
- 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 +4 -2
- package/src/rsc/ssr-setup.ts +11 -5
- package/src/server/cookie-parse.ts +36 -12
- package/src/server/cookie-store.ts +7 -3
- package/src/server/handle-store.ts +48 -17
- package/src/server/request-context.ts +154 -63
- 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/ssr/index.tsx +118 -16
- package/src/testing/dispatch.ts +138 -67
- package/src/testing/index.ts +1 -0
- package/src/testing/render-route.tsx +16 -3
- package/src/testing/run-loader.ts +2 -0
- package/src/testing/run-middleware.ts +56 -11
- package/src/testing/serve-shell-request.ts +15 -2
- package/src/thenable-status.ts +11 -0
- package/src/types/request-scope.ts +8 -0
- package/src/vite/discovery/state.ts +21 -13
- package/src/vite/plugins/expose-internal-ids.ts +2 -3
- package/src/vite/router-discovery.ts +29 -27
package/skills/loader/SKILL.md
CHANGED
|
@@ -285,7 +285,9 @@ See `/route` for the full handler-context contract (shared with loaders) and
|
|
|
285
285
|
|
|
286
286
|
- `ctx.params` — merged route params + explicit loader params. For fetchable
|
|
287
287
|
loaders called with `load(Loader, { params: { ... } })`, explicit params
|
|
288
|
-
override route-matched params.
|
|
288
|
+
override route-matched params. Explicit params are string values; a request
|
|
289
|
+
with another value or a `__proto__`, `constructor` or `prototype` key is
|
|
290
|
+
rejected (400, `load()` fails with `Invalid loader params`).
|
|
289
291
|
- `ctx.routeParams` — server-trusted route params from URL pattern matching.
|
|
290
292
|
Cannot be overridden by client-provided params.
|
|
291
293
|
|
|
@@ -333,6 +335,10 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
333
335
|
]);
|
|
334
336
|
```
|
|
335
337
|
|
|
338
|
+
In a `clientUrls()` group the same `loader(Def, () => [revalidate(fn)])` form
|
|
339
|
+
runs on the server with these arguments; the build moves the predicate out of
|
|
340
|
+
the `"use client"` module (`/client-urls` → "Revalidation runs on the server").
|
|
341
|
+
|
|
336
342
|
### `revalidate()` return shapes
|
|
337
343
|
|
|
338
344
|
> **Scope: `revalidate()` is a partial-render concern, not a cache concern.**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: middleware
|
|
3
|
-
description: Define global (router.use) and route (middleware()) middleware in @rangojs/router — auth gates, logging, response headers, context variables, redirects. Use when gating routes behind auth checks, logging requests, setting headers or ctx variables before a handler runs, or deciding whether middleware wraps server actions.
|
|
3
|
+
description: Define global (router.use) and route (middleware()) middleware in @rangojs/router — auth gates, logging, response headers, context variables, redirects, rewrites (throw rewrite() renders another route at the same URL). Use when gating routes behind auth checks, logging requests, setting headers or ctx variables before a handler runs, showing a maintenance page at the visitor's URL, or deciding whether middleware wraps server actions.
|
|
4
4
|
argument-hint: [middleware-name]
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -19,7 +19,7 @@ There are two levels of middleware with different execution scopes:
|
|
|
19
19
|
|
|
20
20
|
### Global middleware (`router.use()`)
|
|
21
21
|
|
|
22
|
-
Registered on the router instance. Wraps the **entire request**, including server actions, rendering, and progressive enhancement (PE) re-renders.
|
|
22
|
+
Registered on the router instance. Wraps the **entire request**, including server actions, rendering, and progressive enhancement (PE) re-renders. A request a middleware rewrites (`throw rewrite()`, below) runs the chain again for the target: once per pass.
|
|
23
23
|
|
|
24
24
|
```typescript
|
|
25
25
|
const router = createRouter<AppEnv>({})
|
|
@@ -257,6 +257,8 @@ export const myMiddleware: Middleware = async (ctx, next) => {
|
|
|
257
257
|
ctx.headers; // response Headers (stub before next(), the real response after)
|
|
258
258
|
ctx.setLocationState(FlashMessage({ text: "Saved" })); // history state for the client
|
|
259
259
|
ctx.reverse("home"); // URL for a global route name (no ".name", no param auto-fill)
|
|
260
|
+
ctx.rewrittenFrom; // the visitor's URL when this request was rewritten, else undefined
|
|
261
|
+
ctx.isRewrite(error); // true for a thrown rewrite() (rethrow it from a try/catch)
|
|
260
262
|
|
|
261
263
|
// Opt the current request out of PPR shell lookup/capture.
|
|
262
264
|
ctx.dynamic();
|
|
@@ -428,6 +430,185 @@ Read the flash on the target page with `useLocationState(FlashMessage)`. The `{
|
|
|
428
430
|
|
|
429
431
|
A redirect is followed on a link click, `router.push()`, and Back/Forward to an entry the client has to fetch. On Back/Forward the redirect replaces the history entry it was triggered from: Back from the target lands on the entry before the redirecting one, not on the redirecting one again. The client follows at most 20 redirects in one navigation; a loop (two pages redirecting to each other) stops there with the error boundary and a `[rango] Server redirect loop` console error.
|
|
430
432
|
|
|
433
|
+
## Rewrite: another route at the same URL
|
|
434
|
+
|
|
435
|
+
`throw rewrite(target)` renders another route while the address bar keeps the
|
|
436
|
+
visitor's URL: a maintenance or lock page, a deploy migration, a URL override,
|
|
437
|
+
an A/B or geo split. A redirect changes the URL; a rewrite does not.
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
import { getRequestContext, rewrite } from "@rangojs/router";
|
|
441
|
+
import type { Middleware } from "@rangojs/router";
|
|
442
|
+
|
|
443
|
+
export const maintenance: Middleware = async (ctx, next) => {
|
|
444
|
+
if (!locked || ctx.pathname === "/maintenance") return next();
|
|
445
|
+
// Mutations get a Response: a rewrite cannot answer a server action or a form post.
|
|
446
|
+
if (ctx.request.method !== "GET") {
|
|
447
|
+
return new Response("Maintenance", {
|
|
448
|
+
status: 503,
|
|
449
|
+
headers: { "Retry-After": "600" },
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
ctx.set(Lock, { until });
|
|
453
|
+
throw rewrite("maintenance");
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
// urls.tsx — the target sets its own status
|
|
457
|
+
path("/maintenance", MaintenancePage, { name: "maintenance" }, () => [
|
|
458
|
+
middleware(async (ctx, next) => {
|
|
459
|
+
getRequestContext().setStatus(503);
|
|
460
|
+
ctx.header("Retry-After", "600");
|
|
461
|
+
await next();
|
|
462
|
+
}),
|
|
463
|
+
]);
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Targets, like `ctx.reverse` with a path form:
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
throw rewrite("/maintenance"); // a path (basename-prefixed like redirect())
|
|
470
|
+
throw rewrite("/maintenance?from=shop"); // a path with search
|
|
471
|
+
throw rewrite("maintenance"); // a global route name without params
|
|
472
|
+
throw rewrite("product", { slug }); // with params
|
|
473
|
+
throw rewrite("product", { slug }, { tab: "reviews" }); // and search
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
**How it runs.** The throw unwinds the request to the router, which runs it
|
|
477
|
+
again as the target, from the top, as a direct visit would: global middleware
|
|
478
|
+
matched on the target's path (a `.use("/admin/*", guard)` or an inline
|
|
479
|
+
`ctx.pathname` check decides on the rendered route), the target's route
|
|
480
|
+
middleware, its render. Code after `await next()` in the pass that rewrote
|
|
481
|
+
never runs, and global middleware runs once per pass.
|
|
482
|
+
|
|
483
|
+
**Never skip a guard on a rewritten pass.** Authorization, auth and
|
|
484
|
+
rate-limit checks run on every pass, as written: the target's guards
|
|
485
|
+
deciding on the rendered route is what keeps a rewrite from becoming a way
|
|
486
|
+
around them. `if (!ctx.rewrittenFrom)` is only for side effects you want
|
|
487
|
+
once per request, such as a counter or a log line:
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
export const countVisit: Middleware = async (ctx, next) => {
|
|
491
|
+
if (!ctx.rewrittenFrom) visits.increment(); // once per request
|
|
492
|
+
return next();
|
|
493
|
+
};
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
**Throw it before `await next()`.** Once your `next()` ran the route, the
|
|
497
|
+
page has rendered: a `throw rewrite()` after it (a "rewrite on 404" after
|
|
498
|
+
`const res = await next()`) is an error, "throw rewrite() before await
|
|
499
|
+
next()". Decide before calling `next()`. A rewrite thrown further down
|
|
500
|
+
passes through your `await next()` as usual, and in a `catch` around it you
|
|
501
|
+
may throw another rewrite in its place; after any other failure of `next()`
|
|
502
|
+
(the route may have rendered before it failed) a rewrite is an error too.
|
|
503
|
+
|
|
504
|
+
**Same request context.** Values you `ctx.set()` before the throw are
|
|
505
|
+
readable in the target's middleware, handlers and loaders; cookies, headers
|
|
506
|
+
and the status you set stay on the response; location state, theme and
|
|
507
|
+
`ctx.dynamic()` carry over. The request, URLs, params and route name are the
|
|
508
|
+
target's.
|
|
509
|
+
|
|
510
|
+
**Old and new URL.** `ctx.url`, `ctx.pathname` and `ctx.searchParams` are
|
|
511
|
+
the target's; `ctx.rewrittenFrom` is the visitor's URL (search included; the
|
|
512
|
+
first URL after several rewrites; `undefined` when nothing rewrote).
|
|
513
|
+
`usePathname()` and `useSearchParams()` show the visitor's URL in the server
|
|
514
|
+
HTML and after hydration, except in a render other visitors are served (see
|
|
515
|
+
Caches); `useParams()` and the route name are the target's. The response
|
|
516
|
+
carries `x-rango-rewrite: <target pathname>`.
|
|
517
|
+
|
|
518
|
+
**`ctx.rewrittenFrom` is request identity**, like `cookies()`: reading it
|
|
519
|
+
inside a `cache()` boundary or a `"use cache"` function throws (pass what
|
|
520
|
+
you need in from middleware), and a render other visitors may be served (a
|
|
521
|
+
PPR shell capture, a page the document cache stores) reads `undefined`, as a
|
|
522
|
+
direct visit does.
|
|
523
|
+
|
|
524
|
+
**Middleware only, by design.** `rewrite()` is a middleware tool. A route
|
|
525
|
+
handler or a loader (any loader, `ssr: false` included) cannot rewrite: its
|
|
526
|
+
throw is an error whose message points you to route middleware. A rewrite
|
|
527
|
+
that depends on data goes in the route's middleware, reading the data
|
|
528
|
+
through a cached function the loader shares, so the lookup runs once:
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
532
|
+
middleware(async (ctx, next) => {
|
|
533
|
+
const product = await getProduct(ctx.params.slug); // cached, shared with the loader
|
|
534
|
+
if (product.discontinued) {
|
|
535
|
+
throw rewrite("discontinued", { slug: ctx.params.slug });
|
|
536
|
+
}
|
|
537
|
+
await next();
|
|
538
|
+
}),
|
|
539
|
+
loader(productLoader), // calls getProduct(slug): a cache hit
|
|
540
|
+
]);
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
**Throw it, don't return it.** A returned rewrite is an error. Where a
|
|
544
|
+
rewrite cannot be honored it is an error that names the rule: a route
|
|
545
|
+
handler or a loader (see above), a server action (use `redirect()`),
|
|
546
|
+
intercept middleware, a loader fetch (`useFetchLoader`, `load()`: it
|
|
547
|
+
rejects with the rule) or its middleware, and middleware handling a non-GET
|
|
548
|
+
request (a JS action or a no-JS form post: return a `Response`).
|
|
549
|
+
|
|
550
|
+
**try/catch around next().** A thrown rewrite passes through your
|
|
551
|
+
middleware's `catch`. Rethrow it:
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
export const errorPage: Middleware = async (ctx, next) => {
|
|
555
|
+
try {
|
|
556
|
+
return await next();
|
|
557
|
+
} catch (error) {
|
|
558
|
+
if (ctx.isRewrite(error)) throw error;
|
|
559
|
+
return new Response("Something went wrong", { status: 500 });
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
**Loops.** More than 5 rewrites in one request, or a rewrite to the URL the
|
|
565
|
+
request is on, answers 508 and reports a `RewriteLoopError` (its message has
|
|
566
|
+
the trail, `/a -> /b -> /a`) through `onError` with phase `"routing"`.
|
|
567
|
+
|
|
568
|
+
**Caches.**
|
|
569
|
+
|
|
570
|
+
- The document cache keys a rewritten request by the target's URL, as a
|
|
571
|
+
direct visit. Register the rewriting middleware **before**
|
|
572
|
+
`createDocumentCacheMiddleware()`: a cache hit for the visitor's URL would
|
|
573
|
+
be served before your middleware runs. A page the document cache stores,
|
|
574
|
+
and a PPR shell of the target, are rendered as a direct visit to the
|
|
575
|
+
target: they carry the target's pathname and `ctx.rewrittenFrom` is
|
|
576
|
+
`undefined` there, so a component that renders `usePathname()` can
|
|
577
|
+
mismatch on hydration at the visitor's URL when such a page is served to
|
|
578
|
+
a rewritten request.
|
|
579
|
+
- The client's prefetch and history caches keep a rewritten page under the
|
|
580
|
+
visitor's URL, and nothing tells them about the rewrite: a page prefetched
|
|
581
|
+
during a lock shows the lock page for up to `prefetchCacheTTL` after the
|
|
582
|
+
lock lifts. `invalidateClientCache()` drops a client's caches, for the
|
|
583
|
+
client that receives the response; mark the clients that saw the lock and
|
|
584
|
+
call it on their first response after it lifts:
|
|
585
|
+
|
|
586
|
+
```typescript
|
|
587
|
+
import { cookies, invalidateClientCache, rewrite } from "@rangojs/router";
|
|
588
|
+
|
|
589
|
+
export const maintenance: Middleware = async (ctx, next) => {
|
|
590
|
+
if (!locked) {
|
|
591
|
+
if (cookies().get("saw-lock")) {
|
|
592
|
+
cookies().delete("saw-lock");
|
|
593
|
+
invalidateClientCache();
|
|
594
|
+
}
|
|
595
|
+
return next();
|
|
596
|
+
}
|
|
597
|
+
cookies().set("saw-lock", "1", { path: "/" });
|
|
598
|
+
// ...the lock as above
|
|
599
|
+
throw rewrite("maintenance");
|
|
600
|
+
};
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
**Testing.** `runMiddleware` reports a thrown rewrite as `result.rewrite`
|
|
604
|
+
(`{ pathname, search, routeName? }`). `serveShellRequest` takes a rewritten
|
|
605
|
+
page request through the full production pipeline (document or `partial`
|
|
606
|
+
navigation): the target's middleware, render, status and `x-rango-rewrite`.
|
|
607
|
+
`dispatch` re-dispatches too, for response routes only: it matches the
|
|
608
|
+
visitor's URL first and throws on a component (RSC) route.
|
|
609
|
+
`renderRoute(..., { request: "/shop", rewrite: "/maintenance" })` renders
|
|
610
|
+
the target's route at the visitor's URL. See `/testing`.
|
|
611
|
+
|
|
431
612
|
## Authentication Middleware
|
|
432
613
|
|
|
433
614
|
```typescript
|
package/skills/ppr/SKILL.md
CHANGED
|
@@ -149,6 +149,15 @@ material is shared per host+URL and request partition. `cache(false)`, or a
|
|
|
149
149
|
`condition()` that returns false, on a ppr route now renders the request like a
|
|
150
150
|
cache miss (no shell).
|
|
151
151
|
|
|
152
|
+
### A request middleware rewrote
|
|
153
|
+
|
|
154
|
+
A request middleware rewrote (`throw rewrite()`, `/middleware`) to a `ppr`
|
|
155
|
+
route is served the target's shell under the target's key. The capture
|
|
156
|
+
renders the target as a direct visit: `ctx.rewrittenFrom` reads `undefined`
|
|
157
|
+
there and the payload carries the target's pathname, so the shell is the
|
|
158
|
+
same for direct and rewritten visitors (a component that renders
|
|
159
|
+
`usePathname()` shows the target's URL to a rewritten visitor on a HIT).
|
|
160
|
+
|
|
152
161
|
### Request-partitioned shells
|
|
153
162
|
|
|
154
163
|
A route whose `cache()` record is partitioned by the request partitions its
|
package/skills/rango/SKILL.md
CHANGED
|
@@ -311,7 +311,7 @@ Grouped by concern — read when you need to…
|
|
|
311
311
|
| `/layout` | Layouts that wrap child routes |
|
|
312
312
|
| `/parallel` | Multi-column layouts and sidebars |
|
|
313
313
|
| `/intercept` | Modal/slide-over patterns for soft navigation |
|
|
314
|
-
| `/middleware` | Request processing
|
|
314
|
+
| `/middleware` | Request processing, authentication, `throw rewrite()` (same-URL rewrites) |
|
|
315
315
|
| `/host-router` | Multi-app host routing with domain/subdomain patterns |
|
|
316
316
|
| `/links` | URL generation: ctx.reverse, href, useHref, useMount, scopedReverse |
|
|
317
317
|
| `/response-routes` | JSON/text/HTML/XML/stream endpoints with `path.json()`, `path.text()` |
|
|
@@ -492,6 +492,11 @@ file-path-based revalidation predicates, define the action in a module-level
|
|
|
492
492
|
`"use server"` file rather than inline. See `/loader` for the full revalidation
|
|
493
493
|
contract (deferred returns, soft suggestions).
|
|
494
494
|
|
|
495
|
+
A loader in a `clientUrls()` group takes the same predicate with the same
|
|
496
|
+
arguments, `actionResult` and `formData` included: the build moves it out of
|
|
497
|
+
the `"use client"` module and the server runs it (`/client-urls` →
|
|
498
|
+
"Revalidation runs on the server").
|
|
499
|
+
|
|
495
500
|
### Cross-segment dependencies
|
|
496
501
|
|
|
497
502
|
If a loader reads `ctx.get()` data set by an outer layout/handler, that
|
|
@@ -680,6 +685,28 @@ export async function deletePost(id: string): Promise<void> {
|
|
|
680
685
|
For a recoverable "already gone" case, return a result the form can show
|
|
681
686
|
(`{ error: "Post not found" }`) instead of throwing.
|
|
682
687
|
|
|
688
|
+
### A loader that fails on the revalidation render
|
|
689
|
+
|
|
690
|
+
A loader that throws while it reruns after the action does not undo the
|
|
691
|
+
action, which has already run and returned. What follows depends on the route:
|
|
692
|
+
|
|
693
|
+
- **No `loading()` on the route (and any `clientUrls()` route):** the action
|
|
694
|
+
call rejects with the loader's error, nothing of that revalidation commits,
|
|
695
|
+
and the page keeps its data and its React state. A `<form action>`,
|
|
696
|
+
`useActionState` or `useTransition` call hands that rejection to the nearest
|
|
697
|
+
React error boundary above the form.
|
|
698
|
+
- **A `urls()` route with `loading()`:** the call resolves with the action's
|
|
699
|
+
result, the page renders, and the loader's read throws to the nearest React
|
|
700
|
+
error boundary.
|
|
701
|
+
- **An `errorBoundary()` above the route:** its fallback replaces the route.
|
|
702
|
+
|
|
703
|
+
For a failure the user can retry, return it from the loader as data
|
|
704
|
+
(`{ ok: false, reason }`) and render it where the data goes. A failure returned
|
|
705
|
+
as data is a value like any other, so a `cache()` on the loader (or a `ppr`
|
|
706
|
+
bake, or a prerender) stores and serves it; a thrown error is never stored.
|
|
707
|
+
See `/client-urls` → "A loader that fails on its rerun" for the table and an
|
|
708
|
+
example.
|
|
709
|
+
|
|
683
710
|
### Authorization in actions
|
|
684
711
|
|
|
685
712
|
Route middleware does **not** wrap action execution — only global
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
| `nonce` | `string` | CSP nonce to seed via `NonceContext`, so a component calling `useNonce()` (e.g. an analytics/GTM head script) sees it — mirroring SSR. Defaults to `undefined` (the browser default). |
|
|
27
27
|
| `defaultPrefetch` | `PrefetchStrategy` | Router default for `<Link>` and eligible plain anchors. `data-prefetch="false"`/`"none"` opts out one anchor; ancestor `data-prefetch-scope="false"`/`"none"` hard-disables the subtree; `"true"` permits routed resource suffixes elsewhere. |
|
|
28
28
|
| `hydrate` | `boolean` | Render the same tree to HTML first (`renderToString`, no `window`/`document`), then hydrate that HTML instead of mounting fresh. The result gains `serverHtml` and `recoverableErrors`. See [Hydration](#hydration). |
|
|
29
|
+
| `rewrite` | `string` | The path middleware rewrote `request` to (`throw rewrite()`): the leaf is matched against it for params, while the location (`usePathname`, `useSearchParams`) stays `request`, as a rewritten document renders and hydrates. |
|
|
29
30
|
|
|
30
31
|
`RenderRouteSpec = { path, Component, layout?, loaderIds?, name?, transition? }` — one node of the route definition. The array is the layout chain root-to-leaf; the LAST entry is the leaf route (its pattern is matched against `request` to extract params; layout patterns are informational). `loaderIds` attaches seeded loaders to THIS node's segment; `layout` on the leaf wraps it; `name` is informational. `transition` is the `transition()` config this node declares (`{}` for a bare `transition()`), attached to its segment exactly as the DSL does — see [Held navigation](#held-navigation).
|
|
31
32
|
|
|
@@ -44,16 +44,17 @@ The `ctx` your middleware reads. Notable fields:
|
|
|
44
44
|
|
|
45
45
|
### Returns — `RunMiddlewareResult<TEnv>`
|
|
46
46
|
|
|
47
|
-
| Field | Type | Meaning
|
|
48
|
-
| ----------------- | ------------------------- |
|
|
49
|
-
| `response` | `Response` | The final Response: the downstream response, or a middleware short-circuit.
|
|
50
|
-
| `ctx` | `RequestContext<TEnv>` | The underlying RequestContext (NOT a per-middleware `MiddlewareContext`). Use `ctx.get(...)` for anything the envelope above doesn't surface.
|
|
51
|
-
| `nextCalled` | `number` | Times the terminal handler ran: `0` on short-circuit, `1` on pass-through.
|
|
52
|
-
| `
|
|
53
|
-
| `
|
|
54
|
-
| `
|
|
55
|
-
| `
|
|
56
|
-
| `
|
|
47
|
+
| Field | Type | Meaning |
|
|
48
|
+
| ----------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
49
|
+
| `response` | `Response` | The final Response: the downstream response, or a middleware short-circuit. |
|
|
50
|
+
| `ctx` | `RequestContext<TEnv>` | The underlying RequestContext (NOT a per-middleware `MiddlewareContext`). Use `ctx.get(...)` for anything the envelope above doesn't surface. |
|
|
51
|
+
| `nextCalled` | `number` | Times the terminal handler ran: `0` on short-circuit, `1` on pass-through. |
|
|
52
|
+
| `rewrite` | `RunMiddlewareRewrite?` | The target of a `throw rewrite(...)`: `{ pathname, search, routeName? }`, resolved as production resolves it (a path basename-prefixed, a name through `routeMap`). `undefined` when nothing rewrote. On a rewrite, `response` carries what the chain set before the throw. |
|
|
53
|
+
| `dynamic` | `boolean` | Whether the chain called `ctx.dynamic()` (the PPR shell opt-out). |
|
|
54
|
+
| `cookies` | `Record<string, string>` | Effective cookie view: request cookies merged with chain sets/deletes (last-write-wins), as `{ name: value }`. |
|
|
55
|
+
| `headers` | `Record<string, string>` | Final response headers as `{ name: value }`, lowercased, EXCLUDING `set-cookie` (use `cookies`). |
|
|
56
|
+
| `locationState` | `Record<string, unknown>` | Flat `{ key: value }` state set via `setLocationState()` / `redirect({ state })` (empty when none). |
|
|
57
|
+
| `stateCookieName` | `string` | The resolved rango state cookie name seeded for the run (default `rango-state_router_0`). Assert a middleware's `invalidateClientCache()` rotation against it without recomputing — parity with `runInRequestContext` / `runLoaderResult` / `renderHandler`. |
|
|
57
58
|
|
|
58
59
|
## Recipe
|
|
59
60
|
|
|
@@ -89,6 +90,26 @@ describe("requireUser", () => {
|
|
|
89
90
|
|
|
90
91
|
Pass an array to run several in order. Cookies set inside middleware via the standalone `cookies().set(...)` (imported from `@rangojs/router`, NOT a `ctx` method) surface on the result's `cookies` and on the merged response `Set-Cookie`.
|
|
91
92
|
|
|
93
|
+
### A middleware that rewrites
|
|
94
|
+
|
|
95
|
+
A `throw rewrite(target)` (see `/middleware` → "Rewrite") does not fail the run: `result.rewrite` names the target, and the values and cookies the chain set before the throw are on `result.ctx` and `result.cookies`, as production keeps them for the target. `runMiddleware` does not run the target. To test what a rewritten page renders, use `serveShellRequest` (`./serve-shell-request.md` → "A page middleware rewrites"): it takes the request through the full production pipeline, the target's middleware, render, status and `x-rango-rewrite` included. `dispatch` re-dispatches only between response routes: it matches the visitor's URL before middleware runs and throws on a component (RSC) route.
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
it("rewrites a locked page to the maintenance route", async () => {
|
|
99
|
+
const result = await runMiddleware(maintenance, {
|
|
100
|
+
request: "/shop",
|
|
101
|
+
routeMap: { maintenance: "/maintenance" },
|
|
102
|
+
});
|
|
103
|
+
expect(result.rewrite).toEqual({
|
|
104
|
+
pathname: "/maintenance",
|
|
105
|
+
search: "",
|
|
106
|
+
routeName: "maintenance",
|
|
107
|
+
});
|
|
108
|
+
expect(result.nextCalled).toBe(0);
|
|
109
|
+
expect(result.ctx.get(Lock)).toEqual({ until: "12:00" });
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
92
113
|
## Caveats
|
|
93
114
|
|
|
94
115
|
- No `handles`/`rendered` option by design: the middleware context has no handle APIs in production — no push (`ctx.use(Handle)` is a handler/loader API) and no post-barrier read (`ctx.get(handle)` is loader-only, and middleware runs BEFORE the render barrier anyway). Read handle data in a loader and test it with `runLoader`; assert loader pushes via `runLoaderResult(...).handlePushes` (see `./handles.md`).
|
|
@@ -91,6 +91,7 @@ describe("api routes via dispatch", () => {
|
|
|
91
91
|
- JSON serialization is bare, applied in `response-route-handler.ts`: a `path.json` handler that returns a value is serialized verbatim (`JSON.stringify(value)`, status 200, `application/json`) — no envelope. Returning or throwing a `Response` (e.g. `Response.json(x)`) uses the same control-flow path. Any other thrown error takes the error's status (`RouterError.status`, else 500, or a non-200 status already set upstream in the request pipeline). On a `path.json` route the body is an RFC 9457 problem+json object `{ title, status, detail, code }` (`application/problem+json;charset=utf-8`), where `code` is the `RouterError.code`, else `"INTERNAL"`, and the `type` member is omitted. On text/html/xml/md routes the body is `text/plain`: the `RouterError` message, the error message in dev, or `"Internal Server Error"` in production. Assert the shape matching what your handler returns.
|
|
92
92
|
- Setup: needs the preset (alias + virtual stubs) or a Vite-RSC env (see `./setup.md`); a bare router import throws on Vite virtuals.
|
|
93
93
|
- 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.
|
|
94
|
+
- 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.
|
|
94
95
|
- 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.
|
|
95
96
|
- `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, and any 3xx redirect on a partial OR action request becomes a `204` + `X-RSC-Redirect` (fetch-safe interception), the raw `Location` dropped. Location state carried on such a redirect is dropped too (production embeds it in a Flight payload); cover that at e2e.
|
|
96
97
|
- `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.
|
|
@@ -14,7 +14,7 @@ The one stub is the HTML step (the SSR module), because `react-dom/server` does
|
|
|
14
14
|
|
|
15
15
|
So `prelude` is the shell as captured (Flight text, not HTML): a value the shell froze appears there, and a hole (a live loader under `loading()`) does not.
|
|
16
16
|
|
|
17
|
-
A `Prerender` route is served as in production: from the artifact `router.matchForPrerender` bakes for the URL (on its first request, kept until `resetShellTestState()`), through the production prerender store, so its handler runs once, at the bake, and never on a request. Its first `ppr` request is a MISS with a runtime capture, as a URL without a build-time shell is in production. The shell `vite build` bakes (served first in production for a URL without a query string) is not reproduced: keep that in e2e. The `env` option doubles as the bake's `buildEnv`. An on-demand route (`Prerender(..., { onDemand })`) is baked only when it is wrapped in `Passthrough()` and its `getParams()` lists the param, as a build bakes it; a plain on-demand route is not baked here, so its pages come from the prerender store alone (`router.prerender()`), and "a removed page the build baked" is a case for a `Passthrough` route or for e2e.
|
|
17
|
+
A `Prerender` route is served as in production: from the artifact `router.matchForPrerender` bakes for the URL (on its first request, kept until `resetShellTestState()`), through the production prerender store, so its handler runs once, at the bake, and never on a request. A partial navigation an `intercept()` of the route answers is served from the intercept variant the same bake keeps, as the build writes it: the intercept's handler output is the bake's, its loaders run per request. Its first `ppr` request is a MISS with a runtime capture, as a URL without a build-time shell is in production. The shell `vite build` bakes (served first in production for a URL without a query string) is not reproduced: keep that in e2e. The `env` option doubles as the bake's `buildEnv`. An on-demand route (`Prerender(..., { onDemand })`) is baked only when it is wrapped in `Passthrough()` and its `getParams()` lists the param, as a build bakes it; a plain on-demand route is not baked here, so its pages come from the prerender store alone (`router.prerender()`), and "a removed page the build baked" is a case for a `Passthrough` route or for e2e.
|
|
18
18
|
|
|
19
19
|
## API
|
|
20
20
|
|
|
@@ -392,6 +392,24 @@ const result = await serveShellRequest(router, "/shop", {
|
|
|
392
392
|
|
|
393
393
|
This is the request a `clientUrls()` group's `revalidate()` sees on an action: the action's return value as `actionResult`, its last `FormData` argument as `formData`. The pinned example is `src/testing/__tests__/client-urls-revalidate.rsc-test.tsx`. Not with `partial` or `form`.
|
|
394
394
|
|
|
395
|
+
## A page middleware rewrites
|
|
396
|
+
|
|
397
|
+
A request a middleware rewrites (`throw rewrite()`, see `/middleware`) runs through the same production handler: the visitor's pass, then the target's, with one request context. Assert what the visitor gets: the target's body and status, `x-rango-rewrite`, the visitor's pathname in the payload, and on a `ppr` target the target's shell key.
|
|
398
|
+
|
|
399
|
+
```ts
|
|
400
|
+
it("a locked page renders the maintenance route at its own URL", async () => {
|
|
401
|
+
const result = await serveShellRequest(router, "/shop?lock=on");
|
|
402
|
+
expect(result.response.status).toBe(503); // set by the target's middleware
|
|
403
|
+
expect(result.response.headers.get("x-rango-rewrite")).toBe("/maintenance");
|
|
404
|
+
expect(result.body).toContain("Down for maintenance");
|
|
405
|
+
|
|
406
|
+
const nav = await serveShellRequest(router, "/shop?lock=on", {
|
|
407
|
+
partial: { from: "/" },
|
|
408
|
+
});
|
|
409
|
+
expect(nav.flight).toContain("Down for maintenance"); // the target's segments
|
|
410
|
+
});
|
|
411
|
+
```
|
|
412
|
+
|
|
395
413
|
## Caveats
|
|
396
414
|
|
|
397
415
|
What the HTML stub cannot reproduce, so keep it in e2e:
|
|
@@ -236,8 +236,10 @@ this guard, one user's data would be served to another.
|
|
|
236
236
|
The raw reads throw the same way: `ctx.request.headers` on any `ctx` passed in
|
|
237
237
|
(handler, loader, middleware; it is one `Request` object, also
|
|
238
238
|
`getRequestContext().request`), and `getRequestContext().cookie()` /
|
|
239
|
-
`.cookies()`.
|
|
240
|
-
|
|
239
|
+
`.cookies()`. `ctx.rewrittenFrom` (the visitor's URL of a request middleware
|
|
240
|
+
rewrote) throws too, on a request nothing rewrote as well: it is not in the
|
|
241
|
+
key. A `ctx` or a `Request` argument keys the entry by its route and URL
|
|
242
|
+
only, never by its headers.
|
|
241
243
|
|
|
242
244
|
Extract the value before the cached function and pass it as an argument:
|
|
243
245
|
|
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
import type { DefaultRouteName } from "../types/global-namespace.js";
|
|
20
20
|
import type { InterceptWhenFn } from "../server/context.js";
|
|
21
21
|
import { href } from "../href-client.js";
|
|
22
|
+
import { rewrite, type Rewrite } from "../route-definition/rewrite.js";
|
|
22
23
|
import type { Money, TestBindings } from "./augment.js";
|
|
23
24
|
|
|
24
25
|
type Expect<T extends true> = T;
|
|
@@ -68,6 +69,25 @@ function middlewareReverse(ctx: MiddlewareContext): void {
|
|
|
68
69
|
}
|
|
69
70
|
void middlewareReverse;
|
|
70
71
|
|
|
72
|
+
// rewrite() takes a path, or a global name with the params and search
|
|
73
|
+
// ctx.reverse takes; ctx.isRewrite narrows a caught value to a Rewrite.
|
|
74
|
+
function middlewareRewrite(ctx: MiddlewareContext, caught: unknown): void {
|
|
75
|
+
void rewrite("/maintenance?from=shop");
|
|
76
|
+
void rewrite("home");
|
|
77
|
+
void rewrite("blog.post", { slug: "hello" });
|
|
78
|
+
void rewrite("search", {}, { q: "hats" });
|
|
79
|
+
// @ts-expect-error - a route with params needs them
|
|
80
|
+
void rewrite("blog.post");
|
|
81
|
+
// @ts-expect-error - unknown global name is rejected
|
|
82
|
+
void rewrite("nope");
|
|
83
|
+
// @ts-expect-error - dot-local name; a rewrite resolves global names only
|
|
84
|
+
void rewrite(".home");
|
|
85
|
+
if (ctx.isRewrite(caught)) {
|
|
86
|
+
type _narrowed = Expect<Equal<typeof caught, Rewrite>>;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
void middlewareRewrite;
|
|
90
|
+
|
|
71
91
|
// routeName narrows to the generated route names.
|
|
72
92
|
type _routeName = Expect<
|
|
73
93
|
Equal<DefaultRouteName, "home" | "blog.post" | "search">
|
|
@@ -1037,6 +1037,9 @@ export function createEventController(
|
|
|
1037
1037
|
// If not settled and not completed, this is an error case - force finalize
|
|
1038
1038
|
if (!settled && inflightActions.has(id)) {
|
|
1039
1039
|
actionCompleted = true;
|
|
1040
|
+
// Done, as complete()/fail() would mark it: an action started in its
|
|
1041
|
+
// settle window is sequential, not concurrent (#675).
|
|
1042
|
+
entry.completed = true;
|
|
1040
1043
|
streamingEnded = true;
|
|
1041
1044
|
tryFinalize();
|
|
1042
1045
|
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import type { ResolvedSegment } from "./types.js";
|
|
2
2
|
import { debugLog, IS_BROWSER_DEBUG } from "./logging.js";
|
|
3
|
+
import { settledValue } from "../thenable-status.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Merge partial loader data from server with cached loader data.
|
|
6
7
|
*
|
|
7
8
|
* During partial revalidation (stale or action), the server may return only
|
|
8
|
-
* some loaders that pass the revalidation check
|
|
9
|
-
* all loader data, so we merge fresh data with
|
|
9
|
+
* some loaders that pass the revalidation check, none when every one is held.
|
|
10
|
+
* The component still needs all loader data, so we merge fresh data with
|
|
11
|
+
* cached data. Two settled aggregates merge into the array itself: a fresh
|
|
12
|
+
* Promise.all over settled parts is a promise React has not read, and content
|
|
13
|
+
* on screen handed one suspends (docs/internal/suspense-contract.md, rule 2).
|
|
10
14
|
*
|
|
11
15
|
* @param fromServer - Segment returned from server with partial loaders
|
|
12
16
|
* @param fromCache - Cached segment with full loader data
|
|
@@ -25,24 +29,33 @@ export function mergeSegmentLoaders(
|
|
|
25
29
|
);
|
|
26
30
|
}
|
|
27
31
|
|
|
32
|
+
// Fresh data for the loaders the server re-ran, cached data for the rest.
|
|
33
|
+
const merge = (newData: any[], cachedData: any[]): any[] =>
|
|
34
|
+
cachedLoaderIds.map((id: string, i: number) => {
|
|
35
|
+
const newIndex = serverLoaderIds.indexOf(id);
|
|
36
|
+
return newIndex !== -1 ? newData[newIndex] : cachedData[i];
|
|
37
|
+
});
|
|
38
|
+
const settledNew = settledValue(fromServer.loaderDataPromise!);
|
|
39
|
+
const settledCached = settledValue(fromCache.loaderDataPromise!);
|
|
40
|
+
|
|
28
41
|
return {
|
|
29
42
|
...fromCache,
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
// The server re-ran the slot's handler and layout: their render applies,
|
|
44
|
+
// as it does when every loader re-runs. Only the loaders it held keep
|
|
45
|
+
// their cached data.
|
|
46
|
+
component: fromServer.component,
|
|
47
|
+
layout: fromServer.layout,
|
|
48
|
+
// Every loader held: the cached aggregate itself, so a boundary still
|
|
49
|
+
// waiting on it keeps its one thenable (suspense-contract.md, rule 1).
|
|
50
|
+
loaderDataPromise:
|
|
51
|
+
serverLoaderIds.length === 0
|
|
52
|
+
? fromCache.loaderDataPromise
|
|
53
|
+
: settledNew && settledCached
|
|
54
|
+
? merge(settledNew, settledCached)
|
|
55
|
+
: Promise.all([
|
|
56
|
+
fromServer.loaderDataPromise!,
|
|
57
|
+
fromCache.loaderDataPromise!,
|
|
58
|
+
]).then(([newData, cachedData]) => merge(newData, cachedData)),
|
|
46
59
|
// Keep all loader IDs from cache
|
|
47
60
|
loaderIds: fromCache.loaderIds,
|
|
48
61
|
// The rebuilt aggregate is the source of truth. Cached per-loader
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
pushHistoryWithIdx,
|
|
22
22
|
} from "./history-state.js";
|
|
23
23
|
import { consumeHandles } from "./react/NavigationProvider.js";
|
|
24
|
+
import { withSegmentHandles } from "../handles/pushed-by.js";
|
|
24
25
|
import {
|
|
25
26
|
handleNavigationStart,
|
|
26
27
|
handleNavigationEnd,
|
|
@@ -386,7 +387,9 @@ export function createNavigationBridge(
|
|
|
386
387
|
// since it may have fresher data after an action revalidation.
|
|
387
388
|
// This avoids unnecessary server round-trips for shared layout loaders.
|
|
388
389
|
let cachedSegments = cached?.segments;
|
|
389
|
-
|
|
390
|
+
// The segments taken from the page on screen: their pushes come from
|
|
391
|
+
// it too (targetCacheHandleData below).
|
|
392
|
+
const fromSource = new Set<string>();
|
|
390
393
|
if (cachedSegments && sourceCached?.segments) {
|
|
391
394
|
const sourceSegmentMap = new Map(
|
|
392
395
|
sourceCached.segments.map((s) => [s.id, s]),
|
|
@@ -396,7 +399,9 @@ export function createNavigationBridge(
|
|
|
396
399
|
// Use source (current page) version for shared segments - it's
|
|
397
400
|
// fresher. A placeholder waiting for its fill (prefetch: false) is
|
|
398
401
|
// not a copy of the segment: the target keeps its own.
|
|
399
|
-
|
|
402
|
+
if (!sourceSeg || isPlaceholder(sourceSeg)) return targetSeg;
|
|
403
|
+
fromSource.add(targetSeg.id);
|
|
404
|
+
return sourceSeg;
|
|
400
405
|
});
|
|
401
406
|
}
|
|
402
407
|
|
|
@@ -498,7 +503,18 @@ export function createNavigationBridge(
|
|
|
498
503
|
...(hasUsableCache
|
|
499
504
|
? {
|
|
500
505
|
targetCacheSegments: cachedSegments,
|
|
501
|
-
|
|
506
|
+
// Read at commit: a push the page on screen gets while
|
|
507
|
+
// the request is in flight is in it.
|
|
508
|
+
targetCacheHandleData: () =>
|
|
509
|
+
withSegmentHandles(
|
|
510
|
+
cached?.handleData ?? {},
|
|
511
|
+
liveHandleDataForCache(
|
|
512
|
+
eventController,
|
|
513
|
+
store,
|
|
514
|
+
sourceHistoryKey,
|
|
515
|
+
),
|
|
516
|
+
fromSource,
|
|
517
|
+
),
|
|
502
518
|
}
|
|
503
519
|
: {}),
|
|
504
520
|
...(clientUrlPresentation && {
|