@xenia-martech/sdk-js 1.1.0 → 1.2.0-dev.1

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 (63) hide show
  1. package/README.md +50 -0
  2. package/assets/coverage.json +1 -0
  3. package/dist/{actions-CKeiofxJ.d.cts → actions-D9_aAvCv.d.cts} +188 -2
  4. package/dist/{actions-CKeiofxJ.d.ts → actions-D9_aAvCv.d.ts} +188 -2
  5. package/dist/api-BEPW0Uan.d.cts +88 -0
  6. package/dist/api-CktVQ6Gl.d.ts +88 -0
  7. package/dist/{chunk-U2WBBOYT.cjs → chunk-3PYPPIUM.cjs} +35 -10
  8. package/dist/chunk-3PYPPIUM.cjs.map +1 -0
  9. package/dist/{chunk-FYSEKFIY.js → chunk-7NGJC7CF.js} +249 -3
  10. package/dist/chunk-7NGJC7CF.js.map +1 -0
  11. package/dist/{chunk-WYAMPEVV.js → chunk-DWFVDNU5.js} +28 -3
  12. package/dist/chunk-DWFVDNU5.js.map +1 -0
  13. package/dist/{chunk-JEKVN7HK.cjs → chunk-IBNIRT7J.cjs} +251 -2
  14. package/dist/chunk-IBNIRT7J.cjs.map +1 -0
  15. package/dist/{chunk-TLTG3BTC.js → chunk-OUZU4Z5B.js} +2 -2
  16. package/dist/{chunk-TLTG3BTC.js.map → chunk-OUZU4Z5B.js.map} +1 -1
  17. package/dist/{chunk-5D7SJIO4.cjs → chunk-PKWANLT7.cjs} +2 -2
  18. package/dist/{chunk-5D7SJIO4.cjs.map → chunk-PKWANLT7.cjs.map} +1 -1
  19. package/dist/client/index.cjs +287 -9
  20. package/dist/client/index.cjs.map +1 -1
  21. package/dist/client/index.d.cts +2 -2
  22. package/dist/client/index.d.ts +2 -2
  23. package/dist/client/index.js +285 -10
  24. package/dist/client/index.js.map +1 -1
  25. package/dist/client/react/index.cjs +375 -13
  26. package/dist/client/react/index.cjs.map +1 -1
  27. package/dist/client/react/index.d.cts +53 -3
  28. package/dist/client/react/index.d.ts +53 -3
  29. package/dist/client/react/index.js +369 -14
  30. package/dist/client/react/index.js.map +1 -1
  31. package/dist/cms/contentful.cjs +138 -0
  32. package/dist/cms/contentful.cjs.map +1 -0
  33. package/dist/cms/contentful.d.cts +57 -0
  34. package/dist/cms/contentful.d.ts +57 -0
  35. package/dist/cms/contentful.js +134 -0
  36. package/dist/cms/contentful.js.map +1 -0
  37. package/dist/{cookie-names-QYMiEMAq.d.cts → cookie-names-BACctagz.d.cts} +1 -1
  38. package/dist/{cookie-names-8FxbGKGS.d.ts → cookie-names-CLqtmGBM.d.ts} +1 -1
  39. package/dist/index.cjs +35 -23
  40. package/dist/index.d.cts +4 -3
  41. package/dist/index.d.ts +4 -3
  42. package/dist/index.js +2 -2
  43. package/dist/next/index.cjs +41 -9
  44. package/dist/next/index.cjs.map +1 -1
  45. package/dist/next/index.d.cts +32 -3
  46. package/dist/next/index.d.ts +32 -3
  47. package/dist/next/index.js +14 -3
  48. package/dist/next/index.js.map +1 -1
  49. package/dist/{api-URhxxi4x.d.cts → personalize-content-Clc1oMYY.d.cts} +152 -69
  50. package/dist/{api-URhxxi4x.d.ts → personalize-content-Clc1oMYY.d.ts} +152 -69
  51. package/dist/server/index.cjs +29 -17
  52. package/dist/server/index.d.cts +5 -4
  53. package/dist/server/index.d.ts +5 -4
  54. package/dist/server/index.js +4 -4
  55. package/dist/xenia-sdk.iife.js +3 -3
  56. package/dist/xenia-sdk.iife.js.map +1 -1
  57. package/dist/{xenia-server-Cti94W49.d.cts → xenia-server-BWhWD0La.d.ts} +15 -1
  58. package/dist/{xenia-server-DDtQp6Wi.d.ts → xenia-server-o_wDOcei.d.cts} +15 -1
  59. package/package.json +15 -1
  60. package/dist/chunk-FYSEKFIY.js.map +0 -1
  61. package/dist/chunk-JEKVN7HK.cjs.map +0 -1
  62. package/dist/chunk-U2WBBOYT.cjs.map +0 -1
  63. package/dist/chunk-WYAMPEVV.js.map +0 -1
