@rangojs/router 0.32.0 → 0.33.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/README.md +3 -2
- package/dist/bin/browser-7KIJ5HSV.js +15 -0
- package/dist/bin/chunk-3B7CKBRO.js +463 -0
- package/dist/bin/chunk-524VACCG.js +71 -0
- package/dist/bin/chunk-LD7UBHT4.js +196 -0
- package/dist/bin/chunk-TO6CRWLN.js +1429 -0
- package/dist/bin/chunk-TTXC2U3G.js +2477 -0
- package/dist/bin/client-urls-projection-DQLWPDAT.js +28 -0
- package/dist/bin/forward-user-plugins-URBZKJP4.js +62 -0
- package/dist/bin/rango.js +42 -10173
- package/dist/bin/runtime-EPUDEHO4.js +596 -0
- package/dist/bin/runtime-discovery-CZ2QHT4Q.js +352 -0
- package/dist/bin/state-ACD2MGKJ.js +16 -0
- package/dist/bin/temp-rsc-server-QRR6HD6T.js +3190 -0
- package/dist/types/bin/rango.d.ts +1 -0
- package/dist/types/browser/action-fence.d.ts +20 -0
- package/dist/types/browser/cookie-name.d.ts +51 -10
- package/dist/types/browser/invalidate-client-cache.d.ts +8 -6
- package/dist/types/browser/navigation-bridge.d.ts +36 -1
- package/dist/types/browser/navigation-client.d.ts +19 -0
- package/dist/types/browser/navigation-store-handle.d.ts +37 -4
- package/dist/types/browser/navigation-store.d.ts +2 -2
- package/dist/types/browser/navigation-transaction.d.ts +0 -1
- package/dist/types/browser/partial-update.d.ts +13 -0
- package/dist/types/browser/prefetch/cache.d.ts +96 -8
- package/dist/types/browser/prefetch/fetch.d.ts +26 -2
- package/dist/types/browser/prefetch/queue.d.ts +3 -2
- package/dist/types/browser/rango-state.d.ts +34 -5
- package/dist/types/browser/react/Link.d.ts +15 -6
- package/dist/types/browser/types.d.ts +25 -0
- package/dist/types/build/generate-route-types.d.ts +1 -1
- package/dist/types/build/route-types/include-resolution.d.ts +9 -1
- package/dist/types/build/route-types/router-processing.d.ts +3 -1
- package/dist/types/cache/cache-key-utils.d.ts +12 -8
- package/dist/types/cache/cache-scope.d.ts +4 -2
- package/dist/types/cache/cf/cf-cache-types.d.ts +9 -5
- package/dist/types/cache/document-cache-policy.d.ts +9 -1
- package/dist/types/cache/memory-segment-store.d.ts +3 -2
- package/dist/types/cache/segment-codec.d.ts +2 -2
- package/dist/types/cache/shell-snapshot.d.ts +1 -1
- package/dist/types/cache/types.d.ts +12 -7
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +2 -1
- package/dist/types/client-urls/types.d.ts +7 -5
- package/dist/types/prefetch-markers.d.ts +9 -0
- package/dist/types/prefetch-source-scope.d.ts +66 -0
- package/dist/types/route-definition/helpers-types.d.ts +8 -5
- package/dist/types/router/intercept-resolution.d.ts +25 -11
- package/dist/types/router/match-context.d.ts +1 -0
- package/dist/types/router/match-handlers.d.ts +1 -1
- package/dist/types/router/prefetch-cache-ttl.d.ts +11 -3
- package/dist/types/router/router-context.d.ts +1 -1
- package/dist/types/router/router-options.d.ts +6 -3
- package/dist/types/router/segment-resolution/loader-cache.d.ts +8 -0
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +23 -7
- package/dist/types/router/segment-resolution/revalidation.d.ts +19 -0
- package/dist/types/router/source-reads.d.ts +53 -0
- package/dist/types/router/types.d.ts +1 -1
- package/dist/types/rsc/helpers.d.ts +5 -2
- package/dist/types/rsc/prefetch-render-error.d.ts +7 -2
- package/dist/types/rsc/request-stages.d.ts +2 -0
- package/dist/types/rsc/shell-serve.d.ts +6 -1
- package/dist/types/server/context.d.ts +23 -6
- package/dist/types/server/cookie-store.d.ts +15 -1
- package/dist/types/server/document-location-state.d.ts +5 -2
- package/dist/types/server/handle-store.d.ts +2 -0
- package/dist/types/server/request-context.d.ts +43 -7
- package/dist/types/testing/e2e/server.d.ts +16 -1
- package/dist/types/testing/render-route.d.ts +66 -2
- package/dist/types/testing/serve-shell-request.d.ts +6 -3
- package/dist/types/types/handler-context.d.ts +7 -5
- package/dist/types/types/loader-types.d.ts +7 -4
- package/dist/types/types/segments.d.ts +15 -4
- package/dist/types/urls/path-helper-types.d.ts +7 -5
- package/dist/types/vite/temp-rsc-server.d.ts +2 -2
- package/dist/vite/index.js +449 -189
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +74 -0
- package/skills/app-structure/e-commerce.md +10 -9
- package/skills/app-structure/large-app.md +15 -2
- package/skills/caching/SKILL.md +10 -5
- package/skills/client-urls/SKILL.md +18 -11
- package/skills/comparison/references/framework-comparison.md +13 -8
- package/skills/document-cache/SKILL.md +8 -0
- package/skills/hooks/navigation.md +8 -8
- package/skills/hooks/state.md +24 -8
- package/skills/intercept/SKILL.md +33 -12
- package/skills/layout/SKILL.md +11 -0
- package/skills/links/SKILL.md +101 -4
- package/skills/loader/SKILL.md +49 -22
- package/skills/migrate-nextjs/SKILL.md +8 -2
- package/skills/migrate-react-router/data-and-actions.md +5 -0
- package/skills/ppr/SKILL.md +26 -5
- package/skills/prerender/SKILL.md +4 -1
- package/skills/route/SKILL.md +6 -6
- package/skills/router-setup/SKILL.md +2 -1
- package/skills/server-actions/SKILL.md +41 -8
- package/skills/testing/client-components.md +29 -13
- package/skills/testing/serve-shell-request.md +7 -7
- package/skills/testing/server-actions.md +8 -0
- package/skills/ux-architecture/SKILL.md +67 -18
- package/src/bin/rango.ts +39 -13
- package/src/browser/action-fence.ts +72 -0
- package/src/browser/cookie-name.ts +82 -14
- package/src/browser/invalidate-client-cache.ts +9 -7
- package/src/browser/navigation-bridge.ts +164 -32
- package/src/browser/navigation-client.ts +95 -20
- package/src/browser/navigation-store-handle.ts +67 -5
- package/src/browser/navigation-store.ts +86 -17
- package/src/browser/navigation-transaction.ts +0 -6
- package/src/browser/partial-update.ts +31 -12
- package/src/browser/prefetch/cache.ts +188 -11
- package/src/browser/prefetch/fetch.ts +211 -51
- package/src/browser/prefetch/queue.ts +3 -2
- package/src/browser/rango-state.ts +50 -8
- package/src/browser/react/Link.tsx +20 -9
- package/src/browser/react/use-navigation.ts +1 -1
- package/src/browser/rsc-router.tsx +8 -1
- package/src/browser/server-action-bridge.ts +3 -3
- package/src/browser/types.ts +25 -0
- package/src/build/generate-route-types.ts +1 -0
- package/src/build/route-types/include-resolution.ts +40 -3
- package/src/build/route-types/router-processing.ts +170 -25
- package/src/build/route-types/scan-filter.ts +1 -0
- package/src/cache/cache-key-utils.ts +12 -8
- package/src/cache/cache-runtime.ts +5 -1
- package/src/cache/cache-scope.ts +28 -7
- package/src/cache/cf/cf-cache-types.ts +9 -5
- package/src/cache/document-cache-policy.ts +27 -0
- package/src/cache/document-cache.ts +5 -0
- package/src/cache/memory-segment-store.ts +3 -2
- package/src/cache/segment-codec.ts +2 -2
- package/src/cache/shell-snapshot.ts +1 -1
- package/src/cache/types.ts +12 -7
- package/src/cache/vercel/vercel-cache-store.ts +2 -1
- package/src/client-urls/types.ts +7 -5
- package/src/prefetch-markers.ts +10 -0
- package/src/prefetch-source-scope.ts +137 -0
- package/src/route-definition/helpers-types.ts +8 -5
- package/src/router/intercept-resolution.ts +94 -50
- package/src/router/loader-resolution.ts +8 -6
- package/src/router/match-api.ts +33 -29
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +2 -1
- package/src/router/match-middleware/cache-lookup.ts +21 -13
- package/src/router/match-result.ts +39 -87
- package/src/router/navigation-snapshot.ts +18 -3
- package/src/router/prefetch-cache-ttl.ts +15 -3
- package/src/router/revalidation.ts +53 -23
- package/src/router/router-context.ts +2 -1
- package/src/router/router-options.ts +6 -3
- package/src/router/segment-resolution/loader-cache.ts +21 -3
- package/src/router/segment-resolution/prefetch-deferral.ts +38 -31
- package/src/router/segment-resolution/revalidation.ts +40 -12
- package/src/router/source-reads.ts +122 -0
- package/src/router/types.ts +2 -1
- package/src/router.ts +11 -12
- package/src/rsc/helpers.ts +8 -3
- package/src/rsc/prefetch-render-error.ts +7 -2
- package/src/rsc/progressive-enhancement.ts +3 -2
- package/src/rsc/request-stages.ts +59 -0
- package/src/rsc/rsc-rendering.ts +49 -33
- package/src/rsc/server-action.ts +2 -1
- package/src/rsc/shell-capture.ts +4 -5
- package/src/rsc/shell-serve.ts +12 -1
- package/src/segment-system.tsx +1 -1
- package/src/server/context.ts +37 -6
- package/src/server/cookie-store.ts +22 -1
- package/src/server/document-location-state.ts +23 -2
- package/src/server/handle-store.ts +2 -0
- package/src/server/request-context.ts +113 -28
- package/src/testing/dispatch.ts +3 -0
- package/src/testing/e2e/fixture.ts +4 -12
- package/src/testing/e2e/server.ts +91 -4
- package/src/testing/render-route.tsx +269 -4
- package/src/testing/serve-shell-request.ts +6 -3
- package/src/types/handler-context.ts +7 -5
- package/src/types/loader-types.ts +7 -4
- package/src/types/segments.ts +15 -4
- package/src/urls/path-helper-types.ts +7 -5
- package/src/vite/temp-rsc-server.ts +2 -2
|
@@ -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 | 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
|
|
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`.
|
|
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 the speculative kind a viewport or render `<Link>` prefetch sends (`X-Rango-Prefetch: 1`), so `prefetch: false` work is deferred; add `headers: { "X-Rango-Prefetch": "intent" }` to send a hover's or `prefetch()`'s intent prefetch instead, which defers nothing. `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
|
|
|
@@ -53,6 +53,14 @@
|
|
|
53
53
|
| `stateCookieName` | `string` | The resolved rango state cookie name this run seeded (default `rango-state_router_0`). Assert an `invalidateClientCache()` rotation against it without recomputing. |
|
|
54
54
|
| `locationState` | `Record<string, unknown>` | The flash set via `ctx.setLocationState()` / `redirect({ state })`, as the flat `{ key: value }` the client reads. |
|
|
55
55
|
|
|
56
|
+
`runInRequestContext` runs your function, not the router's action handling,
|
|
57
|
+
so its `response` carries no write bump: the router sets that around every
|
|
58
|
+
action it runs (the state cookie's `{version}:{timestamp}:{write}`, write part
|
|
59
|
+
bumped unless the action called `keepClientCache()`). To assert it, send the
|
|
60
|
+
action through the router with `serveShellRequest(router, url, { action })`
|
|
61
|
+
(or `{ form }` with `createActionForm` for a post before hydration) and read
|
|
62
|
+
the state `Set-Cookie` on `result.response`.
|
|
63
|
+
|
|
56
64
|
Low-level building blocks (all from `@rangojs/router/testing`):
|
|
57
65
|
|
|
58
66
|
- `createTestRequestContext(opts)` builds the context WITHOUT entering it and returns `{ ctx, request, url, variables, stateCookieName }`. Its `ctx.reverse` accepts any name from your `routeMap`.
|
|
@@ -249,6 +249,13 @@ them right:
|
|
|
249
249
|
server").
|
|
250
250
|
- A prefetch runs the predicates too, so a loader that holds costs nothing in
|
|
251
251
|
the prefetch of a tab switch.
|
|
252
|
+
- A predicate that reads `currentParams` or `fromRouteName` and keeps its
|
|
253
|
+
segment shares that prefetch only between the pages with equal values of
|
|
254
|
+
what it read; one that reads `currentUrl` makes it page-only (2.6).
|
|
255
|
+
`bySlug` keeps only on an unchanged slug, so a product's tabs share their
|
|
256
|
+
prefetches; `bySearch` reads the URL, so the cost is the links between one
|
|
257
|
+
query's tabs. A layout rule comparing a param most pages share (a locale,
|
|
258
|
+
an account) groups by that param and keeps the sharing.
|
|
252
259
|
- Unit-test them with `runRevalidate` (`/testing`).
|
|
253
260
|
|
|
254
261
|
### 2.5 loading() or an inline Suspense
|
|
@@ -279,8 +286,14 @@ A prefetch is a navigation sent early: the server renders the destination's
|
|
|
279
286
|
handlers and loaders for it, from the page you are on (`/links`, "What a
|
|
280
287
|
prefetch renders"). A click that adopts a complete prefetch commits with no
|
|
281
288
|
request. Entries live 300 s by default (`prefetchCacheTTL`) in the router's
|
|
282
|
-
memory
|
|
283
|
-
|
|
289
|
+
memory. An action marks them stale unless it calls `keepClientCache()`: a
|
|
290
|
+
click on one shows it at once, then revalidates the page in the background
|
|
291
|
+
(`/server-actions`, "Client Cache After an Action"). The browser HTTP cache
|
|
292
|
+
keeps a prefetch for as long only when nothing in it is live: a document cache
|
|
293
|
+
HIT, or an answer whose every sent segment came from a `cache()` record or a
|
|
294
|
+
Prerender entry and that runs no loader
|
|
295
|
+
(`Cache-Control: private, max-age=<prefetchCacheTTL>`). Any other prefetch
|
|
296
|
+
carries no `Cache-Control` of the router's. A prefetch whose page failed
|
|
284
297
|
is never used: a 500 or 404, and a 200 whose render reported an error (a
|
|
285
298
|
loader that threw, a `loading()` handler that fell back to its
|
|
286
299
|
`errorBoundary()`, an async component that threw). The click sends its own
|
|
@@ -301,11 +314,16 @@ The rule: **prefetch broadly, and flag what is expensive.**
|
|
|
301
314
|
already on screen whose `revalidate()` re-runs it for the destination is
|
|
302
315
|
prefetched with it, so the click does not wait on a fill for data the page
|
|
303
316
|
is already showing.
|
|
304
|
-
-
|
|
305
|
-
|
|
317
|
+
- `prefetch: false` applies to speculative prefetches: viewport and render.
|
|
318
|
+
A hover, or `useRouter().prefetch()`, is intent to click and runs the
|
|
319
|
+
loader, so its click needs no fill. A hover over a link a viewport prefetch
|
|
320
|
+
already answered sends nothing, and that click still fills.
|
|
321
|
+
- The trade-off of a deferring prefetch, measured on a `clientUrls()` tab in
|
|
322
|
+
a consumer app (dev, a loaded machine), before a hover's prefetch ran
|
|
323
|
+
flagged loaders: with the tab's loader flagged, a hover then a click 150 ms
|
|
306
324
|
later showed the content in 843 to 918 ms; with the tab prefetched whole, in
|
|
307
325
|
77 to 157 ms. The flagged click pays the fill request and then React's
|
|
308
|
-
300 ms on the tab's fallback.
|
|
326
|
+
300 ms on the tab's fallback. A viewport-prefetched link still pays it.
|
|
309
327
|
- A click that adopts a prefetch with a flagged loader shows the page at once
|
|
310
328
|
and sends one fill request for the flagged loaders. Read a flagged loader
|
|
311
329
|
under its own `<Suspense>`: with no boundary, React keeps the page being
|
|
@@ -313,12 +331,33 @@ The rule: **prefetch broadly, and flag what is expensive.**
|
|
|
313
331
|
- On a server route, `loading(fallback, { prefetch: false })` keeps the whole
|
|
314
332
|
entry behind that fallback out of the prefetch (`/route`, "`loading()`
|
|
315
333
|
options"). A group's `loading()` takes no options.
|
|
316
|
-
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
334
|
+
- One prefetch of a page serves every page that links to it, so a paginated list
|
|
335
|
+
or a grid does not prefetch each link again after every navigation. The server
|
|
336
|
+
makes an answer page-only (`x-rsc-prefetch-scope: source`) when sharing it
|
|
337
|
+
would be wrong: an intercept targets the route or the page sending it, the
|
|
338
|
+
answer defers a `prefetch: false` unit the page does not show yet, a
|
|
339
|
+
`revalidate()` on the target read `currentUrl` and kept its segment, a rule
|
|
340
|
+
on the route (or inside its `path()`) read the page being left and kept the
|
|
341
|
+
segment through the route's default, or a kept answer sends no segment at
|
|
342
|
+
all. Only a click from that exact URL, query
|
|
343
|
+
and hash included, uses a page-only answer, so every page prefetches it
|
|
344
|
+
again (`/links`, "Shared prefetches and `prefetchKey`").
|
|
345
|
+
- A `revalidate()` that read `currentParams` or `fromRouteName` and kept its
|
|
346
|
+
segment, or a prerendered target reached from a page with the same route
|
|
347
|
+
params, groups the answer (`x-rsc-prefetch-scope: params; <label>`): the
|
|
348
|
+
pages with equal values of what was read share it. A layout rule comparing
|
|
349
|
+
`currentParams.locale` shares each prefetch between the pages of one locale.
|
|
350
|
+
Spreading `currentParams` reads every param; read the ones the rule needs by
|
|
351
|
+
name. A page's own block (a category hero) still goes in its route or in a
|
|
352
|
+
layout inside its `path()`, and links that follow a mount param come from
|
|
353
|
+
`useReverse`; neither needs a rule (`/app-structure`, "Prefetches are
|
|
354
|
+
shared").
|
|
355
|
+
- `prefetchKey=":source"` on a Link, or
|
|
356
|
+
`router.prefetch(url, { key: ":source" })`, makes that link's prefetches
|
|
357
|
+
page-only. Use it when the target's answer depends on the page through
|
|
358
|
+
something the server does not track: `context` in a `revalidate()`, or a
|
|
359
|
+
`Referer` read. It costs the same requests, so set it per link, not
|
|
360
|
+
app-wide.
|
|
322
361
|
|
|
323
362
|
### 2.7 transition() and view transitions
|
|
324
363
|
|
|
@@ -375,16 +414,16 @@ The rule: **prefetch broadly, and flag what is expensive.**
|
|
|
375
414
|
the error to the nearest error boundary, which replaces the page and its
|
|
376
415
|
form (`/client-urls`, "A loader that fails on its rerun").
|
|
377
416
|
- `keepClientCache()` in an action that changed nothing keeps the prefetches
|
|
378
|
-
and the history cache.
|
|
417
|
+
and the history cache fresh: no revalidation after a click or on Back.
|
|
379
418
|
- `useOptimistic` gives feedback before the action answers.
|
|
380
419
|
|
|
381
420
|
### 2.10 Updates you did not make: three channels
|
|
382
421
|
|
|
383
|
-
| The change | The tool
|
|
384
|
-
| -------------------------------------------------- |
|
|
385
|
-
| Your own mutation | A server action plus `revalidate()` rules (`isAction(ref)`, `actionResult`). The server re-runs what the action touched in the same response; nothing else to call.
|
|
386
|
-
| Out of band: a push, polling, a REST call, a login | `useLoader(L).load()` (also `refetch`): a plain `load()` of a route-registered loader updates every reader of it. `refreshGroup` on reads plus `useRefreshLoaders()` refreshes several loaders at once. Both need a fetchable loader, `createLoader(fn, true)`; a plain one answers 403. `router.refresh()` re-renders the current route's server data and keeps client state. `invalidateClientCache()` marks the history cache stale
|
|
387
|
-
| Shared server caches (other users' data) | Tags: `cache({ tags })` or `cacheTag()`, then `updateTag()` in the action (awaitable, the action's response reads fresh) or `revalidateTag()` from a webhook. `revalidate()` is not cache invalidation (`/cache-guide`).
|
|
422
|
+
| The change | The tool |
|
|
423
|
+
| -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
424
|
+
| Your own mutation | A server action plus `revalidate()` rules (`isAction(ref)`, `actionResult`). The server re-runs what the action touched in the same response; nothing else to call. |
|
|
425
|
+
| Out of band: a push, polling, a REST call, a login | `useLoader(L).load()` (also `refetch`): a plain `load()` of a route-registered loader updates every reader of it. `refreshGroup` on reads plus `useRefreshLoaders()` refreshes several loaders at once. Both need a fetchable loader, `createLoader(fn, true)`; a plain one answers 403. `router.refresh()` re-renders the current route's server data and keeps client state. `invalidateClientCache()` marks the history cache stale, drops prefetches and rotates the state when the change makes them wrong. |
|
|
426
|
+
| Shared server caches (other users' data) | Tags: `cache({ tags })` or `cacheTag()`, then `updateTag()` in the action (awaitable, the action's response reads fresh) or `revalidateTag()` from a webhook. `revalidate()` is not cache invalidation (`/cache-guide`). |
|
|
388
427
|
|
|
389
428
|
### 2.11 Handles: titles and breadcrumbs
|
|
390
429
|
|
|
@@ -448,7 +487,7 @@ ctx.reverse(".product", { slug: "b", locale: "de" }); // switch language
|
|
|
448
487
|
take unchecked path strings: prefer the typed `useReverse` map.
|
|
449
488
|
- `useReverse` with another module's map, or `useReverse(NamedRoutes)` under
|
|
450
489
|
a mount, gives a wrong URL with no error. Build cross-module links on the
|
|
451
|
-
server and pass them as props or loader data
|
|
490
|
+
server and pass them as props or loader data.
|
|
452
491
|
- A server layout stays held across navigations, its own `:projectId`
|
|
453
492
|
changing included: partial rendering by design. Links it rendered keep the
|
|
454
493
|
old params. Put param-dependent nav links in a client component with
|
|
@@ -929,6 +968,16 @@ Each entry: what the user sees, why, the fix.
|
|
|
929
968
|
15. **An effect keyed on a location-state object.** In a group the object is
|
|
930
969
|
the one the navigation passed until the commit, then the history entry's
|
|
931
970
|
copy: the effect runs twice. Fix: key it on a primitive field.
|
|
971
|
+
16. **A page's own block in a shared layout, kept by a rule that reads the
|
|
972
|
+
page being left** (a category hero in a template over the product pages,
|
|
973
|
+
re-rendered when `currentParams.category` changes). Nothing looks wrong,
|
|
974
|
+
but every prefetch under that layout is shared only between the pages of
|
|
975
|
+
one category (or with the same params, off the category routes): the
|
|
976
|
+
other pages prefetch their links again and the server renders them
|
|
977
|
+
again. Fix: put
|
|
978
|
+
the block in its route, or in a layout inside its `path()`; it
|
|
979
|
+
re-renders on its own params with no rule (`/app-structure`, "Prefetches
|
|
980
|
+
are shared").
|
|
932
981
|
|
|
933
982
|
## 5. Known gaps
|
|
934
983
|
|
package/src/bin/rango.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve, dirname, basename } from "node:path";
|
|
2
3
|
import { readFileSync, statSync, existsSync } from "node:fs";
|
|
3
4
|
import {
|
|
4
5
|
findTsFiles,
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
detectUnresolvableIncludesForUrlsFile,
|
|
9
10
|
findNestedRouterConflict,
|
|
10
11
|
formatNestedRouterConflictError,
|
|
12
|
+
isRoutableSourceFile,
|
|
11
13
|
type UnresolvableInclude,
|
|
12
14
|
} from "../build/generate-route-types.ts";
|
|
13
15
|
|
|
@@ -93,6 +95,19 @@ Examples:
|
|
|
93
95
|
);
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
/**
|
|
99
|
+
* A createRouter() file that is not a test: a test builds its own router (a
|
|
100
|
+
* Flight test's `*.rsc-test.tsx` usually mounts an inline urls() tree), and a
|
|
101
|
+
* named-routes.gen.ts beside it would merge its routes into the app's global
|
|
102
|
+
* GeneratedRouteMap. The same rule findRouterFiles() applies in Vite.
|
|
103
|
+
*/
|
|
104
|
+
function isRouterFile(filePath: string, source: string): boolean {
|
|
105
|
+
return (
|
|
106
|
+
isRoutableSourceFile(basename(filePath)) &&
|
|
107
|
+
/\bcreateRouter\s*[<(]/.test(source)
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
96
111
|
/**
|
|
97
112
|
* Walk up from a file path to find the project root (directory containing
|
|
98
113
|
* package.json or vite.config.ts).
|
|
@@ -141,7 +156,7 @@ function runStaticGeneration(args: string[], mode: "default" | "static") {
|
|
|
141
156
|
for (const filePath of files) {
|
|
142
157
|
try {
|
|
143
158
|
const source = readFileSync(filePath, "utf-8");
|
|
144
|
-
if (
|
|
159
|
+
if (isRouterFile(filePath, source)) {
|
|
145
160
|
routerFiles.push(filePath);
|
|
146
161
|
}
|
|
147
162
|
// Both spellings, matching the writer's own gate: "clientUrls(" does
|
|
@@ -181,7 +196,8 @@ function runStaticGeneration(args: string[], mode: "default" | "static") {
|
|
|
181
196
|
// Deduplicate diagnostics (router and urls detection may find the same issue)
|
|
182
197
|
const seen = new Set<string>();
|
|
183
198
|
const uniqueDiagnostics = allDiagnostics.filter((d) => {
|
|
184
|
-
|
|
199
|
+
// Every unreadable routes mount has pathPrefix "/": its detail names it.
|
|
200
|
+
const key = `${d.sourceFile}:${d.pathPrefix}:${d.reason}${d.reason === "unresolvable-routes" ? `:${d.detail}` : ""}`;
|
|
185
201
|
if (seen.has(key)) return false;
|
|
186
202
|
seen.add(key);
|
|
187
203
|
return true;
|
|
@@ -192,10 +208,11 @@ function runStaticGeneration(args: string[], mode: "default" | "static") {
|
|
|
192
208
|
console.error("\n[rango] Unresolvable routes detected:\n");
|
|
193
209
|
formatDiagnostics(uniqueDiagnostics);
|
|
194
210
|
console.error(
|
|
195
|
-
"\nThe static parser cannot resolve these includes or basename
|
|
196
|
-
"they use factory functions or dynamic expressions. A
|
|
197
|
-
"
|
|
198
|
-
"
|
|
211
|
+
"\nThe static parser cannot resolve these includes, routes or basename " +
|
|
212
|
+
"because they use factory functions or dynamic expressions. A router's " +
|
|
213
|
+
"routes must be a urls() call, a builder function or a variable holding " +
|
|
214
|
+
"urls(); a basename must be a string literal or a top-level const (in " +
|
|
215
|
+
"the router file or imported from a relative module).\n\n" +
|
|
199
216
|
"Options:\n" +
|
|
200
217
|
" rango generate <path> --runtime Use Vite-based discovery (requires vite)\n" +
|
|
201
218
|
" rango generate <path> --static Accept partial output (missing routes or basename prefix above)\n",
|
|
@@ -255,7 +272,7 @@ async function runRuntimeDiscovery(args: string[], configFile?: string) {
|
|
|
255
272
|
for (const filePath of files) {
|
|
256
273
|
try {
|
|
257
274
|
const source = readFileSync(filePath, "utf-8");
|
|
258
|
-
if (
|
|
275
|
+
if (isRouterFile(filePath, source)) {
|
|
259
276
|
routerEntries.push(filePath);
|
|
260
277
|
}
|
|
261
278
|
// Also generate per-module types for urls files. Both spellings — a
|
|
@@ -281,6 +298,10 @@ async function runRuntimeDiscovery(args: string[], configFile?: string) {
|
|
|
281
298
|
process.exit(1);
|
|
282
299
|
}
|
|
283
300
|
|
|
301
|
+
// Loads from its own chunk only because the bin is built with esbuild
|
|
302
|
+
// --splitting (package.json "build"). A single-file bundle folds this module
|
|
303
|
+
// in and hoists its Vite imports to the top, so every command, --help
|
|
304
|
+
// included, failed without vite installed.
|
|
284
305
|
let discoverAndWriteRouteTypes: typeof import("../build/runtime-discovery.ts").discoverAndWriteRouteTypes;
|
|
285
306
|
try {
|
|
286
307
|
const mod = await import("../build/runtime-discovery.ts");
|
|
@@ -290,9 +311,12 @@ async function runRuntimeDiscovery(args: string[], configFile?: string) {
|
|
|
290
311
|
err.code === "ERR_MODULE_NOT_FOUND" ||
|
|
291
312
|
err.code === "MODULE_NOT_FOUND"
|
|
292
313
|
) {
|
|
314
|
+
// runtime-discovery checks vite and @vitejs/plugin-rsc itself and names
|
|
315
|
+
// what to install, so reaching here means a bundled chunk file is
|
|
316
|
+
// missing: the installed package is incomplete.
|
|
293
317
|
console.error(
|
|
294
|
-
|
|
295
|
-
"
|
|
318
|
+
`[rango] Failed to load runtime discovery: ${err.message}\n` +
|
|
319
|
+
"The installed @rangojs/router looks broken (a CLI chunk is missing). Reinstall it.",
|
|
296
320
|
);
|
|
297
321
|
} else {
|
|
298
322
|
console.error(`[rango] Failed to load runtime discovery: ${err.message}`);
|
|
@@ -320,9 +344,11 @@ function formatDiagnostics(
|
|
|
320
344
|
const prefix =
|
|
321
345
|
d.reason === "unresolvable-basename"
|
|
322
346
|
? "createRouter basename"
|
|
323
|
-
: d.
|
|
324
|
-
?
|
|
325
|
-
:
|
|
347
|
+
: d.reason === "unresolvable-routes"
|
|
348
|
+
? "createRouter routes"
|
|
349
|
+
: d.namePrefix
|
|
350
|
+
? `${d.namePrefix}.*`
|
|
351
|
+
: `${d.pathPrefix}*`;
|
|
326
352
|
console.error(` ${prefix}`);
|
|
327
353
|
console.error(` Reason: ${d.reason} -- ${d.detail}`);
|
|
328
354
|
console.error(` Source: ${d.sourceFile}`);
|
|
@@ -38,12 +38,82 @@
|
|
|
38
38
|
|
|
39
39
|
let fenceCount = 0;
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Whether a cookie read saw the state rotate externally while the fence was
|
|
43
|
+
* up and no action's landing took it yet (navigation-store-handle.ts reports
|
|
44
|
+
* it). Almost always an action's own server-seat `invalidateClientCache()`
|
|
45
|
+
* `Set-Cookie`; a sibling tab's rotation in that window reads the same and
|
|
46
|
+
* costs one full flush, today's behavior. navigation-store.ts takes it when
|
|
47
|
+
* an action lands, so one rotation makes one landing flush, not every later
|
|
48
|
+
* landing of the batch.
|
|
49
|
+
*/
|
|
50
|
+
let rotatedInFence = false;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Whether a cookie read saw a write-only change of the state (a server
|
|
54
|
+
* action's response, this tab's or another's) while the fence was up and no
|
|
55
|
+
* action's landing took it yet. A landing marks every cache stale, so it
|
|
56
|
+
* takes this; when the fence drops with it still set (the actions that
|
|
57
|
+
* landed called keepClientCache(), failed before they were sent, or reloaded
|
|
58
|
+
* on a version mismatch), the drop hands it to `unlandedWriteHandler`.
|
|
59
|
+
*/
|
|
60
|
+
let wroteInFence = false;
|
|
61
|
+
|
|
62
|
+
let unlandedWriteHandler: (() => void) | null = null;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Register what runs when the fence drops holding a write no landing took:
|
|
66
|
+
* the navigation store's markCachesStaleAfterWrite (navigation-store-handle.ts).
|
|
67
|
+
*/
|
|
68
|
+
export function setUnlandedWriteHandler(handler: (() => void) | null): void {
|
|
69
|
+
unlandedWriteHandler = handler;
|
|
70
|
+
}
|
|
71
|
+
|
|
41
72
|
export function enterActionFence(): void {
|
|
73
|
+
if (fenceCount === 0) {
|
|
74
|
+
rotatedInFence = false;
|
|
75
|
+
wroteInFence = false;
|
|
76
|
+
}
|
|
42
77
|
fenceCount++;
|
|
43
78
|
}
|
|
44
79
|
|
|
45
80
|
export function exitActionFence(): void {
|
|
46
81
|
if (fenceCount > 0) fenceCount--;
|
|
82
|
+
if (fenceCount === 0 && wroteInFence) {
|
|
83
|
+
wroteInFence = false;
|
|
84
|
+
unlandedWriteHandler?.();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A cookie read saw an external state rotation (rango-state.ts observer). */
|
|
89
|
+
export function noteStateRotation(): void {
|
|
90
|
+
if (fenceCount > 0) rotatedInFence = true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A cookie read saw a write-only change (rango-state.ts write observer).
|
|
95
|
+
* Returns whether the fence is up and took it; with the fence down the
|
|
96
|
+
* caller marks the caches stale itself.
|
|
97
|
+
*/
|
|
98
|
+
export function noteStateWrite(): boolean {
|
|
99
|
+
if (fenceCount === 0) return false;
|
|
100
|
+
wroteInFence = true;
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** A landing marked every cache stale: a write seen so far is covered. */
|
|
105
|
+
export function takeStateWriteInActionFence(): void {
|
|
106
|
+
wroteInFence = false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Whether the state rotated externally since the fence went up or the last
|
|
111
|
+
* landing took it, and clear it. False with the fence down.
|
|
112
|
+
*/
|
|
113
|
+
export function takeStateRotationInActionFence(): boolean {
|
|
114
|
+
const rotated = fenceCount > 0 && rotatedInFence;
|
|
115
|
+
rotatedInFence = false;
|
|
116
|
+
return rotated;
|
|
47
117
|
}
|
|
48
118
|
|
|
49
119
|
export function isActionFenceActive(): boolean {
|
|
@@ -53,4 +123,6 @@ export function isActionFenceActive(): boolean {
|
|
|
53
123
|
/** Test-only: reset the refcount between cases. */
|
|
54
124
|
export function __resetActionFence(): void {
|
|
55
125
|
fenceCount = 0;
|
|
126
|
+
rotatedInFence = false;
|
|
127
|
+
wroteInFence = false;
|
|
56
128
|
}
|
|
@@ -91,31 +91,48 @@ export function getRawCookieValue(
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
|
-
* Encode a state value for the wire: `encodeURIComponent(version):timestamp
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* shape survives and `:` is a legal cookie-value octet.
|
|
94
|
+
* Encode a state value for the wire: `encodeURIComponent(version):timestamp`,
|
|
95
|
+
* plus `:write` when the write part is not 0. Only the build-derived version
|
|
96
|
+
* is encoded (it is arbitrary); the `:` separators and the numeric parts stay
|
|
97
|
+
* raw, so the shape survives and `:` is a legal cookie-value octet. A value no
|
|
98
|
+
* server action has bumped keeps the two-part shape it had before the write
|
|
99
|
+
* part existed.
|
|
98
100
|
*/
|
|
99
|
-
export function encodeStateValue(
|
|
100
|
-
|
|
101
|
+
export function encodeStateValue(
|
|
102
|
+
version: string,
|
|
103
|
+
timestamp: number,
|
|
104
|
+
write = 0,
|
|
105
|
+
): string {
|
|
106
|
+
const base = `${encodeURIComponent(version)}:${timestamp}`;
|
|
107
|
+
return write === 0 ? base : `${base}:${write}`;
|
|
101
108
|
}
|
|
102
109
|
|
|
103
|
-
/**
|
|
110
|
+
/**
|
|
111
|
+
* Parsed state value. `version` is decoded; `timestamp` (the invalidation
|
|
112
|
+
* part) and `write` are the raw integers. `write` is 0 for a two-part value.
|
|
113
|
+
*/
|
|
104
114
|
export interface StateValue {
|
|
105
115
|
version: string;
|
|
106
116
|
timestamp: number;
|
|
117
|
+
write: number;
|
|
107
118
|
}
|
|
108
119
|
|
|
109
120
|
/**
|
|
110
|
-
* Decode a wire value back into `{version, timestamp}`.
|
|
111
|
-
*
|
|
112
|
-
*
|
|
121
|
+
* Decode a wire value back into `{version, timestamp, write}`. Accepts the
|
|
122
|
+
* two-part `{version}:{timestamp}` (a value no action bumped, or one an open
|
|
123
|
+
* tab wrote before the write part existed) and the three-part
|
|
124
|
+
* `{version}:{timestamp}:{write}`. Returns null for a malformed value (no `:`,
|
|
125
|
+
* empty version, a non-numeric part, or more parts) so callers mint fresh
|
|
126
|
+
* instead of trusting garbage.
|
|
113
127
|
*/
|
|
114
128
|
export function decodeStateValue(raw: string): StateValue | null {
|
|
115
129
|
const colon = raw.indexOf(":");
|
|
116
130
|
if (colon <= 0) return null;
|
|
117
|
-
const
|
|
118
|
-
if (
|
|
131
|
+
const parts = raw.slice(colon + 1).split(":");
|
|
132
|
+
if (parts.length > 2) return null;
|
|
133
|
+
const timestamp = parseStatePart(parts[0]!);
|
|
134
|
+
const write = parts.length === 2 ? parseStatePart(parts[1]!) : 0;
|
|
135
|
+
if (timestamp === null || write === null) return null;
|
|
119
136
|
let version: string;
|
|
120
137
|
try {
|
|
121
138
|
version = decodeURIComponent(raw.slice(0, colon));
|
|
@@ -124,7 +141,31 @@ export function decodeStateValue(raw: string): StateValue | null {
|
|
|
124
141
|
// a thrown URIError here would 500 the server seat or fail client boot.
|
|
125
142
|
return null;
|
|
126
143
|
}
|
|
127
|
-
return { version, timestamp };
|
|
144
|
+
return { version, timestamp, write };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function parseStatePart(part: string): number | null {
|
|
148
|
+
if (part === "") return null;
|
|
149
|
+
const value = Number(part);
|
|
150
|
+
return Number.isFinite(value) ? value : null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The `{version}:{timestamp}` part of a wire value: what every in-memory
|
|
155
|
+
* comparison on the client reads (the prefetch cache keys, the action
|
|
156
|
+
* fence's rotation check, the external-rotation observer). A change to it is
|
|
157
|
+
* a hard invalidation (a deploy, or invalidateClientCache() on either seat);
|
|
158
|
+
* a change to the write part alone is a server action's write, which marks
|
|
159
|
+
* caches stale and flushes nothing. The full value, write part included, is
|
|
160
|
+
* what requests send as `X-Rango-State`, so the browser HTTP cache (Vary)
|
|
161
|
+
* misses after every write. A raw cut, no decode: the version is
|
|
162
|
+
* percent-encoded, so the second `:` is always the write separator.
|
|
163
|
+
*/
|
|
164
|
+
export function stateEpoch(raw: string): string {
|
|
165
|
+
const first = raw.indexOf(":");
|
|
166
|
+
if (first < 0) return raw;
|
|
167
|
+
const second = raw.indexOf(":", first + 1);
|
|
168
|
+
return second < 0 ? raw : raw.slice(0, second);
|
|
128
169
|
}
|
|
129
170
|
|
|
130
171
|
/**
|
|
@@ -132,7 +173,9 @@ export function decodeStateValue(raw: string): StateValue | null {
|
|
|
132
173
|
* one, so a re-mint inside the same millisecond (or a backward clock step) still
|
|
133
174
|
* differs from the current value. `prevRaw` is the current wire value (the
|
|
134
175
|
* client's in-memory mirror, or the server's inbound header/cookie) or null; its
|
|
135
|
-
* timestamp is the floor.
|
|
176
|
+
* timestamp is the floor. The write part starts over (left out): the new
|
|
177
|
+
* timestamp alone makes the value one no response was cached under. Shared by
|
|
178
|
+
* both seats so the monotonic guard lives once.
|
|
136
179
|
*/
|
|
137
180
|
export function mintStateValue(
|
|
138
181
|
version: string,
|
|
@@ -143,6 +186,31 @@ export function mintStateValue(
|
|
|
143
186
|
return encodeStateValue(version, ts);
|
|
144
187
|
}
|
|
145
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Bump the write part of `prevRaw`, keeping its version and timestamp: what
|
|
191
|
+
* the response of a server action without a directive sets. Keeping the
|
|
192
|
+
* version keeps the bump soft for a client still on another build (a form
|
|
193
|
+
* posted from a document served before a deploy); its own boot handles the
|
|
194
|
+
* deploy. Returns null when `prevRaw` is absent or malformed: a client with
|
|
195
|
+
* no state value sends no `X-Rango-State` and has nothing in the HTTP cache
|
|
196
|
+
* keyed by one.
|
|
197
|
+
*
|
|
198
|
+
* The new write part must be one no response was cached under. It is
|
|
199
|
+
* strictly greater than the previous one, and it is the current time in
|
|
200
|
+
* microseconds with random low digits: two actions that start from the same
|
|
201
|
+
* value (two tabs, or two in flight in one tab) can run in the same server
|
|
202
|
+
* millisecond (a Worker's clock does not move between I/O), and the second
|
|
203
|
+
* response must not set the value the first one set, under which a prefetch
|
|
204
|
+
* between them may have been cached with the second write missing.
|
|
205
|
+
*/
|
|
206
|
+
export function bumpStateWrite(prevRaw: string | null): string | null {
|
|
207
|
+
const prev = prevRaw ? decodeStateValue(prevRaw) : null;
|
|
208
|
+
if (!prev) return null;
|
|
209
|
+
const fresh = Date.now() * 1000 + Math.floor(Math.random() * 1000);
|
|
210
|
+
const write = Math.max(fresh, prev.write + 1);
|
|
211
|
+
return encodeStateValue(prev.version, prev.timestamp, write);
|
|
212
|
+
}
|
|
213
|
+
|
|
146
214
|
/**
|
|
147
215
|
* Attribute string for the state cookie. Session cookie (no Max-Age/Expires),
|
|
148
216
|
* Path=/ (whole app), SameSite=Lax (sent on top-level navigations), and Secure
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client seat of `invalidateClientCache()` (the `default` export condition).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* the history cache is marked stale (SWR), the prefetch map
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* The full client invalidation, more than a server action without a
|
|
5
|
+
* directive does: the history cache is marked stale (SWR), the prefetch map
|
|
6
|
+
* is flushed (in-flight prefetches aborted, visible links prefetch again), the
|
|
7
|
+
* state rotates, and sibling tabs are broadcast to, which adopt the new state
|
|
8
|
+
* and flush too (`invalidateCacheAndBroadcast()`). A directive-free action
|
|
9
|
+
* only marks history and prefetches stale and rotates nothing
|
|
10
|
+
* (`markCacheAsStaleAndBroadcast()`). History is marked stale, not cleared, so
|
|
11
|
+
* Back renders the cached entry instantly and revalidates.
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
14
|
import { getRegisteredStore } from "./navigation-store-handle.js";
|
|
@@ -27,7 +29,7 @@ export function invalidateClientCache(): void {
|
|
|
27
29
|
|
|
28
30
|
const store = getRegisteredStore();
|
|
29
31
|
if (store) {
|
|
30
|
-
store.
|
|
32
|
+
store.invalidateCacheAndBroadcast();
|
|
31
33
|
return;
|
|
32
34
|
}
|
|
33
35
|
|