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