@xenia-martech/sdk-js 1.2.0-dev.3 → 1.2.0-dev.4

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 (78) hide show
  1. package/README.md +106 -2
  2. package/dist/{actions-Bhji05Pe.d.cts → actions-C18Kv-4w.d.cts} +35 -0
  3. package/dist/{actions-Bhji05Pe.d.ts → actions-C18Kv-4w.d.ts} +35 -0
  4. package/dist/{api-ByoECRhR.d.ts → api-0IycTYJR.d.cts} +1 -1
  5. package/dist/{api-BqSvWgBZ.d.cts → api-COXWLL5L.d.ts} +1 -1
  6. package/dist/{chunk-TRRMMPBH.cjs → chunk-2DR5AVPX.cjs} +10 -45
  7. package/dist/chunk-2DR5AVPX.cjs.map +1 -0
  8. package/dist/chunk-BQ5PJCZ7.js +179 -0
  9. package/dist/chunk-BQ5PJCZ7.js.map +1 -0
  10. package/dist/{chunk-INM2SPH4.js → chunk-GWRDZIVA.js} +4 -36
  11. package/dist/chunk-GWRDZIVA.js.map +1 -0
  12. package/dist/{chunk-TGG5Y4QD.js → chunk-LW5AEJ6K.js} +5 -179
  13. package/dist/chunk-LW5AEJ6K.js.map +1 -0
  14. package/dist/chunk-N5M4PI7C.cjs +192 -0
  15. package/dist/chunk-N5M4PI7C.cjs.map +1 -0
  16. package/dist/chunk-OMMIRSPK.js +336 -0
  17. package/dist/chunk-OMMIRSPK.js.map +1 -0
  18. package/dist/chunk-QEYONKWY.js +42 -0
  19. package/dist/chunk-QEYONKWY.js.map +1 -0
  20. package/dist/{chunk-YZG6JR4E.cjs → chunk-RWE2OCQL.cjs} +5 -190
  21. package/dist/chunk-RWE2OCQL.cjs.map +1 -0
  22. package/dist/chunk-WHGK55JP.cjs +46 -0
  23. package/dist/chunk-WHGK55JP.cjs.map +1 -0
  24. package/dist/chunk-YLMMTKQL.cjs +358 -0
  25. package/dist/chunk-YLMMTKQL.cjs.map +1 -0
  26. package/dist/client/index.cjs +304 -26
  27. package/dist/client/index.cjs.map +1 -1
  28. package/dist/client/index.d.cts +15 -4
  29. package/dist/client/index.d.ts +15 -4
  30. package/dist/client/index.js +304 -26
  31. package/dist/client/index.js.map +1 -1
  32. package/dist/client/react/index.cjs +712 -26
  33. package/dist/client/react/index.cjs.map +1 -1
  34. package/dist/client/react/index.d.cts +32 -3
  35. package/dist/client/react/index.d.ts +32 -3
  36. package/dist/client/react/index.js +713 -29
  37. package/dist/client/react/index.js.map +1 -1
  38. package/dist/cms/contentful.d.cts +2 -1
  39. package/dist/cms/contentful.d.ts +2 -1
  40. package/dist/consent-Cemn91j3.d.ts +248 -0
  41. package/dist/consent-DgWdjsSy.d.cts +248 -0
  42. package/dist/{cookie-names-Bxd-VNyJ.d.ts → cookie-names-BvUaW3uo.d.ts} +1 -1
  43. package/dist/{cookie-names-RXVfGYPE.d.cts → cookie-names-CnL65Oly.d.cts} +1 -1
  44. package/dist/edge/index.cjs +280 -0
  45. package/dist/edge/index.cjs.map +1 -0
  46. package/dist/edge/index.d.cts +61 -0
  47. package/dist/edge/index.d.ts +61 -0
  48. package/dist/edge/index.js +191 -0
  49. package/dist/edge/index.js.map +1 -0
  50. package/dist/index.cjs +41 -40
  51. package/dist/index.d.cts +5 -4
  52. package/dist/index.d.ts +5 -4
  53. package/dist/index.js +2 -1
  54. package/dist/next/index.cjs +320 -15
  55. package/dist/next/index.cjs.map +1 -1
  56. package/dist/next/index.d.cts +180 -6
  57. package/dist/next/index.d.ts +180 -6
  58. package/dist/next/index.js +309 -9
  59. package/dist/next/index.js.map +1 -1
  60. package/dist/personalize-content-c2DDAV6g.d.ts +140 -0
  61. package/dist/personalize-content-ghZPKlf1.d.cts +140 -0
  62. package/dist/server/index.cjs +34 -32
  63. package/dist/server/index.cjs.map +1 -1
  64. package/dist/server/index.d.cts +6 -5
  65. package/dist/server/index.d.ts +6 -5
  66. package/dist/server/index.js +5 -3
  67. package/dist/server/index.js.map +1 -1
  68. package/dist/{personalize-content-Bf6YZdkZ.d.cts → types-DgVHS4Qw.d.cts} +7 -138
  69. package/dist/{personalize-content-Bf6YZdkZ.d.ts → types-DgVHS4Qw.d.ts} +7 -138
  70. package/dist/xenia-sdk.iife.js +3 -3
  71. package/dist/xenia-sdk.iife.js.map +1 -1
  72. package/dist/{xenia-server-MlI3Khya.d.cts → xenia-server-C3O-cWQT.d.cts} +3 -2
  73. package/dist/{xenia-server-CcjQenru.d.ts → xenia-server-DV8Sjmoo.d.ts} +3 -2
  74. package/package.json +14 -1
  75. package/dist/chunk-INM2SPH4.js.map +0 -1
  76. package/dist/chunk-TGG5Y4QD.js.map +0 -1
  77. package/dist/chunk-TRRMMPBH.cjs.map +0 -1
  78. package/dist/chunk-YZG6JR4E.cjs.map +0 -1
