@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.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
|
|
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.
|
|
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
|
|
@@ -1201,6 +1535,58 @@ function notifyAuthSubscribers(session) {
|
|
|
1201
1535
|
if (typeof console !== "undefined") console.error("[amba] onAuthStateChange callback threw", err);
|
|
1202
1536
|
}
|
|
1203
1537
|
}
|
|
1538
|
+
function normalizeAuthProviderNames(value) {
|
|
1539
|
+
let normalized = value;
|
|
1540
|
+
if (typeof normalized === "string") {
|
|
1541
|
+
const trimmed = normalized.trim();
|
|
1542
|
+
if (trimmed === "") return [];
|
|
1543
|
+
try {
|
|
1544
|
+
normalized = JSON.parse(trimmed);
|
|
1545
|
+
} catch {
|
|
1546
|
+
return [trimmed];
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
if (!Array.isArray(normalized)) {
|
|
1550
|
+
if (normalized && typeof normalized === "object") {
|
|
1551
|
+
const provider = normalized.provider;
|
|
1552
|
+
return typeof provider === "string" ? [provider] : [];
|
|
1553
|
+
}
|
|
1554
|
+
return [];
|
|
1555
|
+
}
|
|
1556
|
+
return normalized.map((entry) => {
|
|
1557
|
+
if (typeof entry === "string") {
|
|
1558
|
+
const trimmed = entry.trim();
|
|
1559
|
+
if (trimmed === "") return null;
|
|
1560
|
+
try {
|
|
1561
|
+
const parsed = JSON.parse(trimmed);
|
|
1562
|
+
if (parsed && typeof parsed === "object") {
|
|
1563
|
+
const provider = parsed.provider;
|
|
1564
|
+
return typeof provider === "string" ? provider : null;
|
|
1565
|
+
}
|
|
1566
|
+
} catch {
|
|
1567
|
+
return entry;
|
|
1568
|
+
}
|
|
1569
|
+
return entry;
|
|
1570
|
+
}
|
|
1571
|
+
if (entry && typeof entry === "object") {
|
|
1572
|
+
const provider = entry.provider;
|
|
1573
|
+
return typeof provider === "string" ? provider : null;
|
|
1574
|
+
}
|
|
1575
|
+
return null;
|
|
1576
|
+
}).filter((provider) => provider !== null);
|
|
1577
|
+
}
|
|
1578
|
+
function normalizeUserAuthProviders(user) {
|
|
1579
|
+
return {
|
|
1580
|
+
...user,
|
|
1581
|
+
auth_providers: normalizeAuthProviderNames(user.auth_providers)
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
function normalizeAuthResult(result) {
|
|
1585
|
+
return {
|
|
1586
|
+
...result,
|
|
1587
|
+
user: normalizeUserAuthProviders(result.user)
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1204
1590
|
async function notifyFromAuthResult(result) {
|
|
1205
1591
|
notifyAuthSubscribers(snapshotSession(result.user));
|
|
1206
1592
|
}
|
|
@@ -1217,8 +1603,9 @@ async function persistAuthResult(state_, result) {
|
|
|
1217
1603
|
});
|
|
1218
1604
|
}
|
|
1219
1605
|
async function postAuth(state_, path, body) {
|
|
1220
|
-
const result = (await state_.http.post(path, body)).data;
|
|
1606
|
+
const result = normalizeAuthResult((await state_.http.post(path, body)).data);
|
|
1221
1607
|
await persistAuthResult(state_, result);
|
|
1608
|
+
state_.eventQueue?.flush();
|
|
1222
1609
|
await notifyFromAuthResult(result);
|
|
1223
1610
|
return result;
|
|
1224
1611
|
}
|
|
@@ -1249,7 +1636,7 @@ async function ensureAnonymousSession(state_) {
|
|
|
1249
1636
|
* Existing session tokens stay untouched.
|
|
1250
1637
|
*/
|
|
1251
1638
|
async function postUserNoRotate(state_, path, body) {
|
|
1252
|
-
return (await state_.http.post(path, body)).data.user;
|
|
1639
|
+
return normalizeUserAuthProviders((await state_.http.post(path, body)).data.user);
|
|
1253
1640
|
}
|
|
1254
1641
|
async function unlinkProvider(provider) {
|
|
1255
1642
|
const s = getState();
|
|
@@ -1340,20 +1727,37 @@ function encodePathSegment(s) {
|
|
|
1340
1727
|
return encodeURIComponent(s);
|
|
1341
1728
|
}
|
|
1342
1729
|
/**
|
|
1343
|
-
* Resolve the full invocation URL for a deployed function.
|
|
1344
|
-
* `functionsBaseUrl`
|
|
1345
|
-
*
|
|
1346
|
-
*
|
|
1347
|
-
*
|
|
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.
|
|
1348
1735
|
*/
|
|
1349
1736
|
async function resolveFunctionUrl(s, name) {
|
|
1350
1737
|
if (s.functionsBaseUrl) return `${s.functionsBaseUrl.replace(/\/$/, "")}/${encodeURIComponent(name)}`;
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
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;
|
|
1355
1745
|
}
|
|
1356
|
-
|
|
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
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
return s.functionsProjectIdInflight;
|
|
1357
1761
|
}
|
|
1358
1762
|
/**
|
|
1359
1763
|
* Bridge the AsyncStorage-backed persistence to the {@link KvStore} the
|
|
@@ -1430,7 +1834,16 @@ const Amba = {
|
|
|
1430
1834
|
configure: async (config) => {
|
|
1431
1835
|
if (!config.projectId || config.projectId.trim() === "") throw new Error("Amba.configure: projectId is required");
|
|
1432
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
|
+
}
|
|
1433
1845
|
const persistence = new AsyncStoragePersistence(config.storagePrefix ?? "amba");
|
|
1846
|
+
if (config.events?.batching) await persistence.assertDurable();
|
|
1434
1847
|
await persistence.hydrate();
|
|
1435
1848
|
const identity = await IdentityStore.create(persistence);
|
|
1436
1849
|
const baseUrl = (config.apiUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
@@ -1449,7 +1862,9 @@ const Amba = {
|
|
|
1449
1862
|
const prior = state;
|
|
1450
1863
|
_offlineEnableToken += 1;
|
|
1451
1864
|
if (prior?.offlineQueue) await prior.offlineQueue.dispose();
|
|
1452
|
-
|
|
1865
|
+
prior?.detachEventAppState?.();
|
|
1866
|
+
if (prior?.eventQueue) await prior.eventQueue.dispose();
|
|
1867
|
+
const nextState = {
|
|
1453
1868
|
http,
|
|
1454
1869
|
identity,
|
|
1455
1870
|
debug: config.debug ?? false,
|
|
@@ -1458,12 +1873,39 @@ const Amba = {
|
|
|
1458
1873
|
baseUrl,
|
|
1459
1874
|
functionsBaseUrl: config.functionsBaseUrl ?? null,
|
|
1460
1875
|
resolvedFunctionsProjectId: null,
|
|
1876
|
+
functionsProjectIdInflight: null,
|
|
1461
1877
|
persistence,
|
|
1462
1878
|
offlineEnabled: false,
|
|
1463
|
-
offlineQueue: null
|
|
1879
|
+
offlineQueue: null,
|
|
1880
|
+
eventQueue: null
|
|
1464
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(() => {});
|
|
1465
1905
|
},
|
|
1466
1906
|
_reset: () => {
|
|
1907
|
+
state?.detachEventAppState?.();
|
|
1908
|
+
state?.eventQueue?.dispose();
|
|
1467
1909
|
state = null;
|
|
1468
1910
|
authSubscribers.clear();
|
|
1469
1911
|
},
|
|
@@ -1492,32 +1934,51 @@ const Amba = {
|
|
|
1492
1934
|
authHeaders: realtimeAuthHeaders
|
|
1493
1935
|
});
|
|
1494
1936
|
},
|
|
1495
|
-
events: {
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
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;
|
|
1517
1963
|
}
|
|
1518
|
-
|
|
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);
|
|
1519
1980
|
}
|
|
1520
|
-
}
|
|
1981
|
+
},
|
|
1521
1982
|
auth: {
|
|
1522
1983
|
signInAnonymously: async () => {
|
|
1523
1984
|
const s = getState();
|
|
@@ -1654,6 +2115,7 @@ const Amba = {
|
|
|
1654
2115
|
if (typeof console !== "undefined") console.warn("[amba] signOut server call failed", err);
|
|
1655
2116
|
}
|
|
1656
2117
|
await s.identity.clearSession(rotateAnonymousId);
|
|
2118
|
+
s.eventQueue?.flush();
|
|
1657
2119
|
notifyAuthSubscribers(null);
|
|
1658
2120
|
},
|
|
1659
2121
|
refresh: async () => {
|
|
@@ -1665,12 +2127,12 @@ const Amba = {
|
|
|
1665
2127
|
me: async () => {
|
|
1666
2128
|
const s = getState();
|
|
1667
2129
|
if (!s.identity.getSessionToken()) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
|
|
1668
|
-
return (await s.http.get(P.authMe)).data;
|
|
2130
|
+
return normalizeUserAuthProviders((await s.http.get(P.authMe)).data);
|
|
1669
2131
|
},
|
|
1670
2132
|
getSession: async () => {
|
|
1671
2133
|
const s = getState();
|
|
1672
2134
|
if (!s.identity.isAuthenticated()) return null;
|
|
1673
|
-
return snapshotSession((await s.http.get(P.authMe)).data);
|
|
2135
|
+
return snapshotSession(normalizeUserAuthProviders((await s.http.get(P.authMe)).data));
|
|
1674
2136
|
},
|
|
1675
2137
|
getAnonymousId: async () => {
|
|
1676
2138
|
return getState().identity.getAnonymousId();
|
|
@@ -1687,7 +2149,7 @@ const Amba = {
|
|
|
1687
2149
|
const s = getState();
|
|
1688
2150
|
if (!s.identity.getSessionToken()) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
|
|
1689
2151
|
const path = userId === void 0 || userId === null || userId === "" ? P.usersMe : `${P.users}/${encodePathSegment(userId)}`;
|
|
1690
|
-
return (await s.http.get(path)).data;
|
|
2152
|
+
return normalizeUserAuthProviders((await s.http.get(path)).data);
|
|
1691
2153
|
},
|
|
1692
2154
|
update: ((arg1, arg2) => {
|
|
1693
2155
|
const s = getState();
|
|
@@ -1695,7 +2157,7 @@ const Amba = {
|
|
|
1695
2157
|
const isById = typeof arg1 === "string";
|
|
1696
2158
|
const patch = isById ? arg2 ?? {} : arg1;
|
|
1697
2159
|
const path = isById ? `${P.users}/${encodePathSegment(arg1)}` : `${P.users}/me`;
|
|
1698
|
-
return s.http.patch(path, patch).then((env) => env.data);
|
|
2160
|
+
return s.http.patch(path, patch).then((env) => normalizeUserAuthProviders(env.data));
|
|
1699
2161
|
})
|
|
1700
2162
|
},
|
|
1701
2163
|
sessions: {
|
|
@@ -1731,7 +2193,16 @@ const Amba = {
|
|
|
1731
2193
|
isEnabled: () => getState().offlineEnabled,
|
|
1732
2194
|
pendingCount: async () => getOfflineQueue(getState()).pendingCount(),
|
|
1733
2195
|
pending: async () => getOfflineQueue(getState()).pending(),
|
|
1734
|
-
flush: async () =>
|
|
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
|
+
},
|
|
1735
2206
|
clear: async () => getOfflineQueue(getState()).clear()
|
|
1736
2207
|
},
|
|
1737
2208
|
collections: {
|
|
@@ -2366,10 +2837,7 @@ const Amba = {
|
|
|
2366
2837
|
complete: async () => {
|
|
2367
2838
|
return (await getState().http.post(`${P.onboarding}/complete`, {})).data;
|
|
2368
2839
|
}
|
|
2369
|
-
}
|
|
2370
|
-
dev: { resetSandbox: async () => {
|
|
2371
|
-
return (await getState().http.post("/v1/client/dev/reset-sandbox", {})).data;
|
|
2372
|
-
} }
|
|
2840
|
+
}
|
|
2373
2841
|
};
|
|
2374
2842
|
|
|
2375
2843
|
//#endregion
|