@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.
Files changed (111) hide show
  1. package/package.json +97 -0
  2. package/src/cms/applySectionConventions.ts +128 -0
  3. package/src/cms/blockSource.test.ts +67 -0
  4. package/src/cms/blockSource.ts +124 -0
  5. package/src/cms/index.ts +104 -0
  6. package/src/cms/layoutCacheRace.test.ts +146 -0
  7. package/src/cms/loadDecofileDirectory.test.ts +52 -0
  8. package/src/cms/loadDecofileDirectory.ts +48 -0
  9. package/src/cms/loader.test.ts +184 -0
  10. package/src/cms/loader.ts +284 -0
  11. package/src/cms/registry.test.ts +118 -0
  12. package/src/cms/registry.ts +252 -0
  13. package/src/cms/resolve.test.ts +547 -0
  14. package/src/cms/resolve.ts +2003 -0
  15. package/src/cms/schema.ts +993 -0
  16. package/src/cms/sectionLoaders.test.ts +409 -0
  17. package/src/cms/sectionLoaders.ts +562 -0
  18. package/src/cms/sectionMixins.test.ts +163 -0
  19. package/src/cms/sectionMixins.ts +153 -0
  20. package/src/hooks/LazySection.tsx +121 -0
  21. package/src/hooks/LiveControls.tsx +122 -0
  22. package/src/hooks/RenderSection.test.tsx +81 -0
  23. package/src/hooks/RenderSection.tsx +91 -0
  24. package/src/hooks/SectionErrorFallback.tsx +97 -0
  25. package/src/hooks/index.ts +4 -0
  26. package/src/index.ts +8 -0
  27. package/src/matchers/builtins.test.ts +251 -0
  28. package/src/matchers/builtins.ts +437 -0
  29. package/src/matchers/countryNames.ts +104 -0
  30. package/src/matchers/override.test.ts +205 -0
  31. package/src/matchers/override.ts +136 -0
  32. package/src/matchers/posthog.ts +154 -0
  33. package/src/middleware/decoState.ts +55 -0
  34. package/src/middleware/healthMetrics.ts +133 -0
  35. package/src/middleware/hydrationContext.test.ts +61 -0
  36. package/src/middleware/hydrationContext.ts +79 -0
  37. package/src/middleware/index.ts +88 -0
  38. package/src/middleware/liveness.ts +21 -0
  39. package/src/middleware/observability.test.ts +238 -0
  40. package/src/middleware/observability.ts +620 -0
  41. package/src/middleware/validateSection.test.ts +147 -0
  42. package/src/middleware/validateSection.ts +100 -0
  43. package/src/sdk/abTesting.test.ts +326 -0
  44. package/src/sdk/abTesting.ts +499 -0
  45. package/src/sdk/analytics.ts +77 -0
  46. package/src/sdk/cacheHeaders.test.ts +115 -0
  47. package/src/sdk/cacheHeaders.ts +424 -0
  48. package/src/sdk/cachedLoader.ts +364 -0
  49. package/src/sdk/clx.ts +5 -0
  50. package/src/sdk/cn.test.ts +34 -0
  51. package/src/sdk/cn.ts +28 -0
  52. package/src/sdk/composite.test.ts +121 -0
  53. package/src/sdk/composite.ts +114 -0
  54. package/src/sdk/cookie.test.ts +108 -0
  55. package/src/sdk/cookie.ts +129 -0
  56. package/src/sdk/crypto.ts +185 -0
  57. package/src/sdk/csp.ts +59 -0
  58. package/src/sdk/djb2.ts +20 -0
  59. package/src/sdk/encoding.test.ts +71 -0
  60. package/src/sdk/encoding.ts +47 -0
  61. package/src/sdk/env.ts +31 -0
  62. package/src/sdk/http.test.ts +71 -0
  63. package/src/sdk/http.ts +124 -0
  64. package/src/sdk/index.ts +90 -0
  65. package/src/sdk/inflightTimeout.test.ts +35 -0
  66. package/src/sdk/inflightTimeout.ts +53 -0
  67. package/src/sdk/instrumentedFetch.test.ts +559 -0
  68. package/src/sdk/instrumentedFetch.ts +339 -0
  69. package/src/sdk/invoke.test.ts +115 -0
  70. package/src/sdk/invoke.ts +260 -0
  71. package/src/sdk/logger.test.ts +432 -0
  72. package/src/sdk/logger.ts +304 -0
  73. package/src/sdk/mergeCacheControl.ts +150 -0
  74. package/src/sdk/normalizeUrls.ts +91 -0
  75. package/src/sdk/observability.ts +109 -0
  76. package/src/sdk/otel.test.ts +526 -0
  77. package/src/sdk/otel.ts +981 -0
  78. package/src/sdk/otelAdapters/clickhouseCollector.ts +65 -0
  79. package/src/sdk/otelAdapters.test.ts +89 -0
  80. package/src/sdk/otelAdapters.ts +144 -0
  81. package/src/sdk/otelHttpLog.test.ts +457 -0
  82. package/src/sdk/otelHttpLog.ts +419 -0
  83. package/src/sdk/otelHttpMeter.test.ts +292 -0
  84. package/src/sdk/otelHttpMeter.ts +506 -0
  85. package/src/sdk/otelHttpTracer.test.ts +474 -0
  86. package/src/sdk/otelHttpTracer.ts +543 -0
  87. package/src/sdk/redirects.ts +225 -0
  88. package/src/sdk/requestContext.ts +281 -0
  89. package/src/sdk/requestContextStorage.browser.test.ts +29 -0
  90. package/src/sdk/requestContextStorage.browser.ts +43 -0
  91. package/src/sdk/requestContextStorage.ts +35 -0
  92. package/src/sdk/retry.ts +45 -0
  93. package/src/sdk/serverTimings.ts +68 -0
  94. package/src/sdk/signal.ts +42 -0
  95. package/src/sdk/sitemap.ts +160 -0
  96. package/src/sdk/urlRedaction.test.ts +73 -0
  97. package/src/sdk/urlRedaction.ts +82 -0
  98. package/src/sdk/urlUtils.ts +134 -0
  99. package/src/sdk/useDevice.test.ts +130 -0
  100. package/src/sdk/useDevice.ts +109 -0
  101. package/src/sdk/useDeviceContext.tsx +108 -0
  102. package/src/sdk/useId.ts +7 -0
  103. package/src/sdk/useScript.test.ts +128 -0
  104. package/src/sdk/useScript.ts +210 -0
  105. package/src/sdk/useSuggestions.test.ts +230 -0
  106. package/src/sdk/useSuggestions.ts +188 -0
  107. package/src/sdk/wrapCaughtErrors.ts +107 -0
  108. package/src/setup.ts +119 -0
  109. package/src/types/index.ts +39 -0
  110. package/src/types/widgets.ts +14 -0
  111. 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
+ }