@@ -542,6 +542,25 @@ var XeniaApi = class {
542
542
  });
543
543
  return data;
544
544
  }
545
+ /**
546
+ * Resolve personalized CMS entries by id for one visitor (≤200) —
547
+ * `POST /api/personalization/resolve-entries`. The page resolver
548
+ * ({@link personalizeContent}) calls this once per round with the runtime
549
+ * targets it found; callers rarely need it directly.
550
+ */
551
+ async resolveEntries(entryIds, opts = {}) {
552
+ const { data } = await this.http.request({
553
+ method: "POST",
554
+ path: "/api/personalization/resolve-entries",
555
+ headers: await this.authHeaders(),
556
+ body: compact({
557
+ entryIds,
558
+ data: opts.data,
559
+ locale: opts.locale
560
+ })
561
+ });
562
+ return data;
563
+ }
545
564
  };
546
565
 
547
566
  // src/core/cookie-names.ts
@@ -656,6 +675,233 @@ function toEventId(raw) {
656
675
  return digest;
657
676
  }
658
677
 
678
+ // src/core/personalize-content.ts
679
+ var DEFAULT_TIMEOUT_MS = 300;
680
+ var DEFAULT_MAX_ROUNDS = 3;
681
+ var copyOf = (v) => v && typeof v === "object" && typeof structuredClone === "function" ? structuredClone(v) : v;
682
+ async function personalizeContent(payload, options) {
683
+ const { cms } = options;
684
+ const maxRounds = options.maxRounds ?? DEFAULT_MAX_ROUNDS;
685
+ const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
686
+ const applied = [];
687
+ const report = (swap) => {
688
+ applied.push(swap);
689
+ try {
690
+ options.onApplied?.(swap);
691
+ } catch {
692
+ }
693
+ };
694
+ let content = payload;
695
+ try {
696
+ if (!options.inPlace && typeof structuredClone === "function") content = structuredClone(content);
697
+ content = cms.prepare ? cms.prepare(content) : content;
698
+ } catch (error) {
699
+ return { content: payload, applied, complete: false, error };
700
+ }
701
+ const box = { root: content };
702
+ const settled = /* @__PURE__ */ new WeakSet();
703
+ const decided = /* @__PURE__ */ new Map();
704
+ let frontier = [{ parent: box, key: "root", value: box.root }];
705
+ const settle = (target, d, round) => {
706
+ settled.add(target.entry);
707
+ cms.clearMarker?.(target.entry);
708
+ const items = d.items.map(copyOf);
709
+ if (!d.decision || items.length === 0) {
710
+ return deferredSlots(target, cms);
711
+ }
712
+ for (const item of items) if (item && typeof item === "object") settled.add(item);
713
+ const op = d.decision.op ?? target.marker.op ?? "replace";
714
+ const next = apply(target, op, d.decision.field ?? target.marker.field, items, cms);
715
+ report({
716
+ target: target.id,
717
+ kind: "runtime",
718
+ op,
719
+ entryIds: items.map((e) => cms.entryId(e)).filter((id) => !!id),
720
+ contentKey: d.decision.contentKey,
721
+ bucket: d.decision.bucket,
722
+ round
723
+ });
724
+ return next;
725
+ };
726
+ for (let round = 0; ; round++) {
727
+ const pending = walk(frontier, cms, settled, round, report, (t) => {
728
+ const d = decided.get(t.id);
729
+ return d ? settle(t, d, round) : void 0;
730
+ });
731
+ if (pending.size === 0) return { content: box.root, applied, complete: true };
732
+ if (!options.resolve || round >= maxRounds) {
733
+ return { content: box.root, applied, complete: !options.resolve };
734
+ }
735
+ progress(options, { content: box.root, round, pending: [...pending.keys()] });
736
+ let decisions;
737
+ let entriesById;
738
+ let queried;
739
+ try {
740
+ decisions = await withDeadline(options.resolve([...pending.keys()]), deadline);
741
+ ({ entriesById, queried } = await withDeadline(loadReplacements(decisions, cms, options.locale), deadline));
742
+ } catch (error) {
743
+ return { content: box.root, applied, complete: false, error: error === TIMEOUT ? void 0 : error };
744
+ }
745
+ const byTarget = new Map(decisions.map((d) => [d.target, d]));
746
+ frontier = [];
747
+ for (const target of pending.values()) {
748
+ const decision = byTarget.get(target.id);
749
+ const items = decision ? decision.ids ? decision.ids.map((id) => entriesById.get(id)).filter((e) => e !== void 0) : queried.get(decision) ?? [] : [];
750
+ const d = { decision, items };
751
+ decided.set(target.id, d);
752
+ frontier.push(...settle(target, d, round));
753
+ }
754
+ progress(options, { content: box.root, round, pending: [] });
755
+ }
756
+ }
757
+ function progress(options, p) {
758
+ try {
759
+ options.onProgress?.(p);
760
+ } catch {
761
+ }
762
+ }
763
+ function walk(frontier, cms, settled, round, report, reuse) {
764
+ const pending = /* @__PURE__ */ new Map();
765
+ const seen = /* @__PURE__ */ new WeakSet();
766
+ const deferred = /* @__PURE__ */ new WeakSet();
767
+ const queue = [...frontier];
768
+ while (queue.length > 0) {
769
+ const slot = queue.shift();
770
+ const value = slot.value;
771
+ if (!value || typeof value !== "object" || deferred.has(value)) continue;
772
+ const id = settled.has(value) ? void 0 : cms.entryId(value);
773
+ const marker = id ? cms.marker(value) : null;
774
+ if (id && marker?.kind === "runtime") {
775
+ const again = reuse({ id, marker, entry: value, slots: [slot] });
776
+ if (again) {
777
+ queue.push(...again);
778
+ continue;
779
+ }
780
+ const existing = pending.get(id);
781
+ if (existing) {
782
+ existing.slots.push(slot);
783
+ } else {
784
+ pending.set(id, { id, marker, entry: value, slots: [slot] });
785
+ if (marker.op === "items" && marker.field) {
786
+ const held = cms.getField(value, marker.field);
787
+ if (held && typeof held === "object") deferred.add(held);
788
+ queue.push(...children(value));
789
+ }
790
+ }
791
+ continue;
792
+ }
793
+ if (id && marker?.kind === "static") {
794
+ const variants = cms.staticVariants(value).filter((v) => v && typeof v === "object");
795
+ settled.add(value);
796
+ cms.clearMarker?.(value);
797
+ if (variants.length > 0) {
798
+ for (const v of variants) settled.add(v);
799
+ const op = marker.op ?? "replace";
800
+ const target = { entry: value, slots: [slot] };
801
+ queue.push(...apply(target, op, marker.field, variants, cms));
802
+ report({
803
+ target: id,
804
+ kind: "static",
805
+ op,
806
+ entryIds: variants.map((v) => cms.entryId(v)).filter((x) => !!x),
807
+ round
808
+ });
809
+ continue;
810
+ }
811
+ }
812
+ if (seen.has(value)) continue;
813
+ seen.add(value);
814
+ queue.push(...children(value));
815
+ }
816
+ return pending;
817
+ }
818
+ function apply(target, op, field, items, cms) {
819
+ if (op === "items") {
820
+ if (field) {
821
+ cms.setField(target.entry, field, items);
822
+ return [{ parent: target.entry, key: field, value: cms.getField(target.entry, field) }];
823
+ }
824
+ if (cms.mergeItems) {
825
+ cms.mergeItems(target.entry, items[0]);
826
+ return target.slots.map((s) => ({ ...s, value: target.entry }));
827
+ }
828
+ op = "replace";
829
+ }
830
+ const next = [];
831
+ for (const slot of target.slots) {
832
+ const placed = op === "expand" && Array.isArray(slot.parent) ? items : [items[0]];
833
+ if (Array.isArray(slot.parent)) {
834
+ const index = slot.parent.indexOf(target.entry);
835
+ if (index < 0) continue;
836
+ slot.parent.splice(index, 1, ...placed);
837
+ } else {
838
+ slot.parent[slot.key] = placed[0];
839
+ }
840
+ for (const value of placed) next.push({ parent: slot.parent, key: slot.key, value });
841
+ }
842
+ return next;
843
+ }
844
+ function deferredSlots(target, cms) {
845
+ if (target.marker.op === "items" && target.marker.field) {
846
+ return [{ parent: target.entry, key: target.marker.field, value: cms.getField(target.entry, target.marker.field) }];
847
+ }
848
+ return target.slots.map((s) => ({ ...s, value: target.entry }));
849
+ }
850
+ function children(value) {
851
+ if (Array.isArray(value)) return value.map((v, i) => ({ parent: value, key: i, value: v }));
852
+ return Object.keys(value).map((k) => ({ parent: value, key: k, value: value[k] }));
853
+ }
854
+ async function loadReplacements(decisions, cms, locale) {
855
+ const ids = [...new Set(decisions.flatMap((d) => d.ids ?? []))];
856
+ const queries = decisions.filter((d) => !d.ids && d.query);
857
+ const [fetched, ...results] = await Promise.all([
858
+ ids.length > 0 ? cms.fetchEntries(ids, locale) : Promise.resolve([]),
859
+ ...queries.map((d) => cms.queryEntries(d.query, locale).catch(() => []))
860
+ ]);
861
+ const entriesById = /* @__PURE__ */ new Map();
862
+ for (const entry of fetched) {
863
+ const id = cms.entryId(entry);
864
+ if (id) entriesById.set(id, entry);
865
+ }
866
+ const queried = /* @__PURE__ */ new Map();
867
+ queries.forEach((d, i) => queried.set(d, results[i]));
868
+ return { entriesById, queried };
869
+ }
870
+ var TIMEOUT = /* @__PURE__ */ Symbol("timeout");
871
+ function withDeadline(promise, deadline) {
872
+ const remaining = deadline - Date.now();
873
+ if (remaining <= 0) return Promise.reject(TIMEOUT);
874
+ let timer;
875
+ return Promise.race([
876
+ promise,
877
+ new Promise((_, reject) => {
878
+ timer = setTimeout(() => reject(TIMEOUT), remaining);
879
+ })
880
+ ]).finally(() => clearTimeout(timer));
881
+ }
882
+ function pendingTargets(payload, cms) {
883
+ try {
884
+ let content = typeof structuredClone === "function" ? structuredClone(payload) : payload;
885
+ content = cms.prepare ? cms.prepare(content) : content;
886
+ const root = { parent: { root: content }, key: "root", value: content };
887
+ return [...walk([root], cms, /* @__PURE__ */ new WeakSet(), 0, () => {
888
+ }, () => void 0).keys()];
889
+ } catch {
890
+ return [];
891
+ }
892
+ }
893
+ async function personalizePage(ref, options) {
894
+ let page;
895
+ try {
896
+ if (!options.cms.fetchPage) throw new Error("This CMS adapter cannot fetch pages; use personalizeContent(page)");
897
+ page = await options.cms.fetchPage(ref, options.locale);
898
+ } catch (error) {
899
+ return { content: null, applied: [], complete: false, error };
900
+ }
901
+ if (page == null) return { content: null, applied: [], complete: true };
902
+ return personalizeContent(page, options);
903
+ }
904
+
659
905
  // src/core/response-store.ts
