@layers/amba-react-native 4.0.5 → 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 +514 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +205 -130
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +205 -130
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +521 -53
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
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.
|
|
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
|
|
@@ -1202,6 +1529,58 @@ function notifyAuthSubscribers(session) {
|
|
|
1202
1529
|
if (typeof console !== "undefined") console.error("[amba] onAuthStateChange callback threw", err);
|
|
1203
1530
|
}
|
|
1204
1531
|
}
|
|
1532
|
+
function normalizeAuthProviderNames(value) {
|
|
1533
|
+
let normalized = value;
|
|
1534
|
+
if (typeof normalized === "string") {
|
|
1535
|
+
const trimmed = normalized.trim();
|
|
1536
|
+
if (trimmed === "") return [];
|
|
1537
|
+
try {
|
|
1538
|
+
normalized = JSON.parse(trimmed);
|
|
1539
|
+
} catch {
|
|
1540
|
+
return [trimmed];
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
if (!Array.isArray(normalized)) {
|
|
1544
|
+
if (normalized && typeof normalized === "object") {
|
|
1545
|
+
const provider = normalized.provider;
|
|
1546
|
+
return typeof provider === "string" ? [provider] : [];
|
|
1547
|
+
}
|
|
1548
|
+
return [];
|
|
1549
|
+
}
|
|
1550
|
+
return normalized.map((entry) => {
|
|
1551
|
+
if (typeof entry === "string") {
|
|
1552
|
+
const trimmed = entry.trim();
|
|
1553
|
+
if (trimmed === "") return null;
|
|
1554
|
+
try {
|
|
1555
|
+
const parsed = JSON.parse(trimmed);
|
|
1556
|
+
if (parsed && typeof parsed === "object") {
|
|
1557
|
+
const provider = parsed.provider;
|
|
1558
|
+
return typeof provider === "string" ? provider : null;
|
|
1559
|
+
}
|
|
1560
|
+
} catch {
|
|
1561
|
+
return entry;
|
|
1562
|
+
}
|
|
1563
|
+
return entry;
|
|
1564
|
+
}
|
|
1565
|
+
if (entry && typeof entry === "object") {
|
|
1566
|
+
const provider = entry.provider;
|
|
1567
|
+
return typeof provider === "string" ? provider : null;
|
|
1568
|
+
}
|
|
1569
|
+
return null;
|
|
1570
|
+
}).filter((provider) => provider !== null);
|
|
1571
|
+
}
|
|
1572
|
+
function normalizeUserAuthProviders(user) {
|
|
1573
|
+
return {
|
|
1574
|
+
...user,
|
|
1575
|
+
auth_providers: normalizeAuthProviderNames(user.auth_providers)
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
function normalizeAuthResult(result) {
|
|
1579
|
+
return {
|
|
1580
|
+
...result,
|
|
1581
|
+
user: normalizeUserAuthProviders(result.user)
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1205
1584
|
async function notifyFromAuthResult(result) {
|
|
1206
1585
|
notifyAuthSubscribers(snapshotSession(result.user));
|
|
1207
1586
|
}
|
|
@@ -1218,8 +1597,9 @@ async function persistAuthResult(state_, result) {
|
|
|
1218
1597
|
});
|
|
1219
1598
|
}
|
|
1220
1599
|
async function postAuth(state_, path, body) {
|
|
1221
|
-
const result = (await state_.http.post(path, body)).data;
|
|
1600
|
+
const result = normalizeAuthResult((await state_.http.post(path, body)).data);
|
|
1222
1601
|
await persistAuthResult(state_, result);
|
|
1602
|
+
state_.eventQueue?.flush();
|
|
1223
1603
|
await notifyFromAuthResult(result);
|
|
1224
1604
|
return result;
|
|
1225
1605
|
}
|
|
@@ -1250,7 +1630,7 @@ async function ensureAnonymousSession(state_) {
|
|
|
1250
1630
|
* Existing session tokens stay untouched.
|
|
1251
1631
|
*/
|
|
1252
1632
|
async function postUserNoRotate(state_, path, body) {
|
|
1253
|
-
return (await state_.http.post(path, body)).data.user;
|
|
1633
|
+
return normalizeUserAuthProviders((await state_.http.post(path, body)).data.user);
|
|
1254
1634
|
}
|
|
1255
1635
|
async function unlinkProvider(provider) {
|
|
1256
1636
|
const s = getState();
|
|
@@ -1341,20 +1721,37 @@ function encodePathSegment(s) {
|
|
|
1341
1721
|
return encodeURIComponent(s);
|
|
1342
1722
|
}
|
|
1343
1723
|
/**
|
|
1344
|
-
* Resolve the full invocation URL for a deployed function.
|
|
1345
|
-
* `functionsBaseUrl`
|
|
1346
|
-
*
|
|
1347
|
-
*
|
|
1348
|
-
*
|
|
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.
|
|
1349
1729
|
*/
|
|
1350
1730
|
async function resolveFunctionUrl(s, name) {
|
|
1351
1731
|
if (s.functionsBaseUrl) return `${s.functionsBaseUrl.replace(/\/$/, "")}/${encodeURIComponent(name)}`;
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
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;
|
|
1356
1739
|
}
|
|
1357
|
-
|
|
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;
|
|
1358
1755
|
}
|
|
1359
1756
|
/**
|
|
1360
1757
|
* Bridge the AsyncStorage-backed persistence to the {@link KvStore} the
|
|
@@ -1431,7 +1828,16 @@ const Amba = {
|
|
|
1431
1828
|
configure: async (config) => {
|
|
1432
1829
|
if (!config.projectId || config.projectId.trim() === "") throw new Error("Amba.configure: projectId is required");
|
|
1433
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
|
+
}
|
|
1434
1839
|
const persistence = new AsyncStoragePersistence(config.storagePrefix ?? "amba");
|
|
1840
|
+
if (config.events?.batching) await persistence.assertDurable();
|
|
1435
1841
|
await persistence.hydrate();
|
|
1436
1842
|
const identity = await IdentityStore.create(persistence);
|
|
1437
1843
|
const baseUrl = (config.apiUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
@@ -1450,7 +1856,9 @@ const Amba = {
|
|
|
1450
1856
|
const prior = state;
|
|
1451
1857
|
_offlineEnableToken += 1;
|
|
1452
1858
|
if (prior?.offlineQueue) await prior.offlineQueue.dispose();
|
|
1453
|
-
|
|
1859
|
+
prior?.detachEventAppState?.();
|
|
1860
|
+
if (prior?.eventQueue) await prior.eventQueue.dispose();
|
|
1861
|
+
const nextState = {
|
|
1454
1862
|
http,
|
|
1455
1863
|
identity,
|
|
1456
1864
|
debug: config.debug ?? false,
|
|
@@ -1459,12 +1867,39 @@ const Amba = {
|
|
|
1459
1867
|
baseUrl,
|
|
1460
1868
|
functionsBaseUrl: config.functionsBaseUrl ?? null,
|
|
1461
1869
|
resolvedFunctionsProjectId: null,
|
|
1870
|
+
functionsProjectIdInflight: null,
|
|
1462
1871
|
persistence,
|
|
1463
1872
|
offlineEnabled: false,
|
|
1464
|
-
offlineQueue: null
|
|
1873
|
+
offlineQueue: null,
|
|
1874
|
+
eventQueue: null
|
|
1465
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(() => {});
|
|
1466
1899
|
},
|
|
1467
1900
|
_reset: () => {
|
|
1901
|
+
state?.detachEventAppState?.();
|
|
1902
|
+
state?.eventQueue?.dispose();
|
|
1468
1903
|
state = null;
|
|
1469
1904
|
authSubscribers.clear();
|
|
1470
1905
|
},
|
|
@@ -1493,32 +1928,51 @@ const Amba = {
|
|
|
1493
1928
|
authHeaders: realtimeAuthHeaders
|
|
1494
1929
|
});
|
|
1495
1930
|
},
|
|
1496
|
-
events: {
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
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;
|
|
1518
1957
|
}
|
|
1519
|
-
|
|
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);
|
|
1520
1974
|
}
|
|
1521
|
-
}
|
|
1975
|
+
},
|
|
1522
1976
|
auth: {
|
|
1523
1977
|
signInAnonymously: async () => {
|
|
1524
1978
|
const s = getState();
|
|
@@ -1655,6 +2109,7 @@ const Amba = {
|
|
|
1655
2109
|
if (typeof console !== "undefined") console.warn("[amba] signOut server call failed", err);
|
|
1656
2110
|
}
|
|
1657
2111
|
await s.identity.clearSession(rotateAnonymousId);
|
|
2112
|
+
s.eventQueue?.flush();
|
|
1658
2113
|
notifyAuthSubscribers(null);
|
|
1659
2114
|
},
|
|
1660
2115
|
refresh: async () => {
|
|
@@ -1666,12 +2121,12 @@ const Amba = {
|
|
|
1666
2121
|
me: async () => {
|
|
1667
2122
|
const s = getState();
|
|
1668
2123
|
if (!s.identity.getSessionToken()) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
|
|
1669
|
-
return (await s.http.get(P.authMe)).data;
|
|
2124
|
+
return normalizeUserAuthProviders((await s.http.get(P.authMe)).data);
|
|
1670
2125
|
},
|
|
1671
2126
|
getSession: async () => {
|
|
1672
2127
|
const s = getState();
|
|
1673
2128
|
if (!s.identity.isAuthenticated()) return null;
|
|
1674
|
-
return snapshotSession((await s.http.get(P.authMe)).data);
|
|
2129
|
+
return snapshotSession(normalizeUserAuthProviders((await s.http.get(P.authMe)).data));
|
|
1675
2130
|
},
|
|
1676
2131
|
getAnonymousId: async () => {
|
|
1677
2132
|
return getState().identity.getAnonymousId();
|
|
@@ -1688,7 +2143,7 @@ const Amba = {
|
|
|
1688
2143
|
const s = getState();
|
|
1689
2144
|
if (!s.identity.getSessionToken()) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
|
|
1690
2145
|
const path = userId === void 0 || userId === null || userId === "" ? P.usersMe : `${P.users}/${encodePathSegment(userId)}`;
|
|
1691
|
-
return (await s.http.get(path)).data;
|
|
2146
|
+
return normalizeUserAuthProviders((await s.http.get(path)).data);
|
|
1692
2147
|
},
|
|
1693
2148
|
update: ((arg1, arg2) => {
|
|
1694
2149
|
const s = getState();
|
|
@@ -1696,7 +2151,7 @@ const Amba = {
|
|
|
1696
2151
|
const isById = typeof arg1 === "string";
|
|
1697
2152
|
const patch = isById ? arg2 ?? {} : arg1;
|
|
1698
2153
|
const path = isById ? `${P.users}/${encodePathSegment(arg1)}` : `${P.users}/me`;
|
|
1699
|
-
return s.http.patch(path, patch).then((env) => env.data);
|
|
2154
|
+
return s.http.patch(path, patch).then((env) => normalizeUserAuthProviders(env.data));
|
|
1700
2155
|
})
|
|
1701
2156
|
},
|
|
1702
2157
|
sessions: {
|
|
@@ -1732,7 +2187,16 @@ const Amba = {
|
|
|
1732
2187
|
isEnabled: () => getState().offlineEnabled,
|
|
1733
2188
|
pendingCount: async () => getOfflineQueue(getState()).pendingCount(),
|
|
1734
2189
|
pending: async () => getOfflineQueue(getState()).pending(),
|
|
1735
|
-
flush: async () =>
|
|
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
|
+
},
|
|
1736
2200
|
clear: async () => getOfflineQueue(getState()).clear()
|
|
1737
2201
|
},
|
|
1738
2202
|
collections: {
|
|
@@ -2367,10 +2831,7 @@ const Amba = {
|
|
|
2367
2831
|
complete: async () => {
|
|
2368
2832
|
return (await getState().http.post(`${P.onboarding}/complete`, {})).data;
|
|
2369
2833
|
}
|
|
2370
|
-
}
|
|
2371
|
-
dev: { resetSandbox: async () => {
|
|
2372
|
-
return (await getState().http.post("/v1/client/dev/reset-sandbox", {})).data;
|
|
2373
|
-
} }
|
|
2834
|
+
}
|
|
2374
2835
|
};
|
|
2375
2836
|
|
|
2376
2837
|
//#endregion
|