@@ -0,0 +1,280 @@
1
+ 'use strict';
2
+
3
+ var chunkYLMMTKQL_cjs = require('../chunk-YLMMTKQL.cjs');
4
+ require('../chunk-WHGK55JP.cjs');
5
+ require('../chunk-N5M4PI7C.cjs');
6
+
7
+ // src/edge/matcher.ts
8
+ function normalizePath(path) {
9
+ if (!path || !path.trim()) return "/";
10
+ let p = path.trim().toLowerCase();
11
+ const cut = Math.min(
12
+ ...["?", "#"].map((c) => p.includes(c) ? p.indexOf(c) : p.length)
13
+ );
14
+ p = p.slice(0, cut);
15
+ if (!p.startsWith("/")) p = "/" + p;
16
+ while (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
17
+ return p;
18
+ }
19
+ var normalizeHost = (host) => (host ?? "").trim().toLowerCase().replace(/:\d+$/, "");
20
+ function matches(pattern, host, path) {
21
+ if (!pattern) return false;
22
+ let patternPath = pattern;
23
+ if (!pattern.startsWith("/")) {
24
+ const slash = pattern.indexOf("/");
25
+ const patternHost = slash < 0 ? pattern : pattern.slice(0, slash);
26
+ if (patternHost !== host) return false;
27
+ patternPath = slash < 0 ? "/" : pattern.slice(slash);
28
+ }
29
+ if (patternPath === "/*") return true;
30
+ if (patternPath.endsWith("/*")) {
31
+ const prefix = patternPath.slice(0, -2);
32
+ return path === prefix || path.startsWith(prefix + "/");
33
+ }
34
+ return path === patternPath;
35
+ }
36
+ function matchesAny(patterns, host, path) {
37
+ if (!patterns?.length) return false;
38
+ const h = normalizeHost(host);
39
+ const p = normalizePath(path);
40
+ return patterns.some((pattern) => matches(pattern, h, p));
41
+ }
42
+
43
+ // src/edge/client.ts
44
+ function memoryDecisionCache(maxEntries = 1e4) {
45
+ const entries = /* @__PURE__ */ new Map();
46
+ return {
47
+ get(key) {
48
+ const hit = entries.get(key);
49
+ if (!hit) return void 0;
50
+ entries.delete(key);
51
+ if (hit.expires < Date.now()) return void 0;
52
+ entries.set(key, hit);
53
+ return hit.value;
54
+ },
55
+ set(key, value, ttlMs) {
56
+ entries.delete(key);
57
+ entries.set(key, { value, expires: Date.now() + ttlMs });
58
+ while (entries.size > maxEntries) {
59
+ entries.delete(entries.keys().next().value);
60
+ }
61
+ }
62
+ };
63
+ }
64
+ var stableJson = (facts) => JSON.stringify(
65
+ Object.keys(facts ?? {}).sort().map((k) => [k, facts[k]])
66
+ );
67
+ function normalizeManifest(m) {
68
+ if ("keys" in m && Array.isArray(m.keys)) return m;
69
+ const pages = m.pages ?? [];
70
+ return {
71
+ version: m.version,
72
+ generatedAt: "",
73
+ keys: pages.map((p) => ({ contentKey: "", mode: "server", pages: [p] }))
74
+ };
75
+ }
76
+ function provisional(profile) {
77
+ const answers = profile?.answers;
78
+ if (!answers || typeof answers !== "object") return false;
79
+ const steps = Object.values(answers);
80
+ return steps.some((a) => a?.source === "signals" && a.jev === true) || steps.length > 0 && steps.every((a) => !a?.choice);
81
+ }
82
+ function createXeniaEdge(config) {
83
+ const baseUrl = config.baseUrl.replace(/\/+$/, "");
84
+ const budgetMs = config.budgetMs ?? 150;
85
+ const manifestWaitMs = config.manifestWaitMs ?? 300;
86
+ const manifestTtlMs = config.manifestTtlMs ?? 3e4;
87
+ const decisionTtlMs = config.decisionTtlMs ?? 30 * 6e4;
88
+ const provisionalDecisionTtlMs = config.provisionalDecisionTtlMs ?? 15e3;
89
+ const cache = config.cache ?? memoryDecisionCache();
90
+ const doFetch = config.fetch ?? fetch;
91
+ const onError = config.onError ?? (() => {
92
+ });
93
+ const auth = { Authorization: `Bearer ${config.token}` };
94
+ let manifest = config.initialManifest?.version ? normalizeManifest(config.initialManifest) : null;
95
+ let manifestAt = 0;
96
+ let refreshing = null;
97
+ const inflight = /* @__PURE__ */ new Map();
98
+ function refreshManifest() {
99
+ refreshing ??= (async () => {
100
+ try {
101
+ const res = await doFetch(config.manifestUrl ?? `${baseUrl}/api/personalization/edge/manifest`, {
102
+ headers: config.manifestUrl ? {} : { ...auth, ...manifest ? { "If-None-Match": `"${manifest.version}"` } : {} },
103
+ signal: AbortSignal.timeout(5e3),
104
+ cache: "no-store"
105
+ });
106
+ if (res.status === 304) {
107
+ manifestAt = Date.now();
108
+ } else if (res.ok) {
109
+ manifest = normalizeManifest(await res.json());
110
+ manifestAt = Date.now();
111
+ } else {
112
+ onError(new Error(`manifest ${res.status}`), "manifest");
113
+ manifestAt = Date.now();
114
+ }
115
+ } catch (e) {
116
+ onError(e, "manifest");
117
+ manifestAt = Date.now();
118
+ } finally {
119
+ refreshing = null;
120
+ }
121
+ })();
122
+ return refreshing;
123
+ }
124
+ async function currentManifest() {
125
+ if (Date.now() - manifestAt > manifestTtlMs) {
126
+ const pending = refreshManifest();
127
+ if (!manifest) {
128
+ await Promise.race([pending, new Promise((r) => setTimeout(r, manifestWaitMs))]);
129
+ }
130
+ }
131
+ return manifest;
132
+ }
133
+ async function match(host, path) {
134
+ const m = await currentManifest();
135
+ if (!m) return null;
136
+ return m.keys.filter((k) => k.mode !== "off" && matchesAny(k.pages, host, path));
137
+ }
138
+ async function decide(keys, input) {
139
+ const version = manifest?.version ?? "";
140
+ const contentKeys = keys.map((k) => k.contentKey).filter(Boolean).sort();
141
+ const scope = contentKeys.length ? contentKeys.join(",") : keys.flatMap((k) => k.pages).sort().join(",");
142
+ const cacheKey = [version, input.visitorId, scope, stableJson(input.cacheFacts)].join("|");
143
+ const cached = cache.get(cacheKey);
144
+ if (cached) return { decisions: cached.decisions, profile: cached.profile, source: "cache", ms: 0 };
145
+ const pending = inflight.get(cacheKey);
146
+ if (pending) return pending;
147
+ const call = (async () => {
148
+ const started = Date.now();
149
+ try {
150
+ const res = await doFetch(`${baseUrl}/api/personalization/edge/decide`, {
151
+ method: "POST",
152
+ headers: { ...auth, "Content-Type": "application/json" },
153
+ body: JSON.stringify({
154
+ contentKeys,
155
+ host: input.host,
156
+ path: input.path,
157
+ visitorId: input.visitorId,
158
+ facts: input.facts,
159
+ ...input.trail?.length ? { trail: input.trail } : {}
160
+ }),
161
+ signal: AbortSignal.timeout(input.budgetMs ?? budgetMs),
162
+ cache: "no-store"
163
+ });
164
+ if (!res.ok) {
165
+ onError(new Error(`decide ${res.status}`), "decide");
166
+ return { decisions: [], source: "error", ms: Date.now() - started };
167
+ }
168
+ const result = await res.json();
169
+ if (!Array.isArray(result?.decisions)) throw new Error("decide: reply has no decisions list");
170
+ const decisions = result.decisions.filter((d) => d && Array.isArray(d.swaps));
171
+ const answer = { decisions, profile: result.profile };
172
+ cache.set(cacheKey, answer, input.ttlMs?.(answer) ?? (provisional(result.profile) ? provisionalDecisionTtlMs : decisionTtlMs));
173
+ if (manifest && result.version && result.version !== manifest.version) {
174
+ void refreshManifest();
175
+ }
176
+ return { decisions, profile: result.profile, source: "xenia", ms: Date.now() - started };
177
+ } catch (e) {
178
+ const timedOut = e instanceof DOMException && e.name === "TimeoutError";
179
+ if (!timedOut) onError(e, "decide");
180
+ return { decisions: [], source: timedOut ? "timeout" : "error", ms: Date.now() - started };
181
+ } finally {
182
+ inflight.delete(cacheKey);
183
+ }
184
+ })();
185
+ inflight.set(cacheKey, call);
186
+ return call;
187
+ }
188
+ return { match, decide, refreshManifest };
189
+ }
190
+
191
+ Object.defineProperty(exports, "MAX_TRAIL", {
192
+ enumerable: true,
193
+ get: function () { return chunkYLMMTKQL_cjs.MAX_TRAIL; }
194
+ });
195
+ Object.defineProperty(exports, "TRAIL_MAX_AGE_S", {
196
+ enumerable: true,
197
+ get: function () { return chunkYLMMTKQL_cjs.TRAIL_MAX_AGE_S; }
198
+ });
199
+ Object.defineProperty(exports, "decodeVariant", {
200
+ enumerable: true,
201
+ get: function () { return chunkYLMMTKQL_cjs.decodeVariant; }
202
+ });
203
+ Object.defineProperty(exports, "deviceOf", {
204
+ enumerable: true,
205
+ get: function () { return chunkYLMMTKQL_cjs.deviceOf; }
206
+ });
207
+ Object.defineProperty(exports, "edgeConsent", {
208
+ enumerable: true,
209
+ get: function () { return chunkYLMMTKQL_cjs.edgeConsent; }
210
+ });
211
+ Object.defineProperty(exports, "encodeVariant", {
212
+ enumerable: true,
213
+ get: function () { return chunkYLMMTKQL_cjs.encodeVariant; }
214
+ });
215
+ Object.defineProperty(exports, "fetchManifest", {
216
+ enumerable: true,
217
+ get: function () { return chunkYLMMTKQL_cjs.fetchManifest; }
218
+ });
219
+ Object.defineProperty(exports, "isBot", {
220
+ enumerable: true,
221
+ get: function () { return chunkYLMMTKQL_cjs.isBot; }
222
+ });
223
+ Object.defineProperty(exports, "isPageLoad", {
224
+ enumerable: true,
225
+ get: function () { return chunkYLMMTKQL_cjs.isPageLoad; }
226
+ });
227
+ Object.defineProperty(exports, "isPrefetch", {
228
+ enumerable: true,
229
+ get: function () { return chunkYLMMTKQL_cjs.isPrefetch; }
230
+ });
231
+ Object.defineProperty(exports, "landingContext", {
232
+ enumerable: true,
233
+ get: function () { return chunkYLMMTKQL_cjs.landingContext; }
234
+ });
235
+ Object.defineProperty(exports, "localTime", {
236
+ enumerable: true,
237
+ get: function () { return chunkYLMMTKQL_cjs.localTime; }
238
+ });
239
+ Object.defineProperty(exports, "readTrail", {
240
+ enumerable: true,
241
+ get: function () { return chunkYLMMTKQL_cjs.readTrail; }
242
+ });
243
+ Object.defineProperty(exports, "seasonOf", {
244
+ enumerable: true,
245
+ get: function () { return chunkYLMMTKQL_cjs.seasonOf; }
246
+ });
247
+ Object.defineProperty(exports, "swapGroups", {
248
+ enumerable: true,
249
+ get: function () { return chunkYLMMTKQL_cjs.swapGroups; }
250
+ });
251
+ Object.defineProperty(exports, "trailCookieName", {
252
+ enumerable: true,
253
+ get: function () { return chunkYLMMTKQL_cjs.trailCookieName; }
254
+ });
255
+ Object.defineProperty(exports, "trailCookieValue", {
256
+ enumerable: true,
257
+ get: function () { return chunkYLMMTKQL_cjs.trailCookieValue; }
258
+ });
259
+ Object.defineProperty(exports, "trimManifest", {
260
+ enumerable: true,
261
+ get: function () { return chunkYLMMTKQL_cjs.trimManifest; }
262
+ });
263
+ Object.defineProperty(exports, "verifyWebhook", {
264
+ enumerable: true,
265
+ get: function () { return chunkYLMMTKQL_cjs.verifyWebhook; }
266
+ });
267
+ Object.defineProperty(exports, "viewerGeo", {
268
+ enumerable: true,
269
+ get: function () { return chunkYLMMTKQL_cjs.viewerGeo; }
270
+ });
271
+ Object.defineProperty(exports, "withView", {
272
+ enumerable: true,
273
+ get: function () { return chunkYLMMTKQL_cjs.withView; }
274
+ });
275
+ exports.createXeniaEdge = createXeniaEdge;
276
+ exports.matchesAny = matchesAny;
277
+ exports.memoryDecisionCache = memoryDecisionCache;
278
+ exports.normalizePath = normalizePath;
279
+ //# sourceMappingURL=index.cjs.map
280
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/edge/matcher.ts","../../src/edge/client.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Page matching for the Xenia manifest — a port of the backend's\n * `EdgePageMatcher`, which is the reference:\n *\n * - `/women` exactly that path (case, trailing slash, query ignored)\n * - `/women/*` `/women` and everything below it, not `/womenswear`\n * - `/*` every path\n * - `uk.example.com/women` that path on that host only; hostless patterns match any host\n */\n\nexport function normalizePath(path: string | null | undefined): string {\n if (!path || !path.trim()) return \"/\";\n let p = path.trim().toLowerCase();\n const cut = Math.min(\n ...[\"?\", \"#\"].map((c) => (p.includes(c) ? p.indexOf(c) : p.length)),\n );\n p = p.slice(0, cut);\n if (!p.startsWith(\"/\")) p = \"/\" + p;\n while (p.length > 1 && p.endsWith(\"/\")) p = p.slice(0, -1);\n return p;\n}\n\nconst normalizeHost = (host: string | null | undefined) =>\n (host ?? \"\").trim().toLowerCase().replace(/:\\d+$/, \"\");\n\nfunction matches(pattern: string, host: string, path: string): boolean {\n if (!pattern) return false;\n let patternPath = pattern;\n if (!pattern.startsWith(\"/\")) {\n const slash = pattern.indexOf(\"/\");\n const patternHost = slash < 0 ? pattern : pattern.slice(0, slash);\n if (patternHost !== host) return false;\n patternPath = slash < 0 ? \"/\" : pattern.slice(slash);\n }\n if (patternPath === \"/*\") return true;\n if (patternPath.endsWith(\"/*\")) {\n const prefix = patternPath.slice(0, -2);\n return path === prefix || path.startsWith(prefix + \"/\");\n }\n return path === patternPath;\n}\n\nexport function matchesAny(\n patterns: readonly string[] | null | undefined,\n host: string | null | undefined,\n path: string | null | undefined,\n): boolean {\n if (!patterns?.length) return false;\n const h = normalizeHost(host);\n const p = normalizePath(path);\n return patterns.some((pattern) => matches(pattern, h, p));\n}\n","/**\n * Proxy-side Xenia client for the hybrid personalization flow:\n *\n * 1. `match()` — synchronous, in-memory: does any journey target this page?\n * Answered from the manifest, refreshed in the background every\n * `manifestTtlMs`. Pages no journey targets never call Xenia.\n * 2. `decide()` — for targeted pages only: a per-visitor decision cache hit\n * costs nothing; a miss calls Xenia's `/decide` with a hard `budgetMs`\n * deadline. Timeouts and errors resolve to \"no decision\" (base content),\n * never an exception — Xenia can't slow down or break a page.\n *\n * Runtime-agnostic (only `fetch`, `AbortSignal`, `Map`). The decision cache is\n * per process; swap in `cache` for a shared store (Redis, edge KV).\n */\nimport { matchesAny } from \"./matcher\";\nimport type {\n DecideResult,\n DecisionSource,\n EdgeDecision,\n EdgeFacts,\n Manifest,\n ManifestKey,\n ProfileFacts,\n PublicManifest,\n TrailView,\n} from \"./types\";\n\n/** What a visitor's cached decision holds: the decisions and the profile they were made with. */\nexport type CachedDecision = { decisions: EdgeDecision[]; profile?: ProfileFacts | null };\n\nexport type DecisionCache = {\n get(key: string): CachedDecision | undefined;\n set(key: string, value: CachedDecision, ttlMs: number): void;\n};\n\nexport type XeniaEdgeConfig = {\n /** Xenia engine base URL, e.g. `https://engine.xeniamartech.com/xenia`. */\n baseUrl: string;\n /** A server-type SDK token. Never expose it to the browser. */\n token: string;\n /** Hard deadline for one `/decide` call. Default 150 ms. */\n budgetMs?: number;\n /**\n * Where to read the manifest. Default: Xenia's `/edge/manifest` (with the token). A storefront\n * can serve its own CDN-cached copy instead (e.g. a Next.js ISR route) — then no token is sent.\n */\n manifestUrl?: string;\n /**\n * A manifest baked in at build time. An instance starts with it in memory, so a cold start never\n * waits: the first request uses it and triggers a background refresh. Empty version = none.\n */\n initialManifest?: Manifest | PublicManifest;\n /** How long a cold start waits for the first manifest (only without `initialManifest`). Default 300 ms. */\n manifestWaitMs?: number;\n /** Manifest refresh interval. Default 30 s. */\n manifestTtlMs?: number;\n /** How long a visitor keeps a decision. Default 30 min (a session). */\n decisionTtlMs?: number;\n /**\n * How long a provisional decision is kept (see `provisional`): Jev's answer, or the visitor's first\n * signals, usually land seconds later, so the next view should ask again. Default 15 s.\n */\n provisionalDecisionTtlMs?: number;\n cache?: DecisionCache;\n fetch?: typeof fetch;\n onError?: (error: unknown, context: string) => void;\n};\n\nexport type DecideInput = {\n host: string | null;\n path: string;\n visitorId: string;\n facts: EdgeFacts;\n /** Facts that identify this visitor's context for caching (e.g. landing UTM, device). */\n cacheFacts?: EdgeFacts;\n /** The visitor's recent page views, most recent first (see `TrailView`). */\n trail?: TrailView[];\n /** This call's time budget, instead of the client's (a decision that calls a back office). */\n budgetMs?: number;\n /** How long to keep this call's answer, from what it said, instead of the client's. */\n ttlMs?: (answer: CachedDecision) => number;\n};\n\nexport type DecideOutcome = {\n decisions: EdgeDecision[];\n /** The profile Xenia decided with (also on cache hits), when profiles are on. */\n profile?: ProfileFacts | null;\n source: DecisionSource;\n /** Time spent in Xenia, 0 on a cache hit. */\n ms: number;\n};\n\n/** Bounded in-memory LRU with per-entry expiry. */\nexport function memoryDecisionCache(maxEntries = 10_000): DecisionCache {\n const entries = new Map<string, { value: CachedDecision; expires: number }>();\n return {\n get(key) {\n const hit = entries.get(key);\n if (!hit) return undefined;\n entries.delete(key);\n if (hit.expires < Date.now()) return undefined;\n entries.set(key, hit); // most recently used last\n return hit.value;\n },\n set(key, value, ttlMs) {\n entries.delete(key);\n entries.set(key, { value, expires: Date.now() + ttlMs });\n while (entries.size > maxEntries) {\n entries.delete(entries.keys().next().value as string);\n }\n },\n };\n}\n\nconst stableJson = (facts: EdgeFacts | undefined) =>\n JSON.stringify(\n Object.keys(facts ?? {})\n .sort()\n .map((k) => [k, facts![k]]),\n );\n\n/**\n * Xenia's manifest, or a storefront's trimmed public copy (`{version, pages}`). The trimmed form\n * becomes one key-less entry per page pattern: the proxy then decides by path, and Xenia picks the\n * content keys itself.\n */\nfunction normalizeManifest(m: Manifest | PublicManifest): Manifest {\n if (\"keys\" in m && Array.isArray(m.keys)) return m;\n const pages = (m as PublicManifest).pages ?? [];\n return {\n version: m.version,\n generatedAt: \"\",\n keys: pages.map((p) => ({ contentKey: \"\", mode: \"server\" as const, pages: [p] })),\n };\n}\n\n/**\n * A provisional decision, to ask again soon:\n * - a Visitor Intent step that refines with Jev answered from its signals because Jev hasn't yet;\n * - Visitor Intent steps with no answer at all: the visitor's first signals may land any moment —\n * and for a visitor who is the same in every browser from another browser,\n * which this browser's cache facts (trail, clicks) never see.\n * Steps answered from signals alone change with the visitor's own views, which the cache facts do see.\n */\nfunction provisional(profile: ProfileFacts | null | undefined): boolean {\n const answers = profile?.answers;\n if (!answers || typeof answers !== \"object\") return false;\n const steps = Object.values(answers as Record<string, { choice?: string | null; source?: string; jev?: boolean } | null>);\n return steps.some((a) => a?.source === \"signals\" && a.jev === true) || (steps.length > 0 && steps.every((a) => !a?.choice));\n}\n\nexport function createXeniaEdge(config: XeniaEdgeConfig) {\n const baseUrl = config.baseUrl.replace(/\\/+$/, \"\");\n const budgetMs = config.budgetMs ?? 150;\n const manifestWaitMs = config.manifestWaitMs ?? 300;\n const manifestTtlMs = config.manifestTtlMs ?? 30_000;\n const decisionTtlMs = config.decisionTtlMs ?? 30 * 60_000;\n const provisionalDecisionTtlMs = config.provisionalDecisionTtlMs ?? 15_000;\n const cache = config.cache ?? memoryDecisionCache();\n const doFetch = config.fetch ?? fetch;\n const onError = config.onError ?? (() => {});\n const auth = { Authorization: `Bearer ${config.token}` };\n\n // A bundled manifest is used right away; manifestAt 0 makes the first request refresh it in the\n // background (currentManifest only blocks when there is no manifest at all).\n let manifest: Manifest | null = config.initialManifest?.version\n ? normalizeManifest(config.initialManifest)\n : null;\n let manifestAt = 0;\n let refreshing: Promise<void> | null = null;\n const inflight = new Map<string, Promise<DecideOutcome>>();\n\n /** Fetch the manifest (304-aware); keeps the last good one on failure. */\n function refreshManifest(): Promise<void> {\n refreshing ??= (async () => {\n try {\n const res = await doFetch(config.manifestUrl ?? `${baseUrl}/api/personalization/edge/manifest`, {\n headers: config.manifestUrl\n ? {}\n : { ...auth, ...(manifest ? { \"If-None-Match\": `\"${manifest.version}\"` } : {}) },\n signal: AbortSignal.timeout(5_000),\n cache: \"no-store\",\n });\n if (res.status === 304) {\n manifestAt = Date.now();\n } else if (res.ok) {\n manifest = normalizeManifest((await res.json()) as Manifest | PublicManifest);\n manifestAt = Date.now();\n } else {\n onError(new Error(`manifest ${res.status}`), \"manifest\");\n manifestAt = Date.now(); // back off for a TTL before retrying\n }\n } catch (e) {\n onError(e, \"manifest\");\n manifestAt = Date.now();\n } finally {\n refreshing = null;\n }\n })();\n return refreshing;\n }\n\n /** The current manifest; stale-while-revalidate, a bounded wait on cold start. */\n async function currentManifest(): Promise<Manifest | null> {\n if (Date.now() - manifestAt > manifestTtlMs) {\n const pending = refreshManifest();\n if (!manifest) {\n await Promise.race([pending, new Promise((r) => setTimeout(r, manifestWaitMs))]);\n }\n }\n return manifest;\n }\n\n /**\n * The content keys targeting this page, or `null` when no manifest is\n * available yet. An empty array means \"don't call Xenia\".\n */\n async function match(host: string | null, path: string): Promise<ManifestKey[] | null> {\n const m = await currentManifest();\n if (!m) return null;\n return m.keys.filter((k) => k.mode !== \"off\" && matchesAny(k.pages, host, path));\n }\n\n async function decide(keys: ManifestKey[], input: DecideInput): Promise<DecideOutcome> {\n const version = manifest?.version ?? \"\";\n // Known keys are sent explicitly; with a trimmed manifest there are none, and Xenia selects\n // the keys whose pages match the path. Either way the cache is shared by every page that\n // matches the same keys / patterns (e.g. all of /women/*).\n const contentKeys = keys.map((k) => k.contentKey).filter(Boolean).sort();\n const scope = contentKeys.length ? contentKeys.join(\",\") : keys.flatMap((k) => k.pages).sort().join(\",\");\n const cacheKey = [version, input.visitorId, scope, stableJson(input.cacheFacts)].join(\"|\");\n\n const cached = cache.get(cacheKey);\n if (cached) return { decisions: cached.decisions, profile: cached.profile, source: \"cache\", ms: 0 };\n\n // Concurrent requests from one visitor (HTML + RSC prefetches) share a call.\n const pending = inflight.get(cacheKey);\n if (pending) return pending;\n\n const call = (async (): Promise<DecideOutcome> => {\n const started = Date.now();\n try {\n const res = await doFetch(`${baseUrl}/api/personalization/edge/decide`, {\n method: \"POST\",\n headers: { ...auth, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n contentKeys,\n host: input.host,\n path: input.path,\n visitorId: input.visitorId,\n facts: input.facts,\n ...(input.trail?.length ? { trail: input.trail } : {}),\n }),\n signal: AbortSignal.timeout(input.budgetMs ?? budgetMs),\n cache: \"no-store\",\n });\n if (!res.ok) {\n onError(new Error(`decide ${res.status}`), \"decide\");\n return { decisions: [], source: \"error\", ms: Date.now() - started };\n }\n const result = (await res.json()) as DecideResult | null;\n // A reply in the wrong shape is an error, never cached: it would break every view for the TTL.\n if (!Array.isArray(result?.decisions)) throw new Error(\"decide: reply has no decisions list\");\n const decisions = result.decisions.filter((d) => d && Array.isArray(d.swaps));\n const answer = { decisions, profile: result.profile };\n cache.set(cacheKey, answer, input.ttlMs?.(answer) ?? (provisional(result.profile) ? provisionalDecisionTtlMs : decisionTtlMs));\n if (manifest && result.version && result.version !== manifest.version) {\n void refreshManifest(); // journeys changed — pick up the new manifest\n }\n return { decisions, profile: result.profile, source: \"xenia\", ms: Date.now() - started };\n } catch (e) {\n const timedOut = e instanceof DOMException && e.name === \"TimeoutError\";\n if (!timedOut) onError(e, \"decide\");\n return { decisions: [], source: timedOut ? \"timeout\" : \"error\", ms: Date.now() - started };\n } finally {\n inflight.delete(cacheKey);\n }\n })();\n inflight.set(cacheKey, call);\n return call;\n }\n\n return { match, decide, refreshManifest };\n}\n\nexport type XeniaEdge = ReturnType<typeof createXeniaEdge>;\n"]}
@@ -0,0 +1,61 @@
1
+ import { M as Manifest, P as PublicManifest, T as TrailView } from '../consent-DgWdjsSy.cjs';
2
+ export { C as CachedDecision, D as DecideInput, a as DecideOutcome, b as DecideResult, c as DecisionCache, d as DecisionSource, E as EdgeConsent, e as EdgeDecision, f as EdgeFacts, g as EdgeMode, G as GeoHeader, L as LocalTime, h as ManifestKey, i as ProfileFacts, S as SwapGroup, V as VariantSwap, j as ViewerGeo, X as XeniaEdge, k as XeniaEdgeConfig, l as createXeniaEdge, m as decodeVariant, n as deviceOf, o as edgeConsent, p as encodeVariant, q as isBot, r as isPageLoad, s as isPrefetch, t as landingContext, u as localTime, v as memoryDecisionCache, w as seasonOf, x as swapGroups, y as viewerGeo } from '../consent-DgWdjsSy.cjs';
3
+ import '../types-DgVHS4Qw.cjs';
4
+
5
+ /**
6
+ * Page matching for the Xenia manifest — a port of the backend's
7
+ * `EdgePageMatcher`, which is the reference:
8
+ *
9
+ * - `/women` exactly that path (case, trailing slash, query ignored)
10
+ * - `/women/*` `/women` and everything below it, not `/womenswear`
11
+ * - `/*` every path
12
+ * - `uk.example.com/women` that path on that host only; hostless patterns match any host
13
+ */
14
+ declare function normalizePath(path: string | null | undefined): string;
15
+ declare function matchesAny(patterns: readonly string[] | null | undefined, host: string | null | undefined, path: string | null | undefined): boolean;
16
+
17
+ interface FetchManifestOptions {
18
+ /** Xenia engine base URL. Default the hosted engine. */
19
+ baseUrl?: string;
20
+ /** A server-type SDK token. */
21
+ token: string;
22
+ /** Default 5000 ms. */
23
+ timeoutMs?: number;
24
+ fetch?: typeof fetch;
25
+ /** Extra fetch options, e.g. Next.js `{ next: { revalidate } }`. */
26
+ init?: RequestInit;
27
+ }
28
+ /** Xenia's manifest (`GET /api/personalization/edge/manifest`). Throws on a non-2xx answer. */
29
+ declare function fetchManifest(opts: FetchManifestOptions): Promise<Manifest>;
30
+ /**
31
+ * The manifest as a storefront may serve it publicly (e.g. from a CDN): the version and the page
32
+ * patterns any live journey targets — no content keys, modes or rules. A proxy reading it decides by
33
+ * path, and Xenia picks the content keys itself.
34
+ */
35
+ declare function trimManifest(m: Manifest): PublicManifest;
36
+
37
+ /**
38
+ * Whether a storefront webhook from Xenia is genuine and fresh:
39
+ * `X-Xenia-Signature: t=<unix s>,v1=<hex HMAC-SHA256(secret, "<t>.<body>")>`. Requests older than
40
+ * `toleranceS` are refused, so a captured request can't be replayed. Pass the raw body text.
41
+ */
42
+ declare function verifyWebhook(input: {
43
+ headers: Headers;
44
+ body: string;
45
+ }, secret: string, toleranceS?: number): Promise<boolean>;
46
+
47
+ declare const MAX_TRAIL = 8;
48
+ /** Cookie lifetime: a visit ends after 30 idle minutes, like the SDK's session. */
49
+ declare const TRAIL_MAX_AGE_S: number;
50
+ declare function trailCookieName(prefix?: string): string;
51
+ /** The trail in a cookie value (raw or URI-encoded); anything malformed reads as no trail. */
52
+ declare function readTrail(value: string | undefined | null): TrailView[];
53
+ /**
54
+ * The trail with this view first: a revisit moves to the front and keeps its first time (its old
55
+ * time on page is dropped — this view's isn't known yet). The same array when `path` is already first.
56
+ */
57
+ declare function withView(trail: TrailView[], path: string, now?: number): TrailView[];
58
+ /** The cookie value for a trail (not URI-encoded; cookie setters encode it). */
59
+ declare function trailCookieValue(trail: TrailView[]): string;
60
+
61
+ export { type FetchManifestOptions, MAX_TRAIL, Manifest, PublicManifest, TRAIL_MAX_AGE_S, TrailView, fetchManifest, matchesAny, normalizePath, readTrail, trailCookieName, trailCookieValue, trimManifest, verifyWebhook, withView };
@@ -0,0 +1,61 @@
1
+ import { M as Manifest, P as PublicManifest, T as TrailView } from '../consent-Cemn91j3.js';
2
+ export { C as CachedDecision, D as DecideInput, a as DecideOutcome, b as DecideResult, c as DecisionCache, d as DecisionSource, E as EdgeConsent, e as EdgeDecision, f as EdgeFacts, g as EdgeMode, G as GeoHeader, L as LocalTime, h as ManifestKey, i as ProfileFacts, S as SwapGroup, V as VariantSwap, j as ViewerGeo, X as XeniaEdge, k as XeniaEdgeConfig, l as createXeniaEdge, m as decodeVariant, n as deviceOf, o as edgeConsent, p as encodeVariant, q as isBot, r as isPageLoad, s as isPrefetch, t as landingContext, u as localTime, v as memoryDecisionCache, w as seasonOf, x as swapGroups, y as viewerGeo } from '../consent-Cemn91j3.js';
3
+ import '../types-DgVHS4Qw.js';
4
+
5
+ /**
6
+ * Page matching for the Xenia manifest — a port of the backend's
7
+ * `EdgePageMatcher`, which is the reference:
8
+ *
9
+ * - `/women` exactly that path (case, trailing slash, query ignored)
10
+ * - `/women/*` `/women` and everything below it, not `/womenswear`
11
+ * - `/*` every path
12
+ * - `uk.example.com/women` that path on that host only; hostless patterns match any host
13
+ */
14
+ declare function normalizePath(path: string | null | undefined): string;
15
+ declare function matchesAny(patterns: readonly string[] | null | undefined, host: string | null | undefined, path: string | null | undefined): boolean;
16
+
17
+ interface FetchManifestOptions {
18
+ /** Xenia engine base URL. Default the hosted engine. */
19
+ baseUrl?: string;
20
+ /** A server-type SDK token. */
21
+ token: string;
22
+ /** Default 5000 ms. */
23
+ timeoutMs?: number;
24
+ fetch?: typeof fetch;
25
+ /** Extra fetch options, e.g. Next.js `{ next: { revalidate } }`. */
26
+ init?: RequestInit;
27
+ }
28
+ /** Xenia's manifest (`GET /api/personalization/edge/manifest`). Throws on a non-2xx answer. */
29
+ declare function fetchManifest(opts: FetchManifestOptions): Promise<Manifest>;
30
+ /**
31
+ * The manifest as a storefront may serve it publicly (e.g. from a CDN): the version and the page
32
+ * patterns any live journey targets — no content keys, modes or rules. A proxy reading it decides by
33
+ * path, and Xenia picks the content keys itself.
34
+ */
35
+ declare function trimManifest(m: Manifest): PublicManifest;
36
+
37
+ /**
38
+ * Whether a storefront webhook from Xenia is genuine and fresh:
39
+ * `X-Xenia-Signature: t=<unix s>,v1=<hex HMAC-SHA256(secret, "<t>.<body>")>`. Requests older than
40
+ * `toleranceS` are refused, so a captured request can't be replayed. Pass the raw body text.
41
+ */
42
+ declare function verifyWebhook(input: {
43
+ headers: Headers;
44
+ body: string;
45
+ }, secret: string, toleranceS?: number): Promise<boolean>;
46
+
47
+ declare const MAX_TRAIL = 8;
48
+ /** Cookie lifetime: a visit ends after 30 idle minutes, like the SDK's session. */
49
+ declare const TRAIL_MAX_AGE_S: number;
50
+ declare function trailCookieName(prefix?: string): string;
51
+ /** The trail in a cookie value (raw or URI-encoded); anything malformed reads as no trail. */
52
+ declare function readTrail(value: string | undefined | null): TrailView[];
53
+ /**
54
+ * The trail with this view first: a revisit moves to the front and keeps its first time (its old
55
+ * time on page is dropped — this view's isn't known yet). The same array when `path` is already first.
56
+ */
57
+ declare function withView(trail: TrailView[], path: string, now?: number): TrailView[];
58
+ /** The cookie value for a trail (not URI-encoded; cookie setters encode it). */
59
+ declare function trailCookieValue(trail: TrailView[]): string;
60
+
61
+ export { type FetchManifestOptions, MAX_TRAIL, Manifest, PublicManifest, TRAIL_MAX_AGE_S, TrailView, fetchManifest, matchesAny, normalizePath, readTrail, trailCookieName, trailCookieValue, trimManifest, verifyWebhook, withView };
@@ -0,0 +1,191 @@
1
+ export { MAX_TRAIL, TRAIL_MAX_AGE_S, decodeVariant, deviceOf, edgeConsent, encodeVariant, fetchManifest, isBot, isPageLoad, isPrefetch, landingContext, localTime, readTrail, seasonOf, swapGroups, trailCookieName, trailCookieValue, trimManifest, verifyWebhook, viewerGeo, withView } from '../chunk-OMMIRSPK.js';
2
+ import '../chunk-QEYONKWY.js';
3
+ import '../chunk-BQ5PJCZ7.js';
4
+
5
+ // src/edge/matcher.ts
6
+ function normalizePath(path) {
7
+ if (!path || !path.trim()) return "/";
8
+ let p = path.trim().toLowerCase();
9
+ const cut = Math.min(
10
+ ...["?", "#"].map((c) => p.includes(c) ? p.indexOf(c) : p.length)
11
+ );
12
+ p = p.slice(0, cut);
13
+ if (!p.startsWith("/")) p = "/" + p;
14
+ while (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
15
+ return p;
16
+ }
17
+ var normalizeHost = (host) => (host ?? "").trim().toLowerCase().replace(/:\d+$/, "");
18
+ function matches(pattern, host, path) {
19
+ if (!pattern) return false;
20
+ let patternPath = pattern;
21
+ if (!pattern.startsWith("/")) {
22
+ const slash = pattern.indexOf("/");
23
+ const patternHost = slash < 0 ? pattern : pattern.slice(0, slash);
24
+ if (patternHost !== host) return false;
25
+ patternPath = slash < 0 ? "/" : pattern.slice(slash);
26
+ }
27
+ if (patternPath === "/*") return true;
28
+ if (patternPath.endsWith("/*")) {
29
+ const prefix = patternPath.slice(0, -2);
30
+ return path === prefix || path.startsWith(prefix + "/");
31
+ }
32
+ return path === patternPath;
33
+ }
34
+ function matchesAny(patterns, host, path) {
35
+ if (!patterns?.length) return false;
36
+ const h = normalizeHost(host);
37
+ const p = normalizePath(path);
38
+ return patterns.some((pattern) => matches(pattern, h, p));
39
+ }
40
+
41
+ // src/edge/client.ts
42
+ function memoryDecisionCache(maxEntries = 1e4) {
43
+ const entries = /* @__PURE__ */ new Map();
44
+ return {
45
+ get(key) {
46
+ const hit = entries.get(key);
47
+ if (!hit) return void 0;
48
+ entries.delete(key);
49
+ if (hit.expires < Date.now()) return void 0;
50
+ entries.set(key, hit);
51
+ return hit.value;
52
+ },
53
+ set(key, value, ttlMs) {
54
+ entries.delete(key);
55
+ entries.set(key, { value, expires: Date.now() + ttlMs });
56
+ while (entries.size > maxEntries) {
57
+ entries.delete(entries.keys().next().value);
58
+ }
59
+ }
60
+ };
61
+ }
62
+ var stableJson = (facts) => JSON.stringify(
63
+ Object.keys(facts ?? {}).sort().map((k) => [k, facts[k]])
64
+ );
65
+ function normalizeManifest(m) {
66
+ if ("keys" in m && Array.isArray(m.keys)) return m;
67
+ const pages = m.pages ?? [];
68
+ return {
69
+ version: m.version,
70
+ generatedAt: "",
71
+ keys: pages.map((p) => ({ contentKey: "", mode: "server", pages: [p] }))
72
+ };
73
+ }
74
+ function provisional(profile) {
75
+ const answers = profile?.answers;
76
+ if (!answers || typeof answers !== "object") return false;
77
+ const steps = Object.values(answers);
78
+ return steps.some((a) => a?.source === "signals" && a.jev === true) || steps.length > 0 && steps.every((a) => !a?.choice);
79
+ }
80
+ function createXeniaEdge(config) {
81
+ const baseUrl = config.baseUrl.replace(/\/+$/, "");
82
+ const budgetMs = config.budgetMs ?? 150;
83
+ const manifestWaitMs = config.manifestWaitMs ?? 300;
84
+ const manifestTtlMs = config.manifestTtlMs ?? 3e4;
85
+ const decisionTtlMs = config.decisionTtlMs ?? 30 * 6e4;
86
+ const provisionalDecisionTtlMs = config.provisionalDecisionTtlMs ?? 15e3;
87
+ const cache = config.cache ?? memoryDecisionCache();
88
+ const doFetch = config.fetch ?? fetch;
89
+ const onError = config.onError ?? (() => {
90
+ });
91
+ const auth = { Authorization: `Bearer ${config.token}` };
92
+ let manifest = config.initialManifest?.version ? normalizeManifest(config.initialManifest) : null;
93
+ let manifestAt = 0;
94
+ let refreshing = null;
95
+ const inflight = /* @__PURE__ */ new Map();
96
+ function refreshManifest() {
97
+ refreshing ??= (async () => {
98
+ try {
99
+ const res = await doFetch(config.manifestUrl ?? `${baseUrl}/api/personalization/edge/manifest`, {
100
+ headers: config.manifestUrl ? {} : { ...auth, ...manifest ? { "If-None-Match": `"${manifest.version}"` } : {} },
101
+ signal: AbortSignal.timeout(5e3),
102
+ cache: "no-store"
103
+ });
104
+ if (res.status === 304) {
105
+ manifestAt = Date.now();
106
+ } else if (res.ok) {
107
+ manifest = normalizeManifest(await res.json());
108
+ manifestAt = Date.now();
109
+ } else {
110
+ onError(new Error(`manifest ${res.status}`), "manifest");
111
+ manifestAt = Date.now();
112
+ }
113
+ } catch (e) {
114
+ onError(e, "manifest");
115
+ manifestAt = Date.now();
116
+ } finally {
117
+ refreshing = null;
118
+ }
119
+ })();
120
+ return refreshing;
121
+ }
122
+ async function currentManifest() {
123
+ if (Date.now() - manifestAt > manifestTtlMs) {
124
+ const pending = refreshManifest();
125
+ if (!manifest) {
126
+ await Promise.race([pending, new Promise((r) => setTimeout(r, manifestWaitMs))]);
127
+ }
128
+ }
129
+ return manifest;
130
+ }
131
+ async function match(host, path) {
132
+ const m = await currentManifest();
133
+ if (!m) return null;
134
+ return m.keys.filter((k) => k.mode !== "off" && matchesAny(k.pages, host, path));
135
+ }
136
+ async function decide(keys, input) {
137
+ const version = manifest?.version ?? "";
138
+ const contentKeys = keys.map((k) => k.contentKey).filter(Boolean).sort();
139
+ const scope = contentKeys.length ? contentKeys.join(",") : keys.flatMap((k) => k.pages).sort().join(",");
140
+ const cacheKey = [version, input.visitorId, scope, stableJson(input.cacheFacts)].join("|");
141
+ const cached = cache.get(cacheKey);
142
+ if (cached) return { decisions: cached.decisions, profile: cached.profile, source: "cache", ms: 0 };
143
+ const pending = inflight.get(cacheKey);
144
+ if (pending) return pending;
145
+ const call = (async () => {
146
+ const started = Date.now();
147
+ try {
148
+ const res = await doFetch(`${baseUrl}/api/personalization/edge/decide`, {
149
+ method: "POST",
150
+ headers: { ...auth, "Content-Type": "application/json" },
151
+ body: JSON.stringify({
152
+ contentKeys,
153
+ host: input.host,
154
+ path: input.path,
155
+ visitorId: input.visitorId,
156
+ facts: input.facts,
157
+ ...input.trail?.length ? { trail: input.trail } : {}
158
+ }),
159
+ signal: AbortSignal.timeout(input.budgetMs ?? budgetMs),
160
+ cache: "no-store"
161
+ });
162
+ if (!res.ok) {
163
+ onError(new Error(`decide ${res.status}`), "decide");
164
+ return { decisions: [], source: "error", ms: Date.now() - started };
165
+ }
166
+ const result = await res.json();
167
+ if (!Array.isArray(result?.decisions)) throw new Error("decide: reply has no decisions list");
168
+ const decisions = result.decisions.filter((d) => d && Array.isArray(d.swaps));
169
+ const answer = { decisions, profile: result.profile };
170
+ cache.set(cacheKey, answer, input.ttlMs?.(answer) ?? (provisional(result.profile) ? provisionalDecisionTtlMs : decisionTtlMs));
171
+ if (manifest && result.version && result.version !== manifest.version) {
172
+ void refreshManifest();
173
+ }
174
+ return { decisions, profile: result.profile, source: "xenia", ms: Date.now() - started };
175
+ } catch (e) {
176
+ const timedOut = e instanceof DOMException && e.name === "TimeoutError";
177
+ if (!timedOut) onError(e, "decide");
178
+ return { decisions: [], source: timedOut ? "timeout" : "error", ms: Date.now() - started };
179
+ } finally {
180
+ inflight.delete(cacheKey);
181
+ }
182
+ })();
183
+ inflight.set(cacheKey, call);
184
+ return call;
185
+ }
186
+ return { match, decide, refreshManifest };
187
+ }
188
+
189
+ export { createXeniaEdge, matchesAny, memoryDecisionCache, normalizePath };
190
+ //# sourceMappingURL=index.js.map
191
+ //# sourceMappingURL=index.js.map