@decocms/tanstack 8.0.0 → 8.1.0-next.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/package.json +12 -5
- package/src/__conformance__/observability.test.ts +221 -0
- package/src/hooks/CdnSegmentMarker.tsx +38 -0
- package/src/hooks/DecoPageRenderer.tsx +91 -29
- package/src/hooks/DecoRootLayout.tsx +139 -58
- package/src/hooks/DraftPreviewIndicator.test.tsx +60 -0
- package/src/hooks/DraftPreviewIndicator.tsx +64 -0
- package/src/hooks/NavigationProgress.test.tsx +58 -0
- package/src/hooks/NavigationProgress.tsx +45 -3
- package/src/hooks/PreviewProviders.test.tsx +42 -0
- package/src/hooks/PreviewProviders.tsx +21 -2
- package/src/hooks/deferredObserverTiming.test.tsx +91 -0
- package/src/hooks/index.ts +1 -0
- package/src/index.ts +30 -13
- package/src/routes/cmsRoute.test.ts +168 -1
- package/src/routes/cmsRoute.ts +148 -39
- package/src/routes/pageUrl.test.ts +6 -6
- package/src/routes/pageUrl.ts +5 -4
- package/src/routes/withSiteGlobals.test.ts +37 -0
- package/src/routes/withSiteGlobals.ts +45 -24
- package/src/sdk/backgroundTasks.test.ts +68 -0
- package/src/sdk/backgroundTasks.ts +38 -0
- package/src/sdk/cdnSegment.test.ts +67 -0
- package/src/sdk/cdnSegment.ts +71 -0
- package/src/sdk/deferredSectionLoader.test.ts +1 -1
- package/src/sdk/draft.test.ts +341 -0
- package/src/sdk/draft.ts +277 -0
- package/src/sdk/draftShared.ts +16 -0
- package/src/sdk/kvHydration.test.ts +156 -0
- package/src/sdk/kvHydration.ts +82 -12
- package/src/sdk/kvRedirects.test.ts +132 -0
- package/src/sdk/kvRedirects.ts +125 -0
- package/src/sdk/router.ts +70 -61
- package/src/sdk/serverFnFetch.ts +59 -0
- package/src/sdk/speculationRules.test.ts +82 -0
- package/src/sdk/speculationRules.ts +136 -0
- package/src/sdk/startEntry.ts +31 -0
- package/src/sdk/workerEntry.cacheStorage.test.ts +147 -0
- package/src/sdk/workerEntry.test.ts +554 -16
- package/src/sdk/workerEntry.trackingCache.test.ts +328 -0
- package/src/sdk/workerEntry.ts +915 -68
- package/src/v8/__conformance__/delivery.test.ts +100 -0
- package/src/v8/binding.ts +70 -0
- package/src/v8/kvLoader.bundle.test.ts +42 -0
- package/src/v8/kvLoader.test.ts +74 -0
- package/src/v8/kvLoader.ts +43 -0
- package/src/vite/plugin.js +180 -8
- package/src/vite/plugin.test.js +126 -1
- package/src/vite/plugin.test.ts +125 -1
|
@@ -17,16 +17,17 @@
|
|
|
17
17
|
*
|
|
18
18
|
* export const Route = createFileRoute("/$")({
|
|
19
19
|
* ...withSiteGlobals(cmsRouteConfig({
|
|
20
|
-
* siteName: "
|
|
21
|
-
* defaultTitle: "
|
|
20
|
+
* siteName: "Acme",
|
|
21
|
+
* defaultTitle: "Acme",
|
|
22
22
|
* })),
|
|
23
23
|
* component: ...,
|
|
24
24
|
* });
|
|
25
25
|
* ```
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
import type { ResolvedSection } from "@decocms/blocks/cms";
|
|
28
|
+
import type { MatcherContext, ResolvedSection } from "@decocms/blocks/cms";
|
|
29
29
|
import { loadBlocks, onChange, resolvePageSections } from "@decocms/blocks/cms";
|
|
30
|
+
import { createCacheStore, getCacheStorageContext } from "@decocms/blocks/sdk/cacheStorage";
|
|
30
31
|
|
|
31
32
|
// ---------------------------------------------------------------------------
|
|
32
33
|
// Types
|
|
@@ -73,12 +74,18 @@ interface CacheEntry {
|
|
|
73
74
|
const DEFAULT_CACHE_TTL_MS = 5 * 60_000;
|
|
74
75
|
const cacheTtlMs = DEFAULT_CACHE_TTL_MS;
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
// Keyed by request path — site.global/site.pageSections can contain
|
|
78
|
+
// path-dependent matchers (pathname, multi(ETC Segment), date, etc.), so a
|
|
79
|
+
// single process-wide cache entry would leak one route's matched variant onto
|
|
80
|
+
// every other route (e.g. a /section-only alert winning on "/"). Caching a
|
|
81
|
+
// resolvedSections array per path preserves the original 5-minute SWR behavior
|
|
82
|
+
// while keeping each route's matcher evaluation correct.
|
|
83
|
+
const cache = createCacheStore<CacheEntry>("site-globals");
|
|
84
|
+
const inflight = new Map<string, Promise<CacheEntry>>();
|
|
78
85
|
|
|
79
86
|
onChange(() => {
|
|
80
|
-
cache
|
|
81
|
-
inflight
|
|
87
|
+
cache.clear();
|
|
88
|
+
inflight.clear();
|
|
82
89
|
});
|
|
83
90
|
|
|
84
91
|
function readSiteBlock(): SiteBlock | null {
|
|
@@ -99,7 +106,7 @@ function gatherSectionRefs(site: SiteBlock): SiteGlobalRef[] {
|
|
|
99
106
|
const EMPTY_ENTRY: CacheEntry = {
|
|
100
107
|
resolvedSections: [],
|
|
101
108
|
rawRefs: [],
|
|
102
|
-
expiresAt:
|
|
109
|
+
expiresAt: 0,
|
|
103
110
|
};
|
|
104
111
|
|
|
105
112
|
/**
|
|
@@ -112,49 +119,60 @@ const EMPTY_ENTRY: CacheEntry = {
|
|
|
112
119
|
* Exposed as a util so sites can call it directly if they need globals
|
|
113
120
|
* outside the route loader path (rare).
|
|
114
121
|
*/
|
|
115
|
-
export async function resolveSiteGlobals(): Promise<{
|
|
122
|
+
export async function resolveSiteGlobals(matcherCtx?: MatcherContext): Promise<{
|
|
116
123
|
resolvedSections: ResolvedSection[];
|
|
117
124
|
rawRefs: SiteGlobalRef[];
|
|
118
125
|
}> {
|
|
126
|
+
// Path/date/segment matchers inside site-global sections (e.g. a multivariate
|
|
127
|
+
// `Alert` gated by a pathname matcher) must evaluate against the actual
|
|
128
|
+
// request, and the result is cached per path so routes don't share variants.
|
|
129
|
+
const cacheKey = cache.key(matcherCtx?.path ?? "");
|
|
119
130
|
const now = Date.now();
|
|
120
|
-
|
|
121
|
-
|
|
131
|
+
const cached = getCacheStorageContext()?.storage
|
|
132
|
+
? await cache.read(cacheKey)
|
|
133
|
+
: cache.get(cacheKey);
|
|
134
|
+
if (cached && cached.expiresAt > now) return cached;
|
|
135
|
+
const inflightEntry = inflight.get(cacheKey);
|
|
136
|
+
if (inflightEntry) return inflightEntry;
|
|
122
137
|
|
|
123
138
|
const site = readSiteBlock();
|
|
124
139
|
if (!site) {
|
|
125
140
|
// Cache the empty result so subsequent requests don't re-walk the
|
|
126
141
|
// block registry. `onChange` invalidation still applies — if a Site
|
|
127
|
-
// block appears later, the listener
|
|
128
|
-
|
|
129
|
-
|
|
142
|
+
// block appears later, the listener clears `cache` and we re-check.
|
|
143
|
+
const empty = { ...EMPTY_ENTRY, expiresAt: Date.now() + cacheTtlMs };
|
|
144
|
+
cache.set(cacheKey, empty, empty.expiresAt);
|
|
145
|
+
return empty;
|
|
130
146
|
}
|
|
131
147
|
|
|
132
148
|
const rawRefs = gatherSectionRefs(site);
|
|
133
149
|
if (rawRefs.length === 0) {
|
|
134
|
-
|
|
135
|
-
|
|
150
|
+
const empty = { ...EMPTY_ENTRY, expiresAt: Date.now() + cacheTtlMs };
|
|
151
|
+
cache.set(cacheKey, empty, empty.expiresAt);
|
|
152
|
+
return empty;
|
|
136
153
|
}
|
|
137
154
|
|
|
138
|
-
|
|
155
|
+
const p = (async () => {
|
|
139
156
|
try {
|
|
140
|
-
const resolvedSections = await resolvePageSections(rawRefs);
|
|
157
|
+
const resolvedSections = await resolvePageSections(rawRefs, matcherCtx);
|
|
141
158
|
const entry: CacheEntry = {
|
|
142
159
|
resolvedSections,
|
|
143
160
|
rawRefs,
|
|
144
161
|
expiresAt: Date.now() + cacheTtlMs,
|
|
145
162
|
};
|
|
146
|
-
cache
|
|
163
|
+
cache.set(cacheKey, entry, entry.expiresAt);
|
|
147
164
|
return entry;
|
|
148
165
|
} catch (err) {
|
|
149
166
|
console.error("[site-globals] failed to resolve:", err);
|
|
150
167
|
// Don't cache failures — let the next request retry.
|
|
151
168
|
return { resolvedSections: [], rawRefs, expiresAt: 0 };
|
|
152
169
|
} finally {
|
|
153
|
-
inflight
|
|
170
|
+
inflight.delete(cacheKey);
|
|
154
171
|
}
|
|
155
172
|
})();
|
|
156
173
|
|
|
157
|
-
|
|
174
|
+
inflight.set(cacheKey, p);
|
|
175
|
+
return p;
|
|
158
176
|
}
|
|
159
177
|
|
|
160
178
|
// ---------------------------------------------------------------------------
|
|
@@ -170,7 +188,10 @@ export async function resolveSiteGlobals(): Promise<{
|
|
|
170
188
|
* both in `site.global` and in a page's section list, which would otherwise
|
|
171
189
|
* render twice.
|
|
172
190
|
*/
|
|
173
|
-
export function dedupeGlobals(
|
|
191
|
+
export function dedupeGlobals(
|
|
192
|
+
globals: ResolvedSection[],
|
|
193
|
+
existing: ResolvedSection[],
|
|
194
|
+
): ResolvedSection[] {
|
|
174
195
|
if (globals.length === 0) return [];
|
|
175
196
|
const seenComponents = new Set<string>();
|
|
176
197
|
for (const s of existing) {
|
|
@@ -217,6 +238,6 @@ export function withSiteGlobals<T extends { loader: AnyLoader }>(routeConfig: T)
|
|
|
217
238
|
|
|
218
239
|
/** @internal */
|
|
219
240
|
export function __resetSiteGlobalsCache() {
|
|
220
|
-
cache
|
|
221
|
-
inflight
|
|
241
|
+
cache.clear();
|
|
242
|
+
inflight.clear();
|
|
222
243
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workers run no timers between requests: core background work (release
|
|
3
|
+
* checks, telemetry batches) runs after the response, inside ctx.waitUntil
|
|
4
|
+
* (hosted-publishing.mdx, hosted-releases-internals.mdx, telemetry-internals.mdx).
|
|
5
|
+
*/
|
|
6
|
+
import { createCMS, type Loader, resetForTests } from "@decocms/blocks";
|
|
7
|
+
import { setBlocks } from "@decocms/blocks/cms";
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
9
|
+
import { createDecoWorkerEntry } from "./workerEntry";
|
|
10
|
+
|
|
11
|
+
const HOOK = Symbol.for("decocms.blocks.background");
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
resetForTests();
|
|
15
|
+
setBlocks({}); // the v7 path this entry still runs reads redirects from it
|
|
16
|
+
});
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
delete (globalThis as Record<symbol, unknown>)[HOOK];
|
|
19
|
+
resetForTests();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function context() {
|
|
23
|
+
const waited: Promise<unknown>[] = [];
|
|
24
|
+
return {
|
|
25
|
+
waited,
|
|
26
|
+
ctx: { waitUntil: (p: Promise<unknown>) => void waited.push(p), passThroughOnException() {} },
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("createDecoWorkerEntry: background work", () => {
|
|
31
|
+
it("runs a due release check after the response, inside ctx.waitUntil", async () => {
|
|
32
|
+
const update = vi.fn(async () => ({ updated: false }));
|
|
33
|
+
const loader: Loader = {
|
|
34
|
+
load: async () => ({ revision: "r1", blocks: {} }),
|
|
35
|
+
update,
|
|
36
|
+
};
|
|
37
|
+
const cms = createCMS({ blocks: {}, content: loader });
|
|
38
|
+
let checkedBeforeResponse = true;
|
|
39
|
+
const worker = createDecoWorkerEntry(
|
|
40
|
+
{
|
|
41
|
+
fetch: async () => {
|
|
42
|
+
cms.forRelease();
|
|
43
|
+
await Promise.resolve();
|
|
44
|
+
checkedBeforeResponse = update.mock.calls.length > 0;
|
|
45
|
+
return new Response("ok");
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{ observability: false },
|
|
49
|
+
);
|
|
50
|
+
const { waited, ctx } = context();
|
|
51
|
+
const response = await worker.fetch(new Request("https://example.com/"), {}, ctx);
|
|
52
|
+
expect(await response.text()).toBe("ok");
|
|
53
|
+
expect(checkedBeforeResponse).toBe(false);
|
|
54
|
+
expect(waited).toHaveLength(1);
|
|
55
|
+
await Promise.all(waited);
|
|
56
|
+
expect(update).toHaveBeenCalledTimes(1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("calls waitUntil only when there's something queued", async () => {
|
|
60
|
+
const worker = createDecoWorkerEntry(
|
|
61
|
+
{ fetch: async () => new Response("ok") },
|
|
62
|
+
{ observability: false },
|
|
63
|
+
);
|
|
64
|
+
const { waited, ctx } = context();
|
|
65
|
+
await worker.fetch(new Request("https://example.com/"), {}, ctx);
|
|
66
|
+
expect(waited).toHaveLength(0);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs `@decocms/blocks` background work (release checks, telemetry batches)
|
|
3
|
+
* after each response, inside `ctx.waitUntil`. Workers run no timers between
|
|
4
|
+
* requests, so the core's timer fallback would never fire or would be bound
|
|
5
|
+
* to a finished request. See /next/hosted-releases-internals and
|
|
6
|
+
* /next/telemetry-internals#sending-in-the-background.
|
|
7
|
+
*
|
|
8
|
+
* The hook is installed once per isolate under the `Symbol.for` key the core
|
|
9
|
+
* looks for; its queue lives on the hook itself, so two copies of this module
|
|
10
|
+
* share one queue.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const HOOK = Symbol.for("decocms.blocks.background");
|
|
14
|
+
|
|
15
|
+
type Task = () => Promise<void>;
|
|
16
|
+
type Hook = ((task: Task) => void) & { queue: Task[] };
|
|
17
|
+
|
|
18
|
+
function hook(): Hook {
|
|
19
|
+
const g = globalThis as unknown as Record<symbol, Hook | undefined>;
|
|
20
|
+
const existing = g[HOOK];
|
|
21
|
+
if (typeof existing === "function" && Array.isArray(existing.queue)) return existing;
|
|
22
|
+
const queue: Task[] = [];
|
|
23
|
+
const installed = Object.assign((task: Task) => void queue.push(task), { queue });
|
|
24
|
+
g[HOOK] = installed;
|
|
25
|
+
return installed;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Installs the hook (idempotent): from now on, core background work queues here. */
|
|
29
|
+
export function installBackgroundHook(): void {
|
|
30
|
+
hook();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Runs everything queued so far inside `ctx.waitUntil`. */
|
|
34
|
+
export function runBackgroundTasks(ctx: { waitUntil(promise: Promise<unknown>): void }): void {
|
|
35
|
+
const tasks = hook().queue.splice(0);
|
|
36
|
+
if (tasks.length === 0) return;
|
|
37
|
+
ctx.waitUntil(Promise.all(tasks.map((task) => task().catch(() => {}))));
|
|
38
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { segmentToken } from "./cdnSegment";
|
|
3
|
+
|
|
4
|
+
const BUILD = "abc123";
|
|
5
|
+
|
|
6
|
+
describe("segmentToken", () => {
|
|
7
|
+
it("produces a token for an anonymous visitor", () => {
|
|
8
|
+
expect(segmentToken("desktop", false, BUILD)).toBeTruthy();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("gives different segments different tokens", () => {
|
|
12
|
+
// The whole point: the URL has to distinguish what the Worker's key
|
|
13
|
+
// distinguishes, or a cache in front serves one visitor another's response.
|
|
14
|
+
const tokens = ["desktop", "mobile", "desktop|r=RJ", "desktop|r=SP", "desktop|sc=3|r=RJ"].map(
|
|
15
|
+
(s) => segmentToken(s, false, BUILD),
|
|
16
|
+
);
|
|
17
|
+
expect(new Set(tokens).size).toBe(tokens.length);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("regionalized and non-regionalized visitors never share a token", () => {
|
|
21
|
+
// This is the case that made the first version inert on real stores: region
|
|
22
|
+
// is resolved server-side from cf.regionCode, so it MUST be in the token.
|
|
23
|
+
expect(segmentToken("desktop", false, BUILD)).not.toBe(
|
|
24
|
+
segmentToken("desktop|r=RJ", false, BUILD),
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("is stable for the same segment and build", () => {
|
|
29
|
+
// The worker verifies by recomputing, so the same input must always give
|
|
30
|
+
// the same token — otherwise nothing would ever match.
|
|
31
|
+
expect(segmentToken("desktop|r=RJ", false, BUILD)).toBe(
|
|
32
|
+
segmentToken("desktop|r=RJ", false, BUILD),
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("refuses a logged-in visitor regardless of segment precision", () => {
|
|
37
|
+
// Personalized responses never belong in a shared entry, however exact the
|
|
38
|
+
// key is.
|
|
39
|
+
expect(segmentToken("desktop|auth|r=RJ", true, BUILD)).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("refuses a missing or dev build hash", () => {
|
|
43
|
+
// Without a build hash there is no way to invalidate on deploy — the cache
|
|
44
|
+
// in front would keep serving stale code.
|
|
45
|
+
expect(segmentToken("desktop", false, undefined)).toBeNull();
|
|
46
|
+
expect(segmentToken("desktop", false, "")).toBeNull();
|
|
47
|
+
expect(segmentToken("desktop", false, "dev")).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("changes with the build, so a deploy invalidates it", () => {
|
|
51
|
+
expect(segmentToken("desktop", false, "buildA")).not.toBe(
|
|
52
|
+
segmentToken("desktop", false, "buildB"),
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("refuses an empty segment descriptor", () => {
|
|
57
|
+
expect(segmentToken("", false, BUILD)).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("is URL-safe even when the segment carries a VTEX region id", () => {
|
|
61
|
+
// `v2.XXXX` contains the character an earlier format used as a separator;
|
|
62
|
+
// hashing sidesteps escaping entirely.
|
|
63
|
+
const t = segmentToken("desktop|r=v2.ABC-123|sc=3", false, BUILD);
|
|
64
|
+
expect(t).toMatch(/^[a-z0-9]+$/);
|
|
65
|
+
expect(encodeURIComponent(t as string)).toBe(t);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Segment marker on `/_serverFn` URLs, so a cache in front of the Worker can
|
|
3
|
+
* serve the response without invoking it.
|
|
4
|
+
*
|
|
5
|
+
* The problem: whatever sits in front (Workers Cache, a CDN rule) keys on the
|
|
6
|
+
* raw URL. The Worker keys on a SYNTHETIC Request carrying
|
|
7
|
+
* `__seg`/`__cf_device`/`__bot`/`__fetch`/`__abf` (`buildCacheKey` in
|
|
8
|
+
* `./workerEntry`) — params the URL never shows. That mismatch is why the
|
|
9
|
+
* framework stamps `CDN-Cache-Control: no-store` on public responses.
|
|
10
|
+
*
|
|
11
|
+
* The fix: put the segment in the URL itself, so the two keys become
|
|
12
|
+
* equivalent.
|
|
13
|
+
*
|
|
14
|
+
* ## Why the SERVER issues the token
|
|
15
|
+
*
|
|
16
|
+
* The first version had the client derive the token from what it could see
|
|
17
|
+
* (`navigator.userAgent`). That only ever worked on sites whose segment
|
|
18
|
+
* reduces to device alone. A regionalized VTEX store — most of them — segments
|
|
19
|
+
* on region too, and region is resolved from `request.cf.regionCode`, which
|
|
20
|
+
* exists **only** on the server. The client cannot see it, so its token never
|
|
21
|
+
* matched and the feature stayed permanently inert.
|
|
22
|
+
*
|
|
23
|
+
* So the server computes the token from the segment it already built, publishes
|
|
24
|
+
* it to the page, and the client only echoes it back. The Worker still verifies
|
|
25
|
+
* by recomputing — the marker is never trusted, so a stale or forged one just
|
|
26
|
+
* keeps the `no-store`.
|
|
27
|
+
*
|
|
28
|
+
* The token is a hash rather than readable fields: `regionId` can contain the
|
|
29
|
+
* separator (`v2.XXXX`), values would need escaping, and there is no reason to
|
|
30
|
+
* publish a visitor's region in a URL that ends up in logs.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { djb2Hex } from "@decocms/blocks/sdk/djb2";
|
|
34
|
+
|
|
35
|
+
/** `__d` is reserved: `workerEntry` uses `?__d=` as an OTel debug flag. */
|
|
36
|
+
export const CSEG_PARAM = "__cseg";
|
|
37
|
+
|
|
38
|
+
/** Global the SSR publishes the token on, read back by `decoServerFnFetch`. */
|
|
39
|
+
export const CSEG_GLOBAL = "__DECO_CSEG";
|
|
40
|
+
|
|
41
|
+
/** RequestContext bag key the worker entry fills before rendering. */
|
|
42
|
+
export const CSEG_BAG_KEY = "deco.cdn.segmentToken";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build the token for a segment, or `null` when this request must not be
|
|
46
|
+
* cached in front of the Worker.
|
|
47
|
+
*
|
|
48
|
+
* `null` — i.e. keep `no-store` — when:
|
|
49
|
+
*
|
|
50
|
+
* - the visitor is logged in. Personalized responses never belong in a shared
|
|
51
|
+
* entry, no matter how precise the key is.
|
|
52
|
+
* - there is no build hash, or it is `"dev"`. The build is part of the token
|
|
53
|
+
* because deploying does not purge the cache in front, so the URL has to
|
|
54
|
+
* change on its own when the bundle does.
|
|
55
|
+
*
|
|
56
|
+
* Everything else in the segment — device, sales channel, region, a site's own
|
|
57
|
+
* custom dimensions — is folded INTO the token rather than rejected. That is
|
|
58
|
+
* the difference from the first version: those are what the key needs to
|
|
59
|
+
* distinguish, not reasons to give up on caching.
|
|
60
|
+
*/
|
|
61
|
+
export function segmentToken(
|
|
62
|
+
segmentDescriptor: string,
|
|
63
|
+
loggedIn: boolean,
|
|
64
|
+
buildHash: string | undefined,
|
|
65
|
+
): string | null {
|
|
66
|
+
if (loggedIn) return null;
|
|
67
|
+
if (!buildHash || buildHash === "dev") return null;
|
|
68
|
+
if (!segmentDescriptor) return null;
|
|
69
|
+
|
|
70
|
+
return djb2Hex(`${segmentDescriptor}|${buildHash}`);
|
|
71
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* (`@decocms/tanstack/sdk/deferredSectionLoader`).
|
|
4
4
|
*
|
|
5
5
|
* Before 7.7 this wrapper was unreachable from any public subpath, so
|
|
6
|
-
* migrated
|
|
6
|
+
* migrated production storefronts each carried a
|
|
7
7
|
* byte-identical local shim wrapping the public `loadDeferredSection`
|
|
8
8
|
* export. The cases below are derived from how those sites call it: it is
|
|
9
9
|
* passed verbatim as `<DecoPageRenderer loadDeferredSectionFn={...} />`,
|