@rangojs/router 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/types/browser/navigation-client.d.ts +1 -1
- package/dist/types/browser/prefetch/cache.d.ts +20 -8
- package/dist/types/cache/cf/cf-cache-store.d.ts +19 -8
- package/dist/types/cache/cf/cf-cache-types.d.ts +4 -2
- package/dist/types/cache/read-through-swr.d.ts +7 -6
- package/dist/types/cloudflare/tracing.d.ts +16 -12
- package/dist/types/router/instrument.d.ts +45 -15
- package/dist/types/router/telemetry-otel.d.ts +4 -4
- package/dist/types/router/timeout.d.ts +12 -1
- package/dist/types/router/tracing.d.ts +31 -17
- package/dist/types/rsc/capture-queue.d.ts +22 -1
- package/dist/types/rsc/shell-capture.d.ts +17 -9
- package/dist/types/rsc/stream-idle.d.ts +60 -0
- package/dist/types/segment-fragments.d.ts +27 -4
- package/dist/types/server/request-context.d.ts +28 -9
- package/dist/types/vercel/tracing.d.ts +9 -8
- package/dist/vite/index.js +1 -1
- package/package.json +1 -1
- package/skills/cloudflare/SKILL.md +4 -2
- package/skills/observability/SKILL.md +33 -4
- package/skills/ppr/SKILL.md +25 -9
- package/src/browser/navigation-client.ts +77 -27
- package/src/browser/prefetch/cache.ts +40 -10
- package/src/browser/prefetch/fetch.ts +5 -0
- package/src/browser/rsc-router.tsx +12 -6
- package/src/cache/cache-runtime.ts +39 -25
- package/src/cache/cache-scope.ts +5 -1
- package/src/cache/cf/cf-cache-store.ts +423 -90
- package/src/cache/cf/cf-cache-types.ts +4 -2
- package/src/cache/document-cache.ts +63 -25
- package/src/cache/read-through-swr.ts +22 -16
- package/src/cloudflare/tracing.ts +16 -12
- package/src/router/instrument.ts +61 -15
- package/src/router/segment-resolution/loader-cache.ts +12 -1
- package/src/router/segment-resolution/revalidation.ts +16 -1
- package/src/router/telemetry-otel.ts +4 -4
- package/src/router/timeout.ts +12 -1
- package/src/router/tracing.ts +37 -17
- package/src/rsc/capture-queue.ts +72 -17
- package/src/rsc/handler.ts +171 -73
- package/src/rsc/rsc-rendering.ts +61 -6
- package/src/rsc/shell-capture.ts +83 -31
- package/src/rsc/stream-idle.ts +137 -0
- package/src/segment-fragments.ts +45 -4
- package/src/server/request-context.ts +29 -8
- package/src/testing/dispatch.ts +55 -1
- package/src/vercel/tracing.ts +9 -8
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
_getRequestContext,
|
|
43
43
|
type RequestContext,
|
|
44
44
|
} from "../../server/request-context.js";
|
|
45
|
+
import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
|
|
45
46
|
import { VERSION } from "@rangojs/router:version";
|
|
46
47
|
import {
|
|
47
48
|
isPerClientSignalHeader,
|
|
@@ -262,10 +263,10 @@ interface KVItemEnvelope {
|
|
|
262
263
|
}
|
|
263
264
|
|
|
264
265
|
/**
|
|
265
|
-
* KV envelope for PPR shell cache entries.
|
|
266
|
+
* Coupled Cache API/KV envelope for PPR shell cache entries.
|
|
266
267
|
* @internal
|
|
267
268
|
*/
|
|
268
|
-
interface
|
|
269
|
+
interface CFShellEnvelope {
|
|
269
270
|
/** base64-encoded prelude bytes */
|
|
270
271
|
p: string;
|
|
271
272
|
/** postponed state JSON, or null (DATA variant — no holes) */
|
|
@@ -309,6 +310,67 @@ interface KVShellEnvelope {
|
|
|
309
310
|
no?: true;
|
|
310
311
|
}
|
|
311
312
|
|
|
313
|
+
type CFShellDebugOutcome =
|
|
314
|
+
| "l1-hit"
|
|
315
|
+
| "l1-miss"
|
|
316
|
+
| "kv-hit"
|
|
317
|
+
| "kv-miss"
|
|
318
|
+
| "kv-promoted"
|
|
319
|
+
| "marker-invalidated"
|
|
320
|
+
| "l1-stored"
|
|
321
|
+
| "kv-stored"
|
|
322
|
+
| "write-invalidated";
|
|
323
|
+
|
|
324
|
+
interface CFShellDebugDetails {
|
|
325
|
+
tier?: "l1" | "kv";
|
|
326
|
+
reason?:
|
|
327
|
+
| "absent"
|
|
328
|
+
| "timeout"
|
|
329
|
+
| "error"
|
|
330
|
+
| "non-200"
|
|
331
|
+
| "corrupt"
|
|
332
|
+
| "malformed"
|
|
333
|
+
| "expired"
|
|
334
|
+
| "unavailable";
|
|
335
|
+
freshness?: "fresh" | "stale";
|
|
336
|
+
status?: number;
|
|
337
|
+
matchMs?: number;
|
|
338
|
+
bodyReadMs?: number;
|
|
339
|
+
markerMs?: number;
|
|
340
|
+
readMs?: number;
|
|
341
|
+
expiresAt?: number;
|
|
342
|
+
remainingTtl?: number;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Validate the coupled PPR shell envelope before any field reaches resume. */
|
|
346
|
+
function isShellEnvelope(value: unknown): value is CFShellEnvelope {
|
|
347
|
+
if (value == null || typeof value !== "object") return false;
|
|
348
|
+
const envelope = value as Partial<CFShellEnvelope>;
|
|
349
|
+
return (
|
|
350
|
+
typeof envelope.p === "string" &&
|
|
351
|
+
(envelope.po === null || typeof envelope.po === "string") &&
|
|
352
|
+
typeof envelope.rv === "string" &&
|
|
353
|
+
(envelope.bv === undefined || typeof envelope.bv === "string") &&
|
|
354
|
+
typeof envelope.c === "number" &&
|
|
355
|
+
Number.isFinite(envelope.c) &&
|
|
356
|
+
typeof envelope.s === "number" &&
|
|
357
|
+
Number.isFinite(envelope.s) &&
|
|
358
|
+
typeof envelope.e === "number" &&
|
|
359
|
+
Number.isFinite(envelope.e) &&
|
|
360
|
+
(envelope.t === undefined ||
|
|
361
|
+
(Array.isArray(envelope.t) &&
|
|
362
|
+
envelope.t.every((tag) => typeof tag === "string"))) &&
|
|
363
|
+
(envelope.ta === undefined ||
|
|
364
|
+
(typeof envelope.ta === "number" && Number.isFinite(envelope.ta))) &&
|
|
365
|
+
(envelope.i === undefined || typeof envelope.i === "string") &&
|
|
366
|
+
(envelope.sn === undefined || Array.isArray(envelope.sn)) &&
|
|
367
|
+
(envelope.dk === undefined || typeof envelope.dk === "string") &&
|
|
368
|
+
(envelope.lh === undefined || typeof envelope.lh === "boolean") &&
|
|
369
|
+
(envelope.tw === undefined || envelope.tw === true) &&
|
|
370
|
+
(envelope.no === undefined || envelope.no === true)
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
312
374
|
/**
|
|
313
375
|
* KV envelope for document cache entries.
|
|
314
376
|
* @internal
|
|
@@ -515,6 +577,36 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
515
577
|
}
|
|
516
578
|
}
|
|
517
579
|
|
|
580
|
+
/** Build-time-gated shell tier trace for deployed cross-colo diagnostics. */
|
|
581
|
+
private debugShell(
|
|
582
|
+
key: string,
|
|
583
|
+
outcome: CFShellDebugOutcome,
|
|
584
|
+
details: CFShellDebugDetails = {},
|
|
585
|
+
): void {
|
|
586
|
+
if (!INTERNAL_RANGO_DEBUG) return;
|
|
587
|
+
const request = _getRequestContext()?.request as
|
|
588
|
+
| (Request & { cf?: { colo?: unknown } })
|
|
589
|
+
| undefined;
|
|
590
|
+
const ray = request?.headers.get("cf-ray") ?? undefined;
|
|
591
|
+
const cfColo = request?.cf?.colo;
|
|
592
|
+
const colo =
|
|
593
|
+
typeof cfColo === "string"
|
|
594
|
+
? cfColo
|
|
595
|
+
: ray?.includes("-")
|
|
596
|
+
? ray.slice(ray.lastIndexOf("-") + 1)
|
|
597
|
+
: undefined;
|
|
598
|
+
console.log(
|
|
599
|
+
`[CFCacheStore][shell] ${JSON.stringify({
|
|
600
|
+
key,
|
|
601
|
+
outcome,
|
|
602
|
+
at: Date.now(),
|
|
603
|
+
ray,
|
|
604
|
+
colo,
|
|
605
|
+
...details,
|
|
606
|
+
})}`,
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
|
|
518
610
|
/**
|
|
519
611
|
* Resolve the cache-key base URL for the current cache operation.
|
|
520
612
|
* Prefers an explicit `baseUrl` option; otherwise derives it from the live
|
|
@@ -1765,17 +1857,18 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1765
1857
|
}
|
|
1766
1858
|
|
|
1767
1859
|
// ============================================================================
|
|
1768
|
-
// Shell Cache Methods (PPR shell resume) — KV
|
|
1860
|
+
// Shell Cache Methods (PPR shell resume) — Cache API L1 + KV L2
|
|
1769
1861
|
// ============================================================================
|
|
1770
1862
|
//
|
|
1771
|
-
//
|
|
1772
|
-
//
|
|
1773
|
-
//
|
|
1774
|
-
//
|
|
1775
|
-
//
|
|
1776
|
-
//
|
|
1777
|
-
//
|
|
1778
|
-
//
|
|
1863
|
+
// KV remains the durable, cross-colo shell tier. Cache API is a per-colo
|
|
1864
|
+
// read-through accelerator: writes populate both tiers, and a valid KV hit
|
|
1865
|
+
// promotes the same coupled envelope into L1. The family still requires KV;
|
|
1866
|
+
// without it, getShell/putShell no-op and PPR fails open to a full HTML render.
|
|
1867
|
+
//
|
|
1868
|
+
// Shell L1 hits deliberately keep the KV generation-marker check even in
|
|
1869
|
+
// purge mode. A shell's taggedAt is its CAPTURE START, not its write time: an
|
|
1870
|
+
// invalidation can purge while an older capture is still running, then that
|
|
1871
|
+
// capture can land after the purge. The marker check rejects that resurrection.
|
|
1779
1872
|
|
|
1780
1873
|
/**
|
|
1781
1874
|
* Warn once per isolate that the shell family is inert: getShell/putShell
|
|
@@ -1799,10 +1892,10 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1799
1892
|
}
|
|
1800
1893
|
|
|
1801
1894
|
/**
|
|
1802
|
-
* Get a cached PPR shell entry
|
|
1803
|
-
*
|
|
1804
|
-
*
|
|
1805
|
-
*
|
|
1895
|
+
* Get a cached PPR shell entry from Cache API, falling through to KV and
|
|
1896
|
+
* promoting a valid KV hit. Both tiers store one envelope so the prelude,
|
|
1897
|
+
* postponed state, snapshot, versions, and generation metadata cannot mix.
|
|
1898
|
+
* SWR remains a plain staleness flag; the capture scheduler's module-level
|
|
1806
1899
|
* in-flight set is the recapture stampede guard.
|
|
1807
1900
|
*/
|
|
1808
1901
|
async getShell(
|
|
@@ -1813,56 +1906,128 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1813
1906
|
return null;
|
|
1814
1907
|
}
|
|
1815
1908
|
try {
|
|
1816
|
-
const
|
|
1817
|
-
const
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1909
|
+
const cache = await this.getCache();
|
|
1910
|
+
const request = this.keyToRequest(`shell:${key}`);
|
|
1911
|
+
const matchStartedAt = INTERNAL_RANGO_DEBUG ? Date.now() : 0;
|
|
1912
|
+
const {
|
|
1913
|
+
response,
|
|
1914
|
+
timedOut,
|
|
1915
|
+
error: matchError,
|
|
1916
|
+
} = await this.matchWithTimeout(cache, request);
|
|
1917
|
+
const matchMs = INTERNAL_RANGO_DEBUG
|
|
1918
|
+
? Date.now() - matchStartedAt
|
|
1919
|
+
: undefined;
|
|
1920
|
+
|
|
1921
|
+
if (!response) {
|
|
1922
|
+
if (matchError) {
|
|
1923
|
+
reportCacheError(
|
|
1924
|
+
matchError,
|
|
1925
|
+
"cache-read",
|
|
1926
|
+
"[CFCacheStore] getShell L1 match",
|
|
1927
|
+
);
|
|
1928
|
+
}
|
|
1929
|
+
this.debugShell(key, "l1-miss", {
|
|
1930
|
+
reason: matchError ? "error" : timedOut ? "timeout" : "absent",
|
|
1931
|
+
matchMs,
|
|
1932
|
+
});
|
|
1933
|
+
return this.kvGetShell(key);
|
|
1934
|
+
}
|
|
1935
|
+
if (response.status !== 200) {
|
|
1936
|
+
this.debugShell(key, "l1-miss", {
|
|
1937
|
+
reason: "non-200",
|
|
1938
|
+
status: response.status,
|
|
1939
|
+
matchMs,
|
|
1940
|
+
});
|
|
1941
|
+
return this.kvGetShell(key);
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
const bodyStartedAt = INTERNAL_RANGO_DEBUG ? Date.now() : 0;
|
|
1945
|
+
const { value, errored, error } =
|
|
1946
|
+
await this.readJsonWithTimeout<unknown>(response);
|
|
1947
|
+
const bodyReadMs = INTERNAL_RANGO_DEBUG
|
|
1948
|
+
? Date.now() - bodyStartedAt
|
|
1949
|
+
: undefined;
|
|
1950
|
+
if (value === undefined) {
|
|
1951
|
+
this.debugShell(key, "l1-miss", {
|
|
1952
|
+
reason: errored ? "corrupt" : "timeout",
|
|
1953
|
+
matchMs,
|
|
1954
|
+
bodyReadMs,
|
|
1955
|
+
});
|
|
1956
|
+
if (errored) {
|
|
1957
|
+
return this.healCorruptL1(cache, request, error, "getShell", () =>
|
|
1958
|
+
this.kvGetShell(key),
|
|
1959
|
+
);
|
|
1960
|
+
}
|
|
1961
|
+
return this.kvGetShell(key);
|
|
1962
|
+
}
|
|
1963
|
+
if (!isShellEnvelope(value)) {
|
|
1964
|
+
this.debugShell(key, "l1-miss", {
|
|
1965
|
+
reason: "malformed",
|
|
1966
|
+
matchMs,
|
|
1967
|
+
bodyReadMs,
|
|
1968
|
+
});
|
|
1969
|
+
return this.healCorruptL1(
|
|
1970
|
+
cache,
|
|
1971
|
+
request,
|
|
1972
|
+
new Error("malformed/partial L1 shell envelope"),
|
|
1826
1973
|
"getShell",
|
|
1974
|
+
() => this.kvGetShell(key),
|
|
1827
1975
|
);
|
|
1828
|
-
|
|
1829
|
-
if (timedOut || !envelope) return null;
|
|
1976
|
+
}
|
|
1830
1977
|
|
|
1831
1978
|
const now = Date.now();
|
|
1832
|
-
if (now >
|
|
1979
|
+
if (now > value.e) {
|
|
1980
|
+
this.debugShell(key, "l1-miss", {
|
|
1981
|
+
reason: "expired",
|
|
1982
|
+
matchMs,
|
|
1983
|
+
bodyReadMs,
|
|
1984
|
+
expiresAt: value.e,
|
|
1985
|
+
});
|
|
1986
|
+
return this.kvGetShell(key);
|
|
1987
|
+
}
|
|
1833
1988
|
|
|
1834
|
-
|
|
1989
|
+
// Unlike other L1 families, shells always check the durable generation
|
|
1990
|
+
// marker. See the capture-start/purge race documented above.
|
|
1991
|
+
const markerStartedAt = INTERNAL_RANGO_DEBUG ? Date.now() : 0;
|
|
1992
|
+
const invalidated = await this.isGloballyInvalidated(value.t, value.ta);
|
|
1993
|
+
const markerMs = INTERNAL_RANGO_DEBUG
|
|
1994
|
+
? Date.now() - markerStartedAt
|
|
1995
|
+
: undefined;
|
|
1996
|
+
if (invalidated) {
|
|
1997
|
+
this.debugShell(key, "marker-invalidated", {
|
|
1998
|
+
tier: "l1",
|
|
1999
|
+
matchMs,
|
|
2000
|
+
bodyReadMs,
|
|
2001
|
+
markerMs,
|
|
2002
|
+
});
|
|
1835
2003
|
return null;
|
|
1836
2004
|
}
|
|
1837
2005
|
|
|
1838
|
-
const shouldRevalidate =
|
|
2006
|
+
const shouldRevalidate = value.s > 0 && now > value.s;
|
|
2007
|
+
this.debugShell(key, "l1-hit", {
|
|
2008
|
+
freshness: shouldRevalidate ? "stale" : "fresh",
|
|
2009
|
+
matchMs,
|
|
2010
|
+
bodyReadMs,
|
|
2011
|
+
markerMs,
|
|
2012
|
+
expiresAt: value.e,
|
|
2013
|
+
});
|
|
1839
2014
|
return {
|
|
1840
|
-
entry:
|
|
1841
|
-
prelude: envelope.p,
|
|
1842
|
-
postponed: envelope.po,
|
|
1843
|
-
reactVersion: envelope.rv,
|
|
1844
|
-
buildVersion: envelope.bv,
|
|
1845
|
-
initialTheme: envelope.i,
|
|
1846
|
-
snapshot: envelope.sn,
|
|
1847
|
-
docKey: envelope.dk,
|
|
1848
|
-
handlerLiveHoles: envelope.lh,
|
|
1849
|
-
transitionWhen: envelope.tw,
|
|
1850
|
-
navigationOnly: envelope.no,
|
|
1851
|
-
createdAt: envelope.c,
|
|
1852
|
-
},
|
|
2015
|
+
entry: this.shellEnvelopeToEntry(value),
|
|
1853
2016
|
shouldRevalidate,
|
|
1854
2017
|
};
|
|
1855
2018
|
} catch (error) {
|
|
1856
2019
|
reportCacheError(error, "cache-read", "[CFCacheStore] getShell");
|
|
1857
|
-
|
|
2020
|
+
this.debugShell(key, "l1-miss", { reason: "error" });
|
|
2021
|
+
return this.kvGetShell(key);
|
|
1858
2022
|
}
|
|
1859
2023
|
}
|
|
1860
2024
|
|
|
1861
2025
|
/**
|
|
1862
|
-
* Store a PPR shell
|
|
1863
|
-
*
|
|
1864
|
-
*
|
|
1865
|
-
*
|
|
2026
|
+
* Store a PPR shell envelope in Cache API and, when its retention meets KV's
|
|
2027
|
+
* 60-second floor, KV. The shared write is registered with waitUntil and
|
|
2028
|
+
* awaited so invalidation rejection can be acknowledged to the capture
|
|
2029
|
+
* scheduler. Short-lived shells remain useful in L1 even though KV rejects
|
|
2030
|
+
* them.
|
|
1866
2031
|
*/
|
|
1867
2032
|
async putShell(
|
|
1868
2033
|
key: string,
|
|
@@ -1871,9 +2036,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1871
2036
|
swrSeconds?: number,
|
|
1872
2037
|
tags?: string[],
|
|
1873
2038
|
): Promise<"stored" | "invalidated" | void> {
|
|
1874
|
-
// KV
|
|
1875
|
-
// registered for isolate lifetime and awaited so invalidation rejection can
|
|
1876
|
-
// be acknowledged to the capture scheduler.
|
|
2039
|
+
// KV remains required for durable generation markers and cross-colo reads.
|
|
1877
2040
|
if (!this.kv) {
|
|
1878
2041
|
this.warnShellFamilyInertOnce();
|
|
1879
2042
|
return;
|
|
@@ -1883,9 +2046,6 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1883
2046
|
const ttl = resolveTtl(ttlSeconds, this.defaults, DEFAULT_FUNCTION_TTL);
|
|
1884
2047
|
const swrWindow = resolveSwrWindow(swrSeconds, this.defaults);
|
|
1885
2048
|
const totalTtl = ttl + swrWindow;
|
|
1886
|
-
// KV requires expirationTtl >= 60s; skip a shorter-lived shell rather than
|
|
1887
|
-
// letting kv.put reject inside waitUntil (mirrors setItem/kvSetSegment).
|
|
1888
|
-
if (totalTtl < 60) return;
|
|
1889
2049
|
|
|
1890
2050
|
const retentionTtl =
|
|
1891
2051
|
tags && tags.length > 0 && this.tagInvalidationTtl
|
|
@@ -1898,55 +2058,93 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1898
2058
|
Array.isArray(tags) && tags.length > 0 ? entry.createdAt : undefined;
|
|
1899
2059
|
|
|
1900
2060
|
const kvKey = this.toKVKey(`shell:${key}`);
|
|
1901
|
-
|
|
1902
|
-
// and skip the doomed write (mirrors kvSetSegment).
|
|
2061
|
+
let writeKv = retentionTtl >= KV_MIN_EXPIRATION_TTL;
|
|
1903
2062
|
const kvKeyBytes = kvKeyByteLength(kvKey);
|
|
1904
2063
|
if (kvKeyBytes > KV_MAX_KEY_BYTES) {
|
|
1905
2064
|
reportCacheError(
|
|
1906
2065
|
new Error(
|
|
1907
2066
|
`shell cache key produces a ${kvKeyBytes}-byte KV key, over the ` +
|
|
1908
|
-
`${KV_MAX_KEY_BYTES}-byte limit; the shell was not persisted.`,
|
|
2067
|
+
`${KV_MAX_KEY_BYTES}-byte limit; the shell was not persisted to KV (L2).`,
|
|
1909
2068
|
),
|
|
1910
2069
|
"cache-write",
|
|
1911
2070
|
"[CFCacheStore] putShell",
|
|
1912
2071
|
);
|
|
1913
|
-
|
|
2072
|
+
writeKv = false;
|
|
1914
2073
|
}
|
|
1915
2074
|
|
|
1916
2075
|
const write = (async (): Promise<"stored" | "invalidated" | void> => {
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
)
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
2076
|
+
if (
|
|
2077
|
+
tags &&
|
|
2078
|
+
tags.length > 0 &&
|
|
2079
|
+
(await this.isGloballyInvalidated(tags, entry.createdAt))
|
|
2080
|
+
) {
|
|
2081
|
+
this.debugShell(key, "write-invalidated");
|
|
2082
|
+
return "invalidated";
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
const envelope: CFShellEnvelope = {
|
|
2086
|
+
p: entry.prelude,
|
|
2087
|
+
po: entry.postponed,
|
|
2088
|
+
rv: entry.reactVersion,
|
|
2089
|
+
bv: entry.buildVersion,
|
|
2090
|
+
c: entry.createdAt,
|
|
2091
|
+
s: staleAt,
|
|
2092
|
+
e: expiresAt,
|
|
2093
|
+
t: tags,
|
|
2094
|
+
ta: taggedAt,
|
|
2095
|
+
i: entry.initialTheme,
|
|
2096
|
+
sn: entry.snapshot,
|
|
2097
|
+
dk: entry.docKey,
|
|
2098
|
+
lh: entry.handlerLiveHoles,
|
|
2099
|
+
tw: entry.transitionWhen,
|
|
2100
|
+
no: entry.navigationOnly,
|
|
2101
|
+
};
|
|
2102
|
+
const body = JSON.stringify(envelope);
|
|
2103
|
+
const writes: Promise<boolean>[] = [
|
|
2104
|
+
(async () => {
|
|
2105
|
+
try {
|
|
2106
|
+
const cache = await this.getCache();
|
|
2107
|
+
await cache.put(
|
|
2108
|
+
this.keyToRequest(`shell:${key}`),
|
|
2109
|
+
this.shellEnvelopeResponse(body, envelope),
|
|
2110
|
+
);
|
|
2111
|
+
this.debugShell(key, "l1-stored", {
|
|
2112
|
+
expiresAt: envelope.e,
|
|
2113
|
+
});
|
|
2114
|
+
return true;
|
|
2115
|
+
} catch (error) {
|
|
2116
|
+
reportCacheError(
|
|
2117
|
+
error,
|
|
2118
|
+
"cache-write",
|
|
2119
|
+
"[CFCacheStore] putShell L1",
|
|
2120
|
+
);
|
|
2121
|
+
return false;
|
|
2122
|
+
}
|
|
2123
|
+
})(),
|
|
2124
|
+
];
|
|
2125
|
+
if (writeKv) {
|
|
2126
|
+
writes.push(
|
|
2127
|
+
(async () => {
|
|
2128
|
+
try {
|
|
2129
|
+
await this.kv!.put(kvKey, body, {
|
|
2130
|
+
expirationTtl: retentionTtl,
|
|
2131
|
+
});
|
|
2132
|
+
this.debugShell(key, "kv-stored", {
|
|
2133
|
+
expiresAt: envelope.e,
|
|
2134
|
+
});
|
|
2135
|
+
return true;
|
|
2136
|
+
} catch (error) {
|
|
2137
|
+
reportCacheError(
|
|
2138
|
+
error,
|
|
2139
|
+
"cache-write",
|
|
2140
|
+
"[CFCacheStore] putShell L2",
|
|
2141
|
+
);
|
|
2142
|
+
return false;
|
|
2143
|
+
}
|
|
2144
|
+
})(),
|
|
2145
|
+
);
|
|
1949
2146
|
}
|
|
2147
|
+
return (await Promise.all(writes)).some(Boolean) ? "stored" : undefined;
|
|
1950
2148
|
})();
|
|
1951
2149
|
this.waitUntil(async () => {
|
|
1952
2150
|
await write;
|
|
@@ -1957,6 +2155,141 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1957
2155
|
}
|
|
1958
2156
|
}
|
|
1959
2157
|
|
|
2158
|
+
/** Rebuild the public shell entry from its validated storage envelope. */
|
|
2159
|
+
private shellEnvelopeToEntry(envelope: CFShellEnvelope): ShellCacheEntry {
|
|
2160
|
+
return {
|
|
2161
|
+
prelude: envelope.p,
|
|
2162
|
+
postponed: envelope.po,
|
|
2163
|
+
reactVersion: envelope.rv,
|
|
2164
|
+
buildVersion: envelope.bv,
|
|
2165
|
+
initialTheme: envelope.i,
|
|
2166
|
+
snapshot: envelope.sn,
|
|
2167
|
+
docKey: envelope.dk,
|
|
2168
|
+
handlerLiveHoles: envelope.lh,
|
|
2169
|
+
transitionWhen: envelope.tw,
|
|
2170
|
+
navigationOnly: envelope.no,
|
|
2171
|
+
createdAt: envelope.c,
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
/** Build the Cache API representation of the coupled shell envelope. */
|
|
2176
|
+
private shellEnvelopeResponse(
|
|
2177
|
+
body: string,
|
|
2178
|
+
envelope: CFShellEnvelope,
|
|
2179
|
+
): Response {
|
|
2180
|
+
const remainingTtl = Math.max(
|
|
2181
|
+
1,
|
|
2182
|
+
Math.floor((envelope.e - Date.now()) / 1000),
|
|
2183
|
+
);
|
|
2184
|
+
return new Response(body, {
|
|
2185
|
+
headers: {
|
|
2186
|
+
"Content-Type": "application/json",
|
|
2187
|
+
"Cache-Control": `public, max-age=${remainingTtl}`,
|
|
2188
|
+
[CACHE_STALE_AT_HEADER]: String(envelope.s),
|
|
2189
|
+
[CACHE_EXPIRES_AT_HEADER]: String(envelope.e),
|
|
2190
|
+
[CACHE_STATUS_HEADER]: "HIT",
|
|
2191
|
+
...this.tagHeaderEntries(envelope.t, envelope.ta),
|
|
2192
|
+
},
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
/** KV shell fallback with corruption checks and background L1 promotion. */
|
|
2197
|
+
private async kvGetShell(
|
|
2198
|
+
key: string,
|
|
2199
|
+
): Promise<{ entry: ShellCacheEntry; shouldRevalidate?: boolean } | null> {
|
|
2200
|
+
if (!this.kv) return null;
|
|
2201
|
+
try {
|
|
2202
|
+
const readStartedAt = INTERNAL_RANGO_DEBUG ? Date.now() : 0;
|
|
2203
|
+
const kvKey = this.toKVKey(`shell:${key}`);
|
|
2204
|
+
const { value: envelope, timedOut } =
|
|
2205
|
+
await this.kvGetOrEvict<CFShellEnvelope>(
|
|
2206
|
+
kvKey,
|
|
2207
|
+
isShellEnvelope,
|
|
2208
|
+
"getShell",
|
|
2209
|
+
);
|
|
2210
|
+
const readMs = INTERNAL_RANGO_DEBUG
|
|
2211
|
+
? Date.now() - readStartedAt
|
|
2212
|
+
: undefined;
|
|
2213
|
+
if (timedOut || !envelope) {
|
|
2214
|
+
this.debugShell(key, "kv-miss", {
|
|
2215
|
+
reason: timedOut ? "timeout" : "unavailable",
|
|
2216
|
+
readMs,
|
|
2217
|
+
});
|
|
2218
|
+
return null;
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
const now = Date.now();
|
|
2222
|
+
if (now > envelope.e) {
|
|
2223
|
+
this.debugShell(key, "kv-miss", {
|
|
2224
|
+
reason: "expired",
|
|
2225
|
+
readMs,
|
|
2226
|
+
expiresAt: envelope.e,
|
|
2227
|
+
});
|
|
2228
|
+
return null;
|
|
2229
|
+
}
|
|
2230
|
+
const markerStartedAt = INTERNAL_RANGO_DEBUG ? Date.now() : 0;
|
|
2231
|
+
const invalidated = await this.isGloballyInvalidated(
|
|
2232
|
+
envelope.t,
|
|
2233
|
+
envelope.ta,
|
|
2234
|
+
);
|
|
2235
|
+
const markerMs = INTERNAL_RANGO_DEBUG
|
|
2236
|
+
? Date.now() - markerStartedAt
|
|
2237
|
+
: undefined;
|
|
2238
|
+
if (invalidated) {
|
|
2239
|
+
this.debugShell(key, "marker-invalidated", {
|
|
2240
|
+
tier: "kv",
|
|
2241
|
+
readMs,
|
|
2242
|
+
markerMs,
|
|
2243
|
+
});
|
|
2244
|
+
return null;
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
const shouldRevalidate = envelope.s > 0 && now > envelope.s;
|
|
2248
|
+
this.debugShell(key, "kv-hit", {
|
|
2249
|
+
freshness: shouldRevalidate ? "stale" : "fresh",
|
|
2250
|
+
readMs,
|
|
2251
|
+
markerMs,
|
|
2252
|
+
expiresAt: envelope.e,
|
|
2253
|
+
});
|
|
2254
|
+
this.promoteShellToL1(key, envelope);
|
|
2255
|
+
return {
|
|
2256
|
+
entry: this.shellEnvelopeToEntry(envelope),
|
|
2257
|
+
shouldRevalidate,
|
|
2258
|
+
};
|
|
2259
|
+
} catch (error) {
|
|
2260
|
+
reportCacheError(error, "cache-read", "[CFCacheStore] kvGetShell");
|
|
2261
|
+
this.debugShell(key, "kv-miss", { reason: "error" });
|
|
2262
|
+
return null;
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
/** Promote a valid KV shell into the per-colo Cache API tier. */
|
|
2267
|
+
private promoteShellToL1(key: string, envelope: CFShellEnvelope): void {
|
|
2268
|
+
if (!this.waitUntil) return;
|
|
2269
|
+
this.waitUntil(() =>
|
|
2270
|
+
reportingAsync(
|
|
2271
|
+
async () => {
|
|
2272
|
+
if (Date.now() > envelope.e) return;
|
|
2273
|
+
const cache = await this.getCache();
|
|
2274
|
+
const body = JSON.stringify(envelope);
|
|
2275
|
+
await cache.put(
|
|
2276
|
+
this.keyToRequest(`shell:${key}`),
|
|
2277
|
+
this.shellEnvelopeResponse(body, envelope),
|
|
2278
|
+
);
|
|
2279
|
+
this.debugShell(key, "kv-promoted", {
|
|
2280
|
+
remainingTtl: Math.max(
|
|
2281
|
+
1,
|
|
2282
|
+
Math.floor((envelope.e - Date.now()) / 1000),
|
|
2283
|
+
),
|
|
2284
|
+
expiresAt: envelope.e,
|
|
2285
|
+
});
|
|
2286
|
+
},
|
|
2287
|
+
"cache-write",
|
|
2288
|
+
"[CFCacheStore] promoteShellToL1",
|
|
2289
|
+
),
|
|
2290
|
+
);
|
|
2291
|
+
}
|
|
2292
|
+
|
|
1960
2293
|
// ============================================================================
|
|
1961
2294
|
// Key Helpers
|
|
1962
2295
|
// ============================================================================
|
|
@@ -228,8 +228,10 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
|
|
|
228
228
|
* per-request memo is consulted (synchronously, no KV read), so a request
|
|
229
229
|
* that ran `updateTag()` still masks its own not-yet-purged entries
|
|
230
230
|
* (read-your-own-writes).
|
|
231
|
-
* - The KV tier and PPR
|
|
232
|
-
*
|
|
231
|
+
* - The KV tier and PPR shell reads still use the KV markers. Runtime shell
|
|
232
|
+
* L1 entries are purgeable, but their taggedAt is the capture-start time: a
|
|
233
|
+
* capture can finish after an invalidation purge, so eviction alone cannot
|
|
234
|
+
* reject that older generation. Keep `kv` configured for shell L2 + markers.
|
|
233
235
|
* Without `kv`, purge mode is the ONLY tag invalidation and the store is
|
|
234
236
|
* L1-only: a supported configuration (previously tag invalidation without
|
|
235
237
|
* KV had no read-side effect at all). One KV-less caveat: an entry whose
|