@rangojs/router 0.21.0 → 0.22.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 +13 -1
- package/dist/types/browser/event-controller.d.ts +30 -13
- package/dist/types/browser/navigation-store.d.ts +9 -0
- package/dist/types/browser/partial-update.d.ts +2 -1
- package/dist/types/browser/prefetch/default-strategy.d.ts +16 -20
- package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
- package/dist/types/browser/react/context.d.ts +14 -0
- package/dist/types/browser/rsc-router.d.ts +13 -2
- package/dist/types/browser/types.d.ts +10 -0
- package/dist/types/build/generate-manifest.d.ts +2 -0
- package/dist/types/cache/background-task.d.ts +22 -0
- package/dist/types/cache/cache-key-utils.d.ts +50 -5
- package/dist/types/cache/cache-policy.d.ts +17 -0
- package/dist/types/cache/cache-scope.d.ts +1 -1
- package/dist/types/cache/cf/cf-cache-store.d.ts +7 -1
- package/dist/types/cache/document-cache.d.ts +8 -5
- package/dist/types/cache/handle-snapshot.d.ts +23 -0
- package/dist/types/cache/index.d.ts +1 -1
- package/dist/types/cache/invalidation-order.d.ts +14 -0
- package/dist/types/cache/memory-segment-store.d.ts +33 -1
- package/dist/types/cache/shell-snapshot.d.ts +10 -1
- package/dist/types/cache/store-scope.d.ts +22 -0
- package/dist/types/cache/tag-invalidation.d.ts +7 -6
- package/dist/types/cache/types.d.ts +63 -14
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +7 -1
- package/dist/types/errors.d.ts +6 -0
- package/dist/types/handles/deferred-resolution.d.ts +7 -0
- package/dist/types/internal-suspense-audit.d.ts +1 -0
- package/dist/types/loader-store.d.ts +4 -8
- package/dist/types/outlet-provider.d.ts +2 -1
- package/dist/types/prerender/cloudflare.d.ts +49 -0
- package/dist/types/prerender/create-prerender-trigger.d.ts +121 -0
- package/dist/types/prerender/index.d.ts +12 -0
- package/dist/types/prerender/memory-prerender-store.d.ts +29 -0
- package/dist/types/prerender/on-demand.d.ts +272 -0
- package/dist/types/prerender/producer-guard.d.ts +33 -0
- package/dist/types/prerender/shell-manifest-key.d.ts +1 -18
- package/dist/types/prerender/warm-request.d.ts +65 -0
- package/dist/types/prerender/warm.d.ts +65 -0
- package/dist/types/prerender/writable-store.d.ts +170 -0
- package/dist/types/prerender.d.ts +27 -0
- package/dist/types/reverse.d.ts +1 -1
- package/dist/types/route-content-wrapper.d.ts +3 -1
- package/dist/types/router/error-handling.d.ts +2 -0
- package/dist/types/router/handler-context.d.ts +1 -1
- package/dist/types/router/match-middleware/cache-lookup.d.ts +14 -0
- package/dist/types/router/pattern-matching.d.ts +2 -0
- package/dist/types/router/prefetch-default.d.ts +4 -4
- package/dist/types/router/prerender-match.d.ts +23 -1
- package/dist/types/router/route-trie-builder.d.ts +8 -1
- package/dist/types/router/router-context.d.ts +1 -0
- package/dist/types/router/router-interfaces.d.ts +30 -0
- package/dist/types/router/router-options.d.ts +24 -0
- package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +2 -1
- package/dist/types/router/segment-resolution/revalidation.d.ts +5 -1
- package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +43 -0
- package/dist/types/router/trie-matching.d.ts +2 -0
- package/dist/types/rsc/shell-build-manifest.d.ts +6 -4
- package/dist/types/rsc/shell-capture-constants.d.ts +20 -5
- package/dist/types/rsc/shell-capture.d.ts +14 -1
- package/dist/types/rsc/shell-serve.d.ts +2 -2
- package/dist/types/rsc/ssr-module-loader.d.ts +15 -0
- package/dist/types/rsc/types.d.ts +8 -0
- package/dist/types/segment-boundary-content.d.ts +17 -0
- package/dist/types/segment-loader-promise.d.ts +2 -6
- package/dist/types/server/build-version-table.d.ts +9 -0
- package/dist/types/server/context.d.ts +12 -4
- package/dist/types/server/handle-store.d.ts +2 -0
- package/dist/types/server/request-context.d.ts +63 -2
- package/dist/types/suspense-audit-react.d.ts +26 -0
- package/dist/types/suspense-audit.d.ts +157 -0
- package/dist/types/testing/action-form.d.ts +34 -0
- package/dist/types/testing/flight.entry.d.ts +2 -0
- package/dist/types/testing/index.d.ts +4 -0
- package/dist/types/testing/internal/server-modules.d.ts +8 -0
- package/dist/types/testing/serve-shell-request.d.ts +34 -4
- package/dist/types/testing/shell-status.d.ts +14 -6
- package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +9 -6
- package/dist/types/thenable-status.d.ts +6 -0
- package/dist/types/types/handler-context.d.ts +7 -0
- package/dist/types/types/route-entry.d.ts +5 -0
- package/dist/types/urls/path-helper.d.ts +18 -0
- package/dist/types/vite/discovery/state.d.ts +11 -0
- package/dist/types/vite/inject-client-debug.d.ts +6 -25
- package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
- package/dist/vite/index.js +179 -496
- package/package.json +12 -2
- package/skills/cache-guide/SKILL.md +18 -17
- package/skills/caching/SKILL.md +40 -4
- package/skills/catalog.json +1 -1
- package/skills/cloudflare/SKILL.md +131 -0
- package/skills/comparison/references/framework-comparison.md +1 -1
- package/skills/deployment-caching/SKILL.md +33 -12
- package/skills/document-cache/SKILL.md +16 -14
- package/skills/hooks/SKILL.md +22 -0
- package/skills/hooks/handle-and-actions.md +3 -4
- package/skills/hooks/navigation.md +10 -0
- package/skills/hooks/state.md +5 -4
- package/skills/host-router/SKILL.md +18 -0
- package/skills/loader/SKILL.md +6 -4
- package/skills/middleware/SKILL.md +1 -1
- package/skills/migrate-nextjs/SKILL.md +82 -9
- package/skills/ppr/SKILL.md +31 -8
- package/skills/prerender/SKILL.md +645 -8
- package/skills/rango/SKILL.md +1 -0
- package/skills/response-routes/SKILL.md +5 -5
- package/skills/route/SKILL.md +29 -0
- package/skills/testing/SKILL.md +2 -1
- package/skills/testing/cache-prerender.md +18 -17
- package/skills/testing/serve-shell-request.md +104 -8
- package/skills/use-cache/SKILL.md +9 -5
- package/skills/vercel/SKILL.md +2 -0
- package/skills/view-transitions/SKILL.md +9 -4
- package/src/browser/event-controller.ts +69 -25
- package/src/browser/link-interceptor.ts +2 -2
- package/src/browser/navigation-bridge.ts +75 -35
- package/src/browser/navigation-store.ts +26 -0
- package/src/browser/navigation-transaction.ts +9 -2
- package/src/browser/network-error-handler.ts +2 -0
- package/src/browser/partial-update.ts +48 -12
- package/src/browser/prefetch/default-strategy.ts +22 -42
- package/src/browser/react/Link.tsx +37 -42
- package/src/browser/react/NavigationProvider.tsx +63 -3
- package/src/browser/react/context.ts +17 -0
- package/src/browser/react/location-state.ts +5 -19
- package/src/browser/react/use-action.ts +4 -1
- package/src/browser/react/use-handle.ts +4 -20
- package/src/browser/react/use-link-status.ts +4 -3
- package/src/browser/react/use-navigation.ts +4 -1
- package/src/browser/react/use-params.ts +4 -1
- package/src/browser/react/use-pathname.ts +4 -1
- package/src/browser/react/use-search-params.ts +5 -3
- package/src/browser/react/use-segments.ts +36 -54
- package/src/browser/rsc-router.tsx +79 -39
- package/src/browser/segment-reconciler.ts +3 -3
- package/src/browser/server-action-bridge.ts +26 -6
- package/src/browser/types.ts +11 -0
- package/src/build/generate-manifest.ts +17 -0
- package/src/build/merge-full-manifests.ts +5 -0
- package/src/build/route-trie.ts +1 -0
- package/src/cache/background-task.ts +61 -0
- package/src/cache/cache-key-utils.ts +91 -12
- package/src/cache/cache-policy.ts +11 -2
- package/src/cache/cache-runtime.ts +23 -23
- package/src/cache/cache-scope.ts +38 -36
- package/src/cache/cf/cf-cache-store.ts +16 -11
- package/src/cache/document-cache.ts +35 -14
- package/src/cache/handle-snapshot.ts +50 -0
- package/src/cache/index.ts +1 -0
- package/src/cache/invalidation-order.ts +53 -0
- package/src/cache/memory-segment-store.ts +52 -1
- package/src/cache/shell-snapshot.ts +17 -0
- package/src/cache/store-scope.ts +36 -0
- package/src/cache/tag-invalidation.ts +7 -5
- package/src/cache/types.ts +69 -14
- package/src/cache/vercel/vercel-cache-store.ts +20 -6
- package/src/client.tsx +40 -3
- package/src/errors.ts +13 -6
- package/src/handles/deferred-resolution.ts +7 -2
- package/src/internal-suspense-audit.ts +12 -0
- package/src/loader-store.ts +4 -9
- package/src/outlet-provider.tsx +25 -1
- package/src/prerender/cloudflare.ts +130 -0
- package/src/prerender/create-prerender-trigger.ts +780 -0
- package/src/prerender/index.ts +39 -0
- package/src/prerender/memory-prerender-store.ts +115 -0
- package/src/prerender/on-demand.ts +339 -0
- package/src/prerender/producer-guard.ts +66 -0
- package/src/prerender/shell-manifest-key.ts +12 -17
- package/src/prerender/warm-request.ts +154 -0
- package/src/prerender/warm.ts +193 -0
- package/src/prerender/writable-store.ts +278 -0
- package/src/prerender.ts +29 -0
- package/src/reverse.ts +1 -1
- package/src/route-content-wrapper.tsx +107 -13
- package/src/router/error-handling.ts +12 -12
- package/src/router/find-match.ts +1 -0
- package/src/router/handler-context.ts +2 -0
- package/src/router/intercept-resolution.ts +15 -8
- package/src/router/loader-resolution.ts +2 -10
- package/src/router/match-middleware/background-revalidation.ts +2 -0
- package/src/router/match-middleware/cache-lookup.ts +230 -38
- package/src/router/pattern-matching.ts +14 -0
- package/src/router/prefetch-default.ts +4 -4
- package/src/router/prerender-match.ts +460 -352
- package/src/router/route-trie-builder.ts +9 -0
- package/src/router/router-context.ts +1 -0
- package/src/router/router-interfaces.ts +33 -0
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +20 -19
- package/src/router/segment-resolution/loader-cache.ts +15 -17
- package/src/router/segment-resolution/revalidation.ts +34 -24
- package/src/router/segment-resolution/streamed-handler-boundary.ts +133 -0
- package/src/router/trie-matching.ts +3 -0
- package/src/router.ts +107 -1
- package/src/rsc/handler.ts +51 -4
- package/src/rsc/progressive-enhancement.ts +5 -1
- package/src/rsc/response-cache-serve.ts +10 -4
- package/src/rsc/rsc-rendering.ts +53 -11
- package/src/rsc/shell-build-manifest.ts +10 -8
- package/src/rsc/shell-capture-constants.ts +39 -7
- package/src/rsc/shell-capture.ts +108 -19
- package/src/rsc/shell-serve.ts +12 -6
- package/src/rsc/ssr-module-loader.ts +26 -0
- package/src/rsc/types.ts +8 -0
- package/src/segment-boundary-content.ts +25 -0
- package/src/segment-loader-promise.ts +28 -38
- package/src/segment-system.tsx +117 -57
- package/src/server/build-version-table.ts +15 -0
- package/src/server/context.ts +40 -17
- package/src/server/cookie-store.ts +5 -0
- package/src/server/handle-store.ts +13 -0
- package/src/server/request-context.ts +87 -1
- package/src/ssr/ssr-root.tsx +20 -13
- package/src/suspense-audit-react.tsx +98 -0
- package/src/suspense-audit.ts +1030 -0
- package/src/testing/action-form.ts +66 -0
- package/src/testing/flight-runtime.d.ts +27 -0
- package/src/testing/flight.entry.ts +3 -0
- package/src/testing/index.ts +19 -0
- package/src/testing/internal/server-modules.ts +32 -0
- package/src/testing/render-route.tsx +25 -12
- package/src/testing/serve-shell-request.ts +190 -34
- package/src/testing/shell-status.ts +17 -6
- package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
- package/src/thenable-status.ts +10 -0
- package/src/types/handler-context.ts +7 -0
- package/src/types/route-entry.ts +6 -0
- package/src/urls/path-helper.ts +69 -9
- package/src/use-loader.tsx +57 -30
- package/src/vite/discovery/bundle-postprocess.ts +4 -0
- package/src/vite/discovery/discover-routers.ts +15 -0
- package/src/vite/discovery/shell-prerender-phase.ts +5 -5
- package/src/vite/discovery/state.ts +17 -1
- package/src/vite/inject-client-debug.ts +42 -20
- package/src/vite/plugins/virtual-entries.ts +8 -1
- package/src/vite/router-discovery.ts +14 -0
- package/src/vite/utils/bundle-analysis.ts +39 -7
- package/dist/types/browser/react/subscribe-to-nothing.d.ts +0 -7
- package/dist/types/segment-content-promise.d.ts +0 -13
- package/src/browser/react/subscribe-to-nothing.ts +0 -7
- package/src/segment-content-promise.ts +0 -67
|
@@ -13,6 +13,15 @@ import type { RequestContext } from "../server/request-context.js";
|
|
|
13
13
|
import { normalizeTags } from "./cache-tag.js";
|
|
14
14
|
import { reportCacheError } from "./cache-error.js";
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* How long a follower trusts an in-flight leader before evicting it and
|
|
18
|
+
* running fresh. High enough that a slow-but-healthy upstream never triggers
|
|
19
|
+
* duplicate work (a legitimate cached call taking >15s is already pathological);
|
|
20
|
+
* low enough to bound the blast radius of a wedged leader to seconds, not the
|
|
21
|
+
* isolate lifetime. Aligned with SHELL_CAPTURE_MAX_WAIT_MS.
|
|
22
|
+
*/
|
|
23
|
+
export const IN_FLIGHT_LEADER_MAX_WAIT_MS = 15_000;
|
|
24
|
+
|
|
16
25
|
/**
|
|
17
26
|
* Default TTL for route-level cache() DSL and loader cache.
|
|
18
27
|
* Applied when neither the cache options nor the store defaults specify a TTL.
|
|
@@ -184,8 +193,8 @@ export function resolveTagsOption<TEnv>(
|
|
|
184
193
|
* and on CFCacheStore would also cost a wasted KV marker read per request.
|
|
185
194
|
* Returns undefined when nothing usable remains, keeping the entry header-free.
|
|
186
195
|
*/
|
|
187
|
-
function normalizeTagList(tags: string[]): string[] | undefined {
|
|
188
|
-
const out = normalizeTags(tags);
|
|
196
|
+
export function normalizeTagList(tags: string[]): string[] | undefined {
|
|
197
|
+
const out = [...new Set(normalizeTags(tags))];
|
|
189
198
|
return out.length > 0 ? out : undefined;
|
|
190
199
|
}
|
|
191
200
|
|
|
@@ -42,7 +42,11 @@ import {
|
|
|
42
42
|
} from "./handle-snapshot.js";
|
|
43
43
|
import { startHandleCapture, useCacheRecordKey } from "./handle-capture.js";
|
|
44
44
|
import { isHandle } from "../handle.js";
|
|
45
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
requestArgKey,
|
|
47
|
+
requestKeyBase,
|
|
48
|
+
sortedSearchString,
|
|
49
|
+
} from "./cache-key-utils.js";
|
|
46
50
|
import { encodeKV } from "../encode-kv.js";
|
|
47
51
|
import { runBackground } from "./background-task.js";
|
|
48
52
|
import { observePhase, PHASES } from "../router/instrument.js";
|
|
@@ -60,6 +64,8 @@ import {
|
|
|
60
64
|
type CacheExecScope,
|
|
61
65
|
} from "./cache-exec-scope.js";
|
|
62
66
|
import { reportCacheError } from "./cache-error.js";
|
|
67
|
+
import { IN_FLIGHT_LEADER_MAX_WAIT_MS } from "./cache-policy.js";
|
|
68
|
+
import { isWarmReplace, noteWarmWrite } from "../prerender/warm-request.js";
|
|
63
69
|
import {
|
|
64
70
|
executionStart,
|
|
65
71
|
invalidatedSince,
|
|
@@ -314,15 +320,6 @@ interface InFlightExecution {
|
|
|
314
320
|
|
|
315
321
|
const inFlightExecutions = new Map<string, InFlightExecution>();
|
|
316
322
|
|
|
317
|
-
/**
|
|
318
|
-
* How long a follower trusts an in-flight leader before evicting it and
|
|
319
|
-
* running fresh. High enough that a slow-but-healthy upstream never triggers
|
|
320
|
-
* duplicate work (a legitimate cached call taking >15s is already pathological);
|
|
321
|
-
* low enough to bound the blast radius of a wedged leader to seconds, not the
|
|
322
|
-
* isolate lifetime. Aligned with SHELL_CAPTURE_MAX_WAIT_MS.
|
|
323
|
-
*/
|
|
324
|
-
const IN_FLIGHT_LEADER_MAX_WAIT_MS = 15_000;
|
|
325
|
-
|
|
326
323
|
/** Distinguishes a leader timeout from a leader rejection in raceLeader. */
|
|
327
324
|
const LEADER_TIMED_OUT = Symbol("leader-timed-out");
|
|
328
325
|
|
|
@@ -499,9 +496,11 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
499
496
|
}
|
|
500
497
|
if (ctx.params && typeof ctx.params === "object") {
|
|
501
498
|
// Include host to prevent cross-host cache collisions (same
|
|
502
|
-
// pattern as route-level cache-scope.ts key generation)
|
|
499
|
+
// pattern as route-level cache-scope.ts key generation), led by the
|
|
500
|
+
// router part: a ctx is one router's (cache-key-utils.ts, the
|
|
501
|
+
// router rule).
|
|
503
502
|
if (ctx.url?.host) {
|
|
504
|
-
keyArgs.push(ctx.url.host);
|
|
503
|
+
keyArgs.push(requestKeyBase(requestCtx, ctx.url.host, ""));
|
|
505
504
|
}
|
|
506
505
|
// Include route name to prevent collisions when the same cached
|
|
507
506
|
// function is reused across routes with identical pathname/params
|
|
@@ -529,15 +528,8 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
529
528
|
} else if (arg instanceof Request) {
|
|
530
529
|
// A raw Request (ctx.request) is request-scoped like ctx: fold in its
|
|
531
530
|
// URL with the same host-namespacing and search normalization.
|
|
532
|
-
const url = new URL(arg.url);
|
|
533
531
|
keyArgs.push(
|
|
534
|
-
|
|
535
|
-
url.host,
|
|
536
|
-
url.pathname,
|
|
537
|
-
url.searchParams,
|
|
538
|
-
undefined,
|
|
539
|
-
requestCtx?._searchParamsFilter,
|
|
540
|
-
),
|
|
532
|
+
requestArgKey(new URL(arg.url), requestCtx?._searchParamsFilter),
|
|
541
533
|
);
|
|
542
534
|
} else if (arg != null && arg === requestCtx?.env) {
|
|
543
535
|
// The request's env is left out: constant per deployment, and its
|
|
@@ -617,8 +609,12 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
617
609
|
return result;
|
|
618
610
|
}
|
|
619
611
|
|
|
620
|
-
// Cache lookup
|
|
621
|
-
|
|
612
|
+
// Cache lookup. A router.prerender() warm reads a miss and leads its own
|
|
613
|
+
// execution below: a leader that started before the warm was called may
|
|
614
|
+
// be reading data from before the change the warm publishes, and a
|
|
615
|
+
// follower writes nothing of its own.
|
|
616
|
+
const warmReplace = isWarmReplace(requestCtx);
|
|
617
|
+
const cached = warmReplace ? null : await store.getItem(cacheKey);
|
|
622
618
|
|
|
623
619
|
// Serve a cached entry on the hit path: deserialize the stored value,
|
|
624
620
|
// replay handle data (gated on tainted args), and surface the entry's tags
|
|
@@ -767,7 +763,9 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
767
763
|
ttl: profile.ttl,
|
|
768
764
|
swr: profile.swr,
|
|
769
765
|
tags: freshTags.length > 0 ? freshTags : undefined,
|
|
766
|
+
startedAt: start.at,
|
|
770
767
|
});
|
|
768
|
+
noteWarmWrite(requestCtx, "item");
|
|
771
769
|
}
|
|
772
770
|
},
|
|
773
771
|
),
|
|
@@ -806,7 +804,7 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
806
804
|
// against its OWN handle store (gated on ITS hasTaintedArgs), and records
|
|
807
805
|
// tags into its OWN request — no deserialized result is shared across
|
|
808
806
|
// requests. The store write stays exactly once (the leader's).
|
|
809
|
-
let existing = inFlightExecutions.get(cacheKey);
|
|
807
|
+
let existing = warmReplace ? undefined : inFlightExecutions.get(cacheKey);
|
|
810
808
|
while (existing) {
|
|
811
809
|
const remainingMs =
|
|
812
810
|
IN_FLIGHT_LEADER_MAX_WAIT_MS - (Date.now() - existing.registeredAt);
|
|
@@ -1022,7 +1020,9 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
1022
1020
|
ttl: profile.ttl,
|
|
1023
1021
|
swr: profile.swr,
|
|
1024
1022
|
tags: tags.length > 0 ? tags : undefined,
|
|
1023
|
+
startedAt: start.at,
|
|
1025
1024
|
});
|
|
1025
|
+
noteWarmWrite(requestCtx, "item");
|
|
1026
1026
|
} catch (writeError) {
|
|
1027
1027
|
requestCtx?._reportBackgroundError?.(writeError, "cache-write");
|
|
1028
1028
|
}
|
package/src/cache/cache-scope.ts
CHANGED
|
@@ -38,12 +38,11 @@ import {
|
|
|
38
38
|
import {
|
|
39
39
|
captureHandles,
|
|
40
40
|
captureHandleOwners,
|
|
41
|
-
|
|
41
|
+
restoreRecordHandles,
|
|
42
42
|
type OwnedPushDelivery,
|
|
43
43
|
encodeHandles,
|
|
44
|
-
decodeHandles,
|
|
45
44
|
} from "./handle-snapshot.js";
|
|
46
|
-
import {
|
|
45
|
+
import { composeCacheKeys, requestKeyBase } from "./cache-key-utils.js";
|
|
47
46
|
import {
|
|
48
47
|
DEFAULT_ROUTE_TTL,
|
|
49
48
|
isFiniteNonNegativeSeconds,
|
|
@@ -53,6 +52,7 @@ import {
|
|
|
53
52
|
} from "./cache-policy.js";
|
|
54
53
|
import type { RequestContext } from "../server/request-context.js";
|
|
55
54
|
import { runIdentityExempt } from "./cache-exec-scope.js";
|
|
55
|
+
import { isWarmReplace, noteWarmWrite } from "../prerender/warm-request.js";
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* Narrow the request's route-record window (RequestContext._routeRecordWindow)
|
|
@@ -138,7 +138,7 @@ function getDefaultRouteCacheKey(
|
|
|
138
138
|
? "intercept"
|
|
139
139
|
: (prefixOverride ?? (isPartial ? "partial" : "doc"));
|
|
140
140
|
|
|
141
|
-
return `${prefix}:${
|
|
141
|
+
return `${prefix}:${requestKeyBase(ctx, host, pathname, searchParams, params, ctx?._searchParamsFilter)}`;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
// ============================================================================
|
|
@@ -786,6 +786,12 @@ export class CacheScope {
|
|
|
786
786
|
const store = this.getStore();
|
|
787
787
|
if (!store) return { status: "bypass" };
|
|
788
788
|
|
|
789
|
+
// A router.prerender() warm renders as on a cold cache: the opt-outs
|
|
790
|
+
// above still answer `bypass`, and the miss lets the write path replace
|
|
791
|
+
// the record under the key it resolves (cacheRoute).
|
|
792
|
+
const ambientContext = _getRequestContext();
|
|
793
|
+
if (isWarmReplace(ambientContext)) return { status: "miss" };
|
|
794
|
+
|
|
789
795
|
// Resolve cache key INSIDE the try so a throwing consumer key() (or a
|
|
790
796
|
// store.keyGenerator) degrades to a cache miss (return null -> render
|
|
791
797
|
// uncached) instead of crashing the foreground render. resolveCacheKey
|
|
@@ -826,7 +832,6 @@ export class CacheScope {
|
|
|
826
832
|
// seeded record, which a document tail turns into the degrade
|
|
827
833
|
// (ShellRecordUnavailableError).
|
|
828
834
|
let segments: ResolvedSegment[];
|
|
829
|
-
const ambientContext = _getRequestContext();
|
|
830
835
|
try {
|
|
831
836
|
const codec = await import("./segment-codec.js");
|
|
832
837
|
segments = ambientContext?._shellFragmentPayload
|
|
@@ -876,32 +881,17 @@ export class CacheScope {
|
|
|
876
881
|
await this.onHit?.();
|
|
877
882
|
|
|
878
883
|
const handleStore = _getRequestContext()?._handleStore;
|
|
879
|
-
//
|
|
880
|
-
//
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
const handlesRecord = await decodeHandles(cached.handles);
|
|
891
|
-
if (handlesRecord) {
|
|
892
|
-
if (this.boundary !== undefined) {
|
|
893
|
-
const kept = new Set(segments.map((s) => s.id));
|
|
894
|
-
for (const id of Object.keys(handlesRecord)) {
|
|
895
|
-
if (!kept.has(id)) delete handlesRecord[id];
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
restoreHandles(
|
|
899
|
-
handlesRecord,
|
|
900
|
-
handleStore,
|
|
901
|
-
cached.handleOwners,
|
|
902
|
-
owned,
|
|
903
|
-
);
|
|
904
|
-
}
|
|
884
|
+
// An empty handles string means the route pushed none (the common
|
|
885
|
+
// case); a decode failure skips the restore but keeps the segments.
|
|
886
|
+
if (handleStore) {
|
|
887
|
+
await restoreRecordHandles(
|
|
888
|
+
handleStore,
|
|
889
|
+
cached,
|
|
890
|
+
ownedPushes?.(),
|
|
891
|
+
this.boundary !== undefined
|
|
892
|
+
? new Set(segments.map((s) => s.id))
|
|
893
|
+
: undefined,
|
|
894
|
+
);
|
|
905
895
|
}
|
|
906
896
|
|
|
907
897
|
if (INTERNAL_RANGO_DEBUG) {
|
|
@@ -1115,15 +1105,25 @@ export class CacheScope {
|
|
|
1115
1105
|
// the tree) or a rejected handle value as an error row and completes
|
|
1116
1106
|
// normally; stored, every HIT would render that error until expiry.
|
|
1117
1107
|
if (flightErrors.length > 0) throw flightErrors[0];
|
|
1108
|
+
// A streamed handler recovered into its boundary fallback resolves, so
|
|
1109
|
+
// Flight reports nothing; refuse like a Flight error row.
|
|
1110
|
+
const recovered = requestCtx._recoveredHandlerErrors;
|
|
1111
|
+
if (recovered?.size) {
|
|
1112
|
+
const failed = nonLoaderSegments.find((s) => recovered.has(s.id));
|
|
1113
|
+
if (failed) throw recovered.get(failed.id);
|
|
1114
|
+
}
|
|
1118
1115
|
|
|
1116
|
+
const recordTags = collectRecordTags(requestCtx, tags, [
|
|
1117
|
+
...nonLoaderSegments.map((s) => s.id),
|
|
1118
|
+
handlesOwnerId,
|
|
1119
|
+
]);
|
|
1119
1120
|
const data: CachedEntryData = {
|
|
1120
1121
|
segments: serializedSegments,
|
|
1121
1122
|
handles: encodedHandles,
|
|
1122
1123
|
expiresAt: Date.now() + ttl * 1000,
|
|
1123
|
-
tags:
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
]),
|
|
1124
|
+
tags: recordTags,
|
|
1125
|
+
// The stamp is the render's start, not the write's (#1068).
|
|
1126
|
+
...(start && recordTags?.length ? { taggedAt: start.at } : {}),
|
|
1127
1127
|
};
|
|
1128
1128
|
if (handleOwners && encodedHandles) data.handleOwners = handleOwners;
|
|
1129
1129
|
|
|
@@ -1137,6 +1137,8 @@ export class CacheScope {
|
|
|
1137
1137
|
}
|
|
1138
1138
|
|
|
1139
1139
|
await store.set(key, data, ttl, swr);
|
|
1140
|
+
// The implicit doc scope's record lives in the shell entry only.
|
|
1141
|
+
if (!this.isShellImplicitDocScope) noteWarmWrite(requestCtx, "record");
|
|
1140
1142
|
|
|
1141
1143
|
if (INTERNAL_RANGO_DEBUG) {
|
|
1142
1144
|
const segmentTypes = nonLoaderSegments.map((s) =>
|
|
@@ -1185,7 +1187,7 @@ function collectRecordTags(
|
|
|
1185
1187
|
* enclosing scopes on another store (#974) and of a scope without its own
|
|
1186
1188
|
* `key()` (#970)); with none, the app store's keyGenerator result
|
|
1187
1189
|
* given the document default key. Undefined (synchronously, with no work)
|
|
1188
|
-
* when neither applies: the shell key stays
|
|
1190
|
+
* when neither applies: the shell key stays unpartitioned (buildShellKey).
|
|
1189
1191
|
* Resolves to null when the partition is the default key itself (a
|
|
1190
1192
|
* keyGenerator that returns it unchanged): that partitions nothing, so the
|
|
1191
1193
|
* shell stays unpartitioned and keeps its build shell. The shell's key, its
|
|
@@ -27,6 +27,7 @@ declare global {
|
|
|
27
27
|
|
|
28
28
|
import { requestHeaders } from "../../server/request-headers.js";
|
|
29
29
|
import type {
|
|
30
|
+
CacheStoreScope,
|
|
30
31
|
SegmentCacheStore,
|
|
31
32
|
CachedEntryData,
|
|
32
33
|
CacheDefaults,
|
|
@@ -118,6 +119,7 @@ import {
|
|
|
118
119
|
DEFAULT_CF_MARKER_MAX_STALE_MS,
|
|
119
120
|
} from "../isolate-tag-memo.js";
|
|
120
121
|
import { createCloudflareZonePurge } from "./cf-zone-purge.js";
|
|
122
|
+
import { responseStartedAt, taggedStamp } from "../invalidation-order.js";
|
|
121
123
|
|
|
122
124
|
// ============================================================================
|
|
123
125
|
// Constants
|
|
@@ -519,6 +521,12 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
519
521
|
private readonly kv?: KVNamespace;
|
|
520
522
|
/** True when constructed without KV: no durable tag history (see ctor). */
|
|
521
523
|
readonly tagHistoryInert?: boolean;
|
|
524
|
+
/**
|
|
525
|
+
* SegmentCacheStore.scope: with KV every colo's L1 miss reads the same L2
|
|
526
|
+
* entry ("global"); without it entries live in one colo's Cache API
|
|
527
|
+
* ("local"), purge mode included (a purge evicts, it stores nothing).
|
|
528
|
+
*/
|
|
529
|
+
readonly scope: CacheStoreScope;
|
|
522
530
|
private readonly onRevalidateTag?: (tags: string[]) => Promise<void>;
|
|
523
531
|
private readonly tagPurge?: (cacheTags: string[]) => Promise<void>;
|
|
524
532
|
private readonly tagInvalidationTtl?: number;
|
|
@@ -604,6 +612,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
604
612
|
// evict it (purge cannot delete a build asset), so the manifest gate
|
|
605
613
|
// declines on this flag (shell-build-manifest.ts).
|
|
606
614
|
this.tagHistoryInert = options.kv ? undefined : true;
|
|
615
|
+
this.scope = options.kv ? "global" : "local";
|
|
607
616
|
this.onRevalidateTag = options.onRevalidateTag;
|
|
608
617
|
// tagPurge accepts a ready purge function or a credentials object; the
|
|
609
618
|
// object form is normalized through the built-in zone purge client, which
|
|
@@ -1389,13 +1398,11 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1389
1398
|
const totalTtl = ttl + swrWindow;
|
|
1390
1399
|
const staleAt = Date.now() + ttl * 1000;
|
|
1391
1400
|
|
|
1392
|
-
// Stamp the tag timestamp
|
|
1393
|
-
//
|
|
1394
|
-
//
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
? Date.now()
|
|
1398
|
-
: undefined;
|
|
1401
|
+
// Stamp the tag timestamp and carry it (with the tags) into both the
|
|
1402
|
+
// L1 body and the KV envelope so reads can run the invalidation check.
|
|
1403
|
+
// It is the render's start when the writer passed it (#1068), else the
|
|
1404
|
+
// write time: a marker written while the render ran then rejects it.
|
|
1405
|
+
const taggedAt = taggedStamp(data.tags, data.taggedAt);
|
|
1399
1406
|
const dataToStore: CachedEntryData = taggedAt
|
|
1400
1407
|
? { ...data, taggedAt }
|
|
1401
1408
|
: data;
|
|
@@ -1640,8 +1647,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1640
1647
|
const swrWindow = resolveSwrWindow(swr, this.defaults);
|
|
1641
1648
|
const totalTtl = ttl + swrWindow;
|
|
1642
1649
|
const staleAt = Date.now() + ttl * 1000;
|
|
1643
|
-
const taggedAt =
|
|
1644
|
-
Array.isArray(tags) && tags.length > 0 ? Date.now() : undefined;
|
|
1650
|
+
const taggedAt = taggedStamp(tags, responseStartedAt(response));
|
|
1645
1651
|
|
|
1646
1652
|
// Clone body for potential KV write before consuming it for L1
|
|
1647
1653
|
const [l1Body, kvBody] = this.kv
|
|
@@ -1941,8 +1947,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1941
1947
|
const staleAt = Date.now() + ttl * 1000;
|
|
1942
1948
|
|
|
1943
1949
|
const tags = options?.tags;
|
|
1944
|
-
const taggedAt =
|
|
1945
|
-
Array.isArray(tags) && tags.length > 0 ? Date.now() : undefined;
|
|
1950
|
+
const taggedAt = taggedStamp(tags, options?.startedAt);
|
|
1946
1951
|
|
|
1947
1952
|
// Serialize value/handles once; L1 body and KV envelope.v/h share the
|
|
1948
1953
|
// escaped strings so a large RSC payload is not re-escaped for L2.
|
|
@@ -22,10 +22,19 @@ import {
|
|
|
22
22
|
type RequestContext,
|
|
23
23
|
} from "../server/request-context.js";
|
|
24
24
|
import { mayNeedSSR } from "../rsc/ssr-setup.js";
|
|
25
|
-
import {
|
|
25
|
+
import { requestKeyBase } from "./cache-key-utils.js";
|
|
26
26
|
import { runBackground } from "./background-task.js";
|
|
27
|
+
import {
|
|
28
|
+
isWarmReplace,
|
|
29
|
+
noteWarmDocument,
|
|
30
|
+
noteWarmWrite,
|
|
31
|
+
} from "../prerender/warm-request.js";
|
|
27
32
|
import { reportCacheError } from "./cache-error.js";
|
|
28
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
executionStart,
|
|
35
|
+
markResponseStart,
|
|
36
|
+
predatesInvalidation,
|
|
37
|
+
} from "./tag-invalidation.js";
|
|
29
38
|
import { observePhase, PHASES } from "../router/instrument.js";
|
|
30
39
|
import {
|
|
31
40
|
SEGMENT_FRAGMENT_CAPABILITY_HEADER,
|
|
@@ -239,11 +248,14 @@ export interface DocumentCacheOptions<TEnv = any> {
|
|
|
239
248
|
/**
|
|
240
249
|
* Custom cache key generator.
|
|
241
250
|
*
|
|
242
|
-
* Replaces the default `host + pathname + search` key entirely.
|
|
243
|
-
* multi-domain deployment served by one function you MUST include
|
|
244
|
-
* (or an equivalent tenant discriminator) yourself — the default
|
|
245
|
-
* host-namespaced, but a custom generator's output is used verbatim,
|
|
246
|
-
* omitting host bleeds one hostname's cached response to another.
|
|
251
|
+
* Replaces the default `router id + host + pathname + search` key entirely.
|
|
252
|
+
* On a multi-domain deployment served by one function you MUST include
|
|
253
|
+
* `url.host` (or an equivalent tenant discriminator) yourself — the default
|
|
254
|
+
* key is host-namespaced, but a custom generator's output is used verbatim,
|
|
255
|
+
* so omitting host bleeds one hostname's cached response to another. The
|
|
256
|
+
* same goes for the router: two routers that share a cache store and can
|
|
257
|
+
* serve one host and path (a `hostOverride` cookie) need a generator that
|
|
258
|
+
* tells them apart.
|
|
247
259
|
*/
|
|
248
260
|
keyGenerator?: (url: URL) => string;
|
|
249
261
|
|
|
@@ -347,7 +359,8 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
347
359
|
return next();
|
|
348
360
|
}
|
|
349
361
|
|
|
350
|
-
// The write gate's start (#977)
|
|
362
|
+
// The write gate's start (#977) and the entry's stamp (#1068): the
|
|
363
|
+
// request's, not this middleware's, also for a stale refresh. A
|
|
351
364
|
// middleware ahead of this one can read tagged data the document bakes,
|
|
352
365
|
// and a stale refresh re-runs the handler over what they set.
|
|
353
366
|
const start = requestCtx._requestStart ?? executionStart();
|
|
@@ -392,7 +405,8 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
392
405
|
// existing keys and double any host they already include).
|
|
393
406
|
const cacheKey = keyGenerator
|
|
394
407
|
? keyGenerator(url) + segmentHash + fragmentSuffix + typeSuffix
|
|
395
|
-
:
|
|
408
|
+
: requestKeyBase(
|
|
409
|
+
requestCtx,
|
|
396
410
|
url.host,
|
|
397
411
|
url.pathname,
|
|
398
412
|
url.searchParams,
|
|
@@ -406,9 +420,11 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
406
420
|
// Recovery must reach CacheScope's server decoder so it can evict the bad
|
|
407
421
|
// segment. Treat it as a miss, then let the ordinary write path replace
|
|
408
422
|
// the corrupt fragment-capable response with the valid fallback bytes.
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
423
|
+
// A router.prerender() warm is a miss too: it renders and replaces.
|
|
424
|
+
const cached =
|
|
425
|
+
isFragmentRecovery || isWarmReplace(requestCtx)
|
|
426
|
+
? null
|
|
427
|
+
: await store.getResponse(cacheKey);
|
|
412
428
|
// Every path past the lookup either returns a fresh HIT, which renders
|
|
413
429
|
// nothing, or renders a response this cache may store.
|
|
414
430
|
requestCtx._documentCacheRender = true;
|
|
@@ -462,11 +478,13 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
462
478
|
if (await predatesInvalidation(store, tags, start)) return;
|
|
463
479
|
await store.putResponse!(
|
|
464
480
|
cacheKey,
|
|
465
|
-
new Response(body, fresh),
|
|
481
|
+
markResponseStart(new Response(body, fresh), start),
|
|
466
482
|
directives.sMaxAge!,
|
|
467
483
|
directives.staleWhileRevalidate,
|
|
468
484
|
tags,
|
|
469
485
|
);
|
|
486
|
+
noteWarmWrite(requestCtx, "response");
|
|
487
|
+
noteWarmDocument(requestCtx, "stored");
|
|
470
488
|
log(
|
|
471
489
|
`[DocumentCache] REVALIDATED ${typeLabel}: ${url.pathname}`,
|
|
472
490
|
);
|
|
@@ -531,11 +549,13 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
531
549
|
if (await predatesInvalidation(store, tags, start)) return;
|
|
532
550
|
await store.putResponse!(
|
|
533
551
|
cacheKey,
|
|
534
|
-
new Response(body, originalResponse),
|
|
552
|
+
markResponseStart(new Response(body, originalResponse), start),
|
|
535
553
|
directives.sMaxAge!,
|
|
536
554
|
directives.staleWhileRevalidate,
|
|
537
555
|
tags,
|
|
538
556
|
);
|
|
557
|
+
noteWarmWrite(requestCtx, "response");
|
|
558
|
+
noteWarmDocument(requestCtx, "stored");
|
|
539
559
|
} catch (error) {
|
|
540
560
|
// Detached waitUntil task — pass the captured requestCtx so onError
|
|
541
561
|
// fires even though the ALS context is gone (see the revalidation
|
|
@@ -556,6 +576,7 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
556
576
|
}
|
|
557
577
|
|
|
558
578
|
// No cache headers - pass through
|
|
579
|
+
noteWarmDocument(requestCtx, "not-cacheable");
|
|
559
580
|
return originalResponse;
|
|
560
581
|
} catch (error) {
|
|
561
582
|
reportCacheError(error, "cache-read", "[DocumentCache] middleware");
|
|
@@ -134,6 +134,31 @@ export function captureHandleOwners(
|
|
|
134
134
|
return owners;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/**
|
|
138
|
+
* captureHandles and captureHandleOwners cut down to the handle arrays that
|
|
139
|
+
* hold an owned value, each whole (its unowned values too, in place), so
|
|
140
|
+
* restoreHandles rebuilds exactly those arrays over a store that already
|
|
141
|
+
* holds the rest. A prerender-served shell capture stores this: the
|
|
142
|
+
* prerender store restores the handler pushes on a HIT, and the loader
|
|
143
|
+
* pushes have no other record (shell-capture.ts captureAndStoreShell).
|
|
144
|
+
*/
|
|
145
|
+
export function captureOwnedHandles(handleStore: HandleStore): {
|
|
146
|
+
handles: HandleRecord;
|
|
147
|
+
owners?: HandleOwners;
|
|
148
|
+
} {
|
|
149
|
+
const handles: HandleRecord = {};
|
|
150
|
+
let owners: HandleOwners | undefined;
|
|
151
|
+
for (const id of handleStore.getOwnedSegmentIds()) {
|
|
152
|
+
const segOwners = handleStore.getRecordOwners(id);
|
|
153
|
+
if (!segOwners) continue;
|
|
154
|
+
const kept = handleStore.getDataForSegment(id, true);
|
|
155
|
+
handles[id] = {};
|
|
156
|
+
for (const name in segOwners) handles[id][name] = kept[name] ?? [];
|
|
157
|
+
(owners ??= {})[id] = segOwners;
|
|
158
|
+
}
|
|
159
|
+
return { handles, owners };
|
|
160
|
+
}
|
|
161
|
+
|
|
137
162
|
/**
|
|
138
163
|
* What the record being restored is to each loader's handle pushes on this
|
|
139
164
|
* request (HandleStore RecordAuthority). Built by loaderPins (loader-cache.ts)
|
|
@@ -190,6 +215,31 @@ export function restoreHandles(
|
|
|
190
215
|
}
|
|
191
216
|
}
|
|
192
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Serve a doc record's handles: set its authority over the loaders' pushes
|
|
220
|
+
* (with or without a handles blob: a pinned loader whose capture pushed
|
|
221
|
+
* nothing has no copy, and its run must still add no settled push), then
|
|
222
|
+
* decode and restoreHandles. `keepIds` limits the restore to the segments a
|
|
223
|
+
* boundary-scoped scope serves. A decode failure skips the restore.
|
|
224
|
+
*/
|
|
225
|
+
export async function restoreRecordHandles(
|
|
226
|
+
handleStore: HandleStore,
|
|
227
|
+
record: { handles?: string; handleOwners?: HandleOwners },
|
|
228
|
+
owned: OwnedPushDelivery | undefined,
|
|
229
|
+
keepIds?: ReadonlySet<string>,
|
|
230
|
+
): Promise<void> {
|
|
231
|
+
if (owned) handleStore.setRecordAuthority(owned);
|
|
232
|
+
if (!record.handles) return;
|
|
233
|
+
const decoded = await decodeHandles(record.handles);
|
|
234
|
+
if (!decoded) return;
|
|
235
|
+
if (keepIds) {
|
|
236
|
+
for (const id of Object.keys(decoded)) {
|
|
237
|
+
if (!keepIds.has(id)) delete decoded[id];
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
restoreHandles(decoded, handleStore, record.handleOwners, owned);
|
|
241
|
+
}
|
|
242
|
+
|
|
193
243
|
/**
|
|
194
244
|
* Append recorded handle pushes to the store, in recorded order, via push()
|
|
195
245
|
* so active captures and loader-scope tagging see them like live pushes. For
|
package/src/cache/index.ts
CHANGED
|
@@ -43,6 +43,59 @@ export interface ExecutionStart {
|
|
|
43
43
|
readonly at: number;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* The `taggedAt` / `ta` stamp a store writes on a tagged data entry: the start
|
|
48
|
+
* of the execution that produced the value (`startedAt`), so a marker written
|
|
49
|
+
* while it ran rejects the entry on read (#1068). Write time when the writer
|
|
50
|
+
* knows no start. Never later than now: a start from a skewed clock must not
|
|
51
|
+
* stamp the future, which would make the entry look newer than every marker.
|
|
52
|
+
*/
|
|
53
|
+
export function entryStamp(startedAt: number | undefined): number {
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
// 0 is "no start", not the epoch: no marker is older than it, so such an
|
|
56
|
+
// entry would never be rejected.
|
|
57
|
+
return typeof startedAt === "number" &&
|
|
58
|
+
Number.isFinite(startedAt) &&
|
|
59
|
+
startedAt > 0
|
|
60
|
+
? Math.min(startedAt, now)
|
|
61
|
+
: now;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The stamp of a data entry: `entryStamp` for a tagged entry, none otherwise. */
|
|
65
|
+
export function taggedStamp(
|
|
66
|
+
tags: readonly string[] | undefined,
|
|
67
|
+
startedAt: number | undefined,
|
|
68
|
+
): number | undefined {
|
|
69
|
+
return Array.isArray(tags) && tags.length > 0
|
|
70
|
+
? entryStamp(startedAt)
|
|
71
|
+
: undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* putResponse has no options object to carry the start and takes no new
|
|
76
|
+
* positional parameter, so the writer marks the Response it hands over.
|
|
77
|
+
* On globalThis like the order: a second evaluated copy of this module that
|
|
78
|
+
* wrote the mark must be read by the store's copy, else the stamp silently
|
|
79
|
+
* falls back to write time.
|
|
80
|
+
*/
|
|
81
|
+
const responseStarts: WeakMap<Response, number> = ((globalThis as any)[
|
|
82
|
+
Symbol.for("rangojs-router:response-execution-start")
|
|
83
|
+
] ??= new WeakMap());
|
|
84
|
+
|
|
85
|
+
/** Mark the execution start behind a Response handed to putResponse. */
|
|
86
|
+
export function markResponseStart(
|
|
87
|
+
response: Response,
|
|
88
|
+
start: ExecutionStart,
|
|
89
|
+
): Response {
|
|
90
|
+
responseStarts.set(response, start.at);
|
|
91
|
+
return response;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The start a writer marked on `response`, if any. */
|
|
95
|
+
export function responseStartedAt(response: Response): number | undefined {
|
|
96
|
+
return responseStarts.get(response);
|
|
97
|
+
}
|
|
98
|
+
|
|
46
99
|
/** Record an execution's start, before it reads its data. */
|
|
47
100
|
export function executionStart(): ExecutionStart {
|
|
48
101
|
return { seq: order.seq, at: Date.now() };
|