@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
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, ElementType, ComponentType } from 'react';
3
- import { y as XeniaClientOptions, x as XeniaClient, s as ResolveResult, q as ResolveBatchResult, J as JsonValue, t as WebhookResponse, R as ResolveBatchItem, g as AnalyticsEventName, T as TrackOptions, e as XeniaAction, I as InteractionType, o as MessageMountContext, c as MessageComponentRenderer } from '../../actions-CKeiofxJ.cjs';
4
- export { A as AnalyticsContext, h as AnalyticsIdentity, i as AnalyticsIngestResponse, j as ClientEvent, C as ConsentState, k as ContentSwap, l as IdentifyRequest, m as IdentifyResponse, M as MessageCta, f as MessagePlacement, p as MessageRenderContext, b as MessageTemplateFn, P as PersonalizationSwap, r as ResolveOptions, X as XeniaConfig, a as XeniaEvent, z as createXeniaClient, B as extractContentSwaps, D as parseActions, E as spliceSlots } from '../../actions-CKeiofxJ.cjs';
3
+ import { E as XeniaClientOptions, D as XeniaClient, w as ResolveResult, u as ResolveBatchResult, J as JsonValue, x as WebhookResponse, R as ResolveBatchItem, g as AnalyticsEventName, T as TrackOptions, P as PagePersonalizeOptions, j as AppliedSwap, s as PageRef, e as XeniaAction, I as InteractionType, q as MessageMountContext, c as MessageComponentRenderer } from '../../actions-D9_aAvCv.cjs';
4
+ export { A as AnalyticsContext, h as AnalyticsIdentity, i as AnalyticsIngestResponse, k as ClientEvent, l as CmsAdapter, C as ConsentState, m as ContentSwap, n as IdentifyRequest, o as IdentifyResponse, M as MessageCta, f as MessagePlacement, r as MessageRenderContext, b as MessageTemplateFn, t as PersonalizationSwap, v as ResolveOptions, X as XeniaConfig, a as XeniaEvent, F as XeniaMarker, G as createXeniaClient, H as extractContentSwaps, K as parseActions, L as pendingTargets, N as personalizeContent, O as personalizePage, Q as spliceSlots } from '../../actions-D9_aAvCv.cjs';
5
5
 
