@layers/amba-web 4.0.6 → 4.0.8
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.browser.d.ts +2437 -0
- package/dist/index.browser.d.ts.map +1 -0
- package/dist/index.browser.js +2451 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.d.ts +31 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +41 -16
- package/dist/index.js.map +1 -1
- package/dist-rn/index.d.ts +31 -2
- package/dist-rn/index.js +30 -5
- package/package.json +3 -3
|
@@ -0,0 +1,2451 @@
|
|
|
1
|
+
import init, { AmbaCoreWasm } from "@layers/amba-core/inline";
|
|
2
|
+
|
|
3
|
+
//#region src/wasm-loader.ts
|
|
4
|
+
let coreInit;
|
|
5
|
+
let coreWasmClass;
|
|
6
|
+
function setCoreRuntime(init$1, wasmClass) {
|
|
7
|
+
coreInit = init$1;
|
|
8
|
+
coreWasmClass = wasmClass;
|
|
9
|
+
}
|
|
10
|
+
async function getCoreRuntime() {
|
|
11
|
+
if (coreInit === void 0 || coreWasmClass === void 0) {
|
|
12
|
+
const core = await import("@layers/amba-core");
|
|
13
|
+
setCoreRuntime(core.default, core.AmbaCoreWasm);
|
|
14
|
+
}
|
|
15
|
+
if (coreInit === void 0 || coreWasmClass === void 0) throw new Error("Amba.configure: core WASM runtime failed to load");
|
|
16
|
+
return {
|
|
17
|
+
initCore: coreInit,
|
|
18
|
+
wasmClass: coreWasmClass
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/storage-adapter.ts
|
|
24
|
+
/**
|
|
25
|
+
* Browser localStorage adapter for SDK identity persistence.
|
|
26
|
+
*
|
|
27
|
+
* The SDK runtime currently re-seeds an `anonymous_id` on every page load.
|
|
28
|
+
* This adapter is the documented home for storage-prefix handling so
|
|
29
|
+
* customers can override the localStorage key prefix used by the
|
|
30
|
+
* (eventual) identity cache.
|
|
31
|
+
*/
|
|
32
|
+
var LocalStoragePersistence = class {
|
|
33
|
+
constructor(prefix) {
|
|
34
|
+
this.prefix = prefix;
|
|
35
|
+
}
|
|
36
|
+
/** Read everything we'd want to hydrate at init. No-op until wired through. */
|
|
37
|
+
hydrate() {}
|
|
38
|
+
read(key) {
|
|
39
|
+
if (typeof localStorage === "undefined") return null;
|
|
40
|
+
return localStorage.getItem(`${this.prefix}:${key}`);
|
|
41
|
+
}
|
|
42
|
+
write(key, value) {
|
|
43
|
+
if (typeof localStorage === "undefined") return;
|
|
44
|
+
localStorage.setItem(`${this.prefix}:${key}`, value);
|
|
45
|
+
}
|
|
46
|
+
delete(key) {
|
|
47
|
+
if (typeof localStorage === "undefined") return;
|
|
48
|
+
localStorage.removeItem(`${this.prefix}:${key}`);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region src/json.ts
|
|
54
|
+
/**
|
|
55
|
+
* JSON encode helper — guarantees stable serialization across browser
|
|
56
|
+
* runtimes and provides a single hook for future schema validation.
|
|
57
|
+
*/
|
|
58
|
+
function encodeJson(value) {
|
|
59
|
+
return JSON.stringify(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/ai-images.ts
|
|
64
|
+
/**
|
|
65
|
+
* Normalize an image input into a content block, applying the SAME validity
|
|
66
|
+
* rule the backend's gateway + the MCP tool use: trim `url`/`data`, treat a
|
|
67
|
+
* blank/whitespace-only value as absent, and return `null` when NEITHER a real
|
|
68
|
+
* url nor real data survives. A `null` block is dropped by the caller so we
|
|
69
|
+
* never emit an empty image — keeping the SDK, MCP, and gateway in agreement.
|
|
70
|
+
*/
|
|
71
|
+
function normalizeImageBlock(img) {
|
|
72
|
+
const url = typeof img.url === "string" && img.url.trim().length > 0 ? img.url.trim() : void 0;
|
|
73
|
+
const data = typeof img.data === "string" && img.data.trim().length > 0 ? img.data.trim() : void 0;
|
|
74
|
+
if (!url && !data) return null;
|
|
75
|
+
const block = { type: "image" };
|
|
76
|
+
if (url !== void 0) block.url = url;
|
|
77
|
+
if (data !== void 0) block.data = data;
|
|
78
|
+
if (img.mime !== void 0) block.mime = img.mime;
|
|
79
|
+
return block;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Expand a request's convenience `images` into content blocks on `messages`.
|
|
83
|
+
* Returns the SAME object reference when there's nothing to expand, or a new
|
|
84
|
+
* request (with `images` removed) when images were attached.
|
|
85
|
+
*/
|
|
86
|
+
function expandAiImages(request) {
|
|
87
|
+
const images = request.images;
|
|
88
|
+
if (!images || images.length === 0) return request;
|
|
89
|
+
const imageBlocks = images.map(normalizeImageBlock).filter((b) => b !== null);
|
|
90
|
+
const { images: _images,...rest } = request;
|
|
91
|
+
if (imageBlocks.length === 0) return rest;
|
|
92
|
+
const messages = (rest.messages ?? []).map((m) => ({ ...m }));
|
|
93
|
+
let lastUserIdx = -1;
|
|
94
|
+
for (let i = messages.length - 1; i >= 0; i--) if (messages[i].role === "user") {
|
|
95
|
+
lastUserIdx = i;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
if (lastUserIdx === -1) {
|
|
99
|
+
const variableBlocks = rest.variables && Object.keys(rest.variables).length > 0 ? [{
|
|
100
|
+
type: "text",
|
|
101
|
+
text: JSON.stringify(rest.variables)
|
|
102
|
+
}] : [];
|
|
103
|
+
messages.push({
|
|
104
|
+
role: "user",
|
|
105
|
+
content: [...variableBlocks, ...imageBlocks]
|
|
106
|
+
});
|
|
107
|
+
} else {
|
|
108
|
+
const existing = messages[lastUserIdx].content;
|
|
109
|
+
let leadingBlocks;
|
|
110
|
+
if (typeof existing === "string") leadingBlocks = [{
|
|
111
|
+
type: "text",
|
|
112
|
+
text: existing
|
|
113
|
+
}];
|
|
114
|
+
else if (Array.isArray(existing)) leadingBlocks = existing;
|
|
115
|
+
else if (existing === null || existing === void 0) leadingBlocks = [];
|
|
116
|
+
else leadingBlocks = [{
|
|
117
|
+
type: "text",
|
|
118
|
+
text: JSON.stringify(existing)
|
|
119
|
+
}];
|
|
120
|
+
messages[lastUserIdx] = {
|
|
121
|
+
...messages[lastUserIdx],
|
|
122
|
+
content: [...leadingBlocks, ...imageBlocks]
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
...rest,
|
|
127
|
+
messages
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/error.ts
|
|
133
|
+
var AmbaApiError = class extends Error {
|
|
134
|
+
code;
|
|
135
|
+
details;
|
|
136
|
+
/**
|
|
137
|
+
* Server-side request / trace id (the `X-Amba-Request-Id` response
|
|
138
|
+
* header, falling back to the `request_id` field in the standard error
|
|
139
|
+
* envelope). Present on errors thrown by `Amba.functions.invoke()` so a
|
|
140
|
+
* failed run can be correlated with server logs
|
|
141
|
+
* (`functions logs --request-id <traceId>`). `undefined` when the failure
|
|
142
|
+
* happened before a response was received (e.g. a transport error).
|
|
143
|
+
*/
|
|
144
|
+
traceId;
|
|
145
|
+
constructor(code, message, details) {
|
|
146
|
+
super(message);
|
|
147
|
+
this.name = "AmbaApiError";
|
|
148
|
+
this.code = code;
|
|
149
|
+
this.details = details;
|
|
150
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Best-effort: pull a stable error code out of an `AmbaError` display
|
|
155
|
+
* string. Used by `toAmbaApiError` when the underlying thrown value is a
|
|
156
|
+
* generic `Error` produced by the SDK core.
|
|
157
|
+
*
|
|
158
|
+
* Keep in sync with the SDK core's `AmbaError` format strings — but the
|
|
159
|
+
* parser falls back to UNKNOWN_ERROR for any pattern it doesn't recognize,
|
|
160
|
+
* so a stale match list isn't a footgun, just a quality-of-life regression.
|
|
161
|
+
*/
|
|
162
|
+
function codeFromMessage(message) {
|
|
163
|
+
if (message.startsWith("Unauthorized")) return "UNAUTHORIZED";
|
|
164
|
+
if (message.startsWith("Forbidden")) return "FORBIDDEN";
|
|
165
|
+
if (message.startsWith("Not found")) return "NOT_FOUND";
|
|
166
|
+
if (message.startsWith("Conflict")) return "CONFLICT";
|
|
167
|
+
if (message.startsWith("Rate limited")) return "RATE_LIMITED";
|
|
168
|
+
if (message.startsWith("Validation error")) return "VALIDATION_ERROR";
|
|
169
|
+
if (message.startsWith("Network error")) return "NETWORK_ERROR";
|
|
170
|
+
if (message.startsWith("HTTP error")) return "HTTP_ERROR";
|
|
171
|
+
if (message.startsWith("Circuit breaker")) return "CIRCUIT_OPEN";
|
|
172
|
+
if (message.startsWith("Consent not granted")) return "CONSENT_NOT_GRANTED";
|
|
173
|
+
if (message.includes("not initialized")) return "NOT_INITIALIZED";
|
|
174
|
+
if (message.startsWith("Invalid configuration")) return "INVALID_CONFIG";
|
|
175
|
+
if (message.startsWith("Invalid argument")) return "INVALID_ARGUMENT";
|
|
176
|
+
return "UNKNOWN_ERROR";
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Coerce any thrown value into an `AmbaApiError`. Idempotent — passing
|
|
180
|
+
* an existing `AmbaApiError` returns it unchanged so wrappers can stack
|
|
181
|
+
* without re-wrapping.
|
|
182
|
+
*
|
|
183
|
+
* Heuristics:
|
|
184
|
+
* 1. Existing `AmbaApiError` → returned as-is.
|
|
185
|
+
* 2. Object with `.code` → use that code + `.message` + `.details`.
|
|
186
|
+
* 3. `Error` (e.g. runtime-emitted error) → infer code from message prefix.
|
|
187
|
+
* 4. Anything else → stringify and tag UNKNOWN_ERROR.
|
|
188
|
+
*/
|
|
189
|
+
function toAmbaApiError(err) {
|
|
190
|
+
if (err instanceof AmbaApiError) return err;
|
|
191
|
+
if (err && typeof err === "object") {
|
|
192
|
+
const obj = err;
|
|
193
|
+
if (typeof obj.code === "string") return new AmbaApiError(obj.code, typeof obj.message === "string" ? obj.message : "unknown", obj.details);
|
|
194
|
+
}
|
|
195
|
+
if (err instanceof Error) return new AmbaApiError(codeFromMessage(err.message), err.message);
|
|
196
|
+
return new AmbaApiError("UNKNOWN_ERROR", String(err));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region src/realtime.ts
|
|
201
|
+
const INITIAL_BACKOFF_MS = 500;
|
|
202
|
+
const MAX_BACKOFF_MS = 15e3;
|
|
203
|
+
const DEFAULT_API_URL$1 = "https://api.amba.dev";
|
|
204
|
+
const DEFAULT_REALTIME_URL = "https://realtime.amba.host";
|
|
205
|
+
/**
|
|
206
|
+
* Resolve the base URL for live-update connections. Long-lived streams are
|
|
207
|
+
* served from a dedicated realtime host (no request-duration ceiling), so
|
|
208
|
+
* when the SDK points at the default production API the stream connects
|
|
209
|
+
* there instead. Any custom `apiUrl` (dev, staging, self-hosted, proxy)
|
|
210
|
+
* keeps live updates on that same origin — override semantics unchanged.
|
|
211
|
+
*/
|
|
212
|
+
function resolveRealtimeBaseUrl(baseUrl) {
|
|
213
|
+
return baseUrl.replace(/\/+$/, "") === DEFAULT_API_URL$1 ? DEFAULT_REALTIME_URL : baseUrl;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Builder returned by `Amba.collection(name)`. Chain `.where(...)` to
|
|
217
|
+
* narrow, then `.subscribe(cb)` to start streaming.
|
|
218
|
+
*/
|
|
219
|
+
var CollectionSubscription = class {
|
|
220
|
+
filters = {};
|
|
221
|
+
constructor(ctx, name) {
|
|
222
|
+
this.ctx = ctx;
|
|
223
|
+
this.name = name;
|
|
224
|
+
}
|
|
225
|
+
/** Add an equality filter. Chainable. All filters must match (AND). */
|
|
226
|
+
where(column, value) {
|
|
227
|
+
this.filters[column] = value;
|
|
228
|
+
return this;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Start listening. `cb` fires once per matching row change. Returns an
|
|
232
|
+
* unsubscribe handle — call it to close the connection.
|
|
233
|
+
*/
|
|
234
|
+
subscribe(cb, options = {}) {
|
|
235
|
+
if (!this.ctx.authHeaders()) throw new Error("Amba.collection().subscribe() requires an authenticated session — call Amba.auth.signIn*/verify* first.");
|
|
236
|
+
const qs = Object.keys(this.filters).length > 0 ? `?where=${encodeURIComponent(JSON.stringify(this.filters))}` : "";
|
|
237
|
+
return openStream({
|
|
238
|
+
url: `${resolveRealtimeBaseUrl(this.ctx.baseUrl)}/v1/client/realtime/collections/${encodeURIComponent(this.name)}${qs}`,
|
|
239
|
+
authHeaders: this.ctx.authHeaders,
|
|
240
|
+
onChange: cb,
|
|
241
|
+
options
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
/**
|
|
246
|
+
* Open a streamed GET to a realtime endpoint and dispatch row-change events
|
|
247
|
+
* to `onChange`. Reconnects with exponential backoff until the returned
|
|
248
|
+
* unsubscribe handle is invoked. Shared by both collection and conversation
|
|
249
|
+
* subscriptions — the only thing that differs between them is the URL.
|
|
250
|
+
*
|
|
251
|
+
* Uses `fetch` + a streamed response body (rather than the browser's
|
|
252
|
+
* native `EventSource`) because `EventSource` cannot set request headers —
|
|
253
|
+
* which would force the session token into the URL query string, where it
|
|
254
|
+
* would leak into access logs. Streaming `fetch` keeps auth in headers.
|
|
255
|
+
*/
|
|
256
|
+
function openStream(args) {
|
|
257
|
+
let stopped = false;
|
|
258
|
+
let controller = null;
|
|
259
|
+
let backoff = INITIAL_BACKOFF_MS;
|
|
260
|
+
const run = async () => {
|
|
261
|
+
while (!stopped) {
|
|
262
|
+
controller = new AbortController();
|
|
263
|
+
try {
|
|
264
|
+
const headers = args.authHeaders();
|
|
265
|
+
if (!headers) throw new Error("no active session");
|
|
266
|
+
const resp = await fetch(args.url, {
|
|
267
|
+
method: "GET",
|
|
268
|
+
headers: {
|
|
269
|
+
...headers,
|
|
270
|
+
Accept: "text/event-stream"
|
|
271
|
+
},
|
|
272
|
+
signal: controller.signal
|
|
273
|
+
});
|
|
274
|
+
if (!resp.ok || !resp.body) throw new Error(`realtime connect failed: HTTP ${resp.status}`);
|
|
275
|
+
await consume(resp.body, args, () => {
|
|
276
|
+
backoff = INITIAL_BACKOFF_MS;
|
|
277
|
+
args.options.onOpen?.();
|
|
278
|
+
});
|
|
279
|
+
} catch (err) {
|
|
280
|
+
if (stopped) break;
|
|
281
|
+
args.options.onError?.(err);
|
|
282
|
+
}
|
|
283
|
+
if (stopped) break;
|
|
284
|
+
await delay(backoff);
|
|
285
|
+
backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
run();
|
|
289
|
+
return () => {
|
|
290
|
+
stopped = true;
|
|
291
|
+
controller?.abort();
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
async function consume(body, args, onFirstFrame) {
|
|
295
|
+
const reader = body.getReader();
|
|
296
|
+
const decoder = new TextDecoder();
|
|
297
|
+
let buffer = "";
|
|
298
|
+
let sawFrame = false;
|
|
299
|
+
for (;;) {
|
|
300
|
+
const { done, value } = await reader.read();
|
|
301
|
+
if (done) return;
|
|
302
|
+
buffer += decoder.decode(value, { stream: true });
|
|
303
|
+
let idx;
|
|
304
|
+
while ((idx = buffer.indexOf("\n\n")) !== -1) {
|
|
305
|
+
const frame = buffer.slice(0, idx);
|
|
306
|
+
buffer = buffer.slice(idx + 2);
|
|
307
|
+
if (!sawFrame) {
|
|
308
|
+
sawFrame = true;
|
|
309
|
+
onFirstFrame();
|
|
310
|
+
}
|
|
311
|
+
dispatchFrame(frame, args.onChange);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function dispatchFrame(frame, onChange) {
|
|
316
|
+
let event = "";
|
|
317
|
+
const dataLines = [];
|
|
318
|
+
for (const line of frame.split("\n")) {
|
|
319
|
+
if (line.startsWith(":")) continue;
|
|
320
|
+
if (line.startsWith("event:")) event = line.slice(6).trim();
|
|
321
|
+
else if (line.startsWith("data:")) dataLines.push(line.slice(5).trim());
|
|
322
|
+
}
|
|
323
|
+
if (event !== "change") return;
|
|
324
|
+
try {
|
|
325
|
+
const payload = JSON.parse(dataLines.join("\n"));
|
|
326
|
+
onChange({
|
|
327
|
+
op: payload.op,
|
|
328
|
+
row: payload.row,
|
|
329
|
+
truncated: payload.truncated
|
|
330
|
+
});
|
|
331
|
+
} catch {}
|
|
332
|
+
}
|
|
333
|
+
function delay(ms) {
|
|
334
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Builder returned by `Amba.messaging.conversation(id)`. Call `.subscribe(cb)`
|
|
338
|
+
* to receive new messages live instead of polling.
|
|
339
|
+
*
|
|
340
|
+
* const unsub = Amba.messaging
|
|
341
|
+
* .conversation(conversationId)
|
|
342
|
+
* .subscribe((change) => {
|
|
343
|
+
* // change.op === "INSERT"; change.row is the new message
|
|
344
|
+
* });
|
|
345
|
+
* unsub(); // stop listening
|
|
346
|
+
*
|
|
347
|
+
* Delivery is scoped to conversation participants — you receive messages for
|
|
348
|
+
* a conversation only if your session is a member of it. Reconnects with the
|
|
349
|
+
* same exponential backoff as collection subscriptions.
|
|
350
|
+
*/
|
|
351
|
+
var ConversationSubscription = class {
|
|
352
|
+
constructor(ctx, conversationId) {
|
|
353
|
+
this.ctx = ctx;
|
|
354
|
+
this.conversationId = conversationId;
|
|
355
|
+
}
|
|
356
|
+
subscribe(cb, options = {}) {
|
|
357
|
+
if (!this.ctx.authHeaders()) throw new Error("Amba.messaging.conversation().subscribe() requires an authenticated session — call Amba.auth.signIn*/verify* first.");
|
|
358
|
+
return openStream({
|
|
359
|
+
url: `${resolveRealtimeBaseUrl(this.ctx.baseUrl)}/v1/client/realtime/conversations/${encodeURIComponent(this.conversationId)}`,
|
|
360
|
+
authHeaders: this.ctx.authHeaders,
|
|
361
|
+
onChange: cb,
|
|
362
|
+
options
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
/**
|
|
367
|
+
* Builder returned by `Amba.gamification`. Call `.subscribe(cb)` to receive the
|
|
368
|
+
* signed-in user's xp / level / achievement / streak changes live instead of
|
|
369
|
+
* polling.
|
|
370
|
+
*
|
|
371
|
+
* const unsub = Amba.gamification.subscribe((change) => {
|
|
372
|
+
* // change.kind: "xp" | "level" | "achievement" | "streak"
|
|
373
|
+
* // change.data: the new state
|
|
374
|
+
* });
|
|
375
|
+
* unsub(); // stop listening
|
|
376
|
+
*
|
|
377
|
+
* Delivery is scoped to the signed-in user — you only ever receive your own
|
|
378
|
+
* gamification changes, never another user's. Reconnects with the same
|
|
379
|
+
* exponential backoff as collection subscriptions.
|
|
380
|
+
*/
|
|
381
|
+
var GamificationSubscription = class {
|
|
382
|
+
constructor(ctx) {
|
|
383
|
+
this.ctx = ctx;
|
|
384
|
+
}
|
|
385
|
+
subscribe(cb, options = {}) {
|
|
386
|
+
if (!this.ctx.authHeaders()) throw new Error("Amba.gamification.subscribe() requires an authenticated session — call Amba.auth.signIn*/verify* first.");
|
|
387
|
+
return openGamificationStream({
|
|
388
|
+
url: `${resolveRealtimeBaseUrl(this.ctx.baseUrl)}/v1/client/realtime/gamification`,
|
|
389
|
+
authHeaders: this.ctx.authHeaders,
|
|
390
|
+
onChange: cb,
|
|
391
|
+
options
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
/**
|
|
396
|
+
* Open a streamed GET to the gamification realtime endpoint. Identical
|
|
397
|
+
* transport + backoff/reconnect to {@link openStream}; only the frame payload
|
|
398
|
+
* shape differs (gamification carries `kind`, not `op`/`row`), so it gets its
|
|
399
|
+
* own thin consumer rather than overloading the row-change parser.
|
|
400
|
+
*/
|
|
401
|
+
function openGamificationStream(args) {
|
|
402
|
+
let stopped = false;
|
|
403
|
+
let controller = null;
|
|
404
|
+
let backoff = INITIAL_BACKOFF_MS;
|
|
405
|
+
const run = async () => {
|
|
406
|
+
while (!stopped) {
|
|
407
|
+
controller = new AbortController();
|
|
408
|
+
try {
|
|
409
|
+
const headers = args.authHeaders();
|
|
410
|
+
if (!headers) throw new Error("no active session");
|
|
411
|
+
const resp = await fetch(args.url, {
|
|
412
|
+
method: "GET",
|
|
413
|
+
headers: {
|
|
414
|
+
...headers,
|
|
415
|
+
Accept: "text/event-stream"
|
|
416
|
+
},
|
|
417
|
+
signal: controller.signal
|
|
418
|
+
});
|
|
419
|
+
if (!resp.ok || !resp.body) throw new Error(`realtime connect failed: HTTP ${resp.status}`);
|
|
420
|
+
await consumeGamification(resp.body, args, () => {
|
|
421
|
+
backoff = INITIAL_BACKOFF_MS;
|
|
422
|
+
args.options.onOpen?.();
|
|
423
|
+
});
|
|
424
|
+
} catch (err) {
|
|
425
|
+
if (stopped) break;
|
|
426
|
+
args.options.onError?.(err);
|
|
427
|
+
}
|
|
428
|
+
if (stopped) break;
|
|
429
|
+
await delay(backoff);
|
|
430
|
+
backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
run();
|
|
434
|
+
return () => {
|
|
435
|
+
stopped = true;
|
|
436
|
+
controller?.abort();
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
async function consumeGamification(body, args, onFirstFrame) {
|
|
440
|
+
const reader = body.getReader();
|
|
441
|
+
const decoder = new TextDecoder();
|
|
442
|
+
let buffer = "";
|
|
443
|
+
let sawFrame = false;
|
|
444
|
+
for (;;) {
|
|
445
|
+
const { done, value } = await reader.read();
|
|
446
|
+
if (done) return;
|
|
447
|
+
buffer += decoder.decode(value, { stream: true });
|
|
448
|
+
let idx;
|
|
449
|
+
while ((idx = buffer.indexOf("\n\n")) !== -1) {
|
|
450
|
+
const frame = buffer.slice(0, idx);
|
|
451
|
+
buffer = buffer.slice(idx + 2);
|
|
452
|
+
if (!sawFrame) {
|
|
453
|
+
sawFrame = true;
|
|
454
|
+
onFirstFrame();
|
|
455
|
+
}
|
|
456
|
+
dispatchGamificationFrame(frame, args.onChange);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function dispatchGamificationFrame(frame, onChange) {
|
|
461
|
+
let event = "";
|
|
462
|
+
const dataLines = [];
|
|
463
|
+
for (const line of frame.split("\n")) {
|
|
464
|
+
if (line.startsWith(":")) continue;
|
|
465
|
+
if (line.startsWith("event:")) event = line.slice(6).trim();
|
|
466
|
+
else if (line.startsWith("data:")) dataLines.push(line.slice(5).trim());
|
|
467
|
+
}
|
|
468
|
+
if (event !== "change") return;
|
|
469
|
+
try {
|
|
470
|
+
const payload = JSON.parse(dataLines.join("\n"));
|
|
471
|
+
onChange({
|
|
472
|
+
kind: payload.kind,
|
|
473
|
+
data: payload.data,
|
|
474
|
+
truncated: payload.truncated
|
|
475
|
+
});
|
|
476
|
+
} catch {}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
//#endregion
|
|
480
|
+
//#region src/ai-stream.ts
|
|
481
|
+
function newUsageAccumulator() {
|
|
482
|
+
return {
|
|
483
|
+
input_tokens: 0,
|
|
484
|
+
output_tokens: 0,
|
|
485
|
+
cache_creation_input_tokens: 0,
|
|
486
|
+
cache_read_input_tokens: 0
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
function toAiUsage(acc) {
|
|
490
|
+
return {
|
|
491
|
+
input_tokens: acc.input_tokens,
|
|
492
|
+
output_tokens: acc.output_tokens,
|
|
493
|
+
cache_creation_input_tokens: acc.cache_creation_input_tokens,
|
|
494
|
+
cache_read_input_tokens: acc.cache_read_input_tokens
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
/** Read a numeric field defensively (vendors occasionally send null). */
|
|
498
|
+
function num(v) {
|
|
499
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Merge a vendor `usage`-shaped object into the accumulator. Handles both
|
|
503
|
+
* Anthropic (`input_tokens` / `output_tokens` / `cache_*`) and OpenAI
|
|
504
|
+
* (`prompt_tokens` / `completion_tokens`) field names, taking the max so a
|
|
505
|
+
* later frame that re-reports a counter doesn't shrink it.
|
|
506
|
+
*/
|
|
507
|
+
function mergeUsage(acc, usage) {
|
|
508
|
+
const input = usage["input_tokens"] ?? usage["prompt_tokens"];
|
|
509
|
+
const output = usage["output_tokens"] ?? usage["completion_tokens"];
|
|
510
|
+
if (input !== void 0) acc.input_tokens = Math.max(acc.input_tokens, num(input));
|
|
511
|
+
if (output !== void 0) acc.output_tokens = Math.max(acc.output_tokens, num(output));
|
|
512
|
+
if (usage["cache_creation_input_tokens"] !== void 0) acc.cache_creation_input_tokens = Math.max(acc.cache_creation_input_tokens, num(usage["cache_creation_input_tokens"]));
|
|
513
|
+
if (usage["cache_read_input_tokens"] !== void 0) acc.cache_read_input_tokens = Math.max(acc.cache_read_input_tokens, num(usage["cache_read_input_tokens"]));
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Parse one SSE frame (everything between blank lines) and return the text
|
|
517
|
+
* delta it carries, if any. Side-effect: folds any usage / model / stop
|
|
518
|
+
* metadata into `acc`. Returns `null` for keepalive comments, the `[DONE]`
|
|
519
|
+
* sentinel, and frames with no text (e.g. Anthropic's `message_start`).
|
|
520
|
+
*
|
|
521
|
+
* Tolerant by design — a malformed `data:` payload is skipped rather than
|
|
522
|
+
* tearing down the stream.
|
|
523
|
+
*/
|
|
524
|
+
function parseAiSseFrame(frame, acc) {
|
|
525
|
+
const dataLines = [];
|
|
526
|
+
for (const line of frame.split(/\r\n|\r|\n/)) {
|
|
527
|
+
if (line.startsWith(":")) continue;
|
|
528
|
+
if (line.startsWith("data:")) dataLines.push(line.slice(5).trimStart());
|
|
529
|
+
}
|
|
530
|
+
if (dataLines.length === 0) return null;
|
|
531
|
+
const data = dataLines.join("\n").trim();
|
|
532
|
+
if (data === "" || data === "[DONE]") return null;
|
|
533
|
+
let payload;
|
|
534
|
+
try {
|
|
535
|
+
payload = JSON.parse(data);
|
|
536
|
+
} catch {
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
const type = typeof payload["type"] === "string" ? payload["type"] : "";
|
|
540
|
+
if (type === "content_block_delta") {
|
|
541
|
+
const text = payload["delta"]?.["text"];
|
|
542
|
+
return typeof text === "string" ? text : null;
|
|
543
|
+
}
|
|
544
|
+
if (type === "message_start") {
|
|
545
|
+
const message = payload["message"];
|
|
546
|
+
const usage = message?.["usage"];
|
|
547
|
+
if (usage && typeof usage === "object") mergeUsage(acc, usage);
|
|
548
|
+
if (typeof message?.["model"] === "string") acc.model = message["model"];
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
if (type === "message_delta") {
|
|
552
|
+
const usage = payload["usage"];
|
|
553
|
+
if (usage && typeof usage === "object") mergeUsage(acc, usage);
|
|
554
|
+
const delta = payload["delta"];
|
|
555
|
+
if (typeof delta?.["stop_reason"] === "string") acc.stopReason = delta["stop_reason"];
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
if (type === "message_stop") return null;
|
|
559
|
+
if (Array.isArray(payload["choices"])) {
|
|
560
|
+
const choices = payload["choices"];
|
|
561
|
+
if (typeof payload["model"] === "string") acc.model = payload["model"];
|
|
562
|
+
const usage = payload["usage"];
|
|
563
|
+
if (usage && typeof usage === "object") mergeUsage(acc, usage);
|
|
564
|
+
const first = choices[0];
|
|
565
|
+
if (first) {
|
|
566
|
+
const finish = first["finish_reason"];
|
|
567
|
+
if (typeof finish === "string") acc.stopReason = finish;
|
|
568
|
+
const content = first["delta"]?.["content"];
|
|
569
|
+
if (typeof content === "string" && content.length > 0) return content;
|
|
570
|
+
}
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* SSE event-frame delimiter: a blank line. The spec permits any line ending
|
|
577
|
+
* (`\r\n`, `\n`, `\r`), so a blank line is two consecutive line breaks in any
|
|
578
|
+
* combination — `\r\n\r\n`, `\n\n`, or `\r\r`. Some servers / proxies emit
|
|
579
|
+
* CRLF, so splitting on `\n\n` alone would never frame those streams.
|
|
580
|
+
*/
|
|
581
|
+
const SSE_FRAME_DELIMITER = /\r\n\r\n|\n\n|\r\r/;
|
|
582
|
+
/**
|
|
583
|
+
* Pull the next complete SSE frame off the front of `buffer`, returning the
|
|
584
|
+
* frame text (without the trailing blank-line delimiter) and the remaining
|
|
585
|
+
* buffer, or `null` when no complete frame is present yet.
|
|
586
|
+
*
|
|
587
|
+
* Only *complete* delimiters match: a buffer ending mid-delimiter (e.g.
|
|
588
|
+
* `…\r\n\r`, awaiting the final `\n`) leaves the partial bytes buffered for the
|
|
589
|
+
* next read rather than framing prematurely.
|
|
590
|
+
*/
|
|
591
|
+
function splitNextSseFrame(buffer) {
|
|
592
|
+
const m = SSE_FRAME_DELIMITER.exec(buffer);
|
|
593
|
+
if (!m) return null;
|
|
594
|
+
return {
|
|
595
|
+
frame: buffer.slice(0, m.index),
|
|
596
|
+
rest: buffer.slice(m.index + m[0].length)
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Open the streaming route and yield vendor-neutral events. Throws an
|
|
601
|
+
* `AmbaApiError` on a transport failure or a non-2xx response (the error body
|
|
602
|
+
* is read and surfaced just like the buffered path).
|
|
603
|
+
*
|
|
604
|
+
* The generator yields zero or more `{type:'delta'}` events followed by
|
|
605
|
+
* exactly one terminal `{type:'done'}` when the stream completes cleanly.
|
|
606
|
+
*/
|
|
607
|
+
async function* invokeAiStream(args) {
|
|
608
|
+
let resp;
|
|
609
|
+
try {
|
|
610
|
+
resp = await fetch(args.url, {
|
|
611
|
+
method: "POST",
|
|
612
|
+
headers: args.headers,
|
|
613
|
+
body: args.body
|
|
614
|
+
});
|
|
615
|
+
} catch (err) {
|
|
616
|
+
throw new AmbaApiError("NETWORK_ERROR", `AI stream network error: ${String(err)}`);
|
|
617
|
+
}
|
|
618
|
+
if (!resp.ok) {
|
|
619
|
+
const text = await resp.text().catch(() => "");
|
|
620
|
+
let parsed = void 0;
|
|
621
|
+
if (text.length > 0) try {
|
|
622
|
+
parsed = JSON.parse(text);
|
|
623
|
+
} catch {
|
|
624
|
+
parsed = text;
|
|
625
|
+
}
|
|
626
|
+
const envelope = parsed && typeof parsed === "object" ? parsed : void 0;
|
|
627
|
+
throw new AmbaApiError(typeof envelope?.error?.code === "string" ? envelope.error.code : `HTTP_${resp.status}`, typeof envelope?.error?.message === "string" ? envelope.error.message : typeof parsed === "string" && parsed.length > 0 ? parsed : `AI stream failed with status ${resp.status}`, parsed);
|
|
628
|
+
}
|
|
629
|
+
if (!resp.body) throw new AmbaApiError("NETWORK_ERROR", "AI stream response had no readable body (the environment does not expose a streaming fetch body).");
|
|
630
|
+
const reader = resp.body.getReader();
|
|
631
|
+
const decoder = new TextDecoder();
|
|
632
|
+
const acc = newUsageAccumulator();
|
|
633
|
+
let buffer = "";
|
|
634
|
+
try {
|
|
635
|
+
for (;;) {
|
|
636
|
+
const { done, value } = await reader.read();
|
|
637
|
+
if (value) buffer += decoder.decode(value, { stream: true });
|
|
638
|
+
if (done) buffer += decoder.decode();
|
|
639
|
+
let split;
|
|
640
|
+
while ((split = splitNextSseFrame(buffer)) !== null) {
|
|
641
|
+
buffer = split.rest;
|
|
642
|
+
const text = parseAiSseFrame(split.frame, acc);
|
|
643
|
+
if (text !== null && text.length > 0) {
|
|
644
|
+
args.onChunk?.(text);
|
|
645
|
+
yield {
|
|
646
|
+
type: "delta",
|
|
647
|
+
text
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
if (done) {
|
|
652
|
+
const tail = buffer.trim();
|
|
653
|
+
if (tail.length > 0) {
|
|
654
|
+
const text = parseAiSseFrame(tail, acc);
|
|
655
|
+
if (text !== null && text.length > 0) {
|
|
656
|
+
args.onChunk?.(text);
|
|
657
|
+
yield {
|
|
658
|
+
type: "delta",
|
|
659
|
+
text
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
} finally {
|
|
667
|
+
try {
|
|
668
|
+
reader.releaseLock();
|
|
669
|
+
} catch {}
|
|
670
|
+
}
|
|
671
|
+
yield {
|
|
672
|
+
type: "done",
|
|
673
|
+
usage: toAiUsage(acc),
|
|
674
|
+
model: acc.model,
|
|
675
|
+
stopReason: acc.stopReason ?? null
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
//#endregion
|
|
680
|
+
//#region src/resilience.ts
|
|
681
|
+
/**
|
|
682
|
+
* Best-effort online probe. In a browser/RN runtime `navigator.onLine`
|
|
683
|
+
* exists; under Node / SSR it doesn't, so we treat "unknown" as online
|
|
684
|
+
* (the executor's own error handling then decides).
|
|
685
|
+
*/
|
|
686
|
+
function isOnline() {
|
|
687
|
+
const nav = typeof navigator !== "undefined" ? navigator : void 0;
|
|
688
|
+
if (nav && typeof nav.onLine === "boolean") return nav.onLine;
|
|
689
|
+
return true;
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Error codes the server raised deterministically — replaying them would fail
|
|
693
|
+
* identically, so they're NEVER queued even when the device reports offline.
|
|
694
|
+
* (A request can fail validation/auth/etc. while `navigator.onLine` is briefly
|
|
695
|
+
* false; we must not buffer-and-retry those forever.)
|
|
696
|
+
*/
|
|
697
|
+
const DETERMINISTIC_ERROR_CODES = new Set([
|
|
698
|
+
"VALIDATION_ERROR",
|
|
699
|
+
"INVALID_ARGUMENT",
|
|
700
|
+
"INVALID_INPUT",
|
|
701
|
+
"UNAUTHORIZED",
|
|
702
|
+
"FORBIDDEN",
|
|
703
|
+
"NOT_FOUND",
|
|
704
|
+
"CONFLICT",
|
|
705
|
+
"CONSENT_NOT_GRANTED",
|
|
706
|
+
"NOT_INITIALIZED"
|
|
707
|
+
]);
|
|
708
|
+
/**
|
|
709
|
+
* Is this a deterministic server rejection — a coded error the server would
|
|
710
|
+
* raise identically on every replay (bad input, auth, not-found, conflict)? A
|
|
711
|
+
* queued op that fails this way is poison: retrying it forever would wedge the
|
|
712
|
+
* queue, so the flush DROPS it. Everything else (transport failures, 5xx
|
|
713
|
+
* `HTTP_ERROR`, `RATE_LIMITED`, `CIRCUIT_OPEN`, anything uncoded) is treated as
|
|
714
|
+
* transient and RETAINED for a later attempt — favoring retain over data loss.
|
|
715
|
+
*/
|
|
716
|
+
function isDeterministicError(err) {
|
|
717
|
+
const code = err?.code;
|
|
718
|
+
return typeof code === "string" && DETERMINISTIC_ERROR_CODES.has(code);
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Heuristic: does a thrown value look like a transient offline / transport
|
|
722
|
+
* failure (vs a deterministic 4xx the server would reject again)? Used at
|
|
723
|
+
* ENQUEUE time to decide whether a failed write is worth buffering — a
|
|
724
|
+
* `VALIDATION_ERROR` will fail identically on replay, so it throws instead.
|
|
725
|
+
*
|
|
726
|
+
* A deterministic server error is excluded even when the device reports
|
|
727
|
+
* offline: the request clearly reached the server (it got a coded response),
|
|
728
|
+
* so retrying it on reconnect is pointless and would wedge the queue.
|
|
729
|
+
*
|
|
730
|
+
* NOTE: this is intentionally stricter than {@link isDeterministicError}'s
|
|
731
|
+
* inverse — at enqueue time we only buffer GENUINE connectivity failures (a
|
|
732
|
+
* non-offline 5xx/429 surfaces to the caller immediately rather than silently
|
|
733
|
+
* queueing). The flush drop-decision uses {@link isDeterministicError} so a
|
|
734
|
+
* transient 5xx/429 encountered DURING replay is retained, not dropped.
|
|
735
|
+
*/
|
|
736
|
+
function isOfflineError(err) {
|
|
737
|
+
const code = err?.code;
|
|
738
|
+
if (typeof code === "string" && DETERMINISTIC_ERROR_CODES.has(code)) return false;
|
|
739
|
+
if (code === "NETWORK_ERROR") return true;
|
|
740
|
+
const msg = err?.message;
|
|
741
|
+
if (typeof msg === "string" && /network (error|request failed)|failed to fetch|fetch failed|load failed/i.test(msg)) return true;
|
|
742
|
+
return !isOnline();
|
|
743
|
+
}
|
|
744
|
+
/** Generate a stable idempotency key for a queued op. */
|
|
745
|
+
function newIdempotencyKey() {
|
|
746
|
+
const c = typeof globalThis !== "undefined" ? globalThis.crypto : void 0;
|
|
747
|
+
if (c && typeof c.randomUUID === "function") return `amba_obx_${c.randomUUID()}`;
|
|
748
|
+
return `amba_obx_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Durable, ordered, idempotent offline outbox. Opt-in via `Amba.offline`.
|
|
752
|
+
* Single in-flight flush is enforced so concurrent reconnect signals don't
|
|
753
|
+
* double-replay.
|
|
754
|
+
*/
|
|
755
|
+
var OfflineQueue = class {
|
|
756
|
+
store;
|
|
757
|
+
executor;
|
|
758
|
+
storageKey;
|
|
759
|
+
/**
|
|
760
|
+
* Notified when a queued op is dropped because its replay failed
|
|
761
|
+
* deterministically (a poisoned op the server will always reject). Lets the
|
|
762
|
+
* SDK surface the loss (debug log) instead of silently discarding.
|
|
763
|
+
*/
|
|
764
|
+
onDropped;
|
|
765
|
+
flushing = null;
|
|
766
|
+
/**
|
|
767
|
+
* Serializes every read-modify-write of the persisted outbox. Each mutation
|
|
768
|
+
* chains onto the previous one's promise, so concurrent `enqueue`s — and an
|
|
769
|
+
* `enqueue` racing the per-op removal inside `flush` — can never read a stale
|
|
770
|
+
* snapshot and clobber each other's writes (the lost-update bug). All
|
|
771
|
+
* storage mutations funnel through `mutate()`; only `pendingCount`/`pending`
|
|
772
|
+
* read without taking the lock (a stale-by-one read is harmless).
|
|
773
|
+
*/
|
|
774
|
+
tail = Promise.resolve();
|
|
775
|
+
/** Detach function for the auto-flush `online` listener, if installed. */
|
|
776
|
+
detachOnline = null;
|
|
777
|
+
/**
|
|
778
|
+
* Set once `dispose()` is called (on teardown / reconfigure). A disposed
|
|
779
|
+
* queue refuses to mutate the persisted outbox, so an orphaned in-flight
|
|
780
|
+
* flush — or any late call against the old instance — can never race the new
|
|
781
|
+
* queue by reading/writing the same storage after it's been replaced.
|
|
782
|
+
*/
|
|
783
|
+
disposed = false;
|
|
784
|
+
constructor(opts) {
|
|
785
|
+
this.store = opts.store;
|
|
786
|
+
this.executor = opts.executor;
|
|
787
|
+
this.storageKey = opts.storageKey ?? "offline_outbox";
|
|
788
|
+
this.onDropped = opts.onDropped;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Run `fn` exclusively against the persisted outbox: it chains after every
|
|
792
|
+
* prior mutation and the next one waits for it. `fn` receives the current
|
|
793
|
+
* ops and returns the next ops to persist plus its own result.
|
|
794
|
+
*/
|
|
795
|
+
mutate(fn, opts) {
|
|
796
|
+
const run = this.tail.then(async () => {
|
|
797
|
+
if (this.disposed && opts?.rejectIfDisposed) throw new Error("amba offline queue was disposed (SDK reconfigured)");
|
|
798
|
+
const { next, result } = await fn(await this.load());
|
|
799
|
+
await this.save(next);
|
|
800
|
+
return result;
|
|
801
|
+
});
|
|
802
|
+
this.tail = run.catch(() => void 0);
|
|
803
|
+
return run;
|
|
804
|
+
}
|
|
805
|
+
async load() {
|
|
806
|
+
const raw = await this.store.get(this.storageKey);
|
|
807
|
+
if (!raw) return [];
|
|
808
|
+
try {
|
|
809
|
+
const parsed = JSON.parse(raw);
|
|
810
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
811
|
+
} catch {
|
|
812
|
+
return [];
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
async save(ops) {
|
|
816
|
+
if (this.disposed) return;
|
|
817
|
+
if (ops.length === 0) {
|
|
818
|
+
await this.store.delete(this.storageKey);
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
await this.store.set(this.storageKey, JSON.stringify(ops));
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Permanently retire this queue. Detaches the auto-flush listener and marks
|
|
825
|
+
* it disposed so it can no longer mutate the persisted outbox. Returns once
|
|
826
|
+
* any in-flight flush + pending mutation chain has settled, so a caller (e.g.
|
|
827
|
+
* `configure()` teardown) can safely create a replacement queue knowing the
|
|
828
|
+
* old one will never touch storage again. Idempotent.
|
|
829
|
+
*/
|
|
830
|
+
async dispose() {
|
|
831
|
+
this.disposed = true;
|
|
832
|
+
if (this.detachOnline) {
|
|
833
|
+
this.detachOnline();
|
|
834
|
+
this.detachOnline = null;
|
|
835
|
+
}
|
|
836
|
+
try {
|
|
837
|
+
await this.flushing;
|
|
838
|
+
} catch {}
|
|
839
|
+
await this.tail.catch(() => void 0);
|
|
840
|
+
}
|
|
841
|
+
/** Whether this queue has been disposed. */
|
|
842
|
+
get isDisposed() {
|
|
843
|
+
return this.disposed;
|
|
844
|
+
}
|
|
845
|
+
/** Append an op to the durable outbox. Returns the persisted op. */
|
|
846
|
+
async enqueue(op) {
|
|
847
|
+
if (this.disposed) throw new Error("amba offline queue was disposed (SDK reconfigured)");
|
|
848
|
+
const full = {
|
|
849
|
+
...op,
|
|
850
|
+
idempotencyKey: op.idempotencyKey ?? newIdempotencyKey(),
|
|
851
|
+
enqueuedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
852
|
+
};
|
|
853
|
+
return this.mutate((ops) => ({
|
|
854
|
+
next: [...ops, full],
|
|
855
|
+
result: full
|
|
856
|
+
}), { rejectIfDisposed: true });
|
|
857
|
+
}
|
|
858
|
+
/** Number of ops awaiting replay. */
|
|
859
|
+
async pendingCount() {
|
|
860
|
+
return (await this.load()).length;
|
|
861
|
+
}
|
|
862
|
+
/** Snapshot the pending ops (a copy — mutating it does not affect the queue). */
|
|
863
|
+
async pending() {
|
|
864
|
+
return [...await this.load()];
|
|
865
|
+
}
|
|
866
|
+
/** Drop every queued op without replaying. */
|
|
867
|
+
async clear() {
|
|
868
|
+
await this.mutate(() => ({
|
|
869
|
+
next: [],
|
|
870
|
+
result: void 0
|
|
871
|
+
}));
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Replay the outbox in FIFO order. Each op is removed only after it
|
|
875
|
+
* succeeds; the first failure stops the flush and retains the remainder
|
|
876
|
+
* (so order is preserved and nothing is dropped mid-batch). Concurrent
|
|
877
|
+
* calls share one in-flight flush.
|
|
878
|
+
*/
|
|
879
|
+
async flush() {
|
|
880
|
+
if (this.disposed) return {
|
|
881
|
+
flushed: 0,
|
|
882
|
+
dropped: 0,
|
|
883
|
+
remaining: 0
|
|
884
|
+
};
|
|
885
|
+
if (this.flushing) return this.flushing;
|
|
886
|
+
this.flushing = this.runFlush().finally(() => {
|
|
887
|
+
this.flushing = null;
|
|
888
|
+
});
|
|
889
|
+
return this.flushing;
|
|
890
|
+
}
|
|
891
|
+
async runFlush() {
|
|
892
|
+
let flushed = 0;
|
|
893
|
+
let dropped = 0;
|
|
894
|
+
for (;;) {
|
|
895
|
+
if (this.disposed) break;
|
|
896
|
+
const head = (await this.load())[0];
|
|
897
|
+
if (!head) break;
|
|
898
|
+
let replayError = void 0;
|
|
899
|
+
let ok = false;
|
|
900
|
+
try {
|
|
901
|
+
await this.executor(head);
|
|
902
|
+
ok = true;
|
|
903
|
+
} catch (err) {
|
|
904
|
+
replayError = err;
|
|
905
|
+
}
|
|
906
|
+
if (!ok && !isDeterministicError(replayError)) {
|
|
907
|
+
await this.mutate((ops) => {
|
|
908
|
+
return {
|
|
909
|
+
next: ops.some((o) => o.idempotencyKey === head.idempotencyKey) ? ops : [head, ...ops],
|
|
910
|
+
result: void 0
|
|
911
|
+
};
|
|
912
|
+
});
|
|
913
|
+
break;
|
|
914
|
+
}
|
|
915
|
+
if (!await this.mutate((ops) => {
|
|
916
|
+
const idx = ops.findIndex((o) => o.idempotencyKey === head.idempotencyKey);
|
|
917
|
+
if (idx === -1) return {
|
|
918
|
+
next: ops,
|
|
919
|
+
result: false
|
|
920
|
+
};
|
|
921
|
+
return {
|
|
922
|
+
next: ops.slice(0, idx).concat(ops.slice(idx + 1)),
|
|
923
|
+
result: true
|
|
924
|
+
};
|
|
925
|
+
})) break;
|
|
926
|
+
if (ok) flushed += 1;
|
|
927
|
+
else {
|
|
928
|
+
dropped += 1;
|
|
929
|
+
try {
|
|
930
|
+
this.onDropped?.(head, replayError);
|
|
931
|
+
} catch {}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
const remaining = (await this.load()).length;
|
|
935
|
+
return {
|
|
936
|
+
flushed,
|
|
937
|
+
dropped,
|
|
938
|
+
remaining
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Install an auto-flush listener on the runtime's `online` event so the
|
|
943
|
+
* queue drains the moment connectivity returns. Idempotent — a second
|
|
944
|
+
* call is a no-op. Returns a detach function.
|
|
945
|
+
*/
|
|
946
|
+
startAutoFlush() {
|
|
947
|
+
if (this.detachOnline) return this.detachOnline;
|
|
948
|
+
const target = typeof globalThis !== "undefined" && typeof globalThis.addEventListener === "function" ? globalThis : null;
|
|
949
|
+
if (!target) {
|
|
950
|
+
this.detachOnline = () => {};
|
|
951
|
+
return this.detachOnline;
|
|
952
|
+
}
|
|
953
|
+
const handler = () => {
|
|
954
|
+
this.flush();
|
|
955
|
+
};
|
|
956
|
+
target.addEventListener("online", handler);
|
|
957
|
+
this.detachOnline = () => {
|
|
958
|
+
target.removeEventListener("online", handler);
|
|
959
|
+
this.detachOnline = null;
|
|
960
|
+
};
|
|
961
|
+
return this.detachOnline;
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
/**
|
|
965
|
+
* Apply a local change immediately, fire the real mutation, and roll the
|
|
966
|
+
* local change back if the mutation fails. Framework-agnostic: `apply` and
|
|
967
|
+
* `rollback` operate on whatever local view you hand them (a React state
|
|
968
|
+
* setter, a store, an in-memory cache).
|
|
969
|
+
*
|
|
970
|
+
* Resolves with the mutation's result on success. On failure it invokes
|
|
971
|
+
* `rollback` (with the value `apply` returned, so you can restore a captured
|
|
972
|
+
* snapshot) and rethrows the original error.
|
|
973
|
+
*
|
|
974
|
+
* @example
|
|
975
|
+
* await withOptimistic({
|
|
976
|
+
* apply: () => { const prev = todos; setTodos([...todos, draft]); return prev; },
|
|
977
|
+
* mutate: () => Amba.collections.insert("todos", draft),
|
|
978
|
+
* rollback: (prev) => setTodos(prev),
|
|
979
|
+
* });
|
|
980
|
+
*/
|
|
981
|
+
async function withOptimistic(spec) {
|
|
982
|
+
const applied = spec.apply();
|
|
983
|
+
try {
|
|
984
|
+
return await spec.mutate();
|
|
985
|
+
} catch (err) {
|
|
986
|
+
try {
|
|
987
|
+
spec.rollback(applied);
|
|
988
|
+
} catch {}
|
|
989
|
+
throw err;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
//#endregion
|
|
994
|
+
//#region src/push-rotation.ts
|
|
995
|
+
/**
|
|
996
|
+
* Subscribe to browser push-subscription rotation. On a
|
|
997
|
+
* `pushsubscriptionchange`, the new subscription endpoint is forwarded to
|
|
998
|
+
* `reRegister` (which the SDK points at its idempotent token-upsert).
|
|
999
|
+
*
|
|
1000
|
+
* Returns an unsubscribe function. No-ops (returning a no-op detacher) when
|
|
1001
|
+
* the runtime has no service-worker / push support — e.g. SSR or a browser
|
|
1002
|
+
* without Web Push — so callers can invoke it unconditionally.
|
|
1003
|
+
*/
|
|
1004
|
+
/**
|
|
1005
|
+
* Are we executing inside a ServiceWorkerGlobalScope? `pushsubscriptionchange`
|
|
1006
|
+
* ONLY fires there — on a normal page `self === window`, where the event never
|
|
1007
|
+
* fires, so attaching a listener silently does nothing. We detect the scope by
|
|
1008
|
+
* the `ServiceWorkerGlobalScope` constructor when present, falling back to the
|
|
1009
|
+
* SW-only `self.registration.pushManager` shape (a window has no `registration`
|
|
1010
|
+
* on its global).
|
|
1011
|
+
*/
|
|
1012
|
+
function inServiceWorkerScope(g) {
|
|
1013
|
+
if (typeof g !== "object" || g === null) return false;
|
|
1014
|
+
const swCtor = globalThis.ServiceWorkerGlobalScope;
|
|
1015
|
+
if (typeof swCtor === "function" && g instanceof swCtor) return true;
|
|
1016
|
+
const reg = g.registration;
|
|
1017
|
+
return !!reg && typeof reg.pushManager === "object" && reg.pushManager !== null;
|
|
1018
|
+
}
|
|
1019
|
+
let pageScopeWarned = false;
|
|
1020
|
+
function enableWebPushAutoRotation(reRegister) {
|
|
1021
|
+
const g = typeof self !== "undefined" ? self : void 0;
|
|
1022
|
+
if (!g || !inServiceWorkerScope(g)) {
|
|
1023
|
+
if (!pageScopeWarned && typeof console !== "undefined") {
|
|
1024
|
+
pageScopeWarned = true;
|
|
1025
|
+
console.warn("[amba] push.enableAutoRotation() must run inside your service worker — `pushsubscriptionchange` doesn't fire on a page/window. Call it from your amba service worker. No-op here.");
|
|
1026
|
+
}
|
|
1027
|
+
return () => {};
|
|
1028
|
+
}
|
|
1029
|
+
const sw = g;
|
|
1030
|
+
if (typeof sw.addEventListener !== "function") return () => {};
|
|
1031
|
+
const onChange = (event) => {
|
|
1032
|
+
(async () => {
|
|
1033
|
+
try {
|
|
1034
|
+
const ev = event;
|
|
1035
|
+
let endpoint = ev?.newSubscription?.endpoint;
|
|
1036
|
+
if (!endpoint && sw.registration?.pushManager?.getSubscription) endpoint = (await sw.registration.pushManager.getSubscription())?.endpoint ?? void 0;
|
|
1037
|
+
if (!endpoint) return;
|
|
1038
|
+
const work = reRegister(endpoint, "web");
|
|
1039
|
+
if (typeof ev?.waitUntil === "function") ev.waitUntil(work);
|
|
1040
|
+
await work;
|
|
1041
|
+
} catch (err) {
|
|
1042
|
+
if (typeof console !== "undefined") console.error("[amba] push auto-rotation re-register failed", err);
|
|
1043
|
+
}
|
|
1044
|
+
})();
|
|
1045
|
+
};
|
|
1046
|
+
sw.addEventListener("pushsubscriptionchange", onChange);
|
|
1047
|
+
return () => {
|
|
1048
|
+
sw.removeEventListener?.("pushsubscriptionchange", onChange);
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
//#endregion
|
|
1053
|
+
//#region src/local-notifications.ts
|
|
1054
|
+
/**
|
|
1055
|
+
* Browsers store a `setTimeout` delay in a signed 32-bit int. A delay
|
|
1056
|
+
* larger than `2^31 - 1` ms (~24.8 days) overflows and is clamped — most
|
|
1057
|
+
* browsers fire (often immediately) rather than wait. So a far-future
|
|
1058
|
+
* local notification scheduled via the in-page timer fallback would fire
|
|
1059
|
+
* at the wrong time. We chunk long delays: arm a timer for at most
|
|
1060
|
+
* `MAX_TIMEOUT_MS`, and on wake re-arm for the remainder until due.
|
|
1061
|
+
*/
|
|
1062
|
+
const MAX_TIMEOUT_MS = 2147483647;
|
|
1063
|
+
const pendingTimers = /* @__PURE__ */ new Map();
|
|
1064
|
+
function hasNotificationApi() {
|
|
1065
|
+
return typeof globalThis !== "undefined" && typeof globalThis.Notification === "function";
|
|
1066
|
+
}
|
|
1067
|
+
function timestampTriggerCtor() {
|
|
1068
|
+
const ctor = globalThis.TimestampTrigger;
|
|
1069
|
+
return typeof ctor === "function" ? ctor : void 0;
|
|
1070
|
+
}
|
|
1071
|
+
function genId() {
|
|
1072
|
+
const c = globalThis.crypto;
|
|
1073
|
+
if (c?.randomUUID) return c.randomUUID();
|
|
1074
|
+
return `amba_local_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Per-INSTANCE token. The stable `__ambaLocalId` survives a reschedule of the
|
|
1078
|
+
* same id; this token does not — every individual `scheduleLocal` call mints a
|
|
1079
|
+
* fresh one. It lets `listScheduledLocal` distinguish a NEW pending schedule
|
|
1080
|
+
* for an id from an OLD already-fired-but-still-visible notification of the
|
|
1081
|
+
* SAME id (which the Service Worker reports under `getNotifications()`),
|
|
1082
|
+
* instead of conflating the two by id and hiding the pending one. (Round-5.)
|
|
1083
|
+
*/
|
|
1084
|
+
function genInstanceToken() {
|
|
1085
|
+
return `${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Resolve the id a schedule will be filed under. A caller may omit `id`
|
|
1089
|
+
* (we mint one) but must not pass a BLANK/whitespace id: `cancelLocal`
|
|
1090
|
+
* rejects blank ids, so returning one in the handle would make the
|
|
1091
|
+
* notification uncancellable. We reject blank at schedule time so every
|
|
1092
|
+
* returned handle id round-trips through `cancelLocal`. Shared by both
|
|
1093
|
+
* SDK platforms (web + react-native) for identical validation.
|
|
1094
|
+
*/
|
|
1095
|
+
function normalizeScheduleId(rawId) {
|
|
1096
|
+
if (rawId === void 0) return genId();
|
|
1097
|
+
if (typeof rawId !== "string" || rawId.trim() === "") throw new AmbaApiError("INVALID_ARGUMENT", "Amba.notifications.scheduleLocal: id, when provided, must be a non-empty string");
|
|
1098
|
+
return rawId;
|
|
1099
|
+
}
|
|
1100
|
+
function parseTrigger(input) {
|
|
1101
|
+
const trigger = input.trigger;
|
|
1102
|
+
if (!trigger || typeof trigger !== "object") throw new AmbaApiError("INVALID_ARGUMENT", "Amba.notifications.scheduleLocal: trigger must be { at: Date } or { inSeconds: number }");
|
|
1103
|
+
if ("at" in trigger) {
|
|
1104
|
+
const ms = (trigger.at instanceof Date ? trigger.at : new Date(trigger.at)).getTime();
|
|
1105
|
+
if (Number.isNaN(ms)) throw new AmbaApiError("INVALID_ARGUMENT", "Amba.notifications.scheduleLocal: trigger.at must be a valid Date");
|
|
1106
|
+
return {
|
|
1107
|
+
kind: "absolute",
|
|
1108
|
+
at: ms
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
if ("inSeconds" in trigger) {
|
|
1112
|
+
if (!Number.isFinite(trigger.inSeconds) || trigger.inSeconds < 0) throw new AmbaApiError("INVALID_ARGUMENT", "Amba.notifications.scheduleLocal: trigger.inSeconds must be a non-negative number");
|
|
1113
|
+
return {
|
|
1114
|
+
kind: "relative",
|
|
1115
|
+
seconds: trigger.inSeconds
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
throw new AmbaApiError("INVALID_ARGUMENT", "Amba.notifications.scheduleLocal: trigger must be { at: Date } or { inSeconds: number }");
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Resolve a parsed trigger to an absolute fire time (ms epoch). For a
|
|
1122
|
+
* relative trigger this is `now + seconds` — call it ONLY after permission
|
|
1123
|
+
* has resolved so `inSeconds` counts from when scheduling actually completes.
|
|
1124
|
+
*/
|
|
1125
|
+
function computeFireAt(parsed) {
|
|
1126
|
+
return parsed.kind === "absolute" ? parsed.at : Date.now() + parsed.seconds * 1e3;
|
|
1127
|
+
}
|
|
1128
|
+
function validateContent(input) {
|
|
1129
|
+
if (!input || typeof input.title !== "string" || input.title.trim() === "") throw new AmbaApiError("INVALID_ARGUMENT", "Amba.notifications.scheduleLocal: title is required");
|
|
1130
|
+
if (typeof input.body !== "string") throw new AmbaApiError("INVALID_ARGUMENT", "Amba.notifications.scheduleLocal: body must be a string");
|
|
1131
|
+
}
|
|
1132
|
+
async function ensurePermission() {
|
|
1133
|
+
const N = globalThis.Notification;
|
|
1134
|
+
let permission = N.permission;
|
|
1135
|
+
if (permission === "default") permission = await N.requestPermission();
|
|
1136
|
+
if (permission !== "granted") throw new AmbaApiError("LOCAL_NOTIFICATIONS_PERMISSION_DENIED", "user denied notification permission — local notifications cannot be shown");
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* How long we'll wait for an existing-but-not-yet-active registration to
|
|
1140
|
+
* become `ready` before falling through to the in-page timer path. A real
|
|
1141
|
+
* activation is near-instant; this cap only guards against a wedged
|
|
1142
|
+
* activation hanging the call.
|
|
1143
|
+
*/
|
|
1144
|
+
const SW_READY_TIMEOUT_MS = 1e3;
|
|
1145
|
+
/**
|
|
1146
|
+
* Resolve the Service Worker registration to use for OS-trigger
|
|
1147
|
+
* notifications — WITHOUT ever hanging.
|
|
1148
|
+
*
|
|
1149
|
+
* The footgun this avoids: `navigator.serviceWorker.ready` NEVER resolves
|
|
1150
|
+
* when no service worker is registered (the common case for a page that has
|
|
1151
|
+
* the Notifications API but no SW). Awaiting it unconditionally would hang
|
|
1152
|
+
* `scheduleLocal` / `cancelLocal` / `listScheduledLocal` forever instead of
|
|
1153
|
+
* falling back to the documented in-page timer path. (Round-6 HIGH.)
|
|
1154
|
+
*
|
|
1155
|
+
* Instead we first call `getRegistration()`, which resolves to `undefined`
|
|
1156
|
+
* when nothing is registered (no hang). Only if a registration exists do we
|
|
1157
|
+
* wait — briefly, raced against a timeout — for `.ready`, so an active SW is
|
|
1158
|
+
* used while a wedged activation can't stall the caller.
|
|
1159
|
+
*/
|
|
1160
|
+
async function getServiceWorkerRegistration() {
|
|
1161
|
+
const nav = globalThis.navigator;
|
|
1162
|
+
if (!nav || !("serviceWorker" in nav)) return void 0;
|
|
1163
|
+
const swc = nav.serviceWorker;
|
|
1164
|
+
let existing;
|
|
1165
|
+
try {
|
|
1166
|
+
existing = await swc.getRegistration();
|
|
1167
|
+
} catch {
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
if (!existing) return void 0;
|
|
1171
|
+
try {
|
|
1172
|
+
return await Promise.race([swc.ready, new Promise((resolve) => setTimeout(() => resolve(void 0), SW_READY_TIMEOUT_MS))]) ?? existing;
|
|
1173
|
+
} catch {
|
|
1174
|
+
return existing;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Cancel (close) any Service Worker OS-trigger notification scheduled by us
|
|
1179
|
+
* under `tag === id`. Passes `includeTriggered: true` so that
|
|
1180
|
+
* scheduled-but-not-yet-fired trigger notifications are returned by
|
|
1181
|
+
* `getNotifications` — without it, pending OS-trigger notifications are
|
|
1182
|
+
* invisible and therefore impossible to cancel. No-op when there's no
|
|
1183
|
+
* registration (degraded / timer-only environments).
|
|
1184
|
+
*/
|
|
1185
|
+
async function cancelOsTriggerNotification(registration, id) {
|
|
1186
|
+
if (!registration) return;
|
|
1187
|
+
try {
|
|
1188
|
+
const matches = await registration.getNotifications({
|
|
1189
|
+
tag: id,
|
|
1190
|
+
includeTriggered: true
|
|
1191
|
+
});
|
|
1192
|
+
for (const n of matches) {
|
|
1193
|
+
if ((n.data ?? {}).__ambaLocalId !== id) continue;
|
|
1194
|
+
n.close();
|
|
1195
|
+
}
|
|
1196
|
+
} catch {}
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Show the in-page `Notification` for a fired timer. Tolerant of permission
|
|
1200
|
+
* being revoked between scheduling and firing (construction can throw).
|
|
1201
|
+
*/
|
|
1202
|
+
function fireInPageNotification(record, id) {
|
|
1203
|
+
try {
|
|
1204
|
+
const N = globalThis.Notification;
|
|
1205
|
+
new N(record.title, {
|
|
1206
|
+
body: record.body,
|
|
1207
|
+
data: {
|
|
1208
|
+
...record.data,
|
|
1209
|
+
__ambaLocalId: id
|
|
1210
|
+
},
|
|
1211
|
+
tag: id
|
|
1212
|
+
});
|
|
1213
|
+
} catch {}
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Arm the next chunk of an in-page timer for the registry entry under `id`.
|
|
1217
|
+
* If the remaining delay until `entry.fireAt` exceeds `MAX_TIMEOUT_MS`, arm
|
|
1218
|
+
* for `MAX_TIMEOUT_MS` and re-arm on wake; otherwise arm for the exact
|
|
1219
|
+
* remainder, then fire and remove the entry.
|
|
1220
|
+
*
|
|
1221
|
+
* `entry.handle` is reassigned to each chunk's timeout id, so a concurrent
|
|
1222
|
+
* `clearTimeout(entry.handle)` (cancel / reschedule) always targets the live
|
|
1223
|
+
* chunk and stops the chain.
|
|
1224
|
+
*/
|
|
1225
|
+
function armTimerChunk(id, entry) {
|
|
1226
|
+
const remaining = entry.fireAt - Date.now();
|
|
1227
|
+
if (remaining > MAX_TIMEOUT_MS) {
|
|
1228
|
+
entry.handle = setTimeout(() => {
|
|
1229
|
+
if (pendingTimers.get(id) === entry) armTimerChunk(id, entry);
|
|
1230
|
+
}, MAX_TIMEOUT_MS);
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
entry.handle = setTimeout(() => {
|
|
1234
|
+
if (pendingTimers.get(id) !== entry) return;
|
|
1235
|
+
pendingTimers.delete(id);
|
|
1236
|
+
fireInPageNotification(entry.record, id);
|
|
1237
|
+
}, Math.max(0, remaining));
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* The web `notifications` namespace. Web capability is partial — see the
|
|
1241
|
+
* module-level doc. The returned handle includes `scheduledVia` so apps
|
|
1242
|
+
* can detect whether the notification was handed to the OS
|
|
1243
|
+
* (`"os-trigger"`) or relies on an in-page timer (`"timer"`).
|
|
1244
|
+
*/
|
|
1245
|
+
const notifications = {
|
|
1246
|
+
async scheduleLocal(input) {
|
|
1247
|
+
validateContent(input);
|
|
1248
|
+
if (!hasNotificationApi()) throw new AmbaApiError("LOCAL_NOTIFICATIONS_UNAVAILABLE", "the Notifications API is not available in this environment (server-side rendering, or a browser with notifications disabled). Local notifications require a browser context.");
|
|
1249
|
+
const parsedTrigger = parseTrigger(input);
|
|
1250
|
+
const id = normalizeScheduleId(input.id);
|
|
1251
|
+
await ensurePermission();
|
|
1252
|
+
const fireAt = computeFireAt(parsedTrigger);
|
|
1253
|
+
const instanceToken = genInstanceToken();
|
|
1254
|
+
const TriggerCtor = timestampTriggerCtor();
|
|
1255
|
+
const registration = await getServiceWorkerRegistration();
|
|
1256
|
+
if (TriggerCtor && registration) {
|
|
1257
|
+
await registration.showNotification(input.title, {
|
|
1258
|
+
body: input.body,
|
|
1259
|
+
data: {
|
|
1260
|
+
...input.data,
|
|
1261
|
+
__ambaLocalId: id,
|
|
1262
|
+
__ambaInstance: instanceToken
|
|
1263
|
+
},
|
|
1264
|
+
tag: id,
|
|
1265
|
+
showTrigger: new TriggerCtor(fireAt)
|
|
1266
|
+
});
|
|
1267
|
+
const priorTimer$1 = pendingTimers.get(id);
|
|
1268
|
+
if (priorTimer$1) {
|
|
1269
|
+
clearTimeout(priorTimer$1.handle);
|
|
1270
|
+
pendingTimers.delete(id);
|
|
1271
|
+
}
|
|
1272
|
+
return {
|
|
1273
|
+
id,
|
|
1274
|
+
scheduledVia: "os-trigger"
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
const priorTimer = pendingTimers.get(id);
|
|
1278
|
+
if (priorTimer) {
|
|
1279
|
+
clearTimeout(priorTimer.handle);
|
|
1280
|
+
pendingTimers.delete(id);
|
|
1281
|
+
}
|
|
1282
|
+
await cancelOsTriggerNotification(registration, id);
|
|
1283
|
+
const entry = {
|
|
1284
|
+
handle: void 0,
|
|
1285
|
+
fireAt,
|
|
1286
|
+
record: {
|
|
1287
|
+
id,
|
|
1288
|
+
title: input.title,
|
|
1289
|
+
body: input.body,
|
|
1290
|
+
data: input.data
|
|
1291
|
+
}
|
|
1292
|
+
};
|
|
1293
|
+
pendingTimers.set(id, entry);
|
|
1294
|
+
armTimerChunk(id, entry);
|
|
1295
|
+
return {
|
|
1296
|
+
id,
|
|
1297
|
+
scheduledVia: "timer"
|
|
1298
|
+
};
|
|
1299
|
+
},
|
|
1300
|
+
async cancelLocal(id) {
|
|
1301
|
+
if (typeof id !== "string" || id.trim() === "") throw new AmbaApiError("INVALID_ARGUMENT", "Amba.notifications.cancelLocal: id is required");
|
|
1302
|
+
const timer = pendingTimers.get(id);
|
|
1303
|
+
if (timer) {
|
|
1304
|
+
clearTimeout(timer.handle);
|
|
1305
|
+
pendingTimers.delete(id);
|
|
1306
|
+
}
|
|
1307
|
+
await cancelOsTriggerNotification(await getServiceWorkerRegistration(), id);
|
|
1308
|
+
},
|
|
1309
|
+
async listScheduledLocal() {
|
|
1310
|
+
const out = [];
|
|
1311
|
+
for (const { record } of pendingTimers.values()) out.push({ ...record });
|
|
1312
|
+
const registration = await getServiceWorkerRegistration();
|
|
1313
|
+
if (registration) try {
|
|
1314
|
+
const [withTriggered, shownOnly] = await Promise.all([registration.getNotifications({ includeTriggered: true }), registration.getNotifications()]);
|
|
1315
|
+
const shownInstances = /* @__PURE__ */ new Set();
|
|
1316
|
+
for (const n of shownOnly) {
|
|
1317
|
+
const token = (n.data ?? {}).__ambaInstance;
|
|
1318
|
+
if (token) shownInstances.add(token);
|
|
1319
|
+
}
|
|
1320
|
+
for (const n of withTriggered) {
|
|
1321
|
+
const data = n.data ?? {};
|
|
1322
|
+
const id = data.__ambaLocalId;
|
|
1323
|
+
if (!id) continue;
|
|
1324
|
+
const token = data.__ambaInstance;
|
|
1325
|
+
if (token && shownInstances.has(token)) continue;
|
|
1326
|
+
if (out.some((existing) => existing.id === id)) continue;
|
|
1327
|
+
out.push({
|
|
1328
|
+
id,
|
|
1329
|
+
title: n.title,
|
|
1330
|
+
body: n.body ?? "",
|
|
1331
|
+
data
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
} catch {}
|
|
1335
|
+
return out;
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1338
|
+
|
|
1339
|
+
//#endregion
|
|
1340
|
+
//#region src/index.ts
|
|
1341
|
+
const DEFAULT_API_URL = "https://api.amba.dev";
|
|
1342
|
+
/**
|
|
1343
|
+
* Default host that serves deployed functions. `Amba.functions.invoke(name)`
|
|
1344
|
+
* targets `https://<projectId>.<DEFAULT_FUNCTIONS_HOST>/<name>` unless a
|
|
1345
|
+
* `functionsBaseUrl` override is configured. The `<projectId>` label is the
|
|
1346
|
+
* server-resolved id (from `diagnostics.ping`), which the function router
|
|
1347
|
+
* accepts directly.
|
|
1348
|
+
*/
|
|
1349
|
+
const DEFAULT_FUNCTIONS_HOST = "fn.amba.host";
|
|
1350
|
+
/**
|
|
1351
|
+
* Internal state — one instance per page. Created lazily by `Amba.configure()`.
|
|
1352
|
+
*/
|
|
1353
|
+
let coreInstance = null;
|
|
1354
|
+
/**
|
|
1355
|
+
* Stored at configure-time so `Amba.projectId` returns the configured
|
|
1356
|
+
* value without going through the engine. Cached only after the
|
|
1357
|
+
* core finishes initializing.
|
|
1358
|
+
*/
|
|
1359
|
+
let _configuredProjectId = null;
|
|
1360
|
+
/**
|
|
1361
|
+
* Realtime (`Amba.collection(name).subscribe`) needs the API base URL,
|
|
1362
|
+
* client key, and current session token in the TS layer to open the
|
|
1363
|
+
* streamed connection — the engine doesn't expose them back. We capture
|
|
1364
|
+
* the base URL + client key at configure-time and the session token from
|
|
1365
|
+
* every auth result (the engine rotates it internally on refresh; the
|
|
1366
|
+
* subscribe connection picks up the latest at connect time).
|
|
1367
|
+
*/
|
|
1368
|
+
let _configuredApiUrl = DEFAULT_API_URL;
|
|
1369
|
+
let _configuredClientKey = null;
|
|
1370
|
+
let _currentSessionToken = null;
|
|
1371
|
+
/**
|
|
1372
|
+
* Functions invocation state. `_configuredFunctionsBaseUrl` is the optional
|
|
1373
|
+
* override from `Amba.configure({ functionsBaseUrl })`. When it's null, the
|
|
1374
|
+
* default-host path resolves the project id once via `diagnostics.ping` and
|
|
1375
|
+
* caches it in `_resolvedFunctionsProjectId` so subsequent invokes skip the
|
|
1376
|
+
* round-trip. Both reset on every `configure()`.
|
|
1377
|
+
*/
|
|
1378
|
+
let _configuredFunctionsBaseUrl = null;
|
|
1379
|
+
let _resolvedFunctionsProjectId = null;
|
|
1380
|
+
/**
|
|
1381
|
+
* Offline-resilience state. `_persistence` is the localStorage-backed store
|
|
1382
|
+
* captured at configure-time; `_offlineQueue` is lazily built the first time
|
|
1383
|
+
* `Amba.offline.enable()` runs. `_offlineEnabled` gates whether failed writes
|
|
1384
|
+
* are diverted to the durable outbox (opt-in — default behavior is unchanged,
|
|
1385
|
+
* a failed write throws). All three reset on every `configure()`.
|
|
1386
|
+
*/
|
|
1387
|
+
let _persistence = null;
|
|
1388
|
+
let _offlineQueue = null;
|
|
1389
|
+
let _offlineEnabled = false;
|
|
1390
|
+
/**
|
|
1391
|
+
* Detacher for the active queue's `online` auto-flush listener, if installed.
|
|
1392
|
+
* Held so a `configure()` (or a fresh `enable()`) can tear it down — otherwise
|
|
1393
|
+
* the previous queue's listener leaks and fires against a dead queue.
|
|
1394
|
+
*/
|
|
1395
|
+
let _offlineDetach = null;
|
|
1396
|
+
/**
|
|
1397
|
+
* Monotonic token identifying the CURRENT offline activation. Bumped on every
|
|
1398
|
+
* `enable()` AND every teardown/`configure()`, and captured by each `enable()`
|
|
1399
|
+
* into its disable callback. A disabler only acts while the token still matches
|
|
1400
|
+
* — so a stale disabler (held across a re-`enable()` OR a reconfigure) becomes
|
|
1401
|
+
* a no-op instead of wrongly turning the now-active outbox off.
|
|
1402
|
+
*/
|
|
1403
|
+
let _offlineEnableToken = 0;
|
|
1404
|
+
/**
|
|
1405
|
+
* Tear down any active offline wiring: detach the `online` listener, DISPOSE
|
|
1406
|
+
* the active queue (awaiting any in-flight flush so it can never write the
|
|
1407
|
+
* shared outbox again), and drop the references. Bumps the generation so stale
|
|
1408
|
+
* `enable()` disable callbacks no-op. Called before re-initializing on
|
|
1409
|
+
* `configure()`. Async so the in-flight flush is awaited before a replacement
|
|
1410
|
+
* queue is installed — preventing an orphaned flush from racing the new queue.
|
|
1411
|
+
*/
|
|
1412
|
+
async function teardownOffline() {
|
|
1413
|
+
if (_offlineDetach) {
|
|
1414
|
+
_offlineDetach();
|
|
1415
|
+
_offlineDetach = null;
|
|
1416
|
+
}
|
|
1417
|
+
const old = _offlineQueue;
|
|
1418
|
+
_offlineQueue = null;
|
|
1419
|
+
_offlineEnabled = false;
|
|
1420
|
+
_offlineEnableToken += 1;
|
|
1421
|
+
if (old) await old.dispose();
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* Bridge the localStorage persistence adapter to the {@link KvStore} contract
|
|
1425
|
+
* the offline outbox persists through. Namespaced under the configured
|
|
1426
|
+
* storage prefix so multiple projects in one origin don't collide.
|
|
1427
|
+
*/
|
|
1428
|
+
function offlineStore() {
|
|
1429
|
+
const p = _persistence;
|
|
1430
|
+
if (!p) throw new AmbaApiError("NOT_INITIALIZED", "amba SDK not configured. Call Amba.configure({ projectId, clientKey }) before using Amba.offline.");
|
|
1431
|
+
return {
|
|
1432
|
+
get: (key) => p.read(key),
|
|
1433
|
+
set: (key, value) => p.write(key, value),
|
|
1434
|
+
delete: (key) => p.delete(key)
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* Replay a single queued mutation against the live API, forwarding the op's
|
|
1439
|
+
* idempotency key on collection writes so a write that landed before the
|
|
1440
|
+
* connection dropped is never double-applied. Used as the {@link OfflineQueue}
|
|
1441
|
+
* executor.
|
|
1442
|
+
*/
|
|
1443
|
+
async function replayOutboxOp(op) {
|
|
1444
|
+
switch (op.kind) {
|
|
1445
|
+
case "collection.insert":
|
|
1446
|
+
await coreCall((c) => c.collectionsInsert(op.collection, encodeJson({
|
|
1447
|
+
...op.payload ?? {},
|
|
1448
|
+
idempotency_key: op.idempotencyKey
|
|
1449
|
+
})));
|
|
1450
|
+
return;
|
|
1451
|
+
case "collection.update":
|
|
1452
|
+
await coreCall((c) => c.collectionsUpdate(op.collection, op.id, encodeJson(op.payload ?? {})));
|
|
1453
|
+
return;
|
|
1454
|
+
case "collection.delete":
|
|
1455
|
+
await coreCall((c) => c.collectionsDelete(op.collection, op.id));
|
|
1456
|
+
return;
|
|
1457
|
+
case "track":
|
|
1458
|
+
if (op.occurredAt !== void 0) await replayTrackDirect(op);
|
|
1459
|
+
else await coreCall((c) => c.track(op.event, op.properties ? encodeJson(op.properties) : void 0, op.telemetry));
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
/**
|
|
1464
|
+
* Replay a buffered `track` op via a direct POST to `/v1/client/events`,
|
|
1465
|
+
* preserving the original `timestamp` captured at enqueue (the events route
|
|
1466
|
+
* honors body `timestamp` → `occurred_at`, but attributes the event to the
|
|
1467
|
+
* current session user — it has no body-`anonymous_id` attribution). Throws on
|
|
1468
|
+
* a transport failure or non-2xx so the queue retains the op.
|
|
1469
|
+
*
|
|
1470
|
+
* A `401`/`403` (no / expired session at replay) is surfaced as a coded
|
|
1471
|
+
* `NETWORK_ERROR` so the outbox RETAINS the event for a later attempt — an
|
|
1472
|
+
* unauthenticated replay must NOT be dropped as a deterministic rejection
|
|
1473
|
+
* (the session may refresh and the event then lands).
|
|
1474
|
+
*/
|
|
1475
|
+
async function replayTrackDirect(op) {
|
|
1476
|
+
const headers = { "content-type": "application/json" };
|
|
1477
|
+
if (_configuredClientKey) headers["X-Api-Key"] = _configuredClientKey;
|
|
1478
|
+
if (_currentSessionToken) headers["Authorization"] = `Bearer ${_currentSessionToken}`;
|
|
1479
|
+
const body = {
|
|
1480
|
+
event: op.event,
|
|
1481
|
+
properties: op.properties ?? void 0,
|
|
1482
|
+
timestamp: op.occurredAt,
|
|
1483
|
+
...op.telemetry ? { telemetry: true } : {}
|
|
1484
|
+
};
|
|
1485
|
+
const eventsUrl = `${_configuredApiUrl.replace(/\/$/, "")}/v1/client/events`;
|
|
1486
|
+
let resp;
|
|
1487
|
+
try {
|
|
1488
|
+
resp = await fetch(eventsUrl, {
|
|
1489
|
+
method: "POST",
|
|
1490
|
+
headers,
|
|
1491
|
+
body: JSON.stringify(body)
|
|
1492
|
+
});
|
|
1493
|
+
} catch (err) {
|
|
1494
|
+
throw new AmbaApiError("NETWORK_ERROR", `track replay network error: ${String(err)}`);
|
|
1495
|
+
}
|
|
1496
|
+
if (!resp.ok) {
|
|
1497
|
+
if (resp.status === 401 || resp.status === 403) throw new AmbaApiError("NETWORK_ERROR", `track replay deferred: no valid session (status ${resp.status})`);
|
|
1498
|
+
const text = await resp.text().catch(() => "");
|
|
1499
|
+
let code = `HTTP_${resp.status}`;
|
|
1500
|
+
try {
|
|
1501
|
+
const parsed = JSON.parse(text);
|
|
1502
|
+
if (typeof parsed?.error?.code === "string") code = parsed.error.code;
|
|
1503
|
+
} catch {}
|
|
1504
|
+
throw new AmbaApiError(code, `track replay failed with status ${resp.status}`, text);
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
function getOfflineQueue() {
|
|
1508
|
+
if (!_offlineQueue) _offlineQueue = new OfflineQueue({
|
|
1509
|
+
store: offlineStore(),
|
|
1510
|
+
storageKey: `offline_outbox:${_configuredProjectId ?? "default"}`,
|
|
1511
|
+
executor: replayOutboxOp,
|
|
1512
|
+
onDropped: (op, err) => {
|
|
1513
|
+
if (typeof console !== "undefined") console.warn(`[amba] dropped un-replayable offline ${op.kind} (${op.collection ?? op.event ?? "?"}):`, err);
|
|
1514
|
+
}
|
|
1515
|
+
});
|
|
1516
|
+
return _offlineQueue;
|
|
1517
|
+
}
|
|
1518
|
+
async function insertDirect(name, row) {
|
|
1519
|
+
return await coreCall((c) => c.collectionsInsert(name, encodeJson(row)));
|
|
1520
|
+
}
|
|
1521
|
+
async function updateDirect(name, id, set) {
|
|
1522
|
+
return await coreCall((c) => c.collectionsUpdate(name, id, encodeJson(set)));
|
|
1523
|
+
}
|
|
1524
|
+
async function deleteDirect(name, id) {
|
|
1525
|
+
return await coreCall((c) => c.collectionsDelete(name, id));
|
|
1526
|
+
}
|
|
1527
|
+
function requireCore() {
|
|
1528
|
+
if (!coreInstance) throw new AmbaApiError("NOT_INITIALIZED", "amba SDK not configured. Call Amba.configure({ projectId, clientKey }) at app start before any other API.");
|
|
1529
|
+
return coreInstance;
|
|
1530
|
+
}
|
|
1531
|
+
/**
|
|
1532
|
+
* Run a single SDK-runtime call and rewrap any thrown value into a
|
|
1533
|
+
* stable `AmbaApiError`. Every method on `Amba` funnels through this
|
|
1534
|
+
* so callers see one error shape regardless of which layer failed
|
|
1535
|
+
* (network, validation, etc.). Sync `requireCore()` accessors
|
|
1536
|
+
* (e.g. the `anonymousId` getter) already throw the typed shape on
|
|
1537
|
+
* their own.
|
|
1538
|
+
*/
|
|
1539
|
+
async function coreCall(fn) {
|
|
1540
|
+
try {
|
|
1541
|
+
return await fn(requireCore());
|
|
1542
|
+
} catch (err) {
|
|
1543
|
+
throw toAmbaApiError(err);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
const authSubscribers = /* @__PURE__ */ new Set();
|
|
1547
|
+
/**
|
|
1548
|
+
* Translate a public `TransactionOp` (camelCase `onConflict` /
|
|
1549
|
+
* `conflictTarget`) into the server's wire op (snake_case `on_conflict` /
|
|
1550
|
+
* `conflict_target`). Unset optionals are dropped so the server applies its
|
|
1551
|
+
* defaults.
|
|
1552
|
+
*/
|
|
1553
|
+
function toWireTransactionOp(op) {
|
|
1554
|
+
const wire = {
|
|
1555
|
+
collection: op.collection,
|
|
1556
|
+
op: op.op
|
|
1557
|
+
};
|
|
1558
|
+
if (op.id !== void 0) wire["id"] = op.id;
|
|
1559
|
+
if (op.row !== void 0) wire["row"] = op.row;
|
|
1560
|
+
if (op.set !== void 0) wire["set"] = op.set;
|
|
1561
|
+
if (op.where !== void 0) wire["where"] = op.where;
|
|
1562
|
+
if (op.expected !== void 0) wire["expected"] = op.expected;
|
|
1563
|
+
if (op.onConflict !== void 0) wire["on_conflict"] = op.onConflict;
|
|
1564
|
+
if (op.conflictTarget !== void 0) wire["conflict_target"] = op.conflictTarget;
|
|
1565
|
+
return wire;
|
|
1566
|
+
}
|
|
1567
|
+
function snapshotSession(user) {
|
|
1568
|
+
if (!user) return null;
|
|
1569
|
+
return {
|
|
1570
|
+
sessionToken: "",
|
|
1571
|
+
refreshToken: "",
|
|
1572
|
+
user,
|
|
1573
|
+
expiresAt: ""
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
function notifyAuthSubscribers(session) {
|
|
1577
|
+
for (const cb of authSubscribers) try {
|
|
1578
|
+
cb(session);
|
|
1579
|
+
} catch (err) {
|
|
1580
|
+
if (typeof console !== "undefined") console.error("[amba] onAuthStateChange callback threw", err);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
async function notifyFromAuthResult(result) {
|
|
1584
|
+
if (result?.session_token) _currentSessionToken = result.session_token;
|
|
1585
|
+
notifyAuthSubscribers(snapshotSession(result.user));
|
|
1586
|
+
}
|
|
1587
|
+
/**
|
|
1588
|
+
* Build the auth headers for a realtime subscribe connection, or `null`
|
|
1589
|
+
* when there's no live session. Read lazily at connect-time so reconnects
|
|
1590
|
+
* pick up a rotated session token.
|
|
1591
|
+
*/
|
|
1592
|
+
function realtimeAuthHeaders() {
|
|
1593
|
+
if (!_configuredClientKey || !_currentSessionToken) return null;
|
|
1594
|
+
return {
|
|
1595
|
+
"X-Api-Key": _configuredClientKey,
|
|
1596
|
+
Authorization: `Bearer ${_currentSessionToken}`
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
/** SDK-facing AI streaming routes (vendor split is a stable wire shape). */
|
|
1600
|
+
const AI_ANTHROPIC_PATH = "/v1/client/ai/anthropic/messages";
|
|
1601
|
+
const AI_OPENAI_PATH = "/v1/client/ai/openai/chat/completions";
|
|
1602
|
+
/**
|
|
1603
|
+
* Build the request (url + headers + body) for a streamed AI invocation.
|
|
1604
|
+
* Forces `stream: true` onto the request, injects the client key + live
|
|
1605
|
+
* session token, and targets the configured API base. Shared by the
|
|
1606
|
+
* Anthropic + OpenAI streaming methods so the auth/path wiring can't drift.
|
|
1607
|
+
*/
|
|
1608
|
+
function buildAiStreamRequest(path, request) {
|
|
1609
|
+
requireCore();
|
|
1610
|
+
if (!_configuredClientKey) throw new AmbaApiError("NOT_INITIALIZED", "amba SDK not configured. Call Amba.configure({ projectId, clientKey }) before streaming AI.");
|
|
1611
|
+
if (!request.prompt_slug || request.prompt_slug.trim() === "") throw new AmbaApiError("INVALID_ARGUMENT", "prompt_slug is required");
|
|
1612
|
+
const headers = {
|
|
1613
|
+
"X-Api-Key": _configuredClientKey,
|
|
1614
|
+
"content-type": "application/json",
|
|
1615
|
+
accept: "text/event-stream"
|
|
1616
|
+
};
|
|
1617
|
+
if (_currentSessionToken) headers["Authorization"] = `Bearer ${_currentSessionToken}`;
|
|
1618
|
+
const expanded = expandAiImages(request);
|
|
1619
|
+
return {
|
|
1620
|
+
url: `${_configuredApiUrl.replace(/\/$/, "")}${path}`,
|
|
1621
|
+
headers,
|
|
1622
|
+
body: JSON.stringify({
|
|
1623
|
+
...expanded,
|
|
1624
|
+
stream: true
|
|
1625
|
+
})
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
/**
|
|
1629
|
+
* Resolve the full invocation URL for a deployed function.
|
|
1630
|
+
*
|
|
1631
|
+
* With `functionsBaseUrl` configured we POST to `<base>/<name>` verbatim
|
|
1632
|
+
* (custom domains / non-prod). Otherwise we resolve the server-side project
|
|
1633
|
+
* id once (via `diagnostics.ping`, cached) and target
|
|
1634
|
+
* `https://<projectId>.fn.amba.host/<name>` — the function router accepts the
|
|
1635
|
+
* project id as the host label directly.
|
|
1636
|
+
*/
|
|
1637
|
+
async function resolveFunctionUrl(name) {
|
|
1638
|
+
if (_configuredFunctionsBaseUrl) return `${_configuredFunctionsBaseUrl.replace(/\/$/, "")}/${encodeURIComponent(name)}`;
|
|
1639
|
+
if (!_resolvedFunctionsProjectId) {
|
|
1640
|
+
const ping = await Amba.diagnostics.ping();
|
|
1641
|
+
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);
|
|
1642
|
+
_resolvedFunctionsProjectId = ping.server_project_id;
|
|
1643
|
+
}
|
|
1644
|
+
return `https://${_resolvedFunctionsProjectId}.${DEFAULT_FUNCTIONS_HOST}/${encodeURIComponent(name)}`;
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* Top-level SDK namespace.
|
|
1648
|
+
*
|
|
1649
|
+
* Stateless from the JS side — all state lives in the SDK runtime. Calling
|
|
1650
|
+
* `Amba.configure()` multiple times replaces the previous instance.
|
|
1651
|
+
*/
|
|
1652
|
+
const Amba = {
|
|
1653
|
+
configure: async (config) => {
|
|
1654
|
+
if (!config.projectId || config.projectId.trim() === "") throw new Error("Amba.configure: projectId is required");
|
|
1655
|
+
if (!config.clientKey || config.clientKey.trim() === "") throw new Error("Amba.configure: clientKey is required");
|
|
1656
|
+
if (config.wasmUrl !== void 0 && config.wasmModule !== void 0) throw new Error("Amba.configure: pass only one of wasmUrl or wasmModule");
|
|
1657
|
+
else {
|
|
1658
|
+
const { initCore } = await getCoreRuntime();
|
|
1659
|
+
const explicitWasmInput = config.wasmModule ?? config.wasmUrl;
|
|
1660
|
+
if (explicitWasmInput !== void 0) await initCore({ module_or_path: explicitWasmInput });
|
|
1661
|
+
else if (typeof process !== "undefined" && !!(process.versions && process.versions.node)) {
|
|
1662
|
+
const [{ readFile }, { fileURLToPath, pathToFileURL }, { createRequire }] = await Promise.all([
|
|
1663
|
+
import("node:fs/promises"),
|
|
1664
|
+
import("node:url"),
|
|
1665
|
+
import("node:module")
|
|
1666
|
+
]);
|
|
1667
|
+
const resolveFn = import.meta.resolve;
|
|
1668
|
+
let wasmJsUrl;
|
|
1669
|
+
if (resolveFn) wasmJsUrl = await resolveFn("@layers/amba-core/wasm");
|
|
1670
|
+
else wasmJsUrl = pathToFileURL(createRequire(import.meta.url).resolve("@layers/amba-core/wasm")).href;
|
|
1671
|
+
await initCore({ module_or_path: await readFile(fileURLToPath(wasmJsUrl)) });
|
|
1672
|
+
} else await initCore();
|
|
1673
|
+
}
|
|
1674
|
+
const persistence = new LocalStoragePersistence(config.storagePrefix ?? "amba");
|
|
1675
|
+
persistence.hydrate();
|
|
1676
|
+
let nextCore;
|
|
1677
|
+
try {
|
|
1678
|
+
const { wasmClass } = await getCoreRuntime();
|
|
1679
|
+
nextCore = wasmClass.init(encodeJson({
|
|
1680
|
+
api_key: config.clientKey,
|
|
1681
|
+
base_url: config.apiUrl,
|
|
1682
|
+
project_id: config.projectId,
|
|
1683
|
+
sdk_platform: "web",
|
|
1684
|
+
sdk_wrapper_version: `amba-web/${SDK_VERSION}`,
|
|
1685
|
+
consent_required: config.consentRequired,
|
|
1686
|
+
debug: config.debug
|
|
1687
|
+
}), globalThis.fetch.bind(globalThis));
|
|
1688
|
+
} catch (err) {
|
|
1689
|
+
throw toAmbaApiError(err);
|
|
1690
|
+
}
|
|
1691
|
+
await teardownOffline();
|
|
1692
|
+
_persistence = persistence;
|
|
1693
|
+
coreInstance = nextCore;
|
|
1694
|
+
_configuredProjectId = config.projectId;
|
|
1695
|
+
_configuredApiUrl = config.apiUrl ?? DEFAULT_API_URL;
|
|
1696
|
+
_configuredClientKey = config.clientKey;
|
|
1697
|
+
_configuredFunctionsBaseUrl = config.functionsBaseUrl ?? null;
|
|
1698
|
+
_resolvedFunctionsProjectId = null;
|
|
1699
|
+
},
|
|
1700
|
+
get projectId() {
|
|
1701
|
+
if (!_configuredProjectId) throw new AmbaApiError("NOT_INITIALIZED", "amba SDK not configured. Call Amba.configure({ projectId, clientKey }) at app start before any other API.");
|
|
1702
|
+
return _configuredProjectId;
|
|
1703
|
+
},
|
|
1704
|
+
get anonymousId() {
|
|
1705
|
+
return requireCore().anonymousId();
|
|
1706
|
+
},
|
|
1707
|
+
get appUserId() {
|
|
1708
|
+
return requireCore().appUserId() ?? void 0;
|
|
1709
|
+
},
|
|
1710
|
+
get isAuthenticated() {
|
|
1711
|
+
return requireCore().isAuthenticated();
|
|
1712
|
+
},
|
|
1713
|
+
setDebug: (enabled) => {
|
|
1714
|
+
requireCore().setDebug(enabled);
|
|
1715
|
+
},
|
|
1716
|
+
events: {
|
|
1717
|
+
track: async (event, properties, options) => {
|
|
1718
|
+
try {
|
|
1719
|
+
await coreCall((c) => c.track(event, properties ? encodeJson(properties) : void 0, options?.telemetry));
|
|
1720
|
+
} catch (err) {
|
|
1721
|
+
if (_offlineEnabled && isOfflineError(err)) {
|
|
1722
|
+
await getOfflineQueue().enqueue({
|
|
1723
|
+
kind: "track",
|
|
1724
|
+
event,
|
|
1725
|
+
properties,
|
|
1726
|
+
telemetry: options?.telemetry,
|
|
1727
|
+
occurredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1728
|
+
});
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
throw err;
|
|
1732
|
+
}
|
|
1733
|
+
},
|
|
1734
|
+
explain: async (event, properties, options) => {
|
|
1735
|
+
if (!_configuredClientKey) throw new AmbaApiError("NOT_INITIALIZED", "amba SDK not configured. Call Amba.configure({ projectId, clientKey }) before Amba.events.explain().");
|
|
1736
|
+
if (!event || event.trim() === "") throw new AmbaApiError("INVALID_ARGUMENT", "Amba.events.explain: an event name is required.");
|
|
1737
|
+
const candidate = { name: event };
|
|
1738
|
+
if (properties !== void 0) candidate["properties"] = properties;
|
|
1739
|
+
if (options && "userId" in options) candidate["user_id"] = options.userId ?? null;
|
|
1740
|
+
const headers = {
|
|
1741
|
+
"Content-Type": "application/json",
|
|
1742
|
+
"X-Api-Key": _configuredClientKey
|
|
1743
|
+
};
|
|
1744
|
+
if (_currentSessionToken) headers["Authorization"] = `Bearer ${_currentSessionToken}`;
|
|
1745
|
+
let resp;
|
|
1746
|
+
try {
|
|
1747
|
+
resp = await fetch(`${_configuredApiUrl.replace(/\/$/, "")}/v1/client/events/explain`, {
|
|
1748
|
+
method: "POST",
|
|
1749
|
+
headers,
|
|
1750
|
+
body: JSON.stringify({ event: candidate })
|
|
1751
|
+
});
|
|
1752
|
+
} catch (err) {
|
|
1753
|
+
throw new AmbaApiError("NETWORK_ERROR", `Amba.events.explain("${event}") network error: ${String(err)}`);
|
|
1754
|
+
}
|
|
1755
|
+
const text = await resp.text();
|
|
1756
|
+
let parsed = void 0;
|
|
1757
|
+
if (text.length > 0) try {
|
|
1758
|
+
parsed = JSON.parse(text);
|
|
1759
|
+
} catch {
|
|
1760
|
+
parsed = void 0;
|
|
1761
|
+
}
|
|
1762
|
+
if (!resp.ok) {
|
|
1763
|
+
const envelope = parsed && typeof parsed === "object" ? parsed : void 0;
|
|
1764
|
+
throw new AmbaApiError(typeof envelope?.error?.code === "string" ? envelope.error.code : `HTTP_${resp.status}`, typeof envelope?.error?.message === "string" ? envelope.error.message : `Amba.events.explain("${event}") failed with status ${resp.status}`, parsed);
|
|
1765
|
+
}
|
|
1766
|
+
return parsed.data;
|
|
1767
|
+
}
|
|
1768
|
+
},
|
|
1769
|
+
auth: {
|
|
1770
|
+
signInAnonymously: async () => {
|
|
1771
|
+
const r = await coreCall((c) => c.signInAnonymously());
|
|
1772
|
+
await notifyFromAuthResult(r);
|
|
1773
|
+
return r;
|
|
1774
|
+
},
|
|
1775
|
+
signInWithEmail: async (email, password) => {
|
|
1776
|
+
const r = await coreCall((c) => c.signInWithEmail(email, password));
|
|
1777
|
+
await notifyFromAuthResult(r);
|
|
1778
|
+
return r;
|
|
1779
|
+
},
|
|
1780
|
+
signUpWithEmail: async (email, password) => {
|
|
1781
|
+
const r = await coreCall((c) => c.signUpWithEmail(email, password));
|
|
1782
|
+
await notifyFromAuthResult(r);
|
|
1783
|
+
return r;
|
|
1784
|
+
},
|
|
1785
|
+
signInWithApple: async (identityToken) => {
|
|
1786
|
+
const r = await coreCall((c) => c.signInWithSocial("apple", identityToken));
|
|
1787
|
+
await notifyFromAuthResult(r);
|
|
1788
|
+
return r;
|
|
1789
|
+
},
|
|
1790
|
+
signInWithGoogle: async (idToken) => {
|
|
1791
|
+
const r = await coreCall((c) => c.signInWithSocial("google", idToken));
|
|
1792
|
+
await notifyFromAuthResult(r);
|
|
1793
|
+
return r;
|
|
1794
|
+
},
|
|
1795
|
+
requestEmailOtp: async (email) => {
|
|
1796
|
+
await coreCall((c) => c.requestEmailOtp(email));
|
|
1797
|
+
},
|
|
1798
|
+
verifyEmailOtp: async (email, code) => {
|
|
1799
|
+
const r = await coreCall((c) => c.verifyEmailOtp(email, code));
|
|
1800
|
+
await notifyFromAuthResult(r);
|
|
1801
|
+
return r;
|
|
1802
|
+
},
|
|
1803
|
+
requestSmsOtp: async (phone) => {
|
|
1804
|
+
await coreCall((c) => c.requestSmsOtp(phone));
|
|
1805
|
+
},
|
|
1806
|
+
verifySmsOtp: async (phone, code) => {
|
|
1807
|
+
const r = await coreCall((c) => c.verifySmsOtp(phone, code));
|
|
1808
|
+
await notifyFromAuthResult(r);
|
|
1809
|
+
return r;
|
|
1810
|
+
},
|
|
1811
|
+
requestMagicLink: async (email) => {
|
|
1812
|
+
await coreCall((c) => c.requestMagicLink(email));
|
|
1813
|
+
},
|
|
1814
|
+
verifyMagicLink: async (token) => {
|
|
1815
|
+
const r = await coreCall((c) => c.verifyMagicLink(token));
|
|
1816
|
+
await notifyFromAuthResult(r);
|
|
1817
|
+
return r;
|
|
1818
|
+
},
|
|
1819
|
+
linkAccount: async (provider, credential) => {
|
|
1820
|
+
const r = await coreCall((c) => c.linkAccount(provider, credential));
|
|
1821
|
+
await notifyFromAuthResult(r);
|
|
1822
|
+
return r;
|
|
1823
|
+
},
|
|
1824
|
+
linkSmsOtp: async (phone, code) => {
|
|
1825
|
+
return await coreCall((c) => c.linkSmsOtp(phone, code));
|
|
1826
|
+
},
|
|
1827
|
+
linkEmailOtp: async (email, code) => {
|
|
1828
|
+
return await coreCall((c) => c.linkEmailOtp(email, code));
|
|
1829
|
+
},
|
|
1830
|
+
linkEmailPassword: async (email, password) => {
|
|
1831
|
+
return await coreCall((c) => c.linkEmailPassword(email, password));
|
|
1832
|
+
},
|
|
1833
|
+
unlinkSmsOtp: async () => {
|
|
1834
|
+
return await coreCall((c) => c.unlinkSmsOtp());
|
|
1835
|
+
},
|
|
1836
|
+
unlinkEmailOtp: async () => {
|
|
1837
|
+
return await coreCall((c) => c.unlinkEmailOtp());
|
|
1838
|
+
},
|
|
1839
|
+
unlinkApple: async () => {
|
|
1840
|
+
return await coreCall((c) => c.unlinkApple());
|
|
1841
|
+
},
|
|
1842
|
+
unlinkGoogle: async () => {
|
|
1843
|
+
return await coreCall((c) => c.unlinkGoogle());
|
|
1844
|
+
},
|
|
1845
|
+
getCurrentIdentifiers: async () => {
|
|
1846
|
+
return await coreCall((c) => c.currentIdentifiers());
|
|
1847
|
+
},
|
|
1848
|
+
signOut: async (rotateAnonymousId = false) => {
|
|
1849
|
+
await coreCall((c) => c.signOut(rotateAnonymousId));
|
|
1850
|
+
_currentSessionToken = null;
|
|
1851
|
+
notifyAuthSubscribers(null);
|
|
1852
|
+
},
|
|
1853
|
+
refresh: async () => {
|
|
1854
|
+
const r = await coreCall((c) => c.refreshSession());
|
|
1855
|
+
await notifyFromAuthResult(r);
|
|
1856
|
+
return r;
|
|
1857
|
+
},
|
|
1858
|
+
me: async () => {
|
|
1859
|
+
return await coreCall((c) => c.me());
|
|
1860
|
+
},
|
|
1861
|
+
getSession: async () => {
|
|
1862
|
+
if (!requireCore().isAuthenticated()) return null;
|
|
1863
|
+
return snapshotSession(await coreCall((c) => c.me()));
|
|
1864
|
+
},
|
|
1865
|
+
getAnonymousId: async () => {
|
|
1866
|
+
return requireCore().anonymousId();
|
|
1867
|
+
},
|
|
1868
|
+
onAuthStateChange: (cb) => {
|
|
1869
|
+
authSubscribers.add(cb);
|
|
1870
|
+
return () => {
|
|
1871
|
+
authSubscribers.delete(cb);
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
},
|
|
1875
|
+
users: {
|
|
1876
|
+
get: async (userId) => {
|
|
1877
|
+
return await coreCall((c) => c.usersGet(userId ?? void 0));
|
|
1878
|
+
},
|
|
1879
|
+
update: ((arg1, arg2) => {
|
|
1880
|
+
const isById = typeof arg1 === "string";
|
|
1881
|
+
const userId = isById ? arg1 : void 0;
|
|
1882
|
+
const patch = isById ? arg2 ?? {} : arg1;
|
|
1883
|
+
return coreCall((c) => c.usersUpdate(userId ?? void 0, encodeJson(patch)));
|
|
1884
|
+
})
|
|
1885
|
+
},
|
|
1886
|
+
sessions: {
|
|
1887
|
+
list: async () => {
|
|
1888
|
+
return await coreCall((c) => c.sessionsList());
|
|
1889
|
+
},
|
|
1890
|
+
revoke: async (sessionId) => {
|
|
1891
|
+
await coreCall((c) => c.sessionsRevoke(sessionId));
|
|
1892
|
+
}
|
|
1893
|
+
},
|
|
1894
|
+
collection: (name) => new CollectionSubscription({
|
|
1895
|
+
baseUrl: _configuredApiUrl,
|
|
1896
|
+
authHeaders: realtimeAuthHeaders
|
|
1897
|
+
}, name),
|
|
1898
|
+
collections: {
|
|
1899
|
+
find: async (name, options = {}) => {
|
|
1900
|
+
return await coreCall((c) => c.collectionsFind(name, encodeJson(options)));
|
|
1901
|
+
},
|
|
1902
|
+
findOne: async (name, id) => {
|
|
1903
|
+
return await coreCall((c) => c.collectionsFindOne(name, id));
|
|
1904
|
+
},
|
|
1905
|
+
insert: async (name, row) => {
|
|
1906
|
+
if (_offlineEnabled) {
|
|
1907
|
+
const idempotencyKey = (typeof row["idempotency_key"] === "string" ? row["idempotency_key"] : void 0) ?? newIdempotencyKey();
|
|
1908
|
+
const wireRow = {
|
|
1909
|
+
...row,
|
|
1910
|
+
idempotency_key: idempotencyKey
|
|
1911
|
+
};
|
|
1912
|
+
try {
|
|
1913
|
+
return await coreCall((c) => c.collectionsInsert(name, encodeJson(wireRow)));
|
|
1914
|
+
} catch (err) {
|
|
1915
|
+
if (isOfflineError(err)) {
|
|
1916
|
+
await getOfflineQueue().enqueue({
|
|
1917
|
+
kind: "collection.insert",
|
|
1918
|
+
collection: name,
|
|
1919
|
+
payload: row,
|
|
1920
|
+
idempotencyKey
|
|
1921
|
+
});
|
|
1922
|
+
return {
|
|
1923
|
+
...row,
|
|
1924
|
+
_amba_pending: true
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
throw err;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
return await coreCall((c) => c.collectionsInsert(name, encodeJson(row)));
|
|
1931
|
+
},
|
|
1932
|
+
update: async (name, id, set) => {
|
|
1933
|
+
if (_offlineEnabled) try {
|
|
1934
|
+
return await coreCall((c) => c.collectionsUpdate(name, id, encodeJson(set)));
|
|
1935
|
+
} catch (err) {
|
|
1936
|
+
if (isOfflineError(err)) {
|
|
1937
|
+
await getOfflineQueue().enqueue({
|
|
1938
|
+
kind: "collection.update",
|
|
1939
|
+
collection: name,
|
|
1940
|
+
id,
|
|
1941
|
+
payload: set
|
|
1942
|
+
});
|
|
1943
|
+
return {
|
|
1944
|
+
id,
|
|
1945
|
+
...set,
|
|
1946
|
+
_amba_pending: true
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
throw err;
|
|
1950
|
+
}
|
|
1951
|
+
return await coreCall((c) => c.collectionsUpdate(name, id, encodeJson(set)));
|
|
1952
|
+
},
|
|
1953
|
+
delete: async (name, id) => {
|
|
1954
|
+
if (_offlineEnabled) try {
|
|
1955
|
+
return await coreCall((c) => c.collectionsDelete(name, id));
|
|
1956
|
+
} catch (err) {
|
|
1957
|
+
if (isOfflineError(err)) {
|
|
1958
|
+
await getOfflineQueue().enqueue({
|
|
1959
|
+
kind: "collection.delete",
|
|
1960
|
+
collection: name,
|
|
1961
|
+
id
|
|
1962
|
+
});
|
|
1963
|
+
return { data: { deleted: true } };
|
|
1964
|
+
}
|
|
1965
|
+
throw err;
|
|
1966
|
+
}
|
|
1967
|
+
return await coreCall((c) => c.collectionsDelete(name, id));
|
|
1968
|
+
},
|
|
1969
|
+
upsert: async (name, row, options) => {
|
|
1970
|
+
const wire = {
|
|
1971
|
+
on_conflict: options.onConflict,
|
|
1972
|
+
conflict_target: options.conflictTarget
|
|
1973
|
+
};
|
|
1974
|
+
return await coreCall((c) => c.collectionsUpsert(name, encodeJson(row), encodeJson(wire)));
|
|
1975
|
+
},
|
|
1976
|
+
deleteWhere: async (name, filter) => {
|
|
1977
|
+
return await coreCall((c) => c.collectionsDeleteWhere(name, encodeJson(filter)));
|
|
1978
|
+
},
|
|
1979
|
+
restore: async (name, id) => {
|
|
1980
|
+
return await coreCall((c) => c.collectionsRestore(name, id));
|
|
1981
|
+
},
|
|
1982
|
+
transaction: async (operations) => {
|
|
1983
|
+
return (await coreCall((c) => c.collectionsTransaction(encodeJson(operations.map(toWireTransactionOp))))).data;
|
|
1984
|
+
},
|
|
1985
|
+
findNearest: async (name, options) => {
|
|
1986
|
+
return await coreCall((c) => c.collectionsFindNearest(name, encodeJson(options)));
|
|
1987
|
+
},
|
|
1988
|
+
count: async (name, filter) => {
|
|
1989
|
+
return await coreCall((c) => c.collectionsCount(name, filter ? encodeJson(filter) : void 0));
|
|
1990
|
+
},
|
|
1991
|
+
optimisticInsert: async (name, row, view) => withOptimistic({
|
|
1992
|
+
apply: view.apply,
|
|
1993
|
+
mutate: () => insertDirect(name, row),
|
|
1994
|
+
rollback: view.rollback
|
|
1995
|
+
}),
|
|
1996
|
+
optimisticUpdate: async (name, id, set, view) => withOptimistic({
|
|
1997
|
+
apply: view.apply,
|
|
1998
|
+
mutate: () => updateDirect(name, id, set),
|
|
1999
|
+
rollback: view.rollback
|
|
2000
|
+
}),
|
|
2001
|
+
optimisticDelete: async (name, id, view) => withOptimistic({
|
|
2002
|
+
apply: view.apply,
|
|
2003
|
+
mutate: () => deleteDirect(name, id),
|
|
2004
|
+
rollback: view.rollback
|
|
2005
|
+
}),
|
|
2006
|
+
where: {
|
|
2007
|
+
eq: (column, value) => ({
|
|
2008
|
+
column,
|
|
2009
|
+
op: "eq",
|
|
2010
|
+
value
|
|
2011
|
+
}),
|
|
2012
|
+
ne: (column, value) => ({
|
|
2013
|
+
column,
|
|
2014
|
+
op: "ne",
|
|
2015
|
+
value
|
|
2016
|
+
}),
|
|
2017
|
+
gt: (column, value) => ({
|
|
2018
|
+
column,
|
|
2019
|
+
op: "gt",
|
|
2020
|
+
value
|
|
2021
|
+
}),
|
|
2022
|
+
gte: (column, value) => ({
|
|
2023
|
+
column,
|
|
2024
|
+
op: "gte",
|
|
2025
|
+
value
|
|
2026
|
+
}),
|
|
2027
|
+
lt: (column, value) => ({
|
|
2028
|
+
column,
|
|
2029
|
+
op: "lt",
|
|
2030
|
+
value
|
|
2031
|
+
}),
|
|
2032
|
+
lte: (column, value) => ({
|
|
2033
|
+
column,
|
|
2034
|
+
op: "lte",
|
|
2035
|
+
value
|
|
2036
|
+
}),
|
|
2037
|
+
in: (column, values) => ({
|
|
2038
|
+
column,
|
|
2039
|
+
op: "in",
|
|
2040
|
+
value: values
|
|
2041
|
+
}),
|
|
2042
|
+
notIn: (column, values) => ({
|
|
2043
|
+
column,
|
|
2044
|
+
op: "not_in",
|
|
2045
|
+
value: values
|
|
2046
|
+
}),
|
|
2047
|
+
like: (column, pattern) => ({
|
|
2048
|
+
column,
|
|
2049
|
+
op: "like",
|
|
2050
|
+
value: pattern
|
|
2051
|
+
}),
|
|
2052
|
+
ilike: (column, pattern) => ({
|
|
2053
|
+
column,
|
|
2054
|
+
op: "ilike",
|
|
2055
|
+
value: pattern
|
|
2056
|
+
}),
|
|
2057
|
+
isNull: (column) => ({
|
|
2058
|
+
column,
|
|
2059
|
+
op: "is_null"
|
|
2060
|
+
}),
|
|
2061
|
+
isNotNull: (column) => ({
|
|
2062
|
+
column,
|
|
2063
|
+
op: "is_not_null"
|
|
2064
|
+
}),
|
|
2065
|
+
and: (...filters) => ({ and: filters }),
|
|
2066
|
+
or: (...filters) => ({ or: filters }),
|
|
2067
|
+
not: (filter) => ({ not: filter })
|
|
2068
|
+
}
|
|
2069
|
+
},
|
|
2070
|
+
storage: {
|
|
2071
|
+
presign: async (params) => {
|
|
2072
|
+
return await coreCall((c) => c.storagePresign(params.bucket, params.filename, params.mimeType, params.sizeBytes, params.retentionDays ?? void 0));
|
|
2073
|
+
},
|
|
2074
|
+
upload: async (params) => {
|
|
2075
|
+
const filename = params.filename ?? ("name" in params.file ? params.file.name : "upload");
|
|
2076
|
+
const mimeType = params.file.type || "application/octet-stream";
|
|
2077
|
+
const presign = await Amba.storage.presign({
|
|
2078
|
+
bucket: params.bucket,
|
|
2079
|
+
filename,
|
|
2080
|
+
mimeType,
|
|
2081
|
+
sizeBytes: params.file.size,
|
|
2082
|
+
retentionDays: params.retentionDays
|
|
2083
|
+
});
|
|
2084
|
+
const uploadHeaders = {};
|
|
2085
|
+
for (const [k, v] of presign.upload_headers) uploadHeaders[k] = v;
|
|
2086
|
+
const putResp = await fetch(presign.upload_url, {
|
|
2087
|
+
method: "PUT",
|
|
2088
|
+
headers: uploadHeaders,
|
|
2089
|
+
body: params.file
|
|
2090
|
+
});
|
|
2091
|
+
if (!putResp.ok) throw new Error(`upload to storage failed: status ${putResp.status}`);
|
|
2092
|
+
return await coreCall((c) => c.storageCommit(presign.upload_id, presign.asset_id));
|
|
2093
|
+
},
|
|
2094
|
+
list: async (prefix) => {
|
|
2095
|
+
return await coreCall((c) => c.storageList(prefix ?? void 0));
|
|
2096
|
+
},
|
|
2097
|
+
delete: async (assetId) => {
|
|
2098
|
+
await coreCall((c) => c.storageDelete(assetId));
|
|
2099
|
+
},
|
|
2100
|
+
download: async (assetId) => {
|
|
2101
|
+
const bytes = await coreCall((c) => c.storageDownload(assetId));
|
|
2102
|
+
return new Blob([bytes]);
|
|
2103
|
+
}
|
|
2104
|
+
},
|
|
2105
|
+
media: { catalog: async (slug) => {
|
|
2106
|
+
if (!_configuredClientKey) throw new AmbaApiError("NOT_INITIALIZED", "amba SDK not configured. Call Amba.configure({ projectId, clientKey }) before reading a media catalog.");
|
|
2107
|
+
if (!slug || slug.trim() === "") throw new AmbaApiError("INVALID_ARGUMENT", "Amba.media.catalog: a catalog slug is required.");
|
|
2108
|
+
const url = `${_configuredApiUrl.replace(/\/$/, "")}/v1/client/media/catalogs/${encodeURIComponent(slug)}`;
|
|
2109
|
+
let resp;
|
|
2110
|
+
try {
|
|
2111
|
+
resp = await fetch(url, {
|
|
2112
|
+
method: "GET",
|
|
2113
|
+
headers: { "X-Api-Key": _configuredClientKey }
|
|
2114
|
+
});
|
|
2115
|
+
} catch (err) {
|
|
2116
|
+
throw new AmbaApiError("NETWORK_ERROR", `Amba.media.catalog("${slug}") network error: ${String(err)}`);
|
|
2117
|
+
}
|
|
2118
|
+
const text = await resp.text();
|
|
2119
|
+
let parsed = void 0;
|
|
2120
|
+
if (text.length > 0) try {
|
|
2121
|
+
parsed = JSON.parse(text);
|
|
2122
|
+
} catch {
|
|
2123
|
+
parsed = void 0;
|
|
2124
|
+
}
|
|
2125
|
+
if (!resp.ok) {
|
|
2126
|
+
const envelope = parsed && typeof parsed === "object" ? parsed : void 0;
|
|
2127
|
+
throw new AmbaApiError(typeof envelope?.error?.code === "string" ? envelope.error.code : `HTTP_${resp.status}`, typeof envelope?.error?.message === "string" ? envelope.error.message : `Amba.media.catalog("${slug}") failed with status ${resp.status}`, parsed);
|
|
2128
|
+
}
|
|
2129
|
+
return parsed.data;
|
|
2130
|
+
} },
|
|
2131
|
+
sync: {
|
|
2132
|
+
pushChanges: async (changes) => {
|
|
2133
|
+
return await coreCall((c) => c.syncPushChanges(encodeJson(changes)));
|
|
2134
|
+
},
|
|
2135
|
+
pullChanges: async (since) => {
|
|
2136
|
+
return await coreCall((c) => c.syncPullChanges(encodeJson(since)));
|
|
2137
|
+
}
|
|
2138
|
+
},
|
|
2139
|
+
offline: {
|
|
2140
|
+
enable: () => {
|
|
2141
|
+
if (_offlineDetach) {
|
|
2142
|
+
_offlineDetach();
|
|
2143
|
+
_offlineDetach = null;
|
|
2144
|
+
}
|
|
2145
|
+
_offlineEnabled = true;
|
|
2146
|
+
_offlineEnableToken += 1;
|
|
2147
|
+
const token = _offlineEnableToken;
|
|
2148
|
+
const q = getOfflineQueue();
|
|
2149
|
+
_offlineDetach = q.startAutoFlush();
|
|
2150
|
+
q.flush();
|
|
2151
|
+
return () => {
|
|
2152
|
+
if (_offlineEnableToken !== token) return;
|
|
2153
|
+
_offlineEnabled = false;
|
|
2154
|
+
if (_offlineDetach) {
|
|
2155
|
+
_offlineDetach();
|
|
2156
|
+
_offlineDetach = null;
|
|
2157
|
+
}
|
|
2158
|
+
};
|
|
2159
|
+
},
|
|
2160
|
+
isEnabled: () => _offlineEnabled,
|
|
2161
|
+
pendingCount: async () => getOfflineQueue().pendingCount(),
|
|
2162
|
+
pending: async () => getOfflineQueue().pending(),
|
|
2163
|
+
flush: async () => getOfflineQueue().flush(),
|
|
2164
|
+
clear: async () => getOfflineQueue().clear()
|
|
2165
|
+
},
|
|
2166
|
+
push: {
|
|
2167
|
+
register: async (token, platform, bundleId) => {
|
|
2168
|
+
return await coreCall((c) => c.pushRegister(token, platform, bundleId ?? void 0));
|
|
2169
|
+
},
|
|
2170
|
+
unregister: async (token) => {
|
|
2171
|
+
await coreCall((c) => c.pushUnregister(token));
|
|
2172
|
+
},
|
|
2173
|
+
getTokens: async () => {
|
|
2174
|
+
return await coreCall((c) => c.pushGetTokens());
|
|
2175
|
+
},
|
|
2176
|
+
subscribe: async (topic) => {
|
|
2177
|
+
await coreCall((c) => c.pushSubscribe(topic));
|
|
2178
|
+
},
|
|
2179
|
+
unsubscribe: async (topic) => {
|
|
2180
|
+
await coreCall((c) => c.pushUnsubscribe(topic));
|
|
2181
|
+
},
|
|
2182
|
+
enableAutoRotation: () => enableWebPushAutoRotation((token) => Amba.push.register(token, "web"))
|
|
2183
|
+
},
|
|
2184
|
+
notifications,
|
|
2185
|
+
entitlements: {
|
|
2186
|
+
list: async () => {
|
|
2187
|
+
return await coreCall((c) => c.entitlementsList());
|
|
2188
|
+
},
|
|
2189
|
+
has: async (name) => {
|
|
2190
|
+
return await coreCall((c) => c.entitlementsHas(name));
|
|
2191
|
+
},
|
|
2192
|
+
restore: async () => {
|
|
2193
|
+
return await coreCall((c) => c.entitlementsRestore());
|
|
2194
|
+
}
|
|
2195
|
+
},
|
|
2196
|
+
offerings: async (offeringId) => {
|
|
2197
|
+
return await coreCall((c) => offeringId ? c.offeringsGet(offeringId) : c.offeringsList());
|
|
2198
|
+
},
|
|
2199
|
+
ai: {
|
|
2200
|
+
anthropic: { messages: {
|
|
2201
|
+
create: async (request) => {
|
|
2202
|
+
return await coreCall((c) => c.aiAnthropicMessages(encodeJson(expandAiImages(request))));
|
|
2203
|
+
},
|
|
2204
|
+
stream: (request, options) => {
|
|
2205
|
+
const { url, headers, body } = buildAiStreamRequest(AI_ANTHROPIC_PATH, request);
|
|
2206
|
+
return invokeAiStream({
|
|
2207
|
+
url,
|
|
2208
|
+
headers,
|
|
2209
|
+
body,
|
|
2210
|
+
onChunk: options?.onChunk
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
} },
|
|
2214
|
+
openai: { chat: { completions: { stream: (request, options) => {
|
|
2215
|
+
const { url, headers, body } = buildAiStreamRequest(AI_OPENAI_PATH, request);
|
|
2216
|
+
return invokeAiStream({
|
|
2217
|
+
url,
|
|
2218
|
+
headers,
|
|
2219
|
+
body,
|
|
2220
|
+
onChunk: options?.onChunk
|
|
2221
|
+
});
|
|
2222
|
+
} } } }
|
|
2223
|
+
},
|
|
2224
|
+
functions: { invoke: async (name, body, options) => {
|
|
2225
|
+
requireCore();
|
|
2226
|
+
if (!_configuredClientKey) throw new AmbaApiError("NOT_INITIALIZED", "amba SDK not configured. Call Amba.configure({ projectId, clientKey }) before Amba.functions.invoke().");
|
|
2227
|
+
if (!name || name.trim() === "") throw new AmbaApiError("INVALID_ARGUMENT", "Amba.functions.invoke: a function name is required.");
|
|
2228
|
+
const url = await resolveFunctionUrl(name);
|
|
2229
|
+
const method = options?.method ?? "POST";
|
|
2230
|
+
const headers = {
|
|
2231
|
+
"X-Api-Key": _configuredClientKey,
|
|
2232
|
+
...options?.headers ?? {}
|
|
2233
|
+
};
|
|
2234
|
+
if (_currentSessionToken) headers["Authorization"] = `Bearer ${_currentSessionToken}`;
|
|
2235
|
+
let requestBody;
|
|
2236
|
+
if (body !== void 0 && method !== "GET") {
|
|
2237
|
+
headers["content-type"] = "application/json";
|
|
2238
|
+
requestBody = JSON.stringify(body);
|
|
2239
|
+
}
|
|
2240
|
+
let resp;
|
|
2241
|
+
try {
|
|
2242
|
+
resp = await fetch(url, {
|
|
2243
|
+
method,
|
|
2244
|
+
headers,
|
|
2245
|
+
body: requestBody
|
|
2246
|
+
});
|
|
2247
|
+
} catch (err) {
|
|
2248
|
+
throw new AmbaApiError("NETWORK_ERROR", `Amba.functions.invoke("${name}") network error: ${String(err)}`);
|
|
2249
|
+
}
|
|
2250
|
+
const text = await resp.text();
|
|
2251
|
+
let parsed = void 0;
|
|
2252
|
+
if (text.length > 0) try {
|
|
2253
|
+
parsed = JSON.parse(text);
|
|
2254
|
+
} catch {
|
|
2255
|
+
parsed = text;
|
|
2256
|
+
}
|
|
2257
|
+
const envelopeRequestId = parsed && typeof parsed === "object" ? parsed.error?.request_id ?? parsed.request_id : void 0;
|
|
2258
|
+
const traceId = resp.headers.get("X-Amba-Request-Id") ?? (typeof envelopeRequestId === "string" ? envelopeRequestId : null);
|
|
2259
|
+
options?.onTraceId?.(traceId);
|
|
2260
|
+
if (!resp.ok) {
|
|
2261
|
+
const envelope = parsed && typeof parsed === "object" ? parsed : void 0;
|
|
2262
|
+
const error = new AmbaApiError(typeof envelope?.error?.code === "string" ? envelope.error.code : `HTTP_${resp.status}`, typeof envelope?.error?.message === "string" ? envelope.error.message : typeof parsed === "string" && parsed.length > 0 ? parsed : `Amba.functions.invoke("${name}") failed with status ${resp.status}`, parsed);
|
|
2263
|
+
if (traceId !== null) error.traceId = traceId;
|
|
2264
|
+
throw error;
|
|
2265
|
+
}
|
|
2266
|
+
return parsed;
|
|
2267
|
+
} },
|
|
2268
|
+
config: { fetch: async () => {
|
|
2269
|
+
return await coreCall((c) => c.configFetch());
|
|
2270
|
+
} },
|
|
2271
|
+
flags: {
|
|
2272
|
+
fetch: async () => {
|
|
2273
|
+
return await coreCall((c) => c.flagsFetch());
|
|
2274
|
+
},
|
|
2275
|
+
get: async (key) => {
|
|
2276
|
+
return await coreCall((c) => c.flagsGet(key));
|
|
2277
|
+
}
|
|
2278
|
+
},
|
|
2279
|
+
diagnostics: { ping: async () => {
|
|
2280
|
+
return await coreCall((c) => c.diagnosticsPing());
|
|
2281
|
+
} },
|
|
2282
|
+
achievements: {
|
|
2283
|
+
getAll: async () => await coreCall((c) => c.achievementsGetAll()),
|
|
2284
|
+
getProgress: async () => await coreCall((c) => c.achievementsGetProgress())
|
|
2285
|
+
},
|
|
2286
|
+
challenges: {
|
|
2287
|
+
getActive: async () => await coreCall((c) => c.challengesGetActive()),
|
|
2288
|
+
get: async (id) => await coreCall((c) => c.challengesGet(id)),
|
|
2289
|
+
getProgress: async (id) => await coreCall((c) => c.challengesGetProgress(id)),
|
|
2290
|
+
claim: async (id) => await coreCall((c) => c.challengesClaim(id))
|
|
2291
|
+
},
|
|
2292
|
+
currencies: {
|
|
2293
|
+
getBalance: async () => await coreCall((c) => c.currenciesGetBalance()),
|
|
2294
|
+
getTransactions: async (currencyKey) => await coreCall((c) => c.currenciesGetTransactions(currencyKey))
|
|
2295
|
+
},
|
|
2296
|
+
inventory: {
|
|
2297
|
+
getItems: async () => await coreCall((c) => c.inventoryGetItems()),
|
|
2298
|
+
getItem: async (id) => await coreCall((c) => c.inventoryGetItem(id)),
|
|
2299
|
+
purchase: async (request) => await coreCall((c) => c.inventoryPurchase(encodeJson(request))),
|
|
2300
|
+
consume: async (request) => await coreCall((c) => c.inventoryConsume(encodeJson(request)))
|
|
2301
|
+
},
|
|
2302
|
+
leaderboards: {
|
|
2303
|
+
get: async (key) => await coreCall((c) => c.leaderboardsGet(key)),
|
|
2304
|
+
getEntries: async (key, limit) => await coreCall((c) => c.leaderboardsGetEntries(key, limit ?? void 0)),
|
|
2305
|
+
getMyRank: async (key) => await coreCall((c) => c.leaderboardsGetMyRank(key))
|
|
2306
|
+
},
|
|
2307
|
+
leagues: {
|
|
2308
|
+
me: async () => {
|
|
2309
|
+
return await coreCall((c) => c.leaguesMe());
|
|
2310
|
+
},
|
|
2311
|
+
cohort: async () => {
|
|
2312
|
+
return await coreCall((c) => c.leaguesCohort());
|
|
2313
|
+
}
|
|
2314
|
+
},
|
|
2315
|
+
stores: {
|
|
2316
|
+
list: async () => await coreCall((c) => c.storesList()),
|
|
2317
|
+
getPurchaseOptions: async (storeKey) => await coreCall((c) => c.storesGetPurchaseOptions(storeKey)),
|
|
2318
|
+
purchase: async (storeKey, purchaseOptionId, receipt) => await coreCall((c) => c.storesPurchase(storeKey, purchaseOptionId, encodeJson(receipt ?? {})))
|
|
2319
|
+
},
|
|
2320
|
+
xp: {
|
|
2321
|
+
getBalance: async () => await coreCall((c) => c.xpGetBalance()),
|
|
2322
|
+
getHistory: async (limit) => await coreCall((c) => c.xpGetHistory(limit ?? void 0)),
|
|
2323
|
+
claim: async (grantKey) => await coreCall((c) => c.xpClaim(grantKey))
|
|
2324
|
+
},
|
|
2325
|
+
streaks: {
|
|
2326
|
+
getAll: async () => await coreCall((c) => c.streaksGetAll()),
|
|
2327
|
+
qualify: async (streakKey) => await coreCall((c) => c.streaksQualify(streakKey))
|
|
2328
|
+
},
|
|
2329
|
+
get gamification() {
|
|
2330
|
+
return new GamificationSubscription({
|
|
2331
|
+
baseUrl: _configuredApiUrl,
|
|
2332
|
+
authHeaders: realtimeAuthHeaders
|
|
2333
|
+
});
|
|
2334
|
+
},
|
|
2335
|
+
feeds: { getActivity: async (feed, cursor) => await coreCall((c) => c.feedsGetActivity(feed ?? void 0, cursor ?? void 0)) },
|
|
2336
|
+
friends: {
|
|
2337
|
+
getList: async () => await coreCall((c) => c.friendsGetList()),
|
|
2338
|
+
getFriends: async () => await coreCall((c) => c.friendsGetFriends()),
|
|
2339
|
+
sendRequest: async (userId) => await coreCall((c) => c.friendsSendRequest(userId)),
|
|
2340
|
+
acceptRequest: async (friendshipId) => await coreCall((c) => c.friendsAcceptRequest(friendshipId)),
|
|
2341
|
+
declineRequest: async (friendshipId) => {
|
|
2342
|
+
await coreCall((c) => c.friendsDeclineRequest(friendshipId));
|
|
2343
|
+
},
|
|
2344
|
+
blockUser: async (userId) => await coreCall((c) => c.friendsBlockUser(userId)),
|
|
2345
|
+
unblockUser: async (userId) => {
|
|
2346
|
+
await coreCall((c) => c.friendsUnblockUser(userId));
|
|
2347
|
+
},
|
|
2348
|
+
removeBlock: async (friendshipId) => {
|
|
2349
|
+
await coreCall((c) => c.friendsRemoveBlock(friendshipId));
|
|
2350
|
+
},
|
|
2351
|
+
removeFriend: async (userId) => {
|
|
2352
|
+
await coreCall((c) => c.friendsRemoveFriend(userId));
|
|
2353
|
+
}
|
|
2354
|
+
},
|
|
2355
|
+
groups: {
|
|
2356
|
+
create: async (params) => await coreCall((c) => c.groupsCreate(encodeJson(params))),
|
|
2357
|
+
get: async (id) => await coreCall((c) => c.groupsGet(id)),
|
|
2358
|
+
update: async (id, patch) => await coreCall((c) => c.groupsUpdate(id, encodeJson(patch))),
|
|
2359
|
+
delete: async (id) => {
|
|
2360
|
+
await coreCall((c) => c.groupsDelete(id));
|
|
2361
|
+
},
|
|
2362
|
+
getMembers: async (id) => await coreCall((c) => c.groupsGetMembers(id)),
|
|
2363
|
+
join: async (id) => await coreCall((c) => c.groupsJoin(id)),
|
|
2364
|
+
leave: async (id) => {
|
|
2365
|
+
await coreCall((c) => c.groupsLeave(id));
|
|
2366
|
+
},
|
|
2367
|
+
invite: async (id, userId) => await coreCall((c) => c.groupsInvite(id, userId))
|
|
2368
|
+
},
|
|
2369
|
+
messaging: {
|
|
2370
|
+
createConversation: async (request) => await coreCall((c) => c.messagingCreateConversation(encodeJson(request))),
|
|
2371
|
+
conversations: async () => await coreCall((c) => c.messagingGetConversations()),
|
|
2372
|
+
sendMessage: async (conversationId, request) => await coreCall((c) => c.messagingSendMessage(encodeJson({
|
|
2373
|
+
...request,
|
|
2374
|
+
conversation_id: conversationId
|
|
2375
|
+
}))),
|
|
2376
|
+
listMessages: async (conversationId, opts) => await coreCall((c) => c.messagingListMessages(conversationId, opts?.limit ?? void 0, opts?.offset ?? void 0)),
|
|
2377
|
+
getMessage: async (conversationId, messageId) => {
|
|
2378
|
+
return await coreCall((c) => c.messagingGetMessage(conversationId, messageId));
|
|
2379
|
+
},
|
|
2380
|
+
addParticipant: async (conversationId, userId) => await coreCall((c) => c.messagingAddParticipant(conversationId, userId)),
|
|
2381
|
+
markRead: async (conversationId) => await coreCall((c) => c.messagingMarkRead(conversationId)),
|
|
2382
|
+
conversation: (conversationId) => new ConversationSubscription({
|
|
2383
|
+
baseUrl: _configuredApiUrl,
|
|
2384
|
+
authHeaders: realtimeAuthHeaders
|
|
2385
|
+
}, conversationId)
|
|
2386
|
+
},
|
|
2387
|
+
moderation: {
|
|
2388
|
+
reportUser: async (request) => await coreCall((c) => c.moderationReportUser(encodeJson(request))),
|
|
2389
|
+
reportContent: async (request) => await coreCall((c) => c.moderationReportContent(encodeJson(request))),
|
|
2390
|
+
getReportStatus: async (id) => await coreCall((c) => c.moderationGetReportStatus(id))
|
|
2391
|
+
},
|
|
2392
|
+
reviews: {
|
|
2393
|
+
list: async (targetType, targetId) => await coreCall((c) => c.reviewsList(targetType, targetId)),
|
|
2394
|
+
create: async (params) => await coreCall((c) => c.reviewsCreate(encodeJson(params))),
|
|
2395
|
+
update: async (id, patch) => await coreCall((c) => c.reviewsUpdate(id, encodeJson(patch))),
|
|
2396
|
+
delete: async (id) => {
|
|
2397
|
+
await coreCall((c) => c.reviewsDelete(id));
|
|
2398
|
+
}
|
|
2399
|
+
},
|
|
2400
|
+
roles: {
|
|
2401
|
+
getMyRoles: async () => await coreCall((c) => c.rolesGetMyRoles()),
|
|
2402
|
+
hasPermission: async (permission) => await coreCall((c) => c.rolesHasPermission(permission))
|
|
2403
|
+
},
|
|
2404
|
+
referrals: {
|
|
2405
|
+
getReferralCode: async () => await coreCall((c) => c.referralsGetReferralCode()),
|
|
2406
|
+
claimReferral: async (code) => await coreCall((c) => c.referralsClaimReferral(code)),
|
|
2407
|
+
create: async (code, maxUses) => await coreCall((c) => c.referralsCreate(code ?? void 0, maxUses ?? void 0))
|
|
2408
|
+
},
|
|
2409
|
+
catalog: {
|
|
2410
|
+
list: async () => await coreCall((c) => c.catalogList()),
|
|
2411
|
+
get: async (id) => await coreCall((c) => c.catalogGet(id))
|
|
2412
|
+
},
|
|
2413
|
+
content: {
|
|
2414
|
+
today: async (channel = "default") => await coreCall((c) => c.contentGetToday(channel)),
|
|
2415
|
+
library: async (channel = "default", options) => await coreCall((c) => c.contentGetLibrary(channel, options?.limit ?? void 0, options?.cursor ?? void 0)),
|
|
2416
|
+
item: async (id) => await coreCall((c) => c.contentGetItem(id)),
|
|
2417
|
+
updateItem: async (id, state) => await coreCall((c) => c.contentUpdateItem(id, encodeJson(state))),
|
|
2418
|
+
createItem: async (channel, item) => await coreCall((c) => c.contentCreateItem(channel, encodeJson(item)))
|
|
2419
|
+
},
|
|
2420
|
+
deepLinks: {
|
|
2421
|
+
get: async (shortCode) => await coreCall((c) => c.deepLinksGet(shortCode)),
|
|
2422
|
+
create: async (params) => await coreCall((c) => c.deepLinksCreate(encodeJson(params)))
|
|
2423
|
+
},
|
|
2424
|
+
onboarding: {
|
|
2425
|
+
getStatus: async () => await coreCall((c) => c.onboardingGetStatus()),
|
|
2426
|
+
nextStep: async (payload) => await coreCall((c) => c.onboardingNextStep(encodeJson(payload))),
|
|
2427
|
+
skipStep: async () => await coreCall((c) => c.onboardingSkipStep()),
|
|
2428
|
+
complete: async () => await coreCall((c) => c.onboardingComplete())
|
|
2429
|
+
}
|
|
2430
|
+
};
|
|
2431
|
+
/** Escape hatch — the raw SDK runtime handle, for namespaces not yet wrapped. */
|
|
2432
|
+
function getCoreWasm() {
|
|
2433
|
+
return requireCore();
|
|
2434
|
+
}
|
|
2435
|
+
/**
|
|
2436
|
+
* Package version stamped at build time. Sent in the
|
|
2437
|
+
* `sdk_wrapper_version` header on every customer request.
|
|
2438
|
+
*
|
|
2439
|
+
* **Must match `package.json#version` exactly** — the
|
|
2440
|
+
* `verify-sdk-version-coherence` GH action gates every publish on the
|
|
2441
|
+
* pair agreeing (and matching the `web-v<X.Y.Z>` tag when tag-triggered).
|
|
2442
|
+
*/
|
|
2443
|
+
const SDK_VERSION = "4.0.8";
|
|
2444
|
+
|
|
2445
|
+
//#endregion
|
|
2446
|
+
//#region src/browser.ts
|
|
2447
|
+
setCoreRuntime(init, AmbaCoreWasm);
|
|
2448
|
+
|
|
2449
|
+
//#endregion
|
|
2450
|
+
export { Amba, AmbaApiError, CollectionSubscription, ConversationSubscription, GamificationSubscription, SDK_VERSION, getCoreWasm, withOptimistic };
|
|
2451
|
+
//# sourceMappingURL=index.browser.js.map
|