@genlook/storefront 0.1.0

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 (60) hide show
  1. package/README.md +180 -0
  2. package/dist/analytics.d.ts +26 -0
  3. package/dist/analytics.js +7 -0
  4. package/dist/anonymous-id.d.ts +30 -0
  5. package/dist/anonymous-id.js +33 -0
  6. package/dist/client.d.ts +289 -0
  7. package/dist/client.js +465 -0
  8. package/dist/consent.d.ts +17 -0
  9. package/dist/consent.js +34 -0
  10. package/dist/create-client.d.ts +84 -0
  11. package/dist/create-client.js +131 -0
  12. package/dist/default-tracking.d.ts +64 -0
  13. package/dist/default-tracking.js +101 -0
  14. package/dist/email.d.ts +5 -0
  15. package/dist/email.js +24 -0
  16. package/dist/entities.d.ts +186 -0
  17. package/dist/entities.js +47 -0
  18. package/dist/erasure.d.ts +36 -0
  19. package/dist/erasure.js +23 -0
  20. package/dist/events.d.ts +226 -0
  21. package/dist/events.js +41 -0
  22. package/dist/fetch-transport.d.ts +65 -0
  23. package/dist/fetch-transport.js +112 -0
  24. package/dist/generation.d.ts +132 -0
  25. package/dist/generation.js +382 -0
  26. package/dist/history.d.ts +76 -0
  27. package/dist/history.js +68 -0
  28. package/dist/memory-storage.d.ts +10 -0
  29. package/dist/memory-storage.js +12 -0
  30. package/dist/pending-upload.d.ts +32 -0
  31. package/dist/pending-upload.js +13 -0
  32. package/dist/persistence.d.ts +12 -0
  33. package/dist/persistence.js +101 -0
  34. package/dist/policy.d.ts +24 -0
  35. package/dist/policy.js +45 -0
  36. package/dist/ports.d.ts +187 -0
  37. package/dist/ports.js +1 -0
  38. package/dist/public-api.d.ts +235 -0
  39. package/dist/public-api.js +1 -0
  40. package/dist/public.d.ts +33 -0
  41. package/dist/public.js +12 -0
  42. package/dist/settings.d.ts +38 -0
  43. package/dist/settings.js +70 -0
  44. package/dist/sharing.d.ts +26 -0
  45. package/dist/sharing.js +42 -0
  46. package/dist/storage-adapters.d.ts +69 -0
  47. package/dist/storage-adapters.js +82 -0
  48. package/dist/store.d.ts +9 -0
  49. package/dist/store.js +23 -0
  50. package/dist/tracker.d.ts +124 -0
  51. package/dist/tracker.js +229 -0
  52. package/dist/types.d.ts +91 -0
  53. package/dist/types.js +1 -0
  54. package/dist/upload.d.ts +123 -0
  55. package/dist/upload.js +176 -0
  56. package/dist/usage.d.ts +45 -0
  57. package/dist/usage.js +110 -0
  58. package/dist/version.d.ts +10 -0
  59. package/dist/version.js +1 -0
  60. package/package.json +30 -0
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Merchant-owned widget settings, as `GET /settings` returns them.
3
+ *
4
+ * The storefront widget reads these off its inline theme config; an SDK
5
+ * integrator has no such config, so the factory pulls them from the store. Pure
6
+ * functions over the KVStorage + Transport ports (type-only imports), so the
7
+ * module stays value-self-contained and directly testable.
8
+ */
9
+ import type { KVStorage, Transport } from "./ports";
10
+ export interface StoreSettings {
11
+ maxGenerations: number;
12
+ period: "daily" | "weekly";
13
+ emailCollectionStep: number;
14
+ loggedInCustomersOnly: boolean;
15
+ }
16
+ /** Cache window. Fresher than this and the factory issues no request at all. */
17
+ export declare const SETTINGS_CACHE_TTL_MS: number;
18
+ export declare const SETTINGS_ENDPOINT = "/settings";
19
+ /**
20
+ * Read a settings payload field by field. Per-field rather than all-or-nothing:
21
+ * the cascade is itself per-field, so a response missing a key means "no server
22
+ * opinion here", not "discard the whole thing". Returns null when nothing usable
23
+ * was found.
24
+ */
25
+ export declare function parseSettings(raw: unknown): Partial<StoreSettings> | null;
26
+ /**
27
+ * Cached settings for this store, with whether they are still inside the TTL.
28
+ * A stale entry is still returned — it seeds the state immediately while the
29
+ * background refresh runs, so an offline app keeps the last known settings.
30
+ */
31
+ export declare function loadSettings(storage: KVStorage, storeId: string, now: number): {
32
+ settings: Partial<StoreSettings>;
33
+ fresh: boolean;
34
+ } | null;
35
+ export declare function saveSettings(storage: KVStorage, storeId: string, settings: Partial<StoreSettings>, now: number): void;
36
+ /** `GET /settings`. Never rejects — a failure returns null and the cascade keeps
37
+ * whatever it already resolved. */
38
+ export declare function fetchSettings(transport: Transport): Promise<Partial<StoreSettings> | null>;
@@ -0,0 +1,70 @@
1
+ export const SETTINGS_CACHE_TTL_MS = 5 * 60 * 1000;
2
+ export const SETTINGS_ENDPOINT = "/settings";
3
+ function storageKey(storeId) {
4
+ return `tryon-settings:${storeId || "default"}`;
5
+ }
6
+ export function parseSettings(raw) {
7
+ if (!raw || typeof raw !== "object")
8
+ return null;
9
+ const r = raw;
10
+ const out = {};
11
+ if (typeof r.maxGenerations === "number" && Number.isFinite(r.maxGenerations)) {
12
+ out.maxGenerations = r.maxGenerations;
13
+ }
14
+ if (typeof r.emailCollectionStep === "number" && Number.isFinite(r.emailCollectionStep)) {
15
+ out.emailCollectionStep = r.emailCollectionStep;
16
+ }
17
+ if (r.period === "daily" || r.period === "weekly")
18
+ out.period = r.period;
19
+ if (typeof r.loggedInCustomersOnly === "boolean") {
20
+ out.loggedInCustomersOnly = r.loggedInCustomersOnly;
21
+ }
22
+ return Object.keys(out).length > 0 ? out : null;
23
+ }
24
+ export function loadSettings(storage, storeId, now) {
25
+ let raw;
26
+ try {
27
+ raw = storage.getItem(storageKey(storeId));
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ if (!raw)
33
+ return null;
34
+ let parsed;
35
+ try {
36
+ parsed = JSON.parse(raw);
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ if (!parsed || typeof parsed !== "object")
42
+ return null;
43
+ const record = parsed;
44
+ if (typeof record.ts !== "number" || !Number.isFinite(record.ts))
45
+ return null;
46
+ const settings = parseSettings(record.settings);
47
+ if (!settings)
48
+ return null;
49
+ const age = now - record.ts;
50
+ return { settings, fresh: age >= 0 && age < SETTINGS_CACHE_TTL_MS };
51
+ }
52
+ export function saveSettings(storage, storeId, settings, now) {
53
+ try {
54
+ const record = { settings, ts: now };
55
+ storage.setItem(storageKey(storeId), JSON.stringify(record));
56
+ }
57
+ catch {
58
+ }
59
+ }
60
+ export async function fetchSettings(transport) {
61
+ try {
62
+ const response = await transport.fetch(SETTINGS_ENDPOINT, { method: "GET" });
63
+ if (!response.ok)
64
+ return null;
65
+ return parseSettings(await response.json());
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
@@ -0,0 +1,26 @@
1
+ import type { Transport } from "./ports";
2
+ import type { ShareGenerationResponse } from "./types";
3
+ import type { Store } from "./store";
4
+ import type { CoreState } from "./entities";
5
+ export declare function createShareLink(transport: Transport, generationId: string, effectiveDomain?: string, productUrl?: string): Promise<ShareGenerationResponse>;
6
+ export interface ShareEngineDeps {
7
+ transport: Transport;
8
+ store: Store<CoreState>;
9
+ /** Emits tryon:share_link_created (bound to analytics by the client). Absent on
10
+ * the short-circuit path — a cached hit creates no link, so nothing fires. */
11
+ emitShareLinkCreated?: (input: {
12
+ generationId: string;
13
+ entryId: string;
14
+ }) => void;
15
+ }
16
+ export interface GetShareUrlOptions {
17
+ effectiveDomain?: string;
18
+ productUrl?: string;
19
+ }
20
+ /**
21
+ * Resolve the share URL for a history entry: short-circuit on a cached/prebaked
22
+ * `entry.shareUrl` (zero network — scripted demo entries rely on this), else
23
+ * create a share link for its generation and write the URL back into the entry
24
+ * (write-through cache, so the next share is free).
25
+ */
26
+ export declare function getShareUrl(deps: ShareEngineDeps, entryId: string, opts?: GetShareUrlOptions): Promise<string>;
@@ -0,0 +1,42 @@
1
+ export async function createShareLink(transport, generationId, effectiveDomain, productUrl) {
2
+ try {
3
+ const endpoint = `/try-ons/${encodeURIComponent(generationId)}/share`;
4
+ console.log("Creating share link at:", endpoint);
5
+ const response = await transport.fetch(endpoint, {
6
+ method: "POST",
7
+ headers: { "Content-Type": "application/json" },
8
+ body: JSON.stringify({
9
+ effectiveDomain: effectiveDomain || undefined,
10
+ productUrl: productUrl || undefined,
11
+ }),
12
+ });
13
+ if (!response.ok) {
14
+ const errorText = await response.text();
15
+ throw new Error(`Failed to create share link: ${response.status} ${response.statusText}. ${errorText}`);
16
+ }
17
+ const result = (await response.json());
18
+ console.log("Share link created:", result.shareUrl);
19
+ return result;
20
+ }
21
+ catch (error) {
22
+ if (error instanceof Error)
23
+ throw error;
24
+ throw new Error(`Unexpected error: ${error}`);
25
+ }
26
+ }
27
+ export async function getShareUrl(deps, entryId, opts = {}) {
28
+ const entry = deps.store.getState().history.find((r) => r.id === entryId);
29
+ if (!entry)
30
+ throw new Error(`Unknown history entry: ${entryId}`);
31
+ if (entry.shareUrl)
32
+ return entry.shareUrl;
33
+ if (!entry.generationId)
34
+ throw new Error("Generation ID is required to create share link");
35
+ const response = await createShareLink(deps.transport, entry.generationId, opts.effectiveDomain, opts.productUrl);
36
+ deps.store.setState((s) => ({
37
+ ...s,
38
+ history: s.history.map((r) => r.id === entryId ? { ...r, shareUrl: response.shareUrl } : r),
39
+ }));
40
+ deps.emitShareLinkCreated?.({ generationId: entry.generationId, entryId });
41
+ return response.shareUrl;
42
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Concrete {@link KVStorage} adapters shipped with the core, so an integrator
3
+ * holding a publishable key does not have to write one.
4
+ *
5
+ * {@link KVStorage} is deliberately SYNCHRONOUS (`getItem` returns
6
+ * `string | null`, not a promise): the core reads usage, jobs, history and
7
+ * consent during construction, and making the port async would turn every
8
+ * domain call into a promise chain. Hosts whose only store is async (React
9
+ * Native AsyncStorage) get {@link createHydratedStorage} instead.
10
+ */
11
+ import type { KVStorage } from "./ports";
12
+ /** The `localStorage`/`sessionStorage` shape. Declared locally because the core
13
+ * tsconfig keeps "dom" out of `lib` (the headlessness guard). */
14
+ export interface WebStorageLike {
15
+ getItem(key: string): string | null;
16
+ setItem(key: string, value: string): void;
17
+ removeItem(key: string): void;
18
+ }
19
+ /** True when a usable `localStorage` is present (absent in RN, and access can
20
+ * throw outright in a partitioned/blocked-cookies browser context). */
21
+ export declare function isLocalStorageAvailable(): boolean;
22
+ /**
23
+ * Browser adapter over `localStorage` (or any storage passed in — a namespaced
24
+ * wrapper, `sessionStorage`, a test double). Throws when no storage is
25
+ * reachable, so a misconfigured host fails loudly instead of silently losing
26
+ * every shopper's history on reload.
27
+ */
28
+ export declare function localStorageAdapter(store?: WebStorageLike): KVStorage;
29
+ /**
30
+ * Minimal async key/value store — the shape of React Native's AsyncStorage, so
31
+ * the core can be handed one without depending on any mobile package. Either
32
+ * `multiGet` or `getItem` is enough for hydration; `multiGet` is preferred (one
33
+ * bridge round-trip instead of N).
34
+ */
35
+ export interface AsyncKVStore {
36
+ getAllKeys(): Promise<readonly string[]>;
37
+ multiGet?(keys: readonly string[]): Promise<readonly (readonly [string, string | null])[]>;
38
+ getItem?(key: string): Promise<string | null>;
39
+ setItem(key: string, value: string): Promise<void>;
40
+ removeItem(key: string): Promise<void>;
41
+ }
42
+ export interface HydratedStorageOptions {
43
+ /** Hydrate only keys starting with this prefix (skips the host app's own
44
+ * keys). Omit to preload everything the store holds. */
45
+ keyPrefix?: string;
46
+ /** Called when a write-through fails. Default: `console.warn`. Never fatal —
47
+ * the in-memory value already took effect. */
48
+ onWriteError?: (error: unknown, key: string) => void;
49
+ }
50
+ /**
51
+ * Async store → synchronous {@link KVStorage}, by preloading it into memory.
52
+ *
53
+ * Reads are served from the in-memory snapshot; writes go to memory
54
+ * synchronously AND are mirrored to the async store (write-through). A failed
55
+ * mirror is reported and swallowed: the session keeps working, only durability
56
+ * is lost for that key.
57
+ *
58
+ * IMPORTANT: the returned storage only reflects persisted state once this
59
+ * promise resolves. Await it before constructing the client — a client built on
60
+ * a not-yet-hydrated store starts from empty state and would mint a second
61
+ * anonymous id.
62
+ */
63
+ export declare function createHydratedStorage(store: AsyncKVStore, options?: HydratedStorageOptions): Promise<KVStorage>;
64
+ /**
65
+ * Best-effort default storage: `localStorage` when the runtime has one,
66
+ * otherwise an in-memory store (state lasts the session only). Used by
67
+ * {@link createTryOnClient} when no `storage` is supplied.
68
+ */
69
+ export declare function defaultStorage(): KVStorage;
@@ -0,0 +1,82 @@
1
+ import { memoryStorage } from "./memory-storage";
2
+ function globalLocalStorage() {
3
+ return globalThis.localStorage;
4
+ }
5
+ export function isLocalStorageAvailable() {
6
+ try {
7
+ const store = globalLocalStorage();
8
+ if (!store)
9
+ return false;
10
+ const probe = "__genlook_probe__";
11
+ store.setItem(probe, "1");
12
+ store.removeItem(probe);
13
+ return true;
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
19
+ export function localStorageAdapter(store) {
20
+ const target = store ?? globalLocalStorage();
21
+ if (!target) {
22
+ throw new Error("[Genlook] localStorage is not available in this runtime. Pass a `storage` adapter (see createHydratedStorage for React Native).");
23
+ }
24
+ return {
25
+ getItem: (key) => target.getItem(key),
26
+ setItem: (key, value) => target.setItem(key, String(value)),
27
+ removeItem: (key) => target.removeItem(key),
28
+ };
29
+ }
30
+ export async function createHydratedStorage(store, options = {}) {
31
+ const memory = memoryStorage();
32
+ const onWriteError = options.onWriteError ??
33
+ ((error, key) => {
34
+ console.warn("[Genlook] Storage write failed:", key, error);
35
+ });
36
+ const allKeys = await store.getAllKeys();
37
+ const keys = options.keyPrefix
38
+ ? allKeys.filter((key) => key.startsWith(options.keyPrefix))
39
+ : allKeys;
40
+ if (keys.length > 0) {
41
+ if (typeof store.multiGet === "function") {
42
+ for (const [key, value] of await store.multiGet(keys)) {
43
+ if (value != null)
44
+ memory.setItem(key, value);
45
+ }
46
+ }
47
+ else if (typeof store.getItem === "function") {
48
+ const getItem = store.getItem.bind(store);
49
+ const values = await Promise.all(keys.map((key) => getItem(key)));
50
+ keys.forEach((key, i) => {
51
+ const value = values[i];
52
+ if (value != null)
53
+ memory.setItem(key, value);
54
+ });
55
+ }
56
+ else {
57
+ throw new Error("[Genlook] AsyncKVStore must implement multiGet or getItem to hydrate.");
58
+ }
59
+ }
60
+ return {
61
+ getItem: (key) => memory.getItem(key),
62
+ setItem: (key, value) => {
63
+ const str = String(value);
64
+ memory.setItem(key, str);
65
+ void Promise.resolve()
66
+ .then(() => store.setItem(key, str))
67
+ .catch((error) => onWriteError(error, key));
68
+ },
69
+ removeItem: (key) => {
70
+ memory.removeItem(key);
71
+ void Promise.resolve()
72
+ .then(() => store.removeItem(key))
73
+ .catch((error) => onWriteError(error, key));
74
+ },
75
+ };
76
+ }
77
+ export function defaultStorage() {
78
+ if (isLocalStorageAvailable())
79
+ return localStorageAdapter();
80
+ console.warn("[Genlook] No localStorage available — falling back to in-memory storage. Try-on history and quota will not survive a reload.");
81
+ return memoryStorage();
82
+ }
@@ -0,0 +1,9 @@
1
+ export type Listener = () => void;
2
+ /** Minimal observable store. Zero dependencies. */
3
+ export interface Store<S> {
4
+ getState(): S;
5
+ subscribe(listener: Listener): () => void;
6
+ /** Replace state with the updater's result; notifies listeners only on identity change. */
7
+ setState(updater: (prev: S) => S): void;
8
+ }
9
+ export declare function createStore<S>(initial: S): Store<S>;
package/dist/store.js ADDED
@@ -0,0 +1,23 @@
1
+ export function createStore(initial) {
2
+ let state = initial;
3
+ const listeners = new Set();
4
+ return {
5
+ getState() {
6
+ return state;
7
+ },
8
+ subscribe(listener) {
9
+ listeners.add(listener);
10
+ return () => {
11
+ listeners.delete(listener);
12
+ };
13
+ },
14
+ setState(updater) {
15
+ const next = updater(state);
16
+ if (next === state)
17
+ return;
18
+ state = next;
19
+ for (const listener of [...listeners])
20
+ listener();
21
+ },
22
+ };
23
+ }
@@ -0,0 +1,124 @@
1
+ import type { Transport, ConsentSource, PageContext } from "./ports";
2
+ /** Periodic-flush cadence (ms). The batch queue is core-owned; the DOM timer that
3
+ * ticks at this interval is registered by the host runtime. */
4
+ export declare const FLUSH_INTERVAL_MS = 5000;
5
+ /** Minimal failed-response shape captureFetchError reads. The browser `Response`
6
+ * (passed by the HttpClient) structurally satisfies it — the core needs no dom. */
7
+ interface FetchErrorResponse {
8
+ status: number;
9
+ text(): Promise<string>;
10
+ headers: {
11
+ forEach(cb: (value: string, key: string) => void): void;
12
+ };
13
+ }
14
+ export interface TrackerDeps {
15
+ transport: Transport;
16
+ consent: ConsentSource;
17
+ pageContext: PageContext;
18
+ /** Secure uuid factory (host-provided; the core takes no crypto dependency). */
19
+ uuid: () => string;
20
+ /** Injectable clock (tests). Defaults to Date.now. */
21
+ now?: () => number;
22
+ }
23
+ /**
24
+ * The single tracking pipeline. `capture`/`captureFetchError` and the property
25
+ * name `tracking` are the frozen back-compat surface consumed by widget UI code,
26
+ * so both keep their exact signatures.
27
+ */
28
+ export declare class Tracker {
29
+ private queue;
30
+ private pageviewId;
31
+ private lastPathname;
32
+ private isFlushing;
33
+ private readonly isBot;
34
+ private consentSnapshot;
35
+ private consentSubscribed;
36
+ private uiVariant;
37
+ private uiBuild;
38
+ /** Widget-scope consent override (see grantWidgetScopeConsent). */
39
+ private widgetScopeConsent;
40
+ /** In-memory identity fallbacks, minted on first need if the host can't supply
41
+ * persisted ids. Ensures a batch is NEVER built without an anonymous_id +
42
+ * session_id (the backend now requires both). Prefixed to match the ^anon_/
43
+ * ^sess_ wire contract; stable for the tracker's lifetime once minted. */
44
+ private fallbackAnonymousId;
45
+ private fallbackSessionId;
46
+ private readonly transport;
47
+ private readonly consent;
48
+ private readonly pageContext;
49
+ private readonly uuid;
50
+ private readonly now;
51
+ private static readonly MAX_QUEUE_SIZE;
52
+ private static readonly EARLY_FLUSH_SIZE;
53
+ constructor(deps: TrackerDeps);
54
+ /** Stamp the active experiment variant onto every subsequent batch context
55
+ * (`ui_variant`). Additive, BQ-safe; pass null to clear. */
56
+ setUiVariant(variant: string | null): void;
57
+ /** Stamp `ui_version` onto every subsequent batch context. Called by the UI
58
+ * bundle once it loads, reporting its OWN content hash — the boot's
59
+ * `app_version` cannot, because the UI bundle is lazy-loaded and hashed
60
+ * separately, so a shopper can run a new boot against a cached older UI. */
61
+ setUiBuild(build: string | null): void;
62
+ /** Event-category prefixes that a widget-scope consent grant unlocks while SITE
63
+ * analytics consent is denied. product_page:* is deliberately absent — it
64
+ * stays dropped until site consent is (re)granted. */
65
+ private static readonly WIDGET_SCOPE_PREFIXES;
66
+ /** SITE-LEVEL analytics gate. Public so the experiment layer can tell whether
67
+ * an exposure would be silently dropped before emitting it. Only an explicit
68
+ * analytics=false snapshot blocks; true / null / undefined / no consent source
69
+ * all allow (the Shopify Customer Privacy snapshot encodes regional rules). */
70
+ isTrackingAllowed(): boolean;
71
+ /**
72
+ * Grant the widget-scope consent override. After the shopper accepts the
73
+ * widget's own legal-consent gate, widget:/tryon:/sheet:/api: events ARE
74
+ * captured and sent (with full identity) even while SITE analytics consent is
75
+ * denied; product_page:* events stay dropped. Idempotent. The grant is
76
+ * re-derived on reload from the persisted legal-consent record (TryOnClient),
77
+ * so it survives a page refresh.
78
+ */
79
+ grantWidgetScopeConsent(): void;
80
+ /** Per-event gate: site consent allows everything; otherwise only widget-scope
81
+ * events pass, and only once the widget-scope grant is set. */
82
+ private isEventAllowed;
83
+ capture(event: string, properties?: Record<string, unknown>): void;
84
+ captureFetchError(url: string, method: string, error: Error | FetchErrorResponse): Promise<void>;
85
+ /**
86
+ * Drain the queue to /events. `beacon` requests unload semantics
87
+ * (sendBeacon / keepalive) from the transport. One path for both the periodic
88
+ * flush and the page-hide flush — the wire body is identical either way.
89
+ */
90
+ flush(opts?: {
91
+ beacon?: boolean;
92
+ }): Promise<void>;
93
+ private enqueue;
94
+ /**
95
+ * Read the consent snapshot once and subscribe to changes. Cheap: the snapshot
96
+ * is cached and refreshed via the change hook, so batches never trigger a
97
+ * per-event CMP call. Idempotent — safe to call every capture/flush.
98
+ */
99
+ private ensureConsentSubscribed;
100
+ /**
101
+ * On explicit consent deny (`analytics === "no"`), drop the two tracking-owned
102
+ * storage keys and the cached session id via the PageContext port. Functional
103
+ * `genlook-*` storage (quota counters, photo history) sits on a different legal
104
+ * basis and is deliberately left untouched (the port only removes those keys).
105
+ */
106
+ private handleConsentChange;
107
+ /**
108
+ * Assemble the batch context. Every sent batch is fully identified (the gate
109
+ * already dropped denied captures), so identity + attribution + referrer always
110
+ * ride. The pageview id is a plain in-memory value, re-minted when the
111
+ * pathname changes.
112
+ */
113
+ private buildContext;
114
+ /**
115
+ * Read the host identity context, guaranteeing a non-empty anonymous_id +
116
+ * session_id. Normally the host supplies persisted ids; if that read throws
117
+ * (storage unavailable) or returns empty ids, in-memory fallbacks fill in so a
118
+ * fully-identified batch is still built — a batch is never sent without ids.
119
+ */
120
+ private readIdentityContext;
121
+ private getFallbackAnonymousId;
122
+ private getFallbackSessionId;
123
+ }
124
+ export {};