@rangojs/router 0.18.0 → 0.19.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/types/browser/event-controller.d.ts +3 -3
- package/dist/types/browser/history-state.d.ts +2 -0
- package/dist/types/browser/navigation-transaction.d.ts +20 -2
- package/dist/types/browser/partial-update.d.ts +16 -1
- package/dist/types/browser/prefetch/cache.d.ts +6 -6
- package/dist/types/browser/react/Link.d.ts +19 -9
- package/dist/types/browser/react/context.d.ts +2 -2
- package/dist/types/browser/react/location-state-shared.d.ts +95 -56
- package/dist/types/browser/react/location-state.d.ts +1 -1
- package/dist/types/browser/transition-when.d.ts +80 -0
- package/dist/types/browser/types.d.ts +104 -9
- package/dist/types/build/generate-manifest.d.ts +8 -0
- package/dist/types/cache/background-task.d.ts +24 -2
- package/dist/types/cache/cache-exec-scope.d.ts +31 -0
- package/dist/types/cache/cache-key-utils.d.ts +44 -13
- package/dist/types/cache/cache-policy.d.ts +1 -1
- package/dist/types/cache/cache-runtime.d.ts +7 -0
- package/dist/types/cache/cache-scope.d.ts +117 -35
- package/dist/types/cache/cache-tag.d.ts +24 -4
- package/dist/types/cache/cf/cf-cache-store.d.ts +55 -44
- package/dist/types/cache/cf/cf-shell-frame.d.ts +1 -1
- package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +0 -15
- package/dist/types/cache/document-cache.d.ts +10 -0
- package/dist/types/cache/handle-snapshot.d.ts +40 -8
- package/dist/types/cache/invalidation-order.d.ts +23 -0
- package/dist/types/cache/request-tag-mask.d.ts +54 -0
- package/dist/types/cache/shell-snapshot.d.ts +88 -141
- package/dist/types/cache/tag-invalidation.d.ts +38 -14
- package/dist/types/cache/types.d.ts +68 -61
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +23 -16
- package/dist/types/client-urls/navigation.d.ts +28 -2
- package/dist/types/client-urls/server-projection.d.ts +5 -2
- package/dist/types/client-urls/types.d.ts +5 -5
- package/dist/types/client.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.rsc.d.ts +1 -1
- package/dist/types/route-definition/helpers-types.d.ts +11 -11
- package/dist/types/route-name.d.ts +7 -0
- package/dist/types/router/error-handling.d.ts +1 -1
- package/dist/types/router/intercept-resolution.d.ts +2 -0
- package/dist/types/router/match-context.d.ts +5 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
- package/dist/types/router/router-options.d.ts +4 -1
- package/dist/types/router/segment-resolution/loader-cache.d.ts +18 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +14 -40
- package/dist/types/router/segment-resolution/mask-nested.d.ts +11 -3
- package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
- package/dist/types/router/transition-when.d.ts +24 -10
- package/dist/types/rsc/attach-transition-when.d.ts +18 -0
- package/dist/types/rsc/full-payload.d.ts +14 -7
- package/dist/types/rsc/response-cache-serve.d.ts +15 -12
- package/dist/types/rsc/shell-build-manifest.d.ts +4 -3
- package/dist/types/rsc/shell-capture-constants.d.ts +77 -20
- package/dist/types/rsc/shell-capture.d.ts +78 -81
- package/dist/types/rsc/shell-serve.d.ts +19 -48
- package/dist/types/rsc/types.d.ts +2 -0
- package/dist/types/segment-system.d.ts +7 -0
- package/dist/types/server/context.d.ts +89 -61
- package/dist/types/server/cookie-store.d.ts +30 -18
- package/dist/types/server/handle-store.d.ts +52 -1
- package/dist/types/server/request-context.d.ts +66 -45
- package/dist/types/server/request-headers.d.ts +44 -0
- package/dist/types/server.d.ts +1 -0
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/index.d.ts +3 -2
- package/dist/types/testing/internal/action-id.d.ts +7 -0
- package/dist/types/testing/location-state-key.d.ts +24 -0
- package/dist/types/testing/render-route.d.ts +17 -1
- package/dist/types/testing/run-transition-when.d.ts +44 -69
- package/dist/types/testing/serve-shell-request.d.ts +7 -6
- package/dist/types/testing/shell-status.d.ts +34 -22
- package/dist/types/transition-when-ref.d.ts +79 -0
- package/dist/types/types/cache-types.d.ts +15 -3
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/segments.d.ts +86 -71
- package/dist/types/urls/path-helper-types.d.ts +3 -4
- package/dist/types/urls/pattern-types.d.ts +3 -4
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
- package/dist/vite/index.js +960 -97
- package/package.json +1 -1
- package/skills/cache-guide/SKILL.md +76 -49
- package/skills/caching/SKILL.md +167 -69
- package/skills/client-urls/SKILL.md +1 -1
- package/skills/comparison/references/framework-comparison.md +8 -5
- package/skills/document-cache/SKILL.md +24 -0
- package/skills/hooks/SKILL.md +3 -1
- package/skills/hooks/navigation.md +23 -2
- package/skills/hooks/state.md +98 -6
- package/skills/intercept/SKILL.md +29 -12
- package/skills/loader/SKILL.md +52 -21
- package/skills/middleware/SKILL.md +18 -0
- package/skills/migrate-nextjs/SKILL.md +3 -3
- package/skills/observability/SKILL.md +6 -5
- package/skills/parallel/SKILL.md +1 -1
- package/skills/ppr/SKILL.md +129 -77
- package/skills/prerender/SKILL.md +3 -3
- package/skills/rango/SKILL.md +25 -6
- package/skills/route/SKILL.md +2 -0
- package/skills/testing/SKILL.md +27 -25
- package/skills/testing/cache-prerender.md +9 -9
- package/skills/testing/client-components.md +45 -3
- package/skills/testing/loader.md +3 -2
- package/skills/testing/middleware.md +1 -0
- package/skills/testing/predicates.md +49 -49
- package/skills/testing/render-handler.md +1 -0
- package/skills/testing/serve-shell-request.md +14 -0
- package/skills/testing/server-actions.md +9 -3
- package/skills/theme/SKILL.md +8 -4
- package/skills/typesafety/env-and-bindings.md +8 -0
- package/skills/use-cache/SKILL.md +64 -19
- package/skills/view-transitions/SKILL.md +150 -29
- package/src/__augment-tests__/augmented.check.ts +23 -0
- package/src/browser/event-controller.ts +4 -4
- package/src/browser/history-state.ts +52 -1
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +77 -18
- package/src/browser/navigation-client.ts +4 -3
- package/src/browser/navigation-store.ts +43 -1
- package/src/browser/navigation-transaction.ts +31 -11
- package/src/browser/partial-update.ts +146 -35
- package/src/browser/prefetch/cache.ts +6 -6
- package/src/browser/prefetch/fetch.ts +2 -2
- package/src/browser/react/Link.tsx +38 -33
- package/src/browser/react/NavigationProvider.tsx +2 -2
- package/src/browser/react/context.ts +2 -2
- package/src/browser/react/location-state-shared.ts +241 -49
- package/src/browser/react/location-state.ts +5 -2
- package/src/browser/rsc-router.tsx +4 -0
- package/src/browser/server-action-bridge.ts +84 -15
- package/src/browser/transition-when.ts +205 -0
- package/src/browser/types.ts +124 -10
- package/src/build/generate-manifest.ts +77 -1
- package/src/cache/background-task.ts +55 -2
- package/src/cache/cache-exec-scope.ts +48 -1
- package/src/cache/cache-key-utils.ts +56 -16
- package/src/cache/cache-policy.ts +9 -4
- package/src/cache/cache-runtime.ts +197 -144
- package/src/cache/cache-scope.ts +304 -112
- package/src/cache/cache-tag.ts +105 -13
- package/src/cache/cf/cf-cache-store.ts +152 -76
- package/src/cache/cf/cf-shell-frame.ts +2 -2
- package/src/cache/cf/cf-tag-marker-memo.ts +11 -100
- package/src/cache/document-cache.ts +53 -7
- package/src/cache/handle-snapshot.ts +52 -9
- package/src/cache/invalidation-order.ts +84 -0
- package/src/cache/request-tag-mask.ts +147 -0
- package/src/cache/shell-snapshot.ts +130 -348
- package/src/cache/tag-invalidation.ts +57 -51
- package/src/cache/types.ts +71 -67
- package/src/cache/vercel/vercel-cache-store.ts +61 -58
- package/src/client-urls/client-root.tsx +8 -4
- package/src/client-urls/client-urls.ts +8 -6
- package/src/client-urls/navigation.ts +55 -4
- package/src/client-urls/server-projection.ts +31 -10
- package/src/client-urls/types.ts +12 -5
- package/src/client.tsx +2 -0
- package/src/host/cookie-handler.ts +2 -1
- package/src/index.rsc.ts +3 -0
- package/src/index.ts +3 -0
- package/src/loader-redirect.tsx +7 -3
- package/src/prerender/build-shell-capture.ts +155 -146
- package/src/route-definition/dsl-helpers.ts +2 -0
- package/src/route-definition/helpers-types.ts +11 -11
- package/src/route-name.ts +12 -0
- package/src/router/content-negotiation.ts +4 -1
- package/src/router/error-handling.ts +5 -2
- package/src/router/handler-context.ts +10 -7
- package/src/router/intercept-resolution.ts +11 -1
- package/src/router/loader-resolution.ts +28 -3
- package/src/router/logging.ts +5 -3
- package/src/router/match-api.ts +62 -34
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +9 -29
- package/src/router/match-middleware/background-revalidation.ts +3 -5
- package/src/router/match-middleware/cache-lookup.ts +35 -19
- package/src/router/match-middleware/cache-store.ts +15 -9
- package/src/router/match-result.ts +1 -0
- package/src/router/middleware.ts +2 -5
- package/src/router/navigation-snapshot.ts +6 -5
- package/src/router/prerender-match.ts +4 -0
- package/src/router/request-classification.ts +7 -4
- package/src/router/router-options.ts +4 -1
- package/src/router/segment-resolution/fresh.ts +0 -4
- package/src/router/segment-resolution/loader-cache.ts +150 -25
- package/src/router/segment-resolution/loader-mask.ts +3 -4
- package/src/router/segment-resolution/loader-snapshot.ts +15 -78
- package/src/router/segment-resolution/mask-nested.ts +47 -22
- package/src/router/segment-resolution/revalidation.ts +0 -3
- package/src/router/segment-resolution/view-transition-default.ts +6 -21
- package/src/router/telemetry.ts +6 -3
- package/src/router/transition-when.ts +34 -52
- package/src/router.ts +2 -1
- package/src/rsc/attach-transition-when.ts +47 -0
- package/src/rsc/full-payload.ts +30 -10
- package/src/rsc/handler.ts +12 -10
- package/src/rsc/loader-fetch.ts +2 -1
- package/src/rsc/manifest-init.ts +4 -0
- package/src/rsc/origin-guard.ts +8 -5
- package/src/rsc/progressive-enhancement.ts +7 -30
- package/src/rsc/response-cache-serve.ts +68 -80
- package/src/rsc/response-route-handler.ts +2 -2
- package/src/rsc/rsc-rendering.ts +206 -252
- package/src/rsc/server-action.ts +8 -20
- package/src/rsc/shell-build-manifest.ts +22 -20
- package/src/rsc/shell-capture-constants.ts +136 -20
- package/src/rsc/shell-capture.ts +352 -589
- package/src/rsc/shell-serve.ts +73 -109
- package/src/rsc/ssr-setup.ts +3 -2
- package/src/rsc/types.ts +2 -0
- package/src/segment-system.tsx +40 -7
- package/src/server/context.ts +189 -124
- package/src/server/cookie-store.ts +153 -127
- package/src/server/handle-store.ts +245 -27
- package/src/server/request-context.ts +123 -84
- package/src/server/request-headers.ts +134 -0
- package/src/server.ts +4 -0
- package/src/testing/dispatch.ts +4 -7
- package/src/testing/e2e/index.ts +1 -0
- package/src/testing/index.ts +5 -0
- package/src/testing/internal/action-id.ts +25 -0
- package/src/testing/internal/context.ts +1 -1
- package/src/testing/location-state-key.ts +45 -0
- package/src/testing/render-handler.ts +1 -1
- package/src/testing/render-route.tsx +109 -16
- package/src/testing/run-client-revalidate.ts +3 -19
- package/src/testing/run-loader.ts +5 -1
- package/src/testing/run-transition-when.ts +109 -172
- package/src/testing/serve-shell-request.ts +10 -7
- package/src/testing/shell-status.ts +60 -56
- package/src/transition-when-ref.ts +220 -0
- package/src/types/cache-types.ts +15 -3
- package/src/types/index.ts +3 -0
- package/src/types/segments.ts +94 -91
- package/src/urls/path-helper-types.ts +3 -4
- package/src/urls/pattern-types.ts +3 -4
- package/src/vite/discovery/discover-routers.ts +3 -0
- package/src/vite/discovery/shell-prerender-phase.ts +0 -2
- package/src/vite/discovery/virtual-module-codegen.ts +16 -6
- package/src/vite/plugins/transition-when-hoist.ts +1064 -0
- package/src/vite/rango.ts +2 -0
- package/src/vite/router-discovery.ts +75 -5
- package/dist/types/rsc/transition-gate.d.ts +0 -23
- package/src/rsc/transition-gate.ts +0 -86
|
@@ -30,22 +30,27 @@
|
|
|
30
30
|
* Invariant: the mask only turns the invalidating request's hits into misses.
|
|
31
31
|
* If the durable write then fails, that request paid extra misses, never a
|
|
32
32
|
* stale read, and other requests read the durable state. Both also record
|
|
33
|
-
* the tags
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* entry (#980), for this gate as for eviction. Not covered (#977): such an
|
|
38
|
-
* execution in another request, or a loader cache() write, still lands, and
|
|
39
|
-
* the store accepts it (docs/design/caching.md "Read-your-own-writes").
|
|
33
|
+
* the tags in the isolate's invalidation order (markInvalidated), so a cache
|
|
34
|
+
* execution that started before one of its tags was invalidated, in any
|
|
35
|
+
* request, is neither joined nor written (#973, #977): see
|
|
36
|
+
* predatesInvalidation, and docs/design/caching.md "Read-your-own-writes".
|
|
40
37
|
*/
|
|
41
38
|
|
|
42
|
-
import {
|
|
43
|
-
_getRequestContext,
|
|
44
|
-
type RequestContext,
|
|
45
|
-
} from "../server/request-context.js";
|
|
39
|
+
import { _getRequestContext } from "../server/request-context.js";
|
|
46
40
|
import { reportingAsync } from "./cache-error.js";
|
|
47
41
|
import { normalizeTags } from "./cache-tag.js";
|
|
48
42
|
import type { SegmentCacheStore } from "./types.js";
|
|
43
|
+
import {
|
|
44
|
+
invalidatedSince,
|
|
45
|
+
markInvalidated,
|
|
46
|
+
type ExecutionStart,
|
|
47
|
+
} from "./invalidation-order.js";
|
|
48
|
+
|
|
49
|
+
export {
|
|
50
|
+
executionStart,
|
|
51
|
+
invalidatedSince,
|
|
52
|
+
type ExecutionStart,
|
|
53
|
+
} from "./invalidation-order.js";
|
|
49
54
|
|
|
50
55
|
/**
|
|
51
56
|
* Collect every store that may hold entries tagged for this request's handler:
|
|
@@ -139,48 +144,49 @@ function markFreshReads(stores: SegmentCacheStore[]): void {
|
|
|
139
144
|
}
|
|
140
145
|
|
|
141
146
|
/**
|
|
142
|
-
* The
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
147
|
+
* The write gate of every cache execution (#977): whether one of `tags` was
|
|
148
|
+
* invalidated after the execution that produced the value started.
|
|
149
|
+
*
|
|
150
|
+
* Every built-in store stamps an entry when it is written (CFCacheStore's
|
|
151
|
+
* `taggedAt`, VercelCacheStore's `ta`) or checks nothing on write
|
|
152
|
+
* (MemorySegmentCacheStore), so a value read before an invalidation and
|
|
153
|
+
* written after it would be served as newer than the invalidation until it
|
|
154
|
+
* expires. Two answers, either one skips the write:
|
|
155
|
+
*
|
|
156
|
+
* - this isolate's order (invalidatedSince), which sees every request's
|
|
157
|
+
* updateTag()/revalidateTag() here, the calling one included;
|
|
158
|
+
* - the store's markers (isTagsInvalidatedSince), for another isolate's
|
|
159
|
+
* invalidation, as far as the store can tell: KV markers on CFCacheStore
|
|
160
|
+
* (KV-less, only this request's own), `tm` markers on VercelCacheStore,
|
|
161
|
+
* the process markers on MemorySegmentCacheStore. Asked about the
|
|
162
|
+
* milliseconds after the start's: this isolate's own invalidation that
|
|
163
|
+
* preceded the start usually shares its millisecond (Workers freeze
|
|
164
|
+
* Date.now() between I/O), and `>=` would skip every write started right
|
|
165
|
+
* after it. Another isolate's invalidation in the start's own millisecond
|
|
166
|
+
* is not caught. Asked fail-closed: a marker the store cannot read
|
|
167
|
+
* counts as an invalidation.
|
|
168
|
+
*
|
|
169
|
+
* The order is read again once the store answers: its read can be in flight
|
|
170
|
+
* while this isolate invalidates (an action awaits a "use cache" miss, whose
|
|
171
|
+
* write waits on the read, then runs updateTag()), and it then answers with
|
|
172
|
+
* the markers from before. markInvalidated is synchronous, so this second
|
|
173
|
+
* look sees every invalidation made before the answer.
|
|
174
|
+
*
|
|
175
|
+
* Invariant: a skipped write only costs a later miss, never a stale read.
|
|
176
|
+
* The caller still returns the value it computed.
|
|
158
177
|
*/
|
|
159
|
-
export function
|
|
160
|
-
|
|
178
|
+
export async function predatesInvalidation(
|
|
179
|
+
store: SegmentCacheStore,
|
|
161
180
|
tags: readonly string[] | undefined,
|
|
162
|
-
|
|
163
|
-
): boolean {
|
|
164
|
-
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
return
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Record the invalidation on the request (RequestContext._tagInvalidations,
|
|
175
|
-
* on the root so derived contexts share it), before any store sees it.
|
|
176
|
-
*/
|
|
177
|
-
function markInvalidated(tags: readonly string[]): void {
|
|
178
|
-
const ctx = _getRequestContext();
|
|
179
|
-
if (!ctx) return;
|
|
180
|
-
const root = ctx._requestRoot ?? ctx;
|
|
181
|
-
const at = ++cacheSeq;
|
|
182
|
-
const record = (root._tagInvalidations ??= new Map());
|
|
183
|
-
for (const tag of tags) record.set(tag, at);
|
|
181
|
+
start: ExecutionStart,
|
|
182
|
+
): Promise<boolean> {
|
|
183
|
+
if (!tags || tags.length === 0) return false;
|
|
184
|
+
if (invalidatedSince(tags, start.seq)) return true;
|
|
185
|
+
if (!store.isTagsInvalidatedSince) return false;
|
|
186
|
+
const marked = await store.isTagsInvalidatedSince([...tags], start.at + 1, {
|
|
187
|
+
failClosed: true,
|
|
188
|
+
});
|
|
189
|
+
return marked || invalidatedSince(tags, start.seq);
|
|
184
190
|
}
|
|
185
191
|
|
|
186
192
|
async function invalidateAcross(
|
package/src/cache/types.ts
CHANGED
|
@@ -75,6 +75,12 @@ export interface SegmentCacheStore<TEnv = unknown> {
|
|
|
75
75
|
* 2. Store-level `keyGenerator` (modifies default key)
|
|
76
76
|
* 3. Default key generation (prefix:pathname:params)
|
|
77
77
|
*
|
|
78
|
+
* Return `defaultKey` unchanged to leave a request unpartitioned. An empty
|
|
79
|
+
* string is not a key: where the result partitions a nested cache() on
|
|
80
|
+
* another store, or a ppr shell, it fails key resolution, so the request
|
|
81
|
+
* renders uncached (as when the keyGenerator throws), and the router warns
|
|
82
|
+
* once naming the store.
|
|
83
|
+
*
|
|
78
84
|
* @example Using headers for cache segmentation
|
|
79
85
|
* ```typescript
|
|
80
86
|
* keyGenerator: (ctx, defaultKey) => {
|
|
@@ -327,12 +333,27 @@ export interface SegmentCacheStore<TEnv = unknown> {
|
|
|
327
333
|
* freshness). Consulted by build-shell read-through and runtime shell stores:
|
|
328
334
|
* "was it evicted" is answered by tag markers against the entry's createdAt,
|
|
329
335
|
* including when invalidation races a capture that has not been written yet.
|
|
336
|
+
* Also the store's half of every cache write's gate (#977,
|
|
337
|
+
* tag-invalidation.ts predatesInvalidation): a "use cache", loader cache(),
|
|
338
|
+
* route cache() or document-cache write whose execution started before an
|
|
339
|
+
* invalidation another isolate made is skipped when this answers true, so
|
|
340
|
+
* it must reflect invalidations made after this request started reading
|
|
341
|
+
* (not a value memoized earlier in the request).
|
|
330
342
|
* Optional: without it, TAGGED build entries are not served (untagged ones
|
|
331
|
-
* are unaffected — they are evictable only by deploy/buildVersion anyway)
|
|
343
|
+
* are unaffected — they are evictable only by deploy/buildVersion anyway),
|
|
344
|
+
* and writes are gated by this isolate's invalidations only.
|
|
332
345
|
* Fail open to `false` on marker-read errors: a transient store fault must
|
|
333
346
|
* degrade to "still valid", the same posture as the envelope tag checks.
|
|
334
|
-
|
|
335
|
-
|
|
347
|
+
* The write gate passes `{ failClosed: true }` instead: a marker it cannot
|
|
348
|
+
* read (an error, a timed-out read) answers `true`, since a skipped write
|
|
349
|
+
* only costs a later miss. A store that ignores the option still works;
|
|
350
|
+
* its fault then lets that write through.
|
|
351
|
+
*/
|
|
352
|
+
isTagsInvalidatedSince?(
|
|
353
|
+
tags: string[],
|
|
354
|
+
sinceMs: number,
|
|
355
|
+
options?: { failClosed?: boolean },
|
|
356
|
+
): Promise<boolean>;
|
|
336
357
|
}
|
|
337
358
|
|
|
338
359
|
/**
|
|
@@ -399,11 +420,10 @@ export interface ShellCacheEntry {
|
|
|
399
420
|
* on dev RSC-module edits). The second read-time gate: a persistent shared
|
|
400
421
|
* store (KV/runtime-cache) survives deploys, and an app-code change that
|
|
401
422
|
* keeps the same React version would otherwise leave a stale-build
|
|
402
|
-
* prelude+postponed live under the same key.
|
|
403
|
-
*
|
|
404
|
-
* recapture re-stamps them (pre-release, no compat shim).
|
|
423
|
+
* prelude+postponed live under the same key. A custom store returns it as
|
|
424
|
+
* putShell received it.
|
|
405
425
|
*/
|
|
406
|
-
buildVersion
|
|
426
|
+
buildVersion: string;
|
|
407
427
|
/**
|
|
408
428
|
* The initialTheme the CAPTURE render was built with: the no-cookie default
|
|
409
429
|
* (payloadInitialTheme, rsc/full-payload.ts), never the capturing visitor's
|
|
@@ -418,22 +438,20 @@ export interface ShellCacheEntry {
|
|
|
418
438
|
*/
|
|
419
439
|
initialTheme?: string;
|
|
420
440
|
/**
|
|
421
|
-
* The CAPTURE DATA SNAPSHOT
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
* shell-baked cached value that drifted mismatches the prelude). Recapture
|
|
431
|
-
* heals it. See docs/design/ppr-shell-resume.md ("the capture data snapshot").
|
|
441
|
+
* The CAPTURE DATA SNAPSHOT, in stored/serialized form: the doc segment
|
|
442
|
+
* record every HIT replays the handler layer from (`docKey`) and the
|
|
443
|
+
* bake-lane loader pins (pruned to what a HIT reads, issue #941). Replaying
|
|
444
|
+
* them on a HIT keeps the freshly rendered hydration payload equal to the
|
|
445
|
+
* frozen prelude after the underlying cache entries have drifted; every
|
|
446
|
+
* other read on a HIT, a hole's included, reads the store. Empty when the capture
|
|
447
|
+
* recorded nothing (a prerender-served capture without bake-lane loaders,
|
|
448
|
+
* a tombstone). A custom store returns it as putShell received it. See
|
|
449
|
+
* docs/design/ppr-shell-resume.md ("the capture data snapshot").
|
|
432
450
|
*/
|
|
433
|
-
snapshot
|
|
451
|
+
snapshot: ShellSnapshotRecord[];
|
|
434
452
|
/**
|
|
435
453
|
* Records the capture dropped from `snapshot` because no reader of this
|
|
436
|
-
* entry consumes them, by family (`
|
|
454
|
+
* entry consumes them, by family (`loader:1`). Diagnostic only: the
|
|
437
455
|
* HIT tail timing reports it next to the kept records. See
|
|
438
456
|
* docs/design/shell-entry-layout.md ("Record only what a HIT reads").
|
|
439
457
|
*/
|
|
@@ -459,6 +477,13 @@ export interface ShellCacheEntry {
|
|
|
459
477
|
createdAt: number;
|
|
460
478
|
}
|
|
461
479
|
|
|
480
|
+
/**
|
|
481
|
+
* @internal A shell entry without its prelude and snapshot: what a
|
|
482
|
+
* prelude-first read (SegmentCacheStore.readShellDocument) resolves with
|
|
483
|
+
* before the snapshot bytes arrive.
|
|
484
|
+
*/
|
|
485
|
+
export type ShellEntryHead = Omit<ShellCacheEntry, "prelude" | "snapshot">;
|
|
486
|
+
|
|
462
487
|
/**
|
|
463
488
|
* A shell entry whose document half is present — what the document HIT path
|
|
464
489
|
* (serveShellHit / lookupBuildShell) consumes. hasIntactShellPayload
|
|
@@ -493,7 +518,7 @@ export type ShellSnapshotFailure = "unavailable" | "corrupt";
|
|
|
493
518
|
*/
|
|
494
519
|
export interface ShellDocumentRead {
|
|
495
520
|
/** The entry without its prelude and snapshot (delivered separately). */
|
|
496
|
-
entry:
|
|
521
|
+
entry: ShellEntryHead;
|
|
497
522
|
/** Raw prelude bytes (empty for a navigationOnly entry). */
|
|
498
523
|
prelude: Uint8Array;
|
|
499
524
|
shouldRevalidate?: boolean;
|
|
@@ -580,14 +605,13 @@ export interface ShellReadStats {
|
|
|
580
605
|
}
|
|
581
606
|
|
|
582
607
|
/**
|
|
583
|
-
* The families a shell snapshot pins
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
* it would be self-referential.
|
|
608
|
+
* The families a shell snapshot pins: the doc segment record (recorded by
|
|
609
|
+
* RecordingShellStore) and the settled CONTAINER of each bake-lane loader
|
|
610
|
+
* (lane rule: see resolveLoaderData, loader-cache.ts). No cache read is
|
|
611
|
+
* pinned: a HIT's holes, and a bake-lane loader body that runs on the HIT,
|
|
612
|
+
* read the store.
|
|
589
613
|
*/
|
|
590
|
-
export type ShellSnapshotFamily = "
|
|
614
|
+
export type ShellSnapshotFamily = "segment" | "loader";
|
|
591
615
|
|
|
592
616
|
/**
|
|
593
617
|
* The stored form of a loader-family snapshot value: the bake-lane loader's
|
|
@@ -604,57 +628,35 @@ export interface ShellSnapshotLoaderValue {
|
|
|
604
628
|
* container carries hole markers, so a HIT must gate the overlay on the
|
|
605
629
|
* fresh run (only the loader body can mint the live nested promises);
|
|
606
630
|
* 0 = fully pinned, so a HIT resolves the payload promise immediately from
|
|
607
|
-
* the pin and does not run the loader body (unless `runs`).
|
|
608
|
-
* pre-bit snapshots, which keep the gated path.
|
|
631
|
+
* the pin and does not run the loader body (unless `runs`).
|
|
609
632
|
*/
|
|
610
|
-
holes
|
|
633
|
+
holes: 0 | 1;
|
|
611
634
|
/**
|
|
612
635
|
* 1 when the capture saw a loader push it could not record (a deferred
|
|
613
636
|
* push, one with masked nested promises, or one made outside any loader
|
|
614
637
|
* body): a HIT then still runs the loader body in the background
|
|
615
638
|
* (pin-first) so those pushes reach the page. 0: a hole-free record is
|
|
616
|
-
* served from the pin alone and its body does not run on a HIT.
|
|
617
|
-
*
|
|
618
|
-
*
|
|
639
|
+
* served from the pin alone and its body does not run on a HIT.
|
|
640
|
+
*
|
|
641
|
+
* Both bits are always written. A record stored before they existed (v0.17)
|
|
642
|
+
* reads each missing bit as 1 (buildShellLoaderSeed): its snapshot lacks
|
|
643
|
+
* the loader-owned pushes, so the body supplies them.
|
|
619
644
|
*/
|
|
620
|
-
runs
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
/** A serialized cached Response for the response family of a shell snapshot. */
|
|
624
|
-
export interface ShellSnapshotResponseValue {
|
|
625
|
-
status: number;
|
|
626
|
-
/** Client-facing header pairs (per-client signal headers excluded at record). */
|
|
627
|
-
headers: [string, string][];
|
|
628
|
-
/** base64-encoded response body (binary-safe, JSON-serializable). */
|
|
629
|
-
body: string;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
/** The stored form of an item-family (use cache / loader cache) snapshot value. */
|
|
633
|
-
export interface ShellSnapshotItemValue {
|
|
634
|
-
/** RSC-serialized return value. */
|
|
635
|
-
value: string;
|
|
636
|
-
/** RSC-encoded handle data, if any. */
|
|
637
|
-
handles?: string;
|
|
638
|
-
/** The entry's cache tags. */
|
|
639
|
-
tags?: string[];
|
|
645
|
+
runs: 0 | 1;
|
|
640
646
|
}
|
|
641
647
|
|
|
642
648
|
/**
|
|
643
|
-
* One
|
|
644
|
-
* carries
|
|
645
|
-
* JSON-serializing store (KV, CF, Vercel) with the rest of the
|
|
646
|
-
*
|
|
649
|
+
* One snapshot record: the doc segment record or a bake-lane loader pin.
|
|
650
|
+
* `value` carries it in its stored/serialized shape so it round-trips
|
|
651
|
+
* through a JSON-serializing store (KV, CF, Vercel) with the rest of the
|
|
652
|
+
* ShellCacheEntry:
|
|
647
653
|
* - `segment` -> {@link CachedEntryData} (already JSON-able)
|
|
648
|
-
* - `
|
|
654
|
+
* - `loader` -> {@link ShellSnapshotLoaderValue}
|
|
649
655
|
*/
|
|
650
656
|
export interface ShellSnapshotRecord {
|
|
651
657
|
family: ShellSnapshotFamily;
|
|
652
658
|
key: string;
|
|
653
|
-
value:
|
|
654
|
-
| ShellSnapshotItemValue
|
|
655
|
-
| CachedEntryData
|
|
656
|
-
| ShellSnapshotResponseValue
|
|
657
|
-
| ShellSnapshotLoaderValue;
|
|
659
|
+
value: CachedEntryData | ShellSnapshotLoaderValue;
|
|
658
660
|
}
|
|
659
661
|
|
|
660
662
|
/**
|
|
@@ -759,8 +761,10 @@ export type SegmentHandleData = Record<string, unknown[]>;
|
|
|
759
761
|
* index (null: not owned). Only a PPR shell capture writes owners: the
|
|
760
762
|
* settled pushes of the loader bodies it ran (an `ssr: false` loader's own,
|
|
761
763
|
* the loaders it awaits, and its own cache() replays), which the record keeps
|
|
762
|
-
* because the prelude rendered them.
|
|
763
|
-
* through HandleStore.
|
|
764
|
-
*
|
|
764
|
+
* because the prelude rendered them. A document HIT restores a bake-lane
|
|
765
|
+
* owner's value through HandleStore.pushRestored, and it stands against a run
|
|
766
|
+
* of its loader there; a live-lane owner's (a hole), and every owner's on
|
|
767
|
+
* any other replay of the record (a client navigation), go through
|
|
768
|
+
* pushReplayed, so a run of the loader replaces it with its live push.
|
|
765
769
|
*/
|
|
766
770
|
export type HandleOwners = Record<string, Record<string, (string | null)[]>>;
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* far smaller: Vercel's Runtime Cache already IS a distributed, tag-aware cache
|
|
7
7
|
* (regional storage, global tag-expire within ~300ms), so none of CFCacheStore's
|
|
8
8
|
* L1/L2 tiering or per-tier timeout budgets are needed here - the platform does
|
|
9
|
-
* that work. Tag markers
|
|
10
|
-
*
|
|
9
|
+
* that work. Tag markers (`tm`, see isTagsInvalidatedSince) gate shell reads
|
|
10
|
+
* and writes and every cache write that started before an invalidation
|
|
11
|
+
* (#977); only PPR shell reads memoize them. What this
|
|
11
12
|
* store adds on top of the raw primitive is the parts Vercel does NOT give us:
|
|
12
13
|
*
|
|
13
14
|
* 1. Stale-while-revalidate. `getCache` has no stale-but-serve: a TTL'd entry
|
|
@@ -76,9 +77,10 @@ import {
|
|
|
76
77
|
base64ToBytes,
|
|
77
78
|
} from "../cf/cf-base64.js";
|
|
78
79
|
import {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
gateMarkerRead,
|
|
81
|
+
maskRequestTags,
|
|
82
|
+
maskedForRequest,
|
|
83
|
+
} from "../request-tag-mask.js";
|
|
82
84
|
import {
|
|
83
85
|
ShellMemo,
|
|
84
86
|
freshReadsWindowMs,
|
|
@@ -170,8 +172,9 @@ type CacheFamily = "s" | "i" | "r" | "h" | "tm";
|
|
|
170
172
|
|
|
171
173
|
/**
|
|
172
174
|
* TTL for tag-invalidation marker entries ("tm" family), written by
|
|
173
|
-
* invalidateTags for the build-shell read-through
|
|
174
|
-
* gate
|
|
175
|
+
* invalidateTags for isTagsInvalidatedSince (the build-shell read-through,
|
|
176
|
+
* the shell write gate, and the write gate of cache executions, #977).
|
|
177
|
+
* The platform's expireTag() DELETES tagged entries (no queryable
|
|
175
178
|
* history), so the markers are rango's own record of "tag X was invalidated
|
|
176
179
|
* at T". One year: runtime tagged-shell retention is capped to this lifetime,
|
|
177
180
|
* while buildVersion retires build shells on the next deploy. An expired marker
|
|
@@ -237,7 +240,7 @@ interface VercelShellEnvelope {
|
|
|
237
240
|
/** React.version at capture. */
|
|
238
241
|
rv: string;
|
|
239
242
|
/** Build version at capture (ShellCacheEntry.buildVersion). */
|
|
240
|
-
bv
|
|
243
|
+
bv: string;
|
|
241
244
|
/** Capture-generation start time, used by tag marker checks. */
|
|
242
245
|
c: number;
|
|
243
246
|
/** staleAt (ms since epoch). */
|
|
@@ -248,8 +251,8 @@ interface VercelShellEnvelope {
|
|
|
248
251
|
t?: string[];
|
|
249
252
|
/** initialTheme the capture render was built with (resume theme fidelity). */
|
|
250
253
|
i?: string;
|
|
251
|
-
/** Capture data snapshot
|
|
252
|
-
sn
|
|
254
|
+
/** Capture data snapshot (ShellCacheEntry.snapshot). */
|
|
255
|
+
sn: ShellSnapshotRecord[];
|
|
253
256
|
/**
|
|
254
257
|
* ShellCacheEntry.docKey. Must round-trip: navigation-replay eligibility
|
|
255
258
|
* requires the exact canonical doc segment record named here — dropping the
|
|
@@ -417,18 +420,6 @@ function handleMemos(cache: VercelRuntimeCache): VercelHandleMemos {
|
|
|
417
420
|
return memos;
|
|
418
421
|
}
|
|
419
422
|
|
|
420
|
-
/**
|
|
421
|
-
* The tags a request invalidated through a store, with their invalidatedAt:
|
|
422
|
-
* request root context (RequestContext._requestRoot, so contexts derived
|
|
423
|
-
* from the request share it) -> store -> tag -> ms (#973). invalidateTags()
|
|
424
|
-
* writes it before its first await, and every read in that request treats an entry
|
|
425
|
-
* carrying one of the tags, written at or before that time, as invalidated.
|
|
426
|
-
* The platform's expireTag() and the tm marker writes are what other
|
|
427
|
-
* requests see, and revalidateTag() does not wait for them.
|
|
428
|
-
*/
|
|
429
|
-
const requestInvalidations: PerRequestStoreMap<Map<string, number>> =
|
|
430
|
-
new WeakMap();
|
|
431
|
-
|
|
432
423
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
433
424
|
return typeof value === "object" && value !== null;
|
|
434
425
|
}
|
|
@@ -1217,8 +1208,10 @@ export class VercelCacheStore<
|
|
|
1217
1208
|
* fresh values are used as is, stale ones are used and refreshed in the
|
|
1218
1209
|
* background, older ones block on the runtime-cache read. A request
|
|
1219
1210
|
* carrying the fresh-reads cookie skips the memo. A failed read rejects and
|
|
1220
|
-
* is never memoized. The shell write gate and
|
|
1221
|
-
* (isTagsInvalidatedSince) read the runtime cache
|
|
1211
|
+
* is never memoized. The shell write gate, build shells and the data
|
|
1212
|
+
* write gates (isTagsInvalidatedSince) read the runtime cache each time
|
|
1213
|
+
* they ask; gates of a request asking while a read is in flight share it
|
|
1214
|
+
* (gateMarkerRead), and nothing is kept once it settles.
|
|
1222
1215
|
*/
|
|
1223
1216
|
private async readTagMarker(
|
|
1224
1217
|
tag: string,
|
|
@@ -1314,16 +1307,38 @@ export class VercelCacheStore<
|
|
|
1314
1307
|
// --- Tags ---
|
|
1315
1308
|
|
|
1316
1309
|
/**
|
|
1317
|
-
*
|
|
1318
|
-
*
|
|
1319
|
-
*
|
|
1320
|
-
*
|
|
1310
|
+
* SegmentCacheStore.isTagsInvalidatedSince. expireTag() keeps no queryable
|
|
1311
|
+
* history, so invalidateTags() writes "tm" markers for runtime capture
|
|
1312
|
+
* races, immutable build shells and data writes that started before an
|
|
1313
|
+
* invalidation (#977, tag-invalidation.ts predatesInvalidation). Marker >=
|
|
1314
|
+
* generation start wins. Read errors fail open like the CF marker check,
|
|
1315
|
+
* except for the write gate (`failClosed`), which counts them as an
|
|
1316
|
+
* invalidation. Each tag's marker is read through gateMarkerRead
|
|
1317
|
+
* (request-tag-mask.ts): gates asking while a read is in flight share it.
|
|
1321
1318
|
*/
|
|
1322
1319
|
async isTagsInvalidatedSince(
|
|
1323
1320
|
tags: string[],
|
|
1324
1321
|
sinceMs: number,
|
|
1322
|
+
options?: { failClosed?: boolean },
|
|
1325
1323
|
): Promise<boolean> {
|
|
1326
|
-
|
|
1324
|
+
if (this.maskedForRequest(tags, sinceMs)) return true;
|
|
1325
|
+
const ctx = _getRequestContext();
|
|
1326
|
+
const reads = new Map(
|
|
1327
|
+
tags.map((tag) => [
|
|
1328
|
+
tag,
|
|
1329
|
+
gateMarkerRead(ctx, this, tag, sinceMs, () =>
|
|
1330
|
+
this.readTagMarker(tag, false),
|
|
1331
|
+
),
|
|
1332
|
+
]),
|
|
1333
|
+
);
|
|
1334
|
+
return this.tagsInvalidatedSince(
|
|
1335
|
+
tags,
|
|
1336
|
+
sinceMs,
|
|
1337
|
+
false,
|
|
1338
|
+
reads,
|
|
1339
|
+
undefined,
|
|
1340
|
+
options?.failClosed === true,
|
|
1341
|
+
);
|
|
1327
1342
|
}
|
|
1328
1343
|
|
|
1329
1344
|
private async tagsInvalidatedSince(
|
|
@@ -1332,6 +1347,8 @@ export class VercelCacheStore<
|
|
|
1332
1347
|
isolateMemo: boolean,
|
|
1333
1348
|
prefetched?: Map<string, Promise<number | null>>,
|
|
1334
1349
|
outcomes?: Map<string, MarkerMemoOutcome>,
|
|
1350
|
+
/** What a failed marker read answers: true for the write gate (#977). */
|
|
1351
|
+
failClosed = false,
|
|
1335
1352
|
): Promise<boolean> {
|
|
1336
1353
|
if (this.maskedForRequest(tags, sinceMs)) return true;
|
|
1337
1354
|
try {
|
|
@@ -1342,6 +1359,9 @@ export class VercelCacheStore<
|
|
|
1342
1359
|
this.readTagMarker(tag, isolateMemo, outcomes),
|
|
1343
1360
|
),
|
|
1344
1361
|
);
|
|
1362
|
+
// A read in flight when this request invalidated one of the tags
|
|
1363
|
+
// resolved to the marker before it.
|
|
1364
|
+
if (this.maskedForRequest(tags, sinceMs)) return true;
|
|
1345
1365
|
for (const at of markers) {
|
|
1346
1366
|
if (at !== null && at >= sinceMs) return true;
|
|
1347
1367
|
}
|
|
@@ -1352,7 +1372,7 @@ export class VercelCacheStore<
|
|
|
1352
1372
|
"cache-read",
|
|
1353
1373
|
"[VercelCacheStore] tag invalidation check",
|
|
1354
1374
|
);
|
|
1355
|
-
return
|
|
1375
|
+
return failClosed;
|
|
1356
1376
|
}
|
|
1357
1377
|
}
|
|
1358
1378
|
|
|
@@ -1408,42 +1428,24 @@ export class VercelCacheStore<
|
|
|
1408
1428
|
|
|
1409
1429
|
/**
|
|
1410
1430
|
* The request-local half of invalidateTags(), run before its first await
|
|
1411
|
-
* (#973):
|
|
1412
|
-
*
|
|
1431
|
+
* (#973): the request masks the tags (request-tag-mask.ts). The platform's
|
|
1432
|
+
* expireTag() and the tm marker writes are what other requests see, and
|
|
1433
|
+
* revalidateTag() does not wait for them.
|
|
1413
1434
|
*/
|
|
1414
1435
|
private maskTagsForRequest(tags: string[], at: number): void {
|
|
1415
1436
|
const ctx = _getRequestContext();
|
|
1416
|
-
if (
|
|
1417
|
-
const mask = perRequestStoreValue(
|
|
1418
|
-
requestInvalidations,
|
|
1419
|
-
ctx._requestRoot ?? ctx,
|
|
1420
|
-
this,
|
|
1421
|
-
() => new Map<string, number>(),
|
|
1422
|
-
);
|
|
1423
|
-
for (const tag of tags) mask.set(tag, at);
|
|
1437
|
+
if (ctx) maskRequestTags(ctx, this, tags, at);
|
|
1424
1438
|
}
|
|
1425
1439
|
|
|
1426
1440
|
/**
|
|
1427
|
-
* Whether this request
|
|
1428
|
-
*
|
|
1429
|
-
* invalidation's millisecond may hold a value computed before it, so it
|
|
1430
|
-
* misses (a false miss, never a stale read). An entry without a stamp was
|
|
1431
|
-
* written before the stamp existed and counts as older.
|
|
1441
|
+
* Whether this request masked one of `tags` through this store at or after
|
|
1442
|
+
* `taggedAt`. An entry without a stamp counts as older than any mask.
|
|
1432
1443
|
*/
|
|
1433
1444
|
private maskedForRequest(
|
|
1434
1445
|
tags: string[] | undefined,
|
|
1435
1446
|
taggedAt: number | undefined,
|
|
1436
1447
|
): boolean {
|
|
1437
|
-
|
|
1438
|
-
const ctx = _getRequestContext();
|
|
1439
|
-
const mask =
|
|
1440
|
-
ctx && requestInvalidations.get(ctx._requestRoot ?? ctx)?.get(this);
|
|
1441
|
-
if (!mask) return false;
|
|
1442
|
-
for (const tag of tags) {
|
|
1443
|
-
const at = mask.get(tag);
|
|
1444
|
-
if (at !== undefined && at >= (taggedAt ?? 0)) return true;
|
|
1445
|
-
}
|
|
1446
|
-
return false;
|
|
1448
|
+
return maskedForRequest(_getRequestContext(), this, tags, taggedAt ?? 0);
|
|
1447
1449
|
}
|
|
1448
1450
|
|
|
1449
1451
|
/** A data read's mask check: reports the miss and returns true when masked. */
|
|
@@ -1700,7 +1702,7 @@ export class VercelCacheStore<
|
|
|
1700
1702
|
private asShellEnvelope(raw: unknown): VercelShellEnvelope | null {
|
|
1701
1703
|
if (!isRecord(raw)) return null;
|
|
1702
1704
|
const { p, po, rv, bv, c, s, e, t, i, sn, dk, pr, no } = raw;
|
|
1703
|
-
if (typeof rv !== "string") return null;
|
|
1705
|
+
if (typeof rv !== "string" || typeof bv !== "string") return null;
|
|
1704
1706
|
// Document half: required unless navigationOnly (`no`), which stores
|
|
1705
1707
|
// neither field. Tolerate a legacy navigationOnly envelope that still
|
|
1706
1708
|
// carries them.
|
|
@@ -1716,13 +1718,14 @@ export class VercelCacheStore<
|
|
|
1716
1718
|
...(typeof p === "string" ? { p } : {}),
|
|
1717
1719
|
...(po !== undefined ? { po: po as string | null } : {}),
|
|
1718
1720
|
rv,
|
|
1719
|
-
bv
|
|
1721
|
+
bv,
|
|
1720
1722
|
c,
|
|
1721
1723
|
s,
|
|
1722
1724
|
e,
|
|
1723
1725
|
t: Array.isArray(t) ? (t as string[]) : undefined,
|
|
1724
1726
|
i: typeof i === "string" ? i : undefined,
|
|
1725
|
-
|
|
1727
|
+
// A non-array field reads as no records.
|
|
1728
|
+
sn: Array.isArray(sn) ? (sn as ShellSnapshotRecord[]) : [],
|
|
1726
1729
|
dk: typeof dk === "string" ? dk : undefined,
|
|
1727
1730
|
pr: typeof pr === "string" ? pr : undefined,
|
|
1728
1731
|
no: no === true ? true : undefined,
|
|
@@ -125,8 +125,9 @@ export function ClientUrlsRoot({
|
|
|
125
125
|
const mount = useMount();
|
|
126
126
|
const [intent, setIntent] = useState<ClientUrlNavigationIntent | null>(null);
|
|
127
127
|
useEffect(
|
|
128
|
-
() =>
|
|
129
|
-
|
|
128
|
+
() =>
|
|
129
|
+
registerClientUrlGroup(definition, mount, namePrefix, setIntent, routeId),
|
|
130
|
+
[definition, mount, namePrefix, routeId],
|
|
130
131
|
);
|
|
131
132
|
|
|
132
133
|
// Optimistic destination (design: docs/design/client-urls-optimistic-destination.md).
|
|
@@ -136,9 +137,12 @@ export function ClientUrlsRoot({
|
|
|
136
137
|
// own keeps the previous content visible (React's transition hold — the
|
|
137
138
|
// pre-existing contract for routes without loading()), one with loading()
|
|
138
139
|
// or inline <Suspense> at its reads presents immediately. Same-route intents
|
|
139
|
-
// never swap: held data + transition() own that case.
|
|
140
|
+
// never swap: held data + transition() own that case. A navigation its
|
|
141
|
+
// transition({ when }) gated off at the swap presents urgently: no hold,
|
|
142
|
+
// no view transition, the destination's loading() streams.
|
|
140
143
|
const pending = intent !== null;
|
|
141
|
-
const
|
|
144
|
+
const deferred = useDeferredValue(intent);
|
|
145
|
+
const presented = intent?.transitionGatedOff ? intent : deferred;
|
|
142
146
|
const optimisticRoute =
|
|
143
147
|
presented && presented.routeId !== routeId
|
|
144
148
|
? findRoute(definition, presented.routeId)
|
|
@@ -29,6 +29,7 @@ const TRANSITION_CONFIG_KEYS = new Set<PropertyKey>([
|
|
|
29
29
|
"default",
|
|
30
30
|
"name",
|
|
31
31
|
"viewTransition",
|
|
32
|
+
"when",
|
|
32
33
|
]);
|
|
33
34
|
|
|
34
35
|
const PPR_NUMBER_KEYS = new Set<PropertyKey>([
|
|
@@ -431,12 +432,6 @@ function createHelpers(): ClientUrlHelpers {
|
|
|
431
432
|
);
|
|
432
433
|
}
|
|
433
434
|
for (const key of Reflect.ownKeys(config)) {
|
|
434
|
-
if (key === "when") {
|
|
435
|
-
throw new Error(
|
|
436
|
-
"clientUrls() transition() does not support `when` — the gate is a " +
|
|
437
|
-
"server-executed predicate; declare it with a server-tree transition()",
|
|
438
|
-
);
|
|
439
|
-
}
|
|
440
435
|
if (!TRANSITION_CONFIG_KEYS.has(key)) {
|
|
441
436
|
throw new Error(
|
|
442
437
|
`clientUrls() transition() option ${JSON.stringify(String(key))} is not supported`,
|
|
@@ -446,6 +441,13 @@ function createHelpers(): ClientUrlHelpers {
|
|
|
446
441
|
if (config.name !== undefined && typeof config.name !== "string") {
|
|
447
442
|
throw new Error("clientUrls() transition() name must be a string");
|
|
448
443
|
}
|
|
444
|
+
if (config.when !== undefined && typeof config.when !== "function") {
|
|
445
|
+
throw new TypeError(
|
|
446
|
+
process.env.NODE_ENV !== "production"
|
|
447
|
+
? `clientUrls() transition() when must be a function (it runs in the browser), got ${config.when === null ? "null" : typeof config.when}`
|
|
448
|
+
: "clientUrls() transition() when must be a function",
|
|
449
|
+
);
|
|
450
|
+
}
|
|
449
451
|
if (
|
|
450
452
|
config.viewTransition !== undefined &&
|
|
451
453
|
config.viewTransition !== "auto" &&
|