6
6
  interface XeniaProviderProps extends XeniaClientOptions {
7
7
  children: ReactNode;
@@ -118,6 +118,56 @@ declare function useWebhook(triggerId: string): UseWebhookResult;
118
118
  /** Read the last saved webhook response for a key (reactive to sends via re-render). */
119
119
  declare function useXeniaResponse(key: string): WebhookResponse | undefined;
120
120
 
121
+ /** Where one personalized entry stands while the page resolves. */
122
+ type EntryStatus = "pending" | "personalized" | "base";
123
+ interface UsePersonalizedContentOptions extends PagePersonalizeOptions {
124
+ /**
125
+ * What to render before Xenia answers:
126
+ * - `"base-first"` (default): the CMS content immediately, swapped in place round by round
127
+ * (best LCP; personalized sections can visibly change).
128
+ * - `"hide-until-resolved"`: `content` is null until done or `hideTimeoutMs` — render a skeleton
129
+ * while `isLoading` (no flicker, costs up to `hideTimeoutMs` of blank).
130
+ */
131
+ render?: "base-first" | "hide-until-resolved";
132
+ /** Cap on the hidden period for `"hide-until-resolved"`. Default: `timeoutMs` (300). */
133
+ hideTimeoutMs?: number;
134
+ }
135
+ interface UsePersonalizedContentResult<T> {
136
+ content: T | null;
137
+ isLoading: boolean;
138
+ /**
139
+ * True once every personalized entry was decided. False while loading, and after a timeout or a
140
+ * failed Xenia/CMS call — the undecided entries then show their base content.
141
+ */
142
+ complete: boolean;
143
+ applied: AppliedSwap[];
144
+ /** Per-entry state, for skeletons / fades on just the sections being decided. */
145
+ status(entryId: string): EntryStatus;
146
+ }
147
+ /**
148
+ * Personalize a CMS page in the browser — `XeniaClient.personalizeContent` as a hook. Re-runs when
149
+ * `page` (by identity) or the serialized `data`/`locale` change. Never throws: on failure the base
150
+ * content renders. For pages that can render on the server, prefer `XeniaServer.personalizeContent`
151
+ * (no flicker, no client round trip).
152
+ */
153
+ declare function usePersonalizedContent<T>(page: T | null | undefined, opts: UsePersonalizedContentOptions): UsePersonalizedContentResult<T>;
154
+ /**
155
+ * Fetch a page through `opts.cms` and personalize it in the browser — `usePersonalizedContent`
156
+ * without the fetch in app code. `content` is null (and `isLoading` true) while the page loads; a
157
+ * failed or missing page ends with `content: null`, `isLoading: false`.
158
+ */
159
+ declare function usePersonalizedPage<T = unknown>(ref: PageRef, opts: UsePersonalizedContentOptions): UsePersonalizedContentResult<T>;
160
+ /** Shares a `usePersonalizedContent` result with nested sections (`useXeniaEntry`). */
161
+ declare function XeniaContentProvider(props: {
162
+ value: UsePersonalizedContentResult<unknown>;
163
+ children?: ReactNode;
164
+ }): react.FunctionComponentElement<react.ProviderProps<UsePersonalizedContentResult<unknown> | null>>;
165
+ /** Loading state of one entry inside a `<XeniaContentProvider>` — e.g. a skeleton for just that section. */
166
+ declare function useXeniaEntry(entryId: string): {
167
+ status: EntryStatus;
168
+ isLoading: boolean;
169
+ };
170
+
121
171
  interface XeniaTrackProps {
122
172
  /** Event id to fire. */
123
173
  event: string;
@@ -173,4 +223,4 @@ type XeniaMessageComponentProps = MessageMountContext;
173
223
  */
174
224
  declare function reactMessageComponent(Component: ComponentType<XeniaMessageComponentProps>): MessageComponentRenderer;
175
225
 
176
- export { AnalyticsEventName, InteractionType, JsonValue, MessageComponentRenderer, MessageMountContext, ResolveBatchItem, ResolveBatchResult, ResolveResult, TrackOptions, type UsePersonalizationResolveResult, type UsePersonalizationResolveSlotsResult, type UsePersonalizationResult, type UsePersonalizationSlotsResult, type UseWebhookResult, WebhookResponse, XeniaAction, XeniaClient, XeniaClientOptions, XeniaContext, XeniaMessage, type XeniaMessageComponentProps, type XeniaMessageProps, XeniaProvider, type XeniaProviderProps, XeniaTrack, type XeniaTrackProps, reactMessageComponent, usePersonalization, usePersonalizationResolve, usePersonalizationResolveSlots, usePersonalizationSlots, useTrack, useWebhook, useXenia, useXeniaResponse };
226
+ export { AnalyticsEventName, AppliedSwap, type EntryStatus, InteractionType, JsonValue, MessageComponentRenderer, MessageMountContext, PagePersonalizeOptions, PageRef, ResolveBatchItem, ResolveBatchResult, ResolveResult, TrackOptions, type UsePersonalizationResolveResult, type UsePersonalizationResolveSlotsResult, type UsePersonalizationResult, type UsePersonalizationSlotsResult, type UsePersonalizedContentOptions, type UsePersonalizedContentResult, type UseWebhookResult, WebhookResponse, XeniaAction, XeniaClient, XeniaClientOptions, XeniaContentProvider, XeniaContext, XeniaMessage, type XeniaMessageComponentProps, type XeniaMessageProps, XeniaProvider, type XeniaProviderProps, XeniaTrack, type XeniaTrackProps, reactMessageComponent, usePersonalization, usePersonalizationResolve, usePersonalizationResolveSlots, usePersonalizationSlots, usePersonalizedContent, usePersonalizedPage, useTrack, useWebhook, useXenia, useXeniaEntry, useXeniaResponse };
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, ElementType, ComponentType } from 'react';
3
- import { y as XeniaClientOptions, x as XeniaClient, s as ResolveResult, q as ResolveBatchResult, J as JsonValue, t as WebhookResponse, R as ResolveBatchItem, g as AnalyticsEventName, T as TrackOptions, e as XeniaAction, I as InteractionType, o as MessageMountContext, c as MessageComponentRenderer } from '../../actions-CKeiofxJ.js';
4
- export { A as AnalyticsContext, h as AnalyticsIdentity, i as AnalyticsIngestResponse, j as ClientEvent, C as ConsentState, k as ContentSwap, l as IdentifyRequest, m as IdentifyResponse, M as MessageCta, f as MessagePlacement, p as MessageRenderContext, b as MessageTemplateFn, P as PersonalizationSwap, r as ResolveOptions, X as XeniaConfig, a as XeniaEvent, z as createXeniaClient, B as extractContentSwaps, D as parseActions, E as spliceSlots } from '../../actions-CKeiofxJ.js';
3
+ import { E as XeniaClientOptions, D as XeniaClient, w as ResolveResult, u as ResolveBatchResult, J as JsonValue, x as WebhookResponse, R as ResolveBatchItem, g as AnalyticsEventName, T as TrackOptions, P as PagePersonalizeOptions, j as AppliedSwap, s as PageRef, e as XeniaAction, I as InteractionType, q as MessageMountContext, c as MessageComponentRenderer } from '../../actions-D9_aAvCv.js';
4
+ export { A as AnalyticsContext, h as AnalyticsIdentity, i as AnalyticsIngestResponse, k as ClientEvent, l as CmsAdapter, C as ConsentState, m as ContentSwap, n as IdentifyRequest, o as IdentifyResponse, M as MessageCta, f as MessagePlacement, r as MessageRenderContext, b as MessageTemplateFn, t as PersonalizationSwap, v as ResolveOptions, X as XeniaConfig, a as XeniaEvent, F as XeniaMarker, G as createXeniaClient, H as extractContentSwaps, K as parseActions, L as pendingTargets, N as personalizeContent, O as personalizePage, Q as spliceSlots } from '../../actions-D9_aAvCv.js';
5
5
 
6
6
  interface XeniaProviderProps extends XeniaClientOptions {
7
7
  children: ReactNode;
@@ -118,6 +118,56 @@ declare function useWebhook(triggerId: string): UseWebhookResult;
118
118
  /** Read the last saved webhook response for a key (reactive to sends via re-render). */
119
119
  declare function useXeniaResponse(key: string): WebhookResponse | undefined;
120
120
 
121
+ /** Where one personalized entry stands while the page resolves. */
122
+ type EntryStatus = "pending" | "personalized" | "base";
123
+ interface UsePersonalizedContentOptions extends PagePersonalizeOptions {
124
+ /**
125
+ * What to render before Xenia answers:
126
+ * - `"base-first"` (default): the CMS content immediately, swapped in place round by round
127
+ * (best LCP; personalized sections can visibly change).
128
+ * - `"hide-until-resolved"`: `content` is null until done or `hideTimeoutMs` — render a skeleton
129
+ * while `isLoading` (no flicker, costs up to `hideTimeoutMs` of blank).
130
+ */
131
+ render?: "base-first" | "hide-until-resolved";
132
+ /** Cap on the hidden period for `"hide-until-resolved"`. Default: `timeoutMs` (300). */
133
+ hideTimeoutMs?: number;
134
+ }
135
+ interface UsePersonalizedContentResult<T> {
136
+ content: T | null;
137
+ isLoading: boolean;
138
+ /**
139
+ * True once every personalized entry was decided. False while loading, and after a timeout or a
140
+ * failed Xenia/CMS call — the undecided entries then show their base content.
141
+ */
142
+ complete: boolean;
143
+ applied: AppliedSwap[];
144
+ /** Per-entry state, for skeletons / fades on just the sections being decided. */
145
+ status(entryId: string): EntryStatus;
146
+ }
147
+ /**
148
+ * Personalize a CMS page in the browser — `XeniaClient.personalizeContent` as a hook. Re-runs when
149
+ * `page` (by identity) or the serialized `data`/`locale` change. Never throws: on failure the base
150
+ * content renders. For pages that can render on the server, prefer `XeniaServer.personalizeContent`
151
+ * (no flicker, no client round trip).
152
+ */
153
+ declare function usePersonalizedContent<T>(page: T | null | undefined, opts: UsePersonalizedContentOptions): UsePersonalizedContentResult<T>;
154
+ /**
155
+ * Fetch a page through `opts.cms` and personalize it in the browser — `usePersonalizedContent`
156
+ * without the fetch in app code. `content` is null (and `isLoading` true) while the page loads; a
157
+ * failed or missing page ends with `content: null`, `isLoading: false`.
158
+ */
159
+ declare function usePersonalizedPage<T = unknown>(ref: PageRef, opts: UsePersonalizedContentOptions): UsePersonalizedContentResult<T>;
160
+ /** Shares a `usePersonalizedContent` result with nested sections (`useXeniaEntry`). */
161
+ declare function XeniaContentProvider(props: {
162
+ value: UsePersonalizedContentResult<unknown>;
163
+ children?: ReactNode;
164
+ }): react.FunctionComponentElement<react.ProviderProps<UsePersonalizedContentResult<unknown> | null>>;
165
+ /** Loading state of one entry inside a `<XeniaContentProvider>` — e.g. a skeleton for just that section. */
166
+ declare function useXeniaEntry(entryId: string): {
167
+ status: EntryStatus;
168
+ isLoading: boolean;
169
+ };
170
+
121
171
  interface XeniaTrackProps {
122
172
  /** Event id to fire. */
123
173
  event: string;
@@ -173,4 +223,4 @@ type XeniaMessageComponentProps = MessageMountContext;
173
223
  */
174
224
  declare function reactMessageComponent(Component: ComponentType<XeniaMessageComponentProps>): MessageComponentRenderer;
175
225
 
176
- export { AnalyticsEventName, InteractionType, JsonValue, MessageComponentRenderer, MessageMountContext, ResolveBatchItem, ResolveBatchResult, ResolveResult, TrackOptions, type UsePersonalizationResolveResult, type UsePersonalizationResolveSlotsResult, type UsePersonalizationResult, type UsePersonalizationSlotsResult, type UseWebhookResult, WebhookResponse, XeniaAction, XeniaClient, XeniaClientOptions, XeniaContext, XeniaMessage, type XeniaMessageComponentProps, type XeniaMessageProps, XeniaProvider, type XeniaProviderProps, XeniaTrack, type XeniaTrackProps, reactMessageComponent, usePersonalization, usePersonalizationResolve, usePersonalizationResolveSlots, usePersonalizationSlots, useTrack, useWebhook, useXenia, useXeniaResponse };
226
+ export { AnalyticsEventName, AppliedSwap, type EntryStatus, InteractionType, JsonValue, MessageComponentRenderer, MessageMountContext, PagePersonalizeOptions, PageRef, ResolveBatchItem, ResolveBatchResult, ResolveResult, TrackOptions, type UsePersonalizationResolveResult, type UsePersonalizationResolveSlotsResult, type UsePersonalizationResult, type UsePersonalizationSlotsResult, type UsePersonalizedContentOptions, type UsePersonalizedContentResult, type UseWebhookResult, WebhookResponse, XeniaAction, XeniaClient, XeniaClientOptions, XeniaContentProvider, XeniaContext, XeniaMessage, type XeniaMessageComponentProps, type XeniaMessageProps, XeniaProvider, type XeniaProviderProps, XeniaTrack, type XeniaTrackProps, reactMessageComponent, usePersonalization, usePersonalizationResolve, usePersonalizationResolveSlots, usePersonalizationSlots, usePersonalizedContent, usePersonalizedPage, useTrack, useWebhook, useXenia, useXeniaEntry, useXeniaResponse };
@@ -547,6 +547,25 @@ var XeniaApi = class {
547
547
  });
548
548
  return data;
549
549
  }
550
+ /**
551
+ * Resolve personalized CMS entries by id for one visitor (≤200) —
552
+ * `POST /api/personalization/resolve-entries`. The page resolver
553
+ * ({@link personalizeContent}) calls this once per round with the runtime
554
+ * targets it found; callers rarely need it directly.
555
+ */
556
+ async resolveEntries(entryIds, opts = {}) {
557
+ const { data } = await this.http.request({
558
+ method: "POST",
559
+ path: "/api/personalization/resolve-entries",
560
+ headers: await this.authHeaders(),
561
+ body: compact({
562
+ entryIds,
563
+ data: opts.data,
564
+ locale: opts.locale
565
+ })
566
+ });
567
+ return data;
568
+ }
550
569
  };
