@decocms/blocks 7.0.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 +97 -0
- package/src/cms/applySectionConventions.ts +128 -0
- package/src/cms/blockSource.test.ts +67 -0
- package/src/cms/blockSource.ts +124 -0
- package/src/cms/index.ts +104 -0
- package/src/cms/layoutCacheRace.test.ts +146 -0
- package/src/cms/loadDecofileDirectory.test.ts +52 -0
- package/src/cms/loadDecofileDirectory.ts +48 -0
- package/src/cms/loader.test.ts +184 -0
- package/src/cms/loader.ts +284 -0
- package/src/cms/registry.test.ts +118 -0
- package/src/cms/registry.ts +252 -0
- package/src/cms/resolve.test.ts +547 -0
- package/src/cms/resolve.ts +2003 -0
- package/src/cms/schema.ts +993 -0
- package/src/cms/sectionLoaders.test.ts +409 -0
- package/src/cms/sectionLoaders.ts +562 -0
- package/src/cms/sectionMixins.test.ts +163 -0
- package/src/cms/sectionMixins.ts +153 -0
- package/src/hooks/LazySection.tsx +121 -0
- package/src/hooks/LiveControls.tsx +122 -0
- package/src/hooks/RenderSection.test.tsx +81 -0
- package/src/hooks/RenderSection.tsx +91 -0
- package/src/hooks/SectionErrorFallback.tsx +97 -0
- package/src/hooks/index.ts +4 -0
- package/src/index.ts +8 -0
- package/src/matchers/builtins.test.ts +251 -0
- package/src/matchers/builtins.ts +437 -0
- package/src/matchers/countryNames.ts +104 -0
- package/src/matchers/override.test.ts +205 -0
- package/src/matchers/override.ts +136 -0
- package/src/matchers/posthog.ts +154 -0
- package/src/middleware/decoState.ts +55 -0
- package/src/middleware/healthMetrics.ts +133 -0
- package/src/middleware/hydrationContext.test.ts +61 -0
- package/src/middleware/hydrationContext.ts +79 -0
- package/src/middleware/index.ts +88 -0
- package/src/middleware/liveness.ts +21 -0
- package/src/middleware/observability.test.ts +238 -0
- package/src/middleware/observability.ts +620 -0
- package/src/middleware/validateSection.test.ts +147 -0
- package/src/middleware/validateSection.ts +100 -0
- package/src/sdk/abTesting.test.ts +326 -0
- package/src/sdk/abTesting.ts +499 -0
- package/src/sdk/analytics.ts +77 -0
- package/src/sdk/cacheHeaders.test.ts +115 -0
- package/src/sdk/cacheHeaders.ts +424 -0
- package/src/sdk/cachedLoader.ts +364 -0
- package/src/sdk/clx.ts +5 -0
- package/src/sdk/cn.test.ts +34 -0
- package/src/sdk/cn.ts +28 -0
- package/src/sdk/composite.test.ts +121 -0
- package/src/sdk/composite.ts +114 -0
- package/src/sdk/cookie.test.ts +108 -0
- package/src/sdk/cookie.ts +129 -0
- package/src/sdk/crypto.ts +185 -0
- package/src/sdk/csp.ts +59 -0
- package/src/sdk/djb2.ts +20 -0
- package/src/sdk/encoding.test.ts +71 -0
- package/src/sdk/encoding.ts +47 -0
- package/src/sdk/env.ts +31 -0
- package/src/sdk/http.test.ts +71 -0
- package/src/sdk/http.ts +124 -0
- package/src/sdk/index.ts +90 -0
- package/src/sdk/inflightTimeout.test.ts +35 -0
- package/src/sdk/inflightTimeout.ts +53 -0
- package/src/sdk/instrumentedFetch.test.ts +559 -0
- package/src/sdk/instrumentedFetch.ts +339 -0
- package/src/sdk/invoke.test.ts +115 -0
- package/src/sdk/invoke.ts +260 -0
- package/src/sdk/logger.test.ts +432 -0
- package/src/sdk/logger.ts +304 -0
- package/src/sdk/mergeCacheControl.ts +150 -0
- package/src/sdk/normalizeUrls.ts +91 -0
- package/src/sdk/observability.ts +109 -0
- package/src/sdk/otel.test.ts +526 -0
- package/src/sdk/otel.ts +981 -0
- package/src/sdk/otelAdapters/clickhouseCollector.ts +65 -0
- package/src/sdk/otelAdapters.test.ts +89 -0
- package/src/sdk/otelAdapters.ts +144 -0
- package/src/sdk/otelHttpLog.test.ts +457 -0
- package/src/sdk/otelHttpLog.ts +419 -0
- package/src/sdk/otelHttpMeter.test.ts +292 -0
- package/src/sdk/otelHttpMeter.ts +506 -0
- package/src/sdk/otelHttpTracer.test.ts +474 -0
- package/src/sdk/otelHttpTracer.ts +543 -0
- package/src/sdk/redirects.ts +225 -0
- package/src/sdk/requestContext.ts +281 -0
- package/src/sdk/requestContextStorage.browser.test.ts +29 -0
- package/src/sdk/requestContextStorage.browser.ts +43 -0
- package/src/sdk/requestContextStorage.ts +35 -0
- package/src/sdk/retry.ts +45 -0
- package/src/sdk/serverTimings.ts +68 -0
- package/src/sdk/signal.ts +42 -0
- package/src/sdk/sitemap.ts +160 -0
- package/src/sdk/urlRedaction.test.ts +73 -0
- package/src/sdk/urlRedaction.ts +82 -0
- package/src/sdk/urlUtils.ts +134 -0
- package/src/sdk/useDevice.test.ts +130 -0
- package/src/sdk/useDevice.ts +109 -0
- package/src/sdk/useDeviceContext.tsx +108 -0
- package/src/sdk/useId.ts +7 -0
- package/src/sdk/useScript.test.ts +128 -0
- package/src/sdk/useScript.ts +210 -0
- package/src/sdk/useSuggestions.test.ts +230 -0
- package/src/sdk/useSuggestions.ts +188 -0
- package/src/sdk/wrapCaughtErrors.ts +107 -0
- package/src/setup.ts +119 -0
- package/src/types/index.ts +39 -0
- package/src/types/widgets.ts +14 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified cache profile system for Deco storefronts.
|
|
3
|
+
*
|
|
4
|
+
* Each named profile (product, listing, search, static, etc.) defines cache
|
|
5
|
+
* timing across ALL layers — edge, browser, loader, and client — in a single
|
|
6
|
+
* object. This is the single source of truth for cache configuration.
|
|
7
|
+
*
|
|
8
|
+
* Sites override specific values via `setCacheProfile()` without touching
|
|
9
|
+
* framework code. Derivation functions (`cacheHeaders`, `routeCacheDefaults`,
|
|
10
|
+
* `loaderCacheOptions`, `edgeCacheConfig`) read from the profiles.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* // Site-level override (src/cache-config.ts):
|
|
15
|
+
* import { setCacheProfile } from "@decocms/start/sdk/cacheHeaders";
|
|
16
|
+
* setCacheProfile("product", { edge: { fresh: 600 } }); // 10min instead of 5min
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Types
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export type CacheProfileName =
|
|
25
|
+
| "static"
|
|
26
|
+
| "product"
|
|
27
|
+
| "listing"
|
|
28
|
+
| "search"
|
|
29
|
+
| "cart"
|
|
30
|
+
| "private"
|
|
31
|
+
| "none";
|
|
32
|
+
|
|
33
|
+
/** Time windows for a single caching layer (in seconds). */
|
|
34
|
+
export interface CacheTimingWindow {
|
|
35
|
+
/** How long content is considered fresh — served without origin contact. */
|
|
36
|
+
fresh: number;
|
|
37
|
+
/** After fresh expires, serve stale while refreshing in background. */
|
|
38
|
+
swr: number;
|
|
39
|
+
/** After fresh expires and origin is erroring, serve stale for this long. */
|
|
40
|
+
sie: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Unified cache profile covering all layers. */
|
|
44
|
+
export interface CacheProfileConfig {
|
|
45
|
+
/** Edge / CDN layer (Cloudflare Cache API). Times in seconds. */
|
|
46
|
+
edge: CacheTimingWindow;
|
|
47
|
+
/** Browser layer (Cache-Control header). Times in seconds. */
|
|
48
|
+
browser: CacheTimingWindow;
|
|
49
|
+
/** In-memory loader layer. Times in milliseconds. */
|
|
50
|
+
loader: {
|
|
51
|
+
fresh: number;
|
|
52
|
+
sie: number;
|
|
53
|
+
};
|
|
54
|
+
/** Client-side TanStack Router. Times in milliseconds. */
|
|
55
|
+
client: {
|
|
56
|
+
staleTime: number;
|
|
57
|
+
gcTime: number;
|
|
58
|
+
};
|
|
59
|
+
/** Whether CDN can cache this profile. False = private, never cached. */
|
|
60
|
+
isPublic: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Deep partial of CacheProfileConfig for site-level overrides.
|
|
65
|
+
* Only the fields you specify are merged; everything else keeps its default.
|
|
66
|
+
*/
|
|
67
|
+
export type CacheProfileOverrides = {
|
|
68
|
+
[K in keyof CacheProfileConfig]?: CacheProfileConfig[K] extends object
|
|
69
|
+
? Partial<CacheProfileConfig[K]>
|
|
70
|
+
: CacheProfileConfig[K];
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Default profiles
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
const PROFILES: Record<CacheProfileName, CacheProfileConfig> = {
|
|
78
|
+
static: {
|
|
79
|
+
edge: { fresh: 900, swr: 7200, sie: 21600 },
|
|
80
|
+
browser: { fresh: 120, swr: 1800, sie: 7200 },
|
|
81
|
+
loader: { fresh: 300_000, sie: 1_800_000 },
|
|
82
|
+
client: { staleTime: 300_000, gcTime: 1_800_000 },
|
|
83
|
+
isPublic: true,
|
|
84
|
+
},
|
|
85
|
+
product: {
|
|
86
|
+
edge: { fresh: 300, swr: 1800, sie: 7200 },
|
|
87
|
+
browser: { fresh: 60, swr: 600, sie: 3600 },
|
|
88
|
+
loader: { fresh: 30_000, sie: 600_000 },
|
|
89
|
+
client: { staleTime: 60_000, gcTime: 300_000 },
|
|
90
|
+
isPublic: true,
|
|
91
|
+
},
|
|
92
|
+
listing: {
|
|
93
|
+
edge: { fresh: 120, swr: 900, sie: 3600 },
|
|
94
|
+
browser: { fresh: 30, swr: 300, sie: 1800 },
|
|
95
|
+
loader: { fresh: 60_000, sie: 300_000 },
|
|
96
|
+
client: { staleTime: 60_000, gcTime: 300_000 },
|
|
97
|
+
isPublic: true,
|
|
98
|
+
},
|
|
99
|
+
search: {
|
|
100
|
+
edge: { fresh: 60, swr: 300, sie: 1800 },
|
|
101
|
+
browser: { fresh: 0, swr: 120, sie: 600 },
|
|
102
|
+
loader: { fresh: 60_000, sie: 180_000 },
|
|
103
|
+
client: { staleTime: 30_000, gcTime: 120_000 },
|
|
104
|
+
isPublic: true,
|
|
105
|
+
},
|
|
106
|
+
cart: {
|
|
107
|
+
edge: { fresh: 0, swr: 0, sie: 0 },
|
|
108
|
+
browser: { fresh: 0, swr: 0, sie: 0 },
|
|
109
|
+
loader: { fresh: 0, sie: 0 },
|
|
110
|
+
client: { staleTime: 0, gcTime: 0 },
|
|
111
|
+
isPublic: false,
|
|
112
|
+
},
|
|
113
|
+
private: {
|
|
114
|
+
edge: { fresh: 0, swr: 0, sie: 0 },
|
|
115
|
+
browser: { fresh: 0, swr: 0, sie: 0 },
|
|
116
|
+
loader: { fresh: 0, sie: 0 },
|
|
117
|
+
client: { staleTime: 0, gcTime: 0 },
|
|
118
|
+
isPublic: false,
|
|
119
|
+
},
|
|
120
|
+
none: {
|
|
121
|
+
edge: { fresh: 0, swr: 0, sie: 0 },
|
|
122
|
+
browser: { fresh: 0, swr: 0, sie: 0 },
|
|
123
|
+
loader: { fresh: 0, sie: 0 },
|
|
124
|
+
client: { staleTime: 0, gcTime: 0 },
|
|
125
|
+
isPublic: false,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Profile accessors
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
export function getCacheProfile(profile: CacheProfileName): CacheProfileConfig {
|
|
134
|
+
return PROFILES[profile];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Override specific values of a cache profile. Only the fields you specify
|
|
139
|
+
* are merged; everything else keeps its default.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```ts
|
|
143
|
+
* setCacheProfile("product", { edge: { fresh: 600 } });
|
|
144
|
+
* setCacheProfile("static", { loader: { sie: 3_600_000 } });
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
export function setCacheProfile(
|
|
148
|
+
profile: CacheProfileName,
|
|
149
|
+
overrides: CacheProfileOverrides,
|
|
150
|
+
): void {
|
|
151
|
+
const current = PROFILES[profile];
|
|
152
|
+
PROFILES[profile] = {
|
|
153
|
+
edge: { ...current.edge, ...overrides.edge },
|
|
154
|
+
browser: { ...current.browser, ...overrides.browser },
|
|
155
|
+
loader: { ...current.loader, ...overrides.loader },
|
|
156
|
+
client: { ...current.client, ...overrides.client },
|
|
157
|
+
isPublic: overrides.isPublic ?? current.isPublic,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Derivation: Cache-Control headers (browser layer)
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Generate a `Cache-Control` header from a named profile.
|
|
167
|
+
* Returns a headers object ready to spread into route `headers()`.
|
|
168
|
+
*/
|
|
169
|
+
export function cacheHeaders(profile: CacheProfileName): Record<string, string> {
|
|
170
|
+
const p = PROFILES[profile];
|
|
171
|
+
|
|
172
|
+
if (!p.isPublic || (p.edge.fresh === 0 && p.browser.fresh === 0)) {
|
|
173
|
+
return {
|
|
174
|
+
"Cache-Control": "private, no-cache, no-store, must-revalidate",
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const parts: string[] = ["public"];
|
|
179
|
+
parts.push(p.browser.fresh > 0 ? `max-age=${p.browser.fresh}` : "max-age=0");
|
|
180
|
+
if (p.edge.fresh > 0) parts.push(`s-maxage=${p.edge.fresh}`);
|
|
181
|
+
if (p.browser.swr > 0) parts.push(`stale-while-revalidate=${p.browser.swr}`);
|
|
182
|
+
if (p.browser.sie > 0) parts.push(`stale-if-error=${p.browser.sie}`);
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
"Cache-Control": parts.join(", "),
|
|
186
|
+
Vary: "Accept-Encoding",
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Derivation: Edge cache config (for workerEntry SWR/SIE logic)
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
export interface EdgeCacheConfig {
|
|
195
|
+
fresh: number;
|
|
196
|
+
swr: number;
|
|
197
|
+
sie: number;
|
|
198
|
+
isPublic: boolean;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function edgeCacheConfig(profile: CacheProfileName): EdgeCacheConfig {
|
|
202
|
+
const p = PROFILES[profile];
|
|
203
|
+
return { ...p.edge, isPublic: p.isPublic };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Derivation: Client-side route cache defaults (TanStack Router)
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Returns `{ staleTime, gcTime }` for a cache profile, ready to spread
|
|
212
|
+
* into a TanStack Router route definition.
|
|
213
|
+
*
|
|
214
|
+
* In dev mode, uses short staleTime (5s) to keep data fresh enough for
|
|
215
|
+
* development while avoiding redundant re-fetches.
|
|
216
|
+
*/
|
|
217
|
+
export function routeCacheDefaults(profile: CacheProfileName): { staleTime: number; gcTime: number } {
|
|
218
|
+
const env = typeof globalThis.process !== "undefined" ? globalThis.process.env : undefined;
|
|
219
|
+
const isDev = env?.DECO_CACHE_DISABLE === "true" || env?.NODE_ENV === "development";
|
|
220
|
+
if (isDev) return { staleTime: 5_000, gcTime: 30_000 };
|
|
221
|
+
return { ...PROFILES[profile].client };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// Derivation: Loader cache options (for createCachedLoader)
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
export interface LoaderCacheOptions {
|
|
229
|
+
policy: "stale-while-revalidate";
|
|
230
|
+
maxAge: number;
|
|
231
|
+
staleIfError: number;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get loader-layer cache options for a profile.
|
|
236
|
+
* Pass directly to `createCachedLoader` as the options argument.
|
|
237
|
+
*/
|
|
238
|
+
export function loaderCacheOptions(profile: CacheProfileName): LoaderCacheOptions {
|
|
239
|
+
const p = PROFILES[profile];
|
|
240
|
+
return {
|
|
241
|
+
policy: "stale-while-revalidate",
|
|
242
|
+
maxAge: p.loader.fresh,
|
|
243
|
+
staleIfError: p.loader.sie,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
// URL-based cache profile detection
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
interface CachePattern {
|
|
252
|
+
test: (pathname: string, searchParams: URLSearchParams) => boolean;
|
|
253
|
+
profile: CacheProfileName;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const PRIVATE_PREFIX_RE =
|
|
257
|
+
/^\/(cart|checkout|account|login|my-account)(\/|$)/;
|
|
258
|
+
|
|
259
|
+
const builtinPatterns: CachePattern[] = [
|
|
260
|
+
{
|
|
261
|
+
test: (p) => PRIVATE_PREFIX_RE.test(p),
|
|
262
|
+
profile: "private",
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
test: (p) =>
|
|
266
|
+
p.startsWith("/api/") ||
|
|
267
|
+
p.startsWith("/deco/") ||
|
|
268
|
+
p.startsWith("/_build"),
|
|
269
|
+
profile: "none",
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
test: (p, sp) => p === "/s" || p.startsWith("/s/") || sp.has("q"),
|
|
273
|
+
profile: "search",
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
test: (p) => p.endsWith("/p") || /\/p[?#]/.test(p),
|
|
277
|
+
profile: "product",
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
test: (p) => p === "/" || p === "",
|
|
281
|
+
profile: "static",
|
|
282
|
+
},
|
|
283
|
+
];
|
|
284
|
+
|
|
285
|
+
const customPatterns: CachePattern[] = [];
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Register additional URL-to-profile patterns. Custom patterns are evaluated
|
|
289
|
+
* before built-in ones, so they can override defaults.
|
|
290
|
+
*/
|
|
291
|
+
export function registerCachePattern(pattern: CachePattern): void {
|
|
292
|
+
customPatterns.push(pattern);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Detect the appropriate cache profile based on a URL.
|
|
297
|
+
* Evaluates custom patterns first, then built-in patterns.
|
|
298
|
+
* Falls back to "listing" for unmatched paths.
|
|
299
|
+
*/
|
|
300
|
+
export function detectCacheProfile(pathnameOrUrl: string | URL): CacheProfileName {
|
|
301
|
+
let pathname: string;
|
|
302
|
+
let searchParams: URLSearchParams;
|
|
303
|
+
|
|
304
|
+
if (typeof pathnameOrUrl === "string" && !pathnameOrUrl.startsWith("http")) {
|
|
305
|
+
const qIdx = pathnameOrUrl.indexOf("?");
|
|
306
|
+
pathname = qIdx >= 0 ? pathnameOrUrl.slice(0, qIdx) : pathnameOrUrl;
|
|
307
|
+
searchParams = new URLSearchParams(qIdx >= 0 ? pathnameOrUrl.slice(qIdx) : "");
|
|
308
|
+
} else {
|
|
309
|
+
const url = pathnameOrUrl instanceof URL ? pathnameOrUrl : new URL(pathnameOrUrl);
|
|
310
|
+
pathname = url.pathname;
|
|
311
|
+
searchParams = url.searchParams;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
for (const pattern of customPatterns) {
|
|
315
|
+
if (pattern.test(pathname, searchParams)) return pattern.profile;
|
|
316
|
+
}
|
|
317
|
+
for (const pattern of builtinPatterns) {
|
|
318
|
+
if (pattern.test(pathname, searchParams)) return pattern.profile;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return "listing";
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* For a TanStack GET server-fn request (`/_serverFn/<hash>?payload=...`), the
|
|
326
|
+
* page being loaded is encoded inside the serialized `payload` arg (e.g.
|
|
327
|
+
* `loadCmsPage`'s `data` string `/product/p`). The request pathname itself is
|
|
328
|
+
* `/_serverFn/...`, which {@link detectCacheProfile} can only classify as the
|
|
329
|
+
* generic "listing" profile — so SPA-navigation data requests would cache under
|
|
330
|
+
* a short TTL and rarely hit, even though the underlying page is a product
|
|
331
|
+
* (5min) or the homepage (1 day).
|
|
332
|
+
*
|
|
333
|
+
* Returns the embedded page path so the data request can inherit the SAME cache
|
|
334
|
+
* profile as its HTML document. Returns null for non-server-fn URLs or on any
|
|
335
|
+
* parse miss, so callers fall back to URL-based detection (never worse than the
|
|
336
|
+
* pre-existing behavior).
|
|
337
|
+
*
|
|
338
|
+
* The payload is TanStack's serialized envelope; rather than depend on its
|
|
339
|
+
* exact shape, we walk the parsed JSON for the first arg-value string that
|
|
340
|
+
* looks like a route path. `loadCmsPage` encodes exactly one such string.
|
|
341
|
+
*/
|
|
342
|
+
export function serverFnPagePath(url: URL): string | null {
|
|
343
|
+
if (
|
|
344
|
+
!url.pathname.startsWith("/_serverFn/") &&
|
|
345
|
+
!url.pathname.startsWith("/_server/")
|
|
346
|
+
) {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
const payload = url.searchParams.get("payload");
|
|
350
|
+
if (!payload) return null;
|
|
351
|
+
try {
|
|
352
|
+
let found: string | null = null;
|
|
353
|
+
const visit = (v: unknown): void => {
|
|
354
|
+
if (found !== null) return;
|
|
355
|
+
if (typeof v === "string") {
|
|
356
|
+
if (v.startsWith("/") && !v.startsWith("//")) found = v;
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (Array.isArray(v)) {
|
|
360
|
+
for (const x of v) visit(x);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (v && typeof v === "object") {
|
|
364
|
+
for (const x of Object.values(v)) visit(x);
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
visit(JSON.parse(payload));
|
|
368
|
+
return found;
|
|
369
|
+
} catch {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Search params that never change a CMS page's resolved server-fn response and
|
|
376
|
+
* so must not fragment its edge cache key. `skuId`/`idsku` select a product
|
|
377
|
+
* variant client-side (mirrors `cmsRouteConfig`'s default
|
|
378
|
+
* `ignoreSearchParams: ["skuId"]`) — without stripping them, every PDP→PDP
|
|
379
|
+
* navigation that carries a variant param is a unique key and always MISSes,
|
|
380
|
+
* re-running the full page resolution (~1-2s) even though the response is
|
|
381
|
+
* identical to the canonical `/product/p`.
|
|
382
|
+
*/
|
|
383
|
+
export const DEFAULT_SERVERFN_CACHE_IGNORE_PARAMS = ["skuId", "idsku"];
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Canonicalize a TanStack GET server-fn `payload` for use in the edge cache
|
|
387
|
+
* key: re-serialize it (so equivalent requests always produce byte-identical
|
|
388
|
+
* keys) and strip the given cache-irrelevant search params from any embedded
|
|
389
|
+
* route path. This makes `/product/p?skuId=148940` and `/product/p` share one
|
|
390
|
+
* cache entry, since `loadCmsPage` resolves both to the same page.
|
|
391
|
+
*
|
|
392
|
+
* Returns the original payload unchanged on any parse failure (fail-safe: the
|
|
393
|
+
* caller keeps the raw payload, never worse than before).
|
|
394
|
+
*/
|
|
395
|
+
export function canonicalizeServerFnPayloadForCacheKey(
|
|
396
|
+
payload: string,
|
|
397
|
+
ignoreParams: string[] = DEFAULT_SERVERFN_CACHE_IGNORE_PARAMS,
|
|
398
|
+
): string {
|
|
399
|
+
try {
|
|
400
|
+
const stripQuery = (s: string): string => {
|
|
401
|
+
if (!s.startsWith("/") || s.startsWith("//")) return s;
|
|
402
|
+
const qIdx = s.indexOf("?");
|
|
403
|
+
if (qIdx < 0) return s;
|
|
404
|
+
const path = s.slice(0, qIdx);
|
|
405
|
+
const sp = new URLSearchParams(s.slice(qIdx + 1));
|
|
406
|
+
for (const p of ignoreParams) sp.delete(p);
|
|
407
|
+
const rest = sp.toString();
|
|
408
|
+
return rest ? `${path}?${rest}` : path;
|
|
409
|
+
};
|
|
410
|
+
const visit = (v: unknown): unknown => {
|
|
411
|
+
if (typeof v === "string") return stripQuery(v);
|
|
412
|
+
if (Array.isArray(v)) return v.map(visit);
|
|
413
|
+
if (v && typeof v === "object") {
|
|
414
|
+
const out: Record<string, unknown> = {};
|
|
415
|
+
for (const [k, val] of Object.entries(v)) out[k] = visit(val);
|
|
416
|
+
return out;
|
|
417
|
+
}
|
|
418
|
+
return v;
|
|
419
|
+
};
|
|
420
|
+
return JSON.stringify(visit(JSON.parse(payload)));
|
|
421
|
+
} catch {
|
|
422
|
+
return payload;
|
|
423
|
+
}
|
|
424
|
+
}
|