@rangojs/router 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/rango.js +26 -14
- package/dist/types/browser/event-controller.d.ts +32 -0
- package/dist/types/browser/history-state.d.ts +16 -4
- package/dist/types/browser/navigation-transaction.d.ts +7 -0
- package/dist/types/browser/rango-state.d.ts +3 -1
- package/dist/types/browser/react/context.d.ts +11 -0
- package/dist/types/browser/react/location-state-shared.d.ts +84 -10
- package/dist/types/browser/react/location-state.d.ts +7 -2
- package/dist/types/browser/react/subscribe-to-nothing.d.ts +7 -0
- package/dist/types/cache/cache-scope.d.ts +4 -5
- package/dist/types/cache/cf/cf-cache-store.d.ts +27 -6
- package/dist/types/cache/cf/cf-cache-types.d.ts +12 -4
- package/dist/types/cache/handle-snapshot.d.ts +36 -50
- package/dist/types/cache/index.d.ts +2 -0
- package/dist/types/cache/shell-snapshot.d.ts +19 -3
- package/dist/types/cache/types.d.ts +21 -10
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +28 -16
- package/dist/types/client-urls/navigation.d.ts +7 -1
- package/dist/types/client-urls/optimistic-location.d.ts +10 -4
- package/dist/types/handles/deferred-resolution.d.ts +11 -1
- package/dist/types/prerender/build-shell-capture.d.ts +8 -6
- package/dist/types/router/match-middleware/cache-lookup.d.ts +1 -1
- package/dist/types/router/navigation-snapshot.d.ts +8 -1
- package/dist/types/router/router-interfaces.d.ts +2 -1
- package/dist/types/router/router-options.d.ts +17 -7
- package/dist/types/router/segment-resolution/loader-cache.d.ts +21 -9
- package/dist/types/router-versions.d.ts +39 -0
- package/dist/types/rsc/handler-context.d.ts +5 -0
- package/dist/types/rsc/shell-build-manifest.d.ts +3 -2
- package/dist/types/rsc/types.d.ts +4 -6
- package/dist/types/server/build-version-table.d.ts +44 -0
- package/dist/types/server/handle-store.d.ts +87 -46
- package/dist/types/server/request-context.d.ts +22 -8
- package/dist/types/testing/build-versions.d.ts +32 -0
- package/dist/types/testing/dom.entry.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +2 -0
- package/dist/types/testing/internal/seed-vars.d.ts +7 -2
- package/dist/types/testing/location-state-key.d.ts +5 -1
- package/dist/types/testing/render-route.d.ts +77 -6
- package/dist/types/testing/serve-shell-request.d.ts +21 -0
- package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
- package/dist/types/types/handler-context.d.ts +8 -5
- package/dist/types/vite/discovery/build-versions.d.ts +393 -0
- package/dist/types/vite/discovery/installed-packages.d.ts +26 -0
- package/dist/types/vite/discovery/router-versions-phase.d.ts +52 -0
- package/dist/types/vite/discovery/shell-prerender-phase.d.ts +7 -17
- package/dist/types/vite/discovery/state.d.ts +14 -0
- package/dist/types/vite/encryption-key.d.ts +18 -0
- package/dist/types/vite/plugin-types.d.ts +27 -0
- package/dist/types/vite/plugins/{version-injector.d.ts → entry-bootstrap-injector.d.ts} +4 -6
- package/dist/types/vite/plugins/expose-ids/export-analysis.d.ts +1 -0
- package/dist/types/vite/plugins/expose-ids/router-transform.d.ts +14 -0
- package/dist/types/vite/plugins/stable-rsc-output.d.ts +51 -0
- package/dist/types/vite/plugins/version-plugin.d.ts +9 -11
- package/dist/types/vite/plugins/virtual-entries.d.ts +18 -1
- package/dist/types/vite/router-discovery.d.ts +6 -0
- package/dist/types/vite/utils/client-chunks.d.ts +2 -0
- package/dist/types/vite/utils/compare-strings.d.ts +7 -0
- package/dist/types/vite/utils/package-resolution.d.ts +8 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +2 -0
- package/dist/vite/index.js +1537 -444
- package/package.json +1 -2
- package/skills/cache-guide/SKILL.md +84 -20
- package/skills/caching/SKILL.md +16 -5
- package/skills/client-urls/SKILL.md +16 -0
- package/skills/cloudflare/SKILL.md +2 -1
- package/skills/comparison/references/framework-comparison.md +16 -12
- package/skills/hooks/SKILL.md +3 -0
- package/skills/hooks/handle-and-actions.md +6 -0
- package/skills/hooks/navigation.md +28 -0
- package/skills/hooks/state.md +209 -8
- package/skills/hooks/urls.md +40 -1
- package/skills/host-router/SKILL.md +13 -0
- package/skills/intercept/SKILL.md +6 -0
- package/skills/loader/SKILL.md +19 -1
- package/skills/middleware/SKILL.md +2 -0
- package/skills/ppr/SKILL.md +119 -40
- package/skills/prerender/SKILL.md +7 -3
- package/skills/rango/SKILL.md +8 -7
- package/skills/router-setup/SKILL.md +15 -1
- package/skills/testing/SKILL.md +30 -26
- package/skills/testing/client-components.md +193 -8
- package/skills/testing/handles.md +2 -0
- package/skills/testing/loader.md +1 -1
- package/skills/testing/middleware.md +1 -1
- package/skills/testing/render-handler.md +18 -18
- package/skills/testing/serve-shell-request.md +63 -14
- package/skills/testing/server-actions.md +17 -17
- package/skills/testing/server-tree.md +1 -1
- package/skills/typesafety/generated-files-and-cli.md +1 -1
- package/skills/use-cache/SKILL.md +16 -5
- package/skills/vercel/SKILL.md +3 -3
- package/src/bin/rango.ts +1 -1
- package/src/browser/event-controller.ts +75 -2
- package/src/browser/history-state.ts +39 -17
- package/src/browser/navigation-bridge.ts +41 -18
- package/src/browser/navigation-transaction.ts +15 -10
- package/src/browser/partial-update.ts +10 -2
- package/src/browser/rango-state.ts +3 -1
- package/src/browser/react/Link.tsx +2 -3
- package/src/browser/react/NavigationProvider.tsx +25 -1
- package/src/browser/react/context.ts +13 -0
- package/src/browser/react/location-state-shared.ts +200 -34
- package/src/browser/react/location-state.ts +40 -75
- package/src/browser/react/subscribe-to-nothing.ts +7 -0
- package/src/browser/react/use-handle.ts +20 -1
- package/src/browser/rsc-router.tsx +33 -11
- package/src/browser/server-action-bridge.ts +17 -1
- package/src/cache/cache-scope.ts +18 -8
- package/src/cache/cf/cf-cache-store.ts +75 -55
- package/src/cache/cf/cf-cache-types.ts +12 -4
- package/src/cache/handle-snapshot.ts +69 -87
- package/src/cache/index.ts +4 -0
- package/src/cache/shell-snapshot.ts +35 -5
- package/src/cache/types.ts +21 -10
- package/src/cache/vercel/vercel-cache-store.ts +48 -20
- package/src/client-urls/client-root.tsx +1 -0
- package/src/client-urls/navigation.ts +10 -0
- package/src/client-urls/optimistic-location.ts +10 -4
- package/src/handles/deferred-resolution.ts +31 -0
- package/src/prerender/build-shell-capture.ts +11 -9
- package/src/router/manifest.ts +2 -0
- package/src/router/match-middleware/cache-lookup.ts +11 -18
- package/src/router/navigation-snapshot.ts +25 -6
- package/src/router/request-classification.ts +15 -12
- package/src/router/router-interfaces.ts +2 -1
- package/src/router/router-options.ts +17 -7
- package/src/router/segment-resolution/loader-cache.ts +95 -60
- package/src/router-versions.ts +60 -0
- package/src/rsc/full-payload.ts +6 -2
- package/src/rsc/handler-context.ts +5 -0
- package/src/rsc/handler.ts +14 -4
- package/src/rsc/rsc-rendering.ts +12 -1
- package/src/rsc/shell-build-manifest.ts +10 -7
- package/src/rsc/shell-capture.ts +36 -26
- package/src/rsc/types.ts +4 -6
- package/src/server/build-version-table.ts +115 -0
- package/src/server/handle-store.ts +344 -127
- package/src/server/request-context.ts +62 -11
- package/src/ssr/ssr-root.tsx +16 -4
- package/src/testing/build-versions.ts +64 -0
- package/src/testing/dispatch.ts +6 -0
- package/src/testing/dom.entry.ts +1 -0
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +3 -0
- package/src/testing/internal/context.ts +2 -1
- package/src/testing/internal/seed-vars.ts +14 -2
- package/src/testing/location-state-key.ts +18 -8
- package/src/testing/render-handler.ts +2 -1
- package/src/testing/render-route.tsx +277 -43
- package/src/testing/serve-shell-request.ts +80 -11
- package/src/testing/vitest-stubs/version.ts +4 -1
- package/src/types/handler-context.ts +8 -5
- package/src/vite/discovery/build-versions.ts +1281 -0
- package/src/vite/discovery/installed-packages.ts +159 -0
- package/src/vite/discovery/prerender-collection.ts +38 -1
- package/src/vite/discovery/router-versions-phase.ts +336 -0
- package/src/vite/discovery/shell-prerender-phase.ts +28 -38
- package/src/vite/discovery/state.ts +19 -0
- package/src/vite/discovery/virtual-module-codegen.ts +7 -2
- package/src/vite/encryption-key.ts +101 -9
- package/src/vite/plugin-types.ts +28 -0
- package/src/vite/plugins/{version-injector.ts → entry-bootstrap-injector.ts} +7 -22
- package/src/vite/plugins/expose-ids/export-analysis.ts +1 -1
- package/src/vite/plugins/expose-ids/router-transform.ts +36 -15
- package/src/vite/plugins/expose-internal-ids.ts +12 -3
- package/src/vite/plugins/stable-rsc-output.ts +147 -0
- package/src/vite/plugins/version-plugin.ts +19 -24
- package/src/vite/plugins/version.d.ts +17 -2
- package/src/vite/plugins/virtual-entries.ts +31 -5
- package/src/vite/rango.ts +28 -3
- package/src/vite/router-discovery.ts +82 -13
- package/src/vite/utils/client-chunks.ts +5 -1
- package/src/vite/utils/compare-strings.ts +9 -0
- package/src/vite/utils/package-resolution.ts +9 -0
- package/src/vite/utils/prerender-utils.ts +4 -1
- package/src/vite/utils/shared-utils.ts +1 -1
package/src/cache/cache-scope.ts
CHANGED
|
@@ -735,15 +735,14 @@ export class CacheScope {
|
|
|
735
735
|
* @param pathname - URL pathname for cache key generation
|
|
736
736
|
* @param params - Route params for cache key generation
|
|
737
737
|
* @param isIntercept - Whether this is an intercept navigation (uses different cache key)
|
|
738
|
-
* @param ownedPushes -
|
|
739
|
-
* loader-
|
|
740
|
-
* _claimLoaderPushes, and on a document HIT tail the owners it restores)
|
|
738
|
+
* @param ownedPushes - What the record is to each loader's handle pushes
|
|
739
|
+
* (withCacheLookup: loader-cache.ts loaderPins), called on a hit
|
|
741
740
|
*/
|
|
742
741
|
async lookupRoute(
|
|
743
742
|
pathname: string,
|
|
744
743
|
params: Record<string, string>,
|
|
745
744
|
isIntercept?: boolean,
|
|
746
|
-
ownedPushes?: OwnedPushDelivery,
|
|
745
|
+
ownedPushes?: () => OwnedPushDelivery,
|
|
747
746
|
): Promise<{
|
|
748
747
|
segments: ResolvedSegment[];
|
|
749
748
|
shouldRevalidate: boolean;
|
|
@@ -779,7 +778,7 @@ export class CacheScope {
|
|
|
779
778
|
pathname: string,
|
|
780
779
|
params: Record<string, string>,
|
|
781
780
|
isIntercept?: boolean,
|
|
782
|
-
ownedPushes?: OwnedPushDelivery,
|
|
781
|
+
ownedPushes?: () => OwnedPushDelivery,
|
|
783
782
|
): Promise<CacheRouteLookupOutcome> {
|
|
784
783
|
if (!this.enabled) return { status: "bypass" };
|
|
785
784
|
if (!this.conditionAllows("read")) return { status: "bypass" };
|
|
@@ -870,11 +869,23 @@ export class CacheScope {
|
|
|
870
869
|
cached.tags,
|
|
871
870
|
);
|
|
872
871
|
|
|
872
|
+
// Before the handle replay: the implicit doc scope's marker arms the
|
|
873
|
+
// loader pins of a navigation replay here (matchPartialWithPprReplay),
|
|
874
|
+
// and `ownedPushes` reads them to decide which loader-owned values
|
|
875
|
+
// stand. The segments decoded, so the record serves from here on.
|
|
876
|
+
await this.onHit?.();
|
|
877
|
+
|
|
878
|
+
const handleStore = _getRequestContext()?._handleStore;
|
|
879
|
+
// With or without a handles blob: a pinned loader whose capture pushed
|
|
880
|
+
// nothing has no copy in the record, and its run on this request must
|
|
881
|
+
// still add no settled push (HandleStore.setRecordAuthority).
|
|
882
|
+
const owned = handleStore ? ownedPushes?.() : undefined;
|
|
883
|
+
if (owned) handleStore?.setRecordAuthority(owned);
|
|
884
|
+
|
|
873
885
|
// Replay handle data. An empty string means the route pushed no handles —
|
|
874
886
|
// skip the decode entirely (the common case). Otherwise decode the
|
|
875
887
|
// Flight-encoded blob; a decode failure skips handle restore but keeps the
|
|
876
888
|
// valid cached segments.
|
|
877
|
-
const handleStore = _getRequestContext()?._handleStore;
|
|
878
889
|
if (handleStore && cached.handles) {
|
|
879
890
|
const handlesRecord = await decodeHandles(cached.handles);
|
|
880
891
|
if (handlesRecord) {
|
|
@@ -888,7 +899,7 @@ export class CacheScope {
|
|
|
888
899
|
handlesRecord,
|
|
889
900
|
handleStore,
|
|
890
901
|
cached.handleOwners,
|
|
891
|
-
|
|
902
|
+
owned,
|
|
892
903
|
);
|
|
893
904
|
}
|
|
894
905
|
}
|
|
@@ -905,7 +916,6 @@ export class CacheScope {
|
|
|
905
916
|
if (ambientContext)
|
|
906
917
|
this.noteRecordWindow(ambientContext, cached.expiresAt, false);
|
|
907
918
|
|
|
908
|
-
await this.onHit?.();
|
|
909
919
|
return { status: "hit", result: { segments, shouldRevalidate } };
|
|
910
920
|
} catch (error) {
|
|
911
921
|
// Covers a store.get() failure AND a throwing consumer key()/keyGenerator
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/// <reference path="../../vite/plugins/version.d.ts" />
|
|
2
|
-
|
|
3
1
|
// Extend CacheStorage with Cloudflare's default cache property
|
|
4
2
|
declare global {
|
|
5
3
|
interface CacheStorage {
|
|
@@ -50,7 +48,10 @@ import {
|
|
|
50
48
|
type RequestContext,
|
|
51
49
|
} from "../../server/request-context.js";
|
|
52
50
|
import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
|
|
53
|
-
import {
|
|
51
|
+
import {
|
|
52
|
+
versionKeyPrefix,
|
|
53
|
+
type KeyVersion,
|
|
54
|
+
} from "../../server/build-version-table.js";
|
|
54
55
|
import {
|
|
55
56
|
isPerClientSignalHeader,
|
|
56
57
|
stripPerClientSignals,
|
|
@@ -314,10 +315,9 @@ interface KVItemEnvelope {
|
|
|
314
315
|
|
|
315
316
|
/**
|
|
316
317
|
* Key namespace of the shell family. `shell2:` holds the prelude-first frame
|
|
317
|
-
* (cf-shell-frame.ts); `shell:` held the JSON envelope.
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
* a frame. Old keys age out by their TTL.
|
|
318
|
+
* (cf-shell-frame.ts); `shell:` held the JSON envelope. The separate
|
|
319
|
+
* namespace keeps new code from reporting an old body as corrupt and a
|
|
320
|
+
* rollback from parsing a frame. Old keys age out by their TTL.
|
|
321
321
|
*/
|
|
322
322
|
const SHELL_KEY_PREFIX = "shell2:";
|
|
323
323
|
|
|
@@ -494,7 +494,8 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
494
494
|
private readonly namespace?: string;
|
|
495
495
|
private readonly explicitBaseUrl?: string;
|
|
496
496
|
private readonly waitUntil?: (fn: () => Promise<void>) => void;
|
|
497
|
-
|
|
497
|
+
/** The `version` option: used for every versioned family when set. */
|
|
498
|
+
private readonly explicitVersion?: string;
|
|
498
499
|
private readonly edgeLookupTimeoutMs: number;
|
|
499
500
|
private readonly edgeReadTimeoutMs: number;
|
|
500
501
|
private readonly kvReadTimeoutMs: number;
|
|
@@ -540,7 +541,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
540
541
|
// the internal host. Only the explicit override can be captured eagerly.
|
|
541
542
|
this.explicitBaseUrl = options.baseUrl;
|
|
542
543
|
this.defaults = options.defaults;
|
|
543
|
-
this.
|
|
544
|
+
this.explicitVersion = options.version;
|
|
544
545
|
// Coalesce only finite numbers to the override; a non-finite value (NaN from
|
|
545
546
|
// `Number(env.UNSET)`, or Infinity) would otherwise sail past `?? DEFAULT`
|
|
546
547
|
// (which only replaces null/undefined) into setTimeout, where NaN/Infinity
|
|
@@ -1134,7 +1135,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1134
1135
|
if (this.isReservedSegmentKey(key, "cache-read")) return null;
|
|
1135
1136
|
try {
|
|
1136
1137
|
const cache = await this.getCache();
|
|
1137
|
-
const request = this.keyToRequest(key);
|
|
1138
|
+
const request = this.keyToRequest(key, "data");
|
|
1138
1139
|
const matchStart = Date.now();
|
|
1139
1140
|
const {
|
|
1140
1141
|
response,
|
|
@@ -1381,7 +1382,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1381
1382
|
if (this.skipUncacheableTagSet(data.tags)) return;
|
|
1382
1383
|
try {
|
|
1383
1384
|
const cache = await this.getCache();
|
|
1384
|
-
const request = this.keyToRequest(key);
|
|
1385
|
+
const request = this.keyToRequest(key, "data");
|
|
1385
1386
|
|
|
1386
1387
|
// Extended TTL covers SWR window
|
|
1387
1388
|
const swrWindow = resolveSwrWindow(swr, this.defaults);
|
|
@@ -1457,11 +1458,11 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1457
1458
|
if (this.isReservedSegmentKey(key, "cache-delete")) return false;
|
|
1458
1459
|
try {
|
|
1459
1460
|
const cache = await this.getCache();
|
|
1460
|
-
const result = await cache.delete(this.keyToRequest(key));
|
|
1461
|
+
const result = await cache.delete(this.keyToRequest(key, "data"));
|
|
1461
1462
|
|
|
1462
1463
|
// L2: delete from KV
|
|
1463
1464
|
if (this.kv && this.waitUntil) {
|
|
1464
|
-
const kvKey = await this.toKVKey(key);
|
|
1465
|
+
const kvKey = await this.toKVKey(key, "data");
|
|
1465
1466
|
this.waitUntil(() =>
|
|
1466
1467
|
reportingAsync(
|
|
1467
1468
|
() => this.kv!.delete(kvKey),
|
|
@@ -1492,7 +1493,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1492
1493
|
): Promise<{ response: Response; shouldRevalidate: boolean } | null> {
|
|
1493
1494
|
try {
|
|
1494
1495
|
const cache = await this.getCache();
|
|
1495
|
-
const request = this.keyToRequest(`doc:${key}
|
|
1496
|
+
const request = this.keyToRequest(`doc:${key}`, "document");
|
|
1496
1497
|
// The document path is outside the debug surface (op is only get/getItem),
|
|
1497
1498
|
// so the match-timeout flag is not surfaced as an event here -- though
|
|
1498
1499
|
// matchWithTimeout still warns on a slow match. A miss or timeout falls
|
|
@@ -1633,7 +1634,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1633
1634
|
if (this.skipUncacheableTagSet(tags)) return;
|
|
1634
1635
|
try {
|
|
1635
1636
|
const cache = await this.getCache();
|
|
1636
|
-
const request = this.keyToRequest(`doc:${key}
|
|
1637
|
+
const request = this.keyToRequest(`doc:${key}`, "document");
|
|
1637
1638
|
|
|
1638
1639
|
// Extended TTL covers SWR window
|
|
1639
1640
|
const swrWindow = resolveSwrWindow(swr, this.defaults);
|
|
@@ -1747,7 +1748,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1747
1748
|
async getItem(key: string): Promise<CacheItemResult | null> {
|
|
1748
1749
|
try {
|
|
1749
1750
|
const cache = await this.getCache();
|
|
1750
|
-
const request = this.keyToRequest(`fn:${key}
|
|
1751
|
+
const request = this.keyToRequest(`fn:${key}`, "data");
|
|
1751
1752
|
const matchStart = Date.now();
|
|
1752
1753
|
const {
|
|
1753
1754
|
response,
|
|
@@ -1932,7 +1933,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1932
1933
|
if (this.skipUncacheableTagSet(options?.tags)) return;
|
|
1933
1934
|
try {
|
|
1934
1935
|
const cache = await this.getCache();
|
|
1935
|
-
const request = this.keyToRequest(`fn:${key}
|
|
1936
|
+
const request = this.keyToRequest(`fn:${key}`, "data");
|
|
1936
1937
|
|
|
1937
1938
|
const ttl = resolveTtl(options?.ttl, this.defaults, DEFAULT_FUNCTION_TTL);
|
|
1938
1939
|
const swrWindow = resolveSwrWindow(options?.swr, this.defaults);
|
|
@@ -1984,7 +1985,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1984
1985
|
// JSON.stringify(KVItemEnvelope); field names differ from L1 so we assemble
|
|
1985
1986
|
// from the pre-escaped value/handles pieces rather than re-stringifying.
|
|
1986
1987
|
if (this.kv && this.waitUntil && totalTtl >= 60) {
|
|
1987
|
-
const kvKey = await this.toKVKey(`fn:${key}
|
|
1988
|
+
const kvKey = await this.toKVKey(`fn:${key}`, "data");
|
|
1988
1989
|
const expiresAt = staleAt + swrWindow * 1000;
|
|
1989
1990
|
let envelopeJson = `{"v":${valueJson}`;
|
|
1990
1991
|
if (handlesJson !== undefined) envelopeJson += `,"h":${handlesJson}`;
|
|
@@ -2154,7 +2155,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2154
2155
|
}
|
|
2155
2156
|
try {
|
|
2156
2157
|
const cache = await this.getCache();
|
|
2157
|
-
const request = this.
|
|
2158
|
+
const request = this.shellRequest(key);
|
|
2158
2159
|
const matchStartedAt = stats ? performance.now() : 0;
|
|
2159
2160
|
const {
|
|
2160
2161
|
response,
|
|
@@ -2297,14 +2298,17 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2297
2298
|
}
|
|
2298
2299
|
}
|
|
2299
2300
|
|
|
2300
|
-
/**
|
|
2301
|
+
/**
|
|
2302
|
+
* The isolate marker memo's key for a tag: markers are per namespace, and
|
|
2303
|
+
* shared by every version (tagMarkerKey).
|
|
2304
|
+
*/
|
|
2301
2305
|
private markerMemoKey(tag: string): string {
|
|
2302
|
-
return `${this.namespace ?? ""}\u0000${
|
|
2306
|
+
return `${this.namespace ?? ""}\u0000${tag}`;
|
|
2303
2307
|
}
|
|
2304
2308
|
|
|
2305
|
-
/** The memo key: one per namespace,
|
|
2309
|
+
/** The memo key: one per namespace, document version, base URL, and shell key. */
|
|
2306
2310
|
private shellMemoKey(key: string): string {
|
|
2307
|
-
return `${this.namespace ?? ""}\u0000${this.
|
|
2311
|
+
return `${this.namespace ?? ""}\u0000${this.shellRequest(key).url}`;
|
|
2308
2312
|
}
|
|
2309
2313
|
|
|
2310
2314
|
/**
|
|
@@ -2571,15 +2575,12 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2571
2575
|
const evict = async (): Promise<void> => {
|
|
2572
2576
|
const cache = await this.getCache();
|
|
2573
2577
|
await reportingAsync(
|
|
2574
|
-
() => cache.delete(this.
|
|
2578
|
+
() => cache.delete(this.shellRequest(key)),
|
|
2575
2579
|
"cache-delete",
|
|
2576
2580
|
"[CFCacheStore] getShell: evict corrupt L1",
|
|
2577
2581
|
);
|
|
2578
2582
|
if (this.kv) {
|
|
2579
|
-
await this.evictKvKey(
|
|
2580
|
-
await this.toKVKey(`${SHELL_KEY_PREFIX}${key}`),
|
|
2581
|
-
"getShell",
|
|
2582
|
-
);
|
|
2583
|
+
await this.evictKvKey(await this.shellKVKey(key), "getShell");
|
|
2583
2584
|
}
|
|
2584
2585
|
};
|
|
2585
2586
|
if (this.waitUntil) this.waitUntil(evict);
|
|
@@ -2641,9 +2642,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2641
2642
|
Array.isArray(tags) && tags.length > 0 ? entry.createdAt : undefined;
|
|
2642
2643
|
|
|
2643
2644
|
const writeKv = !!this.kv && retentionTtl > 0;
|
|
2644
|
-
const kvKey = writeKv
|
|
2645
|
-
? await this.toKVKey(`${SHELL_KEY_PREFIX}${key}`)
|
|
2646
|
-
: null;
|
|
2645
|
+
const kvKey = writeKv ? await this.shellKVKey(key) : null;
|
|
2647
2646
|
|
|
2648
2647
|
const write = (async (): Promise<"stored" | "invalidated" | void> => {
|
|
2649
2648
|
if (
|
|
@@ -2684,7 +2683,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2684
2683
|
try {
|
|
2685
2684
|
const cache = await this.getCache();
|
|
2686
2685
|
await cache.put(
|
|
2687
|
-
this.
|
|
2686
|
+
this.shellRequest(key),
|
|
2688
2687
|
this.shellFrameResponse(frame, head),
|
|
2689
2688
|
);
|
|
2690
2689
|
this.debugShell(key, "l1-stored", { expiresAt: head.e });
|
|
@@ -2781,7 +2780,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2781
2780
|
// One kvReadTimeoutMs budget covers opening the value and reading its
|
|
2782
2781
|
// head and prelude; the remainder after the open bounds the frame read.
|
|
2783
2782
|
const deadline = Date.now() + this.kvReadTimeoutMs;
|
|
2784
|
-
const kvKey = await this.
|
|
2783
|
+
const kvKey = await this.shellKVKey(key);
|
|
2785
2784
|
// A transient read REJECTION propagates to the catch below (reported
|
|
2786
2785
|
// cache-read, the entry left intact); only a malformed frame on a body
|
|
2787
2786
|
// that WAS read is corruption that evicts.
|
|
@@ -2884,7 +2883,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2884
2883
|
if (Date.now() > head.e) return;
|
|
2885
2884
|
const cache = await this.getCache();
|
|
2886
2885
|
await cache.put(
|
|
2887
|
-
this.
|
|
2886
|
+
this.shellRequest(key),
|
|
2888
2887
|
this.shellFrameResponse(
|
|
2889
2888
|
encodeShellFrame(head, prelude, snapshotBytes),
|
|
2890
2889
|
head,
|
|
@@ -2905,18 +2904,30 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2905
2904
|
// Key Helpers
|
|
2906
2905
|
// ============================================================================
|
|
2907
2906
|
|
|
2907
|
+
private shellRequest(key: string): Request {
|
|
2908
|
+
return this.keyToRequest(`${SHELL_KEY_PREFIX}${key}`, "document");
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
private shellKVKey(key: string): Promise<string> {
|
|
2912
|
+
return this.toKVKey(`${SHELL_KEY_PREFIX}${key}`, "document");
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2908
2915
|
/**
|
|
2909
2916
|
* Convert string key to Request object for CF Cache API.
|
|
2910
|
-
*
|
|
2917
|
+
*
|
|
2918
|
+
* `family` picks the version in the URL path, so a version change misses:
|
|
2919
|
+
* `"data"` for segment entries and `fn:` items (cached RSC data, valid while
|
|
2920
|
+
* the router's server code is unchanged), `"document"` for `doc:` responses
|
|
2921
|
+
* and `shell2:` shells (their bytes carry asset URLs and the handler
|
|
2922
|
+
* version), `null` for tag markers, which apply to every version.
|
|
2911
2923
|
* @internal
|
|
2912
2924
|
*/
|
|
2913
|
-
private keyToRequest(key: string): Request {
|
|
2925
|
+
private keyToRequest(key: string, family: KeyVersion): Request {
|
|
2914
2926
|
const encodedKey = encodeURIComponent(key);
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
});
|
|
2927
|
+
return new Request(
|
|
2928
|
+
`${this.resolveBaseUrl()}${versionKeyPrefix(this.explicitVersion, family)}${encodedKey}`,
|
|
2929
|
+
{ method: "GET" },
|
|
2930
|
+
);
|
|
2920
2931
|
}
|
|
2921
2932
|
|
|
2922
2933
|
/**
|
|
@@ -2937,9 +2948,8 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2937
2948
|
* and delete paths agree on the stored key.
|
|
2938
2949
|
* @internal
|
|
2939
2950
|
*/
|
|
2940
|
-
private async toKVKey(key: string): Promise<string> {
|
|
2941
|
-
const
|
|
2942
|
-
const composed = `${versionPath}${key}`;
|
|
2951
|
+
private async toKVKey(key: string, family: KeyVersion): Promise<string> {
|
|
2952
|
+
const composed = `${versionKeyPrefix(this.explicitVersion, family)}${key}`;
|
|
2943
2953
|
if (kvKeyByteLength(composed) <= KV_MAX_KEY_BYTES) return composed;
|
|
2944
2954
|
const prefix = truncateToBytes(composed, KV_KEY_PRESERVED_PREFIX_BYTES);
|
|
2945
2955
|
return `${prefix}~${await kvKeyDigest(composed)}`;
|
|
@@ -2973,7 +2983,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2973
2983
|
* @internal
|
|
2974
2984
|
*/
|
|
2975
2985
|
private toDocKVKey(key: string): Promise<string> {
|
|
2976
|
-
return this.toKVKey(`h/${this.docKVHost()}/doc:${key}
|
|
2986
|
+
return this.toKVKey(`h/${this.docKVHost()}/doc:${key}`, "document");
|
|
2977
2987
|
}
|
|
2978
2988
|
|
|
2979
2989
|
/**
|
|
@@ -3087,9 +3097,15 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
3087
3097
|
// Tag Invalidation (single-store: markers live in this.kv)
|
|
3088
3098
|
// ============================================================================
|
|
3089
3099
|
|
|
3090
|
-
/**
|
|
3100
|
+
/**
|
|
3101
|
+
* KV key for a tag's invalidation marker. Unversioned: a content-hash
|
|
3102
|
+
* version can come back (deploy A, deploy B, roll back to A), and a marker
|
|
3103
|
+
* written while B was live has to be visible to A's entries. The staleness
|
|
3104
|
+
* check compares the marker time with the entry's taggedAt and nothing else
|
|
3105
|
+
* (isGloballyInvalidated), so one marker per tag serves every version.
|
|
3106
|
+
*/
|
|
3091
3107
|
private tagMarkerKey(tag: string): Promise<string> {
|
|
3092
|
-
return this.toKVKey(`${TAG_MARKER_PREFIX}${tag}
|
|
3108
|
+
return this.toKVKey(`${TAG_MARKER_PREFIX}${tag}`, null);
|
|
3093
3109
|
}
|
|
3094
3110
|
|
|
3095
3111
|
/**
|
|
@@ -3320,9 +3336,13 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
3320
3336
|
return false;
|
|
3321
3337
|
}
|
|
3322
3338
|
|
|
3323
|
-
/**
|
|
3339
|
+
/**
|
|
3340
|
+
* Synthetic Cache API request for a tag's L1-cached invalidation marker.
|
|
3341
|
+
* Unversioned for the same reason as tagMarkerKey; the cache namespace
|
|
3342
|
+
* (getCache) still scopes it.
|
|
3343
|
+
*/
|
|
3324
3344
|
private tagMarkerRequest(tag: string): Request {
|
|
3325
|
-
return this.keyToRequest(`${TAG_MARKER_CACHE_PREFIX}${tag}
|
|
3345
|
+
return this.keyToRequest(`${TAG_MARKER_CACHE_PREFIX}${tag}`, null);
|
|
3326
3346
|
}
|
|
3327
3347
|
|
|
3328
3348
|
/**
|
|
@@ -3974,7 +3994,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
3974
3994
|
if (!this.kv) return null;
|
|
3975
3995
|
|
|
3976
3996
|
try {
|
|
3977
|
-
const kvKey = await this.toKVKey(key);
|
|
3997
|
+
const kvKey = await this.toKVKey(key, "data");
|
|
3978
3998
|
const { value: envelope, timedOut } =
|
|
3979
3999
|
await this.kvGetOrEvict<KVSegmentEnvelope>(
|
|
3980
4000
|
kvKey,
|
|
@@ -4076,7 +4096,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
4076
4096
|
this.waitUntil(() =>
|
|
4077
4097
|
reportingAsync(
|
|
4078
4098
|
async () =>
|
|
4079
|
-
this.kv!.put(await this.toKVKey(key), envelopeJson, {
|
|
4099
|
+
this.kv!.put(await this.toKVKey(key, "data"), envelopeJson, {
|
|
4080
4100
|
expirationTtl: totalTtl,
|
|
4081
4101
|
}),
|
|
4082
4102
|
"cache-write",
|
|
@@ -4101,7 +4121,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
4101
4121
|
Math.floor((envelope.e - now) / 1000),
|
|
4102
4122
|
);
|
|
4103
4123
|
const cache = await this.getCache();
|
|
4104
|
-
const request = this.keyToRequest(key);
|
|
4124
|
+
const request = this.keyToRequest(key, "data");
|
|
4105
4125
|
|
|
4106
4126
|
const response = new Response(JSON.stringify(envelope.d), {
|
|
4107
4127
|
headers: {
|
|
@@ -4137,7 +4157,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
4137
4157
|
if (!this.kv) return null;
|
|
4138
4158
|
|
|
4139
4159
|
try {
|
|
4140
|
-
const kvKey = await this.toKVKey(`fn:${key}
|
|
4160
|
+
const kvKey = await this.toKVKey(`fn:${key}`, "data");
|
|
4141
4161
|
const { value: envelope, timedOut } =
|
|
4142
4162
|
await this.kvGetOrEvict<KVItemEnvelope>(
|
|
4143
4163
|
kvKey,
|
|
@@ -4221,7 +4241,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
4221
4241
|
Math.floor((envelope.e - now) / 1000),
|
|
4222
4242
|
);
|
|
4223
4243
|
const cache = await this.getCache();
|
|
4224
|
-
const request = this.keyToRequest(`fn:${key}
|
|
4244
|
+
const request = this.keyToRequest(`fn:${key}`, "data");
|
|
4225
4245
|
|
|
4226
4246
|
const body = JSON.stringify({
|
|
4227
4247
|
value: envelope.v,
|
|
@@ -4355,7 +4375,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
4355
4375
|
Math.floor((envelope.e - now) / 1000),
|
|
4356
4376
|
);
|
|
4357
4377
|
const cache = await this.getCache();
|
|
4358
|
-
const request = this.keyToRequest(`doc:${key}
|
|
4378
|
+
const request = this.keyToRequest(`doc:${key}`, "document");
|
|
4359
4379
|
|
|
4360
4380
|
const headers = new Headers(envelope.hd);
|
|
4361
4381
|
const originalCacheControl = headers.get("Cache-Control");
|
|
@@ -320,11 +320,19 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
|
|
|
320
320
|
tagCacheTtl?: number;
|
|
321
321
|
|
|
322
322
|
/**
|
|
323
|
-
* Cache version string override. When this
|
|
324
|
-
*
|
|
323
|
+
* Cache version string override. When set, this exact value prefixes every
|
|
324
|
+
* versioned key the store writes (`v/{version}/...`), for cached data and
|
|
325
|
+
* stored HTML alike; changing it makes every earlier entry unreachable.
|
|
325
326
|
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
327
|
+
* Leave it unset for the default: the store keys with the versions of the
|
|
328
|
+
* router serving the request. Segment entries and `"use cache"` items use
|
|
329
|
+
* the router's data version; document responses and PPR shells use its
|
|
330
|
+
* document version (see `createRouter({ version })`). Those are computed
|
|
331
|
+
* from the router's built code, so a deploy that does not change the router
|
|
332
|
+
* keeps its entries.
|
|
333
|
+
*
|
|
334
|
+
* Tag invalidation markers are never versioned: an invalidation applies to
|
|
335
|
+
* the entries of every version, including one that comes back in a rollback.
|
|
328
336
|
*/
|
|
329
337
|
version?: string;
|
|
330
338
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { replayLoaderPush } from "./handle-capture.js";
|
|
10
10
|
import type { ResolvedSegment } from "../types.js";
|
|
11
|
-
import type { HandleStore } from "../server/handle-store.js";
|
|
11
|
+
import type { HandleStore, RecordAuthority } from "../server/handle-store.js";
|
|
12
12
|
import type { HandleOwners, SegmentHandleData } from "./types.js";
|
|
13
13
|
// segment-codec eagerly pulls @vitejs/plugin-rsc (a virtual: module unresolvable
|
|
14
14
|
// in plain node/vitest). It is imported LAZILY inside the two async encode/decode
|
|
@@ -134,28 +134,13 @@ export function captureHandleOwners(
|
|
|
134
134
|
return owners;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
/**
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* A PPR shell's record (capture, document HIT tail, navigation replay): the
|
|
146
|
-
* loaders the route runs on the live lane (liveLaneLoaderIds), marked on
|
|
147
|
-
* the store (HandleStore.markLiveLane). A hole's values are replayed
|
|
148
|
-
* unclaimed, so its own cache() HIT redelivers its entry's pushes in their
|
|
149
|
-
* place (HandleStore.redeliverReplays), and its run ending without a push
|
|
150
|
-
* drops them (HandleStore.settleLoaderRun).
|
|
151
|
-
*/
|
|
152
|
-
liveLane?: ReadonlySet<string>;
|
|
153
|
-
/**
|
|
154
|
-
* A document HIT tail: every owner outside `liveLane` goes through
|
|
155
|
-
* pushRestored and stands, unclaimed.
|
|
156
|
-
*/
|
|
157
|
-
restore?: boolean;
|
|
158
|
-
}
|
|
137
|
+
/**
|
|
138
|
+
* What the record being restored is to each loader's handle pushes on this
|
|
139
|
+
* request (HandleStore RecordAuthority). Built by loaderPins (loader-cache.ts)
|
|
140
|
+
* over the pins resolveLoaderData serves a loader's value from, so a value
|
|
141
|
+
* and its record copies get one answer.
|
|
142
|
+
*/
|
|
143
|
+
export type OwnedPushDelivery = (loaderId: string) => RecordAuthority;
|
|
159
144
|
|
|
160
145
|
/**
|
|
161
146
|
* Restore handle data from a cached snapshot into the handle store.
|
|
@@ -163,24 +148,12 @@ export interface OwnedPushDelivery {
|
|
|
163
148
|
* cache() record owns its segments' arrays, so it REPLACES them.
|
|
164
149
|
*
|
|
165
150
|
* A segment with `owners` is replaced with empty arrays and re-pushed in
|
|
166
|
-
* recorded order
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* restores as a plain replay.
|
|
173
|
-
*
|
|
174
|
-
* `owned.restore`: a document HIT tail restoring the shell's doc record
|
|
175
|
-
* (withCacheLookup). A bake-lane owner's values are the prelude's, so they go
|
|
176
|
-
* through pushRestored and stand: a run of that loader on the HIT (a
|
|
177
|
-
* promise-carrying `ssr: false` loader, one whose record asks for a run)
|
|
178
|
-
* reads the store, and its settled pushes are dropped, not swapped in.
|
|
179
|
-
* Nothing is claimed, so the loader's own cache() HIT still replays the
|
|
180
|
-
* pushes the record could not keep (the thenable ones pushRestored lets
|
|
181
|
-
* through). A loader the route also runs on the live lane is a hole: its
|
|
182
|
-
* values keep pushReplayed, unclaimed, and its live run replaces them
|
|
183
|
-
* (#936), even inside a restored loader's body (`owned.liveLane`).
|
|
151
|
+
* recorded order. A loader-owned value stands (pushRestored) where `owned`
|
|
152
|
+
* says the record supplies that loader's value too ("pin", "copies"), and is
|
|
153
|
+
* a placeholder (pushPlaceholder) everywhere else: without `owned`, every
|
|
154
|
+
* owner. Neither claims its loader, so the loader's own cache() HIT still
|
|
155
|
+
* delivers (the thenable pushes a restored copy lets through, or its entry's
|
|
156
|
+
* pushes in a placeholder's place).
|
|
184
157
|
*/
|
|
185
158
|
export function restoreHandles(
|
|
186
159
|
handles: Record<string, SegmentHandleData>,
|
|
@@ -188,8 +161,6 @@ export function restoreHandles(
|
|
|
188
161
|
owners?: HandleOwners,
|
|
189
162
|
owned?: OwnedPushDelivery,
|
|
190
163
|
): void {
|
|
191
|
-
let delivers: Map<string, boolean> | undefined;
|
|
192
|
-
if (owned?.liveLane) handleStore.markLiveLane(owned.liveLane);
|
|
193
164
|
for (const [segId, segHandles] of Object.entries(handles)) {
|
|
194
165
|
if (Object.keys(segHandles).length === 0) continue;
|
|
195
166
|
const segOwners = owners?.[segId];
|
|
@@ -208,23 +179,11 @@ export function restoreHandles(
|
|
|
208
179
|
handleStore.push(handleName, segId, values[i]);
|
|
209
180
|
continue;
|
|
210
181
|
}
|
|
211
|
-
const
|
|
212
|
-
if (
|
|
182
|
+
const authority = owned?.(owner);
|
|
183
|
+
if (authority === "pin" || authority === "copies") {
|
|
213
184
|
handleStore.pushRestored(handleName, segId, values[i], owner);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (hole) {
|
|
217
|
-
handleStore.pushReplayed(handleName, segId, values[i], owner);
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
|
-
delivers ??= new Map();
|
|
221
|
-
let deliver = delivers.get(owner);
|
|
222
|
-
if (deliver === undefined) {
|
|
223
|
-
deliver = owned?.claim ? owned.claim(owner) : true;
|
|
224
|
-
delivers.set(owner, deliver);
|
|
225
|
-
}
|
|
226
|
-
if (deliver) {
|
|
227
|
-
handleStore.pushReplayed(handleName, segId, values[i], owner);
|
|
185
|
+
} else {
|
|
186
|
+
handleStore.pushPlaceholder(handleName, segId, values[i], owner);
|
|
228
187
|
}
|
|
229
188
|
}
|
|
230
189
|
}
|
|
@@ -238,43 +197,66 @@ export function restoreHandles(
|
|
|
238
197
|
* restoreHandles would wipe those. `segmentId` redirects every value to one
|
|
239
198
|
* segment (the caller's).
|
|
240
199
|
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
200
|
+
* The replay of every cached unit that records by owner: a "use cache"
|
|
201
|
+
* function (useCacheRecordKey) and a loader's own cache() (loader-cache.ts
|
|
202
|
+
* recordOwnerKey, with `unitLoader` the cached loader).
|
|
203
|
+
*
|
|
204
|
+
* A `${seq}:${loaderId}` group is that loader's pushes, which reach the page
|
|
205
|
+
* once per request: `claim` (setupLoaderAccess _claimLoaderPushes) is asked
|
|
206
|
+
* once per loader and skips one that already ran or was replayed in this
|
|
207
|
+
* request; a claimed loader that runs later replaces its replayed values
|
|
208
|
+
* (HandleStore.pushReplayed). A group without an owner (`${seq}:`, or a key
|
|
209
|
+
* without ":" from a record written before owner keys, which is a segment
|
|
210
|
+
* id) is the unit's own: `unitLoader`'s, or no loader's for a function,
|
|
211
|
+
* whose pushes are plain.
|
|
212
|
+
*
|
|
213
|
+
* The claimed loaders' placeholders give way to this delivery
|
|
214
|
+
* (HandleStore.replacePlaceholders), `unitLoader`'s even when the entry
|
|
215
|
+
* recorded no push for it: the entry stands for that loader's run.
|
|
216
|
+
*
|
|
217
|
+
* No `claim` means the caller's pushes are diverted off the page (a stale
|
|
218
|
+
* refresh: cache-runtime.ts refreshView, setupLoaderAccess
|
|
219
|
+
* _runLoaderIsolated): every group is delivered, into the diverting capture,
|
|
220
|
+
* and the page's placeholders stay, or the page would lose the push.
|
|
249
221
|
*/
|
|
250
222
|
export function appendHandles(
|
|
251
223
|
handles: Record<string, SegmentHandleData>,
|
|
252
224
|
handleStore: HandleStore,
|
|
253
|
-
segmentId
|
|
254
|
-
claim
|
|
225
|
+
segmentId: string | undefined,
|
|
226
|
+
claim: ((loaderId: string) => boolean) | undefined,
|
|
227
|
+
unitLoader?: string,
|
|
255
228
|
): void {
|
|
256
|
-
|
|
257
|
-
for (const [key, segHandles] of Object.entries(handles)) {
|
|
229
|
+
const ownerOf = (key: string): string | undefined => {
|
|
258
230
|
const colon = key.indexOf(":");
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
claims.set(owner, deliver);
|
|
266
|
-
}
|
|
267
|
-
if (!deliver) continue;
|
|
231
|
+
return (colon < 0 ? "" : key.slice(colon + 1)) || unitLoader;
|
|
232
|
+
};
|
|
233
|
+
const claims = new Map<string, boolean>();
|
|
234
|
+
const ask = (owner: string | undefined): void => {
|
|
235
|
+
if (owner !== undefined && !claims.has(owner)) {
|
|
236
|
+
claims.set(owner, claim ? claim(owner) : true);
|
|
268
237
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
238
|
+
};
|
|
239
|
+
ask(unitLoader);
|
|
240
|
+
for (const key in handles) ask(ownerOf(key));
|
|
241
|
+
const deliver = (): void => {
|
|
242
|
+
for (const [key, segHandles] of Object.entries(handles)) {
|
|
243
|
+
const owner = ownerOf(key);
|
|
244
|
+
if (owner !== undefined && !claims.get(owner)) continue;
|
|
245
|
+
const target = segmentId ?? key;
|
|
246
|
+
for (const [handleName, values] of Object.entries(segHandles)) {
|
|
247
|
+
for (const value of values) {
|
|
248
|
+
if (owner !== undefined) {
|
|
249
|
+
replayLoaderPush(handleStore, handleName, target, value, owner);
|
|
250
|
+
} else {
|
|
251
|
+
handleStore.push(handleName, target, value);
|
|
252
|
+
}
|
|
276
253
|
}
|
|
277
254
|
}
|
|
278
255
|
}
|
|
256
|
+
};
|
|
257
|
+
const granted: string[] = [];
|
|
258
|
+
if (claim) {
|
|
259
|
+
for (const [owner, given] of claims) if (given) granted.push(owner);
|
|
279
260
|
}
|
|
261
|
+
handleStore.replacePlaceholders(granted, deliver);
|
|
280
262
|
}
|