@xenia-martech/sdk-js 1.1.0 → 1.2.0-dev.2
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/README.md +50 -0
- package/assets/coverage.json +1 -0
- package/dist/{actions-CKeiofxJ.d.cts → actions-BcdlokGO.d.cts} +192 -4
- package/dist/{actions-CKeiofxJ.d.ts → actions-BcdlokGO.d.ts} +192 -4
- package/dist/api-BEPW0Uan.d.cts +88 -0
- package/dist/api-CktVQ6Gl.d.ts +88 -0
- package/dist/{chunk-U2WBBOYT.cjs → chunk-3PYPPIUM.cjs} +35 -10
- package/dist/chunk-3PYPPIUM.cjs.map +1 -0
- package/dist/{chunk-FYSEKFIY.js → chunk-7NGJC7CF.js} +249 -3
- package/dist/chunk-7NGJC7CF.js.map +1 -0
- package/dist/{chunk-WYAMPEVV.js → chunk-DWFVDNU5.js} +28 -3
- package/dist/chunk-DWFVDNU5.js.map +1 -0
- package/dist/{chunk-JEKVN7HK.cjs → chunk-IBNIRT7J.cjs} +251 -2
- package/dist/chunk-IBNIRT7J.cjs.map +1 -0
- package/dist/{chunk-TLTG3BTC.js → chunk-OUZU4Z5B.js} +2 -2
- package/dist/{chunk-TLTG3BTC.js.map → chunk-OUZU4Z5B.js.map} +1 -1
- package/dist/{chunk-5D7SJIO4.cjs → chunk-PKWANLT7.cjs} +2 -2
- package/dist/{chunk-5D7SJIO4.cjs.map → chunk-PKWANLT7.cjs.map} +1 -1
- package/dist/client/index.cjs +314 -12
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +2 -2
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +312 -13
- package/dist/client/index.js.map +1 -1
- package/dist/client/react/index.cjs +402 -16
- package/dist/client/react/index.cjs.map +1 -1
- package/dist/client/react/index.d.cts +53 -3
- package/dist/client/react/index.d.ts +53 -3
- package/dist/client/react/index.js +396 -17
- package/dist/client/react/index.js.map +1 -1
- package/dist/cms/contentful.cjs +138 -0
- package/dist/cms/contentful.cjs.map +1 -0
- package/dist/cms/contentful.d.cts +57 -0
- package/dist/cms/contentful.d.ts +57 -0
- package/dist/cms/contentful.js +134 -0
- package/dist/cms/contentful.js.map +1 -0
- package/dist/{cookie-names-QYMiEMAq.d.cts → cookie-names-BACctagz.d.cts} +1 -1
- package/dist/{cookie-names-8FxbGKGS.d.ts → cookie-names-CLqtmGBM.d.ts} +1 -1
- package/dist/index.cjs +35 -23
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -2
- package/dist/next/index.cjs +41 -9
- package/dist/next/index.cjs.map +1 -1
- package/dist/next/index.d.cts +32 -3
- package/dist/next/index.d.ts +32 -3
- package/dist/next/index.js +14 -3
- package/dist/next/index.js.map +1 -1
- package/dist/{api-URhxxi4x.d.cts → personalize-content-Clc1oMYY.d.cts} +152 -69
- package/dist/{api-URhxxi4x.d.ts → personalize-content-Clc1oMYY.d.ts} +152 -69
- package/dist/server/index.cjs +29 -17
- package/dist/server/index.d.cts +5 -4
- package/dist/server/index.d.ts +5 -4
- package/dist/server/index.js +4 -4
- package/dist/xenia-sdk.iife.js +3 -3
- package/dist/xenia-sdk.iife.js.map +1 -1
- package/dist/{xenia-server-Cti94W49.d.cts → xenia-server-BWhWD0La.d.ts} +15 -1
- package/dist/{xenia-server-DDtQp6Wi.d.ts → xenia-server-o_wDOcei.d.cts} +15 -1
- package/package.json +15 -1
- package/dist/chunk-FYSEKFIY.js.map +0 -1
- package/dist/chunk-JEKVN7HK.cjs.map +0 -1
- package/dist/chunk-U2WBBOYT.cjs.map +0 -1
- package/dist/chunk-WYAMPEVV.js.map +0 -1
|
@@ -549,6 +549,25 @@ var XeniaApi = class {
|
|
|
549
549
|
});
|
|
550
550
|
return data;
|
|
551
551
|
}
|
|
552
|
+
/**
|
|
553
|
+
* Resolve personalized CMS entries by id for one visitor (≤200) —
|
|
554
|
+
* `POST /api/personalization/resolve-entries`. The page resolver
|
|
555
|
+
* ({@link personalizeContent}) calls this once per round with the runtime
|
|
556
|
+
* targets it found; callers rarely need it directly.
|
|
557
|
+
*/
|
|
558
|
+
async resolveEntries(entryIds, opts = {}) {
|
|
559
|
+
const { data } = await this.http.request({
|
|
560
|
+
method: "POST",
|
|
561
|
+
path: "/api/personalization/resolve-entries",
|
|
562
|
+
headers: await this.authHeaders(),
|
|
563
|
+
body: compact({
|
|
564
|
+
entryIds,
|
|
565
|
+
data: opts.data,
|
|
566
|
+
locale: opts.locale
|
|
567
|
+
})
|
|
568
|
+
});
|
|
569
|
+
return data;
|
|
570
|
+
}
|
|
552
571
|
};
|
|
553
572
|
|
|
554
573
|
// src/core/cookie-names.ts
|
|
@@ -663,6 +682,233 @@ function toEventId(raw) {
|
|
|
663
682
|
return digest;
|
|
664
683
|
}
|
|
665
684
|
|
|
685
|
+
// src/core/personalize-content.ts
|
|
686
|
+
var DEFAULT_TIMEOUT_MS = 300;
|
|
687
|
+
var DEFAULT_MAX_ROUNDS = 3;
|
|
688
|
+
var copyOf = (v) => v && typeof v === "object" && typeof structuredClone === "function" ? structuredClone(v) : v;
|
|
689
|
+
async function personalizeContent(payload, options) {
|
|
690
|
+
const { cms } = options;
|
|
691
|
+
const maxRounds = options.maxRounds ?? DEFAULT_MAX_ROUNDS;
|
|
692
|
+
const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
693
|
+
const applied = [];
|
|
694
|
+
const report = (swap) => {
|
|
695
|
+
applied.push(swap);
|
|
696
|
+
try {
|
|
697
|
+
options.onApplied?.(swap);
|
|
698
|
+
} catch {
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
let content = payload;
|
|
702
|
+
try {
|
|
703
|
+
if (!options.inPlace && typeof structuredClone === "function") content = structuredClone(content);
|
|
704
|
+
content = cms.prepare ? cms.prepare(content) : content;
|
|
705
|
+
} catch (error) {
|
|
706
|
+
return { content: payload, applied, complete: false, error };
|
|
707
|
+
}
|
|
708
|
+
const box = { root: content };
|
|
709
|
+
const settled = /* @__PURE__ */ new WeakSet();
|
|
710
|
+
const decided = /* @__PURE__ */ new Map();
|
|
711
|
+
let frontier = [{ parent: box, key: "root", value: box.root }];
|
|
712
|
+
const settle = (target, d, round) => {
|
|
713
|
+
settled.add(target.entry);
|
|
714
|
+
cms.clearMarker?.(target.entry);
|
|
715
|
+
const items = d.items.map(copyOf);
|
|
716
|
+
if (!d.decision || items.length === 0) {
|
|
717
|
+
return deferredSlots(target, cms);
|
|
718
|
+
}
|
|
719
|
+
for (const item of items) if (item && typeof item === "object") settled.add(item);
|
|
720
|
+
const op = d.decision.op ?? target.marker.op ?? "replace";
|
|
721
|
+
const next = apply(target, op, d.decision.field ?? target.marker.field, items, cms);
|
|
722
|
+
report({
|
|
723
|
+
target: target.id,
|
|
724
|
+
kind: "runtime",
|
|
725
|
+
op,
|
|
726
|
+
entryIds: items.map((e) => cms.entryId(e)).filter((id) => !!id),
|
|
727
|
+
contentKey: d.decision.contentKey,
|
|
728
|
+
bucket: d.decision.bucket,
|
|
729
|
+
round
|
|
730
|
+
});
|
|
731
|
+
return next;
|
|
732
|
+
};
|
|
733
|
+
for (let round = 0; ; round++) {
|
|
734
|
+
const pending = walk(frontier, cms, settled, round, report, (t) => {
|
|
735
|
+
const d = decided.get(t.id);
|
|
736
|
+
return d ? settle(t, d, round) : void 0;
|
|
737
|
+
});
|
|
738
|
+
if (pending.size === 0) return { content: box.root, applied, complete: true };
|
|
739
|
+
if (!options.resolve || round >= maxRounds) {
|
|
740
|
+
return { content: box.root, applied, complete: !options.resolve };
|
|
741
|
+
}
|
|
742
|
+
progress(options, { content: box.root, round, pending: [...pending.keys()] });
|
|
743
|
+
let decisions;
|
|
744
|
+
let entriesById;
|
|
745
|
+
let queried;
|
|
746
|
+
try {
|
|
747
|
+
decisions = await withDeadline(options.resolve([...pending.keys()]), deadline);
|
|
748
|
+
({ entriesById, queried } = await withDeadline(loadReplacements(decisions, cms, options.locale), deadline));
|
|
749
|
+
} catch (error) {
|
|
750
|
+
return { content: box.root, applied, complete: false, error: error === TIMEOUT ? void 0 : error };
|
|
751
|
+
}
|
|
752
|
+
const byTarget = new Map(decisions.map((d) => [d.target, d]));
|
|
753
|
+
frontier = [];
|
|
754
|
+
for (const target of pending.values()) {
|
|
755
|
+
const decision = byTarget.get(target.id);
|
|
756
|
+
const items = decision ? decision.ids ? decision.ids.map((id) => entriesById.get(id)).filter((e) => e !== void 0) : queried.get(decision) ?? [] : [];
|
|
757
|
+
const d = { decision, items };
|
|
758
|
+
decided.set(target.id, d);
|
|
759
|
+
frontier.push(...settle(target, d, round));
|
|
760
|
+
}
|
|
761
|
+
progress(options, { content: box.root, round, pending: [] });
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
function progress(options, p) {
|
|
765
|
+
try {
|
|
766
|
+
options.onProgress?.(p);
|
|
767
|
+
} catch {
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
function walk(frontier, cms, settled, round, report, reuse) {
|
|
771
|
+
const pending = /* @__PURE__ */ new Map();
|
|
772
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
773
|
+
const deferred = /* @__PURE__ */ new WeakSet();
|
|
774
|
+
const queue = [...frontier];
|
|
775
|
+
while (queue.length > 0) {
|
|
776
|
+
const slot = queue.shift();
|
|
777
|
+
const value = slot.value;
|
|
778
|
+
if (!value || typeof value !== "object" || deferred.has(value)) continue;
|
|
779
|
+
const id = settled.has(value) ? void 0 : cms.entryId(value);
|
|
780
|
+
const marker = id ? cms.marker(value) : null;
|
|
781
|
+
if (id && marker?.kind === "runtime") {
|
|
782
|
+
const again = reuse({ id, marker, entry: value, slots: [slot] });
|
|
783
|
+
if (again) {
|
|
784
|
+
queue.push(...again);
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
const existing = pending.get(id);
|
|
788
|
+
if (existing) {
|
|
789
|
+
existing.slots.push(slot);
|
|
790
|
+
} else {
|
|
791
|
+
pending.set(id, { id, marker, entry: value, slots: [slot] });
|
|
792
|
+
if (marker.op === "items" && marker.field) {
|
|
793
|
+
const held = cms.getField(value, marker.field);
|
|
794
|
+
if (held && typeof held === "object") deferred.add(held);
|
|
795
|
+
queue.push(...children(value));
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
if (id && marker?.kind === "static") {
|
|
801
|
+
const variants = cms.staticVariants(value).filter((v) => v && typeof v === "object");
|
|
802
|
+
settled.add(value);
|
|
803
|
+
cms.clearMarker?.(value);
|
|
804
|
+
if (variants.length > 0) {
|
|
805
|
+
for (const v of variants) settled.add(v);
|
|
806
|
+
const op = marker.op ?? "replace";
|
|
807
|
+
const target = { entry: value, slots: [slot] };
|
|
808
|
+
queue.push(...apply(target, op, marker.field, variants, cms));
|
|
809
|
+
report({
|
|
810
|
+
target: id,
|
|
811
|
+
kind: "static",
|
|
812
|
+
op,
|
|
813
|
+
entryIds: variants.map((v) => cms.entryId(v)).filter((x) => !!x),
|
|
814
|
+
round
|
|
815
|
+
});
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
if (seen.has(value)) continue;
|
|
820
|
+
seen.add(value);
|
|
821
|
+
queue.push(...children(value));
|
|
822
|
+
}
|
|
823
|
+
return pending;
|
|
824
|
+
}
|
|
825
|
+
function apply(target, op, field, items, cms) {
|
|
826
|
+
if (op === "items") {
|
|
827
|
+
if (field) {
|
|
828
|
+
cms.setField(target.entry, field, items);
|
|
829
|
+
return [{ parent: target.entry, key: field, value: cms.getField(target.entry, field) }];
|
|
830
|
+
}
|
|
831
|
+
if (cms.mergeItems) {
|
|
832
|
+
cms.mergeItems(target.entry, items[0]);
|
|
833
|
+
return target.slots.map((s) => ({ ...s, value: target.entry }));
|
|
834
|
+
}
|
|
835
|
+
op = "replace";
|
|
836
|
+
}
|
|
837
|
+
const next = [];
|
|
838
|
+
for (const slot of target.slots) {
|
|
839
|
+
const placed = op === "expand" && Array.isArray(slot.parent) ? items : [items[0]];
|
|
840
|
+
if (Array.isArray(slot.parent)) {
|
|
841
|
+
const index = slot.parent.indexOf(target.entry);
|
|
842
|
+
if (index < 0) continue;
|
|
843
|
+
slot.parent.splice(index, 1, ...placed);
|
|
844
|
+
} else {
|
|
845
|
+
slot.parent[slot.key] = placed[0];
|
|
846
|
+
}
|
|
847
|
+
for (const value of placed) next.push({ parent: slot.parent, key: slot.key, value });
|
|
848
|
+
}
|
|
849
|
+
return next;
|
|
850
|
+
}
|
|
851
|
+
function deferredSlots(target, cms) {
|
|
852
|
+
if (target.marker.op === "items" && target.marker.field) {
|
|
853
|
+
return [{ parent: target.entry, key: target.marker.field, value: cms.getField(target.entry, target.marker.field) }];
|
|
854
|
+
}
|
|
855
|
+
return target.slots.map((s) => ({ ...s, value: target.entry }));
|
|
856
|
+
}
|
|
857
|
+
function children(value) {
|
|
858
|
+
if (Array.isArray(value)) return value.map((v, i) => ({ parent: value, key: i, value: v }));
|
|
859
|
+
return Object.keys(value).map((k) => ({ parent: value, key: k, value: value[k] }));
|
|
860
|
+
}
|
|
861
|
+
async function loadReplacements(decisions, cms, locale) {
|
|
862
|
+
const ids = [...new Set(decisions.flatMap((d) => d.ids ?? []))];
|
|
863
|
+
const queries = decisions.filter((d) => !d.ids && d.query);
|
|
864
|
+
const [fetched, ...results] = await Promise.all([
|
|
865
|
+
ids.length > 0 ? cms.fetchEntries(ids, locale) : Promise.resolve([]),
|
|
866
|
+
...queries.map((d) => cms.queryEntries(d.query, locale).catch(() => []))
|
|
867
|
+
]);
|
|
868
|
+
const entriesById = /* @__PURE__ */ new Map();
|
|
869
|
+
for (const entry of fetched) {
|
|
870
|
+
const id = cms.entryId(entry);
|
|
871
|
+
if (id) entriesById.set(id, entry);
|
|
872
|
+
}
|
|
873
|
+
const queried = /* @__PURE__ */ new Map();
|
|
874
|
+
queries.forEach((d, i) => queried.set(d, results[i]));
|
|
875
|
+
return { entriesById, queried };
|
|
876
|
+
}
|
|
877
|
+
var TIMEOUT = /* @__PURE__ */ Symbol("timeout");
|
|
878
|
+
function withDeadline(promise, deadline) {
|
|
879
|
+
const remaining = deadline - Date.now();
|
|
880
|
+
if (remaining <= 0) return Promise.reject(TIMEOUT);
|
|
881
|
+
let timer;
|
|
882
|
+
return Promise.race([
|
|
883
|
+
promise,
|
|
884
|
+
new Promise((_, reject) => {
|
|
885
|
+
timer = setTimeout(() => reject(TIMEOUT), remaining);
|
|
886
|
+
})
|
|
887
|
+
]).finally(() => clearTimeout(timer));
|
|
888
|
+
}
|
|
889
|
+
function pendingTargets(payload, cms) {
|
|
890
|
+
try {
|
|
891
|
+
let content = typeof structuredClone === "function" ? structuredClone(payload) : payload;
|
|
892
|
+
content = cms.prepare ? cms.prepare(content) : content;
|
|
893
|
+
const root = { parent: { root: content }, key: "root", value: content };
|
|
894
|
+
return [...walk([root], cms, /* @__PURE__ */ new WeakSet(), 0, () => {
|
|
895
|
+
}, () => void 0).keys()];
|
|
896
|
+
} catch {
|
|
897
|
+
return [];
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
async function personalizePage(ref, options) {
|
|
901
|
+
let page;
|
|
902
|
+
try {
|
|
903
|
+
if (!options.cms.fetchPage) throw new Error("This CMS adapter cannot fetch pages; use personalizeContent(page)");
|
|
904
|
+
page = await options.cms.fetchPage(ref, options.locale);
|
|
905
|
+
} catch (error) {
|
|
906
|
+
return { content: null, applied: [], complete: false, error };
|
|
907
|
+
}
|
|
908
|
+
if (page == null) return { content: null, applied: [], complete: true };
|
|
909
|
+
return personalizeContent(page, options);
|
|
910
|
+
}
|
|
911
|
+
|
|
666
912
|
// src/core/response-store.ts
|
|
667
913
|
var MemoryResponseStore = class {
|
|
668
914
|
map = /* @__PURE__ */ new Map();
|
|
@@ -992,6 +1238,20 @@ var AutoTracker = class {
|
|
|
992
1238
|
}
|
|
993
1239
|
};
|
|
994
1240
|
|
|
1241
|
+
// src/server/cookies.ts
|
|
1242
|
+
function parseCookies(header) {
|
|
1243
|
+
const out = {};
|
|
1244
|
+
if (!header) return out;
|
|
1245
|
+
for (const part of header.split(";")) {
|
|
1246
|
+
const idx = part.indexOf("=");
|
|
1247
|
+
if (idx === -1) continue;
|
|
1248
|
+
const key = part.slice(0, idx).trim();
|
|
1249
|
+
const value = part.slice(idx + 1).trim();
|
|
1250
|
+
if (key) out[key] = decodeURIComponent(value);
|
|
1251
|
+
}
|
|
1252
|
+
return out;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
995
1255
|
// src/client/identity.ts
|
|
996
1256
|
var SESSION_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
997
1257
|
var Identity = class {
|
|
@@ -1039,10 +1299,19 @@ var Identity = class {
|
|
|
1039
1299
|
} catch {
|
|
1040
1300
|
}
|
|
1041
1301
|
}
|
|
1302
|
+
/** The visitor id already in the cookie — e.g. minted by `xeniaProxy()` on the first request. */
|
|
1303
|
+
readVisitorCookie() {
|
|
1304
|
+
if (!this.cookie || typeof document === "undefined") return null;
|
|
1305
|
+
try {
|
|
1306
|
+
return parseCookies(document.cookie)[this.cookieName()] || null;
|
|
1307
|
+
} catch {
|
|
1308
|
+
return null;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1042
1311
|
// ── Device id ────────────────────────────────────────────────────────────
|
|
1043
1312
|
/**
|
|
1044
|
-
* Resolve a stable device id: localStorage cache →
|
|
1045
|
-
* browser fingerprint. Cached for the session and across reloads.
|
|
1313
|
+
* Resolve a stable device id: localStorage cache → the visitor cookie a server already set →
|
|
1314
|
+
* MediaDevices → SHA-256 browser fingerprint. Cached for the session and across reloads.
|
|
1046
1315
|
*/
|
|
1047
1316
|
async resolveDeviceId() {
|
|
1048
1317
|
if (this.deviceId) {
|
|
@@ -1059,7 +1328,8 @@ var Identity = class {
|
|
|
1059
1328
|
}
|
|
1060
1329
|
} catch {
|
|
1061
1330
|
}
|
|
1062
|
-
let id =
|
|
1331
|
+
let id = this.readVisitorCookie();
|
|
1332
|
+
if (!id) id = await this.mediaDeviceId();
|
|
1063
1333
|
if (!id) id = await this.fingerprint();
|
|
1064
1334
|
this.deviceId = id;
|
|
1065
1335
|
try {
|
|
@@ -1914,7 +2184,10 @@ var XeniaClient = class {
|
|
|
1914
2184
|
renderer;
|
|
1915
2185
|
analyticsQueue;
|
|
1916
2186
|
tracker = null;
|
|
1917
|
-
|
|
2187
|
+
/** The in-flight (or finished) boot; shared so concurrent `init()` calls start one tracker. */
|
|
2188
|
+
booting = null;
|
|
2189
|
+
/** Bumped by `destroy()`, so a boot still waiting on the device id knows to stop. */
|
|
2190
|
+
generation = 0;
|
|
1918
2191
|
consentState;
|
|
1919
2192
|
listeners = /* @__PURE__ */ new Map();
|
|
1920
2193
|
constructor(options) {
|
|
@@ -1944,10 +2217,14 @@ var XeniaClient = class {
|
|
|
1944
2217
|
});
|
|
1945
2218
|
}
|
|
1946
2219
|
/** Boot the client: resolve device id and start auto-tracking. Idempotent. */
|
|
1947
|
-
|
|
1948
|
-
if (
|
|
2220
|
+
init() {
|
|
2221
|
+
if (!isBrowser()) return Promise.resolve();
|
|
2222
|
+
return this.booting ??= this.boot();
|
|
2223
|
+
}
|
|
2224
|
+
async boot() {
|
|
2225
|
+
const generation = this.generation;
|
|
1949
2226
|
await this.identity.resolveDeviceId();
|
|
1950
|
-
this.
|
|
2227
|
+
if (generation !== this.generation) return;
|
|
1951
2228
|
if (this.analyticsAllowed()) this.analyticsQueue.start();
|
|
1952
2229
|
if (this.options.autoTrack !== false) {
|
|
1953
2230
|
this.tracker = new AutoTracker({
|
|
@@ -1983,7 +2260,8 @@ var XeniaClient = class {
|
|
|
1983
2260
|
this.tracker = null;
|
|
1984
2261
|
this.renderer.destroy();
|
|
1985
2262
|
this.analyticsQueue.stop();
|
|
1986
|
-
this.
|
|
2263
|
+
this.booting = null;
|
|
2264
|
+
this.generation++;
|
|
1987
2265
|
}
|
|
1988
2266
|
// ── Identity ───────────────────────────────────────────────────────────────
|
|
1989
2267
|
/**
|
|
@@ -2112,6 +2390,27 @@ var XeniaClient = class {
|
|
|
2112
2390
|
async resolveBatch(items, opts) {
|
|
2113
2391
|
return this.api.resolveBatch(items, this.withVisitor(opts));
|
|
2114
2392
|
}
|
|
2393
|
+
/**
|
|
2394
|
+
* Personalize a fetched CMS page: static swaps are applied from the payload, runtime targets are
|
|
2395
|
+
* decided by Xenia in one `resolve-entries` call per nesting level, and replacements are loaded
|
|
2396
|
+
* through `opts.cms` (e.g. `contentfulAdapter(...)`). Never throws — anything undecided within
|
|
2397
|
+
* `opts.timeoutMs` keeps its base content.
|
|
2398
|
+
*/
|
|
2399
|
+
async personalizeContent(page, opts) {
|
|
2400
|
+
return personalizeContent(page, { ...opts, resolve: this.entryResolver(opts) });
|
|
2401
|
+
}
|
|
2402
|
+
/**
|
|
2403
|
+
* Fetch a page through `opts.cms` and personalize it — the app passes only the page's identity
|
|
2404
|
+
* (entry id, or `{ contentType, fields: { slug } }`). Never throws: a failed fetch is `content:
|
|
2405
|
+
* null` with `error`, a missing page `content: null`.
|
|
2406
|
+
*/
|
|
2407
|
+
async personalizePage(ref, opts) {
|
|
2408
|
+
return personalizePage(ref, { ...opts, resolve: this.entryResolver(opts) });
|
|
2409
|
+
}
|
|
2410
|
+
entryResolver(opts) {
|
|
2411
|
+
const resolveOpts = this.withVisitor({ data: opts.data, locale: opts.locale });
|
|
2412
|
+
return async (ids) => (await this.api.resolveEntries(ids, resolveOpts)).decisions ?? [];
|
|
2413
|
+
}
|
|
2115
2414
|
/** The SDK-owned visitor anchor: the `identify()`/`config.visitorId` id, else the device id. */
|
|
2116
2415
|
resolveVisitorId() {
|
|
2117
2416
|
return this.identity.getVisitorId() ?? this.identity.getDeviceId() ?? void 0;
|
|
@@ -2190,8 +2489,8 @@ var XeniaClient = class {
|
|
|
2190
2489
|
const response = await this.api.webhook(triggerId, body, opts?.method ?? "POST");
|
|
2191
2490
|
this.responses.set(triggerId, response);
|
|
2192
2491
|
this.emit("response", { triggerId, response });
|
|
2193
|
-
const
|
|
2194
|
-
if (
|
|
2492
|
+
const apply2 = opts?.apply ?? this.options.autoApplyActions !== false;
|
|
2493
|
+
if (apply2 && response.actions.length) {
|
|
2195
2494
|
this.applyActions(response.actions);
|
|
2196
2495
|
}
|
|
2197
2496
|
return response;
|
|
@@ -2245,8 +2544,8 @@ var XeniaClient = class {
|
|
|
2245
2544
|
compact({ ...ambient, fields })
|
|
2246
2545
|
);
|
|
2247
2546
|
this.emit("response", { triggerId: ref, response });
|
|
2248
|
-
const
|
|
2249
|
-
if (
|
|
2547
|
+
const apply2 = opts?.apply ?? this.options.autoApplyActions !== false;
|
|
2548
|
+
if (apply2 && response.actions.length) this.applyActions(response.actions);
|
|
2250
2549
|
return response;
|
|
2251
2550
|
}
|
|
2252
2551
|
// ── Imperative render helpers ─────────────────────────────────────────────────
|
|
@@ -2324,7 +2623,7 @@ function useXenia() {
|
|
|
2324
2623
|
}
|
|
2325
2624
|
return client;
|
|
2326
2625
|
}
|
|
2327
|
-
function XeniaProvider({ children, client, ...options }) {
|
|
2626
|
+
function XeniaProvider({ children: children2, client, ...options }) {
|
|
2328
2627
|
const ref = react.useRef(null);
|
|
2329
2628
|
const instance = react.useMemo(() => {
|
|
2330
2629
|
if (client) return client;
|
|
@@ -2335,7 +2634,7 @@ function XeniaProvider({ children, client, ...options }) {
|
|
|
2335
2634
|
void instance.init();
|
|
2336
2635
|
return () => instance.destroy();
|
|
2337
2636
|
}, [instance]);
|
|
2338
|
-
return /* @__PURE__ */ jsxRuntime.jsx(XeniaContext.Provider, { value: instance, children });
|
|
2637
|
+
return /* @__PURE__ */ jsxRuntime.jsx(XeniaContext.Provider, { value: instance, children: children2 });
|
|
2339
2638
|
}
|
|
2340
2639
|
|
|
2341
2640
|
// src/core/content-splice.ts
|
|
@@ -2522,12 +2821,92 @@ function useXeniaResponse(key) {
|
|
|
2522
2821
|
const xenia = useXenia();
|
|
2523
2822
|
return xenia.getResponse(key);
|
|
2524
2823
|
}
|
|
2824
|
+
function usePersonalizedContent(page, opts) {
|
|
2825
|
+
const xenia = useXenia();
|
|
2826
|
+
const hide = opts.render === "hide-until-resolved";
|
|
2827
|
+
const initialPending = react.useMemo(
|
|
2828
|
+
() => new Set(page == null ? [] : pendingTargets(page, opts.cms)),
|
|
2829
|
+
[page]
|
|
2830
|
+
// markers live in `page`; a new adapter object for the same page changes nothing
|
|
2831
|
+
);
|
|
2832
|
+
const [state, setState] = react.useState(
|
|
2833
|
+
() => ({ page, content: hide ? null : page ?? null, done: false, complete: false, applied: [], pending: initialPending })
|
|
2834
|
+
);
|
|
2835
|
+
const optsRef = react.useRef(opts);
|
|
2836
|
+
optsRef.current = opts;
|
|
2837
|
+
const optsKey = JSON.stringify({ data: opts.data ?? {}, locale: opts.locale });
|
|
2838
|
+
react.useEffect(() => {
|
|
2839
|
+
if (page == null) return;
|
|
2840
|
+
let active = true;
|
|
2841
|
+
const o = optsRef.current;
|
|
2842
|
+
setState({ page, content: hide ? null : page, done: false, complete: false, applied: [], pending: initialPending });
|
|
2843
|
+
const reveal = hide ? setTimeout(() => active && setState((s) => s.content ? s : { ...s, content: page }), o.hideTimeoutMs ?? o.timeoutMs ?? 300) : void 0;
|
|
2844
|
+
const applied = [];
|
|
2845
|
+
void xenia.personalizeContent(page, {
|
|
2846
|
+
...o,
|
|
2847
|
+
onApplied(swap) {
|
|
2848
|
+
applied.push(swap);
|
|
2849
|
+
o.onApplied?.(swap);
|
|
2850
|
+
},
|
|
2851
|
+
onProgress(p) {
|
|
2852
|
+
o.onProgress?.(p);
|
|
2853
|
+
if (!active || hide) return;
|
|
2854
|
+
setState({ page, content: p.content, done: false, complete: false, applied: [...applied], pending: new Set(p.pending) });
|
|
2855
|
+
}
|
|
2856
|
+
}).then((r) => {
|
|
2857
|
+
if (active) setState({ page, content: r.content, done: true, complete: r.complete, applied: r.applied, pending: /* @__PURE__ */ new Set() });
|
|
2858
|
+
}).finally(() => clearTimeout(reveal));
|
|
2859
|
+
return () => {
|
|
2860
|
+
active = false;
|
|
2861
|
+
clearTimeout(reveal);
|
|
2862
|
+
};
|
|
2863
|
+
}, [xenia, page, optsKey, hide, initialPending]);
|
|
2864
|
+
const personalized = new Set(state.applied.map((a) => a.target));
|
|
2865
|
+
const pending = state.page === page ? state.pending : initialPending;
|
|
2866
|
+
return {
|
|
2867
|
+
content: state.content,
|
|
2868
|
+
isLoading: page != null && !state.done,
|
|
2869
|
+
complete: state.complete,
|
|
2870
|
+
applied: state.applied,
|
|
2871
|
+
status: (id) => pending.has(id) ? "pending" : personalized.has(id) ? "personalized" : "base"
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
function usePersonalizedPage(ref, opts) {
|
|
2875
|
+
const [page, setPage] = react.useState({ value: null, loaded: false });
|
|
2876
|
+
const cmsRef = react.useRef(opts.cms);
|
|
2877
|
+
cmsRef.current = opts.cms;
|
|
2878
|
+
const refKey = JSON.stringify([ref, opts.locale]);
|
|
2879
|
+
react.useEffect(() => {
|
|
2880
|
+
let active = true;
|
|
2881
|
+
setPage({ value: null, loaded: false });
|
|
2882
|
+
const fetchPage = cmsRef.current.fetchPage;
|
|
2883
|
+
Promise.resolve(fetchPage ? fetchPage.call(cmsRef.current, ref, opts.locale) : null).then(
|
|
2884
|
+
(value) => active && setPage({ value: value ?? null, loaded: true }),
|
|
2885
|
+
() => active && setPage({ value: null, loaded: true, failed: true })
|
|
2886
|
+
);
|
|
2887
|
+
return () => {
|
|
2888
|
+
active = false;
|
|
2889
|
+
};
|
|
2890
|
+
}, [refKey]);
|
|
2891
|
+
const result = usePersonalizedContent(page.value, opts);
|
|
2892
|
+
const complete = page.value == null ? page.loaded && !page.failed : result.complete;
|
|
2893
|
+
return { ...result, isLoading: !page.loaded || result.isLoading, complete };
|
|
2894
|
+
}
|
|
2895
|
+
var PersonalizedContentContext = react.createContext(null);
|
|
2896
|
+
function XeniaContentProvider(props) {
|
|
2897
|
+
return react.createElement(PersonalizedContentContext.Provider, { value: props.value }, props.children);
|
|
2898
|
+
}
|
|
2899
|
+
function useXeniaEntry(entryId) {
|
|
2900
|
+
const ctx = react.useContext(PersonalizedContentContext);
|
|
2901
|
+
const status = ctx ? ctx.status(entryId) : "base";
|
|
2902
|
+
return { status, isLoading: status === "pending" };
|
|
2903
|
+
}
|
|
2525
2904
|
function XeniaTrack({
|
|
2526
2905
|
event,
|
|
2527
2906
|
on = "click",
|
|
2528
2907
|
props,
|
|
2529
2908
|
as = "span",
|
|
2530
|
-
children,
|
|
2909
|
+
children: children2,
|
|
2531
2910
|
className
|
|
2532
2911
|
}) {
|
|
2533
2912
|
const xenia = useXenia();
|
|
@@ -2559,7 +2938,7 @@ function XeniaTrack({
|
|
|
2559
2938
|
const handlers = on === "click" ? {
|
|
2560
2939
|
onClick: () => xenia.track(event, { ...props, interactionType: "click" }, { trigger: true })
|
|
2561
2940
|
} : {};
|
|
2562
|
-
return react.createElement(as, { ref, className, ...handlers },
|
|
2941
|
+
return react.createElement(as, { ref, className, ...handlers }, children2);
|
|
2563
2942
|
}
|
|
2564
2943
|
function XeniaMessage({ actions }) {
|
|
2565
2944
|
const xenia = useXenia();
|
|
@@ -2577,6 +2956,7 @@ function reactMessageComponent(Component) {
|
|
|
2577
2956
|
}
|
|
2578
2957
|
|
|
2579
2958
|
exports.XeniaClient = XeniaClient;
|
|
2959
|
+
exports.XeniaContentProvider = XeniaContentProvider;
|
|
2580
2960
|
exports.XeniaContext = XeniaContext;
|
|
2581
2961
|
exports.XeniaMessage = XeniaMessage;
|
|
2582
2962
|
exports.XeniaProvider = XeniaProvider;
|
|
@@ -2584,15 +2964,21 @@ exports.XeniaTrack = XeniaTrack;
|
|
|
2584
2964
|
exports.createXeniaClient = createXeniaClient;
|
|
2585
2965
|
exports.extractContentSwaps = extractContentSwaps;
|
|
2586
2966
|
exports.parseActions = parseActions;
|
|
2967
|
+
exports.pendingTargets = pendingTargets;
|
|
2968
|
+
exports.personalizeContent = personalizeContent;
|
|
2969
|
+
exports.personalizePage = personalizePage;
|
|
2587
2970
|
exports.reactMessageComponent = reactMessageComponent;
|
|
2588
2971
|
exports.spliceSlots = spliceSlots;
|
|
2589
2972
|
exports.usePersonalization = usePersonalization;
|
|
2590
2973
|
exports.usePersonalizationResolve = usePersonalizationResolve;
|
|
2591
2974
|
exports.usePersonalizationResolveSlots = usePersonalizationResolveSlots;
|
|
2592
2975
|
exports.usePersonalizationSlots = usePersonalizationSlots;
|
|
2976
|
+
exports.usePersonalizedContent = usePersonalizedContent;
|
|
2977
|
+
exports.usePersonalizedPage = usePersonalizedPage;
|
|
2593
2978
|
exports.useTrack = useTrack;
|
|
2594
2979
|
exports.useWebhook = useWebhook;
|
|
2595
2980
|
exports.useXenia = useXenia;
|
|
2981
|
+
exports.useXeniaEntry = useXeniaEntry;
|
|
2596
2982
|
exports.useXeniaResponse = useXeniaResponse;
|
|
2597
2983
|
//# sourceMappingURL=index.cjs.map
|
|
2598
2984
|
//# sourceMappingURL=index.cjs.map
|