@drip-apex/app-sdk 0.0.0-stage → 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.
package/dist/index.cjs ADDED
@@ -0,0 +1,1217 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ Apex: () => Apex,
24
+ ApexClient: () => ApexClient
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+
28
+ // package.json
29
+ var version = "0.1.0";
30
+
31
+ // src/index.ts
32
+ var localStorageAdapter = {
33
+ async get(key) {
34
+ if (typeof globalThis.localStorage === "undefined") throw new Error("Storage unavailable");
35
+ return globalThis.localStorage.getItem(key);
36
+ },
37
+ async set(key, value) {
38
+ if (typeof globalThis.localStorage === "undefined") throw new Error("Storage unavailable");
39
+ globalThis.localStorage.setItem(key, value);
40
+ }
41
+ };
42
+ function detectPlatform() {
43
+ var _a, _b, _c;
44
+ const shell = globalThis;
45
+ try {
46
+ const platform = (_b = (_a = shell.Capacitor) == null ? void 0 : _a.getPlatform) == null ? void 0 : _b.call(_a);
47
+ if (platform === "ios" || platform === "android" || platform === "web") return platform;
48
+ } catch {
49
+ }
50
+ const cordova = typeof ((_c = shell.device) == null ? void 0 : _c.platform) === "string" ? shell.device.platform.toLowerCase() : void 0;
51
+ if (cordova === "ios" || cordova === "android") return cordova;
52
+ const agent = typeof navigator === "undefined" ? "" : navigator.userAgent;
53
+ return /iPhone|iPad|iPod/i.test(agent) ? "ios" : /Android/i.test(agent) ? "android" : "web";
54
+ }
55
+ var isRow = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
56
+ var string = (v) => typeof v === "string" ? v : void 0;
57
+ var safeNumber = (v) => typeof v === "number" && Number.isFinite(v) && (!Number.isInteger(v) || Number.isSafeInteger(v));
58
+ var clamp = (v, fallback, min, max) => typeof v === "number" && Number.isFinite(v) ? Math.min(max, Math.max(min, v)) : fallback;
59
+ var SERVER_MAX_EVENTS = 200;
60
+ var clone = (v) => JSON.parse(JSON.stringify(v));
61
+ var uuid = () => {
62
+ const crypto = typeof globalThis.crypto === "undefined" ? void 0 : globalThis.crypto;
63
+ if (typeof (crypto == null ? void 0 : crypto.randomUUID) === "function") return crypto.randomUUID();
64
+ const bytes = new Uint8Array(16);
65
+ if (typeof (crypto == null ? void 0 : crypto.getRandomValues) === "function") crypto.getRandomValues(bytes);
66
+ else for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
67
+ bytes[6] = bytes[6] & 15 | 64;
68
+ bytes[8] = bytes[8] & 63 | 128;
69
+ const s = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
70
+ return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20)}`;
71
+ };
72
+ var isUuid = (s) => s !== null && /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i.test(s);
73
+ var unsafeKeys = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
74
+ var textInput = (v, max = 1024) => typeof v === "string" && v.length > 0 && v.length <= max && v.trim().length > 0;
75
+ function jsonValue(value) {
76
+ let nodes = 0, bytes = 0;
77
+ const seen = /* @__PURE__ */ new Set();
78
+ const visit = (v, depth) => {
79
+ if (++nodes > 1e4 || depth > 32) return false;
80
+ if (v === null || typeof v === "boolean") return true;
81
+ if (typeof v === "string") {
82
+ bytes += v.length;
83
+ return bytes <= 1048576;
84
+ }
85
+ if (typeof v === "number") return safeNumber(v);
86
+ if (typeof v !== "object" || seen.has(v)) return false;
87
+ if (Array.isArray(v) && v.length > 1e4) return false;
88
+ if (!Array.isArray(v) && Object.getPrototypeOf(v) !== Object.prototype && Object.getPrototypeOf(v) !== null) return false;
89
+ seen.add(v);
90
+ const descriptors = Object.getOwnPropertyDescriptors(v);
91
+ for (const [key, descriptor] of Object.entries(descriptors)) {
92
+ bytes += key.length;
93
+ if (unsafeKeys.has(key) || bytes > 1048576 || !("value" in descriptor) || !visit(descriptor.value, depth + 1)) return false;
94
+ }
95
+ seen.delete(v);
96
+ return true;
97
+ };
98
+ try {
99
+ return visit(value, 0);
100
+ } catch {
101
+ return false;
102
+ }
103
+ }
104
+ function recordInput(v) {
105
+ try {
106
+ return isRow(v) && jsonValue(v);
107
+ } catch {
108
+ return false;
109
+ }
110
+ }
111
+ function optionRecord(v) {
112
+ try {
113
+ if (!isRow(v) || Object.getPrototypeOf(v) !== Object.prototype && Object.getPrototypeOf(v) !== null) return null;
114
+ const result = {};
115
+ for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(v))) {
116
+ if (unsafeKeys.has(key) || !("value" in descriptor)) return null;
117
+ result[key] = descriptor.value;
118
+ }
119
+ return result;
120
+ } catch {
121
+ return null;
122
+ }
123
+ }
124
+ function loggerInput(v) {
125
+ try {
126
+ if (v === null || typeof v !== "object") return void 0;
127
+ const descriptor = Object.getOwnPropertyDescriptor(v, "logger");
128
+ return descriptor && "value" in descriptor && typeof descriptor.value === "function" ? descriptor.value : void 0;
129
+ } catch {
130
+ return void 0;
131
+ }
132
+ }
133
+ function flagOptions(v) {
134
+ if (v === void 0) return true;
135
+ const row = optionRecord(v);
136
+ return row !== null && Object.keys(row).every((key) => key === "exposure" || key === "validate") && (row.exposure === void 0 || row.exposure === "manual" || row.exposure === "auto") && (row.validate === void 0 || typeof row.validate === "function");
137
+ }
138
+ function configuration(v) {
139
+ const row = optionRecord(v);
140
+ if (!row || !textInput(row.publishableKey) || !row.publishableKey.startsWith("apx_pk_") || !textInput(row.shopId)) return null;
141
+ const texts = ["environment", "appVersion", "applicationId", "baseUrl"];
142
+ const numbers = ["flushIntervalSeconds", "batchSize", "queueCapacity", "queueRetentionHours", "initTimeoutMs"];
143
+ const callbacks = ["screen", "fetch", "logger"];
144
+ const keys = ["publishableKey", "shopId", "qaMode", "platform", "storage", "refreshJitterSeconds", ...texts, ...numbers, ...callbacks];
145
+ if (Object.keys(row).some((key) => !keys.includes(key))) return null;
146
+ for (const key of texts) if (row[key] !== void 0 && !textInput(row[key])) return null;
147
+ for (const key of numbers) if (row[key] !== void 0 && !safeNumber(row[key])) return null;
148
+ for (const key of callbacks) if (row[key] !== void 0 && typeof row[key] !== "function") return null;
149
+ if (row.baseUrl !== void 0) {
150
+ try {
151
+ const url = new URL(row.baseUrl);
152
+ if (!["http:", "https:"].includes(url.protocol) || !url.hostname || url.username || url.password) return null;
153
+ } catch {
154
+ return null;
155
+ }
156
+ }
157
+ if (row.qaMode !== void 0 && typeof row.qaMode !== "boolean") return null;
158
+ if (row.platform !== void 0 && !["ios", "android", "web"].includes(row.platform)) return null;
159
+ if (row.refreshJitterSeconds !== void 0) {
160
+ const jitter = row.refreshJitterSeconds;
161
+ if (!Array.isArray(jitter) || jitter.length !== 2 || !jsonValue(jitter) || !jitter.every(safeNumber) || jitter[0] < 0 || jitter[1] < jitter[0]) return null;
162
+ row.refreshJitterSeconds = [...jitter];
163
+ }
164
+ if (row.storage !== void 0) {
165
+ try {
166
+ if (!isRow(row.storage) || typeof row.storage.get !== "function" || typeof row.storage.set !== "function") return null;
167
+ } catch {
168
+ return null;
169
+ }
170
+ }
171
+ return row;
172
+ }
173
+ function screenURL(applicationId, screen) {
174
+ const host = applicationId.toLowerCase();
175
+ if (!/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/.test(host)) return "app://unknown/home";
176
+ try {
177
+ if (!textInput(screen, 4096)) return `app://${host}/home`;
178
+ return `app://${host}/${screen.replace(/^\/+|\/+$/g, "").split("/").map(encodeURIComponent).join("/")}`;
179
+ } catch {
180
+ return `app://${host}/home`;
181
+ }
182
+ }
183
+ var ApexClient = class {
184
+ constructor(options) {
185
+ this.sdkVersion = version;
186
+ this.device = "";
187
+ this.identity = "";
188
+ this.assignmentIdentity = "";
189
+ this.assignments = [];
190
+ this.assignmentsLoaded = false;
191
+ this.cache = /* @__PURE__ */ new Map();
192
+ this.fetchedAt = null;
193
+ this.fromCache = false;
194
+ this.events = [];
195
+ this.durableEvents = [];
196
+ this.queueRevision = 0;
197
+ this.durableQueueRevision = 0;
198
+ this.queueWaiters = [];
199
+ this.queueDropLogs = /* @__PURE__ */ new Set();
200
+ this.session = "";
201
+ this.backgroundAt = null;
202
+ this.appState = "foreground";
203
+ this.lastBackground = -Infinity;
204
+ this.lastForeground = -Infinity;
205
+ this.foregroundPending = false;
206
+ this.settleFirstAssignment = () => {
207
+ };
208
+ this.warnings = /* @__PURE__ */ new Set();
209
+ this.failureLogs = /* @__PURE__ */ new Map();
210
+ this.exposures = /* @__PURE__ */ new Set();
211
+ this.pendingExposures = /* @__PURE__ */ new Set();
212
+ this.handlers = /* @__PURE__ */ new Set();
213
+ this.stopped = false;
214
+ this.generation = 0;
215
+ this.identityRevision = 0;
216
+ this.identityPending = false;
217
+ this.networkingSuspended = false;
218
+ this.networkRevision = 0;
219
+ this.identityWriteWarned = false;
220
+ this.identifyWaiters = /* @__PURE__ */ new Map();
221
+ this.assignFailures = 0;
222
+ this.eventFailures = 0;
223
+ this.assignNotBefore = -Infinity;
224
+ this.eventsNotBefore = -Infinity;
225
+ this.flushing = false;
226
+ this.serial = Promise.resolve();
227
+ this.timers = /* @__PURE__ */ new Map();
228
+ this.requests = /* @__PURE__ */ new Set();
229
+ this.assignmentRequests = /* @__PURE__ */ new Set();
230
+ this.visibility = () => {
231
+ if (typeof document === "undefined") return;
232
+ if (document.visibilityState === "hidden") this.setAppState("background");
233
+ else if (document.visibilityState === "visible") this.setAppState("foreground");
234
+ };
235
+ this.pause = () => this.setAppState("background");
236
+ this.resume = () => this.setAppState("foreground");
237
+ var _a, _b, _c, _d;
238
+ const valid = configuration(options);
239
+ const invalidLogger = (_a = valid == null ? void 0 : valid.logger) != null ? _a : loggerInput(options);
240
+ options = valid != null ? valid : { publishableKey: "", shopId: "", logger: typeof invalidLogger === "function" ? invalidLogger : void 0 };
241
+ this.options = { ...options, environment: (_b = options.environment) != null ? _b : "production" };
242
+ this.storage = (_c = options.storage) != null ? _c : localStorageAdapter;
243
+ this.namespace = `apex_app_${encodeURIComponent(JSON.stringify([options.shopId, this.options.environment]))}`;
244
+ this.batchSize = Math.trunc(clamp(options.batchSize, 20, 1, 20));
245
+ this.capacity = Math.trunc(clamp(options.queueCapacity, 2e3, 1, 2e3));
246
+ this.retention = clamp(options.queueRetentionHours, 47, 0, 47) * 36e5;
247
+ this.flushInterval = clamp(options.flushIntervalSeconds, 5, 0.1, 86400);
248
+ this.platform = (_d = options.platform) != null ? _d : detectPlatform();
249
+ const firstAssignment = new Promise((resolve) => {
250
+ this.settleFirstAssignment = resolve;
251
+ });
252
+ if (!valid) {
253
+ this.stopped = true;
254
+ this.log("Apex configure: invalid options; SDK stopped");
255
+ this.ready = Promise.resolve();
256
+ return;
257
+ }
258
+ this.serial = this.initialize().catch(() => {
259
+ this.stopped = true;
260
+ this.log("Apex initialization failed");
261
+ });
262
+ this.ready = this.serial.then(() => {
263
+ if (this.stopped) return;
264
+ const timeoutMs = clamp(options.initTimeoutMs, 3e3, 0, 2147483647);
265
+ if (timeoutMs === 0) return;
266
+ return new Promise((resolve) => {
267
+ const timer = setTimeout(resolve, timeoutMs);
268
+ void firstAssignment.then(() => {
269
+ clearTimeout(timer);
270
+ resolve();
271
+ });
272
+ });
273
+ });
274
+ }
275
+ /** Drive lifecycle explicitly when the shell cannot expose its events. */
276
+ setAppState(state) {
277
+ var _a;
278
+ if (state !== "foreground" && state !== "background") {
279
+ this.log("Apex ignored invalid app state");
280
+ return;
281
+ }
282
+ if (this.stopped || state === this.appState) return;
283
+ this.appState = state;
284
+ const now = Date.now();
285
+ if (state === "background") {
286
+ (_a = this.backgroundAt) != null ? _a : this.backgroundAt = now;
287
+ if (now - this.lastBackground < 1e3) return;
288
+ this.lastBackground = now;
289
+ this.cancelTimer("foreground");
290
+ void this.task(async () => {
291
+ await this.persistQueue();
292
+ await this.flushLocked(true);
293
+ });
294
+ } else {
295
+ if (this.backgroundAt !== null && Date.now() - this.backgroundAt >= 18e5) {
296
+ this.session = uuid();
297
+ this.exposures.clear();
298
+ this.warnings.clear();
299
+ }
300
+ this.backgroundAt = null;
301
+ const deliver = now - this.lastForeground >= 1e3;
302
+ if (now - this.lastForeground < 1e3 && !this.foregroundPending) return;
303
+ if (now - this.lastForeground >= 1e3) this.lastForeground = now;
304
+ this.foregroundPending = true;
305
+ this.schedule("foreground", this.jitter(), () => {
306
+ if (this.appState === "foreground") {
307
+ this.foregroundPending = false;
308
+ void this.refresh();
309
+ }
310
+ });
311
+ if (deliver) void this.flush();
312
+ }
313
+ }
314
+ /** Durable anonymous identity for registering/looking up a QA device. */
315
+ get deviceId() {
316
+ return this.device;
317
+ }
318
+ get userId() {
319
+ return this.identity;
320
+ }
321
+ log(line) {
322
+ var _a, _b;
323
+ try {
324
+ (_b = (_a = this.options).logger) == null ? void 0 : _b.call(_a, line);
325
+ } catch {
326
+ }
327
+ }
328
+ logFailure(failureClass, line) {
329
+ if (!this.options.logger) return;
330
+ const now = Date.now();
331
+ const previous = this.failureLogs.get(failureClass);
332
+ if (previous !== void 0 && now >= previous && now - previous < 6e4) return;
333
+ if (!this.failureLogs.has(failureClass) && this.failureLogs.size >= 128) return;
334
+ this.failureLogs.set(failureClass, now);
335
+ this.log(line);
336
+ }
337
+ logDeliveryFailure(lane, reply, outcome) {
338
+ const failure = reply ? `HTTP ${reply.status}` : "network error";
339
+ this.logFailure(`${lane}:${failure}`, `Apex ${lane} failed: ${failure}; ${outcome}`);
340
+ }
341
+ async initialize() {
342
+ var _a, _b;
343
+ if (typeof document === "undefined") {
344
+ this.stopped = true;
345
+ this.log("Apex requires a browser or WebView; initialization skipped");
346
+ return;
347
+ }
348
+ try {
349
+ const saved = await this.storage.get("apex_device_id");
350
+ this.device = isUuid(saved) ? saved : uuid();
351
+ if (!isUuid(saved)) await this.storage.set("apex_device_id", this.device);
352
+ this.identity = this.device;
353
+ this.assignmentIdentity = this.device;
354
+ this.session = uuid();
355
+ } catch {
356
+ this.stopped = true;
357
+ this.log("Apex identity storage unavailable; networking stopped");
358
+ return;
359
+ }
360
+ const savedIdentity = await this.read("identity");
361
+ if (typeof savedIdentity === "string") this.identity = savedIdentity;
362
+ this.assignmentIdentity = this.identity;
363
+ const raw = await this.read("cache");
364
+ if (isRow(raw)) {
365
+ const entries = isRow(raw.entries) ? raw.entries : typeof raw.identity === "string" ? { [raw.identity]: raw } : {};
366
+ for (const [id, entry] of Object.entries(entries)) {
367
+ if (isRow(entry) && typeof entry.fetched_at === "number" && Number.isFinite(entry.fetched_at) && Array.isArray(entry.assignments) && entry.assignments.every(isRow)) {
368
+ this.cache.set(id, { fetched_at: entry.fetched_at, assignments: entry.assignments });
369
+ }
370
+ }
371
+ }
372
+ this.trimCache();
373
+ const cached = this.cache.get(this.identity);
374
+ if (cached) {
375
+ this.assignments = cached.assignments;
376
+ this.assignmentsLoaded = true;
377
+ this.fetchedAt = cached.fetched_at;
378
+ this.fromCache = true;
379
+ }
380
+ const events = await this.read("events");
381
+ const restored = Array.isArray(events) ? events.filter(isRow) : [];
382
+ if (restored.length <= this.capacity) this.events = this.durableEvents = restored;
383
+ if (!await this.prune(restored, true) && restored.length > this.capacity) {
384
+ this.stopped = true;
385
+ this.log("Apex queue restore exceeded capacity and could not be saved; networking stopped");
386
+ return;
387
+ }
388
+ if (this.stopped) return;
389
+ if (typeof document !== "undefined") {
390
+ if (document.visibilityState === "hidden") {
391
+ this.backgroundAt = Date.now();
392
+ this.appState = "background";
393
+ }
394
+ document.addEventListener("visibilitychange", this.visibility);
395
+ document.addEventListener("pause", this.pause);
396
+ document.addEventListener("resume", this.resume);
397
+ }
398
+ const app = (_b = (_a = globalThis.Capacitor) == null ? void 0 : _a.Plugins) == null ? void 0 : _b.App;
399
+ try {
400
+ if (typeof (app == null ? void 0 : app.addListener) === "function") {
401
+ void Promise.resolve(app.addListener("appStateChange", (state) => this.setAppState(state.isActive ? "foreground" : "background"))).then((listener) => {
402
+ if (this.stopped) this.removeShellListener(listener);
403
+ else this.shellListener = listener;
404
+ }).catch(() => this.log("Apex shell lifecycle listener unavailable"));
405
+ }
406
+ } catch {
407
+ this.log("Apex shell lifecycle listener unavailable");
408
+ }
409
+ this.requestAssignments();
410
+ this.scheduleFlush(this.events.length ? 0 : this.flushInterval);
411
+ }
412
+ task(action) {
413
+ this.queueActions = void 0;
414
+ const next = this.serial.then(action).catch(() => this.log("Apex operation failed"));
415
+ this.serial = next;
416
+ return next;
417
+ }
418
+ // Keep mutation order with identity/assignment operations, but let subsequent
419
+ // enqueues stage their rows while the adapter is writing. Only the caller waits
420
+ // for its durability barrier, not the shared mutation chain.
421
+ queueTask(action) {
422
+ if (!this.queueActions) {
423
+ const actions = [];
424
+ void this.task(() => {
425
+ if (this.queueActions === actions) this.queueActions = void 0;
426
+ for (const pending of actions) {
427
+ if (this.stopped) {
428
+ pending.resolve(false);
429
+ continue;
430
+ }
431
+ try {
432
+ void pending.action().then(pending.resolve, () => {
433
+ this.log("Apex operation failed");
434
+ pending.resolve(false);
435
+ });
436
+ } catch {
437
+ this.log("Apex operation failed");
438
+ pending.resolve(false);
439
+ }
440
+ }
441
+ });
442
+ this.queueActions = actions;
443
+ }
444
+ return new Promise((resolve) => this.queueActions.push({ action, resolve }));
445
+ }
446
+ persistQueue(failureLog) {
447
+ if (this.queueRevision === this.durableQueueRevision) return Promise.resolve(true);
448
+ const saved = new Promise((resolve) => this.queueWaiters.push({ revision: this.queueRevision, resolve, failureLog }));
449
+ if (!this.queueWriter) {
450
+ this.queueWriter = Promise.resolve().then(async () => {
451
+ while (this.queueWaiters.length) {
452
+ const revision = this.queueRevision, snapshot = this.events;
453
+ for (const line of this.queueDropLogs) this.log(line);
454
+ this.queueDropLogs.clear();
455
+ if (!await this.write("events", snapshot, "")) {
456
+ this.events = this.durableEvents;
457
+ this.durableQueueRevision = this.queueRevision;
458
+ const failed = this.queueWaiters.splice(0);
459
+ this.queueDropLogs.clear();
460
+ this.log("Apex could not persist events");
461
+ for (const waiter of failed) {
462
+ if (waiter.failureLog) this.log(waiter.failureLog);
463
+ waiter.resolve(false);
464
+ }
465
+ break;
466
+ }
467
+ this.durableEvents = snapshot;
468
+ this.durableQueueRevision = revision;
469
+ const completed = this.queueWaiters.filter((waiter) => waiter.revision <= revision);
470
+ this.queueWaiters = this.queueWaiters.filter((waiter) => waiter.revision > revision);
471
+ for (const waiter of completed) waiter.resolve(true);
472
+ }
473
+ this.queueWriter = void 0;
474
+ });
475
+ }
476
+ return saved;
477
+ }
478
+ replaceQueue(next, failureLog) {
479
+ this.events = next;
480
+ this.queueRevision++;
481
+ return this.persistQueue(failureLog);
482
+ }
483
+ async read(name) {
484
+ try {
485
+ const raw = await this.storage.get(`${this.namespace}-${name}.json`);
486
+ return raw === null ? null : JSON.parse(raw);
487
+ } catch {
488
+ this.log(`Apex could not load ${name}`);
489
+ return null;
490
+ }
491
+ }
492
+ async write(name, value, failureLog) {
493
+ try {
494
+ await this.storage.set(`${this.namespace}-${name}.json`, JSON.stringify(value));
495
+ return true;
496
+ } catch {
497
+ const line = failureLog != null ? failureLog : `Apex could not persist ${name}`;
498
+ if (line) this.log(line);
499
+ return false;
500
+ }
501
+ }
502
+ schedule(name, seconds, action) {
503
+ this.cancelTimer(name);
504
+ if (this.stopped || this.networkingSuspended) return;
505
+ this.timers.set(name, setTimeout(() => {
506
+ this.timers.delete(name);
507
+ if (!this.stopped) action();
508
+ }, seconds * 1e3));
509
+ }
510
+ cancelTimer(name) {
511
+ const timer = this.timers.get(name);
512
+ if (timer !== void 0) clearTimeout(timer);
513
+ this.timers.delete(name);
514
+ }
515
+ jitter() {
516
+ var _a;
517
+ const [lo, hi] = (_a = this.options.refreshJitterSeconds) != null ? _a : [0, 10];
518
+ return Number.isFinite(lo) && Number.isFinite(hi) && Number.isFinite(hi - lo) && hi >= lo ? Math.max(0, lo + Math.random() * (hi - lo)) : 0;
519
+ }
520
+ emit(change) {
521
+ for (const handler of [...this.handlers]) {
522
+ try {
523
+ handler(clone(change));
524
+ } catch {
525
+ this.log("Apex listener failed");
526
+ }
527
+ }
528
+ }
529
+ onChange(handler) {
530
+ if (typeof handler !== "function") {
531
+ this.log("Apex ignored invalid change handler");
532
+ return { cancel() {
533
+ } };
534
+ }
535
+ if (this.handlers.size >= 1024) {
536
+ this.logFailure("handler-capacity", "Apex ignored change handler: listener capacity");
537
+ return { cancel() {
538
+ } };
539
+ }
540
+ this.handlers.add(handler);
541
+ return { cancel: () => {
542
+ this.handlers.delete(handler);
543
+ } };
544
+ }
545
+ stop(code) {
546
+ if (this.stopped) return;
547
+ this.stopped = true;
548
+ this.generation++;
549
+ this.logFailure(`auth-stop:${code}`, `Apex SDK stopped: ${code}`);
550
+ this.cleanup();
551
+ this.emit({ type: "stopped", code });
552
+ }
553
+ cleanup() {
554
+ this.settleFirstAssignment();
555
+ for (const settle of this.identifyWaiters.values()) settle();
556
+ for (const timer of this.timers.values()) clearTimeout(timer);
557
+ this.timers.clear();
558
+ for (const request of this.requests) request.abort();
559
+ if (typeof document !== "undefined") {
560
+ document.removeEventListener("visibilitychange", this.visibility);
561
+ document.removeEventListener("pause", this.pause);
562
+ document.removeEventListener("resume", this.resume);
563
+ }
564
+ if (this.shellListener) this.removeShellListener(this.shellListener);
565
+ this.shellListener = void 0;
566
+ }
567
+ removeShellListener(listener) {
568
+ try {
569
+ void Promise.resolve(listener.remove()).catch(() => this.log("Apex shell lifecycle cleanup failed"));
570
+ } catch {
571
+ this.log("Apex shell lifecycle cleanup failed");
572
+ }
573
+ }
574
+ /** Retire this context before replacing it. Completes outstanding storage operations. */
575
+ shutdown() {
576
+ this.stopped = true;
577
+ this.generation++;
578
+ this.cleanup();
579
+ return this.serial.then(() => this.queueWriter);
580
+ }
581
+ identify(userId, _attributes) {
582
+ if (userId !== null && !textInput(userId) || _attributes !== void 0 && (!recordInput(_attributes) || !Object.values(_attributes).every((value) => typeof value === "string" && value.length <= 1024))) {
583
+ this.log("Apex ignored invalid identity or attributes");
584
+ return Promise.resolve();
585
+ }
586
+ if (this.stopped) return Promise.resolve();
587
+ const revision = ++this.identityRevision;
588
+ this.generation++;
589
+ this.identityPending = true;
590
+ for (const [earlier, settle] of [...this.identifyWaiters]) if (earlier < revision) settle();
591
+ const timeoutMs = clamp(this.options.initTimeoutMs, 3e3, 0, 2147483647);
592
+ const loaded = new Promise((resolve) => {
593
+ if (timeoutMs === 0) {
594
+ resolve();
595
+ return;
596
+ }
597
+ const settle = () => {
598
+ clearTimeout(timer);
599
+ this.identifyWaiters.delete(revision);
600
+ resolve();
601
+ };
602
+ const timer = setTimeout(settle, timeoutMs);
603
+ this.identifyWaiters.set(revision, settle);
604
+ });
605
+ const persisted = this.task(async () => {
606
+ var _a, _b;
607
+ if (this.stopped) {
608
+ (_a = this.identifyWaiters.get(revision)) == null ? void 0 : _a();
609
+ return;
610
+ }
611
+ const previous = this.identity;
612
+ this.identity = userId != null ? userId : this.device;
613
+ const warning = "Apex identity could not be saved; the SDK stopped networking";
614
+ if (!await this.write("identity", userId, this.identityWriteWarned ? "" : warning)) {
615
+ this.identity = previous;
616
+ this.identityWriteWarned = true;
617
+ this.networkingSuspended = true;
618
+ this.generation++;
619
+ this.networkRevision++;
620
+ for (const timer of this.timers.values()) clearTimeout(timer);
621
+ this.timers.clear();
622
+ for (const request of this.requests) request.abort();
623
+ this.flushing = false;
624
+ if (revision === this.identityRevision) this.identityPending = this.identity !== this.assignmentIdentity;
625
+ (_b = this.identifyWaiters.get(revision)) == null ? void 0 : _b();
626
+ return;
627
+ }
628
+ if (this.networkingSuspended) {
629
+ this.networkingSuspended = false;
630
+ this.scheduleFlush(this.flushInterval);
631
+ }
632
+ if (revision === this.identityRevision) this.requestAssignments();
633
+ });
634
+ return persisted.then(() => loaded);
635
+ }
636
+ refresh() {
637
+ return this.task(() => this.requestAssignments());
638
+ }
639
+ async send(path, body, background = false) {
640
+ var _a, _b, _c;
641
+ if (this.stopped || this.networkingSuspended) return null;
642
+ const controller = new AbortController();
643
+ if (path === "assign") {
644
+ if (this.assignmentRequests.size >= 3) (_a = this.assignmentRequests.values().next().value) == null ? void 0 : _a.abort();
645
+ this.assignmentRequests.add(controller);
646
+ }
647
+ this.requests.add(controller);
648
+ const timeout = setTimeout(() => controller.abort(), 3e4);
649
+ let abort = () => {
650
+ };
651
+ const aborted = new Promise((_, reject) => {
652
+ abort = () => {
653
+ clearTimeout(timeout);
654
+ this.requests.delete(controller);
655
+ this.assignmentRequests.delete(controller);
656
+ reject(new Error("aborted"));
657
+ };
658
+ controller.signal.addEventListener("abort", abort, { once: true });
659
+ });
660
+ try {
661
+ const payload = JSON.stringify(body);
662
+ const keepalive = background && new TextEncoder().encode(payload).byteLength <= 65536;
663
+ const response = await Promise.race([aborted, ((_b = this.options.fetch) != null ? _b : globalThis.fetch)(`${((_c = this.options.baseUrl) != null ? _c : "https://events.drip-apex.com").replace(/\/+$/, "")}/v1/${path}`, {
664
+ method: "POST",
665
+ headers: { Authorization: `Bearer ${this.options.publishableKey}`, "Content-Type": "application/json" },
666
+ body: payload,
667
+ signal: controller.signal,
668
+ redirect: "error",
669
+ ...keepalive ? { keepalive: true } : {}
670
+ })]);
671
+ if (controller.signal.aborted) return null;
672
+ let parsed = null;
673
+ try {
674
+ parsed = await Promise.race([aborted, response.json()]);
675
+ } catch {
676
+ }
677
+ if (controller.signal.aborted) return null;
678
+ return { status: response.status, body: isRow(parsed) ? parsed : null, retryAfter: response.headers.get("Retry-After") };
679
+ } catch {
680
+ return null;
681
+ } finally {
682
+ clearTimeout(timeout);
683
+ controller.signal.removeEventListener("abort", abort);
684
+ this.requests.delete(controller);
685
+ this.assignmentRequests.delete(controller);
686
+ }
687
+ }
688
+ fatal(reply) {
689
+ var _a;
690
+ const code = string((_a = reply == null ? void 0 : reply.body) == null ? void 0 : _a.code);
691
+ return (reply == null ? void 0 : reply.status) === 401 && (code === "invalid_publishable_key" || code === "publishable_key_not_allowed") ? code : void 0;
692
+ }
693
+ requestAssignments() {
694
+ if (this.stopped || this.networkingSuspended) return;
695
+ const token = ++this.generation;
696
+ this.cancelTimer("assign");
697
+ if (Date.now() < this.assignNotBefore) {
698
+ this.schedule("assign", (this.assignNotBefore - Date.now()) / 1e3, () => {
699
+ void this.refresh();
700
+ });
701
+ return;
702
+ }
703
+ const requested = this.identity;
704
+ const revision = this.identityRevision;
705
+ void this.send("assign", { shop_id: this.options.shopId, user_id: requested, environment: this.options.environment }).then((reply) => this.task(async () => {
706
+ var _a, _b, _c, _d;
707
+ if (this.stopped || this.networkingSuspended) return;
708
+ const fatal = this.fatal(reply);
709
+ if (fatal) {
710
+ this.stop(fatal);
711
+ return;
712
+ }
713
+ if (token !== this.generation || revision !== this.identityRevision || requested !== this.identity) return;
714
+ const list = (_a = reply == null ? void 0 : reply.body) == null ? void 0 : _a.assignments;
715
+ if (reply && reply.status >= 200 && reply.status < 300 && ((_b = reply.body) == null ? void 0 : _b.user_id) === requested && Array.isArray(list) && list.every(isRow)) {
716
+ const before = this.keyed();
717
+ this.assignments = list;
718
+ this.assignmentsLoaded = true;
719
+ this.assignmentIdentity = requested;
720
+ this.identityPending = false;
721
+ this.fetchedAt = Date.now() / 1e3;
722
+ this.fromCache = false;
723
+ this.assignFailures = 0;
724
+ this.assignNotBefore = -Infinity;
725
+ this.cache.set(requested, { fetched_at: this.fetchedAt, assignments: list });
726
+ this.trimCache();
727
+ await this.write("cache", { entries: Object.fromEntries(this.cache) });
728
+ (_c = this.identifyWaiters.get(revision)) == null ? void 0 : _c();
729
+ this.changed(before);
730
+ } else {
731
+ if (reply && reply.status >= 400 && reply.status < 500 && ![401, 403, 408, 429].includes(reply.status) && ((_d = reply.body) == null ? void 0 : _d.retryable) !== true) {
732
+ this.logDeliveryFailure("assignment fetch", reply, "no retry scheduled");
733
+ return;
734
+ }
735
+ const delay = this.retryDelay(reply, ++this.assignFailures);
736
+ this.logDeliveryFailure("assignment fetch", reply, `retry scheduled in ${Math.round(delay * 10) / 10}s`);
737
+ this.assignNotBefore = Date.now() + delay * 1e3;
738
+ this.schedule("assign", delay, () => {
739
+ void this.refresh();
740
+ });
741
+ }
742
+ })).finally(() => this.settleFirstAssignment());
743
+ }
744
+ trimCache() {
745
+ const sorted = [...this.cache].sort(([a, x], [b, y]) => x.fetched_at - y.fetched_at || (a < b ? -1 : a > b ? 1 : 0));
746
+ for (const [id] of sorted.slice(0, Math.max(0, this.cache.size - 4))) this.cache.delete(id);
747
+ }
748
+ keyed() {
749
+ const result = /* @__PURE__ */ new Map();
750
+ for (const item of this.assignments) if (item.kind === "feature_flag" && isRow(item.feature_flag) && typeof item.feature_flag.key === "string") result.set(item.feature_flag.key, item);
751
+ return result;
752
+ }
753
+ changed(before) {
754
+ const after = this.keyed();
755
+ const keys = [.../* @__PURE__ */ new Set([...before.keys(), ...after.keys()])].filter((key) => !equalJSON(before.get(key), after.get(key))).sort();
756
+ if (keys.length) this.emit({ type: "changed", keys });
757
+ }
758
+ reason() {
759
+ return this.fetchedAt !== null && Date.now() / 1e3 - this.fetchedAt > 60 ? "stale" : this.fromCache ? "cached" : "fresh";
760
+ }
761
+ flag(key, defaultValue, opts = {}) {
762
+ if (!textInput(key) || !flagOptions(opts) || !jsonValue(defaultValue) || !["boolean", "string", "number"].includes(typeof defaultValue)) return defaultValue;
763
+ const item = this.keyed().get(key);
764
+ if (!item) {
765
+ this.warn(key);
766
+ return defaultValue;
767
+ }
768
+ const value = item.value;
769
+ if (!jsonValue(value) || typeof value !== typeof defaultValue || typeof defaultValue === "number" && !safeNumber(value)) {
770
+ this.warn(key);
771
+ return defaultValue;
772
+ }
773
+ if (opts.exposure !== "manual") this.expose(item);
774
+ return value;
775
+ }
776
+ flagObject(key, defaultValue, opts = {}) {
777
+ if (!textInput(key) || !flagOptions(opts) || !jsonValue(defaultValue)) return defaultValue;
778
+ const item = this.keyed().get(key);
779
+ if (!item) {
780
+ this.warn(key);
781
+ return defaultValue;
782
+ }
783
+ try {
784
+ if (!jsonValue(item.value) || !Object.prototype.hasOwnProperty.call(item, "value")) throw new Error("missing value");
785
+ const value = clone(item.value);
786
+ if (opts.validate ? !opts.validate(value) : !sameJSONType(value, defaultValue)) throw new Error("incompatible value");
787
+ if (opts.exposure !== "manual") this.expose(item);
788
+ return value;
789
+ } catch {
790
+ this.warn(key);
791
+ return defaultValue;
792
+ }
793
+ }
794
+ variation(key) {
795
+ var _a;
796
+ if (!textInput(key)) return null;
797
+ const item = this.keyed().get(key);
798
+ if (!item || typeof item.variation_id !== "string" || !Number.isSafeInteger(item.variation_index)) {
799
+ return { id: "", index: 0, isControl: false, kind: "", value: null, reason: "default" };
800
+ }
801
+ if (item.value !== void 0 && !jsonValue(item.value)) {
802
+ this.warn(key);
803
+ return null;
804
+ }
805
+ this.expose(item);
806
+ return {
807
+ id: item.variation_id,
808
+ index: item.variation_index,
809
+ isControl: item.is_control === true,
810
+ kind: (_a = string(item.kind)) != null ? _a : "",
811
+ value: item.value === void 0 ? null : clone(item.value),
812
+ reason: this.reason()
813
+ };
814
+ }
815
+ warn(key) {
816
+ if (!this.assignmentsLoaded) return;
817
+ if (this.warnings.size >= 256) return;
818
+ if (!this.warnings.has(key)) {
819
+ this.warnings.add(key);
820
+ this.log(`Apex typed flag '${key}' unavailable or incompatible; using default`);
821
+ }
822
+ }
823
+ logExposure(key) {
824
+ if (!textInput(key)) return;
825
+ const item = this.keyed().get(key);
826
+ if (item) this.expose(item);
827
+ }
828
+ expose(item) {
829
+ var _a;
830
+ if (this.stopped || this.networkingSuspended || this.identityPending || typeof item.experiment_id !== "string" || typeof item.variation_id !== "string" || !Number.isSafeInteger(item.variation_index)) return;
831
+ const epoch = (_a = item.assignment_epoch) != null ? _a : null;
832
+ const key = JSON.stringify([this.session, this.assignmentIdentity, item.experiment_id, item.variation_id, epoch]);
833
+ if (this.exposures.has(key) || this.pendingExposures.has(key)) return;
834
+ if (this.exposures.size + this.pendingExposures.size >= 4096) {
835
+ this.log("Apex dropped exposure: session dedupe capacity");
836
+ return;
837
+ }
838
+ const event = {
839
+ ...this.event("experiment_viewed", { index: item.variation_index, assignment_epoch: epoch }, this.assignmentIdentity),
840
+ experiment_id: item.experiment_id,
841
+ variation_id: item.variation_id
842
+ };
843
+ this.pendingExposures.add(key);
844
+ void this.queueTask(() => this.enqueue([event])).then((saved) => {
845
+ if (saved) this.exposures.add(key);
846
+ this.pendingExposures.delete(key);
847
+ });
848
+ }
849
+ validProperties(properties) {
850
+ const fields = optionRecord(properties);
851
+ if (!fields) {
852
+ this.log("Apex ignored non-JSON event properties");
853
+ return false;
854
+ }
855
+ properties = fields;
856
+ if (Object.prototype.hasOwnProperty.call(properties, "revenue")) {
857
+ const value = properties.revenue;
858
+ if (!(typeof value === "number" && Number.isFinite(value)) && !(typeof value === "string" && /^[-+]?\d+(?:[.,]\d+)?$/.test(value.trim()) && Number.isFinite(Number(value.trim().replace(",", "."))))) {
859
+ this.log("Apex dropped event: revenue must be a finite number");
860
+ return false;
861
+ }
862
+ }
863
+ const forbidden = /* @__PURE__ */ new Set(["email", "phone", "password", "ssn", "credit_card", "card_number"]);
864
+ for (const key of Object.keys(properties).sort()) if (forbidden.has(key.normalize("NFD").replace(/\p{Mn}/gu, "").toLowerCase())) {
865
+ this.log(`Apex dropped event: property ${key} is not allowed`);
866
+ return false;
867
+ }
868
+ if (!jsonValue(properties)) {
869
+ this.log("Apex ignored non-JSON event properties");
870
+ return false;
871
+ }
872
+ return true;
873
+ }
874
+ track(event, properties = {}) {
875
+ if (!textInput(event, 256)) {
876
+ this.log("Apex dropped event: invalid event name");
877
+ return Promise.resolve();
878
+ }
879
+ if (!this.validProperties(properties)) return Promise.resolve();
880
+ const snapshot = clone(properties);
881
+ return this.queueTask(() => this.enqueue([this.event(event, snapshot, this.identity)])).then(() => {
882
+ });
883
+ }
884
+ trackPurchase(orderId, revenue, currency, properties = {}) {
885
+ if (!textInput(orderId)) {
886
+ this.log("Apex dropped purchase: order_id must be a string");
887
+ return Promise.resolve();
888
+ }
889
+ if (!textInput(currency, 32)) {
890
+ this.log("Apex dropped purchase: currency must be a 3-letter ISO 4217 code");
891
+ return Promise.resolve();
892
+ }
893
+ const order = orderId.trim(), code = currency.trim().toUpperCase();
894
+ if (!order) {
895
+ this.log("Apex dropped purchase: order_id is required");
896
+ return Promise.resolve();
897
+ }
898
+ if (!safeNumber(revenue) || revenue < 0) {
899
+ this.log("Apex dropped purchase: revenue must be a finite non-negative number");
900
+ return Promise.resolve();
901
+ }
902
+ if (!/^[A-Z]{3}$/.test(code)) {
903
+ this.log("Apex dropped purchase: currency must be a 3-letter ISO 4217 code");
904
+ return Promise.resolve();
905
+ }
906
+ if (!recordInput(properties)) {
907
+ this.log("Apex ignored non-JSON purchase properties");
908
+ return Promise.resolve();
909
+ }
910
+ const aliases = /* @__PURE__ */ new Set(["orderId", "order_id", "bookingId", "booking_id", "revenue", "currency", "currencyCode", "currency_code"]);
911
+ const merged = { ...Object.fromEntries(Object.entries(properties).filter(([key]) => !aliases.has(key))), order_id: order, revenue, currency: code };
912
+ if (!this.validProperties(merged)) return Promise.resolve();
913
+ const snapshot = clone(merged);
914
+ return this.queueTask(() => {
915
+ const base = this.event("purchase", snapshot, this.identity);
916
+ const batch = [base];
917
+ for (const assignment of this.assignments) {
918
+ if (typeof assignment.experiment_id !== "string" || typeof assignment.variation_id !== "string") continue;
919
+ const copy = { ...base, experiment_id: assignment.experiment_id, variation_id: assignment.variation_id, visitor_id: this.assignmentIdentity };
920
+ delete copy.user_id;
921
+ if (this.assignmentIdentity !== this.device) copy.user_id = this.assignmentIdentity;
922
+ batch.push(copy);
923
+ }
924
+ return this.enqueue(batch, "Apex dropped purchase: queue write failed");
925
+ }).then(() => {
926
+ });
927
+ }
928
+ event(type, properties, visitor) {
929
+ var _a, _b, _c, _d;
930
+ let screen = string(properties.screen);
931
+ if (screen === void 0) {
932
+ try {
933
+ screen = string((_b = (_a = this.options).screen) == null ? void 0 : _b.call(_a));
934
+ } catch {
935
+ }
936
+ }
937
+ return {
938
+ event_type: type,
939
+ event_id: uuid(),
940
+ visitor_id: visitor,
941
+ session_id: this.session,
942
+ url: screenURL((_c = this.options.applicationId) != null ? _c : "unknown", screen != null ? screen : "home"),
943
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
944
+ platform: this.platform,
945
+ environment: this.options.environment,
946
+ app_version: (_d = this.options.appVersion) != null ? _d : "",
947
+ sdk_version: version,
948
+ properties,
949
+ ...visitor !== this.device ? { user_id: visitor } : {},
950
+ ...this.options.qaMode || this.options.environment !== "production" ? { qa_mode: true } : {}
951
+ };
952
+ }
953
+ retained(events = this.events) {
954
+ const cutoff = Date.now() - this.retention;
955
+ const rows = events.filter((event) => typeof event.timestamp === "string" && Date.parse(event.timestamp) > cutoff);
956
+ const result = [];
957
+ for (let start = 0; start < rows.length; ) {
958
+ let end = start + 1;
959
+ while (end < rows.length && rows[end].event_id === rows[start].event_id) end++;
960
+ if (end - start <= Math.min(this.capacity, SERVER_MAX_EVENTS)) result.push(...rows.slice(start, end));
961
+ start = end;
962
+ }
963
+ return result;
964
+ }
965
+ async prune(events = this.events, force = false) {
966
+ const retained = this.retained(events);
967
+ const start = this.capacityStart(retained, this.capacity);
968
+ if (retained.length < events.length) this.queueDropLogs.add("Apex dropped expired or oversized event rows");
969
+ if (start) this.queueDropLogs.add("Apex dropped event rows: queue capacity");
970
+ const next = retained.slice(start);
971
+ if (!force && next.length === this.events.length && next.every((row, i) => row === this.events[i])) return this.persistQueue();
972
+ return this.replaceQueue(next);
973
+ }
974
+ capacityStart(rows, capacity) {
975
+ let first = Math.max(0, rows.length - capacity);
976
+ if (first > 0 && first < rows.length) {
977
+ const id = rows[first - 1].event_id;
978
+ while (first < rows.length && rows[first].event_id === id) first++;
979
+ }
980
+ return first;
981
+ }
982
+ async enqueue(batch, failureLog) {
983
+ if (batch.length > this.capacity || batch.length > SERVER_MAX_EVENTS) {
984
+ this.log(`Apex dropped purchase: group exceeds queue capacity or the ${SERVER_MAX_EVENTS}-event request limit`);
985
+ return false;
986
+ }
987
+ const retained = this.retained();
988
+ const first = this.capacityStart(retained, this.capacity - batch.length);
989
+ if (retained.length < this.events.length) this.queueDropLogs.add("Apex dropped expired or oversized event rows");
990
+ if (first) this.queueDropLogs.add("Apex dropped event rows: queue capacity");
991
+ const next = [...retained.slice(first), ...batch];
992
+ if (!await this.replaceQueue(next, failureLog)) return false;
993
+ if (this.events.length >= this.batchSize) void this.flush();
994
+ return true;
995
+ }
996
+ flush() {
997
+ return this.task(() => this.flushLocked());
998
+ }
999
+ scheduleFlush(delay) {
1000
+ this.schedule("flush", delay, () => {
1001
+ void this.flush();
1002
+ });
1003
+ }
1004
+ deliveryKey(event) {
1005
+ var _a, _b;
1006
+ return JSON.stringify([event.event_id, (_a = event.experiment_id) != null ? _a : null, (_b = event.variation_id) != null ? _b : null]);
1007
+ }
1008
+ async flushLocked(background = this.appState === "background") {
1009
+ if (this.stopped || this.networkingSuspended || this.flushing) return;
1010
+ if (Date.now() < this.eventsNotBefore) {
1011
+ this.scheduleFlush((this.eventsNotBefore - Date.now()) / 1e3);
1012
+ return;
1013
+ }
1014
+ await this.prune();
1015
+ const retained = this.retained();
1016
+ let end = Math.min(retained.length, this.batchSize);
1017
+ if (end > 0) {
1018
+ const id = retained[end - 1].event_id;
1019
+ while (end < retained.length && retained[end].event_id === id) end++;
1020
+ if (end > SERVER_MAX_EVENTS) {
1021
+ while (end > 0 && retained[end - 1].event_id === id) end--;
1022
+ }
1023
+ }
1024
+ const batch = retained.slice(0, end);
1025
+ if (!batch.length) {
1026
+ this.scheduleFlush(this.flushInterval);
1027
+ return;
1028
+ }
1029
+ const ids = new Set(batch.map((event) => this.deliveryKey(event)));
1030
+ this.flushing = true;
1031
+ const revision = this.networkRevision;
1032
+ void this.send("events", { _shopId: this.options.shopId, events: batch }, background).then((reply) => this.task(async () => {
1033
+ var _a, _b;
1034
+ if (revision !== this.networkRevision) return;
1035
+ this.flushing = false;
1036
+ if (this.stopped || this.networkingSuspended) return;
1037
+ const fatal = this.fatal(reply);
1038
+ if (fatal) {
1039
+ this.stop(fatal);
1040
+ return;
1041
+ }
1042
+ const accepted = (reply == null ? void 0 : reply.status) === 202 && ((_a = reply.body) == null ? void 0 : _a.ok) === true;
1043
+ const drop = reply !== null && reply.status >= 400 && reply.status < 500 && ![401, 403, 408, 429].includes(reply.status) && ((_b = reply.body) == null ? void 0 : _b.retryable) !== true;
1044
+ if (accepted || drop) {
1045
+ if (drop) this.queueDropLogs.add("Apex dropped event batch: non-retryable response");
1046
+ const next = this.events.filter((event) => !ids.has(this.deliveryKey(event)));
1047
+ const persisted = await this.replaceQueue(next);
1048
+ this.eventFailures = 0;
1049
+ this.eventsNotBefore = persisted ? -Infinity : Date.now() + this.flushInterval * 1e3;
1050
+ if (drop) this.logDeliveryFailure(
1051
+ "event delivery",
1052
+ reply,
1053
+ persisted ? "batch dropped" : `batch retained after storage failure; retry scheduled in ${Math.round(this.flushInterval * 10) / 10}s`
1054
+ );
1055
+ this.scheduleFlush(persisted && next.length ? 0 : this.flushInterval);
1056
+ } else {
1057
+ const delay = this.retryDelay(reply, ++this.eventFailures);
1058
+ this.logDeliveryFailure("event delivery", reply, `retry scheduled in ${Math.round(delay * 10) / 10}s`);
1059
+ this.eventsNotBefore = Date.now() + delay * 1e3;
1060
+ this.scheduleFlush(delay);
1061
+ }
1062
+ }));
1063
+ }
1064
+ retryDelay(reply, failures) {
1065
+ var _a, _b;
1066
+ if (reply && (reply.status === 429 || reply.status >= 500 || ((_a = reply.body) == null ? void 0 : _a.retryable) === true)) {
1067
+ const header = reply.retryAfter;
1068
+ if (header !== null && header.trim() !== "") {
1069
+ const seconds = Number(header);
1070
+ if (Number.isFinite(seconds) && seconds >= 0) return Math.max(0.1, seconds);
1071
+ const time = /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{1,2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} GMT$/.test(header) ? Date.parse(header) : NaN;
1072
+ if (Number.isFinite(time)) return Math.max(0.1, (time - Date.now()) / 1e3);
1073
+ }
1074
+ if (((_b = reply.body) == null ? void 0 : _b.code) === "publishable_key_lookup_unavailable") return 5;
1075
+ }
1076
+ if ((reply == null ? void 0 : reply.status) === 401 || (reply == null ? void 0 : reply.status) === 403) return 300;
1077
+ if (reply && reply.status >= 400 && reply.status < 500 && ![408, 429].includes(reply.status)) return 30;
1078
+ return Math.min(60, 5 * 2 ** Math.min(4, Math.max(0, failures - 1)) * (1 + Math.random() * 0.2));
1079
+ }
1080
+ };
1081
+ function sameJSONType(value, fallback) {
1082
+ if (fallback === null) return value === null;
1083
+ if (Array.isArray(fallback)) return Array.isArray(value);
1084
+ if (isRow(fallback)) return isRow(value);
1085
+ return typeof value === typeof fallback && (typeof value !== "number" || safeNumber(value));
1086
+ }
1087
+ function equalJSON(a, b) {
1088
+ if (a === b) return true;
1089
+ if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, i) => equalJSON(v, b[i]));
1090
+ if (isRow(a) && isRow(b)) {
1091
+ const keys = Object.keys(a);
1092
+ return keys.length === Object.keys(b).length && keys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && equalJSON(a[k], b[k]));
1093
+ }
1094
+ return false;
1095
+ }
1096
+ var Apex = class {
1097
+ // Snapshot event properties at the public call; a non-JSON value is passed through so the client logs and drops it.
1098
+ static snapshot(properties) {
1099
+ if (properties === void 0) return void 0;
1100
+ return recordInput(properties) ? clone(properties) : properties;
1101
+ }
1102
+ static configure(options) {
1103
+ var _a, _b;
1104
+ const snapshot = (_a = optionRecord(options)) != null ? _a : { publishableKey: "", shopId: "", logger: loggerInput(options) };
1105
+ const retired = (_b = this.client) == null ? void 0 : _b.shutdown();
1106
+ this.client = void 0;
1107
+ const sequence = ++this.configureSequence;
1108
+ this.configuring = this.configuring.then(async () => {
1109
+ var _a2, _b2;
1110
+ await retired;
1111
+ await ((_a2 = this.client) == null ? void 0 : _a2.shutdown());
1112
+ this.client = new ApexClient(snapshot);
1113
+ for (const entry of this.pendingHandlers) {
1114
+ (_b2 = entry.subscription) == null ? void 0 : _b2.cancel();
1115
+ entry.subscription = this.client.onChange(entry.handler);
1116
+ }
1117
+ await this.client.ready;
1118
+ if (sequence === this.configureSequence) this.pendingHandlers.clear();
1119
+ });
1120
+ return this.configuring;
1121
+ }
1122
+ static get deviceId() {
1123
+ var _a;
1124
+ return (_a = this.client) == null ? void 0 : _a.deviceId;
1125
+ }
1126
+ static get userId() {
1127
+ var _a;
1128
+ return (_a = this.client) == null ? void 0 : _a.userId;
1129
+ }
1130
+ static identify(userId, attributes) {
1131
+ var _a, _b;
1132
+ return (_b = (_a = this.client) == null ? void 0 : _a.identify(userId, attributes)) != null ? _b : this.configuring.then(() => {
1133
+ var _a2;
1134
+ return (_a2 = this.client) == null ? void 0 : _a2.identify(userId, attributes);
1135
+ });
1136
+ }
1137
+ static flag(key, defaultValue, opts) {
1138
+ var _a, _b;
1139
+ return (_b = (_a = this.client) == null ? void 0 : _a.flag(key, defaultValue, opts)) != null ? _b : defaultValue;
1140
+ }
1141
+ static flagObject(key, defaultValue, opts) {
1142
+ const client = this.client;
1143
+ return client ? client.flagObject(key, defaultValue, opts) : defaultValue;
1144
+ }
1145
+ static variation(key) {
1146
+ var _a, _b;
1147
+ return (_b = (_a = this.client) == null ? void 0 : _a.variation(key)) != null ? _b : null;
1148
+ }
1149
+ static logExposure(key) {
1150
+ var _a;
1151
+ (_a = this.client) == null ? void 0 : _a.logExposure(key);
1152
+ }
1153
+ static track(event, properties) {
1154
+ if (this.client) return this.client.track(event, properties);
1155
+ const snapshot = this.snapshot(properties);
1156
+ return this.configuring.then(() => {
1157
+ var _a;
1158
+ return (_a = this.client) == null ? void 0 : _a.track(event, snapshot);
1159
+ });
1160
+ }
1161
+ static trackPurchase(orderId, revenue, currency, properties) {
1162
+ if (this.client) return this.client.trackPurchase(orderId, revenue, currency, properties);
1163
+ const snapshot = this.snapshot(properties);
1164
+ return this.configuring.then(() => {
1165
+ var _a;
1166
+ return (_a = this.client) == null ? void 0 : _a.trackPurchase(orderId, revenue, currency, snapshot);
1167
+ });
1168
+ }
1169
+ static onChange(handler) {
1170
+ var _a, _b;
1171
+ if (typeof handler !== "function") return (_b = (_a = this.client) == null ? void 0 : _a.onChange(handler)) != null ? _b : { cancel() {
1172
+ } };
1173
+ if (this.client) return this.client.onChange(handler);
1174
+ if (this.pendingHandlers.size >= 1024) return { cancel() {
1175
+ } };
1176
+ const entry = { handler };
1177
+ this.pendingHandlers.add(entry);
1178
+ return { cancel: () => {
1179
+ var _a2;
1180
+ this.pendingHandlers.delete(entry);
1181
+ (_a2 = entry.subscription) == null ? void 0 : _a2.cancel();
1182
+ } };
1183
+ }
1184
+ static refresh() {
1185
+ var _a, _b;
1186
+ return (_b = (_a = this.client) == null ? void 0 : _a.refresh()) != null ? _b : this.configuring.then(() => {
1187
+ var _a2;
1188
+ return (_a2 = this.client) == null ? void 0 : _a2.refresh();
1189
+ });
1190
+ }
1191
+ static flush() {
1192
+ var _a, _b;
1193
+ return (_b = (_a = this.client) == null ? void 0 : _a.flush()) != null ? _b : this.configuring.then(() => {
1194
+ var _a2;
1195
+ return (_a2 = this.client) == null ? void 0 : _a2.flush();
1196
+ });
1197
+ }
1198
+ static setAppState(state) {
1199
+ if (this.client) this.client.setAppState(state);
1200
+ else void this.configuring.then(() => {
1201
+ var _a;
1202
+ return (_a = this.client) == null ? void 0 : _a.setAppState(state);
1203
+ });
1204
+ }
1205
+ static async shutdown() {
1206
+ var _a;
1207
+ await this.configuring;
1208
+ await ((_a = this.client) == null ? void 0 : _a.shutdown());
1209
+ this.client = void 0;
1210
+ }
1211
+ };
1212
+ Apex.sdkVersion = version;
1213
+ Apex.configuring = Promise.resolve();
1214
+ Apex.configureSequence = 0;
1215
+ // Handlers registered while no client exists attach as soon as the next client is created,
1216
+ // so they also see changes emitted during initialization.
1217
+ Apex.pendingHandlers = /* @__PURE__ */ new Set();