@layers/amba-react-native 4.0.6 → 4.0.7

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 CHANGED
@@ -42,6 +42,10 @@ var AsyncStoragePersistence = class {
42
42
  constructor(prefix) {
43
43
  this.prefix = prefix;
44
44
  }
45
+ /** Batching promises durable enqueue, so memory-only fallback is invalid. */
46
+ async assertDurable() {
47
+ if (!await getStorage()) throw new Error("Amba event batching requires @react-native-async-storage/async-storage");
48
+ }
45
49
  async hydrate() {}
46
50
  async read(key) {
47
51
  const fullKey = `${this.prefix}:${key}`;
@@ -54,15 +58,15 @@ var AsyncStoragePersistence = class {
54
58
  }
55
59
  async write(key, value) {
56
60
  const fullKey = `${this.prefix}:${key}`;
57
- this.memoryCache.set(fullKey, value);
58
61
  const storage = await getStorage();
59
62
  if (storage) await storage.setItem(fullKey, value);
63
+ this.memoryCache.set(fullKey, value);
60
64
  }
61
65
  async delete(key) {
62
66
  const fullKey = `${this.prefix}:${key}`;
63
- this.memoryCache.delete(fullKey);
64
67
  const storage = await getStorage();
65
68
  if (storage) await storage.removeItem(fullKey);
69
+ this.memoryCache.delete(fullKey);
66
70
  }
67
71
  };
68
72
 
@@ -134,18 +138,24 @@ var AmbaHttpClient = class {
134
138
  async post(path, body, query) {
135
139
  return this.request("POST", path, body, query);
136
140
  }
141
+ /** Send a queued event with the session captured for its original user. */
142
+ async postWithSession(path, body, sessionToken) {
143
+ return this.request("POST", path, body, void 0, sessionToken);
144
+ }
137
145
  async patch(path, body) {
138
146
  return this.request("PATCH", path, body);
139
147
  }
140
148
  async delete(path) {
141
149
  return this.request("DELETE", path);
142
150
  }
143
- async request(method, path, body, query) {
144
- if (this.ensureSession && !this.identity.getSessionToken()) await this.ensureSession(path);
151
+ async request(method, path, body, query, sessionToken) {
152
+ if (this.ensureSession && !sessionToken && !this.identity.getSessionToken()) await this.ensureSession(path);
145
153
  const url = this.url(path, query);
154
+ const headers = this.buildHeaders();
155
+ if (sessionToken) headers.Authorization = `Bearer ${sessionToken}`;
146
156
  const init = {
147
157
  method,
148
- headers: this.buildHeaders(),
158
+ headers,
149
159
  body: body === void 0 ? void 0 : JSON.stringify(body ?? {})
150
160
  };
151
161
  let response;
@@ -665,6 +675,322 @@ async function withOptimistic(spec) {
665
675
  }
666
676
  }
667
677
 
678
+ //#endregion
679
+ //#region src/event-batch-queue.ts
680
+ const MAX_BYTES = 512 * 1024;
681
+ const HELD_TTL_MS = 720 * 60 * 60 * 1e3;
682
+ const COALESCE_MS = 250;
683
+ function byteLength(s) {
684
+ if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(s).length;
685
+ return encodeURIComponent(s).replace(/%[A-Fa-f0-9]{2}|./g, "x").length;
686
+ }
687
+ function wire(e) {
688
+ return {
689
+ event: e.event,
690
+ properties: e.properties,
691
+ timestamp: e.timestamp,
692
+ telemetry: e.telemetry,
693
+ idempotency_key: e.idempotency_key
694
+ };
695
+ }
696
+ function safeDrop(cb, e, reason, code) {
697
+ try {
698
+ cb?.({
699
+ idempotencyKey: e.idempotency_key,
700
+ event: e.event,
701
+ userId: e.userId,
702
+ reason,
703
+ code
704
+ });
705
+ } catch {}
706
+ }
707
+ /** Durable queue owned by one configured project and storage prefix. */
708
+ var EventBatchQueue = class {
709
+ events = [];
710
+ pending = [];
711
+ writeTimer = null;
712
+ flushTimer = null;
713
+ retryTimer = null;
714
+ serialized = Promise.resolve();
715
+ inFlight = null;
716
+ disposed = false;
717
+ online = null;
718
+ batchingEnabled = true;
719
+ failures = 0;
720
+ clearingUsers = /* @__PURE__ */ new Set();
721
+ constructor(store, storageKey, options, identity, postBatch, postSingle) {
722
+ this.store = store;
723
+ this.storageKey = storageKey;
724
+ this.options = options;
725
+ this.identity = identity;
726
+ this.postBatch = postBatch;
727
+ this.postSingle = postSingle;
728
+ }
729
+ async start() {
730
+ const raw = await this.store.get(this.storageKey);
731
+ if (raw) try {
732
+ const parsed = JSON.parse(raw);
733
+ this.events = Array.isArray(parsed) ? parsed : [];
734
+ } catch {
735
+ this.events = [];
736
+ }
737
+ await this.expireHeld();
738
+ if (this.events.length) this.flush();
739
+ }
740
+ exclusive(work) {
741
+ const run = this.serialized.then(work);
742
+ this.serialized = run.catch(() => void 0);
743
+ return run;
744
+ }
745
+ async save() {
746
+ if (this.events.length) await this.store.set(this.storageKey, JSON.stringify(this.events));
747
+ else await this.store.delete(this.storageKey);
748
+ }
749
+ enqueue(event, properties, telemetry) {
750
+ if (this.disposed) return Promise.reject(/* @__PURE__ */ new Error("Amba event queue disposed"));
751
+ const id = this.identity();
752
+ const item = {
753
+ event,
754
+ properties,
755
+ telemetry,
756
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
757
+ idempotency_key: newIdempotencyKey(),
758
+ userId: id.sessionToken ? id.userId : null,
759
+ anonymousId: id.anonymousId
760
+ };
761
+ if (!event.trim()) return Promise.reject(/* @__PURE__ */ new Error("event name must be non-empty"));
762
+ if (byteLength(JSON.stringify({ events: [wire(item)] })) > MAX_BYTES) {
763
+ safeDrop(this.options.onDrop, item, "oversized");
764
+ return Promise.reject(/* @__PURE__ */ new Error("event exceeds 512 KB batch limit"));
765
+ }
766
+ return new Promise((resolve, reject) => {
767
+ this.pending.push({
768
+ event: item,
769
+ resolve,
770
+ reject
771
+ });
772
+ if (!this.writeTimer) this.writeTimer = setTimeout(() => {
773
+ this.persistPending();
774
+ }, COALESCE_MS);
775
+ });
776
+ }
777
+ async persistPending() {
778
+ if (this.writeTimer) clearTimeout(this.writeTimer);
779
+ this.writeTimer = null;
780
+ const batch = this.pending.splice(0);
781
+ if (!batch.length) return;
782
+ try {
783
+ let evictedKeys = /* @__PURE__ */ new Set();
784
+ await this.exclusive(async () => {
785
+ const before = this.events;
786
+ this.events = [...this.events, ...batch.map((p) => p.event)];
787
+ const evicted = [];
788
+ while (this.events.length > this.options.maxQueue) {
789
+ const idx = this.events.findIndex((e) => e.telemetry);
790
+ if (idx < 0) break;
791
+ evicted.push(this.events[idx]);
792
+ this.events.splice(idx, 1);
793
+ }
794
+ try {
795
+ await this.save();
796
+ } catch (err) {
797
+ this.events = before;
798
+ throw err;
799
+ }
800
+ evictedKeys = new Set(evicted.map((e) => e.idempotency_key));
801
+ for (const e of evicted) safeDrop(this.options.onDrop, e, "telemetry_evicted");
802
+ });
803
+ for (const p of batch) if (evictedKeys.has(p.event.idempotency_key)) p.reject(/* @__PURE__ */ new Error("event evicted before durable enqueue"));
804
+ else p.resolve();
805
+ this.schedule();
806
+ if (this.online !== false && this.events.length >= this.options.batchSize) this.flush();
807
+ } catch (err) {
808
+ for (const p of batch) p.reject(err);
809
+ }
810
+ }
811
+ setOnline(value) {
812
+ const wasOnline = this.online;
813
+ this.online = value;
814
+ if (!value) {
815
+ this.cancelTimers();
816
+ return;
817
+ }
818
+ this.schedule();
819
+ if (wasOnline !== true) this.flush();
820
+ }
821
+ setBatchingEnabled(enabled) {
822
+ this.batchingEnabled = enabled;
823
+ if (this.online !== false) this.flush();
824
+ }
825
+ get isBatchingEnabled() {
826
+ return this.batchingEnabled;
827
+ }
828
+ async pendingCount(userId) {
829
+ await this.persistPending();
830
+ return this.events.filter((e) => userId === void 0 || e.userId === userId).length;
831
+ }
832
+ async clear(userId) {
833
+ this.clearingUsers.add(userId);
834
+ try {
835
+ await this.inFlight?.catch(() => void 0);
836
+ await this.persistPending();
837
+ await this.exclusive(async () => {
838
+ const prior = this.events;
839
+ const id = this.identity();
840
+ this.events = this.events.filter((e) => e.userId !== userId && !(id.userId === userId && e.userId === null && e.anonymousId === id.anonymousId));
841
+ try {
842
+ await this.save();
843
+ } catch (err) {
844
+ this.events = prior;
845
+ throw err;
846
+ }
847
+ });
848
+ } finally {
849
+ this.clearingUsers.delete(userId);
850
+ this.schedule();
851
+ }
852
+ }
853
+ async expireHeld() {
854
+ const current = this.identity();
855
+ await this.exclusive(async () => {
856
+ const now = Date.now();
857
+ const dropped = [];
858
+ const next = [];
859
+ for (const e of this.events) {
860
+ if (e.userId && e.userId !== current.userId || !e.userId && e.anonymousId !== current.anonymousId) {
861
+ e.heldAt ??= new Date(now).toISOString();
862
+ if (now - Date.parse(e.heldAt) >= HELD_TTL_MS) {
863
+ dropped.push(e);
864
+ continue;
865
+ }
866
+ } else e.heldAt = void 0;
867
+ next.push(e);
868
+ }
869
+ this.events = next;
870
+ await this.save();
871
+ for (const e of dropped) safeDrop(this.options.onDrop, e, "held_expired");
872
+ });
873
+ }
874
+ cancelTimers() {
875
+ if (this.flushTimer) clearTimeout(this.flushTimer);
876
+ if (this.retryTimer) clearTimeout(this.retryTimer);
877
+ this.flushTimer = this.retryTimer = null;
878
+ }
879
+ schedule() {
880
+ if (this.flushTimer) clearTimeout(this.flushTimer);
881
+ this.flushTimer = null;
882
+ if (this.disposed || this.online !== true || this.failures > 0 || this.options.flushIntervalMs === 0) return;
883
+ const id = this.identity();
884
+ if (!id.sessionToken || !id.userId) return;
885
+ const eligible = this.events.filter((e) => e.userId === id.userId || e.userId === null && e.anonymousId === id.anonymousId);
886
+ if (!eligible.length) return;
887
+ const oldest = Math.min(...eligible.map((e) => Date.parse(e.timestamp)));
888
+ const wait = Math.max(0, oldest + this.options.flushIntervalMs - Date.now());
889
+ this.flushTimer = setTimeout(() => {
890
+ this.flush();
891
+ }, wait);
892
+ }
893
+ retry() {
894
+ this.failures += 1;
895
+ if (this.retryTimer) clearTimeout(this.retryTimer);
896
+ if (this.online !== true) return;
897
+ const delay$1 = Math.min(6e4, 1e3 * 2 ** Math.min(this.failures - 1, 6));
898
+ this.retryTimer = setTimeout(() => {
899
+ this.flush();
900
+ }, delay$1);
901
+ }
902
+ async flush() {
903
+ if (this.inFlight) return this.inFlight;
904
+ this.inFlight = this.runFlush().finally(() => {
905
+ this.inFlight = null;
906
+ });
907
+ return this.inFlight;
908
+ }
909
+ async runFlush() {
910
+ await this.persistPending();
911
+ if (this.disposed || this.online === false) return {
912
+ sent: 0,
913
+ rejected: 0,
914
+ remaining: this.events.length
915
+ };
916
+ await this.expireHeld();
917
+ const id = this.identity();
918
+ if (!id.sessionToken || !id.userId) return {
919
+ sent: 0,
920
+ rejected: 0,
921
+ remaining: this.events.length
922
+ };
923
+ let sent = 0;
924
+ let rejected = 0;
925
+ for (;;) {
926
+ if (this.disposed || this.online === false || this.clearingUsers.has(id.userId) || this.identity().userId !== id.userId) break;
927
+ const eligible = this.events.filter((e) => e.userId === id.userId || e.userId === null && e.anonymousId === id.anonymousId);
928
+ if (!eligible.length) break;
929
+ const selected = [];
930
+ for (const e of eligible) {
931
+ if (selected.length >= this.options.batchSize) break;
932
+ const candidate = [...selected, e];
933
+ if (byteLength(JSON.stringify({ events: candidate.map(wire) })) > MAX_BYTES) break;
934
+ selected.push(e);
935
+ }
936
+ if (!selected.length) break;
937
+ await this.exclusive(async () => {
938
+ for (const e of selected) if (e.userId === null) e.userId = id.userId;
939
+ await this.save();
940
+ });
941
+ if (this.clearingUsers.has(id.userId) || this.identity().userId !== id.userId) break;
942
+ try {
943
+ const ack = this.batchingEnabled ? await this.postBatch({ events: selected.map(wire) }, id.sessionToken) : { data: {
944
+ accepted: [],
945
+ rejected: []
946
+ } };
947
+ if (!this.batchingEnabled) for (const e of selected) {
948
+ await this.postSingle(wire(e), id.sessionToken);
949
+ ack.data.accepted.push({
950
+ idempotency_key: e.idempotency_key,
951
+ status: "inserted"
952
+ });
953
+ }
954
+ const accepted = new Set(ack.data.accepted.map((a) => a.idempotency_key));
955
+ const drops = new Map(ack.data.rejected.map((r) => [r.idempotency_key, r.code]));
956
+ const known = new Set(selected.map((e) => e.idempotency_key));
957
+ if ([...accepted, ...drops.keys()].some((k) => !known.has(k))) throw new Error("event batch returned unknown key");
958
+ if (accepted.size + drops.size !== selected.length) throw new Error("event batch omitted acknowledgments");
959
+ await this.exclusive(async () => {
960
+ const prior = this.events;
961
+ this.events = this.events.filter((e) => !accepted.has(e.idempotency_key) && !drops.has(e.idempotency_key));
962
+ try {
963
+ await this.save();
964
+ } catch (err) {
965
+ this.events = prior;
966
+ throw err;
967
+ }
968
+ });
969
+ sent += accepted.size;
970
+ rejected += drops.size;
971
+ for (const e of selected) if (drops.has(e.idempotency_key)) safeDrop(this.options.onDrop, e, "server_rejected", drops.get(e.idempotency_key));
972
+ this.failures = 0;
973
+ } catch {
974
+ this.retry();
975
+ break;
976
+ }
977
+ }
978
+ this.schedule();
979
+ return {
980
+ sent,
981
+ rejected,
982
+ remaining: this.events.length
983
+ };
984
+ }
985
+ async dispose() {
986
+ this.disposed = true;
987
+ this.cancelTimers();
988
+ await this.persistPending();
989
+ await this.inFlight?.catch(() => void 0);
990
+ await this.serialized;
991
+ }
992
+ };
993
+
668
994
  //#endregion
669
995
  //#region src/push-rotation.ts
670
996
  /**
@@ -1150,13 +1476,14 @@ function delay(ms) {
1150
1476
  //#endregion
1151
1477
  //#region src/index.ts
1152
1478
  /** SDK semver — burned in at build time. Bump alongside package.json. */
1153
- const SDK_VERSION = "4.0.6";
1479
+ const SDK_VERSION = "4.0.7";
1154
1480
  /**
1155
1481
  * Default host that serves deployed functions. `Amba.functions.invoke(name)`
1156
1482
  * targets `https://<projectId>.<DEFAULT_FUNCTIONS_HOST>/<name>` unless a
1157
1483
  * `functionsBaseUrl` override is configured.
1158
1484
  */
1159
1485
  const DEFAULT_FUNCTIONS_HOST = "fn.amba.host";
1486
+ const PROJECT_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1160
1487
  let state = null;
1161
1488
  /**
1162
1489
  * Monotonic token identifying the current offline activation. Bumped on every
@@ -1210,7 +1537,7 @@ function normalizeAuthProviderNames(value) {
1210
1537
  try {
1211
1538
  normalized = JSON.parse(trimmed);
1212
1539
  } catch {
1213
- return [normalized];
1540
+ return [trimmed];
1214
1541
  }
1215
1542
  }
1216
1543
  if (!Array.isArray(normalized)) {
@@ -1272,6 +1599,7 @@ async function persistAuthResult(state_, result) {
1272
1599
  async function postAuth(state_, path, body) {
1273
1600
  const result = normalizeAuthResult((await state_.http.post(path, body)).data);
1274
1601
  await persistAuthResult(state_, result);
1602
+ state_.eventQueue?.flush();
1275
1603
  await notifyFromAuthResult(result);
1276
1604
  return result;
1277
1605
  }
@@ -1393,20 +1721,37 @@ function encodePathSegment(s) {
1393
1721
  return encodeURIComponent(s);
1394
1722
  }
1395
1723
  /**
1396
- * Resolve the full invocation URL for a deployed function. With
1397
- * `functionsBaseUrl` configured we POST to `<base>/<name>` verbatim;
1398
- * otherwise we resolve the server-side project id once (via
1399
- * `diagnostics.ping`, cached) and target
1400
- * `https://<projectId>.fn.amba.host/<name>`.
1724
+ * Resolve the full invocation URL for a deployed function. Resolution checks
1725
+ * `functionsBaseUrl` first, then uses a UUID-shaped configured project id
1726
+ * directly, and finally falls back to a deduped `diagnostics.ping` for the
1727
+ * authoritative server-side project id. The resolved default-host id is
1728
+ * cached for subsequent invokes.
1401
1729
  */
1402
1730
  async function resolveFunctionUrl(s, name) {
1403
1731
  if (s.functionsBaseUrl) return `${s.functionsBaseUrl.replace(/\/$/, "")}/${encodeURIComponent(name)}`;
1404
- if (!s.resolvedFunctionsProjectId) {
1405
- const ping = await Amba.diagnostics.ping();
1406
- if (!ping.ok || !ping.server_project_id) throw new AmbaApiError("FUNCTIONS_HOST_UNRESOLVED", "Could not resolve the functions host for this project. Pass `functionsBaseUrl` to Amba.configure() to invoke functions explicitly.", ping);
1407
- s.resolvedFunctionsProjectId = ping.server_project_id;
1732
+ return `https://${await resolveFunctionsProjectId(s)}.${DEFAULT_FUNCTIONS_HOST}/${encodeURIComponent(name)}`;
1733
+ }
1734
+ async function resolveFunctionsProjectId(s) {
1735
+ if (s.resolvedFunctionsProjectId) return s.resolvedFunctionsProjectId;
1736
+ if (PROJECT_ID_PATTERN.test(s.projectId)) {
1737
+ s.resolvedFunctionsProjectId = s.projectId.toLowerCase();
1738
+ return s.resolvedFunctionsProjectId;
1408
1739
  }
1409
- return `https://${s.resolvedFunctionsProjectId}.${DEFAULT_FUNCTIONS_HOST}/${encodeURIComponent(name)}`;
1740
+ if (!s.functionsProjectIdInflight) {
1741
+ const resolution = (async () => {
1742
+ const ping = await Amba.diagnostics.ping();
1743
+ if (!ping.ok || !ping.server_project_id) throw new AmbaApiError("FUNCTIONS_HOST_UNRESOLVED", "Could not resolve the functions host for this project. Pass `functionsBaseUrl` to Amba.configure() to invoke functions explicitly.", ping);
1744
+ s.resolvedFunctionsProjectId = ping.server_project_id;
1745
+ return ping.server_project_id;
1746
+ })();
1747
+ s.functionsProjectIdInflight = resolution;
1748
+ resolution.then(() => {
1749
+ if (s.functionsProjectIdInflight === resolution) s.functionsProjectIdInflight = null;
1750
+ }, () => {
1751
+ if (s.functionsProjectIdInflight === resolution) s.functionsProjectIdInflight = null;
1752
+ });
1753
+ }
1754
+ return s.functionsProjectIdInflight;
1410
1755
  }
1411
1756
  /**
1412
1757
  * Bridge the AsyncStorage-backed persistence to the {@link KvStore} the
@@ -1483,7 +1828,16 @@ const Amba = {
1483
1828
  configure: async (config) => {
1484
1829
  if (!config.projectId || config.projectId.trim() === "") throw new Error("Amba.configure: projectId is required");
1485
1830
  if (!config.clientKey || config.clientKey.trim() === "") throw new Error("Amba.configure: clientKey is required");
1831
+ const batchSize = config.events?.batchSize ?? 100;
1832
+ const flushIntervalMs = config.events?.flushIntervalMs ?? 6e4;
1833
+ const maxQueue = config.events?.maxQueue ?? 1e4;
1834
+ if (config.events?.batching) {
1835
+ if (!Number.isInteger(batchSize) || batchSize < 1 || batchSize > 500) throw new Error("Amba.configure: events.batchSize must be 1–500");
1836
+ if (!Number.isInteger(flushIntervalMs) || flushIntervalMs < 0) throw new Error("Amba.configure: events.flushIntervalMs must be a non-negative integer");
1837
+ if (!Number.isInteger(maxQueue) || maxQueue < 1) throw new Error("Amba.configure: events.maxQueue must be positive");
1838
+ }
1486
1839
  const persistence = new AsyncStoragePersistence(config.storagePrefix ?? "amba");
1840
+ if (config.events?.batching) await persistence.assertDurable();
1487
1841
  await persistence.hydrate();
1488
1842
  const identity = await IdentityStore.create(persistence);
1489
1843
  const baseUrl = (config.apiUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
@@ -1502,7 +1856,9 @@ const Amba = {
1502
1856
  const prior = state;
1503
1857
  _offlineEnableToken += 1;
1504
1858
  if (prior?.offlineQueue) await prior.offlineQueue.dispose();
1505
- state = {
1859
+ prior?.detachEventAppState?.();
1860
+ if (prior?.eventQueue) await prior.eventQueue.dispose();
1861
+ const nextState = {
1506
1862
  http,
1507
1863
  identity,
1508
1864
  debug: config.debug ?? false,
@@ -1511,12 +1867,39 @@ const Amba = {
1511
1867
  baseUrl,
1512
1868
  functionsBaseUrl: config.functionsBaseUrl ?? null,
1513
1869
  resolvedFunctionsProjectId: null,
1870
+ functionsProjectIdInflight: null,
1514
1871
  persistence,
1515
1872
  offlineEnabled: false,
1516
- offlineQueue: null
1873
+ offlineQueue: null,
1874
+ eventQueue: null
1517
1875
  };
1876
+ state = nextState;
1877
+ if (config.events?.batching) {
1878
+ const queue = new EventBatchQueue(offlineStore(nextState), `event_outbox:${nextState.projectId}`, {
1879
+ batchSize,
1880
+ flushIntervalMs,
1881
+ maxQueue,
1882
+ onDrop: config.events.onDrop
1883
+ }, () => ({
1884
+ userId: nextState.identity.getAppUserId(),
1885
+ anonymousId: nextState.identity.getAnonymousId(),
1886
+ sessionToken: nextState.identity.getSessionToken()
1887
+ }), (body, token) => nextState.http.postWithSession("/v1/client/events/batch", body, token), (body, token) => nextState.http.postWithSession(P.events, body, token));
1888
+ nextState.eventQueue = queue;
1889
+ await queue.start();
1890
+ try {
1891
+ const { AppState } = require("react-native");
1892
+ const sub = AppState.addEventListener("change", (status) => {
1893
+ if (status === "background") queue.flush();
1894
+ });
1895
+ nextState.detachEventAppState = () => sub.remove();
1896
+ } catch {}
1897
+ }
1898
+ if (!nextState.functionsBaseUrl && !PROJECT_ID_PATTERN.test(nextState.projectId)) resolveFunctionsProjectId(nextState).catch(() => {});
1518
1899
  },
1519
1900
  _reset: () => {
1901
+ state?.detachEventAppState?.();
1902
+ state?.eventQueue?.dispose();
1520
1903
  state = null;
1521
1904
  authSubscribers.clear();
1522
1905
  },
@@ -1545,32 +1928,51 @@ const Amba = {
1545
1928
  authHeaders: realtimeAuthHeaders
1546
1929
  });
1547
1930
  },
1548
- events: { track: async (event, properties, options) => {
1549
- const s = getState();
1550
- const occurredAt = (/* @__PURE__ */ new Date()).toISOString();
1551
- const body = {
1552
- event,
1553
- properties: properties ?? void 0,
1554
- anonymous_id: s.identity.getAnonymousId(),
1555
- timestamp: occurredAt
1556
- };
1557
- if (options?.telemetry) body["telemetry"] = true;
1558
- try {
1559
- await s.http.post(P.events, body);
1560
- } catch (err) {
1561
- if (s.offlineEnabled && isOfflineError(err)) {
1562
- await getOfflineQueue(s).enqueue({
1563
- kind: "track",
1564
- event,
1565
- properties,
1566
- telemetry: options?.telemetry,
1567
- occurredAt
1568
- });
1569
- return;
1931
+ events: {
1932
+ track: async (event, properties, options) => {
1933
+ const s = getState();
1934
+ if (s.eventQueue && !options?.immediate) return s.eventQueue.enqueue(event, properties, options?.telemetry === true);
1935
+ const occurredAt = (/* @__PURE__ */ new Date()).toISOString();
1936
+ const body = {
1937
+ event,
1938
+ properties: properties ?? void 0,
1939
+ anonymous_id: s.identity.getAnonymousId(),
1940
+ timestamp: occurredAt
1941
+ };
1942
+ if (options?.telemetry) body["telemetry"] = true;
1943
+ try {
1944
+ await s.http.post(P.events, body);
1945
+ } catch (err) {
1946
+ if (s.offlineEnabled && isOfflineError(err)) {
1947
+ await getOfflineQueue(s).enqueue({
1948
+ kind: "track",
1949
+ event,
1950
+ properties,
1951
+ telemetry: options?.telemetry,
1952
+ occurredAt
1953
+ });
1954
+ return;
1955
+ }
1956
+ throw err;
1570
1957
  }
1571
- throw err;
1958
+ },
1959
+ flush: async () => getState().eventQueue?.flush() ?? {
1960
+ sent: 0,
1961
+ rejected: 0,
1962
+ remaining: 0
1963
+ },
1964
+ pendingCount: async (userId) => getState().eventQueue?.pendingCount(userId) ?? 0,
1965
+ clear: async ({ userId }) => {
1966
+ if (!userId) throw new Error("Amba.events.clear: userId is required");
1967
+ await getState().eventQueue?.clear(userId);
1968
+ },
1969
+ setOnline: (online) => {
1970
+ getState().eventQueue?.setOnline(online);
1971
+ },
1972
+ setBatchingEnabled: (enabled) => {
1973
+ getState().eventQueue?.setBatchingEnabled(enabled);
1572
1974
  }
1573
- } },
1975
+ },
1574
1976
  auth: {
1575
1977
  signInAnonymously: async () => {
1576
1978
  const s = getState();
@@ -1707,6 +2109,7 @@ const Amba = {
1707
2109
  if (typeof console !== "undefined") console.warn("[amba] signOut server call failed", err);
1708
2110
  }
1709
2111
  await s.identity.clearSession(rotateAnonymousId);
2112
+ s.eventQueue?.flush();
1710
2113
  notifyAuthSubscribers(null);
1711
2114
  },
1712
2115
  refresh: async () => {
@@ -1784,7 +2187,16 @@ const Amba = {
1784
2187
  isEnabled: () => getState().offlineEnabled,
1785
2188
  pendingCount: async () => getOfflineQueue(getState()).pendingCount(),
1786
2189
  pending: async () => getOfflineQueue(getState()).pending(),
1787
- flush: async () => getOfflineQueue(getState()).flush(),
2190
+ flush: async () => {
2191
+ const s = getState();
2192
+ const result = s.offlineEnabled ? await getOfflineQueue(s).flush() : {
2193
+ flushed: 0,
2194
+ dropped: 0,
2195
+ remaining: s.offlineQueue ? await s.offlineQueue.pendingCount() : 0
2196
+ };
2197
+ await s.eventQueue?.flush();
2198
+ return result;
2199
+ },
1788
2200
  clear: async () => getOfflineQueue(getState()).clear()
1789
2201
  },
1790
2202
  collections: {
@@ -2419,10 +2831,7 @@ const Amba = {
2419
2831
  complete: async () => {
2420
2832
  return (await getState().http.post(`${P.onboarding}/complete`, {})).data;
2421
2833
  }
2422
- },
2423
- dev: { resetSandbox: async () => {
2424
- return (await getState().http.post("/v1/client/dev/reset-sandbox", {})).data;
2425
- } }
2834
+ }
2426
2835
  };
2427
2836
 
2428
2837
  //#endregion