551
570
 
552
571
  // src/core/cookie-names.ts
@@ -661,6 +680,233 @@ function toEventId(raw) {
661
680
  return digest;
662
681
  }
663
682
 
683
+ // src/core/personalize-content.ts
684
+ var DEFAULT_TIMEOUT_MS = 300;
685
+ var DEFAULT_MAX_ROUNDS = 3;
686
+ var copyOf = (v) => v && typeof v === "object" && typeof structuredClone === "function" ? structuredClone(v) : v;
687
+ async function personalizeContent(payload, options) {
688
+ const { cms } = options;
689
+ const maxRounds = options.maxRounds ?? DEFAULT_MAX_ROUNDS;
690
+ const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
691
+ const applied = [];
692
+ const report = (swap) => {
693
+ applied.push(swap);
694
+ try {
695
+ options.onApplied?.(swap);
696
+ } catch {
697
+ }
698
+ };
699
+ let content = payload;
700
+ try {
701
+ if (!options.inPlace && typeof structuredClone === "function") content = structuredClone(content);
702
+ content = cms.prepare ? cms.prepare(content) : content;
703
+ } catch (error) {
704
+ return { content: payload, applied, complete: false, error };
705
+ }
706
+ const box = { root: content };
707
+ const settled = /* @__PURE__ */ new WeakSet();
708
+ const decided = /* @__PURE__ */ new Map();
709
+ let frontier = [{ parent: box, key: "root", value: box.root }];
710
+ const settle = (target, d, round) => {
711
+ settled.add(target.entry);
712
+ cms.clearMarker?.(target.entry);
713
+ const items = d.items.map(copyOf);
714
+ if (!d.decision || items.length === 0) {
715
+ return deferredSlots(target, cms);
716
+ }
717
+ for (const item of items) if (item && typeof item === "object") settled.add(item);
718
+ const op = d.decision.op ?? target.marker.op ?? "replace";
719
+ const next = apply(target, op, d.decision.field ?? target.marker.field, items, cms);
720
+ report({
721
+ target: target.id,
722
+ kind: "runtime",
723
+ op,
724
+ entryIds: items.map((e) => cms.entryId(e)).filter((id) => !!id),
725
+ contentKey: d.decision.contentKey,
726
+ bucket: d.decision.bucket,
727
+ round
728
+ });
729
+ return next;
730
+ };
731
+ for (let round = 0; ; round++) {
732
+ const pending = walk(frontier, cms, settled, round, report, (t) => {
733
+ const d = decided.get(t.id);
734
+ return d ? settle(t, d, round) : void 0;
735
+ });
736
+ if (pending.size === 0) return { content: box.root, applied, complete: true };
737
+ if (!options.resolve || round >= maxRounds) {
738
+ return { content: box.root, applied, complete: !options.resolve };
739
+ }
740
+ progress(options, { content: box.root, round, pending: [...pending.keys()] });
741
+ let decisions;
742
+ let entriesById;
743
+ let queried;
744
+ try {
745
+ decisions = await withDeadline(options.resolve([...pending.keys()]), deadline);
746
+ ({ entriesById, queried } = await withDeadline(loadReplacements(decisions, cms, options.locale), deadline));
747
+ } catch (error) {
748
+ return { content: box.root, applied, complete: false, error: error === TIMEOUT ? void 0 : error };
749
+ }
750
+ const byTarget = new Map(decisions.map((d) => [d.target, d]));
751
+ frontier = [];
752
+ for (const target of pending.values()) {
753
+ const decision = byTarget.get(target.id);
754
+ const items = decision ? decision.ids ? decision.ids.map((id) => entriesById.get(id)).filter((e) => e !== void 0) : queried.get(decision) ?? [] : [];
755
+ const d = { decision, items };
756
+ decided.set(target.id, d);
757
+ frontier.push(...settle(target, d, round));
758
+ }
759
+ progress(options, { content: box.root, round, pending: [] });
760
+ }
761
+ }
762
+ function progress(options, p) {
763
+ try {
764
+ options.onProgress?.(p);
765
+ } catch {
766
+ }
767
+ }
768
+ function walk(frontier, cms, settled, round, report, reuse) {
769
+ const pending = /* @__PURE__ */ new Map();
770
+ const seen = /* @__PURE__ */ new WeakSet();
771
+ const deferred = /* @__PURE__ */ new WeakSet();
772
+ const queue = [...frontier];
773
+ while (queue.length > 0) {
774
+ const slot = queue.shift();
775
+ const value = slot.value;
776
+ if (!value || typeof value !== "object" || deferred.has(value)) continue;
777
+ const id = settled.has(value) ? void 0 : cms.entryId(value);
778
+ const marker = id ? cms.marker(value) : null;
779
+ if (id && marker?.kind === "runtime") {
780
+ const again = reuse({ id, marker, entry: value, slots: [slot] });
781
+ if (again) {
782
+ queue.push(...again);
783
+ continue;
784
+ }
785
+ const existing = pending.get(id);
786
+ if (existing) {
787
+ existing.slots.push(slot);
788
+ } else {
789
+ pending.set(id, { id, marker, entry: value, slots: [slot] });
790
+ if (marker.op === "items" && marker.field) {
791
+ const held = cms.getField(value, marker.field);
792
+ if (held && typeof held === "object") deferred.add(held);
793
+ queue.push(...children(value));
794
+ }
795
+ }
796
+ continue;
797
+ }
798
+ if (id && marker?.kind === "static") {
799
+ const variants = cms.staticVariants(value).filter((v) => v && typeof v === "object");
800
+ settled.add(value);
801
+ cms.clearMarker?.(value);
802
+ if (variants.length > 0) {
803
+ for (const v of variants) settled.add(v);
804
+ const op = marker.op ?? "replace";
805
+ const target = { entry: value, slots: [slot] };
806
+ queue.push(...apply(target, op, marker.field, variants, cms));
807
+ report({
808
+ target: id,
809
+ kind: "static",
810
+ op,
811
+ entryIds: variants.map((v) => cms.entryId(v)).filter((x) => !!x),
812
+ round
813
+ });
814
+ continue;
815
+ }
816
+ }
817
+ if (seen.has(value)) continue;
818
+ seen.add(value);
819
+ queue.push(...children(value));
820
+ }
821
+ return pending;
822
+ }
823
+ function apply(target, op, field, items, cms) {
824
+ if (op === "items") {
825
+ if (field) {
826
+ cms.setField(target.entry, field, items);
827
+ return [{ parent: target.entry, key: field, value: cms.getField(target.entry, field) }];
828
+ }
829
+ if (cms.mergeItems) {
830
+ cms.mergeItems(target.entry, items[0]);
831
+ return target.slots.map((s) => ({ ...s, value: target.entry }));
832
+ }
833
+ op = "replace";
834
+ }
835
+ const next = [];
836
+ for (const slot of target.slots) {
837
+ const placed = op === "expand" && Array.isArray(slot.parent) ? items : [items[0]];
838
+ if (Array.isArray(slot.parent)) {
839
+ const index = slot.parent.indexOf(target.entry);
840
+ if (index < 0) continue;
841
+ slot.parent.splice(index, 1, ...placed);
842
+ } else {
843
+ slot.parent[slot.key] = placed[0];
844
+ }
845
+ for (const value of placed) next.push({ parent: slot.parent, key: slot.key, value });
846
+ }
847
+ return next;
848
+ }
849
+ function deferredSlots(target, cms) {
850
+ if (target.marker.op === "items" && target.marker.field) {
851
+ return [{ parent: target.entry, key: target.marker.field, value: cms.getField(target.entry, target.marker.field) }];
852
+ }
853
+ return target.slots.map((s) => ({ ...s, value: target.entry }));
854
+ }
855
+ function children(value) {
856
+ if (Array.isArray(value)) return value.map((v, i) => ({ parent: value, key: i, value: v }));
857
+ return Object.keys(value).map((k) => ({ parent: value, key: k, value: value[k] }));
858
+ }
859
+ async function loadReplacements(decisions, cms, locale) {
860
+ const ids = [...new Set(decisions.flatMap((d) => d.ids ?? []))];
861
+ const queries = decisions.filter((d) => !d.ids && d.query);
862
+ const [fetched, ...results] = await Promise.all([
863
+ ids.length > 0 ? cms.fetchEntries(ids, locale) : Promise.resolve([]),
864
+ ...queries.map((d) => cms.queryEntries(d.query, locale).catch(() => []))
865
+ ]);
866
+ const entriesById = /* @__PURE__ */ new Map();
867
+ for (const entry of fetched) {
868
+ const id = cms.entryId(entry);
869
+ if (id) entriesById.set(id, entry);
870
+ }
871
+ const queried = /* @__PURE__ */ new Map();
872
+ queries.forEach((d, i) => queried.set(d, results[i]));
873
+ return { entriesById, queried };
874
+ }
875
+ var TIMEOUT = /* @__PURE__ */ Symbol("timeout");
876
+ function withDeadline(promise, deadline) {
877
+ const remaining = deadline - Date.now();
878
+ if (remaining <= 0) return Promise.reject(TIMEOUT);
879
+ let timer;
880
+ return Promise.race([
881
+ promise,
882
+ new Promise((_, reject) => {
883
+ timer = setTimeout(() => reject(TIMEOUT), remaining);
884
+ })
885
+ ]).finally(() => clearTimeout(timer));
886
+ }
887
+ function pendingTargets(payload, cms) {
888
+ try {
889
+ let content = typeof structuredClone === "function" ? structuredClone(payload) : payload;
890
+ content = cms.prepare ? cms.prepare(content) : content;
891
+ const root = { parent: { root: content }, key: "root", value: content };
892
+ return [...walk([root], cms, /* @__PURE__ */ new WeakSet(), 0, () => {
893
+ }, () => void 0).keys()];
894
+ } catch {
895
+ return [];
896
+ }
897
+ }
898
+ async function personalizePage(ref, options) {
899
+ let page;
900
+ try {
901
+ if (!options.cms.fetchPage) throw new Error("This CMS adapter cannot fetch pages; use personalizeContent(page)");
902
+ page = await options.cms.fetchPage(ref, options.locale);
903
+ } catch (error) {
904
+ return { content: null, applied: [], complete: false, error };
905
+ }
906
+ if (page == null) return { content: null, applied: [], complete: true };
907
+ return personalizeContent(page, options);
908
+ }
909
+
664
910
  // src/core/response-store.ts
