@layers/amba-react-native 4.2.0 → 4.2.2
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 +442 -94
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -9
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +43 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +442 -94
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -84,12 +84,200 @@ var AmbaApiError = class extends Error {
|
|
|
84
84
|
}
|
|
85
85
|
};
|
|
86
86
|
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/request-deadline.ts
|
|
89
|
+
/**
|
|
90
|
+
* Request deadlines, shared by every JS SDK target (the same file ships in
|
|
91
|
+
* `@layers/amba-web`, `@layers/amba-node` and `@layers/amba-react-native`).
|
|
92
|
+
*
|
|
93
|
+
* Every request the SDK sends runs under a deadline that covers sending it
|
|
94
|
+
* and reading the whole response. When the deadline passes, the request is
|
|
95
|
+
* aborted and fails with the SDK's retryable network error, so one request
|
|
96
|
+
* can never hold an in-flight slot (the offline outbox flush, the lazy
|
|
97
|
+
* anonymous sign-in, the session refresh, the event batch flush) for the
|
|
98
|
+
* life of the process. Some runtimes never settle a fetch they gave up on:
|
|
99
|
+
* Expo on Android drops a failed DNS lookup and the promise stays pending.
|
|
100
|
+
* The deadline wins the race even then, because it rejects on its own timer
|
|
101
|
+
* and then aborts the fetch for the runtime to clean up.
|
|
102
|
+
*
|
|
103
|
+
* Two bounds:
|
|
104
|
+
* - `requestTimeoutMs` (default 15 s): an ordinary API request.
|
|
105
|
+
* - `longRequestTimeoutMs` (default 5 min): work that legitimately runs
|
|
106
|
+
* longer. AI generation (`/v1/client/ai/*`), function invocations, and
|
|
107
|
+
* media uploads and downloads.
|
|
108
|
+
* Both grow with the bytes a request moves (see `withTransferAllowance`), so
|
|
109
|
+
* a large upload, download or event batch on a slow link has time to finish.
|
|
110
|
+
* Realtime subscriptions are bounded until their stream opens; AI response
|
|
111
|
+
* streams likewise until the response starts. Once open, a stream runs for
|
|
112
|
+
* as long as the caller keeps it.
|
|
113
|
+
*/
|
|
114
|
+
/** Default deadline for one API request, in milliseconds. */
|
|
115
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 15e3;
|
|
116
|
+
/**
|
|
117
|
+
* Default deadline for AI generation, function invocations and media
|
|
118
|
+
* transfers, in milliseconds.
|
|
119
|
+
*/
|
|
120
|
+
const DEFAULT_LONG_REQUEST_TIMEOUT_MS = 3e5;
|
|
121
|
+
/**
|
|
122
|
+
* The slowest link a deadline makes room for, in bytes per second
|
|
123
|
+
* (8 KiB/s, about 64 kbit/s). Each byte a request sends or receives beyond
|
|
124
|
+
* the first {@link TRANSFER_BYTES_WITHIN_BASE} adds the time it takes at this
|
|
125
|
+
* rate to the deadline.
|
|
126
|
+
*/
|
|
127
|
+
const MIN_TRANSFER_BYTES_PER_SECOND = 8 * 1024;
|
|
128
|
+
/**
|
|
129
|
+
* Bytes a request moves within its base deadline (64 KiB: 8 s at the
|
|
130
|
+
* slowest rate). An ordinary API request stays at exactly
|
|
131
|
+
* `requestTimeoutMs`; only larger bodies widen it.
|
|
132
|
+
*/
|
|
133
|
+
const TRANSFER_BYTES_WITHIN_BASE = 64 * 1024;
|
|
134
|
+
/** The largest delay `setTimeout` honors (about 24.8 days). */
|
|
135
|
+
const MAX_TIMEOUT_MS = 2147483647;
|
|
136
|
+
/**
|
|
137
|
+
* Read the deadlines from the configure options, applying the defaults.
|
|
138
|
+
* Throws an `Error` naming the option when a value is not a positive number
|
|
139
|
+
* of milliseconds.
|
|
140
|
+
*/
|
|
141
|
+
function resolveRequestDeadlines(options) {
|
|
142
|
+
return {
|
|
143
|
+
requestMs: readTimeout("Amba.configure: requestTimeoutMs", options.requestTimeoutMs, DEFAULT_REQUEST_TIMEOUT_MS),
|
|
144
|
+
longMs: readTimeout("Amba.configure: longRequestTimeoutMs", options.longRequestTimeoutMs, DEFAULT_LONG_REQUEST_TIMEOUT_MS)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Validate one timeout value; `undefined` keeps `fallback`. `label` names the
|
|
149
|
+
* option in the thrown `Error` (`Amba.configure: requestTimeoutMs`).
|
|
150
|
+
*/
|
|
151
|
+
function readTimeout(label, value, fallback) {
|
|
152
|
+
if (value === void 0) return fallback;
|
|
153
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > MAX_TIMEOUT_MS) throw new Error(`${label} must be a positive number of milliseconds, at most ${MAX_TIMEOUT_MS}`);
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
/** The text a deadline failure carries, e.g. `request timed out after 15000 ms`. */
|
|
157
|
+
function timeoutMessage(timeoutMs) {
|
|
158
|
+
return `request timed out after ${timeoutMs} ms`;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* `timeoutMs` plus the time the bytes beyond the first
|
|
162
|
+
* {@link TRANSFER_BYTES_WITHIN_BASE} take at
|
|
163
|
+
* {@link MIN_TRANSFER_BYTES_PER_SECOND}, capped at the largest delay
|
|
164
|
+
* `setTimeout` honors. Unknown sizes add nothing.
|
|
165
|
+
*/
|
|
166
|
+
function withTransferAllowance(timeoutMs, bytes) {
|
|
167
|
+
const beyond = typeof bytes === "number" && Number.isFinite(bytes) ? bytes - TRANSFER_BYTES_WITHIN_BASE : 0;
|
|
168
|
+
const extra = beyond > 0 ? Math.ceil(beyond / MIN_TRANSFER_BYTES_PER_SECOND * 1e3) : 0;
|
|
169
|
+
return Math.min(MAX_TIMEOUT_MS, timeoutMs + extra);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The size of a request body in bytes: a string's UTF-8 length, a buffer's
|
|
173
|
+
* byte length, a `Blob`'s size. `undefined` for no body or a body of unknown
|
|
174
|
+
* size (a stream).
|
|
175
|
+
*/
|
|
176
|
+
function bodyByteLength(body) {
|
|
177
|
+
if (body === void 0 || body === null) return void 0;
|
|
178
|
+
if (typeof body === "string") return typeof TextEncoder !== "undefined" ? new TextEncoder().encode(body).length : body.length;
|
|
179
|
+
if (typeof ArrayBuffer !== "undefined") {
|
|
180
|
+
if (body instanceof ArrayBuffer) return body.byteLength;
|
|
181
|
+
if (ArrayBuffer.isView(body)) return body.byteLength;
|
|
182
|
+
}
|
|
183
|
+
const size = body.size;
|
|
184
|
+
return typeof size === "number" ? size : void 0;
|
|
185
|
+
}
|
|
186
|
+
/** A response's `Content-Length`, when it states one. */
|
|
187
|
+
function contentLength(response) {
|
|
188
|
+
const raw = response.headers?.get?.("content-length");
|
|
189
|
+
if (!raw) return void 0;
|
|
190
|
+
const n = Number(raw);
|
|
191
|
+
return Number.isFinite(n) && n >= 0 ? n : void 0;
|
|
192
|
+
}
|
|
193
|
+
/** The paths under the API base URL whose requests get the long deadline. */
|
|
194
|
+
const LONG_API_PATH_PREFIXES = ["/v1/client/ai/"];
|
|
195
|
+
/** Whether a request to API `path` (starting `/v1/...`) gets the long deadline. */
|
|
196
|
+
function isLongApiPath(path) {
|
|
197
|
+
return LONG_API_PATH_PREFIXES.some((prefix) => path.startsWith(prefix));
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Race `work` against a timer of `ms`. When the timer fires first the race
|
|
201
|
+
* rejects with `onTimeout(ms)`, then `controller` is aborted so the runtime
|
|
202
|
+
* can release the connection. The rejection does not wait for `work`, so a
|
|
203
|
+
* fetch the runtime never settles still releases its caller on time. The
|
|
204
|
+
* timer is cleared as soon as either side settles.
|
|
205
|
+
*/
|
|
206
|
+
function raceDeadline(work, ms, controller, onTimeout) {
|
|
207
|
+
let timer;
|
|
208
|
+
const deadline = new Promise((_, reject) => {
|
|
209
|
+
timer = setTimeout(() => {
|
|
210
|
+
reject(onTimeout(ms));
|
|
211
|
+
try {
|
|
212
|
+
controller?.abort();
|
|
213
|
+
} catch {}
|
|
214
|
+
}, ms);
|
|
215
|
+
});
|
|
216
|
+
return Promise.race([work, deadline]).finally(() => {
|
|
217
|
+
clearTimeout(timer);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
function newController() {
|
|
221
|
+
return typeof AbortController === "function" ? new AbortController() : void 0;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Send a request and read its response under deadlines that grow with the
|
|
225
|
+
* bytes involved. Sending, until `send` resolves with the response, gets
|
|
226
|
+
* `timeoutMs` plus the allowance for the request body (and, with
|
|
227
|
+
* `fetchBuffersBody`, for the expected response bytes). Reading,
|
|
228
|
+
* `read(response)`, gets `timeoutMs` plus the allowance for the expected
|
|
229
|
+
* response bytes, else the response's `Content-Length`. One `AbortSignal`
|
|
230
|
+
* covers both phases, so a read past its deadline aborts the transfer too.
|
|
231
|
+
* Past either deadline the promise rejects with `onTimeout(ms)`.
|
|
232
|
+
*/
|
|
233
|
+
async function exchange(timeoutMs, send, read, onTimeout, sizes = {}) {
|
|
234
|
+
const controller = newController();
|
|
235
|
+
const sendMs = withTransferAllowance(timeoutMs, (sizes.requestBytes ?? 0) + (sizes.fetchBuffersBody ? sizes.responseBytes ?? 0 : 0));
|
|
236
|
+
const response = await raceDeadline(send(controller?.signal), sendMs, controller, onTimeout);
|
|
237
|
+
const readMs = withTransferAllowance(timeoutMs, sizes.responseBytes ?? contentLength(response));
|
|
238
|
+
return raceDeadline(read(response), readMs, controller, onTimeout);
|
|
239
|
+
}
|
|
240
|
+
/** `init` with `signal` set, when there is one. */
|
|
241
|
+
function withSignal(init, signal) {
|
|
242
|
+
return signal ? {
|
|
243
|
+
...init,
|
|
244
|
+
signal
|
|
245
|
+
} : init;
|
|
246
|
+
}
|
|
247
|
+
|
|
87
248
|
//#endregion
|
|
88
249
|
//#region src/http-client.ts
|
|
89
250
|
/** Default amba API endpoint. */
|
|
90
251
|
const DEFAULT_BASE_URL = "https://api.amba.dev";
|
|
91
|
-
/**
|
|
92
|
-
|
|
252
|
+
/**
|
|
253
|
+
* Server codes for a session token the server no longer accepts.
|
|
254
|
+
* `INVALID_SESSION` is the code the auth routes that verify the session
|
|
255
|
+
* themselves (`/identifiers`, `/link`, `/unlink`) answer with.
|
|
256
|
+
*/
|
|
257
|
+
const SESSION_REFUSED_CODES = new Set([
|
|
258
|
+
"SESSION_REVOKED",
|
|
259
|
+
"INVALID_SESSION_TOKEN",
|
|
260
|
+
"INVALID_SESSION"
|
|
261
|
+
]);
|
|
262
|
+
/** True for a server error that refused the session token a call sent. */
|
|
263
|
+
function isSessionRefusedError(err) {
|
|
264
|
+
return err instanceof AmbaApiError && SESSION_REFUSED_CODES.has(err.code);
|
|
265
|
+
}
|
|
266
|
+
const AUTH_PATH_PREFIX = "/v1/client/auth/";
|
|
267
|
+
/**
|
|
268
|
+
* The auth routes that read the session from the `Authorization` header and
|
|
269
|
+
* mint nothing: a refused session on them recovers like it does on any
|
|
270
|
+
* other authenticated call. The other auth routes stay out of the
|
|
271
|
+
* recovery: `/refresh` is the recovery itself, `/logout` ends the session,
|
|
272
|
+
* the sign-in, sign-up, OTP and magic-link routes take credentials (a 401
|
|
273
|
+
* there is a credential error), and `/link` and `/unlink` carry the session
|
|
274
|
+
* in their body, so their callers recover and rebuild the body themselves.
|
|
275
|
+
*/
|
|
276
|
+
const SESSION_HEADER_AUTH_PATHS = new Set([`${AUTH_PATH_PREFIX}me`, `${AUTH_PATH_PREFIX}identifiers`]);
|
|
277
|
+
/** Whether a refused session on `path` goes through `onSessionRefused`. */
|
|
278
|
+
function recoversRefusedSession(path) {
|
|
279
|
+
return !path.startsWith(AUTH_PATH_PREFIX) || SESSION_HEADER_AUTH_PATHS.has(path);
|
|
280
|
+
}
|
|
93
281
|
var AmbaHttpClient = class {
|
|
94
282
|
apiKey;
|
|
95
283
|
baseUrl;
|
|
@@ -100,6 +288,12 @@ var AmbaHttpClient = class {
|
|
|
100
288
|
deviceOs;
|
|
101
289
|
identity;
|
|
102
290
|
fetchImpl;
|
|
291
|
+
/** The `fetchImpl` option, when one was passed (`send` resolves fetch per call otherwise). */
|
|
292
|
+
injectedFetch;
|
|
293
|
+
/** Deadline for an API request, in milliseconds. */
|
|
294
|
+
requestTimeoutMs;
|
|
295
|
+
/** Deadline for AI generation, function invocations and media transfers. */
|
|
296
|
+
longRequestTimeoutMs;
|
|
103
297
|
ensureSession;
|
|
104
298
|
onSessionRefused;
|
|
105
299
|
constructor(opts) {
|
|
@@ -113,7 +307,39 @@ var AmbaHttpClient = class {
|
|
|
113
307
|
this.identity = opts.identity;
|
|
114
308
|
this.ensureSession = opts.ensureSession;
|
|
115
309
|
this.onSessionRefused = opts.onSessionRefused;
|
|
310
|
+
this.injectedFetch = opts.fetchImpl;
|
|
116
311
|
this.fetchImpl = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
312
|
+
this.requestTimeoutMs = opts.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
313
|
+
this.longRequestTimeoutMs = opts.longRequestTimeoutMs ?? DEFAULT_LONG_REQUEST_TIMEOUT_MS;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Send a request to an absolute URL outside the API's JSON envelope (a
|
|
317
|
+
* function invocation, a CDN download, the config route that reads the
|
|
318
|
+
* `ETag`) under a deadline. `read` takes what the caller needs from the
|
|
319
|
+
* response (the body as text, a `Blob`) and runs inside the same
|
|
320
|
+
* deadline, so a body that stops arriving fails too.
|
|
321
|
+
*
|
|
322
|
+
* A transport failure rejects with `NETWORK_ERROR` and the message
|
|
323
|
+
* `<label>: <cause>`; the deadline rejects with `NETWORK_ERROR` and
|
|
324
|
+
* `<label>: request timed out after <ms> ms`. `label` defaults to
|
|
325
|
+
* `Network error` and `timeoutMs` to the request deadline; the deadline
|
|
326
|
+
* widens with the request body and with `responseBytes`, the size the
|
|
327
|
+
* caller expects to download (see `exchange`). An `AmbaApiError` that
|
|
328
|
+
* `read` throws passes through unchanged.
|
|
329
|
+
*/
|
|
330
|
+
async send(url, init, read, opts = {}) {
|
|
331
|
+
const timeoutMs = opts.timeoutMs ?? this.requestTimeoutMs;
|
|
332
|
+
const label = opts.label ?? "Network error";
|
|
333
|
+
const fetchFn = this.injectedFetch ?? globalThis.fetch;
|
|
334
|
+
try {
|
|
335
|
+
return await exchange(timeoutMs, (signal) => fetchFn(url, withSignal(init, signal)), read, (ms) => timeoutError(label, ms), {
|
|
336
|
+
requestBytes: bodyByteLength(init.body),
|
|
337
|
+
responseBytes: opts.responseBytes,
|
|
338
|
+
fetchBuffersBody: true
|
|
339
|
+
});
|
|
340
|
+
} catch (err) {
|
|
341
|
+
throw asNetworkError(err, label);
|
|
342
|
+
}
|
|
117
343
|
}
|
|
118
344
|
/** Build the absolute URL for an API path, optionally with a query string. */
|
|
119
345
|
url(path, query) {
|
|
@@ -173,16 +399,21 @@ var AmbaHttpClient = class {
|
|
|
173
399
|
headers,
|
|
174
400
|
body: body === void 0 ? void 0 : JSON.stringify(body ?? {})
|
|
175
401
|
};
|
|
402
|
+
const timeoutMs = isLongApiPath(path) ? this.longRequestTimeoutMs : this.requestTimeoutMs;
|
|
176
403
|
let response;
|
|
404
|
+
let text;
|
|
177
405
|
try {
|
|
178
|
-
response = await this.fetchImpl(url, init)
|
|
406
|
+
({response, text} = await exchange(timeoutMs, (signal) => this.fetchImpl(url, withSignal(init, signal)), async (res) => ({
|
|
407
|
+
response: res,
|
|
408
|
+
text: res.status === 204 ? "" : await res.text()
|
|
409
|
+
}), (ms) => timeoutError("Network error", ms), { requestBytes: bodyByteLength(init.body) }));
|
|
179
410
|
} catch (err) {
|
|
180
|
-
throw
|
|
411
|
+
throw asNetworkError(err, "Network error");
|
|
181
412
|
}
|
|
182
413
|
try {
|
|
183
|
-
return
|
|
414
|
+
return parseResponse(response, text);
|
|
184
415
|
} catch (err) {
|
|
185
|
-
if (!resent && storedToken && this.onSessionRefused &&
|
|
416
|
+
if (!resent && storedToken && this.onSessionRefused && recoversRefusedSession(path) && isSessionRefusedError(err) && await this.recoverSession(storedToken, storedUserId)) return this.request(method, path, body, query, void 0, true);
|
|
186
417
|
throw err;
|
|
187
418
|
}
|
|
188
419
|
}
|
|
@@ -195,9 +426,21 @@ var AmbaHttpClient = class {
|
|
|
195
426
|
}
|
|
196
427
|
}
|
|
197
428
|
};
|
|
198
|
-
|
|
429
|
+
/** The `NETWORK_ERROR` a passed deadline raises. */
|
|
430
|
+
function timeoutError(label, timeoutMs) {
|
|
431
|
+
return new AmbaApiError("NETWORK_ERROR", `${label}: ${timeoutMessage(timeoutMs)}`, { timeoutMs });
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Wrap a transport failure as `NETWORK_ERROR` (`<label>: <cause>`). An
|
|
435
|
+
* `AmbaApiError` (the deadline's own, or one a `send` reader throws) passes
|
|
436
|
+
* through unchanged.
|
|
437
|
+
*/
|
|
438
|
+
function asNetworkError(err, label) {
|
|
439
|
+
if (err instanceof AmbaApiError) return err;
|
|
440
|
+
return new AmbaApiError("NETWORK_ERROR", `${label}: ${err instanceof Error ? err.message : typeof err === "string" ? err : "fetch failed"}`, err);
|
|
441
|
+
}
|
|
442
|
+
function parseResponse(response, text) {
|
|
199
443
|
if (response.status === 204) return null;
|
|
200
|
-
const text = await response.text();
|
|
201
444
|
const isJson = (response.headers.get("content-type") ?? "").includes("application/json");
|
|
202
445
|
if (!response.ok) throw mapHttpError(response, text, isJson);
|
|
203
446
|
if (text === "") return null;
|
|
@@ -893,7 +1136,16 @@ const DETERMINISTIC_ERROR_CODES = new Set([
|
|
|
893
1136
|
"NOT_FOUND",
|
|
894
1137
|
"CONFLICT",
|
|
895
1138
|
"CONSENT_NOT_GRANTED",
|
|
896
|
-
"NOT_INITIALIZED"
|
|
1139
|
+
"NOT_INITIALIZED",
|
|
1140
|
+
"INVALID_JSON",
|
|
1141
|
+
"INVALID_BODY",
|
|
1142
|
+
"INVALID_EVENT",
|
|
1143
|
+
"RESERVED_EVENT_NAME",
|
|
1144
|
+
"INVALID_IDEMPOTENCY_KEY",
|
|
1145
|
+
"INVALID_TIMESTAMP",
|
|
1146
|
+
"INVALID_PROPERTIES",
|
|
1147
|
+
"INVALID_TELEMETRY",
|
|
1148
|
+
"IDEMPOTENCY_KEY_CONFLICT"
|
|
897
1149
|
]);
|
|
898
1150
|
/**
|
|
899
1151
|
* Is this a deterministic server rejection — a coded error the server would
|
|
@@ -931,13 +1183,34 @@ function isOfflineError(err) {
|
|
|
931
1183
|
if (typeof msg === "string" && /network (error|request failed)|failed to fetch|fetch failed|load failed/i.test(msg)) return true;
|
|
932
1184
|
return !isOnline();
|
|
933
1185
|
}
|
|
934
|
-
/**
|
|
1186
|
+
/**
|
|
1187
|
+
* Generate a stable idempotency key for a queued op: `amba_obx_` and a random
|
|
1188
|
+
* UUID. The server records event keys project-wide, so the UUID comes from
|
|
1189
|
+
* the runtime's secure random source: `crypto.randomUUID`, else
|
|
1190
|
+
* `crypto.getRandomValues` (Hermes has only the latter).
|
|
1191
|
+
*/
|
|
935
1192
|
function newIdempotencyKey() {
|
|
936
1193
|
const c = typeof globalThis !== "undefined" ? globalThis.crypto : void 0;
|
|
937
1194
|
if (c && typeof c.randomUUID === "function") return `amba_obx_${c.randomUUID()}`;
|
|
1195
|
+
if (c && typeof c.getRandomValues === "function") {
|
|
1196
|
+
const b = c.getRandomValues(new Uint8Array(16));
|
|
1197
|
+
b[6] = b[6] & 15 | 64;
|
|
1198
|
+
b[8] = b[8] & 63 | 128;
|
|
1199
|
+
const h = Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
|
1200
|
+
return `amba_obx_${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
1201
|
+
}
|
|
938
1202
|
return `amba_obx_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
|
|
939
1203
|
}
|
|
940
1204
|
/**
|
|
1205
|
+
* Whether a `track` with these properties can take the events route's keyed
|
|
1206
|
+
* path, which accepts a plain object or no properties. A JS caller can pass
|
|
1207
|
+
* something else (an array); such an event stays on the unkeyed path, which
|
|
1208
|
+
* stores it as it always has.
|
|
1209
|
+
*/
|
|
1210
|
+
function keyableProperties(properties) {
|
|
1211
|
+
return properties === void 0 || properties === null || typeof properties === "object" && !Array.isArray(properties);
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
941
1214
|
* Durable, ordered, idempotent offline outbox. Opt-in via `Amba.offline`.
|
|
942
1215
|
* Single in-flight flush is enforced so concurrent reconnect signals don't
|
|
943
1216
|
* double-replay.
|
|
@@ -2208,6 +2481,7 @@ var CollectionSubscription = class {
|
|
|
2208
2481
|
return openStream({
|
|
2209
2482
|
url: `${resolveRealtimeBaseUrl(this.ctx.baseUrl)}/v1/client/realtime/collections/${pathSegment(this.name)}${qs}`,
|
|
2210
2483
|
authHeaders: this.ctx.authHeaders,
|
|
2484
|
+
connectTimeoutMs: this.ctx.connectTimeoutMs,
|
|
2211
2485
|
onChange: cb,
|
|
2212
2486
|
options,
|
|
2213
2487
|
parse: parseRowChangeFrame
|
|
@@ -2232,6 +2506,7 @@ var ConversationSubscription = class {
|
|
|
2232
2506
|
return openStream({
|
|
2233
2507
|
url: `${resolveRealtimeBaseUrl(this.ctx.baseUrl)}/v1/client/realtime/conversations/${pathSegment(this.conversationId)}`,
|
|
2234
2508
|
authHeaders: this.ctx.authHeaders,
|
|
2509
|
+
connectTimeoutMs: this.ctx.connectTimeoutMs,
|
|
2235
2510
|
onChange: cb,
|
|
2236
2511
|
options,
|
|
2237
2512
|
parse: parseRowChangeFrame
|
|
@@ -2256,6 +2531,7 @@ var GamificationSubscription = class {
|
|
|
2256
2531
|
return openStream({
|
|
2257
2532
|
url: `${resolveRealtimeBaseUrl(this.ctx.baseUrl)}/v1/client/realtime/gamification`,
|
|
2258
2533
|
authHeaders: this.ctx.authHeaders,
|
|
2534
|
+
connectTimeoutMs: this.ctx.connectTimeoutMs,
|
|
2259
2535
|
onChange: cb,
|
|
2260
2536
|
options,
|
|
2261
2537
|
parse: parseGamificationFrame
|
|
@@ -2290,6 +2566,7 @@ function openStream(args) {
|
|
|
2290
2566
|
closeActive = connectOnce({
|
|
2291
2567
|
url: args.url,
|
|
2292
2568
|
headers,
|
|
2569
|
+
connectTimeoutMs: args.connectTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
2293
2570
|
onFrame: (frame) => {
|
|
2294
2571
|
const ev = args.parse(frame);
|
|
2295
2572
|
if (ev !== null) args.onChange(ev);
|
|
@@ -2360,8 +2637,13 @@ function connectXhr(args, XHR) {
|
|
|
2360
2637
|
xhr.open("GET", args.url, true);
|
|
2361
2638
|
for (const [k, v] of Object.entries(args.headers)) xhr.setRequestHeader(k, v);
|
|
2362
2639
|
xhr.setRequestHeader("Accept", "text/event-stream");
|
|
2640
|
+
const connectTimer = setTimeout(() => {
|
|
2641
|
+
if (aborted || xhr.readyState >= 2) return;
|
|
2642
|
+
args.onError(/* @__PURE__ */ new Error(`realtime connection timed out after ${args.connectTimeoutMs} ms`));
|
|
2643
|
+
}, args.connectTimeoutMs);
|
|
2363
2644
|
xhr.onreadystatechange = () => {
|
|
2364
2645
|
if (aborted) return;
|
|
2646
|
+
if (xhr.readyState >= 2) clearTimeout(connectTimer);
|
|
2365
2647
|
if (xhr.readyState === 3) {
|
|
2366
2648
|
if (xhr.status >= 400) return;
|
|
2367
2649
|
drain();
|
|
@@ -2385,10 +2667,12 @@ function connectXhr(args, XHR) {
|
|
|
2385
2667
|
try {
|
|
2386
2668
|
xhr.send();
|
|
2387
2669
|
} catch (err) {
|
|
2670
|
+
clearTimeout(connectTimer);
|
|
2388
2671
|
args.onError(err);
|
|
2389
2672
|
}
|
|
2390
2673
|
return () => {
|
|
2391
2674
|
aborted = true;
|
|
2675
|
+
clearTimeout(connectTimer);
|
|
2392
2676
|
try {
|
|
2393
2677
|
xhr.abort();
|
|
2394
2678
|
} catch {}
|
|
@@ -2403,14 +2687,14 @@ function connectFetchStream(args) {
|
|
|
2403
2687
|
let aborted = false;
|
|
2404
2688
|
(async () => {
|
|
2405
2689
|
try {
|
|
2406
|
-
const resp = await fetch(args.url, {
|
|
2690
|
+
const resp = await raceDeadline(fetch(args.url, {
|
|
2407
2691
|
method: "GET",
|
|
2408
2692
|
headers: {
|
|
2409
2693
|
...args.headers,
|
|
2410
2694
|
Accept: "text/event-stream"
|
|
2411
2695
|
},
|
|
2412
2696
|
signal: controller.signal
|
|
2413
|
-
});
|
|
2697
|
+
}), args.connectTimeoutMs, controller, (ms) => /* @__PURE__ */ new Error(`realtime connection timed out after ${ms} ms`));
|
|
2414
2698
|
if (!resp.ok || !resp.body) throw new Error(`realtime connect failed: HTTP ${resp.status}`);
|
|
2415
2699
|
const reader = resp.body.getReader();
|
|
2416
2700
|
const decoder = new TextDecoder();
|
|
@@ -2490,7 +2774,7 @@ function delay(ms) {
|
|
|
2490
2774
|
//#endregion
|
|
2491
2775
|
//#region src/index.ts
|
|
2492
2776
|
/** SDK semver — burned in at build time. Bump alongside package.json. */
|
|
2493
|
-
const SDK_VERSION = "4.2.
|
|
2777
|
+
const SDK_VERSION = "4.2.2";
|
|
2494
2778
|
/**
|
|
2495
2779
|
* Default host that serves deployed functions. `Amba.functions.invoke(name)`
|
|
2496
2780
|
* targets `https://<projectId>.<DEFAULT_FUNCTIONS_HOST>/<name>` unless a
|
|
@@ -2776,7 +3060,11 @@ function trackLink(state_, link) {
|
|
|
2776
3060
|
}
|
|
2777
3061
|
/**
|
|
2778
3062
|
* After the server refused the stored session token on an authenticated
|
|
2779
|
-
* call (`SESSION_REVOKED`, `INVALID_SESSION_TOKEN`).
|
|
3063
|
+
* call (`SESSION_REVOKED`, `INVALID_SESSION_TOKEN`, `INVALID_SESSION`). The
|
|
3064
|
+
* HTTP client runs it for calls that send the session in the
|
|
3065
|
+
* `Authorization` header, `/v1/client/auth/me` and `/identifiers`
|
|
3066
|
+
* included; `withBodySession` runs it for the link and unlink calls, which
|
|
3067
|
+
* send the session in their body. True when a usable
|
|
2780
3068
|
* session for the user the call was sent for is stored afterwards, so the
|
|
2781
3069
|
* call is sent once more as that user:
|
|
2782
3070
|
* - a link in flight on this device stores the pair it returns, which is
|
|
@@ -2810,6 +3098,40 @@ async function recoverRefusedSession(state_, usedToken, usedUserId) {
|
|
|
2810
3098
|
return usableForCaller(state_.identity.getSessionToken());
|
|
2811
3099
|
}
|
|
2812
3100
|
/**
|
|
3101
|
+
* Send an auth call that carries the stored session token in its body
|
|
3102
|
+
* (`/link`, `/unlink`). `send` builds the request from the token it is
|
|
3103
|
+
* given. When the server refuses that token, the session recovers through
|
|
3104
|
+
* `recoverRefusedSession` (one refresh shared with every other refused
|
|
3105
|
+
* call) and the call is sent once more with the recovered token; a second
|
|
3106
|
+
* refusal reaches the app. A link waits here outside `trackLink`: its own
|
|
3107
|
+
* failed attempt has already left `linksInflight` (the removal is the first
|
|
3108
|
+
* reaction on that promise), so recovery never waits on itself.
|
|
3109
|
+
*/
|
|
3110
|
+
async function withBodySession(state_, send) {
|
|
3111
|
+
const usedToken = state_.identity.getSessionToken();
|
|
3112
|
+
if (!usedToken) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
|
|
3113
|
+
const usedUserId = state_.identity.getAppUserId();
|
|
3114
|
+
try {
|
|
3115
|
+
return await send(usedToken);
|
|
3116
|
+
} catch (err) {
|
|
3117
|
+
if (!isSessionRefusedError(err)) throw err;
|
|
3118
|
+
if (!await recoverRefusedSession(state_, usedToken, usedUserId)) throw err;
|
|
3119
|
+
const recovered = state_.identity.getSessionToken();
|
|
3120
|
+
if (!recovered) throw err;
|
|
3121
|
+
return send(recovered);
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
/**
|
|
3125
|
+
* Before a read of the current session (`me`, `getSession`,
|
|
3126
|
+
* `getCurrentIdentifiers`) while no session token is stored: wait for the
|
|
3127
|
+
* launch restore or sign-in in flight, so a check made at launch sees the
|
|
3128
|
+
* session it restores.
|
|
3129
|
+
*/
|
|
3130
|
+
async function settleSessionInFlight(state_) {
|
|
3131
|
+
if (state_.identity.getSessionToken() || !state_.anonSignInInflight) return;
|
|
3132
|
+
await state_.anonSignInInflight.catch(() => {});
|
|
3133
|
+
}
|
|
3134
|
+
/**
|
|
2813
3135
|
* Continue the stored session with its refresh token. True when it did.
|
|
2814
3136
|
* False when there is no refresh token, or the server rejected it: the
|
|
2815
3137
|
* session is then cleared (the anonymous id rotates unless the session was
|
|
@@ -2920,7 +3242,10 @@ async function refreshSessionOnce(state_) {
|
|
|
2920
3242
|
* account's rotated anonymous id is adopted.
|
|
2921
3243
|
*/
|
|
2922
3244
|
function postLink(state_, body) {
|
|
2923
|
-
return trackLink(state_, postLinkOnce(state_,
|
|
3245
|
+
return withBodySession(state_, (sessionToken) => trackLink(state_, postLinkOnce(state_, {
|
|
3246
|
+
...body,
|
|
3247
|
+
session_token: sessionToken
|
|
3248
|
+
})));
|
|
2924
3249
|
}
|
|
2925
3250
|
async function postLinkOnce(state_, body) {
|
|
2926
3251
|
const env = await state_.http.post(P.authLink, {
|
|
@@ -2956,12 +3281,10 @@ async function postUserNoRotate(state_, path, body) {
|
|
|
2956
3281
|
}
|
|
2957
3282
|
async function unlinkProvider(provider) {
|
|
2958
3283
|
const s = getState();
|
|
2959
|
-
|
|
2960
|
-
if (!sessionToken) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
|
|
2961
|
-
return postUserNoRotate(s, P.authUnlink, {
|
|
3284
|
+
return withBodySession(s, (sessionToken) => postUserNoRotate(s, P.authUnlink, {
|
|
2962
3285
|
provider,
|
|
2963
3286
|
session_token: sessionToken
|
|
2964
|
-
});
|
|
3287
|
+
}));
|
|
2965
3288
|
}
|
|
2966
3289
|
function requireEmail(email) {
|
|
2967
3290
|
const trimmed = email.trim();
|
|
@@ -3101,9 +3424,28 @@ function offlineStore(s) {
|
|
|
3101
3424
|
};
|
|
3102
3425
|
}
|
|
3103
3426
|
/**
|
|
3427
|
+
* Whether a live `track` (outbox on) carries its idempotency key:
|
|
3428
|
+
* engagement events whose properties the keyed path takes. Telemetry stays
|
|
3429
|
+
* on the unkeyed hot path, which writes no idempotency record and answers a
|
|
3430
|
+
* failed telemetry insert with 200; its replays from the outbox are keyed.
|
|
3431
|
+
*/
|
|
3432
|
+
function keyedLiveTrack(properties, telemetry) {
|
|
3433
|
+
return telemetry !== true && keyableProperties(properties);
|
|
3434
|
+
}
|
|
3435
|
+
/**
|
|
3436
|
+
* A JSON copy of `properties`: exactly what a request body serializes, and
|
|
3437
|
+
* immune to the app changing the object after `track()` returns.
|
|
3438
|
+
*/
|
|
3439
|
+
function snapshotProperties(properties) {
|
|
3440
|
+
if (properties === void 0 || properties === null) return void 0;
|
|
3441
|
+
const text = JSON.stringify(properties);
|
|
3442
|
+
return text === void 0 ? void 0 : JSON.parse(text);
|
|
3443
|
+
}
|
|
3444
|
+
/**
|
|
3104
3445
|
* Replay one queued mutation against the live API, forwarding the op's
|
|
3105
|
-
* idempotency key on collection inserts so a write that
|
|
3106
|
-
* connection dropped is never double-applied (API
|
|
3446
|
+
* idempotency key on collection inserts and tracked events so a write that
|
|
3447
|
+
* landed before the connection dropped is never double-applied (API
|
|
3448
|
+
* migration 056 for collections, `client_event_idempotency` for events).
|
|
3107
3449
|
*/
|
|
3108
3450
|
async function replayOutboxOp(s, op) {
|
|
3109
3451
|
switch (op.kind) {
|
|
@@ -3125,8 +3467,9 @@ async function replayOutboxOp(s, op) {
|
|
|
3125
3467
|
event: op.event,
|
|
3126
3468
|
properties: op.properties ?? void 0,
|
|
3127
3469
|
anonymous_id: s.identity.getAnonymousId(),
|
|
3128
|
-
timestamp: op.occurredAt ??
|
|
3129
|
-
...op.telemetry ? { telemetry: true } : {}
|
|
3470
|
+
timestamp: op.occurredAt ?? op.enqueuedAt,
|
|
3471
|
+
...op.telemetry ? { telemetry: true } : {},
|
|
3472
|
+
...keyableProperties(op.properties) ? { idempotency_key: op.idempotencyKey } : {}
|
|
3130
3473
|
});
|
|
3131
3474
|
} catch (err) {
|
|
3132
3475
|
const code = err?.code;
|
|
@@ -3208,6 +3551,7 @@ const Amba = {
|
|
|
3208
3551
|
if (!Number.isInteger(flushIntervalMs) || flushIntervalMs < 0) throw new Error("Amba.configure: events.flushIntervalMs must be a non-negative integer");
|
|
3209
3552
|
if (!Number.isInteger(maxQueue) || maxQueue < 1) throw new Error("Amba.configure: events.maxQueue must be positive");
|
|
3210
3553
|
}
|
|
3554
|
+
const deadlines = resolveRequestDeadlines(config);
|
|
3211
3555
|
const tokenStorage = config.tokenStorage ?? "secure";
|
|
3212
3556
|
if (!TOKEN_STORAGE_MODES.includes(tokenStorage)) throw new Error("Amba.configure: tokenStorage must be \"secure\", \"memory\" or \"asyncStorage\"");
|
|
3213
3557
|
const secureStorage = config.secureStorage;
|
|
@@ -3233,6 +3577,8 @@ const Amba = {
|
|
|
3233
3577
|
deviceRegion: detectDeviceRegion(),
|
|
3234
3578
|
deviceOs: detectDeviceOs(),
|
|
3235
3579
|
identity,
|
|
3580
|
+
requestTimeoutMs: deadlines.requestMs,
|
|
3581
|
+
longRequestTimeoutMs: deadlines.longMs,
|
|
3236
3582
|
ensureSession: config.autoAnonymousSession === false ? void 0 : async (path) => {
|
|
3237
3583
|
if (pathSkipsLazySession(path)) return;
|
|
3238
3584
|
await ensureAnonymousSession(getState());
|
|
@@ -3313,12 +3659,14 @@ const Amba = {
|
|
|
3313
3659
|
},
|
|
3314
3660
|
collection: (name) => new CollectionSubscription({
|
|
3315
3661
|
baseUrl: getState().baseUrl,
|
|
3316
|
-
authHeaders: realtimeAuthHeaders
|
|
3662
|
+
authHeaders: realtimeAuthHeaders,
|
|
3663
|
+
connectTimeoutMs: getState().http.requestTimeoutMs
|
|
3317
3664
|
}, name),
|
|
3318
3665
|
get gamification() {
|
|
3319
3666
|
return new GamificationSubscription({
|
|
3320
3667
|
baseUrl: getState().baseUrl,
|
|
3321
|
-
authHeaders: realtimeAuthHeaders
|
|
3668
|
+
authHeaders: realtimeAuthHeaders,
|
|
3669
|
+
connectTimeoutMs: getState().http.requestTimeoutMs
|
|
3322
3670
|
});
|
|
3323
3671
|
},
|
|
3324
3672
|
events: {
|
|
@@ -3326,13 +3674,16 @@ const Amba = {
|
|
|
3326
3674
|
const s = getState();
|
|
3327
3675
|
if (s.eventQueue && !options?.immediate) return s.eventQueue.enqueue(event, properties, options?.telemetry === true);
|
|
3328
3676
|
const occurredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3677
|
+
const props = s.offlineEnabled ? snapshotProperties(properties) : properties;
|
|
3329
3678
|
const body = {
|
|
3330
3679
|
event,
|
|
3331
|
-
properties:
|
|
3680
|
+
properties: props ?? void 0,
|
|
3332
3681
|
anonymous_id: s.identity.getAnonymousId(),
|
|
3333
3682
|
timestamp: occurredAt
|
|
3334
3683
|
};
|
|
3335
3684
|
if (options?.telemetry) body["telemetry"] = true;
|
|
3685
|
+
const idempotencyKey = s.offlineEnabled ? newIdempotencyKey() : void 0;
|
|
3686
|
+
if (idempotencyKey && keyedLiveTrack(props, options?.telemetry)) body["idempotency_key"] = idempotencyKey;
|
|
3336
3687
|
const outboxTicket = s.offlineEnabled ? getOfflineQueue(s).ticket() : void 0;
|
|
3337
3688
|
try {
|
|
3338
3689
|
await s.http.post(P.events, body);
|
|
@@ -3341,9 +3692,10 @@ const Amba = {
|
|
|
3341
3692
|
await getOfflineQueue(s).enqueue({
|
|
3342
3693
|
kind: "track",
|
|
3343
3694
|
event,
|
|
3344
|
-
properties,
|
|
3695
|
+
properties: props,
|
|
3345
3696
|
telemetry: options?.telemetry,
|
|
3346
|
-
occurredAt
|
|
3697
|
+
occurredAt,
|
|
3698
|
+
idempotencyKey
|
|
3347
3699
|
}, outboxTicket);
|
|
3348
3700
|
return;
|
|
3349
3701
|
}
|
|
@@ -3446,52 +3798,38 @@ const Amba = {
|
|
|
3446
3798
|
if (providerTrimmed === "") throw new AmbaApiError("VALIDATION_ERROR", "provider must not be empty");
|
|
3447
3799
|
if (typeof credential !== "string" || credential.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "credential must not be empty");
|
|
3448
3800
|
const s = getState();
|
|
3449
|
-
|
|
3450
|
-
if (!sessionToken) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
|
|
3451
|
-
return trackLink(s, postAuth(s, P.authLink, {
|
|
3801
|
+
return withBodySession(s, (sessionToken) => trackLink(s, postAuth(s, P.authLink, {
|
|
3452
3802
|
provider: providerTrimmed,
|
|
3453
3803
|
token: credential,
|
|
3454
3804
|
session_token: sessionToken,
|
|
3455
3805
|
...nonceField(options)
|
|
3456
|
-
}));
|
|
3806
|
+
})));
|
|
3457
3807
|
},
|
|
3458
3808
|
linkSmsOtp: async (phone, code) => {
|
|
3459
3809
|
requireE164Phone(phone);
|
|
3460
3810
|
requireOtpCode(code);
|
|
3461
|
-
|
|
3462
|
-
const sessionToken = s.identity.getSessionToken();
|
|
3463
|
-
if (!sessionToken) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
|
|
3464
|
-
return postLink(s, {
|
|
3811
|
+
return postLink(getState(), {
|
|
3465
3812
|
provider: "sms_otp",
|
|
3466
3813
|
phone,
|
|
3467
|
-
code
|
|
3468
|
-
session_token: sessionToken
|
|
3814
|
+
code
|
|
3469
3815
|
});
|
|
3470
3816
|
},
|
|
3471
3817
|
linkEmailOtp: async (email, code) => {
|
|
3472
3818
|
requireEmail(email);
|
|
3473
3819
|
requireOtpCode(code);
|
|
3474
|
-
|
|
3475
|
-
const sessionToken = s.identity.getSessionToken();
|
|
3476
|
-
if (!sessionToken) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
|
|
3477
|
-
return postLink(s, {
|
|
3820
|
+
return postLink(getState(), {
|
|
3478
3821
|
provider: "email_otp",
|
|
3479
3822
|
email,
|
|
3480
|
-
code
|
|
3481
|
-
session_token: sessionToken
|
|
3823
|
+
code
|
|
3482
3824
|
});
|
|
3483
3825
|
},
|
|
3484
3826
|
linkEmailPassword: async (email, password) => {
|
|
3485
3827
|
requireEmail(email);
|
|
3486
3828
|
requirePassword(password);
|
|
3487
|
-
|
|
3488
|
-
const sessionToken = s.identity.getSessionToken();
|
|
3489
|
-
if (!sessionToken) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
|
|
3490
|
-
return postLink(s, {
|
|
3829
|
+
return postLink(getState(), {
|
|
3491
3830
|
provider: "email_password",
|
|
3492
3831
|
email,
|
|
3493
|
-
password
|
|
3494
|
-
session_token: sessionToken
|
|
3832
|
+
password
|
|
3495
3833
|
});
|
|
3496
3834
|
},
|
|
3497
3835
|
unlinkSmsOtp: async () => unlinkProvider("sms_otp"),
|
|
@@ -3500,6 +3838,7 @@ const Amba = {
|
|
|
3500
3838
|
unlinkGoogle: async () => unlinkProvider("google"),
|
|
3501
3839
|
getCurrentIdentifiers: async () => {
|
|
3502
3840
|
const s = getState();
|
|
3841
|
+
await settleSessionInFlight(s);
|
|
3503
3842
|
if (!s.identity.getSessionToken()) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
|
|
3504
3843
|
return (await s.http.get(P.authIdentifiers)).data.identifiers;
|
|
3505
3844
|
},
|
|
@@ -3526,13 +3865,20 @@ const Amba = {
|
|
|
3526
3865
|
refresh: async () => refreshSession(getState()),
|
|
3527
3866
|
me: async () => {
|
|
3528
3867
|
const s = getState();
|
|
3868
|
+
await settleSessionInFlight(s);
|
|
3529
3869
|
if (!s.identity.getSessionToken()) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
|
|
3530
3870
|
return normalizeUserAuthProviders((await s.http.get(P.authMe)).data);
|
|
3531
3871
|
},
|
|
3532
3872
|
getSession: async () => {
|
|
3533
3873
|
const s = getState();
|
|
3874
|
+
await settleSessionInFlight(s);
|
|
3534
3875
|
if (!s.identity.isAuthenticated()) return null;
|
|
3535
|
-
|
|
3876
|
+
try {
|
|
3877
|
+
return snapshotSession(normalizeUserAuthProviders((await s.http.get(P.authMe)).data));
|
|
3878
|
+
} catch (err) {
|
|
3879
|
+
if (isSessionRefusedError(err) && !s.identity.isAuthenticated()) return null;
|
|
3880
|
+
throw err;
|
|
3881
|
+
}
|
|
3536
3882
|
},
|
|
3537
3883
|
getAnonymousId: async () => {
|
|
3538
3884
|
return getState().identity.getAnonymousId();
|
|
@@ -3805,21 +4151,25 @@ const Amba = {
|
|
|
3805
4151
|
await getState().http.delete(`${P.storage}/${encodePathSegment(assetId)}`);
|
|
3806
4152
|
},
|
|
3807
4153
|
download: async (assetId) => {
|
|
3808
|
-
const
|
|
4154
|
+
const s = getState();
|
|
4155
|
+
const assetEnv = await s.http.get(`${P.storage}/${encodePathSegment(assetId)}`);
|
|
3809
4156
|
const cdnUrl = assetEnv.data.url ?? assetEnv.data.storage_url ?? "";
|
|
3810
4157
|
if (!cdnUrl) throw new AmbaApiError("NOT_FOUND", "asset has no resolvable CDN url");
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
response
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
}
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
4158
|
+
const sizeBytes = assetEnv.data.size_bytes;
|
|
4159
|
+
const { response, blob, text } = await s.http.send(cdnUrl, { method: "GET" }, async (res) => res.ok ? {
|
|
4160
|
+
response: res,
|
|
4161
|
+
blob: await res.blob(),
|
|
4162
|
+
text: ""
|
|
4163
|
+
} : {
|
|
4164
|
+
response: res,
|
|
4165
|
+
blob: null,
|
|
4166
|
+
text: await res.text()
|
|
4167
|
+
}, {
|
|
4168
|
+
timeoutMs: s.http.longRequestTimeoutMs,
|
|
4169
|
+
responseBytes: typeof sizeBytes === "number" ? sizeBytes : void 0
|
|
4170
|
+
});
|
|
4171
|
+
if (!blob) throw mapHttpError(response, text, (response.headers.get("content-type") ?? "").includes("application/json"));
|
|
4172
|
+
return blob;
|
|
3823
4173
|
}
|
|
3824
4174
|
},
|
|
3825
4175
|
push: {
|
|
@@ -3867,27 +4217,30 @@ const Amba = {
|
|
|
3867
4217
|
functions: { invoke: async (name, body, options) => {
|
|
3868
4218
|
const s = getState();
|
|
3869
4219
|
if (!name || name.trim() === "") throw new AmbaApiError("INVALID_ARGUMENT", "Amba.functions.invoke: a function name is required.");
|
|
4220
|
+
let timeoutMs;
|
|
4221
|
+
try {
|
|
4222
|
+
timeoutMs = readTimeout("Amba.functions.invoke: timeoutMs", options?.timeoutMs, s.http.longRequestTimeoutMs);
|
|
4223
|
+
} catch (err) {
|
|
4224
|
+
throw new AmbaApiError("INVALID_ARGUMENT", err.message);
|
|
4225
|
+
}
|
|
3870
4226
|
const url = await resolveFunctionUrl(s, name);
|
|
3871
4227
|
const method = options?.method ?? "POST";
|
|
3872
4228
|
const headers = s.http.buildHeaders(options?.headers);
|
|
3873
4229
|
let requestBody;
|
|
3874
4230
|
if (body !== void 0 && method !== "GET") requestBody = JSON.stringify(body);
|
|
3875
4231
|
else delete headers["Content-Type"];
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
}
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
if (!response.ok)
|
|
3888
|
-
const ct = response.headers.get("content-type") ?? "";
|
|
3889
|
-
throw mapHttpError(response, text, ct.includes("application/json"));
|
|
3890
|
-
}
|
|
4232
|
+
const { response, text } = await s.http.send(url, {
|
|
4233
|
+
method,
|
|
4234
|
+
headers,
|
|
4235
|
+
body: requestBody
|
|
4236
|
+
}, async (res) => ({
|
|
4237
|
+
response: res,
|
|
4238
|
+
text: await res.text()
|
|
4239
|
+
}), {
|
|
4240
|
+
timeoutMs,
|
|
4241
|
+
label: `Amba.functions.invoke("${name}") network error`
|
|
4242
|
+
});
|
|
4243
|
+
if (!response.ok) throw mapHttpError(response, text, (response.headers.get("content-type") ?? "").includes("application/json"));
|
|
3891
4244
|
if (!text) return void 0;
|
|
3892
4245
|
try {
|
|
3893
4246
|
return JSON.parse(text);
|
|
@@ -3901,20 +4254,14 @@ const Amba = {
|
|
|
3901
4254
|
const ticket = s.remoteValues.ticket();
|
|
3902
4255
|
const url = s.http.url(P.config);
|
|
3903
4256
|
const headers = s.http.buildHeaders();
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
}
|
|
3911
|
-
|
|
3912
|
-
}
|
|
3913
|
-
const text = await response.text();
|
|
3914
|
-
if (!response.ok) {
|
|
3915
|
-
const ct = response.headers.get("content-type") ?? "";
|
|
3916
|
-
throw mapHttpError(response, text, ct.includes("application/json"));
|
|
3917
|
-
}
|
|
4257
|
+
const { response, text } = await s.http.send(url, {
|
|
4258
|
+
method: "GET",
|
|
4259
|
+
headers
|
|
4260
|
+
}, async (res) => ({
|
|
4261
|
+
response: res,
|
|
4262
|
+
text: await res.text()
|
|
4263
|
+
}));
|
|
4264
|
+
if (!response.ok) throw mapHttpError(response, text, (response.headers.get("content-type") ?? "").includes("application/json"));
|
|
3918
4265
|
let parsed = {};
|
|
3919
4266
|
if (text) try {
|
|
3920
4267
|
parsed = JSON.parse(text);
|
|
@@ -4137,7 +4484,8 @@ const Amba = {
|
|
|
4137
4484
|
messaging: {
|
|
4138
4485
|
conversation: (conversationId) => new ConversationSubscription({
|
|
4139
4486
|
baseUrl: getState().baseUrl,
|
|
4140
|
-
authHeaders: realtimeAuthHeaders
|
|
4487
|
+
authHeaders: realtimeAuthHeaders,
|
|
4488
|
+
connectTimeoutMs: getState().http.requestTimeoutMs
|
|
4141
4489
|
}, conversationId),
|
|
4142
4490
|
createConversation: async (request) => {
|
|
4143
4491
|
return (await getState().http.post(`${P.messaging}/conversations`, request)).data;
|