@rangojs/router 0.17.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/README.md +3 -2
- package/dist/types/browser/cookie-name.d.ts +6 -0
- package/dist/types/browser/event-controller.d.ts +3 -3
- package/dist/types/browser/history-state.d.ts +18 -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 +48 -2
- package/dist/types/cache/cache-policy.d.ts +18 -1
- package/dist/types/cache/cache-runtime.d.ts +7 -0
- package/dist/types/cache/cache-scope.d.ts +192 -26
- package/dist/types/cache/cache-tag.d.ts +116 -11
- package/dist/types/cache/cf/cf-base64.d.ts +6 -0
- package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
- package/dist/types/cache/cf/cf-cache-store.d.ts +224 -72
- package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
- package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
- package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +19 -0
- package/dist/types/cache/document-cache.d.ts +10 -0
- package/dist/types/cache/handle-snapshot.d.ts +41 -8
- package/dist/types/cache/index.d.ts +1 -0
- package/dist/types/cache/invalidation-order.d.ts +23 -0
- package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
- package/dist/types/cache/request-tag-mask.d.ts +54 -0
- package/dist/types/cache/shell-memo.d.ts +178 -0
- package/dist/types/cache/shell-snapshot.d.ts +141 -105
- package/dist/types/cache/tag-invalidation.d.ts +54 -3
- package/dist/types/cache/types.d.ts +267 -96
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +123 -18
- 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/prerender/build-shell-capture.d.ts +11 -4
- 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/instrument.d.ts +8 -0
- 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/middleware-types.d.ts +6 -1
- package/dist/types/router/router-options.d.ts +4 -1
- package/dist/types/router/segment-resolution/loader-cache.d.ts +58 -12
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +33 -48
- package/dist/types/router/segment-resolution/mask-nested.d.ts +73 -24
- 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 +21 -0
- package/dist/types/rsc/handler.d.ts +1 -1
- package/dist/types/rsc/response-cache-serve.d.ts +15 -12
- package/dist/types/rsc/shell-build-manifest.d.ts +12 -3
- package/dist/types/rsc/shell-capture-constants.d.ts +116 -12
- package/dist/types/rsc/shell-capture.d.ts +202 -74
- package/dist/types/rsc/shell-serve.d.ts +144 -50
- package/dist/types/rsc/types.d.ts +9 -0
- package/dist/types/segment-system.d.ts +7 -0
- package/dist/types/server/context.d.ts +155 -33
- package/dist/types/server/cookie-store.d.ts +33 -0
- package/dist/types/server/handle-store.d.ts +52 -1
- package/dist/types/server/request-context.d.ts +210 -63
- package/dist/types/server/request-headers.d.ts +44 -0
- package/dist/types/server.d.ts +1 -0
- package/dist/types/ssr/index.d.ts +9 -0
- package/dist/types/ssr/ssr-root.d.ts +9 -8
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +7 -0
- 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-loader.d.ts +18 -3
- package/dist/types/testing/run-transition-when.d.ts +44 -69
- package/dist/types/testing/serve-shell-request.d.ts +133 -0
- package/dist/types/testing/shell-status.d.ts +44 -19
- package/dist/types/transition-when-ref.d.ts +79 -0
- package/dist/types/types/cache-types.d.ts +15 -3
- package/dist/types/types/handler-context.d.ts +14 -1
- 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 +21 -18
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
- package/dist/types/vite/router-discovery.d.ts +29 -2
- package/dist/vite/index.js +1005 -105
- package/package.json +1 -1
- package/skills/breadcrumbs/SKILL.md +17 -7
- package/skills/cache-guide/SKILL.md +175 -76
- package/skills/caching/SKILL.md +361 -33
- package/skills/catalog.json +1 -1
- package/skills/client-urls/SKILL.md +12 -7
- package/skills/cloudflare/SKILL.md +15 -0
- package/skills/comparison/references/framework-comparison.md +52 -37
- package/skills/defer-hydration/SKILL.md +50 -30
- package/skills/deployment-caching/SKILL.md +5 -2
- package/skills/document-cache/SKILL.md +32 -2
- package/skills/handler-use/SKILL.md +2 -0
- package/skills/hooks/SKILL.md +3 -1
- package/skills/hooks/handle-and-actions.md +3 -1
- package/skills/hooks/navigation.md +23 -2
- package/skills/hooks/state.md +101 -7
- package/skills/intercept/SKILL.md +29 -12
- package/skills/loader/SKILL.md +197 -44
- package/skills/middleware/SKILL.md +34 -9
- package/skills/migrate-nextjs/SKILL.md +68 -37
- package/skills/migrate-react-router/SKILL.md +2 -1
- package/skills/migrate-react-router/component-migration.md +3 -1
- package/skills/migrate-react-router/data-and-actions.md +3 -1
- package/skills/migrate-react-router/route-mapping.md +7 -4
- package/skills/observability/SKILL.md +65 -0
- package/skills/parallel/SKILL.md +13 -8
- package/skills/ppr/SKILL.md +715 -371
- package/skills/prerender/SKILL.md +30 -17
- package/skills/rango/SKILL.md +72 -34
- package/skills/route/SKILL.md +14 -7
- package/skills/router-setup/SKILL.md +4 -2
- package/skills/scripts/SKILL.md +8 -1
- package/skills/server-actions/SKILL.md +11 -0
- package/skills/shell-manifest/SKILL.md +21 -12
- package/skills/testing/SKILL.md +49 -43
- package/skills/testing/cache-prerender.md +15 -14
- package/skills/testing/client-components.md +45 -3
- package/skills/testing/loader.md +19 -1
- 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 +202 -0
- package/skills/testing/server-actions.md +9 -3
- package/skills/theme/SKILL.md +56 -4
- package/skills/typesafety/env-and-bindings.md +18 -2
- package/skills/use-cache/SKILL.md +91 -23
- package/skills/vercel/SKILL.md +14 -2
- package/skills/view-transitions/SKILL.md +150 -29
- package/src/__augment-tests__/augmented.check.ts +23 -0
- package/src/browser/cookie-name.ts +23 -0
- package/src/browser/event-controller.ts +4 -4
- package/src/browser/history-state.ts +75 -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 +9 -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 +62 -2
- package/src/cache/cache-policy.ts +31 -4
- package/src/cache/cache-runtime.ts +209 -129
- package/src/cache/cache-scope.ts +573 -69
- package/src/cache/cache-tag.ts +475 -19
- package/src/cache/cf/cf-base64.ts +36 -4
- package/src/cache/cf/cf-cache-constants.ts +12 -0
- package/src/cache/cf/cf-cache-store.ts +1094 -376
- package/src/cache/cf/cf-cache-types.ts +32 -4
- package/src/cache/cf/cf-shell-frame.ts +258 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +55 -40
- package/src/cache/document-cache.ts +53 -7
- package/src/cache/handle-snapshot.ts +53 -9
- package/src/cache/index.ts +2 -0
- package/src/cache/invalidation-order.ts +84 -0
- package/src/cache/isolate-tag-memo.ts +308 -0
- package/src/cache/request-tag-mask.ts +147 -0
- package/src/cache/shell-memo.ts +341 -0
- package/src/cache/shell-snapshot.ts +249 -277
- package/src/cache/tag-invalidation.ts +109 -18
- package/src/cache/types.ts +274 -102
- package/src/cache/vercel/vercel-cache-store.ts +561 -74
- 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 +173 -110
- package/src/route-definition/dsl-helpers.ts +6 -3
- 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 +42 -5
- package/src/router/instrument.ts +54 -2
- package/src/router/intercept-resolution.ts +30 -8
- package/src/router/loader-resolution.ts +89 -33
- package/src/router/logging.ts +5 -3
- package/src/router/match-api.ts +89 -42
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +14 -29
- package/src/router/match-middleware/background-revalidation.ts +3 -5
- package/src/router/match-middleware/cache-lookup.ts +52 -19
- package/src/router/match-middleware/cache-store.ts +36 -10
- package/src/router/match-result.ts +1 -0
- package/src/router/middleware-types.ts +6 -1
- package/src/router/middleware.ts +20 -4
- package/src/router/navigation-snapshot.ts +6 -5
- package/src/router/prerender-match.ts +10 -0
- package/src/router/request-classification.ts +7 -4
- package/src/router/router-options.ts +4 -1
- package/src/router/segment-resolution/fresh.ts +18 -13
- package/src/router/segment-resolution/helpers.ts +4 -2
- package/src/router/segment-resolution/loader-cache.ts +366 -70
- package/src/router/segment-resolution/loader-mask.ts +3 -4
- package/src/router/segment-resolution/loader-snapshot.ts +215 -126
- package/src/router/segment-resolution/mask-nested.ts +161 -42
- package/src/router/segment-resolution/revalidation.ts +27 -13
- 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 +47 -3
- package/src/rsc/handler.ts +24 -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 +10 -32
- package/src/rsc/response-cache-serve.ts +69 -78
- package/src/rsc/response-route-handler.ts +2 -2
- package/src/rsc/rsc-rendering.ts +712 -338
- package/src/rsc/server-action.ts +8 -20
- package/src/rsc/shell-build-manifest.ts +49 -29
- package/src/rsc/shell-capture-constants.ts +182 -12
- package/src/rsc/shell-capture.ts +1126 -612
- package/src/rsc/shell-serve.ts +331 -85
- package/src/rsc/ssr-setup.ts +3 -2
- package/src/rsc/types.ts +9 -0
- package/src/segment-system.tsx +40 -7
- package/src/server/context.ts +328 -72
- package/src/server/cookie-store.ts +184 -133
- package/src/server/handle-store.ts +245 -27
- package/src/server/request-context.ts +364 -100
- package/src/server/request-headers.ts +134 -0
- package/src/server.ts +4 -0
- package/src/ssr/index.tsx +96 -37
- package/src/ssr/ssr-root.tsx +9 -8
- package/src/testing/dispatch.ts +4 -7
- package/src/testing/e2e/index.ts +1 -0
- package/src/testing/flight.entry.ts +14 -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 +67 -5
- package/src/testing/run-transition-when.ts +109 -172
- package/src/testing/serve-shell-request.ts +381 -0
- package/src/testing/shell-status.ts +73 -47
- package/src/theme/ThemeProvider.tsx +7 -6
- package/src/transition-when-ref.ts +220 -0
- package/src/types/cache-types.ts +15 -3
- package/src/types/handler-context.ts +14 -1
- 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 +21 -18
- package/src/vite/discovery/discover-routers.ts +3 -0
- package/src/vite/discovery/shell-prerender-phase.ts +9 -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 +89 -9
- package/dist/types/rsc/transition-gate.d.ts +0 -23
- package/src/rsc/transition-gate.ts +0 -86
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* getItem()/setItem() methods on SegmentCacheStore.
|
|
7
7
|
*
|
|
8
8
|
* Cache key resolution (3-tier, matching CacheScope.resolveKey):
|
|
9
|
-
* 1. options.key(requestCtx) — full override
|
|
9
|
+
* 1. options.key(requestCtx) — full override, stored namespaced as
|
|
10
|
+
* loader:{loaderId}:key:{encodeURIComponent(result)} (#1009)
|
|
10
11
|
* 2. store.keyGenerator(requestCtx, defaultKey) — store-level modification
|
|
11
12
|
* 3. loader:{loaderId}:{host}{pathname}:{sortedParams} — default
|
|
12
13
|
*
|
|
@@ -17,18 +18,36 @@
|
|
|
17
18
|
* On stale hit (SWR): returns stale data, schedules background revalidation.
|
|
18
19
|
* On miss: executes loader, schedules non-blocking cache write.
|
|
19
20
|
*
|
|
21
|
+
* Identity (#972): without a key() or a store keyGenerator the entry is
|
|
22
|
+
* shared across users, so a fill whose execution recorded a request-identity
|
|
23
|
+
* read fails and is not stored, whoever started that execution
|
|
24
|
+
* (server/context.ts recordLoaderIdentityRead).
|
|
25
|
+
*
|
|
20
26
|
* Handle pushes (`ctx.use(Handle)(...)` in the loader body) are a side effect
|
|
21
27
|
* the HIT must reproduce: the MISS records the pushes of the body and of the
|
|
22
28
|
* loaders it awaits via ctx.use into the entry's `handles` blob (the
|
|
23
29
|
* "use cache" capture/encode), and every HIT — stale included — replays
|
|
24
30
|
* them, each loader's pushes at most once per request (a dependency another
|
|
25
31
|
* reader already ran keeps its live pushes). See replayLoaderHandles.
|
|
32
|
+
*
|
|
33
|
+
* Tags too (#964): the entry stores its cache() tags plus every tag its
|
|
34
|
+
* execution recorded, those of the loader values it read included
|
|
35
|
+
* (cache-tag.ts "Recorded-tag sets"), so updateTag() of any of them drops it.
|
|
36
|
+
* Every HIT records the stored tags through the loader's tag owner
|
|
37
|
+
* (recordLoaderTags), so a route cache() record, PPR shell or document built
|
|
38
|
+
* over the HIT carries them, and a loader that reads this one takes them on.
|
|
39
|
+
* Once the binding started, a loader body's ctx.use of it gets the same
|
|
40
|
+
* dataPromise as a handler read (createLoaderExecutor's useLoader); a stale
|
|
41
|
+
* refresh does not (setupLoaderAccess _runLoaderIsolated).
|
|
26
42
|
*/
|
|
27
43
|
|
|
28
44
|
import {
|
|
29
45
|
getCurrentLoaderBodyId,
|
|
30
46
|
isInsideLoaderBody,
|
|
47
|
+
loaderCacheIdentityError,
|
|
48
|
+
type EntryData,
|
|
31
49
|
type LoaderEntry,
|
|
50
|
+
type LoaderIdentityRead,
|
|
32
51
|
} from "../../server/context.js";
|
|
33
52
|
import type { HandlerContext, InternalHandlerContext } from "../../types.js";
|
|
34
53
|
import type { HandleStore } from "../../server/handle-store.js";
|
|
@@ -39,13 +58,17 @@ import {
|
|
|
39
58
|
} from "../../cache/handle-capture.js";
|
|
40
59
|
import { encodeHandles, decodeHandles } from "../../cache/handle-snapshot.js";
|
|
41
60
|
import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
|
|
61
|
+
import { runIdentityExempt } from "../../cache/cache-exec-scope.js";
|
|
42
62
|
import {
|
|
43
63
|
getRequestContext,
|
|
44
64
|
_getRequestContext,
|
|
45
65
|
runWithRequestContext,
|
|
46
66
|
} from "../../server/request-context.js";
|
|
47
67
|
import { observePhase, PHASES } from "../instrument.js";
|
|
48
|
-
import {
|
|
68
|
+
import {
|
|
69
|
+
KEY_PART_PREFIX,
|
|
70
|
+
sortedRouteParams,
|
|
71
|
+
} from "../../cache/cache-key-utils.js";
|
|
49
72
|
import {
|
|
50
73
|
resolveTtl,
|
|
51
74
|
resolveSwrWindow,
|
|
@@ -55,11 +78,29 @@ import {
|
|
|
55
78
|
DEFAULT_ROUTE_TTL,
|
|
56
79
|
} from "../../cache/cache-policy.js";
|
|
57
80
|
import { readThroughItem } from "../../cache/read-through-swr.js";
|
|
81
|
+
import {
|
|
82
|
+
executionStart,
|
|
83
|
+
predatesInvalidation,
|
|
84
|
+
} from "../../cache/tag-invalidation.js";
|
|
58
85
|
import {
|
|
59
86
|
maskNestedContainerThenables,
|
|
60
87
|
overlayLoaderContainer,
|
|
61
88
|
} from "./loader-snapshot.js";
|
|
62
|
-
import {
|
|
89
|
+
import {
|
|
90
|
+
SHELL_BAKE_TAG_OWNER,
|
|
91
|
+
captureRecordedTags,
|
|
92
|
+
earliestRecordedStart,
|
|
93
|
+
flattenRecordedTags,
|
|
94
|
+
linkLoaderTags,
|
|
95
|
+
linkLoaderTagsTo,
|
|
96
|
+
linkRecordedTags,
|
|
97
|
+
markIdentityRead,
|
|
98
|
+
markTagSetStart,
|
|
99
|
+
readValueTags,
|
|
100
|
+
recordedIdentityRead,
|
|
101
|
+
recordLoaderTags,
|
|
102
|
+
tagLoaderValue,
|
|
103
|
+
} from "../../cache/cache-tag.js";
|
|
63
104
|
import {
|
|
64
105
|
isShellCaptureActive,
|
|
65
106
|
createMaskedLoaderPromise,
|
|
@@ -98,7 +139,30 @@ function getDefaultLoaderCacheKey(
|
|
|
98
139
|
}
|
|
99
140
|
|
|
100
141
|
/**
|
|
101
|
-
*
|
|
142
|
+
* A loader's own `key()` result as its entry key (#1009). The result is
|
|
143
|
+
* often request input, and the item family (`getItem`/`setItem`) has no
|
|
144
|
+
* other discriminator, so stored raw it could name another loader's entry
|
|
145
|
+
* or a `"use cache"` entry (`use-cache:<id>:...`, cache-runtime.ts), which
|
|
146
|
+
* shares that family; route records use `get`/`set`. Namespaced, it can't:
|
|
147
|
+
* - it starts with `loader:`, which no `"use cache"` key does;
|
|
148
|
+
* - an encoded result holds no `:`, so the key reads unambiguously from
|
|
149
|
+
* the right (result, `key:`, loader id), and two namespaced keys are
|
|
150
|
+
* equal only for the same loader and result;
|
|
151
|
+
* - a default key `loader:<id>:<host><pathname>...` holds a `/` past its
|
|
152
|
+
* id, and an encoded result holds none, so the two are equal only if one
|
|
153
|
+
* loader's id is literally `<other id>:<host>/<path>...`. Loader ids can
|
|
154
|
+
* hold `:` (a dev id is a root-relative path, absolute `D:/...` on
|
|
155
|
+
* Windows), so this is the guarantee, not "ids hold no `:`".
|
|
156
|
+
* A store keyGenerator result stays the store's own namespace, as on routes
|
|
157
|
+
* (composeCacheKeys).
|
|
158
|
+
*/
|
|
159
|
+
function loaderKeyFromResult(loaderId: string, result: string): string {
|
|
160
|
+
return `loader:${loaderId}:${KEY_PART_PREFIX}${encodeURIComponent(result)}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Resolve cache key using the shared 3-tier priority. `declared`: a key() or
|
|
165
|
+
* store keyGenerator produced it, so it can carry request identity (#972).
|
|
102
166
|
*/
|
|
103
167
|
async function resolveLoaderKey(
|
|
104
168
|
loaderEntry: LoaderEntry,
|
|
@@ -106,7 +170,7 @@ async function resolveLoaderKey(
|
|
|
106
170
|
loaderId: string,
|
|
107
171
|
pathname: string,
|
|
108
172
|
params: Record<string, string>,
|
|
109
|
-
): Promise<string> {
|
|
173
|
+
): Promise<{ key: string; declared: boolean }> {
|
|
110
174
|
const options = loaderEntry.cache!.options;
|
|
111
175
|
// The host is part of the loader cache identity, matching the route-level
|
|
112
176
|
// cache (cache-scope getCacheKeyBase: `${host}${pathname}`) and "use cache"
|
|
@@ -115,8 +179,16 @@ async function resolveLoaderKey(
|
|
|
115
179
|
// loader data to another.
|
|
116
180
|
const host = getRequestContext()?.url?.host ?? "localhost";
|
|
117
181
|
const defaultKey = getDefaultLoaderCacheKey(loaderId, host, pathname, params);
|
|
118
|
-
if (options === false) return defaultKey;
|
|
119
|
-
|
|
182
|
+
if (options === false) return { key: defaultKey, declared: false };
|
|
183
|
+
const keyFn = options.key;
|
|
184
|
+
return {
|
|
185
|
+
key: await resolveCacheKey(
|
|
186
|
+
keyFn && (async (ctx) => loaderKeyFromResult(loaderId, await keyFn(ctx))),
|
|
187
|
+
store,
|
|
188
|
+
defaultKey,
|
|
189
|
+
),
|
|
190
|
+
declared: Boolean(keyFn || store.keyGenerator),
|
|
191
|
+
};
|
|
120
192
|
}
|
|
121
193
|
|
|
122
194
|
/**
|
|
@@ -131,6 +203,97 @@ function resolveTags(loaderEntry: LoaderEntry): string[] | undefined {
|
|
|
131
203
|
return resolveTagsOption(options.tags, getRequestContext(), "LoaderCache");
|
|
132
204
|
}
|
|
133
205
|
|
|
206
|
+
const bindsLoaderCacheMemo = new WeakMap<EntryData, boolean>();
|
|
207
|
+
|
|
208
|
+
const hasLoaderCache = (loaders: readonly LoaderEntry[] | undefined) =>
|
|
209
|
+
loaders?.some((l) => l.cache !== undefined && l.cache.options !== false) ??
|
|
210
|
+
false;
|
|
211
|
+
|
|
212
|
+
function entryBindsLoaderCache(entry: EntryData): boolean {
|
|
213
|
+
let binds = bindsLoaderCacheMemo.get(entry);
|
|
214
|
+
if (binds === undefined) {
|
|
215
|
+
binds =
|
|
216
|
+
hasLoaderCache(entry.loader) ||
|
|
217
|
+
(entry.intercept?.some((i) => hasLoaderCache(i.loader)) ?? false) ||
|
|
218
|
+
(entry.layout?.some(entryBindsLoaderCache) ?? false) ||
|
|
219
|
+
Object.values(entry.parallel ?? {}).some(
|
|
220
|
+
(p) => p !== undefined && entryBindsLoaderCache(p),
|
|
221
|
+
);
|
|
222
|
+
bindsLoaderCacheMemo.set(entry, binds);
|
|
223
|
+
}
|
|
224
|
+
return binds;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Whether a request over the matched chain `entries` resolves a loader bound
|
|
229
|
+
* with its own cache(): on a chain entry, or on its parallel slots, orphan
|
|
230
|
+
* layouts or intercepts. The match arms the per-execution loader tag sets on
|
|
231
|
+
* it (cache-tag.ts armLoaderTagSets), before any loader starts, so a
|
|
232
|
+
* dependency the handler or another binding starts first still records.
|
|
233
|
+
* Memoized per entry.
|
|
234
|
+
*/
|
|
235
|
+
export function bindsLoaderCache(entries: readonly EntryData[]): boolean {
|
|
236
|
+
return entries.some(entryBindsLoaderCache);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const chainLoaderEntriesMemo = new WeakMap<EntryData, readonly LoaderEntry[]>();
|
|
240
|
+
|
|
241
|
+
// The loaders an entry registers, its orphan layouts' and its parallel
|
|
242
|
+
// slots' included.
|
|
243
|
+
function chainLoaderEntries(entry: EntryData): readonly LoaderEntry[] {
|
|
244
|
+
let loaders = chainLoaderEntriesMemo.get(entry);
|
|
245
|
+
if (loaders === undefined) {
|
|
246
|
+
loaders = [
|
|
247
|
+
...(entry.loader ?? []),
|
|
248
|
+
...(entry.layout ?? []).flatMap(chainLoaderEntries),
|
|
249
|
+
...Object.values(entry.parallel ?? {}).flatMap((p) =>
|
|
250
|
+
p ? chainLoaderEntries(p) : [],
|
|
251
|
+
),
|
|
252
|
+
];
|
|
253
|
+
chainLoaderEntriesMemo.set(entry, loaders);
|
|
254
|
+
}
|
|
255
|
+
return loaders;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* The ids of the loaders a request over the matched chain `entries` runs on
|
|
260
|
+
* the live lane (no `ssr: false`), on a chain entry, its orphan layouts or
|
|
261
|
+
* its parallel slots. A document HIT tail restores the record's pushes of
|
|
262
|
+
* every other owner as authoritative (withCacheLookup, HandleStore
|
|
263
|
+
* pushRestored); a live-lane loader is a hole, so its live run replaces its
|
|
264
|
+
* recorded pushes instead. Memoized per entry.
|
|
265
|
+
*/
|
|
266
|
+
export function liveLaneLoaderIds(
|
|
267
|
+
entries: readonly EntryData[],
|
|
268
|
+
): ReadonlySet<string> {
|
|
269
|
+
const ids = new Set<string>();
|
|
270
|
+
for (const entry of entries) {
|
|
271
|
+
for (const l of chainLoaderEntries(entry)) {
|
|
272
|
+
if (!l.bake) ids.add(l.loader.$$id);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return ids;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* The lane of each loader the matched chain `entries` registers: "live" (no
|
|
280
|
+
* `ssr: false`) or "bake". A loader registered on both is "live". A PPR
|
|
281
|
+
* capture credits a push to the first registered loader around it when that
|
|
282
|
+
* one is live (shell-capture.ts deriveShellCaptureContext).
|
|
283
|
+
*/
|
|
284
|
+
export function routeLoaderLanes(
|
|
285
|
+
entries: readonly EntryData[],
|
|
286
|
+
): ReadonlyMap<string, "live" | "bake"> {
|
|
287
|
+
const lanes = new Map<string, "live" | "bake">();
|
|
288
|
+
for (const entry of entries) {
|
|
289
|
+
for (const l of chainLoaderEntries(entry)) {
|
|
290
|
+
if (!l.bake) lanes.set(l.loader.$$id, "live");
|
|
291
|
+
else if (!lanes.has(l.loader.$$id)) lanes.set(l.loader.$$id, "bake");
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return lanes;
|
|
295
|
+
}
|
|
296
|
+
|
|
134
297
|
function getLoaderStore(
|
|
135
298
|
loaderEntry: LoaderEntry,
|
|
136
299
|
): import("../../cache/types.js").SegmentCacheStore | null {
|
|
@@ -158,35 +321,82 @@ function getLoaderStore(
|
|
|
158
321
|
* keyed on that body scope (executeLoaderData), which would swallow the
|
|
159
322
|
* replay; and the store reads the body scope to tell a live push from a
|
|
160
323
|
* replayed one.
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
324
|
+
* A PPR shell capture records the replayed pushes under this loader (its
|
|
325
|
+
* push funnel reads the pushReplayed owner, shell-capture.ts), so a shell
|
|
326
|
+
* HIT restores them owned (HandleStore.pushRestored). This replay still
|
|
327
|
+
* runs on that HIT: the store drops its settled values, which the record
|
|
328
|
+
* already restored, and keeps the thenable ones the record could not keep
|
|
329
|
+
* (a deferred push, the reason the loader's record carries `runs`).
|
|
164
330
|
*/
|
|
165
331
|
async function replayLoaderHandles(
|
|
166
|
-
encoded: string,
|
|
332
|
+
encoded: string | undefined,
|
|
167
333
|
handleStore: HandleStore,
|
|
168
334
|
segmentId: string,
|
|
335
|
+
cachedLoaderId: string,
|
|
169
336
|
claim: ((loaderId: string) => boolean) | undefined,
|
|
170
337
|
): Promise<void> {
|
|
171
|
-
const recorded = await decodeHandles(encoded);
|
|
338
|
+
const recorded = encoded ? await decodeHandles(encoded) : {};
|
|
172
339
|
if (!recorded) return;
|
|
173
340
|
const delivers = new Map<string, boolean>();
|
|
174
|
-
|
|
175
|
-
const owner = key.slice(key.indexOf(":") + 1);
|
|
341
|
+
const claims = (owner: string): boolean => {
|
|
176
342
|
let deliver = delivers.get(owner);
|
|
177
343
|
if (deliver === undefined) {
|
|
178
344
|
deliver = claim ? claim(owner) : true;
|
|
179
345
|
delivers.set(owner, deliver);
|
|
180
346
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
347
|
+
return deliver;
|
|
348
|
+
};
|
|
349
|
+
const replay = (): void => {
|
|
350
|
+
for (const key in recorded) {
|
|
351
|
+
const owner = key.slice(key.indexOf(":") + 1);
|
|
352
|
+
if (!claims(owner)) continue;
|
|
353
|
+
for (const [handleName, values] of Object.entries(recorded[key])) {
|
|
354
|
+
for (const value of values) {
|
|
355
|
+
handleStore.pushReplayed(handleName, segmentId, value, owner);
|
|
356
|
+
}
|
|
185
357
|
}
|
|
186
358
|
}
|
|
359
|
+
};
|
|
360
|
+
// A hole's entry, pushes or none, takes the place of the shell copies of
|
|
361
|
+
// its pushes (HandleStore.redeliverReplays). Not once the hole delivered
|
|
362
|
+
// in this request: a run a reader started first replaces them itself.
|
|
363
|
+
if (claims(cachedLoaderId)) {
|
|
364
|
+
handleStore.redeliverReplays(cachedLoaderId, replay);
|
|
365
|
+
} else {
|
|
366
|
+
replay();
|
|
187
367
|
}
|
|
188
368
|
}
|
|
189
369
|
|
|
370
|
+
/**
|
|
371
|
+
* Identity mark (#972): a declared-key entry whose MISS recorded a
|
|
372
|
+
* request-identity read stores that read ahead of its payload. A HIT skips the
|
|
373
|
+
* body, so without the mark an unkeyed loader reading this value on a HIT
|
|
374
|
+
* would store another user's data. The value string is this module's own
|
|
375
|
+
* (serialize/deserialize below), so the mark needs no store support; Flight
|
|
376
|
+
* and JSON payloads never start with "~".
|
|
377
|
+
*/
|
|
378
|
+
const IDENTITY_MARK = "~identity:";
|
|
379
|
+
|
|
380
|
+
function markIdentity(
|
|
381
|
+
value: string,
|
|
382
|
+
read: LoaderIdentityRead | undefined,
|
|
383
|
+
): string {
|
|
384
|
+
return read ? `${IDENTITY_MARK}${JSON.stringify(read)}\n${value}` : value;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function unmarkIdentity(value: string): {
|
|
388
|
+
read: LoaderIdentityRead | undefined;
|
|
389
|
+
payload: string;
|
|
390
|
+
} {
|
|
391
|
+
if (!value.startsWith(IDENTITY_MARK))
|
|
392
|
+
return { read: undefined, payload: value };
|
|
393
|
+
const end = value.indexOf("\n");
|
|
394
|
+
return {
|
|
395
|
+
read: JSON.parse(value.slice(IDENTITY_MARK.length, end)),
|
|
396
|
+
payload: value.slice(end + 1),
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
190
400
|
/**
|
|
191
401
|
* Record key for the MISS capture: run-length groups by the innermost loader
|
|
192
402
|
* body, `${seq}:${loaderId}` — the cached loader or a dependency it awaits
|
|
@@ -226,18 +436,23 @@ function recordOwnerKey(cachedLoaderId: string): () => string {
|
|
|
226
436
|
* never run under a shell capture), so the lane is decided here, per LOADER,
|
|
227
437
|
* never by the entry's loading():
|
|
228
438
|
*
|
|
229
|
-
* - BAKE lane: `loader(Def, { ssr: false })` (
|
|
230
|
-
* `bakeSegmentKey`
|
|
439
|
+
* - BAKE lane: `loader(Def, { ssr: false })` (LoaderEntry `bake`: the
|
|
440
|
+
* caller passes a `bakeSegmentKey` for it on document and navigation
|
|
441
|
+
* evaluations alike; a capture, a document evaluation, also has
|
|
442
|
+
* awaitBeforeFlush). The loader EXECUTES at capture (the
|
|
231
443
|
* flag's "data in the HTML before first flush" maps to the frozen prelude)
|
|
232
444
|
* and its settled non-promise data bakes into the shell. Promises nested in
|
|
233
|
-
* plain objects
|
|
234
|
-
* the consumer's own Suspense — per-request material must be
|
|
235
|
-
* (the #692 cross-session scar). The masked container
|
|
236
|
-
* `_shellCaptureLoaderRecords`: it holds the capture gate
|
|
237
|
-
* `ppr.captureTimeout`) and pins into the snapshot's loader
|
|
238
|
-
* HIT the recorded container is overlaid onto the fresh
|
|
239
|
-
* payload matches the frozen prelude
|
|
240
|
-
*
|
|
445
|
+
* plain objects, arrays and JSX props are masked (mask-nested.ts) and stay
|
|
446
|
+
* live holes at the consumer's own Suspense — per-request material must be
|
|
447
|
+
* promise-shaped (the #692 cross-session scar). The masked container
|
|
448
|
+
* registers on `_shellCaptureLoaderRecords`: it holds the capture gate
|
|
449
|
+
* (bounded by `ppr.captureTimeout`) and pins into the snapshot's loader
|
|
450
|
+
* family, and on a HIT the recorded container is overlaid onto the fresh
|
|
451
|
+
* run (below) so the payload matches the frozen prelude outside the holes.
|
|
452
|
+
* A server-component element holding a promise is a hole as a whole, so its
|
|
453
|
+
* other props come from the fresh run (loader-snapshot.ts
|
|
454
|
+
* elideLoaderContainer). cookies()/headers() in its body (nested promise
|
|
455
|
+
* bodies included) trip the capture guard.
|
|
241
456
|
* - LIVE lane: every other loader, whatever its entry's loading(). Never
|
|
242
457
|
* executes at capture: the slot gets a never-resolving promise
|
|
243
458
|
* (loader-mask.ts) and postpones at the reader's boundary — loading() or an
|
|
@@ -269,6 +484,8 @@ export function resolveLoaderData<TEnv>(
|
|
|
269
484
|
);
|
|
270
485
|
maskedPromise.catch(() => {});
|
|
271
486
|
reqCtx?._shellCaptureLoaderRecords?.set(bakeSegmentKey, maskedPromise);
|
|
487
|
+
// Its data bakes into the shell, so its tags tag the shell.
|
|
488
|
+
linkLoaderTagsTo(SHELL_BAKE_TAG_OWNER, loaderEntry.loader.$$id, reqCtx);
|
|
272
489
|
return maskedPromise;
|
|
273
490
|
}
|
|
274
491
|
return createMaskedLoaderPromise();
|
|
@@ -280,31 +497,31 @@ export function resolveLoaderData<TEnv>(
|
|
|
280
497
|
const recorded = seed.get(bakeSegmentKey)!;
|
|
281
498
|
if (!recorded.holes) {
|
|
282
499
|
// Pin-first (hole-free record): the pinned container is what the
|
|
283
|
-
// payload serves
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
500
|
+
// payload serves (recorded paths win wholesale), resolved
|
|
501
|
+
// immediately. The body does not run: a HIT is rendered from the
|
|
502
|
+
// shell, like the handlers it replays, and the loader's settled
|
|
503
|
+
// pushes (and those of loaders it awaits) are restored from the
|
|
504
|
+
// record. Only a record whose capture saw a push it could not keep
|
|
505
|
+
// (`runs`) still runs the body, in the background, lifetime-extended
|
|
506
|
+
// so the runtime cannot cancel it when the stream closes first; its
|
|
507
|
+
// rejection is swallowed (the payload already matches the prelude,
|
|
508
|
+
// which a fresh error value never could).
|
|
291
509
|
//
|
|
292
510
|
// CONTRACT (deliberate divergence from the gated overlay's
|
|
293
511
|
// fresh-only-keys passthrough): a hole-free pin serves the pinned
|
|
294
|
-
// SHAPE wholesale
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
reqCtx?.executionContext?.waitUntil?.(fresh);
|
|
512
|
+
// SHAPE wholesale. A field the body would return now had no hole
|
|
513
|
+
// postponed for it at capture, so the prelude froze the
|
|
514
|
+
// without-that-field branch and the resume pass has nothing to
|
|
515
|
+
// fill. A field that is per-request must be promise-shaped at
|
|
516
|
+
// capture (masked -> hole marker -> holes: 1 -> the gated path
|
|
517
|
+
// below, which preserves fresh-only passthrough) or live behind
|
|
518
|
+
// loading(). Anything else is uncached nondeterminism in shell
|
|
519
|
+
// material — the documented drift residual.
|
|
520
|
+
if (recorded.runs) {
|
|
521
|
+
const fresh = executeLoaderData(loaderEntry, ctx, pathname);
|
|
522
|
+
fresh.catch(() => {});
|
|
523
|
+
reqCtx?.executionContext?.waitUntil?.(fresh);
|
|
524
|
+
}
|
|
308
525
|
return Promise.resolve(
|
|
309
526
|
overlayLoaderContainer(undefined, recorded.container),
|
|
310
527
|
);
|
|
@@ -343,9 +560,10 @@ function executeLoaderData<TEnv>(
|
|
|
343
560
|
|
|
344
561
|
// Evaluate runtime condition if provided
|
|
345
562
|
const options = cacheConfig.options;
|
|
346
|
-
|
|
563
|
+
const condition = options.condition;
|
|
564
|
+
if (condition) {
|
|
347
565
|
const requestCtx = getRequestContext();
|
|
348
|
-
if (requestCtx && !
|
|
566
|
+
if (requestCtx && !runIdentityExempt(() => condition(requestCtx))) {
|
|
349
567
|
return ctx.use(loaderEntry.loader);
|
|
350
568
|
}
|
|
351
569
|
}
|
|
@@ -368,7 +586,13 @@ function executeLoaderData<TEnv>(
|
|
|
368
586
|
internal._loaderCacheOriginalUse = originalUse;
|
|
369
587
|
ctx.use = ((item: any) => {
|
|
370
588
|
const cached = overrides!.get(item?.$$id);
|
|
371
|
-
if (cached)
|
|
589
|
+
if (cached) {
|
|
590
|
+
// A read is a consumption (#957, #964): see createLoaderExecutor's
|
|
591
|
+
// useLoader.
|
|
592
|
+
linkLoaderTags(item.$$id);
|
|
593
|
+
readValueTags(cached);
|
|
594
|
+
return cached;
|
|
595
|
+
}
|
|
372
596
|
return originalUse(item);
|
|
373
597
|
}) as typeof ctx.use;
|
|
374
598
|
}
|
|
@@ -395,7 +619,14 @@ function executeLoaderData<TEnv>(
|
|
|
395
619
|
const swrWindow = resolveSwrWindow(options.swr, store.defaults);
|
|
396
620
|
const swr = swrWindow || undefined;
|
|
397
621
|
const tags = resolveTags(loaderEntry);
|
|
398
|
-
|
|
622
|
+
recordLoaderTags(loaderId, tags);
|
|
623
|
+
// The per-execution loader tag sets were armed before any loader ran: by
|
|
624
|
+
// the match (match-api.ts, bindsLoaderCache), or by runLoader.
|
|
625
|
+
// An execution of this loader outside the binding (a reader started it
|
|
626
|
+
// first, a stale refresh) answers for the cache() tags too.
|
|
627
|
+
if (tags && tags.length > 0) {
|
|
628
|
+
internal._bindLoaderCacheTags?.(loaderId, new Set(tags));
|
|
629
|
+
}
|
|
399
630
|
|
|
400
631
|
// Handle pushes: the store and the owning segment are read synchronously
|
|
401
632
|
// at kickoff, as createLoaderExecutor does for the body's own pushes.
|
|
@@ -406,10 +637,16 @@ function executeLoaderData<TEnv>(
|
|
|
406
637
|
// concurrently. A dependency can still run on a HIT for another reader
|
|
407
638
|
// (memoized per request); the replay's claim keeps its pushes to one copy.
|
|
408
639
|
const isOwnBodyPush = () => isInsideLoaderBody(loaderId);
|
|
640
|
+
// What a reader of this value takes on (tagLoaderValue below,
|
|
641
|
+
// readValueTags): the cache() tags, plus the entry's tags on a HIT or the
|
|
642
|
+
// execution's on a MISS. A HIT's value is as old as its lookup: a cached
|
|
643
|
+
// reader gates its own write from here at the latest (#977).
|
|
644
|
+
const valueTags = new Set(tags);
|
|
645
|
+
markTagSetStart(valueTags, executionStart());
|
|
409
646
|
|
|
410
647
|
const dataPromise = (async () => {
|
|
411
648
|
const codec = await getCodec();
|
|
412
|
-
const key = await resolveLoaderKey(
|
|
649
|
+
const { key, declared } = await resolveLoaderKey(
|
|
413
650
|
loaderEntry,
|
|
414
651
|
store,
|
|
415
652
|
loaderId,
|
|
@@ -428,15 +665,23 @@ function executeLoaderData<TEnv>(
|
|
|
428
665
|
|
|
429
666
|
// One readThroughItem call runs at most one execution — the foreground
|
|
430
667
|
// MISS or the background stale revalidation (flagged by wrapBackground)
|
|
431
|
-
// — so
|
|
668
|
+
// — so one handle capture and one tag set serve the setItem after it.
|
|
669
|
+
// The entry's tags: its cache() tags plus what the execution recorded.
|
|
432
670
|
let capture: HandleCapture | undefined;
|
|
671
|
+
const bodyTags = new Set(tags);
|
|
433
672
|
let revalidating = false;
|
|
434
673
|
let hitHandles: string | undefined;
|
|
674
|
+
let cachedRead = false;
|
|
435
675
|
const onCachedRead = (label: string, cached: CacheItemResult) => {
|
|
676
|
+
cachedRead = true;
|
|
436
677
|
hitHandles = cached.handles;
|
|
678
|
+
const { read } = unmarkIdentity(cached.value);
|
|
679
|
+
if (read) markIdentityRead(valueTags, read);
|
|
680
|
+
recordLoaderTags(loaderId, cached.tags);
|
|
681
|
+
for (const tag of cached.tags ?? []) valueTags.add(tag);
|
|
437
682
|
debugLoaderCacheLog(`[LoaderCache] ${label}: ${key}`);
|
|
438
683
|
};
|
|
439
|
-
const
|
|
684
|
+
const runBody = async (): Promise<any> => {
|
|
440
685
|
// A stale-hit revalidation runs on its own executor: it diverts its
|
|
441
686
|
// pushes (below), so sharing the page's memoized run of a dependency
|
|
442
687
|
// would take that dependency's pushes off the page.
|
|
@@ -457,6 +702,26 @@ function executeLoaderData<TEnv>(
|
|
|
457
702
|
c.stop();
|
|
458
703
|
}
|
|
459
704
|
};
|
|
705
|
+
// #972: the execution's identity reads, and those of the loader values it
|
|
706
|
+
// read, are on the bodyTags links whoever started them. With an undeclared
|
|
707
|
+
// key one fails the fill (the MISS or the stale refresh); one that settles
|
|
708
|
+
// after the value (a nested promise, a pending handle push) skips the write
|
|
709
|
+
// instead. With a declared key it is stored as the entry's identity mark
|
|
710
|
+
// for the HITs, as this loader's readers see it (through this loader when
|
|
711
|
+
// another made it), and a foreground execution marks the value it serves.
|
|
712
|
+
// A stale refresh does not: the page is served the stale entry, which its
|
|
713
|
+
// own mark (or its lack) describes.
|
|
714
|
+
const identityRead = (): LoaderIdentityRead | undefined => {
|
|
715
|
+
const read = recordedIdentityRead(bodyTags);
|
|
716
|
+
if (!read) return undefined;
|
|
717
|
+
if (!declared) throw loaderCacheIdentityError(read, loaderId);
|
|
718
|
+
const seen =
|
|
719
|
+
read.bodyId === undefined || read.bodyId === loaderId
|
|
720
|
+
? { ...read, bodyId: loaderId, via: undefined }
|
|
721
|
+
: { ...read, via: loaderId };
|
|
722
|
+
if (!revalidating) markIdentityRead(valueTags, seen);
|
|
723
|
+
return seen;
|
|
724
|
+
};
|
|
460
725
|
|
|
461
726
|
// Flight encodes a rejected promise in the value or a handle push as an
|
|
462
727
|
// error row and completes normally. setItem runs after serialize and
|
|
@@ -473,15 +738,37 @@ function executeLoaderData<TEnv>(
|
|
|
473
738
|
// pushes awaited up to its timeout, the whole blob dropped on a timeout
|
|
474
739
|
// or a thrown encode). Encoded here, inside the deferred write, so a
|
|
475
740
|
// MISS response never waits on it.
|
|
476
|
-
setItem:
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
741
|
+
setItem: (k, v, o) =>
|
|
742
|
+
captureRecordedTags(bodyTags, async () => {
|
|
743
|
+
const handles = capture
|
|
744
|
+
? await encodeHandles(capture.data, onFlightError)
|
|
745
|
+
: "";
|
|
746
|
+
if (flightErrors.length > 0) throw flightErrors[0];
|
|
747
|
+
// After the handle encode: it settles pending pushes, which can read.
|
|
748
|
+
const read = identityRead();
|
|
749
|
+
const entryTags = [...flattenRecordedTags(bodyTags)];
|
|
750
|
+
// One of the entry's tags was invalidated after the value's oldest
|
|
751
|
+
// part started (#977): this execution, or the run of this loader or
|
|
752
|
+
// of a dependency another reader started earlier. Written now, a
|
|
753
|
+
// value that may predate it would outlive it. Skip the write; the
|
|
754
|
+
// next read runs the body.
|
|
755
|
+
const start = earliestRecordedStart(bodyTags);
|
|
756
|
+
if (start && (await predatesInvalidation(store, entryTags, start))) {
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
await store.setItem!(k, markIdentity(v, read), {
|
|
760
|
+
...o,
|
|
761
|
+
tags: entryTags.length > 0 ? entryTags : undefined,
|
|
762
|
+
...(handles ? { handles } : {}),
|
|
763
|
+
});
|
|
764
|
+
}),
|
|
483
765
|
key,
|
|
484
|
-
execute
|
|
766
|
+
execute: async () => {
|
|
767
|
+
markTagSetStart(bodyTags, executionStart());
|
|
768
|
+
const value = await captureRecordedTags(bodyTags, runBody);
|
|
769
|
+
identityRead();
|
|
770
|
+
return value;
|
|
771
|
+
},
|
|
485
772
|
// The rango.background span (kind=loader-revalidation) wraps the WHOLE
|
|
486
773
|
// stale revalidation — the re-execution AND the serialize/setItem write
|
|
487
774
|
// (read-through-swr routes the full task through wrapBackground) — so
|
|
@@ -495,22 +782,30 @@ function executeLoaderData<TEnv>(
|
|
|
495
782
|
observePhase(PHASES.background("loader-revalidation"), run),
|
|
496
783
|
);
|
|
497
784
|
},
|
|
498
|
-
serialize: (d) =>
|
|
499
|
-
|
|
785
|
+
serialize: (d) =>
|
|
786
|
+
captureRecordedTags(bodyTags, () =>
|
|
787
|
+
codec.serializeResult(d, onFlightError),
|
|
788
|
+
),
|
|
789
|
+
deserialize: (v) => codec.deserializeResult(unmarkIdentity(v).payload),
|
|
500
790
|
storeOptions: { ttl, swr, tags },
|
|
501
791
|
onHit: (cached) => onCachedRead("HIT", cached),
|
|
502
792
|
onStale: (cached) => onCachedRead("STALE", cached),
|
|
503
|
-
onMiss: () =>
|
|
793
|
+
onMiss: () => {
|
|
794
|
+
linkRecordedTags(valueTags, bodyTags);
|
|
795
|
+
debugLoaderCacheLog(`[LoaderCache] MISS: ${key}`);
|
|
796
|
+
},
|
|
504
797
|
onCached: () => debugLoaderCacheLog(`[LoaderCache] Cached: ${key}`),
|
|
505
798
|
host: requestCtxForExecute,
|
|
506
799
|
});
|
|
507
800
|
|
|
508
|
-
// An entry
|
|
509
|
-
|
|
801
|
+
// An entry without handles (none recorded, or dropped by an encode
|
|
802
|
+
// timeout) replays none, and still stands for a hole's shell copies.
|
|
803
|
+
if (cachedRead && handleStore && owningSegmentId) {
|
|
510
804
|
await replayLoaderHandles(
|
|
511
805
|
hitHandles,
|
|
512
806
|
handleStore,
|
|
513
807
|
owningSegmentId,
|
|
808
|
+
loaderId,
|
|
514
809
|
internal._claimLoaderPushes,
|
|
515
810
|
);
|
|
516
811
|
}
|
|
@@ -522,6 +817,7 @@ function executeLoaderData<TEnv>(
|
|
|
522
817
|
// push after full drain throws LateHandlePushError.
|
|
523
818
|
handleStore?.trackAuxiliary(dataPromise);
|
|
524
819
|
|
|
820
|
+
tagLoaderValue(dataPromise, valueTags);
|
|
525
821
|
overrides.set(loaderId, dataPromise);
|
|
526
822
|
|
|
527
823
|
return dataPromise;
|
|
@@ -8,10 +8,9 @@
|
|
|
8
8
|
* fresh into it.
|
|
9
9
|
*
|
|
10
10
|
* Capture mode is signalled by `requestCtx._shellCaptureRun`, set to true ONLY on
|
|
11
|
-
* the derived request context of the background capture task (shell-capture.ts)
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* for the mask so every loader execution site gates the same way (loader-cache.ts
|
|
11
|
+
* the derived request context of the background capture task (shell-capture.ts);
|
|
12
|
+
* the foreground render never sets it. This module is the single home for the
|
|
13
|
+
* mask so every loader execution site gates the same way (loader-cache.ts
|
|
15
14
|
* `resolveLoaderData`, fresh.ts `resolveLoaders`).
|
|
16
15
|
*/
|
|
17
16
|
|