@layers/amba-web 4.0.10 → 4.0.11
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/README.md +11 -10
- package/dist/index.browser.d.ts +47 -4
- package/dist/index.browser.d.ts.map +1 -1
- package/dist/index.browser.js +240 -69
- package/dist/index.browser.js.map +1 -1
- package/dist/index.d.ts +47 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +240 -69
- package/dist/index.js.map +1 -1
- package/dist-rn/index.d.ts +47 -4
- package/dist-rn/index.js +240 -69
- package/package.json +2 -2
package/dist-rn/index.d.ts
CHANGED
|
@@ -38,6 +38,26 @@ interface AmbaConfig {
|
|
|
38
38
|
debug?: boolean;
|
|
39
39
|
/** Override the localStorage key prefix. Defaults to `"amba"`. */
|
|
40
40
|
storagePrefix?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Where the SDK keeps the signed-in session between page loads.
|
|
43
|
+
*
|
|
44
|
+
* - `"local"` (default): the session token, refresh token, user id and
|
|
45
|
+
* anonymous id are saved in `localStorage`, so a reload or a new tab
|
|
46
|
+
* stays signed in. Any script running on your origin can read
|
|
47
|
+
* `localStorage`, so a cross-site scripting bug in the page can copy the
|
|
48
|
+
* refresh token (valid for 90 days) and keep using the session after the
|
|
49
|
+
* tab closes.
|
|
50
|
+
* - `"memory"`: none of that identity is written to storage; it lives in
|
|
51
|
+
* this page's memory and ends with it. A script injected later finds no
|
|
52
|
+
* stored token to copy. Every reload or new tab starts signed out with a
|
|
53
|
+
* new anonymous identity, so users sign in again each visit. Configuring
|
|
54
|
+
* with `"memory"` also removes an identity an earlier `"local"`
|
|
55
|
+
* configuration saved for this project.
|
|
56
|
+
*
|
|
57
|
+
* Queued offline writes (`Amba.offline`) stay in `localStorage` in both
|
|
58
|
+
* modes.
|
|
59
|
+
*/
|
|
60
|
+
sessionPersistence?: SessionPersistence;
|
|
41
61
|
/**
|
|
42
62
|
* Advanced: explicit URL or RequestInfo for the @layers/amba-core WASM file.
|
|
43
63
|
* Browser bundlers should not need this; the browser entry supplies a
|
|
@@ -50,6 +70,8 @@ interface AmbaConfig {
|
|
|
50
70
|
*/
|
|
51
71
|
wasmModule?: Response | BufferSource | WebAssembly.Module;
|
|
52
72
|
}
|
|
73
|
+
/** `AmbaConfig.sessionPersistence` values. */
|
|
74
|
+
type SessionPersistence = "local" | "memory";
|
|
53
75
|
type SocialProvider = "apple" | "google";
|
|
54
76
|
type PushPlatform = "apns" | "fcm" | "web";
|
|
55
77
|
interface User {
|
|
@@ -1244,6 +1266,10 @@ interface AiStreamOptions {
|
|
|
1244
1266
|
* - On reconnect (`online` event) the outbox is flushed in FIFO order;
|
|
1245
1267
|
* each op is removed only after it succeeds, so a mid-flush failure
|
|
1246
1268
|
* leaves the remainder intact for the next attempt.
|
|
1269
|
+
* - Each op records its owner (the user id and anonymous id live when it
|
|
1270
|
+
* was queued). A flush replays only the current owner's ops and holds
|
|
1271
|
+
* the rest until that owner signs back in; `pending()` lists only the
|
|
1272
|
+
* current owner's ops. The SDKs clear the whole outbox on sign-out.
|
|
1247
1273
|
*/
|
|
1248
1274
|
/**
|
|
1249
1275
|
* Minimal durable key/value contract the outbox persists through. Both the
|
|
@@ -1255,6 +1281,14 @@ interface KvStore {
|
|
|
1255
1281
|
set(key: string, value: string): Promise<void> | void;
|
|
1256
1282
|
delete(key: string): Promise<void> | void;
|
|
1257
1283
|
}
|
|
1284
|
+
/**
|
|
1285
|
+
* The identity a queued op belongs to: the signed-in user id (null before any
|
|
1286
|
+
* session) and the install's anonymous id, both captured at enqueue.
|
|
1287
|
+
*/
|
|
1288
|
+
interface OutboxOwner {
|
|
1289
|
+
appUserId: string | null;
|
|
1290
|
+
anonymousId: string | null;
|
|
1291
|
+
}
|
|
1258
1292
|
/** The kinds of mutation the outbox can durably replay. */
|
|
1259
1293
|
type OutboxOpKind = "collection.insert" | "collection.update" | "collection.delete" | "track";
|
|
1260
1294
|
/**
|
|
@@ -1288,6 +1322,11 @@ interface OutboxOp {
|
|
|
1288
1322
|
* session user at replay time — the events route has no body-id channel.)
|
|
1289
1323
|
*/
|
|
1290
1324
|
occurredAt?: string;
|
|
1325
|
+
/**
|
|
1326
|
+
* Who queued the op. Stamped at enqueue when the queue has a `currentOwner`
|
|
1327
|
+
* source; a flush replays the op only while the same owner is signed in.
|
|
1328
|
+
*/
|
|
1329
|
+
owner?: OutboxOwner;
|
|
1291
1330
|
enqueuedAt: string;
|
|
1292
1331
|
}
|
|
1293
1332
|
/** Result of a {@link OfflineQueue.flush}. */
|
|
@@ -1659,8 +1698,12 @@ declare const Amba: {
|
|
|
1659
1698
|
* password)`. Requires an active session — throws `UNAUTHORIZED`
|
|
1660
1699
|
* otherwise.
|
|
1661
1700
|
*
|
|
1662
|
-
*
|
|
1663
|
-
*
|
|
1701
|
+
* The address stays unverified (`email_verified = false`) until the
|
|
1702
|
+
* inbox is proven, unless this user had already verified the same
|
|
1703
|
+
* address. `linkEmailOtp` with the same address from this session
|
|
1704
|
+
* verifies it and keeps the password.
|
|
1705
|
+
*
|
|
1706
|
+
* @returns the updated user record (with `email` set).
|
|
1664
1707
|
*/
|
|
1665
1708
|
linkEmailPassword: (email: string, password: string) => Promise<User>;
|
|
1666
1709
|
/**
|
|
@@ -2511,7 +2554,7 @@ declare function getCoreWasm(): AmbaCoreWasm;
|
|
|
2511
2554
|
* `verify-sdk-version-coherence` GH action gates every publish on the
|
|
2512
2555
|
* pair agreeing (and matching the `web-v<X.Y.Z>` tag when tag-triggered).
|
|
2513
2556
|
*/
|
|
2514
|
-
declare const SDK_VERSION = "4.0.
|
|
2557
|
+
declare const SDK_VERSION = "4.0.11";
|
|
2515
2558
|
//#endregion
|
|
2516
|
-
export { Achievement, AchievementProgress, AiContentBlock, AiImageInput, AiMessage, AiMessageRequest, AiMessageResponse, type AiStreamDelta, type AiStreamDone, type AiStreamEvent, type AiStreamOptions, AiUsage, Amba, AmbaApiError, type AmbaApiErrorCode, AmbaConfig, AmbaGroupScopedClient, AndFilter, AppSession, AuthResult, CatalogItem, Challenge, ChallengeProgress, CollectionSubscription, ConditionFilter, ConfigBundle, ConsumeRequest, ContentItem, Conversation, ConversationParticipant, ConversationSubscription, ConversationType, CreateConversationRequest, CurrencyBalance, CurrencyTransaction, DeepLink, DeepLinkCreate, DeleteManyResult, DiagnosticsEnvironment, EntitlementPeriodType, EntitlementStore, EventExplainResult, ExplainMatchedRule, FeedItem, FeedResponse, Filter, FilterValue, FindOptions, FindResponse, FlagAssignment, type FlushResult, Friendship, FriendshipState, type GamificationChange, type GamificationKind, GamificationSubscription, Group, GroupCreate, GroupMember, GroupUpdate, IncludeClause, InventoryItem, InvokeOptions, type KvStore, Leaderboard, LeaderboardEntry, LeagueCohort, LeagueCohortMember, LeagueCohortView, LeagueMembership, LeagueTier, LinkedIdentifier, ListMessagesOptions, LocalNotificationHandle, LocalNotificationTrigger, type LocalNotificationsNamespace, MarkReadResult, MediaAsset, MediaCatalog, MediaCatalogItem, MediaListResponse, Message, type MessageChange, MessageType, NearestOptions, NearestResponse, NotFilter, Offering, OfferingPackage, OfferingProduct, OfferingsResult, OnboardingStatus, OrFilter, OrderBy, type OutboxOp, type OutboxOpKind, PingResult, PresignData, PullChangesCursor, PullChangesResult, PurchaseOption, PurchaseRequest, PurchaseResult, PushChangesResult, PushPlatform, PushToken, type RealtimeChange, ReferralClaim, ReferralCode, Report, ReportRequest, RestoreResult, Review, ReviewCreate, ReviewUpdate, Role, type RowChangeOp, SDK_VERSION, ScheduleLocalNotificationInput, ScheduledLocalNotification, SendMessageRequest, Session, SocialProvider, Store, Streak, type SubscribeOptions, SyncChange, SyncConflict, TransactionOp, TransactionOpResult, type Unsubscribe, UpsertOptions, User, UserEntitlement, XpBalance, XpTransaction, getCoreWasm, withOptimistic };
|
|
2559
|
+
export { Achievement, AchievementProgress, AiContentBlock, AiImageInput, AiMessage, AiMessageRequest, AiMessageResponse, type AiStreamDelta, type AiStreamDone, type AiStreamEvent, type AiStreamOptions, AiUsage, Amba, AmbaApiError, type AmbaApiErrorCode, AmbaConfig, AmbaGroupScopedClient, AndFilter, AppSession, AuthResult, CatalogItem, Challenge, ChallengeProgress, CollectionSubscription, ConditionFilter, ConfigBundle, ConsumeRequest, ContentItem, Conversation, ConversationParticipant, ConversationSubscription, ConversationType, CreateConversationRequest, CurrencyBalance, CurrencyTransaction, DeepLink, DeepLinkCreate, DeleteManyResult, DiagnosticsEnvironment, EntitlementPeriodType, EntitlementStore, EventExplainResult, ExplainMatchedRule, FeedItem, FeedResponse, Filter, FilterValue, FindOptions, FindResponse, FlagAssignment, type FlushResult, Friendship, FriendshipState, type GamificationChange, type GamificationKind, GamificationSubscription, Group, GroupCreate, GroupMember, GroupUpdate, IncludeClause, InventoryItem, InvokeOptions, type KvStore, Leaderboard, LeaderboardEntry, LeagueCohort, LeagueCohortMember, LeagueCohortView, LeagueMembership, LeagueTier, LinkedIdentifier, ListMessagesOptions, LocalNotificationHandle, LocalNotificationTrigger, type LocalNotificationsNamespace, MarkReadResult, MediaAsset, MediaCatalog, MediaCatalogItem, MediaListResponse, Message, type MessageChange, MessageType, NearestOptions, NearestResponse, NotFilter, Offering, OfferingPackage, OfferingProduct, OfferingsResult, OnboardingStatus, OrFilter, OrderBy, type OutboxOp, type OutboxOpKind, PingResult, PresignData, PullChangesCursor, PullChangesResult, PurchaseOption, PurchaseRequest, PurchaseResult, PushChangesResult, PushPlatform, PushToken, type RealtimeChange, ReferralClaim, ReferralCode, Report, ReportRequest, RestoreResult, Review, ReviewCreate, ReviewUpdate, Role, type RowChangeOp, SDK_VERSION, ScheduleLocalNotificationInput, ScheduledLocalNotification, SendMessageRequest, Session, SessionPersistence, SocialProvider, Store, Streak, type SubscribeOptions, SyncChange, SyncConflict, TransactionOp, TransactionOpResult, type Unsubscribe, UpsertOptions, User, UserEntitlement, XpBalance, XpTransaction, getCoreWasm, withOptimistic };
|
|
2517
2560
|
//# sourceMappingURL=index.d.ts.map
|
package/dist-rn/index.js
CHANGED
|
@@ -192,6 +192,20 @@ function toAmbaApiError(err) {
|
|
|
192
192
|
return new AmbaApiError("UNKNOWN_ERROR", String(err));
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
//#endregion
|
|
196
|
+
//#region src/path-segment.ts
|
|
197
|
+
/**
|
|
198
|
+
* Encode one caller-supplied id, key, slug or name as a single URL path
|
|
199
|
+
* segment. `encodeURIComponent` escapes `/`, `?` and `#`, so the value stays
|
|
200
|
+
* inside its segment. `.` and `..` are refused because URL resolution treats
|
|
201
|
+
* them as steps to another route, and the empty string is refused because it
|
|
202
|
+
* collapses onto the parent route.
|
|
203
|
+
*/
|
|
204
|
+
function pathSegment(value) {
|
|
205
|
+
if (value === "" || value === "." || value === "..") throw new AmbaApiError("VALIDATION_ERROR", `path segment must not be ${value === "" ? "empty" : `'${value}'`}`);
|
|
206
|
+
return encodeURIComponent(value);
|
|
207
|
+
}
|
|
208
|
+
|
|
195
209
|
//#endregion
|
|
196
210
|
//#region src/realtime.ts
|
|
197
211
|
const INITIAL_BACKOFF_MS = 500;
|
|
@@ -231,7 +245,7 @@ var CollectionSubscription = class {
|
|
|
231
245
|
if (!this.ctx.authHeaders()) throw new Error("Amba.collection().subscribe() requires an authenticated session — call Amba.auth.signIn*/verify* first.");
|
|
232
246
|
const qs = Object.keys(this.filters).length > 0 ? `?where=${encodeURIComponent(JSON.stringify(this.filters))}` : "";
|
|
233
247
|
return openStream({
|
|
234
|
-
url: `${resolveRealtimeBaseUrl(this.ctx.baseUrl)}/v1/client/realtime/collections/${
|
|
248
|
+
url: `${resolveRealtimeBaseUrl(this.ctx.baseUrl)}/v1/client/realtime/collections/${pathSegment(this.name)}${qs}`,
|
|
235
249
|
authHeaders: this.ctx.authHeaders,
|
|
236
250
|
onChange: cb,
|
|
237
251
|
options
|
|
@@ -352,7 +366,7 @@ var ConversationSubscription = class {
|
|
|
352
366
|
subscribe(cb, options = {}) {
|
|
353
367
|
if (!this.ctx.authHeaders()) throw new Error("Amba.messaging.conversation().subscribe() requires an authenticated session — call Amba.auth.signIn*/verify* first.");
|
|
354
368
|
return openStream({
|
|
355
|
-
url: `${resolveRealtimeBaseUrl(this.ctx.baseUrl)}/v1/client/realtime/conversations/${
|
|
369
|
+
url: `${resolveRealtimeBaseUrl(this.ctx.baseUrl)}/v1/client/realtime/conversations/${pathSegment(this.conversationId)}`,
|
|
356
370
|
authHeaders: this.ctx.authHeaders,
|
|
357
371
|
onChange: cb,
|
|
358
372
|
options
|
|
@@ -675,6 +689,19 @@ async function* invokeAiStream(args) {
|
|
|
675
689
|
//#endregion
|
|
676
690
|
//#region src/resilience.ts
|
|
677
691
|
/**
|
|
692
|
+
* Whether `op` belongs to `current`. An op queued under a user id matches only
|
|
693
|
+
* that user id. An op queued before any session matches the same anonymous id,
|
|
694
|
+
* so a guest's offline write lands once that guest's session starts. An op
|
|
695
|
+
* with no recorded owner (queued by an SDK version before owners existed)
|
|
696
|
+
* matches nobody.
|
|
697
|
+
*/
|
|
698
|
+
function outboxOwnerMatches(op, current) {
|
|
699
|
+
const owner = op.owner;
|
|
700
|
+
if (!owner) return false;
|
|
701
|
+
if (owner.appUserId) return owner.appUserId === current.appUserId;
|
|
702
|
+
return owner.anonymousId !== null && owner.anonymousId === current.anonymousId;
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
678
705
|
* Best-effort online probe. In a browser/RN runtime `navigator.onLine`
|
|
679
706
|
* exists; under Node / SSR it doesn't, so we treat "unknown" as online
|
|
680
707
|
* (the executor's own error handling then decides).
|
|
@@ -758,8 +785,18 @@ var OfflineQueue = class {
|
|
|
758
785
|
* SDK surface the loss (debug log) instead of silently discarding.
|
|
759
786
|
*/
|
|
760
787
|
onDropped;
|
|
788
|
+
/**
|
|
789
|
+
* The identity that is signed in right now. When set, enqueue stamps it on
|
|
790
|
+
* each op and flush/pending only touch ops it owns.
|
|
791
|
+
*/
|
|
792
|
+
currentOwner;
|
|
761
793
|
flushing = null;
|
|
762
794
|
/**
|
|
795
|
+
* Bumped by `purge()`. A replay that started before a purge sees the change
|
|
796
|
+
* and does not re-queue its op.
|
|
797
|
+
*/
|
|
798
|
+
purgeGeneration = 0;
|
|
799
|
+
/**
|
|
763
800
|
* Serializes every read-modify-write of the persisted outbox. Each mutation
|
|
764
801
|
* chains onto the previous one's promise, so concurrent `enqueue`s — and an
|
|
765
802
|
* `enqueue` racing the per-op removal inside `flush` — can never read a stale
|
|
@@ -782,6 +819,16 @@ var OfflineQueue = class {
|
|
|
782
819
|
this.executor = opts.executor;
|
|
783
820
|
this.storageKey = opts.storageKey ?? "offline_outbox";
|
|
784
821
|
this.onDropped = opts.onDropped;
|
|
822
|
+
this.currentOwner = opts.currentOwner;
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* The ops the current owner may see and replay. Without an owner source
|
|
826
|
+
* every op qualifies.
|
|
827
|
+
*/
|
|
828
|
+
visible(ops) {
|
|
829
|
+
if (!this.currentOwner) return ops;
|
|
830
|
+
const current = this.currentOwner();
|
|
831
|
+
return ops.filter((op) => outboxOwnerMatches(op, current));
|
|
785
832
|
}
|
|
786
833
|
/**
|
|
787
834
|
* Run `fn` exclusively against the persisted outbox: it chains after every
|
|
@@ -838,28 +885,55 @@ var OfflineQueue = class {
|
|
|
838
885
|
get isDisposed() {
|
|
839
886
|
return this.disposed;
|
|
840
887
|
}
|
|
841
|
-
/**
|
|
842
|
-
|
|
888
|
+
/**
|
|
889
|
+
* Capture the current owner and purge generation. Take it before a write's
|
|
890
|
+
* live attempt and pass it to {@link enqueue}.
|
|
891
|
+
*/
|
|
892
|
+
ticket() {
|
|
893
|
+
return {
|
|
894
|
+
owner: this.currentOwner?.(),
|
|
895
|
+
generation: this.purgeGeneration
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Append an op to the durable outbox. Returns the persisted op. With a
|
|
900
|
+
* `ticket`, the op belongs to the ticket's owner, and the enqueue rejects
|
|
901
|
+
* (code `OFFLINE_QUEUE_PURGED`) when the queue was purged after the ticket
|
|
902
|
+
* was taken, which is what a sign-out during the live attempt does.
|
|
903
|
+
*/
|
|
904
|
+
async enqueue(op, ticket) {
|
|
843
905
|
if (this.disposed) throw new Error("amba offline queue was disposed (SDK reconfigured)");
|
|
906
|
+
const owner = op.owner ?? (ticket ? ticket.owner : this.currentOwner?.());
|
|
844
907
|
const full = {
|
|
845
908
|
...op,
|
|
909
|
+
...owner ? { owner: { ...owner } } : {},
|
|
846
910
|
idempotencyKey: op.idempotencyKey ?? newIdempotencyKey(),
|
|
847
911
|
enqueuedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
848
912
|
};
|
|
849
|
-
return this.mutate((ops) =>
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
913
|
+
return this.mutate((ops) => {
|
|
914
|
+
if (ticket && ticket.generation !== this.purgeGeneration) {
|
|
915
|
+
const err = /* @__PURE__ */ new Error("amba offline queue was cleared by a sign-out while this write was in flight");
|
|
916
|
+
err.code = "OFFLINE_QUEUE_PURGED";
|
|
917
|
+
throw err;
|
|
918
|
+
}
|
|
919
|
+
return {
|
|
920
|
+
next: [...ops, full],
|
|
921
|
+
result: full
|
|
922
|
+
};
|
|
923
|
+
}, { rejectIfDisposed: true });
|
|
853
924
|
}
|
|
854
|
-
/** Number of ops awaiting replay. */
|
|
925
|
+
/** Number of the current owner's ops awaiting replay. */
|
|
855
926
|
async pendingCount() {
|
|
856
|
-
return (await this.load()).length;
|
|
927
|
+
return this.visible(await this.load()).length;
|
|
857
928
|
}
|
|
858
|
-
/**
|
|
929
|
+
/**
|
|
930
|
+
* Snapshot the current owner's pending ops (a copy — mutating it does not
|
|
931
|
+
* affect the queue). Ops queued by another user stay out of the list.
|
|
932
|
+
*/
|
|
859
933
|
async pending() {
|
|
860
|
-
return [...await this.load()];
|
|
934
|
+
return [...this.visible(await this.load())];
|
|
861
935
|
}
|
|
862
|
-
/** Drop every queued op without replaying. */
|
|
936
|
+
/** Drop every queued op, whoever queued it, without replaying. */
|
|
863
937
|
async clear() {
|
|
864
938
|
await this.mutate(() => ({
|
|
865
939
|
next: [],
|
|
@@ -867,10 +941,23 @@ var OfflineQueue = class {
|
|
|
867
941
|
}));
|
|
868
942
|
}
|
|
869
943
|
/**
|
|
870
|
-
*
|
|
871
|
-
*
|
|
872
|
-
*
|
|
873
|
-
|
|
944
|
+
* Drop every queued op, including one whose replay is in flight right now:
|
|
945
|
+
* that op is not re-queued when its replay fails. The SDKs call this on
|
|
946
|
+
* sign-out so nothing the signed-out user queued survives on the device.
|
|
947
|
+
*/
|
|
948
|
+
async purge() {
|
|
949
|
+
this.purgeGeneration += 1;
|
|
950
|
+
await this.clear();
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Replay the current owner's ops in FIFO order. Each op is removed only
|
|
954
|
+
* after it succeeds; the first failure stops the flush and retains the
|
|
955
|
+
* remainder (so order is preserved and nothing is dropped mid-batch). Ops
|
|
956
|
+
* another user queued stay held for that user. Ops that can never replay
|
|
957
|
+
* are dropped through `onDropped`: one with no recorded owner (code
|
|
958
|
+
* `OFFLINE_OWNER_UNKNOWN`), and one queued before any session under an
|
|
959
|
+
* anonymous id the install no longer has (code `OFFLINE_OWNER_GONE`).
|
|
960
|
+
* Concurrent calls share one in-flight flush.
|
|
874
961
|
*/
|
|
875
962
|
async flush() {
|
|
876
963
|
if (this.disposed) return {
|
|
@@ -889,8 +976,26 @@ var OfflineQueue = class {
|
|
|
889
976
|
let dropped = 0;
|
|
890
977
|
for (;;) {
|
|
891
978
|
if (this.disposed) break;
|
|
892
|
-
const
|
|
979
|
+
const all = await this.load();
|
|
980
|
+
if (this.currentOwner) {
|
|
981
|
+
const current = this.currentOwner();
|
|
982
|
+
const orphan = all.find((o) => !o.owner || o.owner.appUserId === null && current.anonymousId !== null && o.owner.anonymousId !== current.anonymousId);
|
|
983
|
+
if (orphan) {
|
|
984
|
+
if (!await this.removeOp(orphan)) break;
|
|
985
|
+
dropped += 1;
|
|
986
|
+
this.reportDropped(orphan, orphan.owner ? {
|
|
987
|
+
code: "OFFLINE_OWNER_GONE",
|
|
988
|
+
message: "queued before sign-in under an anonymous id this install no longer has; not replayed"
|
|
989
|
+
} : {
|
|
990
|
+
code: "OFFLINE_OWNER_UNKNOWN",
|
|
991
|
+
message: "queued by an earlier SDK version without an owner; not replayed"
|
|
992
|
+
});
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
const head = this.visible(all)[0];
|
|
893
997
|
if (!head) break;
|
|
998
|
+
const generation = this.purgeGeneration;
|
|
894
999
|
let replayError = void 0;
|
|
895
1000
|
let ok = false;
|
|
896
1001
|
try {
|
|
@@ -902,32 +1007,20 @@ var OfflineQueue = class {
|
|
|
902
1007
|
if (!ok && !isDeterministicError(replayError)) {
|
|
903
1008
|
await this.mutate((ops) => {
|
|
904
1009
|
return {
|
|
905
|
-
next: ops.some((o) => o.idempotencyKey === head.idempotencyKey)
|
|
1010
|
+
next: !ops.some((o) => o.idempotencyKey === head.idempotencyKey) && generation === this.purgeGeneration ? [head, ...ops] : ops,
|
|
906
1011
|
result: void 0
|
|
907
1012
|
};
|
|
908
1013
|
});
|
|
909
1014
|
break;
|
|
910
1015
|
}
|
|
911
|
-
if (!await this.
|
|
912
|
-
const idx = ops.findIndex((o) => o.idempotencyKey === head.idempotencyKey);
|
|
913
|
-
if (idx === -1) return {
|
|
914
|
-
next: ops,
|
|
915
|
-
result: false
|
|
916
|
-
};
|
|
917
|
-
return {
|
|
918
|
-
next: ops.slice(0, idx).concat(ops.slice(idx + 1)),
|
|
919
|
-
result: true
|
|
920
|
-
};
|
|
921
|
-
})) break;
|
|
1016
|
+
if (!await this.removeOp(head)) break;
|
|
922
1017
|
if (ok) flushed += 1;
|
|
923
1018
|
else {
|
|
924
1019
|
dropped += 1;
|
|
925
|
-
|
|
926
|
-
this.onDropped?.(head, replayError);
|
|
927
|
-
} catch {}
|
|
1020
|
+
this.reportDropped(head, replayError);
|
|
928
1021
|
}
|
|
929
1022
|
}
|
|
930
|
-
const remaining = (await this.load()).length;
|
|
1023
|
+
const remaining = this.visible(await this.load()).length;
|
|
931
1024
|
return {
|
|
932
1025
|
flushed,
|
|
933
1026
|
dropped,
|
|
@@ -935,6 +1028,29 @@ var OfflineQueue = class {
|
|
|
935
1028
|
};
|
|
936
1029
|
}
|
|
937
1030
|
/**
|
|
1031
|
+
* Remove exactly `op` (by idempotency key) under the lock. Robust against a
|
|
1032
|
+
* concurrent enqueue having shifted the array. Resolves false when the op
|
|
1033
|
+
* is already gone.
|
|
1034
|
+
*/
|
|
1035
|
+
removeOp(op) {
|
|
1036
|
+
return this.mutate((ops) => {
|
|
1037
|
+
const idx = ops.findIndex((o) => o.idempotencyKey === op.idempotencyKey);
|
|
1038
|
+
if (idx === -1) return {
|
|
1039
|
+
next: ops,
|
|
1040
|
+
result: false
|
|
1041
|
+
};
|
|
1042
|
+
return {
|
|
1043
|
+
next: ops.slice(0, idx).concat(ops.slice(idx + 1)),
|
|
1044
|
+
result: true
|
|
1045
|
+
};
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
reportDropped(op, err) {
|
|
1049
|
+
try {
|
|
1050
|
+
this.onDropped?.(op, err);
|
|
1051
|
+
} catch {}
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
938
1054
|
* Install an auto-flush listener on the runtime's `online` event so the
|
|
939
1055
|
* queue drains the moment connectivity returns. Idempotent — a second
|
|
940
1056
|
* call is a no-op. Returns a detach function.
|
|
@@ -1388,6 +1504,13 @@ let _currentSessionExpiresAt = null;
|
|
|
1388
1504
|
let _currentUser = null;
|
|
1389
1505
|
const BROWSER_IDENTITY_VERSION = 1;
|
|
1390
1506
|
const DEFAULT_SESSION_TTL_MS = 3600 * 1e3;
|
|
1507
|
+
/**
|
|
1508
|
+
* `AmbaConfig.sessionPersistence` for the current configuration. In
|
|
1509
|
+
* `"memory"` mode no identity snapshot (tokens, user id, anonymous id) is read
|
|
1510
|
+
* from or written to `localStorage`.
|
|
1511
|
+
*/
|
|
1512
|
+
let _sessionPersistence = "local";
|
|
1513
|
+
const SESSION_PERSISTENCE_MODES = ["local", "memory"];
|
|
1391
1514
|
function identityStorageKey(projectId) {
|
|
1392
1515
|
return `identity:v${BROWSER_IDENTITY_VERSION}:project:${encodeURIComponent(projectId)}`;
|
|
1393
1516
|
}
|
|
@@ -1427,8 +1550,26 @@ function readPersistedIdentity(persistence, projectId) {
|
|
|
1427
1550
|
return null;
|
|
1428
1551
|
}
|
|
1429
1552
|
}
|
|
1553
|
+
/**
|
|
1554
|
+
* The identity the current core holds, for a `"memory"` re-configure of the
|
|
1555
|
+
* same project: the new core starts from it, so a second `configure()` in the
|
|
1556
|
+
* same page keeps the session.
|
|
1557
|
+
*/
|
|
1558
|
+
function liveIdentity(projectId) {
|
|
1559
|
+
if (!coreInstance || _configuredProjectId !== projectId) return null;
|
|
1560
|
+
return {
|
|
1561
|
+
version: BROWSER_IDENTITY_VERSION,
|
|
1562
|
+
project_id: projectId,
|
|
1563
|
+
anonymous_id: coreInstance.anonymousId(),
|
|
1564
|
+
app_user_id: coreInstance.appUserId() ?? null,
|
|
1565
|
+
session_token: _currentSessionToken,
|
|
1566
|
+
refresh_token: _currentRefreshToken,
|
|
1567
|
+
expires_at: _currentSessionExpiresAt,
|
|
1568
|
+
user: _currentUser
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1430
1571
|
function writeIdentitySnapshot() {
|
|
1431
|
-
if (!_persistence?.available || !_configuredProjectId || !coreInstance) return;
|
|
1572
|
+
if (_sessionPersistence === "memory" || !_persistence?.available || !_configuredProjectId || !coreInstance) return;
|
|
1432
1573
|
const snapshot = {
|
|
1433
1574
|
version: BROWSER_IDENTITY_VERSION,
|
|
1434
1575
|
project_id: _configuredProjectId,
|
|
@@ -1582,12 +1723,29 @@ function getOfflineQueue() {
|
|
|
1582
1723
|
store: offlineStore(),
|
|
1583
1724
|
storageKey: `offline_outbox:${_configuredProjectId ?? "default"}`,
|
|
1584
1725
|
executor: replayOutboxOp,
|
|
1726
|
+
currentOwner: () => ({
|
|
1727
|
+
appUserId: _currentSessionToken || _currentRefreshToken ? coreInstance?.appUserId() ?? null : null,
|
|
1728
|
+
anonymousId: coreInstance?.anonymousId() ?? null
|
|
1729
|
+
}),
|
|
1585
1730
|
onDropped: (op, err) => {
|
|
1586
1731
|
if (typeof console !== "undefined") console.warn(`[amba] dropped un-replayable offline ${op.kind} (${op.collection ?? op.event ?? "?"}):`, err);
|
|
1587
1732
|
}
|
|
1588
1733
|
});
|
|
1589
1734
|
return _offlineQueue;
|
|
1590
1735
|
}
|
|
1736
|
+
/**
|
|
1737
|
+
* Empty the offline outbox on sign-out, including writes queued on earlier
|
|
1738
|
+
* page loads, so nothing the signed-out user queued replays under the next
|
|
1739
|
+
* session.
|
|
1740
|
+
*/
|
|
1741
|
+
async function purgeOfflineOutbox() {
|
|
1742
|
+
if (!_persistence) return;
|
|
1743
|
+
try {
|
|
1744
|
+
await getOfflineQueue().purge();
|
|
1745
|
+
} catch (err) {
|
|
1746
|
+
if (typeof console !== "undefined") console.warn("[amba] signOut could not clear the offline outbox", err);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1591
1749
|
async function insertDirect(name, row) {
|
|
1592
1750
|
return await coreCall((c) => c.collectionsInsert(name, encodeJson(row)));
|
|
1593
1751
|
}
|
|
@@ -1711,8 +1869,8 @@ function buildAiStreamRequest(path, request) {
|
|
|
1711
1869
|
* resolved default-host id is cached for subsequent invokes.
|
|
1712
1870
|
*/
|
|
1713
1871
|
async function resolveFunctionUrl(name) {
|
|
1714
|
-
if (_configuredFunctionsBaseUrl) return `${_configuredFunctionsBaseUrl.replace(/\/$/, "")}/${
|
|
1715
|
-
return `https://${await resolveFunctionsProjectId()}.${DEFAULT_FUNCTIONS_HOST}/${
|
|
1872
|
+
if (_configuredFunctionsBaseUrl) return `${_configuredFunctionsBaseUrl.replace(/\/$/, "")}/${pathSegment(name)}`;
|
|
1873
|
+
return `https://${await resolveFunctionsProjectId()}.${DEFAULT_FUNCTIONS_HOST}/${pathSegment(name)}`;
|
|
1716
1874
|
}
|
|
1717
1875
|
async function resolveFunctionsProjectId() {
|
|
1718
1876
|
if (_resolvedFunctionsProjectId) return _resolvedFunctionsProjectId;
|
|
@@ -1871,8 +2029,10 @@ const Amba = {
|
|
|
1871
2029
|
if (explicitWasmInput !== void 0) await initCore({ module_or_path: explicitWasmInput });
|
|
1872
2030
|
else await initCore(); // Hermes-stripped Node-init block (DX-15) — see scripts/build-rn.mjs
|
|
1873
2031
|
}
|
|
2032
|
+
const sessionPersistence = config.sessionPersistence ?? "local";
|
|
2033
|
+
if (!SESSION_PERSISTENCE_MODES.includes(sessionPersistence)) throw new AmbaApiError("INVALID_CONFIG", "Amba.configure: sessionPersistence must be \"local\" or \"memory\"");
|
|
1874
2034
|
const persistence = new LocalStoragePersistence(config.storagePrefix ?? "amba");
|
|
1875
|
-
const restoredIdentity = readPersistedIdentity(persistence, config.projectId);
|
|
2035
|
+
const restoredIdentity = sessionPersistence === "memory" ? liveIdentity(config.projectId) : readPersistedIdentity(persistence, config.projectId);
|
|
1876
2036
|
let nextCore;
|
|
1877
2037
|
try {
|
|
1878
2038
|
const { wasmClass } = await getCoreRuntime();
|
|
@@ -1897,6 +2057,8 @@ const Amba = {
|
|
|
1897
2057
|
}
|
|
1898
2058
|
await teardownOffline();
|
|
1899
2059
|
_persistence = persistence;
|
|
2060
|
+
_sessionPersistence = sessionPersistence;
|
|
2061
|
+
if (sessionPersistence === "memory") persistence.delete(identityStorageKey(config.projectId));
|
|
1900
2062
|
coreInstance = nextCore;
|
|
1901
2063
|
_configuredProjectId = config.projectId;
|
|
1902
2064
|
_configuredApiUrl = config.apiUrl ?? DEFAULT_API_URL;
|
|
@@ -1929,6 +2091,7 @@ const Amba = {
|
|
|
1929
2091
|
},
|
|
1930
2092
|
events: {
|
|
1931
2093
|
track: async (event, properties, options) => {
|
|
2094
|
+
const outboxTicket = _offlineEnabled ? getOfflineQueue().ticket() : void 0;
|
|
1932
2095
|
try {
|
|
1933
2096
|
await coreCall((c) => c.track(event, properties ? encodeJson(properties) : void 0, options?.telemetry));
|
|
1934
2097
|
} catch (err) {
|
|
@@ -1939,7 +2102,7 @@ const Amba = {
|
|
|
1939
2102
|
properties,
|
|
1940
2103
|
telemetry: options?.telemetry,
|
|
1941
2104
|
occurredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1942
|
-
});
|
|
2105
|
+
}, outboxTicket);
|
|
1943
2106
|
return;
|
|
1944
2107
|
}
|
|
1945
2108
|
throw err;
|
|
@@ -2066,6 +2229,7 @@ const Amba = {
|
|
|
2066
2229
|
_currentSessionExpiresAt = null;
|
|
2067
2230
|
_currentUser = null;
|
|
2068
2231
|
writeIdentitySnapshot();
|
|
2232
|
+
await purgeOfflineOutbox();
|
|
2069
2233
|
notifyAuthSubscribers(null);
|
|
2070
2234
|
},
|
|
2071
2235
|
refresh: async () => {
|
|
@@ -2141,6 +2305,7 @@ const Amba = {
|
|
|
2141
2305
|
...row,
|
|
2142
2306
|
idempotency_key: idempotencyKey
|
|
2143
2307
|
};
|
|
2308
|
+
const outboxTicket = _offlineEnabled ? getOfflineQueue().ticket() : void 0;
|
|
2144
2309
|
try {
|
|
2145
2310
|
return await coreCall((c) => c.collectionsInsert(name, encodeJson(wireRow)));
|
|
2146
2311
|
} catch (err) {
|
|
@@ -2150,7 +2315,7 @@ const Amba = {
|
|
|
2150
2315
|
collection: name,
|
|
2151
2316
|
payload: row,
|
|
2152
2317
|
idempotencyKey
|
|
2153
|
-
});
|
|
2318
|
+
}, outboxTicket);
|
|
2154
2319
|
return {
|
|
2155
2320
|
...row,
|
|
2156
2321
|
_amba_pending: true
|
|
@@ -2162,39 +2327,45 @@ const Amba = {
|
|
|
2162
2327
|
return await coreCall((c) => c.collectionsInsert(name, encodeJson(row)));
|
|
2163
2328
|
},
|
|
2164
2329
|
update: async (name, id, set) => {
|
|
2165
|
-
if (_offlineEnabled)
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2330
|
+
if (_offlineEnabled) {
|
|
2331
|
+
const outboxTicket = _offlineEnabled ? getOfflineQueue().ticket() : void 0;
|
|
2332
|
+
try {
|
|
2333
|
+
return await coreCall((c) => c.collectionsUpdate(name, id, encodeJson(set)));
|
|
2334
|
+
} catch (err) {
|
|
2335
|
+
if (isOfflineError(err)) {
|
|
2336
|
+
await getOfflineQueue().enqueue({
|
|
2337
|
+
kind: "collection.update",
|
|
2338
|
+
collection: name,
|
|
2339
|
+
id,
|
|
2340
|
+
payload: set
|
|
2341
|
+
}, outboxTicket);
|
|
2342
|
+
return {
|
|
2343
|
+
id,
|
|
2344
|
+
...set,
|
|
2345
|
+
_amba_pending: true
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
2348
|
+
throw err;
|
|
2180
2349
|
}
|
|
2181
|
-
throw err;
|
|
2182
2350
|
}
|
|
2183
2351
|
return await coreCall((c) => c.collectionsUpdate(name, id, encodeJson(set)));
|
|
2184
2352
|
},
|
|
2185
2353
|
delete: async (name, id) => {
|
|
2186
|
-
if (_offlineEnabled)
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2354
|
+
if (_offlineEnabled) {
|
|
2355
|
+
const outboxTicket = _offlineEnabled ? getOfflineQueue().ticket() : void 0;
|
|
2356
|
+
try {
|
|
2357
|
+
return await coreCall((c) => c.collectionsDelete(name, id));
|
|
2358
|
+
} catch (err) {
|
|
2359
|
+
if (isOfflineError(err)) {
|
|
2360
|
+
await getOfflineQueue().enqueue({
|
|
2361
|
+
kind: "collection.delete",
|
|
2362
|
+
collection: name,
|
|
2363
|
+
id
|
|
2364
|
+
}, outboxTicket);
|
|
2365
|
+
return { data: { deleted: true } };
|
|
2366
|
+
}
|
|
2367
|
+
throw err;
|
|
2196
2368
|
}
|
|
2197
|
-
throw err;
|
|
2198
2369
|
}
|
|
2199
2370
|
return await coreCall((c) => c.collectionsDelete(name, id));
|
|
2200
2371
|
},
|
|
@@ -2337,7 +2508,7 @@ const Amba = {
|
|
|
2337
2508
|
media: { catalog: async (slug) => {
|
|
2338
2509
|
if (!_configuredClientKey) throw new AmbaApiError("NOT_INITIALIZED", "amba SDK not configured. Call Amba.configure({ projectId, clientKey }) before reading a media catalog.");
|
|
2339
2510
|
if (!slug || slug.trim() === "") throw new AmbaApiError("INVALID_ARGUMENT", "Amba.media.catalog: a catalog slug is required.");
|
|
2340
|
-
const url = `${_configuredApiUrl.replace(/\/$/, "")}/v1/client/media/catalogs/${
|
|
2511
|
+
const url = `${_configuredApiUrl.replace(/\/$/, "")}/v1/client/media/catalogs/${pathSegment(slug)}`;
|
|
2341
2512
|
let resp;
|
|
2342
2513
|
try {
|
|
2343
2514
|
resp = await fetch(url, {
|
|
@@ -2672,7 +2843,7 @@ function getCoreWasm() {
|
|
|
2672
2843
|
* `verify-sdk-version-coherence` GH action gates every publish on the
|
|
2673
2844
|
* pair agreeing (and matching the `web-v<X.Y.Z>` tag when tag-triggered).
|
|
2674
2845
|
*/
|
|
2675
|
-
const SDK_VERSION = "4.0.
|
|
2846
|
+
const SDK_VERSION = "4.0.11";
|
|
2676
2847
|
|
|
2677
2848
|
//#endregion
|
|
2678
2849
|
export { Amba, AmbaApiError, AmbaGroupScopedClient, CollectionSubscription, ConversationSubscription, GamificationSubscription, SDK_VERSION, getCoreWasm, withOptimistic };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@layers/amba-web",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.11",
|
|
4
4
|
"description": "amba SDK for browser apps — full SDK 4.0 surface (34 modules: auth + magic link, users, sessions, sync, collections + vector search, storage + list/delete/download, push, entitlements, flags, config, deep links, onboarding, content, messaging, friends, groups, feeds, reviews, moderation, roles, referrals, achievements, xp, streaks, challenges, leaderboards, leagues, currencies, inventory, stores, catalog, ai, diagnostics).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"sideEffects": false,
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@layers/amba-core": "^4.0.
|
|
35
|
+
"@layers/amba-core": "^4.0.11"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "^22.13.14",
|