665
911
  var MemoryResponseStore = class {
666
912
  map = /* @__PURE__ */ new Map();
@@ -1912,7 +2158,10 @@ var XeniaClient = class {
1912
2158
  renderer;
1913
2159
  analyticsQueue;
1914
2160
  tracker = null;
1915
- ready = false;
2161
+ /** The in-flight (or finished) boot; shared so concurrent `init()` calls start one tracker. */
2162
+ booting = null;
2163
+ /** Bumped by `destroy()`, so a boot still waiting on the device id knows to stop. */
2164
+ generation = 0;
1916
2165
  consentState;
1917
2166
  listeners = /* @__PURE__ */ new Map();
1918
2167
  constructor(options) {
@@ -1942,10 +2191,14 @@ var XeniaClient = class {
1942
2191
  });
1943
2192
  }
1944
2193
  /** Boot the client: resolve device id and start auto-tracking. Idempotent. */
1945
- async init() {
1946
- if (this.ready || !isBrowser()) return;
2194
+ init() {
2195
+ if (!isBrowser()) return Promise.resolve();
2196
+ return this.booting ??= this.boot();
2197
+ }
2198
+ async boot() {
2199
+ const generation = this.generation;
1947
2200
  await this.identity.resolveDeviceId();
1948
- this.ready = true;
2201
+ if (generation !== this.generation) return;
1949
2202
  if (this.analyticsAllowed()) this.analyticsQueue.start();
1950
2203
  if (this.options.autoTrack !== false) {
1951
2204
  this.tracker = new AutoTracker({
@@ -1981,7 +2234,8 @@ var XeniaClient = class {
1981
2234
  this.tracker = null;
1982
2235
  this.renderer.destroy();
1983
2236
  this.analyticsQueue.stop();
1984
- this.ready = false;
2237
+ this.booting = null;
2238
+ this.generation++;
1985
2239
  }
1986
2240
  // ── Identity ───────────────────────────────────────────────────────────────
1987
2241
  /**
@@ -2110,6 +2364,27 @@ var XeniaClient = class {
2110
2364
  async resolveBatch(items, opts) {
2111
2365
  return this.api.resolveBatch(items, this.withVisitor(opts));
2112
2366
  }
2367
+ /**
2368
+ * Personalize a fetched CMS page: static swaps are applied from the payload, runtime targets are
2369
+ * decided by Xenia in one `resolve-entries` call per nesting level, and replacements are loaded
2370
+ * through `opts.cms` (e.g. `contentfulAdapter(...)`). Never throws — anything undecided within
2371
+ * `opts.timeoutMs` keeps its base content.
2372
+ */
2373
+ async personalizeContent(page, opts) {
2374
+ return personalizeContent(page, { ...opts, resolve: this.entryResolver(opts) });
2375
+ }
2376
+ /**
2377
+ * Fetch a page through `opts.cms` and personalize it — the app passes only the page's identity
2378
+ * (entry id, or `{ contentType, fields: { slug } }`). Never throws: a failed fetch is `content:
2379
+ * null` with `error`, a missing page `content: null`.
2380
+ */
2381
+ async personalizePage(ref, opts) {
2382
+ return personalizePage(ref, { ...opts, resolve: this.entryResolver(opts) });
2383
+ }
2384
+ entryResolver(opts) {
2385
+ const resolveOpts = this.withVisitor({ data: opts.data, locale: opts.locale });
2386
+ return async (ids) => (await this.api.resolveEntries(ids, resolveOpts)).decisions ?? [];
2387
+ }
2113
2388
  /** The SDK-owned visitor anchor: the `identify()`/`config.visitorId` id, else the device id. */
2114
2389
  resolveVisitorId() {
2115
2390
  return this.identity.getVisitorId() ?? this.identity.getDeviceId() ?? void 0;
@@ -2188,8 +2463,8 @@ var XeniaClient = class {
2188
2463
  const response = await this.api.webhook(triggerId, body, opts?.method ?? "POST");
2189
2464
  this.responses.set(triggerId, response);
2190
2465
  this.emit("response", { triggerId, response });
2191
- const apply = opts?.apply ?? this.options.autoApplyActions !== false;
2192
- if (apply && response.actions.length) {
2466
+ const apply2 = opts?.apply ?? this.options.autoApplyActions !== false;
2467
+ if (apply2 && response.actions.length) {
2193
2468
  this.applyActions(response.actions);
2194
2469
  }
2195
2470
  return response;
@@ -2243,8 +2518,8 @@ var XeniaClient = class {
2243
2518
  compact({ ...ambient, fields })
2244
2519
  );
2245
2520
  this.emit("response", { triggerId: ref, response });
2246
- const apply = opts?.apply ?? this.options.autoApplyActions !== false;
2247
- if (apply && response.actions.length) this.applyActions(response.actions);
2521
+ const apply2 = opts?.apply ?? this.options.autoApplyActions !== false;
2522
+ if (apply2 && response.actions.length) this.applyActions(response.actions);
2248
2523
  return response;
2249
2524
  }
2250
2525
  // ── Imperative render helpers ─────────────────────────────────────────────────
@@ -2322,7 +2597,7 @@ function useXenia() {
2322
2597
  }
2323
2598
  return client;
2324
2599
  }
2325
- function XeniaProvider({ children, client, ...options }) {
2600
+ function XeniaProvider({ children: children2, client, ...options }) {
2326
2601
  const ref = useRef(null);
2327
2602
  const instance = useMemo(() => {
2328
2603
  if (client) return client;
@@ -2333,7 +2608,7 @@ function XeniaProvider({ children, client, ...options }) {
2333
2608
  void instance.init();
2334
2609
  return () => instance.destroy();
2335
2610
  }, [instance]);
2336
- return /* @__PURE__ */ jsx(XeniaContext.Provider, { value: instance, children });
2611
+ return /* @__PURE__ */ jsx(XeniaContext.Provider, { value: instance, children: children2 });
2337
2612
  }
2338
2613
 
2339
2614
  // src/core/content-splice.ts
@@ -2520,12 +2795,92 @@ function useXeniaResponse(key) {
2520
2795
  const xenia = useXenia();
2521
2796
  return xenia.getResponse(key);
2522
2797
  }
2798
+ function usePersonalizedContent(page, opts) {
2799
+ const xenia = useXenia();
2800
+ const hide = opts.render === "hide-until-resolved";
2801
+ const initialPending = useMemo(
2802
+ () => new Set(page == null ? [] : pendingTargets(page, opts.cms)),
2803
+ [page]
2804
+ // markers live in `page`; a new adapter object for the same page changes nothing
2805
+ );
2806
+ const [state, setState] = useState(
2807
+ () => ({ page, content: hide ? null : page ?? null, done: false, complete: false, applied: [], pending: initialPending })
2808
+ );
2809
+ const optsRef = useRef(opts);
2810
+ optsRef.current = opts;
2811
+ const optsKey = JSON.stringify({ data: opts.data ?? {}, locale: opts.locale });
2812
+ useEffect(() => {
2813
+ if (page == null) return;
2814
+ let active = true;
2815
+ const o = optsRef.current;
2816
+ setState({ page, content: hide ? null : page, done: false, complete: false, applied: [], pending: initialPending });
2817
+ const reveal = hide ? setTimeout(() => active && setState((s) => s.content ? s : { ...s, content: page }), o.hideTimeoutMs ?? o.timeoutMs ?? 300) : void 0;
2818
+ const applied = [];
2819
+ void xenia.personalizeContent(page, {
2820
+ ...o,
2821
+ onApplied(swap) {
2822
+ applied.push(swap);
2823
+ o.onApplied?.(swap);
2824
+ },
2825
+ onProgress(p) {
2826
+ o.onProgress?.(p);
2827
+ if (!active || hide) return;
2828
+ setState({ page, content: p.content, done: false, complete: false, applied: [...applied], pending: new Set(p.pending) });
2829
+ }
2830
+ }).then((r) => {
2831
+ if (active) setState({ page, content: r.content, done: true, complete: r.complete, applied: r.applied, pending: /* @__PURE__ */ new Set() });
2832
+ }).finally(() => clearTimeout(reveal));
2833
+ return () => {
2834
+ active = false;
2835
+ clearTimeout(reveal);
2836
+ };
2837
+ }, [xenia, page, optsKey, hide, initialPending]);
2838
+ const personalized = new Set(state.applied.map((a) => a.target));
2839
+ const pending = state.page === page ? state.pending : initialPending;
2840
+ return {
2841
+ content: state.content,
2842
+ isLoading: page != null && !state.done,
2843
+ complete: state.complete,
2844
+ applied: state.applied,
2845
+ status: (id) => pending.has(id) ? "pending" : personalized.has(id) ? "personalized" : "base"
2846
+ };
2847
+ }
2848
+ function usePersonalizedPage(ref, opts) {
2849
+ const [page, setPage] = useState({ value: null, loaded: false });
2850
+ const cmsRef = useRef(opts.cms);
2851
+ cmsRef.current = opts.cms;
2852
+ const refKey = JSON.stringify([ref, opts.locale]);
2853
+ useEffect(() => {
2854
+ let active = true;
2855
+ setPage({ value: null, loaded: false });
2856
+ const fetchPage = cmsRef.current.fetchPage;
2857
+ Promise.resolve(fetchPage ? fetchPage.call(cmsRef.current, ref, opts.locale) : null).then(
2858
+ (value) => active && setPage({ value: value ?? null, loaded: true }),
2859
+ () => active && setPage({ value: null, loaded: true, failed: true })
2860
+ );
2861
+ return () => {
2862
+ active = false;
2863
+ };
2864
+ }, [refKey]);
2865
+ const result = usePersonalizedContent(page.value, opts);
2866
+ const complete = page.value == null ? page.loaded && !page.failed : result.complete;
2867
+ return { ...result, isLoading: !page.loaded || result.isLoading, complete };
2868
+ }
2869
+ var PersonalizedContentContext = createContext(null);
2870
+ function XeniaContentProvider(props) {
2871
+ return createElement(PersonalizedContentContext.Provider, { value: props.value }, props.children);
2872
+ }
2873
+ function useXeniaEntry(entryId) {
2874
+ const ctx = useContext(PersonalizedContentContext);
2875
+ const status = ctx ? ctx.status(entryId) : "base";
2876
+ return { status, isLoading: status === "pending" };
2877
+ }
2523
2878
  function XeniaTrack({
2524
2879
  event,
2525
2880
  on = "click",
2526
2881
  props,
2527
2882
  as = "span",
2528
- children,
2883
+ children: children2,
2529
2884
  className
2530
2885
  }) {
2531
2886
  const xenia = useXenia();
@@ -2557,7 +2912,7 @@ function XeniaTrack({
2557
2912
  const handlers = on === "click" ? {
2558
2913
  onClick: () => xenia.track(event, { ...props, interactionType: "click" }, { trigger: true })
2559
2914
  } : {};
2560
- return createElement(as, { ref, className, ...handlers }, children);
2915
+ return createElement(as, { ref, className, ...handlers }, children2);
2561
2916
  }
2562
2917
  function XeniaMessage({ actions }) {
2563
2918
  const xenia = useXenia();
@@ -2574,6 +2929,6 @@ function reactMessageComponent(Component) {
2574
2929
  };
2575
2930
  }
2576
2931
 
2577
- export { XeniaClient, XeniaContext, XeniaMessage, XeniaProvider, XeniaTrack, createXeniaClient, extractContentSwaps, parseActions, reactMessageComponent, spliceSlots, usePersonalization, usePersonalizationResolve, usePersonalizationResolveSlots, usePersonalizationSlots, useTrack, useWebhook, useXenia, useXeniaResponse };
2932
+ export { XeniaClient, XeniaContentProvider, XeniaContext, XeniaMessage, XeniaProvider, XeniaTrack, createXeniaClient, extractContentSwaps, parseActions, pendingTargets, personalizeContent, personalizePage, reactMessageComponent, spliceSlots, usePersonalization, usePersonalizationResolve, usePersonalizationResolveSlots, usePersonalizationSlots, usePersonalizedContent, usePersonalizedPage, useTrack, useWebhook, useXenia, useXeniaEntry, useXeniaResponse };
2578
2933
  //# sourceMappingURL=index.js.map
2579
2934
  //# sourceMappingURL=index.js.map