@layers/amba-react-native 4.0.2 → 4.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1128 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +599 -18
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +599 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1125 -24
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
package/dist/index.cjs
CHANGED
|
@@ -91,12 +91,14 @@ var AmbaHttpClient = class {
|
|
|
91
91
|
sdkVersionHeader;
|
|
92
92
|
identity;
|
|
93
93
|
fetchImpl;
|
|
94
|
+
ensureSession;
|
|
94
95
|
constructor(opts) {
|
|
95
96
|
this.apiKey = opts.apiKey;
|
|
96
97
|
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
97
98
|
this.sdkPlatform = opts.sdkPlatform;
|
|
98
99
|
this.sdkVersionHeader = opts.sdkVersionHeader;
|
|
99
100
|
this.identity = opts.identity;
|
|
101
|
+
this.ensureSession = opts.ensureSession;
|
|
100
102
|
this.fetchImpl = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
101
103
|
}
|
|
102
104
|
/** Build the absolute URL for an API path, optionally with a query string. */
|
|
@@ -139,6 +141,7 @@ var AmbaHttpClient = class {
|
|
|
139
141
|
return this.request("DELETE", path);
|
|
140
142
|
}
|
|
141
143
|
async request(method, path, body, query) {
|
|
144
|
+
if (this.ensureSession && !this.identity.getSessionToken()) await this.ensureSession(path);
|
|
142
145
|
const url = this.url(path, query);
|
|
143
146
|
const init = {
|
|
144
147
|
method,
|
|
@@ -348,15 +351,828 @@ function freshSeed() {
|
|
|
348
351
|
};
|
|
349
352
|
}
|
|
350
353
|
|
|
354
|
+
//#endregion
|
|
355
|
+
//#region src/resilience.ts
|
|
356
|
+
/**
|
|
357
|
+
* Best-effort online probe. In a browser/RN runtime `navigator.onLine`
|
|
358
|
+
* exists; under Node / SSR it doesn't, so we treat "unknown" as online
|
|
359
|
+
* (the executor's own error handling then decides).
|
|
360
|
+
*/
|
|
361
|
+
function isOnline() {
|
|
362
|
+
const nav = typeof navigator !== "undefined" ? navigator : void 0;
|
|
363
|
+
if (nav && typeof nav.onLine === "boolean") return nav.onLine;
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Error codes the server raised deterministically — replaying them would fail
|
|
368
|
+
* identically, so they're NEVER queued even when the device reports offline.
|
|
369
|
+
* (A request can fail validation/auth/etc. while `navigator.onLine` is briefly
|
|
370
|
+
* false; we must not buffer-and-retry those forever.)
|
|
371
|
+
*/
|
|
372
|
+
const DETERMINISTIC_ERROR_CODES = new Set([
|
|
373
|
+
"VALIDATION_ERROR",
|
|
374
|
+
"INVALID_ARGUMENT",
|
|
375
|
+
"INVALID_INPUT",
|
|
376
|
+
"UNAUTHORIZED",
|
|
377
|
+
"FORBIDDEN",
|
|
378
|
+
"NOT_FOUND",
|
|
379
|
+
"CONFLICT",
|
|
380
|
+
"CONSENT_NOT_GRANTED",
|
|
381
|
+
"NOT_INITIALIZED"
|
|
382
|
+
]);
|
|
383
|
+
/**
|
|
384
|
+
* Is this a deterministic server rejection — a coded error the server would
|
|
385
|
+
* raise identically on every replay (bad input, auth, not-found, conflict)? A
|
|
386
|
+
* queued op that fails this way is poison: retrying it forever would wedge the
|
|
387
|
+
* queue, so the flush DROPS it. Everything else (transport failures, 5xx
|
|
388
|
+
* `HTTP_ERROR`, `RATE_LIMITED`, `CIRCUIT_OPEN`, anything uncoded) is treated as
|
|
389
|
+
* transient and RETAINED for a later attempt — favoring retain over data loss.
|
|
390
|
+
*/
|
|
391
|
+
function isDeterministicError(err) {
|
|
392
|
+
const code = err?.code;
|
|
393
|
+
return typeof code === "string" && DETERMINISTIC_ERROR_CODES.has(code);
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Heuristic: does a thrown value look like a transient offline / transport
|
|
397
|
+
* failure (vs a deterministic 4xx the server would reject again)? Used at
|
|
398
|
+
* ENQUEUE time to decide whether a failed write is worth buffering — a
|
|
399
|
+
* `VALIDATION_ERROR` will fail identically on replay, so it throws instead.
|
|
400
|
+
*
|
|
401
|
+
* A deterministic server error is excluded even when the device reports
|
|
402
|
+
* offline: the request clearly reached the server (it got a coded response),
|
|
403
|
+
* so retrying it on reconnect is pointless and would wedge the queue.
|
|
404
|
+
*
|
|
405
|
+
* NOTE: this is intentionally stricter than {@link isDeterministicError}'s
|
|
406
|
+
* inverse — at enqueue time we only buffer GENUINE connectivity failures (a
|
|
407
|
+
* non-offline 5xx/429 surfaces to the caller immediately rather than silently
|
|
408
|
+
* queueing). The flush drop-decision uses {@link isDeterministicError} so a
|
|
409
|
+
* transient 5xx/429 encountered DURING replay is retained, not dropped.
|
|
410
|
+
*/
|
|
411
|
+
function isOfflineError(err) {
|
|
412
|
+
const code = err?.code;
|
|
413
|
+
if (typeof code === "string" && DETERMINISTIC_ERROR_CODES.has(code)) return false;
|
|
414
|
+
if (code === "NETWORK_ERROR") return true;
|
|
415
|
+
const msg = err?.message;
|
|
416
|
+
if (typeof msg === "string" && /network (error|request failed)|failed to fetch|fetch failed|load failed/i.test(msg)) return true;
|
|
417
|
+
return !isOnline();
|
|
418
|
+
}
|
|
419
|
+
/** Generate a stable idempotency key for a queued op. */
|
|
420
|
+
function newIdempotencyKey() {
|
|
421
|
+
const c = typeof globalThis !== "undefined" ? globalThis.crypto : void 0;
|
|
422
|
+
if (c && typeof c.randomUUID === "function") return `amba_obx_${c.randomUUID()}`;
|
|
423
|
+
return `amba_obx_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Durable, ordered, idempotent offline outbox. Opt-in via `Amba.offline`.
|
|
427
|
+
* Single in-flight flush is enforced so concurrent reconnect signals don't
|
|
428
|
+
* double-replay.
|
|
429
|
+
*/
|
|
430
|
+
var OfflineQueue = class {
|
|
431
|
+
store;
|
|
432
|
+
executor;
|
|
433
|
+
storageKey;
|
|
434
|
+
/**
|
|
435
|
+
* Notified when a queued op is dropped because its replay failed
|
|
436
|
+
* deterministically (a poisoned op the server will always reject). Lets the
|
|
437
|
+
* SDK surface the loss (debug log) instead of silently discarding.
|
|
438
|
+
*/
|
|
439
|
+
onDropped;
|
|
440
|
+
flushing = null;
|
|
441
|
+
/**
|
|
442
|
+
* Serializes every read-modify-write of the persisted outbox. Each mutation
|
|
443
|
+
* chains onto the previous one's promise, so concurrent `enqueue`s — and an
|
|
444
|
+
* `enqueue` racing the per-op removal inside `flush` — can never read a stale
|
|
445
|
+
* snapshot and clobber each other's writes (the lost-update bug). All
|
|
446
|
+
* storage mutations funnel through `mutate()`; only `pendingCount`/`pending`
|
|
447
|
+
* read without taking the lock (a stale-by-one read is harmless).
|
|
448
|
+
*/
|
|
449
|
+
tail = Promise.resolve();
|
|
450
|
+
/** Detach function for the auto-flush `online` listener, if installed. */
|
|
451
|
+
detachOnline = null;
|
|
452
|
+
/**
|
|
453
|
+
* Set once `dispose()` is called (on teardown / reconfigure). A disposed
|
|
454
|
+
* queue refuses to mutate the persisted outbox, so an orphaned in-flight
|
|
455
|
+
* flush — or any late call against the old instance — can never race the new
|
|
456
|
+
* queue by reading/writing the same storage after it's been replaced.
|
|
457
|
+
*/
|
|
458
|
+
disposed = false;
|
|
459
|
+
constructor(opts) {
|
|
460
|
+
this.store = opts.store;
|
|
461
|
+
this.executor = opts.executor;
|
|
462
|
+
this.storageKey = opts.storageKey ?? "offline_outbox";
|
|
463
|
+
this.onDropped = opts.onDropped;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Run `fn` exclusively against the persisted outbox: it chains after every
|
|
467
|
+
* prior mutation and the next one waits for it. `fn` receives the current
|
|
468
|
+
* ops and returns the next ops to persist plus its own result.
|
|
469
|
+
*/
|
|
470
|
+
mutate(fn, opts) {
|
|
471
|
+
const run = this.tail.then(async () => {
|
|
472
|
+
if (this.disposed && opts?.rejectIfDisposed) throw new Error("amba offline queue was disposed (SDK reconfigured)");
|
|
473
|
+
const { next, result } = await fn(await this.load());
|
|
474
|
+
await this.save(next);
|
|
475
|
+
return result;
|
|
476
|
+
});
|
|
477
|
+
this.tail = run.catch(() => void 0);
|
|
478
|
+
return run;
|
|
479
|
+
}
|
|
480
|
+
async load() {
|
|
481
|
+
const raw = await this.store.get(this.storageKey);
|
|
482
|
+
if (!raw) return [];
|
|
483
|
+
try {
|
|
484
|
+
const parsed = JSON.parse(raw);
|
|
485
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
486
|
+
} catch {
|
|
487
|
+
return [];
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
async save(ops) {
|
|
491
|
+
if (this.disposed) return;
|
|
492
|
+
if (ops.length === 0) {
|
|
493
|
+
await this.store.delete(this.storageKey);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
await this.store.set(this.storageKey, JSON.stringify(ops));
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Permanently retire this queue. Detaches the auto-flush listener and marks
|
|
500
|
+
* it disposed so it can no longer mutate the persisted outbox. Returns once
|
|
501
|
+
* any in-flight flush + pending mutation chain has settled, so a caller (e.g.
|
|
502
|
+
* `configure()` teardown) can safely create a replacement queue knowing the
|
|
503
|
+
* old one will never touch storage again. Idempotent.
|
|
504
|
+
*/
|
|
505
|
+
async dispose() {
|
|
506
|
+
this.disposed = true;
|
|
507
|
+
if (this.detachOnline) {
|
|
508
|
+
this.detachOnline();
|
|
509
|
+
this.detachOnline = null;
|
|
510
|
+
}
|
|
511
|
+
try {
|
|
512
|
+
await this.flushing;
|
|
513
|
+
} catch {}
|
|
514
|
+
await this.tail.catch(() => void 0);
|
|
515
|
+
}
|
|
516
|
+
/** Whether this queue has been disposed. */
|
|
517
|
+
get isDisposed() {
|
|
518
|
+
return this.disposed;
|
|
519
|
+
}
|
|
520
|
+
/** Append an op to the durable outbox. Returns the persisted op. */
|
|
521
|
+
async enqueue(op) {
|
|
522
|
+
if (this.disposed) throw new Error("amba offline queue was disposed (SDK reconfigured)");
|
|
523
|
+
const full = {
|
|
524
|
+
...op,
|
|
525
|
+
idempotencyKey: op.idempotencyKey ?? newIdempotencyKey(),
|
|
526
|
+
enqueuedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
527
|
+
};
|
|
528
|
+
return this.mutate((ops) => ({
|
|
529
|
+
next: [...ops, full],
|
|
530
|
+
result: full
|
|
531
|
+
}), { rejectIfDisposed: true });
|
|
532
|
+
}
|
|
533
|
+
/** Number of ops awaiting replay. */
|
|
534
|
+
async pendingCount() {
|
|
535
|
+
return (await this.load()).length;
|
|
536
|
+
}
|
|
537
|
+
/** Snapshot the pending ops (a copy — mutating it does not affect the queue). */
|
|
538
|
+
async pending() {
|
|
539
|
+
return [...await this.load()];
|
|
540
|
+
}
|
|
541
|
+
/** Drop every queued op without replaying. */
|
|
542
|
+
async clear() {
|
|
543
|
+
await this.mutate(() => ({
|
|
544
|
+
next: [],
|
|
545
|
+
result: void 0
|
|
546
|
+
}));
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Replay the outbox in FIFO order. Each op is removed only after it
|
|
550
|
+
* succeeds; the first failure stops the flush and retains the remainder
|
|
551
|
+
* (so order is preserved and nothing is dropped mid-batch). Concurrent
|
|
552
|
+
* calls share one in-flight flush.
|
|
553
|
+
*/
|
|
554
|
+
async flush() {
|
|
555
|
+
if (this.disposed) return {
|
|
556
|
+
flushed: 0,
|
|
557
|
+
dropped: 0,
|
|
558
|
+
remaining: 0
|
|
559
|
+
};
|
|
560
|
+
if (this.flushing) return this.flushing;
|
|
561
|
+
this.flushing = this.runFlush().finally(() => {
|
|
562
|
+
this.flushing = null;
|
|
563
|
+
});
|
|
564
|
+
return this.flushing;
|
|
565
|
+
}
|
|
566
|
+
async runFlush() {
|
|
567
|
+
let flushed = 0;
|
|
568
|
+
let dropped = 0;
|
|
569
|
+
for (;;) {
|
|
570
|
+
if (this.disposed) break;
|
|
571
|
+
const head = (await this.load())[0];
|
|
572
|
+
if (!head) break;
|
|
573
|
+
let replayError = void 0;
|
|
574
|
+
let ok = false;
|
|
575
|
+
try {
|
|
576
|
+
await this.executor(head);
|
|
577
|
+
ok = true;
|
|
578
|
+
} catch (err) {
|
|
579
|
+
replayError = err;
|
|
580
|
+
}
|
|
581
|
+
if (!ok && !isDeterministicError(replayError)) {
|
|
582
|
+
await this.mutate((ops) => {
|
|
583
|
+
return {
|
|
584
|
+
next: ops.some((o) => o.idempotencyKey === head.idempotencyKey) ? ops : [head, ...ops],
|
|
585
|
+
result: void 0
|
|
586
|
+
};
|
|
587
|
+
});
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
if (!await this.mutate((ops) => {
|
|
591
|
+
const idx = ops.findIndex((o) => o.idempotencyKey === head.idempotencyKey);
|
|
592
|
+
if (idx === -1) return {
|
|
593
|
+
next: ops,
|
|
594
|
+
result: false
|
|
595
|
+
};
|
|
596
|
+
return {
|
|
597
|
+
next: ops.slice(0, idx).concat(ops.slice(idx + 1)),
|
|
598
|
+
result: true
|
|
599
|
+
};
|
|
600
|
+
})) break;
|
|
601
|
+
if (ok) flushed += 1;
|
|
602
|
+
else {
|
|
603
|
+
dropped += 1;
|
|
604
|
+
try {
|
|
605
|
+
this.onDropped?.(head, replayError);
|
|
606
|
+
} catch {}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
const remaining = (await this.load()).length;
|
|
610
|
+
return {
|
|
611
|
+
flushed,
|
|
612
|
+
dropped,
|
|
613
|
+
remaining
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Install an auto-flush listener on the runtime's `online` event so the
|
|
618
|
+
* queue drains the moment connectivity returns. Idempotent — a second
|
|
619
|
+
* call is a no-op. Returns a detach function.
|
|
620
|
+
*/
|
|
621
|
+
startAutoFlush() {
|
|
622
|
+
if (this.detachOnline) return this.detachOnline;
|
|
623
|
+
const target = typeof globalThis !== "undefined" && typeof globalThis.addEventListener === "function" ? globalThis : null;
|
|
624
|
+
if (!target) {
|
|
625
|
+
this.detachOnline = () => {};
|
|
626
|
+
return this.detachOnline;
|
|
627
|
+
}
|
|
628
|
+
const handler = () => {
|
|
629
|
+
this.flush();
|
|
630
|
+
};
|
|
631
|
+
target.addEventListener("online", handler);
|
|
632
|
+
this.detachOnline = () => {
|
|
633
|
+
target.removeEventListener("online", handler);
|
|
634
|
+
this.detachOnline = null;
|
|
635
|
+
};
|
|
636
|
+
return this.detachOnline;
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
/**
|
|
640
|
+
* Apply a local change immediately, fire the real mutation, and roll the
|
|
641
|
+
* local change back if the mutation fails. Framework-agnostic: `apply` and
|
|
642
|
+
* `rollback` operate on whatever local view you hand them (a React state
|
|
643
|
+
* setter, a store, an in-memory cache).
|
|
644
|
+
*
|
|
645
|
+
* Resolves with the mutation's result on success. On failure it invokes
|
|
646
|
+
* `rollback` (with the value `apply` returned, so you can restore a captured
|
|
647
|
+
* snapshot) and rethrows the original error.
|
|
648
|
+
*
|
|
649
|
+
* @example
|
|
650
|
+
* await withOptimistic({
|
|
651
|
+
* apply: () => { const prev = todos; setTodos([...todos, draft]); return prev; },
|
|
652
|
+
* mutate: () => Amba.collections.insert("todos", draft),
|
|
653
|
+
* rollback: (prev) => setTodos(prev),
|
|
654
|
+
* });
|
|
655
|
+
*/
|
|
656
|
+
async function withOptimistic(spec) {
|
|
657
|
+
const applied = spec.apply();
|
|
658
|
+
try {
|
|
659
|
+
return await spec.mutate();
|
|
660
|
+
} catch (err) {
|
|
661
|
+
try {
|
|
662
|
+
spec.rollback(applied);
|
|
663
|
+
} catch {}
|
|
664
|
+
throw err;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
//#endregion
|
|
669
|
+
//#region src/push-rotation.ts
|
|
670
|
+
/**
|
|
671
|
+
* Wire a platform token-refresh subscription to the SDK's idempotent
|
|
672
|
+
* re-register path. On every rotation the fresh token is forwarded to
|
|
673
|
+
* `reRegister(token, platform)` — the same upsert the first `push.register`
|
|
674
|
+
* call used, so re-registering the same device is a no-op server-side.
|
|
675
|
+
*
|
|
676
|
+
* `platform` defaults to the conventional pick (`"apns"` on iOS, `"fcm"`
|
|
677
|
+
* elsewhere) supplied by the caller. Returns an unsubscribe function.
|
|
678
|
+
*
|
|
679
|
+
* @example
|
|
680
|
+
* // Expo:
|
|
681
|
+
* import * as Notifications from "expo-notifications";
|
|
682
|
+
* const detach = enableNativePushAutoRotation(
|
|
683
|
+
* (h) => Notifications.addPushTokenListener((t) => h(t.data)).remove,
|
|
684
|
+
* reRegister,
|
|
685
|
+
* "apns",
|
|
686
|
+
* );
|
|
687
|
+
*/
|
|
688
|
+
function enableNativePushAutoRotation(subscribe, reRegister, platform) {
|
|
689
|
+
const unsubscribe = subscribe((token) => {
|
|
690
|
+
if (!token) return;
|
|
691
|
+
(async () => {
|
|
692
|
+
try {
|
|
693
|
+
await reRegister(token, platform);
|
|
694
|
+
} catch (err) {
|
|
695
|
+
if (typeof console !== "undefined") console.error("[amba] push auto-rotation re-register failed", err);
|
|
696
|
+
}
|
|
697
|
+
})();
|
|
698
|
+
});
|
|
699
|
+
return typeof unsubscribe === "function" ? unsubscribe : () => {};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
//#endregion
|
|
703
|
+
//#region src/local-notifications.ts
|
|
704
|
+
/**
|
|
705
|
+
* Marker key stamped into every Amba-scheduled notification's `data`. Lets
|
|
706
|
+
* `listScheduledLocal` distinguish OUR notifications from any other
|
|
707
|
+
* `expo-notifications` schedules the host app created — so foreign
|
|
708
|
+
* schedules never leak into the Amba list. Mirrors the web SDK's
|
|
709
|
+
* `__ambaLocalId` marker.
|
|
710
|
+
*/
|
|
711
|
+
const AMBA_LOCAL_ID_KEY = "__ambaLocalId";
|
|
712
|
+
/**
|
|
713
|
+
* Generate a stable local-notification id when the caller omits one.
|
|
714
|
+
*/
|
|
715
|
+
function genLocalId() {
|
|
716
|
+
const c = globalThis.crypto;
|
|
717
|
+
if (c?.randomUUID) return c.randomUUID();
|
|
718
|
+
return `amba_local_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Resolve the id a schedule will be filed under. A caller may omit `id`
|
|
722
|
+
* (we mint one) but must not pass a BLANK/whitespace id: `cancelLocal`
|
|
723
|
+
* rejects blank ids, so returning one in the handle would make the
|
|
724
|
+
* notification uncancellable. Reject blank at schedule time so every
|
|
725
|
+
* returned handle id round-trips through `cancelLocal`. Mirrors the web SDK.
|
|
726
|
+
*/
|
|
727
|
+
function normalizeScheduleId(rawId) {
|
|
728
|
+
if (rawId === void 0) return genLocalId();
|
|
729
|
+
if (typeof rawId !== "string" || rawId.trim() === "") throw new AmbaApiError("INVALID_ARGUMENT", "Amba.notifications.scheduleLocal: id, when provided, must be a non-empty string");
|
|
730
|
+
return rawId;
|
|
731
|
+
}
|
|
732
|
+
async function loadExpoNotifications() {
|
|
733
|
+
try {
|
|
734
|
+
const mod = await import("expo-notifications");
|
|
735
|
+
return mod.default ?? mod;
|
|
736
|
+
} catch (err) {
|
|
737
|
+
throw new AmbaApiError("LOCAL_NOTIFICATIONS_UNAVAILABLE", "Amba.notifications requires the `expo-notifications` peer dependency. Install it with `npx expo install expo-notifications` (works in both Expo and bare React Native apps).", err);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Translate Amba's platform-neutral trigger into the shape
|
|
742
|
+
* `expo-notifications` expects. We build object triggers keyed by the
|
|
743
|
+
* library's `SchedulableTriggerInputTypes` when present (SDK 0.27+),
|
|
744
|
+
* falling back to the legacy `{ date }` / `{ seconds }` shorthand the
|
|
745
|
+
* older API accepts — both forms remain supported by current releases.
|
|
746
|
+
*/
|
|
747
|
+
function buildExpoTrigger(input, Notifications) {
|
|
748
|
+
const types = Notifications.SchedulableTriggerInputTypes;
|
|
749
|
+
const trigger = input.trigger;
|
|
750
|
+
if (!trigger || typeof trigger !== "object") throw new AmbaApiError("INVALID_ARGUMENT", "Amba.notifications.scheduleLocal: trigger must be { at: Date } or { inSeconds: number, repeats?: boolean }");
|
|
751
|
+
if ("at" in trigger) {
|
|
752
|
+
const at = trigger.at instanceof Date ? trigger.at : new Date(trigger.at);
|
|
753
|
+
if (Number.isNaN(at.getTime())) throw new AmbaApiError("INVALID_ARGUMENT", "Amba.notifications.scheduleLocal: trigger.at must be a valid Date");
|
|
754
|
+
return types ? {
|
|
755
|
+
type: types.DATE,
|
|
756
|
+
date: at
|
|
757
|
+
} : { date: at };
|
|
758
|
+
}
|
|
759
|
+
if ("inSeconds" in trigger) {
|
|
760
|
+
const seconds = trigger.inSeconds;
|
|
761
|
+
if (!Number.isFinite(seconds) || seconds < 0) throw new AmbaApiError("INVALID_ARGUMENT", "Amba.notifications.scheduleLocal: trigger.inSeconds must be a non-negative number");
|
|
762
|
+
const repeats = "repeats" in trigger ? Boolean(trigger.repeats) : false;
|
|
763
|
+
return types ? {
|
|
764
|
+
type: types.TIME_INTERVAL,
|
|
765
|
+
seconds,
|
|
766
|
+
repeats
|
|
767
|
+
} : {
|
|
768
|
+
seconds,
|
|
769
|
+
repeats
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
throw new AmbaApiError("INVALID_ARGUMENT", "Amba.notifications.scheduleLocal: trigger must be { at: Date } or { inSeconds: number, repeats?: boolean }");
|
|
773
|
+
}
|
|
774
|
+
function validateContent(input) {
|
|
775
|
+
if (!input || typeof input.title !== "string" || input.title.trim() === "") throw new AmbaApiError("INVALID_ARGUMENT", "Amba.notifications.scheduleLocal: title is required");
|
|
776
|
+
if (typeof input.body !== "string") throw new AmbaApiError("INVALID_ARGUMENT", "Amba.notifications.scheduleLocal: body must be a string");
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* The `notifications` namespace. Shared by `@layers/amba-react-native`
|
|
780
|
+
* and (via re-export) `@layers/amba-expo`.
|
|
781
|
+
*/
|
|
782
|
+
const notifications = {
|
|
783
|
+
async scheduleLocal(input) {
|
|
784
|
+
validateContent(input);
|
|
785
|
+
const id = normalizeScheduleId(input.id);
|
|
786
|
+
const Notifications = await loadExpoNotifications();
|
|
787
|
+
const trigger = buildExpoTrigger(input, Notifications);
|
|
788
|
+
const existing = await Notifications.getPermissionsAsync();
|
|
789
|
+
let granted = existing.granted ?? existing.status === "granted";
|
|
790
|
+
if (!granted) {
|
|
791
|
+
const requested = await Notifications.requestPermissionsAsync();
|
|
792
|
+
granted = requested.granted ?? requested.status === "granted";
|
|
793
|
+
}
|
|
794
|
+
if (!granted) throw new AmbaApiError("LOCAL_NOTIFICATIONS_PERMISSION_DENIED", "user denied notification permission — local notifications cannot be scheduled");
|
|
795
|
+
return { id: await Notifications.scheduleNotificationAsync({
|
|
796
|
+
identifier: id,
|
|
797
|
+
content: {
|
|
798
|
+
title: input.title,
|
|
799
|
+
body: input.body,
|
|
800
|
+
data: {
|
|
801
|
+
...input.data,
|
|
802
|
+
[AMBA_LOCAL_ID_KEY]: id
|
|
803
|
+
}
|
|
804
|
+
},
|
|
805
|
+
trigger
|
|
806
|
+
}) || id };
|
|
807
|
+
},
|
|
808
|
+
async cancelLocal(id) {
|
|
809
|
+
if (typeof id !== "string" || id.trim() === "") throw new AmbaApiError("INVALID_ARGUMENT", "Amba.notifications.cancelLocal: id is required");
|
|
810
|
+
await (await loadExpoNotifications()).cancelScheduledNotificationAsync(id);
|
|
811
|
+
},
|
|
812
|
+
async listScheduledLocal() {
|
|
813
|
+
const scheduled = await (await loadExpoNotifications()).getAllScheduledNotificationsAsync();
|
|
814
|
+
const out = [];
|
|
815
|
+
for (const n of scheduled) {
|
|
816
|
+
const data = n.content?.data ?? {};
|
|
817
|
+
const markerId = data[AMBA_LOCAL_ID_KEY];
|
|
818
|
+
if (!markerId) continue;
|
|
819
|
+
out.push({
|
|
820
|
+
id: markerId,
|
|
821
|
+
title: n.content?.title ?? "",
|
|
822
|
+
body: n.content?.body ?? "",
|
|
823
|
+
data
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
return out;
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
//#endregion
|
|
831
|
+
//#region src/realtime.ts
|
|
832
|
+
const INITIAL_BACKOFF_MS = 500;
|
|
833
|
+
const MAX_BACKOFF_MS = 15e3;
|
|
834
|
+
/**
|
|
835
|
+
* Builder returned by `Amba.collection(name)`. Chain `.where(...)` to
|
|
836
|
+
* narrow, then `.subscribe(cb)` to start streaming.
|
|
837
|
+
*/
|
|
838
|
+
var CollectionSubscription = class {
|
|
839
|
+
filters = {};
|
|
840
|
+
constructor(ctx, name) {
|
|
841
|
+
this.ctx = ctx;
|
|
842
|
+
this.name = name;
|
|
843
|
+
}
|
|
844
|
+
/** Add an equality filter. Chainable. All filters must match (AND). */
|
|
845
|
+
where(column, value) {
|
|
846
|
+
this.filters[column] = value;
|
|
847
|
+
return this;
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Start listening. `cb` fires once per matching row change. Returns an
|
|
851
|
+
* unsubscribe handle — call it to close the connection.
|
|
852
|
+
*/
|
|
853
|
+
subscribe(cb, options = {}) {
|
|
854
|
+
if (!this.ctx.authHeaders()) throw new Error("Amba.collection().subscribe() requires an authenticated session — call Amba.auth.signIn* first.");
|
|
855
|
+
const qs = Object.keys(this.filters).length > 0 ? `?where=${encodeURIComponent(JSON.stringify(this.filters))}` : "";
|
|
856
|
+
return openStream({
|
|
857
|
+
url: `${this.ctx.baseUrl}/v1/client/realtime/collections/${encodeURIComponent(this.name)}${qs}`,
|
|
858
|
+
authHeaders: this.ctx.authHeaders,
|
|
859
|
+
onChange: cb,
|
|
860
|
+
options,
|
|
861
|
+
parse: parseRowChangeFrame
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
/**
|
|
866
|
+
* Builder returned by `Amba.messaging.conversation(id)`. Call `.subscribe(cb)`
|
|
867
|
+
* to receive new messages live instead of polling.
|
|
868
|
+
*
|
|
869
|
+
* Delivery is scoped to conversation participants — you receive messages for
|
|
870
|
+
* a conversation only if your session is a member of it. Reconnects with the
|
|
871
|
+
* same exponential backoff as collection subscriptions.
|
|
872
|
+
*/
|
|
873
|
+
var ConversationSubscription = class {
|
|
874
|
+
constructor(ctx, conversationId) {
|
|
875
|
+
this.ctx = ctx;
|
|
876
|
+
this.conversationId = conversationId;
|
|
877
|
+
}
|
|
878
|
+
subscribe(cb, options = {}) {
|
|
879
|
+
if (!this.ctx.authHeaders()) throw new Error("Amba.messaging.conversation().subscribe() requires an authenticated session — call Amba.auth.signIn* first.");
|
|
880
|
+
return openStream({
|
|
881
|
+
url: `${this.ctx.baseUrl}/v1/client/realtime/conversations/${encodeURIComponent(this.conversationId)}`,
|
|
882
|
+
authHeaders: this.ctx.authHeaders,
|
|
883
|
+
onChange: cb,
|
|
884
|
+
options,
|
|
885
|
+
parse: parseRowChangeFrame
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
/**
|
|
890
|
+
* Builder returned by `Amba.gamification`. Call `.subscribe(cb)` to receive the
|
|
891
|
+
* signed-in user's xp / level / achievement / streak changes live instead of
|
|
892
|
+
* polling.
|
|
893
|
+
*
|
|
894
|
+
* Delivery is scoped to the signed-in user — you only ever receive your own
|
|
895
|
+
* gamification changes, never another user's. Reconnects with the same
|
|
896
|
+
* exponential backoff as collection subscriptions.
|
|
897
|
+
*/
|
|
898
|
+
var GamificationSubscription = class {
|
|
899
|
+
constructor(ctx) {
|
|
900
|
+
this.ctx = ctx;
|
|
901
|
+
}
|
|
902
|
+
subscribe(cb, options = {}) {
|
|
903
|
+
if (!this.ctx.authHeaders()) throw new Error("Amba.gamification.subscribe() requires an authenticated session — call Amba.auth.signIn* first.");
|
|
904
|
+
return openStream({
|
|
905
|
+
url: `${this.ctx.baseUrl}/v1/client/realtime/gamification`,
|
|
906
|
+
authHeaders: this.ctx.authHeaders,
|
|
907
|
+
onChange: cb,
|
|
908
|
+
options,
|
|
909
|
+
parse: parseGamificationFrame
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
};
|
|
913
|
+
/**
|
|
914
|
+
* Open a long-lived event stream and dispatch parsed events to `onChange`.
|
|
915
|
+
* Reconnects with exponential backoff until the returned unsubscribe handle
|
|
916
|
+
* is invoked. Chooses the XHR incremental-text transport (RN-native) when
|
|
917
|
+
* available, else the streamed-`fetch`-body transport (RN-for-web / Node).
|
|
918
|
+
*/
|
|
919
|
+
function openStream(args) {
|
|
920
|
+
let stopped = false;
|
|
921
|
+
let backoff = INITIAL_BACKOFF_MS;
|
|
922
|
+
let closeActive = null;
|
|
923
|
+
let settleActive = null;
|
|
924
|
+
const run = async () => {
|
|
925
|
+
while (!stopped) {
|
|
926
|
+
const headers = args.authHeaders();
|
|
927
|
+
if (!headers) {
|
|
928
|
+
args.options.onError?.(/* @__PURE__ */ new Error("no active session"));
|
|
929
|
+
break;
|
|
930
|
+
}
|
|
931
|
+
try {
|
|
932
|
+
await new Promise((resolve, reject) => {
|
|
933
|
+
settleActive = resolve;
|
|
934
|
+
const onFirstFrame = () => {
|
|
935
|
+
backoff = INITIAL_BACKOFF_MS;
|
|
936
|
+
args.options.onOpen?.();
|
|
937
|
+
};
|
|
938
|
+
closeActive = connectOnce({
|
|
939
|
+
url: args.url,
|
|
940
|
+
headers,
|
|
941
|
+
onFrame: (frame) => {
|
|
942
|
+
const ev = args.parse(frame);
|
|
943
|
+
if (ev !== null) args.onChange(ev);
|
|
944
|
+
},
|
|
945
|
+
onFirstFrame,
|
|
946
|
+
onDone: resolve,
|
|
947
|
+
onError: reject
|
|
948
|
+
});
|
|
949
|
+
});
|
|
950
|
+
} catch (err) {
|
|
951
|
+
if (stopped) break;
|
|
952
|
+
args.options.onError?.(err);
|
|
953
|
+
} finally {
|
|
954
|
+
closeActive?.();
|
|
955
|
+
closeActive = null;
|
|
956
|
+
settleActive = null;
|
|
957
|
+
}
|
|
958
|
+
if (stopped) break;
|
|
959
|
+
await delay(backoff);
|
|
960
|
+
backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
run();
|
|
964
|
+
return () => {
|
|
965
|
+
stopped = true;
|
|
966
|
+
closeActive?.();
|
|
967
|
+
settleActive?.();
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Establish a single connection using the best available transport. Returns
|
|
972
|
+
* an abort function. The XHR transport is preferred on React Native; the
|
|
973
|
+
* fetch-stream transport covers RN-for-web and any runtime exposing a
|
|
974
|
+
* readable `response.body`.
|
|
975
|
+
*/
|
|
976
|
+
function connectOnce(args) {
|
|
977
|
+
if (typeof XMLHttpRequest !== "undefined") return connectXhr(args, XMLHttpRequest);
|
|
978
|
+
return connectFetchStream(args);
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Incremental-`responseText` transport via XMLHttpRequest — the RN-native
|
|
982
|
+
* path. `onreadystatechange` fires at readyState 3 (LOADING) as bytes
|
|
983
|
+
* stream in; we track how much of `responseText` we've already parsed and
|
|
984
|
+
* extract any newly-completed `\n\n`-delimited frames.
|
|
985
|
+
*/
|
|
986
|
+
function connectXhr(args, XHR) {
|
|
987
|
+
const xhr = new XHR();
|
|
988
|
+
let consumed = 0;
|
|
989
|
+
let buffer = "";
|
|
990
|
+
let sawFrame = false;
|
|
991
|
+
let aborted = false;
|
|
992
|
+
const drain = () => {
|
|
993
|
+
const text = xhr.responseText;
|
|
994
|
+
if (text.length <= consumed) return;
|
|
995
|
+
buffer += text.slice(consumed);
|
|
996
|
+
consumed = text.length;
|
|
997
|
+
let idx;
|
|
998
|
+
while ((idx = buffer.indexOf("\n\n")) !== -1) {
|
|
999
|
+
const frame = buffer.slice(0, idx);
|
|
1000
|
+
buffer = buffer.slice(idx + 2);
|
|
1001
|
+
if (!sawFrame) {
|
|
1002
|
+
sawFrame = true;
|
|
1003
|
+
args.onFirstFrame();
|
|
1004
|
+
}
|
|
1005
|
+
args.onFrame(frame);
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
xhr.open("GET", args.url, true);
|
|
1009
|
+
for (const [k, v] of Object.entries(args.headers)) xhr.setRequestHeader(k, v);
|
|
1010
|
+
xhr.setRequestHeader("Accept", "text/event-stream");
|
|
1011
|
+
xhr.onreadystatechange = () => {
|
|
1012
|
+
if (aborted) return;
|
|
1013
|
+
if (xhr.readyState === 3) {
|
|
1014
|
+
if (xhr.status >= 400) return;
|
|
1015
|
+
drain();
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
xhr.onload = () => {
|
|
1019
|
+
if (aborted) return;
|
|
1020
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
1021
|
+
drain();
|
|
1022
|
+
args.onDone();
|
|
1023
|
+
} else args.onError(/* @__PURE__ */ new Error(`realtime connect failed: HTTP ${xhr.status}`));
|
|
1024
|
+
};
|
|
1025
|
+
xhr.onerror = () => {
|
|
1026
|
+
if (aborted) return;
|
|
1027
|
+
args.onError(/* @__PURE__ */ new Error("realtime transport error"));
|
|
1028
|
+
};
|
|
1029
|
+
xhr.ontimeout = () => {
|
|
1030
|
+
if (aborted) return;
|
|
1031
|
+
args.onError(/* @__PURE__ */ new Error("realtime connection timed out"));
|
|
1032
|
+
};
|
|
1033
|
+
try {
|
|
1034
|
+
xhr.send();
|
|
1035
|
+
} catch (err) {
|
|
1036
|
+
args.onError(err);
|
|
1037
|
+
}
|
|
1038
|
+
return () => {
|
|
1039
|
+
aborted = true;
|
|
1040
|
+
try {
|
|
1041
|
+
xhr.abort();
|
|
1042
|
+
} catch {}
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Streamed-`fetch`-body transport — used when XHR isn't present (RN-for-web,
|
|
1047
|
+
* Node). Identical wire handling to web/node's `consume`.
|
|
1048
|
+
*/
|
|
1049
|
+
function connectFetchStream(args) {
|
|
1050
|
+
const controller = new AbortController();
|
|
1051
|
+
let aborted = false;
|
|
1052
|
+
(async () => {
|
|
1053
|
+
try {
|
|
1054
|
+
const resp = await fetch(args.url, {
|
|
1055
|
+
method: "GET",
|
|
1056
|
+
headers: {
|
|
1057
|
+
...args.headers,
|
|
1058
|
+
Accept: "text/event-stream"
|
|
1059
|
+
},
|
|
1060
|
+
signal: controller.signal
|
|
1061
|
+
});
|
|
1062
|
+
if (!resp.ok || !resp.body) throw new Error(`realtime connect failed: HTTP ${resp.status}`);
|
|
1063
|
+
const reader = resp.body.getReader();
|
|
1064
|
+
const decoder = new TextDecoder();
|
|
1065
|
+
let buffer = "";
|
|
1066
|
+
let sawFrame = false;
|
|
1067
|
+
for (;;) {
|
|
1068
|
+
const { done, value } = await reader.read();
|
|
1069
|
+
if (done) break;
|
|
1070
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1071
|
+
let idx;
|
|
1072
|
+
while ((idx = buffer.indexOf("\n\n")) !== -1) {
|
|
1073
|
+
const frame = buffer.slice(0, idx);
|
|
1074
|
+
buffer = buffer.slice(idx + 2);
|
|
1075
|
+
if (!sawFrame) {
|
|
1076
|
+
sawFrame = true;
|
|
1077
|
+
args.onFirstFrame();
|
|
1078
|
+
}
|
|
1079
|
+
args.onFrame(frame);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
if (!aborted) args.onDone();
|
|
1083
|
+
} catch (err) {
|
|
1084
|
+
if (!aborted) args.onError(err);
|
|
1085
|
+
}
|
|
1086
|
+
})();
|
|
1087
|
+
return () => {
|
|
1088
|
+
aborted = true;
|
|
1089
|
+
controller.abort();
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
/** Pull the `event:` name + joined `data:` lines out of one raw frame. */
|
|
1093
|
+
function readFrame(frame) {
|
|
1094
|
+
let event = "";
|
|
1095
|
+
const dataLines = [];
|
|
1096
|
+
for (const line of frame.split("\n")) {
|
|
1097
|
+
if (line.startsWith(":")) continue;
|
|
1098
|
+
if (line.startsWith("event:")) event = line.slice(6).trim();
|
|
1099
|
+
else if (line.startsWith("data:")) dataLines.push(line.slice(5).trim());
|
|
1100
|
+
}
|
|
1101
|
+
return {
|
|
1102
|
+
event,
|
|
1103
|
+
data: dataLines.join("\n")
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
function parseRowChangeFrame(frame) {
|
|
1107
|
+
const { event, data } = readFrame(frame);
|
|
1108
|
+
if (event !== "change") return null;
|
|
1109
|
+
try {
|
|
1110
|
+
const payload = JSON.parse(data);
|
|
1111
|
+
return {
|
|
1112
|
+
op: payload.op,
|
|
1113
|
+
row: payload.row,
|
|
1114
|
+
truncated: payload.truncated
|
|
1115
|
+
};
|
|
1116
|
+
} catch {
|
|
1117
|
+
return null;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
function parseGamificationFrame(frame) {
|
|
1121
|
+
const { event, data } = readFrame(frame);
|
|
1122
|
+
if (event !== "change") return null;
|
|
1123
|
+
try {
|
|
1124
|
+
const payload = JSON.parse(data);
|
|
1125
|
+
return {
|
|
1126
|
+
kind: payload.kind,
|
|
1127
|
+
data: payload.data,
|
|
1128
|
+
truncated: payload.truncated
|
|
1129
|
+
};
|
|
1130
|
+
} catch {
|
|
1131
|
+
return null;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
function delay(ms) {
|
|
1135
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1136
|
+
}
|
|
1137
|
+
|
|
351
1138
|
//#endregion
|
|
352
1139
|
//#region src/index.ts
|
|
353
1140
|
/** SDK semver — burned in at build time. Bump alongside package.json. */
|
|
354
|
-
const SDK_VERSION = "4.0.
|
|
1141
|
+
const SDK_VERSION = "4.0.4";
|
|
1142
|
+
/**
|
|
1143
|
+
* Default host that serves deployed functions. `Amba.functions.invoke(name)`
|
|
1144
|
+
* targets `https://<projectId>.<DEFAULT_FUNCTIONS_HOST>/<name>` unless a
|
|
1145
|
+
* `functionsBaseUrl` override is configured.
|
|
1146
|
+
*/
|
|
1147
|
+
const DEFAULT_FUNCTIONS_HOST = "fn.amba.host";
|
|
355
1148
|
let state = null;
|
|
1149
|
+
/**
|
|
1150
|
+
* Monotonic token identifying the current offline activation. Bumped on every
|
|
1151
|
+
* `offline.enable()` AND every `configure()`, captured by each `enable()` into
|
|
1152
|
+
* its disable callback. A disabler only acts while the token still matches — so
|
|
1153
|
+
* a stale disabler (held across a re-`enable()` or a reconfigure) no-ops
|
|
1154
|
+
* instead of disabling the now-active outbox.
|
|
1155
|
+
*/
|
|
1156
|
+
let _offlineEnableToken = 0;
|
|
356
1157
|
function getState() {
|
|
357
1158
|
if (!state) throw new AmbaApiError("NOT_INITIALIZED", "@layers/amba-react-native: SDK not configured. Call Amba.configure({ projectId, clientKey }) at app start.");
|
|
358
1159
|
return state;
|
|
359
1160
|
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Build the auth headers for a realtime subscribe connection, or `null`
|
|
1163
|
+
* when the SDK isn't configured or there's no live session. Read lazily at
|
|
1164
|
+
* connect time so reconnects pick up a rotated session token (the identity
|
|
1165
|
+
* store holds the canonical, auto-refreshed copy).
|
|
1166
|
+
*/
|
|
1167
|
+
function realtimeAuthHeaders() {
|
|
1168
|
+
if (!state) return null;
|
|
1169
|
+
const token = state.identity.getSessionToken();
|
|
1170
|
+
if (!token) return null;
|
|
1171
|
+
return {
|
|
1172
|
+
"X-Api-Key": state.clientKey,
|
|
1173
|
+
Authorization: `Bearer ${token}`
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
360
1176
|
const authSubscribers = /* @__PURE__ */ new Set();
|
|
361
1177
|
function snapshotSession(user) {
|
|
362
1178
|
if (!user) return null;
|
|
@@ -395,6 +1211,26 @@ async function postAuth(state_, path, body) {
|
|
|
395
1211
|
await notifyFromAuthResult(result);
|
|
396
1212
|
return result;
|
|
397
1213
|
}
|
|
1214
|
+
/** Paths that must NOT trigger lazy anonymous sign-in. */
|
|
1215
|
+
function pathSkipsLazySession(path) {
|
|
1216
|
+
if (path.startsWith("/v1/client/auth/")) return true;
|
|
1217
|
+
if (path === P.events || path.startsWith(`${P.events}/`)) return true;
|
|
1218
|
+
if (path === P.diagnosticsPing) return true;
|
|
1219
|
+
if (path === P.config || path.startsWith(`${P.config}/`)) return true;
|
|
1220
|
+
if (path === P.flags || path.startsWith(`${P.flags}/`)) return true;
|
|
1221
|
+
return false;
|
|
1222
|
+
}
|
|
1223
|
+
async function ensureAnonymousSession(state_) {
|
|
1224
|
+
if (state_.identity.getSessionToken()) return;
|
|
1225
|
+
if (!state_.anonSignInInflight) state_.anonSignInInflight = (async () => {
|
|
1226
|
+
try {
|
|
1227
|
+
await postAuth(state_, P.authAnonymous, { anonymous_id: state_.identity.getAnonymousId() });
|
|
1228
|
+
} finally {
|
|
1229
|
+
state_.anonSignInInflight = null;
|
|
1230
|
+
}
|
|
1231
|
+
})();
|
|
1232
|
+
await state_.anonSignInInflight;
|
|
1233
|
+
}
|
|
398
1234
|
/**
|
|
399
1235
|
* POST helper for endpoints that return `{ data: { user } }` WITHOUT
|
|
400
1236
|
* minting new tokens. Used by `linkSmsOtp` / `linkEmailOtp` / `unlink*`
|
|
@@ -491,6 +1327,90 @@ function encodePathSegment(s) {
|
|
|
491
1327
|
return encodeURIComponent(s);
|
|
492
1328
|
}
|
|
493
1329
|
/**
|
|
1330
|
+
* Resolve the full invocation URL for a deployed function. With
|
|
1331
|
+
* `functionsBaseUrl` configured we POST to `<base>/<name>` verbatim;
|
|
1332
|
+
* otherwise we resolve the server-side project id once (via
|
|
1333
|
+
* `diagnostics.ping`, cached) and target
|
|
1334
|
+
* `https://<projectId>.fn.amba.host/<name>`.
|
|
1335
|
+
*/
|
|
1336
|
+
async function resolveFunctionUrl(s, name) {
|
|
1337
|
+
if (s.functionsBaseUrl) return `${s.functionsBaseUrl.replace(/\/$/, "")}/${encodeURIComponent(name)}`;
|
|
1338
|
+
if (!s.resolvedFunctionsProjectId) {
|
|
1339
|
+
const ping = await Amba.diagnostics.ping();
|
|
1340
|
+
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);
|
|
1341
|
+
s.resolvedFunctionsProjectId = ping.server_project_id;
|
|
1342
|
+
}
|
|
1343
|
+
return `https://${s.resolvedFunctionsProjectId}.${DEFAULT_FUNCTIONS_HOST}/${encodeURIComponent(name)}`;
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* Bridge the AsyncStorage-backed persistence to the {@link KvStore} the
|
|
1347
|
+
* outbox persists through. Namespaced under the configured storage prefix so
|
|
1348
|
+
* multiple projects in one app don't collide.
|
|
1349
|
+
*/
|
|
1350
|
+
function offlineStore(s) {
|
|
1351
|
+
return {
|
|
1352
|
+
get: (key) => s.persistence.read(key),
|
|
1353
|
+
set: (key, value) => s.persistence.write(key, value),
|
|
1354
|
+
delete: (key) => s.persistence.delete(key)
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Replay one queued mutation against the live API, forwarding the op's
|
|
1359
|
+
* idempotency key on collection inserts so a write that landed before the
|
|
1360
|
+
* connection dropped is never double-applied (API migration 056).
|
|
1361
|
+
*/
|
|
1362
|
+
async function replayOutboxOp(s, op) {
|
|
1363
|
+
switch (op.kind) {
|
|
1364
|
+
case "collection.insert":
|
|
1365
|
+
await s.http.post(`${P.collections}/${encodePathSegment(op.collection)}`, {
|
|
1366
|
+
...op.payload ?? {},
|
|
1367
|
+
idempotency_key: op.idempotencyKey
|
|
1368
|
+
});
|
|
1369
|
+
return;
|
|
1370
|
+
case "collection.update":
|
|
1371
|
+
await s.http.patch(`${P.collections}/${encodePathSegment(op.collection)}/${encodePathSegment(op.id)}`, { set: op.payload ?? {} });
|
|
1372
|
+
return;
|
|
1373
|
+
case "collection.delete":
|
|
1374
|
+
await s.http.delete(`${P.collections}/${encodePathSegment(op.collection)}/${encodePathSegment(op.id)}`);
|
|
1375
|
+
return;
|
|
1376
|
+
case "track":
|
|
1377
|
+
try {
|
|
1378
|
+
await s.http.post(P.events, {
|
|
1379
|
+
event: op.event,
|
|
1380
|
+
properties: op.properties ?? void 0,
|
|
1381
|
+
anonymous_id: s.identity.getAnonymousId(),
|
|
1382
|
+
timestamp: op.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1383
|
+
...op.telemetry ? { telemetry: true } : {}
|
|
1384
|
+
});
|
|
1385
|
+
} catch (err) {
|
|
1386
|
+
const code = err?.code;
|
|
1387
|
+
if (code === "UNAUTHORIZED" || code === "FORBIDDEN") throw new AmbaApiError("NETWORK_ERROR", "track replay deferred: no valid session");
|
|
1388
|
+
throw err;
|
|
1389
|
+
}
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
function getOfflineQueue(s) {
|
|
1394
|
+
if (!s.offlineQueue) s.offlineQueue = new OfflineQueue({
|
|
1395
|
+
store: offlineStore(s),
|
|
1396
|
+
storageKey: `offline_outbox:${s.projectId}`,
|
|
1397
|
+
executor: (op) => replayOutboxOp(s, op),
|
|
1398
|
+
onDropped: (op, err) => {
|
|
1399
|
+
if (typeof console !== "undefined") console.warn(`[amba] dropped un-replayable offline ${op.kind} (${op.collection ?? op.event ?? "?"}):`, err);
|
|
1400
|
+
}
|
|
1401
|
+
});
|
|
1402
|
+
return s.offlineQueue;
|
|
1403
|
+
}
|
|
1404
|
+
async function insertDirect(s, name, row) {
|
|
1405
|
+
return (await s.http.post(`${P.collections}/${encodePathSegment(name)}`, row)).data;
|
|
1406
|
+
}
|
|
1407
|
+
async function updateDirect(s, name, id, set) {
|
|
1408
|
+
return (await s.http.patch(`${P.collections}/${encodePathSegment(name)}/${encodePathSegment(id)}`, { set })).data;
|
|
1409
|
+
}
|
|
1410
|
+
async function deleteDirect(s, name, id) {
|
|
1411
|
+
return await s.http.delete(`${P.collections}/${encodePathSegment(name)}/${encodePathSegment(id)}`) ?? { data: { deleted: true } };
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
494
1414
|
* Top-level SDK namespace for React Native.
|
|
495
1415
|
*/
|
|
496
1416
|
const Amba = {
|
|
@@ -502,17 +1422,32 @@ const Amba = {
|
|
|
502
1422
|
const identity = await IdentityStore.create(persistence);
|
|
503
1423
|
const baseUrl = (config.apiUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
504
1424
|
const sdkVersionHeader = `amba-sdk/${SDK_VERSION} ${`amba-react-native/${SDK_VERSION}`}`;
|
|
1425
|
+
const http = new AmbaHttpClient({
|
|
1426
|
+
apiKey: config.clientKey,
|
|
1427
|
+
baseUrl,
|
|
1428
|
+
sdkPlatform: "react-native",
|
|
1429
|
+
sdkVersionHeader,
|
|
1430
|
+
identity,
|
|
1431
|
+
ensureSession: config.autoAnonymousSession === false ? void 0 : async (path) => {
|
|
1432
|
+
if (pathSkipsLazySession(path)) return;
|
|
1433
|
+
await ensureAnonymousSession(getState());
|
|
1434
|
+
}
|
|
1435
|
+
});
|
|
1436
|
+
const prior = state;
|
|
1437
|
+
_offlineEnableToken += 1;
|
|
1438
|
+
if (prior?.offlineQueue) await prior.offlineQueue.dispose();
|
|
505
1439
|
state = {
|
|
506
|
-
http
|
|
507
|
-
apiKey: config.clientKey,
|
|
508
|
-
baseUrl,
|
|
509
|
-
sdkPlatform: "react-native",
|
|
510
|
-
sdkVersionHeader,
|
|
511
|
-
identity
|
|
512
|
-
}),
|
|
1440
|
+
http,
|
|
513
1441
|
identity,
|
|
514
1442
|
debug: config.debug ?? false,
|
|
515
|
-
projectId: config.projectId
|
|
1443
|
+
projectId: config.projectId,
|
|
1444
|
+
clientKey: config.clientKey,
|
|
1445
|
+
baseUrl,
|
|
1446
|
+
functionsBaseUrl: config.functionsBaseUrl ?? null,
|
|
1447
|
+
resolvedFunctionsProjectId: null,
|
|
1448
|
+
persistence,
|
|
1449
|
+
offlineEnabled: false,
|
|
1450
|
+
offlineQueue: null
|
|
516
1451
|
};
|
|
517
1452
|
},
|
|
518
1453
|
_reset: () => {
|
|
@@ -534,16 +1469,41 @@ const Amba = {
|
|
|
534
1469
|
setDebug: (enabled) => {
|
|
535
1470
|
getState().debug = enabled;
|
|
536
1471
|
},
|
|
1472
|
+
collection: (name) => new CollectionSubscription({
|
|
1473
|
+
baseUrl: getState().baseUrl,
|
|
1474
|
+
authHeaders: realtimeAuthHeaders
|
|
1475
|
+
}, name),
|
|
1476
|
+
get gamification() {
|
|
1477
|
+
return new GamificationSubscription({
|
|
1478
|
+
baseUrl: getState().baseUrl,
|
|
1479
|
+
authHeaders: realtimeAuthHeaders
|
|
1480
|
+
});
|
|
1481
|
+
},
|
|
537
1482
|
events: { track: async (event, properties, options) => {
|
|
538
1483
|
const s = getState();
|
|
1484
|
+
const occurredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
539
1485
|
const body = {
|
|
540
1486
|
event,
|
|
541
1487
|
properties: properties ?? void 0,
|
|
542
1488
|
anonymous_id: s.identity.getAnonymousId(),
|
|
543
|
-
timestamp:
|
|
1489
|
+
timestamp: occurredAt
|
|
544
1490
|
};
|
|
545
1491
|
if (options?.telemetry) body["telemetry"] = true;
|
|
546
|
-
|
|
1492
|
+
try {
|
|
1493
|
+
await s.http.post(P.events, body);
|
|
1494
|
+
} catch (err) {
|
|
1495
|
+
if (s.offlineEnabled && isOfflineError(err)) {
|
|
1496
|
+
await getOfflineQueue(s).enqueue({
|
|
1497
|
+
kind: "track",
|
|
1498
|
+
event,
|
|
1499
|
+
properties,
|
|
1500
|
+
telemetry: options?.telemetry,
|
|
1501
|
+
occurredAt
|
|
1502
|
+
});
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
throw err;
|
|
1506
|
+
}
|
|
547
1507
|
} },
|
|
548
1508
|
auth: {
|
|
549
1509
|
signInAnonymously: async () => {
|
|
@@ -651,6 +1611,19 @@ const Amba = {
|
|
|
651
1611
|
session_token: sessionToken
|
|
652
1612
|
});
|
|
653
1613
|
},
|
|
1614
|
+
linkEmailPassword: async (email, password) => {
|
|
1615
|
+
requireEmail(email);
|
|
1616
|
+
requirePassword(password);
|
|
1617
|
+
const s = getState();
|
|
1618
|
+
const sessionToken = s.identity.getSessionToken();
|
|
1619
|
+
if (!sessionToken) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
|
|
1620
|
+
return postUserNoRotate(s, P.authLink, {
|
|
1621
|
+
provider: "email_password",
|
|
1622
|
+
email,
|
|
1623
|
+
password,
|
|
1624
|
+
session_token: sessionToken
|
|
1625
|
+
});
|
|
1626
|
+
},
|
|
654
1627
|
unlinkSmsOtp: async () => unlinkProvider("sms_otp"),
|
|
655
1628
|
unlinkEmailOtp: async () => unlinkProvider("email_otp"),
|
|
656
1629
|
unlinkApple: async () => unlinkProvider("apple"),
|
|
@@ -731,6 +1704,23 @@ const Amba = {
|
|
|
731
1704
|
return (await getState().http.get(P.sync, query)).data;
|
|
732
1705
|
}
|
|
733
1706
|
},
|
|
1707
|
+
offline: {
|
|
1708
|
+
enable: () => {
|
|
1709
|
+
const s = getState();
|
|
1710
|
+
s.offlineEnabled = true;
|
|
1711
|
+
_offlineEnableToken += 1;
|
|
1712
|
+
const token = _offlineEnableToken;
|
|
1713
|
+
return () => {
|
|
1714
|
+
if (_offlineEnableToken !== token || state !== s) return;
|
|
1715
|
+
s.offlineEnabled = false;
|
|
1716
|
+
};
|
|
1717
|
+
},
|
|
1718
|
+
isEnabled: () => getState().offlineEnabled,
|
|
1719
|
+
pendingCount: async () => getOfflineQueue(getState()).pendingCount(),
|
|
1720
|
+
pending: async () => getOfflineQueue(getState()).pending(),
|
|
1721
|
+
flush: async () => getOfflineQueue(getState()).flush(),
|
|
1722
|
+
clear: async () => getOfflineQueue(getState()).clear()
|
|
1723
|
+
},
|
|
734
1724
|
collections: {
|
|
735
1725
|
find: async (name, options = {}) => {
|
|
736
1726
|
return getState().http.post(`${P.collections}/${encodePathSegment(name)}/find`, options);
|
|
@@ -753,14 +1743,86 @@ const Amba = {
|
|
|
753
1743
|
return (await s.http.get(`${P.collections}/${encodePathSegment(name)}/count`, query)).data.count;
|
|
754
1744
|
},
|
|
755
1745
|
insert: async (name, row) => {
|
|
756
|
-
|
|
1746
|
+
const s = getState();
|
|
1747
|
+
if (s.offlineEnabled) {
|
|
1748
|
+
const idempotencyKey = (typeof row["idempotency_key"] === "string" ? row["idempotency_key"] : void 0) ?? newIdempotencyKey();
|
|
1749
|
+
try {
|
|
1750
|
+
return (await s.http.post(`${P.collections}/${encodePathSegment(name)}`, {
|
|
1751
|
+
...row,
|
|
1752
|
+
idempotency_key: idempotencyKey
|
|
1753
|
+
})).data;
|
|
1754
|
+
} catch (err) {
|
|
1755
|
+
if (isOfflineError(err)) {
|
|
1756
|
+
await getOfflineQueue(s).enqueue({
|
|
1757
|
+
kind: "collection.insert",
|
|
1758
|
+
collection: name,
|
|
1759
|
+
payload: row,
|
|
1760
|
+
idempotencyKey
|
|
1761
|
+
});
|
|
1762
|
+
return {
|
|
1763
|
+
...row,
|
|
1764
|
+
_amba_pending: true
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
throw err;
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
return (await s.http.post(`${P.collections}/${encodePathSegment(name)}`, row)).data;
|
|
757
1771
|
},
|
|
758
1772
|
update: async (name, id, set) => {
|
|
759
|
-
|
|
1773
|
+
const s = getState();
|
|
1774
|
+
if (s.offlineEnabled) try {
|
|
1775
|
+
return (await s.http.patch(`${P.collections}/${encodePathSegment(name)}/${encodePathSegment(id)}`, { set })).data;
|
|
1776
|
+
} catch (err) {
|
|
1777
|
+
if (isOfflineError(err)) {
|
|
1778
|
+
await getOfflineQueue(s).enqueue({
|
|
1779
|
+
kind: "collection.update",
|
|
1780
|
+
collection: name,
|
|
1781
|
+
id,
|
|
1782
|
+
payload: set
|
|
1783
|
+
});
|
|
1784
|
+
return {
|
|
1785
|
+
id,
|
|
1786
|
+
...set,
|
|
1787
|
+
_amba_pending: true
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
throw err;
|
|
1791
|
+
}
|
|
1792
|
+
return (await s.http.patch(`${P.collections}/${encodePathSegment(name)}/${encodePathSegment(id)}`, { set })).data;
|
|
760
1793
|
},
|
|
761
1794
|
delete: async (name, id) => {
|
|
762
|
-
|
|
763
|
-
|
|
1795
|
+
const s = getState();
|
|
1796
|
+
if (s.offlineEnabled) try {
|
|
1797
|
+
return await s.http.delete(`${P.collections}/${encodePathSegment(name)}/${encodePathSegment(id)}`) ?? { data: { deleted: true } };
|
|
1798
|
+
} catch (err) {
|
|
1799
|
+
if (isOfflineError(err)) {
|
|
1800
|
+
await getOfflineQueue(s).enqueue({
|
|
1801
|
+
kind: "collection.delete",
|
|
1802
|
+
collection: name,
|
|
1803
|
+
id
|
|
1804
|
+
});
|
|
1805
|
+
return { data: { deleted: true } };
|
|
1806
|
+
}
|
|
1807
|
+
throw err;
|
|
1808
|
+
}
|
|
1809
|
+
return await s.http.delete(`${P.collections}/${encodePathSegment(name)}/${encodePathSegment(id)}`) ?? { data: { deleted: true } };
|
|
1810
|
+
},
|
|
1811
|
+
optimisticInsert: async (name, row, view) => withOptimistic({
|
|
1812
|
+
apply: view.apply,
|
|
1813
|
+
mutate: () => insertDirect(getState(), name, row),
|
|
1814
|
+
rollback: view.rollback
|
|
1815
|
+
}),
|
|
1816
|
+
optimisticUpdate: async (name, id, set, view) => withOptimistic({
|
|
1817
|
+
apply: view.apply,
|
|
1818
|
+
mutate: () => updateDirect(getState(), name, id, set),
|
|
1819
|
+
rollback: view.rollback
|
|
1820
|
+
}),
|
|
1821
|
+
optimisticDelete: async (name, id, view) => withOptimistic({
|
|
1822
|
+
apply: view.apply,
|
|
1823
|
+
mutate: () => deleteDirect(getState(), name, id),
|
|
1824
|
+
rollback: view.rollback
|
|
1825
|
+
}),
|
|
764
1826
|
where: {
|
|
765
1827
|
eq: (column, value) => ({
|
|
766
1828
|
column,
|
|
@@ -888,19 +1950,54 @@ const Amba = {
|
|
|
888
1950
|
},
|
|
889
1951
|
unsubscribe: async (topic) => {
|
|
890
1952
|
await getState().http.post(P.pushUnsubscribe, { topic });
|
|
891
|
-
}
|
|
1953
|
+
},
|
|
1954
|
+
enableAutoRotation: (subscribe, platform = "fcm") => enableNativePushAutoRotation(subscribe, (token, p) => Amba.push.register(token, p), platform)
|
|
892
1955
|
},
|
|
1956
|
+
notifications,
|
|
893
1957
|
entitlements: {
|
|
894
1958
|
list: async () => {
|
|
895
1959
|
return (await getState().http.get(P.entitlements)).data;
|
|
896
1960
|
},
|
|
897
|
-
has: async (
|
|
898
|
-
return (await getState().http.get(P.entitlements, { active_only: "true" })).data.some((e) => e.
|
|
1961
|
+
has: async (entitlementId) => {
|
|
1962
|
+
return (await getState().http.get(P.entitlements, { active_only: "true" })).data.some((e) => e.entitlement_id === entitlementId && e.is_active);
|
|
899
1963
|
}
|
|
900
1964
|
},
|
|
901
1965
|
ai: { anthropic: { messages: { create: async (request) => {
|
|
902
|
-
|
|
1966
|
+
const env = await getState().http.post(P.aiAnthropic, request);
|
|
1967
|
+
if (!env || typeof env !== "object" || env.data === void 0) throw new AmbaApiError("MALFORMED_RESPONSE", "AI response was not wrapped in the expected { data } envelope", env);
|
|
1968
|
+
return env.data;
|
|
903
1969
|
} } } },
|
|
1970
|
+
functions: { invoke: async (name, body, options) => {
|
|
1971
|
+
const s = getState();
|
|
1972
|
+
if (!name || name.trim() === "") throw new AmbaApiError("INVALID_ARGUMENT", "Amba.functions.invoke: a function name is required.");
|
|
1973
|
+
const url = await resolveFunctionUrl(s, name);
|
|
1974
|
+
const method = options?.method ?? "POST";
|
|
1975
|
+
const headers = s.http.buildHeaders(options?.headers);
|
|
1976
|
+
let requestBody;
|
|
1977
|
+
if (body !== void 0 && method !== "GET") requestBody = JSON.stringify(body);
|
|
1978
|
+
else delete headers["Content-Type"];
|
|
1979
|
+
let response;
|
|
1980
|
+
try {
|
|
1981
|
+
response = await (globalThis.fetch ?? fetch)(url, {
|
|
1982
|
+
method,
|
|
1983
|
+
headers,
|
|
1984
|
+
body: requestBody
|
|
1985
|
+
});
|
|
1986
|
+
} catch (err) {
|
|
1987
|
+
throw new AmbaApiError("NETWORK_ERROR", `Amba.functions.invoke("${name}") network error: ${err instanceof Error ? err.message : String(err)}`, err);
|
|
1988
|
+
}
|
|
1989
|
+
const text = await response.text();
|
|
1990
|
+
if (!response.ok) {
|
|
1991
|
+
const ct = response.headers.get("content-type") ?? "";
|
|
1992
|
+
throw mapHttpError(response, text, ct.includes("application/json"));
|
|
1993
|
+
}
|
|
1994
|
+
if (!text) return void 0;
|
|
1995
|
+
try {
|
|
1996
|
+
return JSON.parse(text);
|
|
1997
|
+
} catch {
|
|
1998
|
+
return text;
|
|
1999
|
+
}
|
|
2000
|
+
} },
|
|
904
2001
|
config: { fetch: async () => {
|
|
905
2002
|
const s = getState();
|
|
906
2003
|
const url = s.http.url(P.config);
|
|
@@ -1110,6 +2207,10 @@ const Amba = {
|
|
|
1110
2207
|
}
|
|
1111
2208
|
},
|
|
1112
2209
|
messaging: {
|
|
2210
|
+
conversation: (conversationId) => new ConversationSubscription({
|
|
2211
|
+
baseUrl: getState().baseUrl,
|
|
2212
|
+
authHeaders: realtimeAuthHeaders
|
|
2213
|
+
}, conversationId),
|
|
1113
2214
|
createConversation: async (request) => {
|
|
1114
2215
|
return (await getState().http.post(`${P.messaging}/conversations`, request)).data;
|
|
1115
2216
|
},
|
|
@@ -1210,19 +2311,19 @@ const Amba = {
|
|
|
1210
2311
|
return (await getState().http.get(`${P.content}/today/${encodePathSegment(channel)}`)).data ?? null;
|
|
1211
2312
|
},
|
|
1212
2313
|
library: async (channel = "default", options) => {
|
|
1213
|
-
return (await getState().http.get(`${P.content}/
|
|
2314
|
+
return (await getState().http.get(`${P.content}/libraries/${encodePathSegment(channel)}`, {
|
|
1214
2315
|
limit: options?.limit,
|
|
1215
2316
|
cursor: options?.cursor
|
|
1216
2317
|
})).data;
|
|
1217
2318
|
},
|
|
1218
2319
|
item: async (id) => {
|
|
1219
|
-
return (await getState().http.get(`${P.content}/${encodePathSegment(id)}`)).data;
|
|
2320
|
+
return (await getState().http.get(`${P.content}/items/${encodePathSegment(id)}`)).data;
|
|
1220
2321
|
},
|
|
1221
2322
|
updateItem: async (id, state_) => {
|
|
1222
|
-
return (await getState().http.
|
|
2323
|
+
return (await getState().http.patch(`${P.content}/items/${encodePathSegment(id)}`, state_)).data;
|
|
1223
2324
|
},
|
|
1224
2325
|
createItem: async (channel, item) => {
|
|
1225
|
-
return (await getState().http.post(`${P.content}/${encodePathSegment(channel)}`, item)).data;
|
|
2326
|
+
return (await getState().http.post(`${P.content}/libraries/${encodePathSegment(channel)}/items`, item)).data;
|
|
1226
2327
|
}
|
|
1227
2328
|
},
|
|
1228
2329
|
deepLinks: {
|
|
@@ -1255,5 +2356,9 @@ const Amba = {
|
|
|
1255
2356
|
//#endregion
|
|
1256
2357
|
exports.Amba = Amba;
|
|
1257
2358
|
exports.AmbaApiError = AmbaApiError;
|
|
2359
|
+
exports.CollectionSubscription = CollectionSubscription;
|
|
2360
|
+
exports.ConversationSubscription = ConversationSubscription;
|
|
2361
|
+
exports.GamificationSubscription = GamificationSubscription;
|
|
1258
2362
|
exports.SDK_VERSION = SDK_VERSION;
|
|
2363
|
+
exports.withOptimistic = withOptimistic;
|
|
1259
2364
|
//# sourceMappingURL=index.cjs.map
|