@layers/amba-web 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.d.ts +243 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +275 -3
- package/dist/index.js.map +1 -1
- package/dist-rn/index.d.ts +243 -8
- package/dist-rn/index.js +275 -3
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -99,8 +99,173 @@ function toAmbaApiError(err) {
|
|
|
99
99
|
return new AmbaApiError("UNKNOWN_ERROR", String(err));
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/realtime.ts
|
|
104
|
+
const INITIAL_BACKOFF_MS = 500;
|
|
105
|
+
const MAX_BACKOFF_MS = 15e3;
|
|
106
|
+
/**
|
|
107
|
+
* Builder returned by `Amba.collection(name)`. Chain `.where(...)` to
|
|
108
|
+
* narrow, then `.subscribe(cb)` to start streaming.
|
|
109
|
+
*/
|
|
110
|
+
var CollectionSubscription = class {
|
|
111
|
+
filters = {};
|
|
112
|
+
constructor(ctx, name) {
|
|
113
|
+
this.ctx = ctx;
|
|
114
|
+
this.name = name;
|
|
115
|
+
}
|
|
116
|
+
/** Add an equality filter. Chainable. All filters must match (AND). */
|
|
117
|
+
where(column, value) {
|
|
118
|
+
this.filters[column] = value;
|
|
119
|
+
return this;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Start listening. `cb` fires once per matching row change. Returns an
|
|
123
|
+
* unsubscribe handle — call it to close the connection.
|
|
124
|
+
*/
|
|
125
|
+
subscribe(cb, options = {}) {
|
|
126
|
+
if (!this.ctx.authHeaders()) throw new Error("Amba.collection().subscribe() requires an authenticated session — call Amba.auth.signIn*/verify* first.");
|
|
127
|
+
const qs = Object.keys(this.filters).length > 0 ? `?where=${encodeURIComponent(JSON.stringify(this.filters))}` : "";
|
|
128
|
+
return openStream({
|
|
129
|
+
url: `${this.ctx.baseUrl}/v1/client/realtime/collections/${encodeURIComponent(this.name)}${qs}`,
|
|
130
|
+
authHeaders: this.ctx.authHeaders,
|
|
131
|
+
onChange: cb,
|
|
132
|
+
options
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Open a streamed GET to a realtime endpoint and dispatch row-change events
|
|
138
|
+
* to `onChange`. Reconnects with exponential backoff until the returned
|
|
139
|
+
* unsubscribe handle is invoked. Shared by both collection and conversation
|
|
140
|
+
* subscriptions — the only thing that differs between them is the URL.
|
|
141
|
+
*
|
|
142
|
+
* Uses `fetch` + a streamed response body (rather than the browser's
|
|
143
|
+
* native `EventSource`) because `EventSource` cannot set request headers —
|
|
144
|
+
* which would force the session token into the URL query string, where it
|
|
145
|
+
* would leak into access logs. Streaming `fetch` keeps auth in headers.
|
|
146
|
+
*/
|
|
147
|
+
function openStream(args) {
|
|
148
|
+
let stopped = false;
|
|
149
|
+
let controller = null;
|
|
150
|
+
let backoff = INITIAL_BACKOFF_MS;
|
|
151
|
+
const run = async () => {
|
|
152
|
+
while (!stopped) {
|
|
153
|
+
controller = new AbortController();
|
|
154
|
+
try {
|
|
155
|
+
const headers = args.authHeaders();
|
|
156
|
+
if (!headers) throw new Error("no active session");
|
|
157
|
+
const resp = await fetch(args.url, {
|
|
158
|
+
method: "GET",
|
|
159
|
+
headers: {
|
|
160
|
+
...headers,
|
|
161
|
+
Accept: "text/event-stream"
|
|
162
|
+
},
|
|
163
|
+
signal: controller.signal
|
|
164
|
+
});
|
|
165
|
+
if (!resp.ok || !resp.body) throw new Error(`realtime connect failed: HTTP ${resp.status}`);
|
|
166
|
+
await consume(resp.body, args, () => {
|
|
167
|
+
backoff = INITIAL_BACKOFF_MS;
|
|
168
|
+
args.options.onOpen?.();
|
|
169
|
+
});
|
|
170
|
+
} catch (err) {
|
|
171
|
+
if (stopped) break;
|
|
172
|
+
args.options.onError?.(err);
|
|
173
|
+
}
|
|
174
|
+
if (stopped) break;
|
|
175
|
+
await delay(backoff);
|
|
176
|
+
backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
run();
|
|
180
|
+
return () => {
|
|
181
|
+
stopped = true;
|
|
182
|
+
controller?.abort();
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
async function consume(body, args, onFirstFrame) {
|
|
186
|
+
const reader = body.getReader();
|
|
187
|
+
const decoder = new TextDecoder();
|
|
188
|
+
let buffer = "";
|
|
189
|
+
let sawFrame = false;
|
|
190
|
+
for (;;) {
|
|
191
|
+
const { done, value } = await reader.read();
|
|
192
|
+
if (done) return;
|
|
193
|
+
buffer += decoder.decode(value, { stream: true });
|
|
194
|
+
let idx;
|
|
195
|
+
while ((idx = buffer.indexOf("\n\n")) !== -1) {
|
|
196
|
+
const frame = buffer.slice(0, idx);
|
|
197
|
+
buffer = buffer.slice(idx + 2);
|
|
198
|
+
if (!sawFrame) {
|
|
199
|
+
sawFrame = true;
|
|
200
|
+
onFirstFrame();
|
|
201
|
+
}
|
|
202
|
+
dispatchFrame(frame, args.onChange);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function dispatchFrame(frame, onChange) {
|
|
207
|
+
let event = "";
|
|
208
|
+
const dataLines = [];
|
|
209
|
+
for (const line of frame.split("\n")) {
|
|
210
|
+
if (line.startsWith(":")) continue;
|
|
211
|
+
if (line.startsWith("event:")) event = line.slice(6).trim();
|
|
212
|
+
else if (line.startsWith("data:")) dataLines.push(line.slice(5).trim());
|
|
213
|
+
}
|
|
214
|
+
if (event !== "change") return;
|
|
215
|
+
try {
|
|
216
|
+
const payload = JSON.parse(dataLines.join("\n"));
|
|
217
|
+
onChange({
|
|
218
|
+
op: payload.op,
|
|
219
|
+
row: payload.row,
|
|
220
|
+
truncated: payload.truncated
|
|
221
|
+
});
|
|
222
|
+
} catch {}
|
|
223
|
+
}
|
|
224
|
+
function delay(ms) {
|
|
225
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Builder returned by `Amba.messaging.conversation(id)`. Call `.subscribe(cb)`
|
|
229
|
+
* to receive new messages live instead of polling.
|
|
230
|
+
*
|
|
231
|
+
* const unsub = Amba.messaging
|
|
232
|
+
* .conversation(conversationId)
|
|
233
|
+
* .subscribe((change) => {
|
|
234
|
+
* // change.op === "INSERT"; change.row is the new message
|
|
235
|
+
* });
|
|
236
|
+
* unsub(); // stop listening
|
|
237
|
+
*
|
|
238
|
+
* Delivery is scoped to conversation participants — you receive messages for
|
|
239
|
+
* a conversation only if your session is a member of it. Reconnects with the
|
|
240
|
+
* same exponential backoff as collection subscriptions.
|
|
241
|
+
*/
|
|
242
|
+
var ConversationSubscription = class {
|
|
243
|
+
constructor(ctx, conversationId) {
|
|
244
|
+
this.ctx = ctx;
|
|
245
|
+
this.conversationId = conversationId;
|
|
246
|
+
}
|
|
247
|
+
subscribe(cb, options = {}) {
|
|
248
|
+
if (!this.ctx.authHeaders()) throw new Error("Amba.messaging.conversation().subscribe() requires an authenticated session — call Amba.auth.signIn*/verify* first.");
|
|
249
|
+
return openStream({
|
|
250
|
+
url: `${this.ctx.baseUrl}/v1/client/realtime/conversations/${encodeURIComponent(this.conversationId)}`,
|
|
251
|
+
authHeaders: this.ctx.authHeaders,
|
|
252
|
+
onChange: cb,
|
|
253
|
+
options
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
102
258
|
//#endregion
|
|
103
259
|
//#region src/index.ts
|
|
260
|
+
const DEFAULT_API_URL = "https://api.amba.dev";
|
|
261
|
+
/**
|
|
262
|
+
* Default host that serves deployed functions. `Amba.functions.invoke(name)`
|
|
263
|
+
* targets `https://<projectId>.<DEFAULT_FUNCTIONS_HOST>/<name>` unless a
|
|
264
|
+
* `functionsBaseUrl` override is configured. The `<projectId>` label is the
|
|
265
|
+
* server-resolved id (from `diagnostics.ping`), which the function router
|
|
266
|
+
* accepts directly.
|
|
267
|
+
*/
|
|
268
|
+
const DEFAULT_FUNCTIONS_HOST = "fn.amba.host";
|
|
104
269
|
/**
|
|
105
270
|
* Internal state — one instance per page. Created lazily by `Amba.configure()`.
|
|
106
271
|
*/
|
|
@@ -111,6 +276,26 @@ let coreInstance = null;
|
|
|
111
276
|
* core finishes initializing.
|
|
112
277
|
*/
|
|
113
278
|
let _configuredProjectId = null;
|
|
279
|
+
/**
|
|
280
|
+
* Realtime (`Amba.collection(name).subscribe`) needs the API base URL,
|
|
281
|
+
* client key, and current session token in the TS layer to open the
|
|
282
|
+
* streamed connection — the engine doesn't expose them back. We capture
|
|
283
|
+
* the base URL + client key at configure-time and the session token from
|
|
284
|
+
* every auth result (the engine rotates it internally on refresh; the
|
|
285
|
+
* subscribe connection picks up the latest at connect time).
|
|
286
|
+
*/
|
|
287
|
+
let _configuredApiUrl = DEFAULT_API_URL;
|
|
288
|
+
let _configuredClientKey = null;
|
|
289
|
+
let _currentSessionToken = null;
|
|
290
|
+
/**
|
|
291
|
+
* Functions invocation state. `_configuredFunctionsBaseUrl` is the optional
|
|
292
|
+
* override from `Amba.configure({ functionsBaseUrl })`. When it's null, the
|
|
293
|
+
* default-host path resolves the project id once via `diagnostics.ping` and
|
|
294
|
+
* caches it in `_resolvedFunctionsProjectId` so subsequent invokes skip the
|
|
295
|
+
* round-trip. Both reset on every `configure()`.
|
|
296
|
+
*/
|
|
297
|
+
let _configuredFunctionsBaseUrl = null;
|
|
298
|
+
let _resolvedFunctionsProjectId = null;
|
|
114
299
|
function requireCore() {
|
|
115
300
|
if (!coreInstance) throw new AmbaApiError("NOT_INITIALIZED", "amba SDK not configured. Call Amba.configure({ projectId, clientKey }) at app start before any other API.");
|
|
116
301
|
return coreInstance;
|
|
@@ -148,9 +333,40 @@ function notifyAuthSubscribers(session) {
|
|
|
148
333
|
}
|
|
149
334
|
}
|
|
150
335
|
async function notifyFromAuthResult(result) {
|
|
336
|
+
if (result?.session_token) _currentSessionToken = result.session_token;
|
|
151
337
|
notifyAuthSubscribers(snapshotSession(result.user));
|
|
152
338
|
}
|
|
153
339
|
/**
|
|
340
|
+
* Build the auth headers for a realtime subscribe connection, or `null`
|
|
341
|
+
* when there's no live session. Read lazily at connect-time so reconnects
|
|
342
|
+
* pick up a rotated session token.
|
|
343
|
+
*/
|
|
344
|
+
function realtimeAuthHeaders() {
|
|
345
|
+
if (!_configuredClientKey || !_currentSessionToken) return null;
|
|
346
|
+
return {
|
|
347
|
+
"X-Api-Key": _configuredClientKey,
|
|
348
|
+
Authorization: `Bearer ${_currentSessionToken}`
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Resolve the full invocation URL for a deployed function.
|
|
353
|
+
*
|
|
354
|
+
* With `functionsBaseUrl` configured we POST to `<base>/<name>` verbatim
|
|
355
|
+
* (custom domains / non-prod). Otherwise we resolve the server-side project
|
|
356
|
+
* id once (via `diagnostics.ping`, cached) and target
|
|
357
|
+
* `https://<projectId>.fn.amba.host/<name>` — the function router accepts the
|
|
358
|
+
* project id as the host label directly.
|
|
359
|
+
*/
|
|
360
|
+
async function resolveFunctionUrl(name) {
|
|
361
|
+
if (_configuredFunctionsBaseUrl) return `${_configuredFunctionsBaseUrl.replace(/\/$/, "")}/${encodeURIComponent(name)}`;
|
|
362
|
+
if (!_resolvedFunctionsProjectId) {
|
|
363
|
+
const ping = await Amba.diagnostics.ping();
|
|
364
|
+
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);
|
|
365
|
+
_resolvedFunctionsProjectId = ping.server_project_id;
|
|
366
|
+
}
|
|
367
|
+
return `https://${_resolvedFunctionsProjectId}.${DEFAULT_FUNCTIONS_HOST}/${encodeURIComponent(name)}`;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
154
370
|
* Top-level SDK namespace.
|
|
155
371
|
*
|
|
156
372
|
* Stateless from the JS side — all state lives in the SDK runtime. Calling
|
|
@@ -187,6 +403,10 @@ const Amba = {
|
|
|
187
403
|
throw toAmbaApiError(err);
|
|
188
404
|
}
|
|
189
405
|
_configuredProjectId = config.projectId;
|
|
406
|
+
_configuredApiUrl = config.apiUrl ?? DEFAULT_API_URL;
|
|
407
|
+
_configuredClientKey = config.clientKey;
|
|
408
|
+
_configuredFunctionsBaseUrl = config.functionsBaseUrl ?? null;
|
|
409
|
+
_resolvedFunctionsProjectId = null;
|
|
190
410
|
},
|
|
191
411
|
get projectId() {
|
|
192
412
|
if (!_configuredProjectId) throw new AmbaApiError("NOT_INITIALIZED", "amba SDK not configured. Call Amba.configure({ projectId, clientKey }) at app start before any other API.");
|
|
@@ -268,6 +488,9 @@ const Amba = {
|
|
|
268
488
|
linkEmailOtp: async (email, code) => {
|
|
269
489
|
return await coreCall((c) => c.linkEmailOtp(email, code));
|
|
270
490
|
},
|
|
491
|
+
linkEmailPassword: async (email, password) => {
|
|
492
|
+
return await coreCall((c) => c.linkEmailPassword(email, password));
|
|
493
|
+
},
|
|
271
494
|
unlinkSmsOtp: async () => {
|
|
272
495
|
return await coreCall((c) => c.unlinkSmsOtp());
|
|
273
496
|
},
|
|
@@ -285,6 +508,7 @@ const Amba = {
|
|
|
285
508
|
},
|
|
286
509
|
signOut: async (rotateAnonymousId = false) => {
|
|
287
510
|
await coreCall((c) => c.signOut(rotateAnonymousId));
|
|
511
|
+
_currentSessionToken = null;
|
|
288
512
|
notifyAuthSubscribers(null);
|
|
289
513
|
},
|
|
290
514
|
refresh: async () => {
|
|
@@ -328,6 +552,10 @@ const Amba = {
|
|
|
328
552
|
await coreCall((c) => c.sessionsRevoke(sessionId));
|
|
329
553
|
}
|
|
330
554
|
},
|
|
555
|
+
collection: (name) => new CollectionSubscription({
|
|
556
|
+
baseUrl: _configuredApiUrl,
|
|
557
|
+
authHeaders: realtimeAuthHeaders
|
|
558
|
+
}, name),
|
|
331
559
|
collections: {
|
|
332
560
|
find: async (name, options = {}) => {
|
|
333
561
|
return await coreCall((c) => c.collectionsFind(name, encodeJson(options)));
|
|
@@ -485,6 +713,45 @@ const Amba = {
|
|
|
485
713
|
ai: { anthropic: { messages: { create: async (request) => {
|
|
486
714
|
return await coreCall((c) => c.aiAnthropicMessages(encodeJson(request)));
|
|
487
715
|
} } } },
|
|
716
|
+
functions: { invoke: async (name, body, options) => {
|
|
717
|
+
requireCore();
|
|
718
|
+
if (!_configuredClientKey) throw new AmbaApiError("NOT_INITIALIZED", "amba SDK not configured. Call Amba.configure({ projectId, clientKey }) before Amba.functions.invoke().");
|
|
719
|
+
if (!name || name.trim() === "") throw new AmbaApiError("INVALID_ARGUMENT", "Amba.functions.invoke: a function name is required.");
|
|
720
|
+
const url = await resolveFunctionUrl(name);
|
|
721
|
+
const method = options?.method ?? "POST";
|
|
722
|
+
const headers = {
|
|
723
|
+
"X-Api-Key": _configuredClientKey,
|
|
724
|
+
...options?.headers ?? {}
|
|
725
|
+
};
|
|
726
|
+
if (_currentSessionToken) headers["Authorization"] = `Bearer ${_currentSessionToken}`;
|
|
727
|
+
let requestBody;
|
|
728
|
+
if (body !== void 0 && method !== "GET") {
|
|
729
|
+
headers["content-type"] = "application/json";
|
|
730
|
+
requestBody = JSON.stringify(body);
|
|
731
|
+
}
|
|
732
|
+
let resp;
|
|
733
|
+
try {
|
|
734
|
+
resp = await fetch(url, {
|
|
735
|
+
method,
|
|
736
|
+
headers,
|
|
737
|
+
body: requestBody
|
|
738
|
+
});
|
|
739
|
+
} catch (err) {
|
|
740
|
+
throw new AmbaApiError("NETWORK_ERROR", `Amba.functions.invoke("${name}") network error: ${String(err)}`);
|
|
741
|
+
}
|
|
742
|
+
const text = await resp.text();
|
|
743
|
+
let parsed = void 0;
|
|
744
|
+
if (text.length > 0) try {
|
|
745
|
+
parsed = JSON.parse(text);
|
|
746
|
+
} catch {
|
|
747
|
+
parsed = text;
|
|
748
|
+
}
|
|
749
|
+
if (!resp.ok) {
|
|
750
|
+
const envelope = parsed && typeof parsed === "object" ? parsed : void 0;
|
|
751
|
+
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 : `Amba.functions.invoke("${name}") failed with status ${resp.status}`, parsed);
|
|
752
|
+
}
|
|
753
|
+
return parsed;
|
|
754
|
+
} },
|
|
488
755
|
config: { fetch: async () => {
|
|
489
756
|
return await coreCall((c) => c.configFetch());
|
|
490
757
|
} },
|
|
@@ -591,7 +858,12 @@ const Amba = {
|
|
|
591
858
|
getMessage: async (conversationId, messageId) => {
|
|
592
859
|
return await coreCall((c) => c.messagingGetMessage(conversationId, messageId));
|
|
593
860
|
},
|
|
594
|
-
|
|
861
|
+
addParticipant: async (conversationId, userId) => await coreCall((c) => c.messagingAddParticipant(conversationId, userId)),
|
|
862
|
+
markRead: async (conversationId) => await coreCall((c) => c.messagingMarkRead(conversationId)),
|
|
863
|
+
conversation: (conversationId) => new ConversationSubscription({
|
|
864
|
+
baseUrl: _configuredApiUrl,
|
|
865
|
+
authHeaders: realtimeAuthHeaders
|
|
866
|
+
}, conversationId)
|
|
595
867
|
},
|
|
596
868
|
moderation: {
|
|
597
869
|
reportUser: async (request) => await coreCall((c) => c.moderationReportUser(encodeJson(request))),
|
|
@@ -649,8 +921,8 @@ function getCoreWasm() {
|
|
|
649
921
|
* `verify-sdk-version-coherence` GH action gates every publish on the
|
|
650
922
|
* pair agreeing (and matching the `web-v<X.Y.Z>` tag when tag-triggered).
|
|
651
923
|
*/
|
|
652
|
-
const SDK_VERSION = "4.0.
|
|
924
|
+
const SDK_VERSION = "4.0.4";
|
|
653
925
|
|
|
654
926
|
//#endregion
|
|
655
|
-
export { Amba, AmbaApiError, SDK_VERSION, getCoreWasm };
|
|
927
|
+
export { Amba, AmbaApiError, CollectionSubscription, ConversationSubscription, SDK_VERSION, getCoreWasm };
|
|
656
928
|
//# sourceMappingURL=index.js.map
|