660
906
  var MemoryResponseStore = class {
661
907
  map = /* @__PURE__ */ new Map();
@@ -1907,7 +2153,10 @@ var XeniaClient = class {
1907
2153
  renderer;
1908
2154
  analyticsQueue;
1909
2155
  tracker = null;
1910
- ready = false;
2156
+ /** The in-flight (or finished) boot; shared so concurrent `init()` calls start one tracker. */
2157
+ booting = null;
2158
+ /** Bumped by `destroy()`, so a boot still waiting on the device id knows to stop. */
2159
+ generation = 0;
1911
2160
  consentState;
1912
2161
  listeners = /* @__PURE__ */ new Map();
1913
2162
  constructor(options) {
@@ -1937,10 +2186,14 @@ var XeniaClient = class {
1937
2186
  });
1938
2187
  }
1939
2188
  /** Boot the client: resolve device id and start auto-tracking. Idempotent. */
1940
- async init() {
1941
- if (this.ready || !isBrowser()) return;
2189
+ init() {
2190
+ if (!isBrowser()) return Promise.resolve();
2191
+ return this.booting ??= this.boot();
2192
+ }
2193
+ async boot() {
2194
+ const generation = this.generation;
1942
2195
  await this.identity.resolveDeviceId();
1943
- this.ready = true;
2196
+ if (generation !== this.generation) return;
1944
2197
  if (this.analyticsAllowed()) this.analyticsQueue.start();
1945
2198
  if (this.options.autoTrack !== false) {
1946
2199
  this.tracker = new AutoTracker({
@@ -1976,7 +2229,8 @@ var XeniaClient = class {
1976
2229
  this.tracker = null;
1977
2230
  this.renderer.destroy();
1978
2231
  this.analyticsQueue.stop();
1979
- this.ready = false;
2232
+ this.booting = null;
2233
+ this.generation++;
1980
2234
  }
1981
2235
  // ── Identity ───────────────────────────────────────────────────────────────
1982
2236
  /**
@@ -2105,6 +2359,27 @@ var XeniaClient = class {
2105
2359
  async resolveBatch(items, opts) {
2106
2360
  return this.api.resolveBatch(items, this.withVisitor(opts));
2107
2361
  }
2362
+ /**
2363
+ * Personalize a fetched CMS page: static swaps are applied from the payload, runtime targets are
2364
+ * decided by Xenia in one `resolve-entries` call per nesting level, and replacements are loaded
2365
+ * through `opts.cms` (e.g. `contentfulAdapter(...)`). Never throws — anything undecided within
2366
+ * `opts.timeoutMs` keeps its base content.
2367
+ */
2368
+ async personalizeContent(page, opts) {
2369
+ return personalizeContent(page, { ...opts, resolve: this.entryResolver(opts) });
2370
+ }
2371
+ /**
2372
+ * Fetch a page through `opts.cms` and personalize it — the app passes only the page's identity
2373
+ * (entry id, or `{ contentType, fields: { slug } }`). Never throws: a failed fetch is `content:
2374
+ * null` with `error`, a missing page `content: null`.
2375
+ */
2376
+ async personalizePage(ref, opts) {
2377
+ return personalizePage(ref, { ...opts, resolve: this.entryResolver(opts) });
2378
+ }
2379
+ entryResolver(opts) {
2380
+ const resolveOpts = this.withVisitor({ data: opts.data, locale: opts.locale });
2381
+ return async (ids) => (await this.api.resolveEntries(ids, resolveOpts)).decisions ?? [];
2382
+ }
2108
2383
  /** The SDK-owned visitor anchor: the `identify()`/`config.visitorId` id, else the device id. */
2109
2384
  resolveVisitorId() {
2110
2385
  return this.identity.getVisitorId() ?? this.identity.getDeviceId() ?? void 0;
@@ -2183,8 +2458,8 @@ var XeniaClient = class {
2183
2458
  const response = await this.api.webhook(triggerId, body, opts?.method ?? "POST");
2184
2459
  this.responses.set(triggerId, response);
2185
2460
  this.emit("response", { triggerId, response });
2186
- const apply = opts?.apply ?? this.options.autoApplyActions !== false;
2187
- if (apply && response.actions.length) {
2461
+ const apply2 = opts?.apply ?? this.options.autoApplyActions !== false;
2462
+ if (apply2 && response.actions.length) {
2188
2463
  this.applyActions(response.actions);
2189
2464
  }
2190
2465
  return response;
@@ -2238,8 +2513,8 @@ var XeniaClient = class {
2238
2513
  compact({ ...ambient, fields })
2239
2514
  );
2240
2515
  this.emit("response", { triggerId: ref, response });
2241
- const apply = opts?.apply ?? this.options.autoApplyActions !== false;
2242
- if (apply && response.actions.length) this.applyActions(response.actions);
2516
+ const apply2 = opts?.apply ?? this.options.autoApplyActions !== false;
2517
+ if (apply2 && response.actions.length) this.applyActions(response.actions);
2243
2518
  return response;
2244
2519
  }
2245
2520
  // ── Imperative render helpers ─────────────────────────────────────────────────
@@ -2373,6 +2648,9 @@ exports.extractContentSwaps = extractContentSwaps;
2373
2648
  exports.hideContent = hideContent;
2374
2649
  exports.parseActions = parseActions;
2375
2650
  exports.parseDataAttribute = parseDataAttribute;
2651
+ exports.pendingTargets = pendingTargets;
2652
+ exports.personalizeContent = personalizeContent;
2653
+ exports.personalizePage = personalizePage;
2376
2654
  exports.readSlotMeta = readSlotMeta;
2377
2655
  exports.resolveTarget = resolveTarget;
2378
2656
  exports.showContent = showContent;