@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.
- package/README.md +50 -0
- package/assets/coverage.json +1 -0
- package/dist/{actions-CKeiofxJ.d.cts → actions-D9_aAvCv.d.cts} +188 -2
- package/dist/{actions-CKeiofxJ.d.ts → actions-D9_aAvCv.d.ts} +188 -2
- 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 +287 -9
- 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 +285 -10
- package/dist/client/index.js.map +1 -1
- package/dist/client/react/index.cjs +375 -13
- 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 +369 -14
- 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
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { LocalStorageResponseStore, MemoryResponseStore, extractContentSwaps, spliceSlots } from './chunk-
|
|
2
|
-
export { CONSENT_COOKIE, CircuitOpenError, DEFAULT_PREFIX, HttpClient, HttpError, MAX_EVENT_ID_LENGTH, VISITOR_COOKIE, XeniaApi, buildUrl, consentCookie, consentCookieName, contextFromUrl, generateUlid, isBrowser, parseActions, toEventId, visitorCookieName } from './chunk-
|
|
1
|
+
export { LocalStorageResponseStore, MemoryResponseStore, extractContentSwaps, spliceSlots } from './chunk-OUZU4Z5B.js';
|
|
2
|
+
export { CONSENT_COOKIE, CircuitOpenError, DEFAULT_PREFIX, HttpClient, HttpError, MAX_EVENT_ID_LENGTH, VISITOR_COOKIE, XeniaApi, buildUrl, consentCookie, consentCookieName, contextFromUrl, generateUlid, isBrowser, parseActions, pendingTargets, personalizeContent, personalizePage, toEventId, visitorCookieName } from './chunk-7NGJC7CF.js';
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
|
4
4
|
//# sourceMappingURL=index.js.map
|
package/dist/next/index.cjs
CHANGED
|
@@ -1,10 +1,31 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
4
|
-
var
|
|
3
|
+
var chunk3PYPPIUM_cjs = require('../chunk-3PYPPIUM.cjs');
|
|
4
|
+
var chunkIBNIRT7J_cjs = require('../chunk-IBNIRT7J.cjs');
|
|
5
|
+
var React = require('react');
|
|
5
6
|
var headers = require('next/headers');
|
|
6
7
|
var server = require('next/server');
|
|
7
8
|
|
|
9
|
+
function _interopNamespace(e) {
|
|
10
|
+
if (e && e.__esModule) return e;
|
|
11
|
+
var n = Object.create(null);
|
|
12
|
+
if (e) {
|
|
13
|
+
Object.keys(e).forEach(function (k) {
|
|
14
|
+
if (k !== 'default') {
|
|
15
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
16
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
17
|
+
enumerable: true,
|
|
18
|
+
get: function () { return e[k]; }
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
n.default = e;
|
|
24
|
+
return Object.freeze(n);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
var React__namespace = /*#__PURE__*/_interopNamespace(React);
|
|
28
|
+
|
|
8
29
|
var URL_HEADER = "x-xenia-url";
|
|
9
30
|
function cookieAttributes(c = {}) {
|
|
10
31
|
const sameSite = c.sameSite ?? "Lax";
|
|
@@ -17,9 +38,9 @@ function cookieAttributes(c = {}) {
|
|
|
17
38
|
};
|
|
18
39
|
}
|
|
19
40
|
function xeniaProxy(request, opts = {}) {
|
|
20
|
-
const name =
|
|
41
|
+
const name = chunkIBNIRT7J_cjs.visitorCookieName(opts.prefix);
|
|
21
42
|
const existing = request.cookies.get(name)?.value;
|
|
22
|
-
const visitorId = existing ??
|
|
43
|
+
const visitorId = existing ?? chunkIBNIRT7J_cjs.generateUlid();
|
|
23
44
|
const forwarded = new Headers(opts.headers ?? request.headers);
|
|
24
45
|
forwarded.set(URL_HEADER, request.url);
|
|
25
46
|
if (!existing) {
|
|
@@ -32,27 +53,38 @@ function xeniaProxy(request, opts = {}) {
|
|
|
32
53
|
}
|
|
33
54
|
async function xeniaServerFromNext(config) {
|
|
34
55
|
const [jar, h] = await Promise.all([headers.cookies(), headers.headers()]);
|
|
35
|
-
const name =
|
|
56
|
+
const name = chunkIBNIRT7J_cjs.visitorCookieName(config.prefix);
|
|
36
57
|
let visitorId = config.visitorId ?? jar.get(name)?.value;
|
|
37
58
|
if (!visitorId) {
|
|
38
|
-
visitorId =
|
|
59
|
+
visitorId = chunkIBNIRT7J_cjs.generateUlid();
|
|
39
60
|
try {
|
|
40
61
|
jar.set(name, visitorId, cookieAttributes());
|
|
41
62
|
} catch {
|
|
42
63
|
}
|
|
43
64
|
}
|
|
44
|
-
return
|
|
65
|
+
return chunk3PYPPIUM_cjs.createXeniaServer({
|
|
45
66
|
...config,
|
|
46
67
|
visitorId,
|
|
47
|
-
context: { ...
|
|
68
|
+
context: { ...chunkIBNIRT7J_cjs.contextFromUrl(h.get(URL_HEADER), h.get("referer")), ...config.context }
|
|
48
69
|
});
|
|
49
70
|
}
|
|
50
71
|
async function consentFromNext(prefix) {
|
|
51
|
-
return
|
|
72
|
+
return chunk3PYPPIUM_cjs.getConsent((await headers.headers()).get("cookie"), prefix);
|
|
73
|
+
}
|
|
74
|
+
var requestStore = typeof React__namespace.cache === "function" ? React__namespace.cache(() => /* @__PURE__ */ new Map()) : () => /* @__PURE__ */ new Map();
|
|
75
|
+
function personalizeOnce(key, personalize) {
|
|
76
|
+
const store = requestStore();
|
|
77
|
+
let run = store.get(key);
|
|
78
|
+
if (!run) {
|
|
79
|
+
run = personalize();
|
|
80
|
+
store.set(key, run);
|
|
81
|
+
}
|
|
82
|
+
return run;
|
|
52
83
|
}
|
|
53
84
|
|
|
54
85
|
exports.URL_HEADER = URL_HEADER;
|
|
55
86
|
exports.consentFromNext = consentFromNext;
|
|
87
|
+
exports.personalizeOnce = personalizeOnce;
|
|
56
88
|
exports.xeniaProxy = xeniaProxy;
|
|
57
89
|
exports.xeniaServerFromNext = xeniaServerFromNext;
|
|
58
90
|
//# sourceMappingURL=index.cjs.map
|
package/dist/next/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/next/index.ts"],"names":["visitorCookieName","generateUlid","NextResponse","cookies","headers","createXeniaServer","contextFromUrl","getConsent"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/next/index.ts"],"names":["visitorCookieName","generateUlid","NextResponse","cookies","headers","createXeniaServer","contextFromUrl","getConsent","React"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * `@xenia-martech/sdk-js/next` — Next.js (App Router, 15+) glue for the server SDK.\n *\n * A Server Component can't see its own URL or set cookies during render, so the two halves split\n * the work: `xeniaProxy()` runs in `proxy.ts`/`middleware.ts` (mints the visitor cookie, stamps the\n * URL onto a request header), and `xeniaServerFromNext()` reads both back to bind a per-request\n * `XeniaServer`. Use them together; each alone degrades gracefully (no URL context / a throwaway\n * visitor id).\n */\n\nimport * as React from \"react\";\nimport { cookies, headers } from \"next/headers\";\nimport { NextResponse, type NextRequest } from \"next/server\";\nimport { visitorCookieName } from \"../core/cookie-names\";\nimport { generateUlid } from \"../core/id\";\nimport { contextFromUrl } from \"../core/request-context\";\nimport type { ConsentState, VisitorCookieOptions, XeniaConfig } from \"../core/types\";\nimport { getConsent } from \"../server/cookies\";\nimport { createXeniaServer, type XeniaServer } from \"../server/xenia-server\";\n\n/** Request header `xeniaProxy()` stamps with the URL being served. */\nexport const URL_HEADER = \"x-xenia-url\";\n\nexport interface XeniaProxyOptions {\n /** Request headers to forward (e.g. your own additions); defaults to the incoming ones. */\n headers?: Headers;\n /** Must match the browser client's `config.prefix`, if you set one. */\n prefix?: string;\n /** Visitor cookie attributes. Default: `path=/`, `SameSite=Lax`, one year. */\n cookie?: VisitorCookieOptions;\n}\n\nfunction cookieAttributes(c: VisitorCookieOptions = {}) {\n const sameSite = c.sameSite ?? \"Lax\";\n return {\n path: \"/\",\n maxAge: Math.floor((c.maxAgeDays ?? 365) * 86400),\n sameSite: sameSite.toLowerCase() as \"lax\" | \"strict\" | \"none\",\n secure: c.secure || sameSite === \"None\",\n ...(c.domain && { domain: c.domain }),\n };\n}\n\n/**\n * Next.js proxy/middleware step: ensure the visitor cookie exists and stamp the URL onto\n * {@link URL_HEADER}, then continue. A new visitor id is also put on the forwarded `Cookie` header,\n * so this very request's render already sees it.\n *\n * ```ts\n * // proxy.ts\n * export const proxy = (request: NextRequest) => xeniaProxy(request);\n * ```\n */\nexport function xeniaProxy(request: NextRequest, opts: XeniaProxyOptions = {}): NextResponse {\n const name = visitorCookieName(opts.prefix);\n const existing = request.cookies.get(name)?.value;\n const visitorId = existing ?? generateUlid();\n\n const forwarded = new Headers(opts.headers ?? request.headers);\n forwarded.set(URL_HEADER, request.url);\n if (!existing) {\n // NextRequest.cookies.set doesn't rewrite the forwarded Cookie header, and cookies() reads that.\n const header = forwarded.get(\"cookie\");\n forwarded.set(\"cookie\", header ? `${header}; ${name}=${visitorId}` : `${name}=${visitorId}`);\n }\n\n const response = NextResponse.next({ request: { headers: forwarded } });\n if (!existing) response.cookies.set(name, visitorId, cookieAttributes(opts.cookie));\n return response;\n}\n\n/**\n * A `XeniaServer` bound to this request's visitor (the cookie `xeniaProxy()` set) and URL context\n * (path, query, UTMs, referrer). For Server Components, Server Actions and Route Handlers.\n * `config.visitorId`/`config.context` still win when passed.\n */\nexport async function xeniaServerFromNext(config: XeniaConfig): Promise<XeniaServer> {\n const [jar, h] = await Promise.all([cookies(), headers()]);\n const name = visitorCookieName(config.prefix);\n let visitorId = config.visitorId ?? jar.get(name)?.value;\n if (!visitorId) {\n visitorId = generateUlid();\n try {\n jar.set(name, visitorId, cookieAttributes());\n } catch {\n // Expected during a Server Component render; xeniaProxy() is the place that persists it.\n }\n }\n return createXeniaServer({\n ...config,\n visitorId,\n context: { ...contextFromUrl(h.get(URL_HEADER), h.get(\"referer\")), ...config.context },\n });\n}\n\n/** This request's consent, from the cookie the browser client's `consent()` (or `consentCookie()`) wrote. */\nexport async function consentFromNext(prefix?: string): Promise<ConsentState> {\n return getConsent((await headers()).get(\"cookie\"), prefix);\n}\n\n// ── CMS page personalization ──────────────────────────────────────────────────\n\n// One store per server request (React.cache scopes it to the render), so every section that asks\n// for the same page shares ONE personalization run. Falls back to \"no sharing\" outside React\n// Server Components, where `cache` is unavailable.\nconst requestStore: () => Map<string, Promise<unknown>> =\n typeof (React as { cache?: unknown }).cache === \"function\"\n ? (React as unknown as { cache: <F extends () => unknown>(fn: F) => F }).cache(() => new Map())\n : () => new Map();\n\n/**\n * Run `personalize` at most once per request for `key`, and share the promise. Lets a page shell\n * render immediately while each personalized section awaits the same run inside its own\n * `<Suspense>`, streaming in as soon as it's ready:\n *\n * ```tsx\n * const run = (ref) => personalizeOnce(`page:${ref}`, async () =>\n * (await xeniaServerFromNext(config)).personalizePage(ref, { cms, timeoutMs: 800 }));\n *\n * export default async function Page({ params }) {\n * const xenia = await xeniaServerFromNext(config);\n * const base = await xenia.personalizePage(ref, { cms, maxRounds: 0 }); // CMS only, no Xenia call\n * const pending = new Set(pendingTargets(base.content, cms)); // from the markers\n * return base.content.fields.sections.map((s) =>\n * pending.has(s.sys.id) ? (\n * <Suspense key={s.sys.id} fallback={<SectionSkeleton />}> // never base content that will change\n * <PersonalizedSection id={s.sys.id} run={() => run(ref)} />\n * </Suspense>\n * ) : (\n * <Section key={s.sys.id} entry={s} />\n * ));\n * }\n * ```\n *\n * To finish deeper levels in the browser instead, pass `maxRounds: 1` here and hand the result's\n * `content` to `usePersonalizedContent` — already-decided entries are not asked about again.\n */\nexport function personalizeOnce<T>(key: string, personalize: () => Promise<T>): Promise<T> {\n const store = requestStore();\n let run = store.get(key) as Promise<T> | undefined;\n if (!run) {\n run = personalize();\n store.set(key, run);\n }\n return run;\n}\n"]}
|
package/dist/next/index.d.cts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import { V as VisitorCookieOptions,
|
|
3
|
-
import { X as XeniaServer } from '../xenia-server-
|
|
2
|
+
import { V as VisitorCookieOptions, f as ConsentState, L as XeniaConfig } from '../personalize-content-Clc1oMYY.cjs';
|
|
3
|
+
import { X as XeniaServer } from '../xenia-server-o_wDOcei.cjs';
|
|
4
|
+
import '../api-BEPW0Uan.cjs';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* `@xenia-martech/sdk-js/next` — Next.js (App Router, 15+) glue for the server SDK.
|
|
@@ -41,5 +42,33 @@ declare function xeniaProxy(request: NextRequest, opts?: XeniaProxyOptions): Nex
|
|
|
41
42
|
declare function xeniaServerFromNext(config: XeniaConfig): Promise<XeniaServer>;
|
|
42
43
|
/** This request's consent, from the cookie the browser client's `consent()` (or `consentCookie()`) wrote. */
|
|
43
44
|
declare function consentFromNext(prefix?: string): Promise<ConsentState>;
|
|
45
|
+
/**
|
|
46
|
+
* Run `personalize` at most once per request for `key`, and share the promise. Lets a page shell
|
|
47
|
+
* render immediately while each personalized section awaits the same run inside its own
|
|
48
|
+
* `<Suspense>`, streaming in as soon as it's ready:
|
|
49
|
+
*
|
|
50
|
+
* ```tsx
|
|
51
|
+
* const run = (ref) => personalizeOnce(`page:${ref}`, async () =>
|
|
52
|
+
* (await xeniaServerFromNext(config)).personalizePage(ref, { cms, timeoutMs: 800 }));
|
|
53
|
+
*
|
|
54
|
+
* export default async function Page({ params }) {
|
|
55
|
+
* const xenia = await xeniaServerFromNext(config);
|
|
56
|
+
* const base = await xenia.personalizePage(ref, { cms, maxRounds: 0 }); // CMS only, no Xenia call
|
|
57
|
+
* const pending = new Set(pendingTargets(base.content, cms)); // from the markers
|
|
58
|
+
* return base.content.fields.sections.map((s) =>
|
|
59
|
+
* pending.has(s.sys.id) ? (
|
|
60
|
+
* <Suspense key={s.sys.id} fallback={<SectionSkeleton />}> // never base content that will change
|
|
61
|
+
* <PersonalizedSection id={s.sys.id} run={() => run(ref)} />
|
|
62
|
+
* </Suspense>
|
|
63
|
+
* ) : (
|
|
64
|
+
* <Section key={s.sys.id} entry={s} />
|
|
65
|
+
* ));
|
|
66
|
+
* }
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* To finish deeper levels in the browser instead, pass `maxRounds: 1` here and hand the result's
|
|
70
|
+
* `content` to `usePersonalizedContent` — already-decided entries are not asked about again.
|
|
71
|
+
*/
|
|
72
|
+
declare function personalizeOnce<T>(key: string, personalize: () => Promise<T>): Promise<T>;
|
|
44
73
|
|
|
45
|
-
export { URL_HEADER, type XeniaProxyOptions, consentFromNext, xeniaProxy, xeniaServerFromNext };
|
|
74
|
+
export { URL_HEADER, type XeniaProxyOptions, consentFromNext, personalizeOnce, xeniaProxy, xeniaServerFromNext };
|
package/dist/next/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import { V as VisitorCookieOptions,
|
|
3
|
-
import { X as XeniaServer } from '../xenia-server-
|
|
2
|
+
import { V as VisitorCookieOptions, f as ConsentState, L as XeniaConfig } from '../personalize-content-Clc1oMYY.js';
|
|
3
|
+
import { X as XeniaServer } from '../xenia-server-BWhWD0La.js';
|
|
4
|
+
import '../api-CktVQ6Gl.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* `@xenia-martech/sdk-js/next` — Next.js (App Router, 15+) glue for the server SDK.
|
|
@@ -41,5 +42,33 @@ declare function xeniaProxy(request: NextRequest, opts?: XeniaProxyOptions): Nex
|
|
|
41
42
|
declare function xeniaServerFromNext(config: XeniaConfig): Promise<XeniaServer>;
|
|
42
43
|
/** This request's consent, from the cookie the browser client's `consent()` (or `consentCookie()`) wrote. */
|
|
43
44
|
declare function consentFromNext(prefix?: string): Promise<ConsentState>;
|
|
45
|
+
/**
|
|
46
|
+
* Run `personalize` at most once per request for `key`, and share the promise. Lets a page shell
|
|
47
|
+
* render immediately while each personalized section awaits the same run inside its own
|
|
48
|
+
* `<Suspense>`, streaming in as soon as it's ready:
|
|
49
|
+
*
|
|
50
|
+
* ```tsx
|
|
51
|
+
* const run = (ref) => personalizeOnce(`page:${ref}`, async () =>
|
|
52
|
+
* (await xeniaServerFromNext(config)).personalizePage(ref, { cms, timeoutMs: 800 }));
|
|
53
|
+
*
|
|
54
|
+
* export default async function Page({ params }) {
|
|
55
|
+
* const xenia = await xeniaServerFromNext(config);
|
|
56
|
+
* const base = await xenia.personalizePage(ref, { cms, maxRounds: 0 }); // CMS only, no Xenia call
|
|
57
|
+
* const pending = new Set(pendingTargets(base.content, cms)); // from the markers
|
|
58
|
+
* return base.content.fields.sections.map((s) =>
|
|
59
|
+
* pending.has(s.sys.id) ? (
|
|
60
|
+
* <Suspense key={s.sys.id} fallback={<SectionSkeleton />}> // never base content that will change
|
|
61
|
+
* <PersonalizedSection id={s.sys.id} run={() => run(ref)} />
|
|
62
|
+
* </Suspense>
|
|
63
|
+
* ) : (
|
|
64
|
+
* <Section key={s.sys.id} entry={s} />
|
|
65
|
+
* ));
|
|
66
|
+
* }
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* To finish deeper levels in the browser instead, pass `maxRounds: 1` here and hand the result's
|
|
70
|
+
* `content` to `usePersonalizedContent` — already-decided entries are not asked about again.
|
|
71
|
+
*/
|
|
72
|
+
declare function personalizeOnce<T>(key: string, personalize: () => Promise<T>): Promise<T>;
|
|
44
73
|
|
|
45
|
-
export { URL_HEADER, type XeniaProxyOptions, consentFromNext, xeniaProxy, xeniaServerFromNext };
|
|
74
|
+
export { URL_HEADER, type XeniaProxyOptions, consentFromNext, personalizeOnce, xeniaProxy, xeniaServerFromNext };
|
package/dist/next/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { createXeniaServer, getConsent } from '../chunk-
|
|
2
|
-
import { visitorCookieName, generateUlid, contextFromUrl } from '../chunk-
|
|
1
|
+
import { createXeniaServer, getConsent } from '../chunk-DWFVDNU5.js';
|
|
2
|
+
import { visitorCookieName, generateUlid, contextFromUrl } from '../chunk-7NGJC7CF.js';
|
|
3
|
+
import * as React from 'react';
|
|
3
4
|
import { cookies, headers } from 'next/headers';
|
|
4
5
|
import { NextResponse } from 'next/server';
|
|
5
6
|
|
|
@@ -48,7 +49,17 @@ async function xeniaServerFromNext(config) {
|
|
|
48
49
|
async function consentFromNext(prefix) {
|
|
49
50
|
return getConsent((await headers()).get("cookie"), prefix);
|
|
50
51
|
}
|
|
52
|
+
var requestStore = typeof React.cache === "function" ? React.cache(() => /* @__PURE__ */ new Map()) : () => /* @__PURE__ */ new Map();
|
|
53
|
+
function personalizeOnce(key, personalize) {
|
|
54
|
+
const store = requestStore();
|
|
55
|
+
let run = store.get(key);
|
|
56
|
+
if (!run) {
|
|
57
|
+
run = personalize();
|
|
58
|
+
store.set(key, run);
|
|
59
|
+
}
|
|
60
|
+
return run;
|
|
61
|
+
}
|
|
51
62
|
|
|
52
|
-
export { URL_HEADER, consentFromNext, xeniaProxy, xeniaServerFromNext };
|
|
63
|
+
export { URL_HEADER, consentFromNext, personalizeOnce, xeniaProxy, xeniaServerFromNext };
|
|
53
64
|
//# sourceMappingURL=index.js.map
|
|
54
65
|
//# sourceMappingURL=index.js.map
|
package/dist/next/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/next/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/next/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * `@xenia-martech/sdk-js/next` — Next.js (App Router, 15+) glue for the server SDK.\n *\n * A Server Component can't see its own URL or set cookies during render, so the two halves split\n * the work: `xeniaProxy()` runs in `proxy.ts`/`middleware.ts` (mints the visitor cookie, stamps the\n * URL onto a request header), and `xeniaServerFromNext()` reads both back to bind a per-request\n * `XeniaServer`. Use them together; each alone degrades gracefully (no URL context / a throwaway\n * visitor id).\n */\n\nimport * as React from \"react\";\nimport { cookies, headers } from \"next/headers\";\nimport { NextResponse, type NextRequest } from \"next/server\";\nimport { visitorCookieName } from \"../core/cookie-names\";\nimport { generateUlid } from \"../core/id\";\nimport { contextFromUrl } from \"../core/request-context\";\nimport type { ConsentState, VisitorCookieOptions, XeniaConfig } from \"../core/types\";\nimport { getConsent } from \"../server/cookies\";\nimport { createXeniaServer, type XeniaServer } from \"../server/xenia-server\";\n\n/** Request header `xeniaProxy()` stamps with the URL being served. */\nexport const URL_HEADER = \"x-xenia-url\";\n\nexport interface XeniaProxyOptions {\n /** Request headers to forward (e.g. your own additions); defaults to the incoming ones. */\n headers?: Headers;\n /** Must match the browser client's `config.prefix`, if you set one. */\n prefix?: string;\n /** Visitor cookie attributes. Default: `path=/`, `SameSite=Lax`, one year. */\n cookie?: VisitorCookieOptions;\n}\n\nfunction cookieAttributes(c: VisitorCookieOptions = {}) {\n const sameSite = c.sameSite ?? \"Lax\";\n return {\n path: \"/\",\n maxAge: Math.floor((c.maxAgeDays ?? 365) * 86400),\n sameSite: sameSite.toLowerCase() as \"lax\" | \"strict\" | \"none\",\n secure: c.secure || sameSite === \"None\",\n ...(c.domain && { domain: c.domain }),\n };\n}\n\n/**\n * Next.js proxy/middleware step: ensure the visitor cookie exists and stamp the URL onto\n * {@link URL_HEADER}, then continue. A new visitor id is also put on the forwarded `Cookie` header,\n * so this very request's render already sees it.\n *\n * ```ts\n * // proxy.ts\n * export const proxy = (request: NextRequest) => xeniaProxy(request);\n * ```\n */\nexport function xeniaProxy(request: NextRequest, opts: XeniaProxyOptions = {}): NextResponse {\n const name = visitorCookieName(opts.prefix);\n const existing = request.cookies.get(name)?.value;\n const visitorId = existing ?? generateUlid();\n\n const forwarded = new Headers(opts.headers ?? request.headers);\n forwarded.set(URL_HEADER, request.url);\n if (!existing) {\n // NextRequest.cookies.set doesn't rewrite the forwarded Cookie header, and cookies() reads that.\n const header = forwarded.get(\"cookie\");\n forwarded.set(\"cookie\", header ? `${header}; ${name}=${visitorId}` : `${name}=${visitorId}`);\n }\n\n const response = NextResponse.next({ request: { headers: forwarded } });\n if (!existing) response.cookies.set(name, visitorId, cookieAttributes(opts.cookie));\n return response;\n}\n\n/**\n * A `XeniaServer` bound to this request's visitor (the cookie `xeniaProxy()` set) and URL context\n * (path, query, UTMs, referrer). For Server Components, Server Actions and Route Handlers.\n * `config.visitorId`/`config.context` still win when passed.\n */\nexport async function xeniaServerFromNext(config: XeniaConfig): Promise<XeniaServer> {\n const [jar, h] = await Promise.all([cookies(), headers()]);\n const name = visitorCookieName(config.prefix);\n let visitorId = config.visitorId ?? jar.get(name)?.value;\n if (!visitorId) {\n visitorId = generateUlid();\n try {\n jar.set(name, visitorId, cookieAttributes());\n } catch {\n // Expected during a Server Component render; xeniaProxy() is the place that persists it.\n }\n }\n return createXeniaServer({\n ...config,\n visitorId,\n context: { ...contextFromUrl(h.get(URL_HEADER), h.get(\"referer\")), ...config.context },\n });\n}\n\n/** This request's consent, from the cookie the browser client's `consent()` (or `consentCookie()`) wrote. */\nexport async function consentFromNext(prefix?: string): Promise<ConsentState> {\n return getConsent((await headers()).get(\"cookie\"), prefix);\n}\n\n// ── CMS page personalization ──────────────────────────────────────────────────\n\n// One store per server request (React.cache scopes it to the render), so every section that asks\n// for the same page shares ONE personalization run. Falls back to \"no sharing\" outside React\n// Server Components, where `cache` is unavailable.\nconst requestStore: () => Map<string, Promise<unknown>> =\n typeof (React as { cache?: unknown }).cache === \"function\"\n ? (React as unknown as { cache: <F extends () => unknown>(fn: F) => F }).cache(() => new Map())\n : () => new Map();\n\n/**\n * Run `personalize` at most once per request for `key`, and share the promise. Lets a page shell\n * render immediately while each personalized section awaits the same run inside its own\n * `<Suspense>`, streaming in as soon as it's ready:\n *\n * ```tsx\n * const run = (ref) => personalizeOnce(`page:${ref}`, async () =>\n * (await xeniaServerFromNext(config)).personalizePage(ref, { cms, timeoutMs: 800 }));\n *\n * export default async function Page({ params }) {\n * const xenia = await xeniaServerFromNext(config);\n * const base = await xenia.personalizePage(ref, { cms, maxRounds: 0 }); // CMS only, no Xenia call\n * const pending = new Set(pendingTargets(base.content, cms)); // from the markers\n * return base.content.fields.sections.map((s) =>\n * pending.has(s.sys.id) ? (\n * <Suspense key={s.sys.id} fallback={<SectionSkeleton />}> // never base content that will change\n * <PersonalizedSection id={s.sys.id} run={() => run(ref)} />\n * </Suspense>\n * ) : (\n * <Section key={s.sys.id} entry={s} />\n * ));\n * }\n * ```\n *\n * To finish deeper levels in the browser instead, pass `maxRounds: 1` here and hand the result's\n * `content` to `usePersonalizedContent` — already-decided entries are not asked about again.\n */\nexport function personalizeOnce<T>(key: string, personalize: () => Promise<T>): Promise<T> {\n const store = requestStore();\n let run = store.get(key) as Promise<T> | undefined;\n if (!run) {\n run = personalize();\n store.set(key, run);\n }\n return run;\n}\n"]}
|
|
@@ -357,6 +357,41 @@ interface ResolveOptions {
|
|
|
357
357
|
*/
|
|
358
358
|
context?: EventContext;
|
|
359
359
|
}
|
|
360
|
+
/** How a personalized entry's replacement is spliced into the page. */
|
|
361
|
+
type EntrySwapOp = "replace" | "expand" | "items";
|
|
362
|
+
/** CMS-neutral filter of a content-type query; the adapter maps it to its CMS syntax. */
|
|
363
|
+
interface EntryFilter {
|
|
364
|
+
field: string;
|
|
365
|
+
op: "eq" | "ne" | "in" | "nin" | "exists" | "lt" | "lte" | "gt" | "gte" | "match";
|
|
366
|
+
/** Already rendered from the visitor data by the backend; comma-separated for `in`/`nin`. */
|
|
367
|
+
value: string;
|
|
368
|
+
}
|
|
369
|
+
/** "Entries of this content type", run by the adapter against the tenant's own delivery API. */
|
|
370
|
+
interface EntryQuery {
|
|
371
|
+
contentType: string;
|
|
372
|
+
filters?: EntryFilter[];
|
|
373
|
+
/** CMS order expression, e.g. `-sys.updatedAt`. */
|
|
374
|
+
order?: string;
|
|
375
|
+
limit: number;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Xenia's decision for one personalized entry: splice `ids` (explicit entries) or the result of
|
|
379
|
+
* `query` at `target`. A requested target with no decision keeps its base content.
|
|
380
|
+
*/
|
|
381
|
+
interface EntryDecision {
|
|
382
|
+
target: string;
|
|
383
|
+
op: EntrySwapOp;
|
|
384
|
+
/** Array field replaced by `op: "items"`; absent = merge the replacement's array fields. */
|
|
385
|
+
field?: string;
|
|
386
|
+
ids?: string[];
|
|
387
|
+
query?: EntryQuery;
|
|
388
|
+
contentKey: string;
|
|
389
|
+
bucket?: string;
|
|
390
|
+
}
|
|
391
|
+
/** Response of `POST /api/personalization/resolve-entries`. */
|
|
392
|
+
interface ResolveEntriesResult {
|
|
393
|
+
decisions: EntryDecision[];
|
|
394
|
+
}
|
|
360
395
|
/** Body posted to `POST /api/events/v1/identify` — binds visitor to a known user. */
|
|
361
396
|
interface IdentifyRequest {
|
|
362
397
|
xeniaVisitorId: string;
|
|
@@ -580,81 +615,129 @@ type XeniaAction = {
|
|
|
580
615
|
};
|
|
581
616
|
|
|
582
617
|
/**
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
618
|
+
* CMS page personalization — walk a fetched CMS page breadth-first, swap the
|
|
619
|
+
* entries Xenia marked as personalized, and do it in as few round trips as
|
|
620
|
+
* the page's nesting allows.
|
|
621
|
+
*
|
|
622
|
+
* Rules (see the "Rounds" section of the feature plan):
|
|
623
|
+
* - **Static** targets carry their replacement in the payload (`xeniaVariants`)
|
|
624
|
+
* and are swapped inline — no network at all.
|
|
625
|
+
* - **Runtime** targets are a barrier: their id is collected, their subtree is
|
|
626
|
+
* NOT walked (it may be replaced). One `resolve-entries` call decides every
|
|
627
|
+
* target found in a round, one parallel CMS fetch loads every replacement,
|
|
628
|
+
* and the next round walks whatever now sits at each position.
|
|
629
|
+
* - Never throws: on error or when `timeoutMs` runs out, whatever is not yet
|
|
630
|
+
* decided keeps its base content.
|
|
631
|
+
*
|
|
632
|
+
* CMS specifics live behind {@link CmsAdapter}; built-in adapters ship as
|
|
633
|
+
* `@xenia-martech/sdk-js/cms/contentful` (and later `/cms/contentstack`). Any
|
|
634
|
+
* other CMS works by implementing the interface.
|
|
587
635
|
*/
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
*/
|
|
612
|
-
webhook(triggerId: string, data?: JsonValue, method?: "GET" | "POST"): Promise<WebhookResponse>;
|
|
613
|
-
/**
|
|
614
|
-
* Widget form submission — `POST /widget-submissions?ref={bindingRef}`. Used whenever the
|
|
615
|
-
* action's `formSubmit.requiresResolve` is true (spin-wheel, which must have a
|
|
616
|
-
* server-resolved outcome before anything renders) or `mode === "endpoint"` — the common
|
|
617
|
-
* plain-popup case (`mode === "journey" && !requiresResolve`) bypasses this entirely and calls
|
|
618
|
-
* {@link webhook} against `flowRef` instead (see `XeniaClient.submitWidgetForm`'s endpoint
|
|
619
|
-
* selection).
|
|
620
|
-
*
|
|
621
|
-
* Not retried (`noRetry: true`), styled like {@link webhook} — a retried POST must never
|
|
622
|
-
* double-fire a journey or double-resolve a prize server-side.
|
|
623
|
-
*/
|
|
624
|
-
submitWidget(ref: string, body?: JsonValue): Promise<WidgetSubmitResponse>;
|
|
625
|
-
/**
|
|
626
|
-
* Unified event batch (max 100/request per the backend) — `POST /api/events/v1`.
|
|
627
|
-
* Records analytics for every event and matches/advances a journey for any
|
|
628
|
-
* event with `trigger: true`. Replaces the old `sendEvent()` (journey trigger)
|
|
629
|
-
* and `sendAnalytics()` (CDP) calls.
|
|
630
|
-
*/
|
|
631
|
-
sendEvents(events: XeniaEvent[], opts?: {
|
|
632
|
-
keepalive?: boolean;
|
|
633
|
-
}): Promise<AnalyticsIngestResponse>;
|
|
634
|
-
/** Bind visitor→user — `POST /api/events/v1/identify`. */
|
|
635
|
-
identify(body: IdentifyRequest): Promise<IdentifyResponse>;
|
|
636
|
+
|
|
637
|
+
/** Any value inside a CMS payload — the walker is shape-agnostic; adapters give it meaning. */
|
|
638
|
+
type Node = any;
|
|
639
|
+
/** The parsed `xenia` marker field of a personalized entry. */
|
|
640
|
+
interface XeniaMarker {
|
|
641
|
+
kind: "static" | "runtime";
|
|
642
|
+
op?: EntrySwapOp;
|
|
643
|
+
/** Array field replaced by `op: "items"`. */
|
|
644
|
+
field?: string;
|
|
645
|
+
limit?: number;
|
|
646
|
+
}
|
|
647
|
+
/** What the page resolver needs to know about a CMS's entries. `N` is the entry shape. */
|
|
648
|
+
interface CmsAdapter<N extends object = Node> {
|
|
649
|
+
/** The entry id when `value` is a CMS entry (not a link stub, asset or plain object). */
|
|
650
|
+
entryId(value: unknown): string | undefined;
|
|
651
|
+
/** The entry's Xenia marker, or null when it is not personalized. */
|
|
652
|
+
marker(entry: N): XeniaMarker | null;
|
|
653
|
+
/** Static replacements pinned on the entry (its resolved `xeniaVariants`). */
|
|
654
|
+
staticVariants(entry: N): N[];
|
|
655
|
+
getField(entry: N, field: string): unknown;
|
|
656
|
+
setField(entry: N, field: string, value: unknown): void;
|
|
657
|
+
/** Legacy `items` without a field: copy the replacement's array fields onto the entry. */
|
|
658
|
+
mergeItems?(entry: N, replacement: N): void;
|
|
636
659
|
/**
|
|
637
|
-
*
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
* one is sent as an explicit `false` rather than left out: a partial body like
|
|
641
|
-
* `consent({ analytics: true })` — the shape most callers actually write — is
|
|
642
|
-
* otherwise rejected with a 400 and the consent record is never stored.
|
|
660
|
+
* Remove the marker from an entry that has been dealt with (kept base, or had its items replaced),
|
|
661
|
+
* so personalizing the output again — e.g. the browser continuing a server render — never decides
|
|
662
|
+
* it twice. Optional; without it a re-run re-asks Xenia about those entries.
|
|
643
663
|
*/
|
|
644
|
-
|
|
664
|
+
clearMarker?(entry: N): void;
|
|
665
|
+
/** Normalize a fetched payload before walking (e.g. resolve links). Defaults to identity. */
|
|
666
|
+
prepare?(payload: unknown): unknown;
|
|
667
|
+
/** Load entries by id in ONE call; order does not matter. */
|
|
668
|
+
fetchEntries(ids: string[], locale?: string): Promise<N[]>;
|
|
669
|
+
/** Run a content-type query in ONE call, in the query's order. */
|
|
670
|
+
queryEntries(query: EntryQuery, locale?: string): Promise<N[]>;
|
|
645
671
|
/**
|
|
646
|
-
*
|
|
647
|
-
* `
|
|
648
|
-
* token (same credential as events; resolve is auth-required). `merge=true`
|
|
649
|
-
* returns server-spliced `content`; otherwise splice `swaps` client-side.
|
|
672
|
+
* Load one page with its whole tree (links resolved), or null when there is none. Optional; it
|
|
673
|
+
* is what lets `personalizePage(ref)` do the fetch, so apps never fetch the page themselves.
|
|
650
674
|
*/
|
|
651
|
-
|
|
675
|
+
fetchPage?(ref: PageRef, locale?: string): Promise<N | null>;
|
|
676
|
+
}
|
|
677
|
+
/** Identifies a page in the CMS: its entry id, or a content type plus exact field values (a slug). */
|
|
678
|
+
type PageRef = string | {
|
|
679
|
+
contentType: string;
|
|
680
|
+
fields: Record<string, string | number | boolean>;
|
|
681
|
+
};
|
|
682
|
+
/** Resolves a round's runtime targets — `XeniaApi.resolveEntries` bound to the visitor. */
|
|
683
|
+
type EntryResolver = (entryIds: string[]) => Promise<EntryDecision[]>;
|
|
684
|
+
/** Reported for every swap actually applied (for impressions / analytics). */
|
|
685
|
+
interface AppliedSwap {
|
|
686
|
+
target: string;
|
|
687
|
+
kind: "static" | "runtime";
|
|
688
|
+
op: EntrySwapOp;
|
|
689
|
+
/** Ids of the entries now at the target's position (or in its field). */
|
|
690
|
+
entryIds: string[];
|
|
691
|
+
contentKey?: string;
|
|
692
|
+
bucket?: string;
|
|
693
|
+
round: number;
|
|
694
|
+
}
|
|
695
|
+
interface PersonalizeContentOptions {
|
|
696
|
+
cms: CmsAdapter;
|
|
697
|
+
/** Decides runtime targets. Omit to apply static swaps only (zero network). */
|
|
698
|
+
resolve?: EntryResolver;
|
|
699
|
+
locale?: string;
|
|
700
|
+
/** Max network rounds (nesting depth of runtime targets). Default 3. */
|
|
701
|
+
maxRounds?: number;
|
|
702
|
+
/** Total budget for all rounds, in ms. Undecided targets keep their base. Default 300. */
|
|
703
|
+
timeoutMs?: number;
|
|
704
|
+
/** Mutate the payload in place instead of personalizing a copy. Default false. */
|
|
705
|
+
inPlace?: boolean;
|
|
706
|
+
onApplied?(swap: AppliedSwap): void;
|
|
652
707
|
/**
|
|
653
|
-
*
|
|
654
|
-
* `
|
|
655
|
-
*
|
|
708
|
+
* Called when a round starts waiting on the network (`pending` = targets being decided) and when
|
|
709
|
+
* it has been applied (`pending` empty). `content` is the live, partially personalized tree —
|
|
710
|
+
* lets a UI render level by level instead of waiting for every round.
|
|
656
711
|
*/
|
|
657
|
-
|
|
712
|
+
onProgress?(progress: {
|
|
713
|
+
content: unknown;
|
|
714
|
+
round: number;
|
|
715
|
+
pending: string[];
|
|
716
|
+
}): void;
|
|
717
|
+
}
|
|
718
|
+
/** `personalizeContent` options as taken by `XeniaClient` / `XeniaServer`, which supply `resolve`. */
|
|
719
|
+
type PagePersonalizeOptions = Omit<PersonalizeContentOptions, "resolve"> & Pick<ResolveOptions, "data" | "identity" | "context">;
|
|
720
|
+
interface PersonalizeContentResult<T> {
|
|
721
|
+
content: T;
|
|
722
|
+
applied: AppliedSwap[];
|
|
723
|
+
/** False when the budget ran out, a call failed, or `maxRounds` was hit with targets left. */
|
|
724
|
+
complete: boolean;
|
|
725
|
+
error?: unknown;
|
|
658
726
|
}
|
|
727
|
+
declare function personalizeContent<T>(payload: T, options: PersonalizeContentOptions): Promise<PersonalizeContentResult<T>>;
|
|
728
|
+
/**
|
|
729
|
+
* The entries Xenia still has to decide on this page — runtime targets reachable without crossing
|
|
730
|
+
* another undecided one. Synchronous and network-free (it reads the `xenia` markers), so a page can
|
|
731
|
+
* render a skeleton in exactly those places from the first frame instead of flashing base content.
|
|
732
|
+
* Nested targets inside a pending one are found once it is decided.
|
|
733
|
+
*/
|
|
734
|
+
declare function pendingTargets(payload: unknown, cms: CmsAdapter): string[];
|
|
735
|
+
/**
|
|
736
|
+
* Fetch a page through `options.cms` and personalize it — {@link personalizeContent} without the
|
|
737
|
+
* fetch in app code. Never throws: a failed fetch comes back as `content: null` with `error`; a page
|
|
738
|
+
* that does not exist as `content: null`, `complete: true`. The page fetch is not counted against
|
|
739
|
+
* `timeoutMs` — that budget is for personalization only.
|
|
740
|
+
*/
|
|
741
|
+
declare function personalizePage<T>(ref: PageRef, options: PersonalizeContentOptions): Promise<PersonalizeContentResult<T | null>>;
|
|
659
742
|
|
|
660
|
-
export { type AnalyticsContext as A, type
|
|
743
|
+
export { type AnalyticsContext as A, type WidgetPrize as B, type CmsAdapter as C, type WidgetSegment as D, type EventContext as E, type WidgetSubmitResponse as F, type WidgetType as G, type HttpMethod as H, type IdentifyRequest as I, type JsonValue as J, type XeniaAction as K, type XeniaConfig as L, type MessageCta as M, type XeniaEvent as N, pendingTargets as O, type PagePersonalizeOptions as P, personalizeContent as Q, type ResolveBatchItem as R, personalizePage as S, type TrackOptions as T, type CallerIdentity as U, type VisitorCookieOptions as V, type WebhookResponse as W, type XeniaMarker as X, type AnalyticsEventName as a, type AnalyticsIdentity as b, type AnalyticsIngestResponse as c, type AppliedSwap as d, type CircuitBreakerConfig as e, type ConsentState as f, type EntryDecision as g, type EntryFilter as h, type EntryQuery as i, type EntryResolver as j, type EntrySwapOp as k, type EventIdentity as l, type IdentifyResponse as m, type InteractionType as n, type JsonObject as o, type MessagePlacement as p, type PageRef as q, type PersonalizationSwap as r, type PersonalizeContentOptions as s, type PersonalizeContentResult as t, type ResolveBatchResult as u, type ResolveEntriesResult as v, type ResolveOptions as w, type ResolveResult as x, type RetryConfig as y, type WidgetFormSubmit as z };
|