@layers/amba-react-native 4.2.0 → 4.2.1
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 +350 -62
- 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 +350 -62
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -90,6 +90,167 @@ var AmbaApiError = class extends Error {
|
|
|
90
90
|
}
|
|
91
91
|
};
|
|
92
92
|
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/request-deadline.ts
|
|
95
|
+
/**
|
|
96
|
+
* Request deadlines, shared by every JS SDK target (the same file ships in
|
|
97
|
+
* `@layers/amba-web`, `@layers/amba-node` and `@layers/amba-react-native`).
|
|
98
|
+
*
|
|
99
|
+
* Every request the SDK sends runs under a deadline that covers sending it
|
|
100
|
+
* and reading the whole response. When the deadline passes, the request is
|
|
101
|
+
* aborted and fails with the SDK's retryable network error, so one request
|
|
102
|
+
* can never hold an in-flight slot (the offline outbox flush, the lazy
|
|
103
|
+
* anonymous sign-in, the session refresh, the event batch flush) for the
|
|
104
|
+
* life of the process. Some runtimes never settle a fetch they gave up on:
|
|
105
|
+
* Expo on Android drops a failed DNS lookup and the promise stays pending.
|
|
106
|
+
* The deadline wins the race even then, because it rejects on its own timer
|
|
107
|
+
* and then aborts the fetch for the runtime to clean up.
|
|
108
|
+
*
|
|
109
|
+
* Two bounds:
|
|
110
|
+
* - `requestTimeoutMs` (default 15 s): an ordinary API request.
|
|
111
|
+
* - `longRequestTimeoutMs` (default 5 min): work that legitimately runs
|
|
112
|
+
* longer. AI generation (`/v1/client/ai/*`), function invocations, and
|
|
113
|
+
* media uploads and downloads.
|
|
114
|
+
* Both grow with the bytes a request moves (see `withTransferAllowance`), so
|
|
115
|
+
* a large upload, download or event batch on a slow link has time to finish.
|
|
116
|
+
* Realtime subscriptions are bounded until their stream opens; AI response
|
|
117
|
+
* streams likewise until the response starts. Once open, a stream runs for
|
|
118
|
+
* as long as the caller keeps it.
|
|
119
|
+
*/
|
|
120
|
+
/** Default deadline for one API request, in milliseconds. */
|
|
121
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 15e3;
|
|
122
|
+
/**
|
|
123
|
+
* Default deadline for AI generation, function invocations and media
|
|
124
|
+
* transfers, in milliseconds.
|
|
125
|
+
*/
|
|
126
|
+
const DEFAULT_LONG_REQUEST_TIMEOUT_MS = 3e5;
|
|
127
|
+
/**
|
|
128
|
+
* The slowest link a deadline makes room for, in bytes per second
|
|
129
|
+
* (8 KiB/s, about 64 kbit/s). Each byte a request sends or receives beyond
|
|
130
|
+
* the first {@link TRANSFER_BYTES_WITHIN_BASE} adds the time it takes at this
|
|
131
|
+
* rate to the deadline.
|
|
132
|
+
*/
|
|
133
|
+
const MIN_TRANSFER_BYTES_PER_SECOND = 8 * 1024;
|
|
134
|
+
/**
|
|
135
|
+
* Bytes a request moves within its base deadline (64 KiB: 8 s at the
|
|
136
|
+
* slowest rate). An ordinary API request stays at exactly
|
|
137
|
+
* `requestTimeoutMs`; only larger bodies widen it.
|
|
138
|
+
*/
|
|
139
|
+
const TRANSFER_BYTES_WITHIN_BASE = 64 * 1024;
|
|
140
|
+
/** The largest delay `setTimeout` honors (about 24.8 days). */
|
|
141
|
+
const MAX_TIMEOUT_MS = 2147483647;
|
|
142
|
+
/**
|
|
143
|
+
* Read the deadlines from the configure options, applying the defaults.
|
|
144
|
+
* Throws an `Error` naming the option when a value is not a positive number
|
|
145
|
+
* of milliseconds.
|
|
146
|
+
*/
|
|
147
|
+
function resolveRequestDeadlines(options) {
|
|
148
|
+
return {
|
|
149
|
+
requestMs: readTimeout("Amba.configure: requestTimeoutMs", options.requestTimeoutMs, DEFAULT_REQUEST_TIMEOUT_MS),
|
|
150
|
+
longMs: readTimeout("Amba.configure: longRequestTimeoutMs", options.longRequestTimeoutMs, DEFAULT_LONG_REQUEST_TIMEOUT_MS)
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Validate one timeout value; `undefined` keeps `fallback`. `label` names the
|
|
155
|
+
* option in the thrown `Error` (`Amba.configure: requestTimeoutMs`).
|
|
156
|
+
*/
|
|
157
|
+
function readTimeout(label, value, fallback) {
|
|
158
|
+
if (value === void 0) return fallback;
|
|
159
|
+
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}`);
|
|
160
|
+
return value;
|
|
161
|
+
}
|
|
162
|
+
/** The text a deadline failure carries, e.g. `request timed out after 15000 ms`. */
|
|
163
|
+
function timeoutMessage(timeoutMs) {
|
|
164
|
+
return `request timed out after ${timeoutMs} ms`;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* `timeoutMs` plus the time the bytes beyond the first
|
|
168
|
+
* {@link TRANSFER_BYTES_WITHIN_BASE} take at
|
|
169
|
+
* {@link MIN_TRANSFER_BYTES_PER_SECOND}, capped at the largest delay
|
|
170
|
+
* `setTimeout` honors. Unknown sizes add nothing.
|
|
171
|
+
*/
|
|
172
|
+
function withTransferAllowance(timeoutMs, bytes) {
|
|
173
|
+
const beyond = typeof bytes === "number" && Number.isFinite(bytes) ? bytes - TRANSFER_BYTES_WITHIN_BASE : 0;
|
|
174
|
+
const extra = beyond > 0 ? Math.ceil(beyond / MIN_TRANSFER_BYTES_PER_SECOND * 1e3) : 0;
|
|
175
|
+
return Math.min(MAX_TIMEOUT_MS, timeoutMs + extra);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The size of a request body in bytes: a string's UTF-8 length, a buffer's
|
|
179
|
+
* byte length, a `Blob`'s size. `undefined` for no body or a body of unknown
|
|
180
|
+
* size (a stream).
|
|
181
|
+
*/
|
|
182
|
+
function bodyByteLength(body) {
|
|
183
|
+
if (body === void 0 || body === null) return void 0;
|
|
184
|
+
if (typeof body === "string") return typeof TextEncoder !== "undefined" ? new TextEncoder().encode(body).length : body.length;
|
|
185
|
+
if (typeof ArrayBuffer !== "undefined") {
|
|
186
|
+
if (body instanceof ArrayBuffer) return body.byteLength;
|
|
187
|
+
if (ArrayBuffer.isView(body)) return body.byteLength;
|
|
188
|
+
}
|
|
189
|
+
const size = body.size;
|
|
190
|
+
return typeof size === "number" ? size : void 0;
|
|
191
|
+
}
|
|
192
|
+
/** A response's `Content-Length`, when it states one. */
|
|
193
|
+
function contentLength(response) {
|
|
194
|
+
const raw = response.headers?.get?.("content-length");
|
|
195
|
+
if (!raw) return void 0;
|
|
196
|
+
const n = Number(raw);
|
|
197
|
+
return Number.isFinite(n) && n >= 0 ? n : void 0;
|
|
198
|
+
}
|
|
199
|
+
/** The paths under the API base URL whose requests get the long deadline. */
|
|
200
|
+
const LONG_API_PATH_PREFIXES = ["/v1/client/ai/"];
|
|
201
|
+
/** Whether a request to API `path` (starting `/v1/...`) gets the long deadline. */
|
|
202
|
+
function isLongApiPath(path) {
|
|
203
|
+
return LONG_API_PATH_PREFIXES.some((prefix) => path.startsWith(prefix));
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Race `work` against a timer of `ms`. When the timer fires first the race
|
|
207
|
+
* rejects with `onTimeout(ms)`, then `controller` is aborted so the runtime
|
|
208
|
+
* can release the connection. The rejection does not wait for `work`, so a
|
|
209
|
+
* fetch the runtime never settles still releases its caller on time. The
|
|
210
|
+
* timer is cleared as soon as either side settles.
|
|
211
|
+
*/
|
|
212
|
+
function raceDeadline(work, ms, controller, onTimeout) {
|
|
213
|
+
let timer;
|
|
214
|
+
const deadline = new Promise((_, reject) => {
|
|
215
|
+
timer = setTimeout(() => {
|
|
216
|
+
reject(onTimeout(ms));
|
|
217
|
+
try {
|
|
218
|
+
controller?.abort();
|
|
219
|
+
} catch {}
|
|
220
|
+
}, ms);
|
|
221
|
+
});
|
|
222
|
+
return Promise.race([work, deadline]).finally(() => {
|
|
223
|
+
clearTimeout(timer);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
function newController() {
|
|
227
|
+
return typeof AbortController === "function" ? new AbortController() : void 0;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Send a request and read its response under deadlines that grow with the
|
|
231
|
+
* bytes involved. Sending, until `send` resolves with the response, gets
|
|
232
|
+
* `timeoutMs` plus the allowance for the request body (and, with
|
|
233
|
+
* `fetchBuffersBody`, for the expected response bytes). Reading,
|
|
234
|
+
* `read(response)`, gets `timeoutMs` plus the allowance for the expected
|
|
235
|
+
* response bytes, else the response's `Content-Length`. One `AbortSignal`
|
|
236
|
+
* covers both phases, so a read past its deadline aborts the transfer too.
|
|
237
|
+
* Past either deadline the promise rejects with `onTimeout(ms)`.
|
|
238
|
+
*/
|
|
239
|
+
async function exchange(timeoutMs, send, read, onTimeout, sizes = {}) {
|
|
240
|
+
const controller = newController();
|
|
241
|
+
const sendMs = withTransferAllowance(timeoutMs, (sizes.requestBytes ?? 0) + (sizes.fetchBuffersBody ? sizes.responseBytes ?? 0 : 0));
|
|
242
|
+
const response = await raceDeadline(send(controller?.signal), sendMs, controller, onTimeout);
|
|
243
|
+
const readMs = withTransferAllowance(timeoutMs, sizes.responseBytes ?? contentLength(response));
|
|
244
|
+
return raceDeadline(read(response), readMs, controller, onTimeout);
|
|
245
|
+
}
|
|
246
|
+
/** `init` with `signal` set, when there is one. */
|
|
247
|
+
function withSignal(init, signal) {
|
|
248
|
+
return signal ? {
|
|
249
|
+
...init,
|
|
250
|
+
signal
|
|
251
|
+
} : init;
|
|
252
|
+
}
|
|
253
|
+
|
|
93
254
|
//#endregion
|
|
94
255
|
//#region src/http-client.ts
|
|
95
256
|
/** Default amba API endpoint. */
|
|
@@ -106,6 +267,12 @@ var AmbaHttpClient = class {
|
|
|
106
267
|
deviceOs;
|
|
107
268
|
identity;
|
|
108
269
|
fetchImpl;
|
|
270
|
+
/** The `fetchImpl` option, when one was passed (`send` resolves fetch per call otherwise). */
|
|
271
|
+
injectedFetch;
|
|
272
|
+
/** Deadline for an API request, in milliseconds. */
|
|
273
|
+
requestTimeoutMs;
|
|
274
|
+
/** Deadline for AI generation, function invocations and media transfers. */
|
|
275
|
+
longRequestTimeoutMs;
|
|
109
276
|
ensureSession;
|
|
110
277
|
onSessionRefused;
|
|
111
278
|
constructor(opts) {
|
|
@@ -119,7 +286,39 @@ var AmbaHttpClient = class {
|
|
|
119
286
|
this.identity = opts.identity;
|
|
120
287
|
this.ensureSession = opts.ensureSession;
|
|
121
288
|
this.onSessionRefused = opts.onSessionRefused;
|
|
289
|
+
this.injectedFetch = opts.fetchImpl;
|
|
122
290
|
this.fetchImpl = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
291
|
+
this.requestTimeoutMs = opts.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
292
|
+
this.longRequestTimeoutMs = opts.longRequestTimeoutMs ?? DEFAULT_LONG_REQUEST_TIMEOUT_MS;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Send a request to an absolute URL outside the API's JSON envelope (a
|
|
296
|
+
* function invocation, a CDN download, the config route that reads the
|
|
297
|
+
* `ETag`) under a deadline. `read` takes what the caller needs from the
|
|
298
|
+
* response (the body as text, a `Blob`) and runs inside the same
|
|
299
|
+
* deadline, so a body that stops arriving fails too.
|
|
300
|
+
*
|
|
301
|
+
* A transport failure rejects with `NETWORK_ERROR` and the message
|
|
302
|
+
* `<label>: <cause>`; the deadline rejects with `NETWORK_ERROR` and
|
|
303
|
+
* `<label>: request timed out after <ms> ms`. `label` defaults to
|
|
304
|
+
* `Network error` and `timeoutMs` to the request deadline; the deadline
|
|
305
|
+
* widens with the request body and with `responseBytes`, the size the
|
|
306
|
+
* caller expects to download (see `exchange`). An `AmbaApiError` that
|
|
307
|
+
* `read` throws passes through unchanged.
|
|
308
|
+
*/
|
|
309
|
+
async send(url, init, read, opts = {}) {
|
|
310
|
+
const timeoutMs = opts.timeoutMs ?? this.requestTimeoutMs;
|
|
311
|
+
const label = opts.label ?? "Network error";
|
|
312
|
+
const fetchFn = this.injectedFetch ?? globalThis.fetch;
|
|
313
|
+
try {
|
|
314
|
+
return await exchange(timeoutMs, (signal) => fetchFn(url, withSignal(init, signal)), read, (ms) => timeoutError(label, ms), {
|
|
315
|
+
requestBytes: bodyByteLength(init.body),
|
|
316
|
+
responseBytes: opts.responseBytes,
|
|
317
|
+
fetchBuffersBody: true
|
|
318
|
+
});
|
|
319
|
+
} catch (err) {
|
|
320
|
+
throw asNetworkError(err, label);
|
|
321
|
+
}
|
|
123
322
|
}
|
|
124
323
|
/** Build the absolute URL for an API path, optionally with a query string. */
|
|
125
324
|
url(path, query) {
|
|
@@ -179,14 +378,19 @@ var AmbaHttpClient = class {
|
|
|
179
378
|
headers,
|
|
180
379
|
body: body === void 0 ? void 0 : JSON.stringify(body ?? {})
|
|
181
380
|
};
|
|
381
|
+
const timeoutMs = isLongApiPath(path) ? this.longRequestTimeoutMs : this.requestTimeoutMs;
|
|
182
382
|
let response;
|
|
383
|
+
let text;
|
|
183
384
|
try {
|
|
184
|
-
response = await this.fetchImpl(url, init)
|
|
385
|
+
({response, text} = await exchange(timeoutMs, (signal) => this.fetchImpl(url, withSignal(init, signal)), async (res) => ({
|
|
386
|
+
response: res,
|
|
387
|
+
text: res.status === 204 ? "" : await res.text()
|
|
388
|
+
}), (ms) => timeoutError("Network error", ms), { requestBytes: bodyByteLength(init.body) }));
|
|
185
389
|
} catch (err) {
|
|
186
|
-
throw
|
|
390
|
+
throw asNetworkError(err, "Network error");
|
|
187
391
|
}
|
|
188
392
|
try {
|
|
189
|
-
return
|
|
393
|
+
return parseResponse(response, text);
|
|
190
394
|
} catch (err) {
|
|
191
395
|
if (!resent && storedToken && this.onSessionRefused && !path.startsWith("/v1/client/auth/") && err instanceof AmbaApiError && SESSION_REFUSED_CODES.has(err.code) && await this.recoverSession(storedToken, storedUserId)) return this.request(method, path, body, query, void 0, true);
|
|
192
396
|
throw err;
|
|
@@ -201,9 +405,21 @@ var AmbaHttpClient = class {
|
|
|
201
405
|
}
|
|
202
406
|
}
|
|
203
407
|
};
|
|
204
|
-
|
|
408
|
+
/** The `NETWORK_ERROR` a passed deadline raises. */
|
|
409
|
+
function timeoutError(label, timeoutMs) {
|
|
410
|
+
return new AmbaApiError("NETWORK_ERROR", `${label}: ${timeoutMessage(timeoutMs)}`, { timeoutMs });
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Wrap a transport failure as `NETWORK_ERROR` (`<label>: <cause>`). An
|
|
414
|
+
* `AmbaApiError` (the deadline's own, or one a `send` reader throws) passes
|
|
415
|
+
* through unchanged.
|
|
416
|
+
*/
|
|
417
|
+
function asNetworkError(err, label) {
|
|
418
|
+
if (err instanceof AmbaApiError) return err;
|
|
419
|
+
return new AmbaApiError("NETWORK_ERROR", `${label}: ${err instanceof Error ? err.message : typeof err === "string" ? err : "fetch failed"}`, err);
|
|
420
|
+
}
|
|
421
|
+
function parseResponse(response, text) {
|
|
205
422
|
if (response.status === 204) return null;
|
|
206
|
-
const text = await response.text();
|
|
207
423
|
const isJson = (response.headers.get("content-type") ?? "").includes("application/json");
|
|
208
424
|
if (!response.ok) throw mapHttpError(response, text, isJson);
|
|
209
425
|
if (text === "") return null;
|
|
@@ -899,7 +1115,16 @@ const DETERMINISTIC_ERROR_CODES = new Set([
|
|
|
899
1115
|
"NOT_FOUND",
|
|
900
1116
|
"CONFLICT",
|
|
901
1117
|
"CONSENT_NOT_GRANTED",
|
|
902
|
-
"NOT_INITIALIZED"
|
|
1118
|
+
"NOT_INITIALIZED",
|
|
1119
|
+
"INVALID_JSON",
|
|
1120
|
+
"INVALID_BODY",
|
|
1121
|
+
"INVALID_EVENT",
|
|
1122
|
+
"RESERVED_EVENT_NAME",
|
|
1123
|
+
"INVALID_IDEMPOTENCY_KEY",
|
|
1124
|
+
"INVALID_TIMESTAMP",
|
|
1125
|
+
"INVALID_PROPERTIES",
|
|
1126
|
+
"INVALID_TELEMETRY",
|
|
1127
|
+
"IDEMPOTENCY_KEY_CONFLICT"
|
|
903
1128
|
]);
|
|
904
1129
|
/**
|
|
905
1130
|
* Is this a deterministic server rejection — a coded error the server would
|
|
@@ -937,13 +1162,34 @@ function isOfflineError(err) {
|
|
|
937
1162
|
if (typeof msg === "string" && /network (error|request failed)|failed to fetch|fetch failed|load failed/i.test(msg)) return true;
|
|
938
1163
|
return !isOnline();
|
|
939
1164
|
}
|
|
940
|
-
/**
|
|
1165
|
+
/**
|
|
1166
|
+
* Generate a stable idempotency key for a queued op: `amba_obx_` and a random
|
|
1167
|
+
* UUID. The server records event keys project-wide, so the UUID comes from
|
|
1168
|
+
* the runtime's secure random source: `crypto.randomUUID`, else
|
|
1169
|
+
* `crypto.getRandomValues` (Hermes has only the latter).
|
|
1170
|
+
*/
|
|
941
1171
|
function newIdempotencyKey() {
|
|
942
1172
|
const c = typeof globalThis !== "undefined" ? globalThis.crypto : void 0;
|
|
943
1173
|
if (c && typeof c.randomUUID === "function") return `amba_obx_${c.randomUUID()}`;
|
|
1174
|
+
if (c && typeof c.getRandomValues === "function") {
|
|
1175
|
+
const b = c.getRandomValues(new Uint8Array(16));
|
|
1176
|
+
b[6] = b[6] & 15 | 64;
|
|
1177
|
+
b[8] = b[8] & 63 | 128;
|
|
1178
|
+
const h = Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
|
1179
|
+
return `amba_obx_${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
1180
|
+
}
|
|
944
1181
|
return `amba_obx_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
|
|
945
1182
|
}
|
|
946
1183
|
/**
|
|
1184
|
+
* Whether a `track` with these properties can take the events route's keyed
|
|
1185
|
+
* path, which accepts a plain object or no properties. A JS caller can pass
|
|
1186
|
+
* something else (an array); such an event stays on the unkeyed path, which
|
|
1187
|
+
* stores it as it always has.
|
|
1188
|
+
*/
|
|
1189
|
+
function keyableProperties(properties) {
|
|
1190
|
+
return properties === void 0 || properties === null || typeof properties === "object" && !Array.isArray(properties);
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
947
1193
|
* Durable, ordered, idempotent offline outbox. Opt-in via `Amba.offline`.
|
|
948
1194
|
* Single in-flight flush is enforced so concurrent reconnect signals don't
|
|
949
1195
|
* double-replay.
|
|
@@ -2214,6 +2460,7 @@ var CollectionSubscription = class {
|
|
|
2214
2460
|
return openStream({
|
|
2215
2461
|
url: `${resolveRealtimeBaseUrl(this.ctx.baseUrl)}/v1/client/realtime/collections/${pathSegment(this.name)}${qs}`,
|
|
2216
2462
|
authHeaders: this.ctx.authHeaders,
|
|
2463
|
+
connectTimeoutMs: this.ctx.connectTimeoutMs,
|
|
2217
2464
|
onChange: cb,
|
|
2218
2465
|
options,
|
|
2219
2466
|
parse: parseRowChangeFrame
|
|
@@ -2238,6 +2485,7 @@ var ConversationSubscription = class {
|
|
|
2238
2485
|
return openStream({
|
|
2239
2486
|
url: `${resolveRealtimeBaseUrl(this.ctx.baseUrl)}/v1/client/realtime/conversations/${pathSegment(this.conversationId)}`,
|
|
2240
2487
|
authHeaders: this.ctx.authHeaders,
|
|
2488
|
+
connectTimeoutMs: this.ctx.connectTimeoutMs,
|
|
2241
2489
|
onChange: cb,
|
|
2242
2490
|
options,
|
|
2243
2491
|
parse: parseRowChangeFrame
|
|
@@ -2262,6 +2510,7 @@ var GamificationSubscription = class {
|
|
|
2262
2510
|
return openStream({
|
|
2263
2511
|
url: `${resolveRealtimeBaseUrl(this.ctx.baseUrl)}/v1/client/realtime/gamification`,
|
|
2264
2512
|
authHeaders: this.ctx.authHeaders,
|
|
2513
|
+
connectTimeoutMs: this.ctx.connectTimeoutMs,
|
|
2265
2514
|
onChange: cb,
|
|
2266
2515
|
options,
|
|
2267
2516
|
parse: parseGamificationFrame
|
|
@@ -2296,6 +2545,7 @@ function openStream(args) {
|
|
|
2296
2545
|
closeActive = connectOnce({
|
|
2297
2546
|
url: args.url,
|
|
2298
2547
|
headers,
|
|
2548
|
+
connectTimeoutMs: args.connectTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
2299
2549
|
onFrame: (frame) => {
|
|
2300
2550
|
const ev = args.parse(frame);
|
|
2301
2551
|
if (ev !== null) args.onChange(ev);
|
|
@@ -2366,8 +2616,13 @@ function connectXhr(args, XHR) {
|
|
|
2366
2616
|
xhr.open("GET", args.url, true);
|
|
2367
2617
|
for (const [k, v] of Object.entries(args.headers)) xhr.setRequestHeader(k, v);
|
|
2368
2618
|
xhr.setRequestHeader("Accept", "text/event-stream");
|
|
2619
|
+
const connectTimer = setTimeout(() => {
|
|
2620
|
+
if (aborted || xhr.readyState >= 2) return;
|
|
2621
|
+
args.onError(/* @__PURE__ */ new Error(`realtime connection timed out after ${args.connectTimeoutMs} ms`));
|
|
2622
|
+
}, args.connectTimeoutMs);
|
|
2369
2623
|
xhr.onreadystatechange = () => {
|
|
2370
2624
|
if (aborted) return;
|
|
2625
|
+
if (xhr.readyState >= 2) clearTimeout(connectTimer);
|
|
2371
2626
|
if (xhr.readyState === 3) {
|
|
2372
2627
|
if (xhr.status >= 400) return;
|
|
2373
2628
|
drain();
|
|
@@ -2391,10 +2646,12 @@ function connectXhr(args, XHR) {
|
|
|
2391
2646
|
try {
|
|
2392
2647
|
xhr.send();
|
|
2393
2648
|
} catch (err) {
|
|
2649
|
+
clearTimeout(connectTimer);
|
|
2394
2650
|
args.onError(err);
|
|
2395
2651
|
}
|
|
2396
2652
|
return () => {
|
|
2397
2653
|
aborted = true;
|
|
2654
|
+
clearTimeout(connectTimer);
|
|
2398
2655
|
try {
|
|
2399
2656
|
xhr.abort();
|
|
2400
2657
|
} catch {}
|
|
@@ -2409,14 +2666,14 @@ function connectFetchStream(args) {
|
|
|
2409
2666
|
let aborted = false;
|
|
2410
2667
|
(async () => {
|
|
2411
2668
|
try {
|
|
2412
|
-
const resp = await fetch(args.url, {
|
|
2669
|
+
const resp = await raceDeadline(fetch(args.url, {
|
|
2413
2670
|
method: "GET",
|
|
2414
2671
|
headers: {
|
|
2415
2672
|
...args.headers,
|
|
2416
2673
|
Accept: "text/event-stream"
|
|
2417
2674
|
},
|
|
2418
2675
|
signal: controller.signal
|
|
2419
|
-
});
|
|
2676
|
+
}), args.connectTimeoutMs, controller, (ms) => /* @__PURE__ */ new Error(`realtime connection timed out after ${ms} ms`));
|
|
2420
2677
|
if (!resp.ok || !resp.body) throw new Error(`realtime connect failed: HTTP ${resp.status}`);
|
|
2421
2678
|
const reader = resp.body.getReader();
|
|
2422
2679
|
const decoder = new TextDecoder();
|
|
@@ -2496,7 +2753,7 @@ function delay(ms) {
|
|
|
2496
2753
|
//#endregion
|
|
2497
2754
|
//#region src/index.ts
|
|
2498
2755
|
/** SDK semver — burned in at build time. Bump alongside package.json. */
|
|
2499
|
-
const SDK_VERSION = "4.2.
|
|
2756
|
+
const SDK_VERSION = "4.2.1";
|
|
2500
2757
|
/**
|
|
2501
2758
|
* Default host that serves deployed functions. `Amba.functions.invoke(name)`
|
|
2502
2759
|
* targets `https://<projectId>.<DEFAULT_FUNCTIONS_HOST>/<name>` unless a
|
|
@@ -3107,9 +3364,28 @@ function offlineStore(s) {
|
|
|
3107
3364
|
};
|
|
3108
3365
|
}
|
|
3109
3366
|
/**
|
|
3367
|
+
* Whether a live `track` (outbox on) carries its idempotency key:
|
|
3368
|
+
* engagement events whose properties the keyed path takes. Telemetry stays
|
|
3369
|
+
* on the unkeyed hot path, which writes no idempotency record and answers a
|
|
3370
|
+
* failed telemetry insert with 200; its replays from the outbox are keyed.
|
|
3371
|
+
*/
|
|
3372
|
+
function keyedLiveTrack(properties, telemetry) {
|
|
3373
|
+
return telemetry !== true && keyableProperties(properties);
|
|
3374
|
+
}
|
|
3375
|
+
/**
|
|
3376
|
+
* A JSON copy of `properties`: exactly what a request body serializes, and
|
|
3377
|
+
* immune to the app changing the object after `track()` returns.
|
|
3378
|
+
*/
|
|
3379
|
+
function snapshotProperties(properties) {
|
|
3380
|
+
if (properties === void 0 || properties === null) return void 0;
|
|
3381
|
+
const text = JSON.stringify(properties);
|
|
3382
|
+
return text === void 0 ? void 0 : JSON.parse(text);
|
|
3383
|
+
}
|
|
3384
|
+
/**
|
|
3110
3385
|
* Replay one queued mutation against the live API, forwarding the op's
|
|
3111
|
-
* idempotency key on collection inserts so a write that
|
|
3112
|
-
* connection dropped is never double-applied (API
|
|
3386
|
+
* idempotency key on collection inserts and tracked events so a write that
|
|
3387
|
+
* landed before the connection dropped is never double-applied (API
|
|
3388
|
+
* migration 056 for collections, `client_event_idempotency` for events).
|
|
3113
3389
|
*/
|
|
3114
3390
|
async function replayOutboxOp(s, op) {
|
|
3115
3391
|
switch (op.kind) {
|
|
@@ -3131,8 +3407,9 @@ async function replayOutboxOp(s, op) {
|
|
|
3131
3407
|
event: op.event,
|
|
3132
3408
|
properties: op.properties ?? void 0,
|
|
3133
3409
|
anonymous_id: s.identity.getAnonymousId(),
|
|
3134
|
-
timestamp: op.occurredAt ??
|
|
3135
|
-
...op.telemetry ? { telemetry: true } : {}
|
|
3410
|
+
timestamp: op.occurredAt ?? op.enqueuedAt,
|
|
3411
|
+
...op.telemetry ? { telemetry: true } : {},
|
|
3412
|
+
...keyableProperties(op.properties) ? { idempotency_key: op.idempotencyKey } : {}
|
|
3136
3413
|
});
|
|
3137
3414
|
} catch (err) {
|
|
3138
3415
|
const code = err?.code;
|
|
@@ -3214,6 +3491,7 @@ const Amba = {
|
|
|
3214
3491
|
if (!Number.isInteger(flushIntervalMs) || flushIntervalMs < 0) throw new Error("Amba.configure: events.flushIntervalMs must be a non-negative integer");
|
|
3215
3492
|
if (!Number.isInteger(maxQueue) || maxQueue < 1) throw new Error("Amba.configure: events.maxQueue must be positive");
|
|
3216
3493
|
}
|
|
3494
|
+
const deadlines = resolveRequestDeadlines(config);
|
|
3217
3495
|
const tokenStorage = config.tokenStorage ?? "secure";
|
|
3218
3496
|
if (!TOKEN_STORAGE_MODES.includes(tokenStorage)) throw new Error("Amba.configure: tokenStorage must be \"secure\", \"memory\" or \"asyncStorage\"");
|
|
3219
3497
|
const secureStorage = config.secureStorage;
|
|
@@ -3239,6 +3517,8 @@ const Amba = {
|
|
|
3239
3517
|
deviceRegion: detectDeviceRegion(),
|
|
3240
3518
|
deviceOs: detectDeviceOs(),
|
|
3241
3519
|
identity,
|
|
3520
|
+
requestTimeoutMs: deadlines.requestMs,
|
|
3521
|
+
longRequestTimeoutMs: deadlines.longMs,
|
|
3242
3522
|
ensureSession: config.autoAnonymousSession === false ? void 0 : async (path) => {
|
|
3243
3523
|
if (pathSkipsLazySession(path)) return;
|
|
3244
3524
|
await ensureAnonymousSession(getState());
|
|
@@ -3319,12 +3599,14 @@ const Amba = {
|
|
|
3319
3599
|
},
|
|
3320
3600
|
collection: (name) => new CollectionSubscription({
|
|
3321
3601
|
baseUrl: getState().baseUrl,
|
|
3322
|
-
authHeaders: realtimeAuthHeaders
|
|
3602
|
+
authHeaders: realtimeAuthHeaders,
|
|
3603
|
+
connectTimeoutMs: getState().http.requestTimeoutMs
|
|
3323
3604
|
}, name),
|
|
3324
3605
|
get gamification() {
|
|
3325
3606
|
return new GamificationSubscription({
|
|
3326
3607
|
baseUrl: getState().baseUrl,
|
|
3327
|
-
authHeaders: realtimeAuthHeaders
|
|
3608
|
+
authHeaders: realtimeAuthHeaders,
|
|
3609
|
+
connectTimeoutMs: getState().http.requestTimeoutMs
|
|
3328
3610
|
});
|
|
3329
3611
|
},
|
|
3330
3612
|
events: {
|
|
@@ -3332,13 +3614,16 @@ const Amba = {
|
|
|
3332
3614
|
const s = getState();
|
|
3333
3615
|
if (s.eventQueue && !options?.immediate) return s.eventQueue.enqueue(event, properties, options?.telemetry === true);
|
|
3334
3616
|
const occurredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3617
|
+
const props = s.offlineEnabled ? snapshotProperties(properties) : properties;
|
|
3335
3618
|
const body = {
|
|
3336
3619
|
event,
|
|
3337
|
-
properties:
|
|
3620
|
+
properties: props ?? void 0,
|
|
3338
3621
|
anonymous_id: s.identity.getAnonymousId(),
|
|
3339
3622
|
timestamp: occurredAt
|
|
3340
3623
|
};
|
|
3341
3624
|
if (options?.telemetry) body["telemetry"] = true;
|
|
3625
|
+
const idempotencyKey = s.offlineEnabled ? newIdempotencyKey() : void 0;
|
|
3626
|
+
if (idempotencyKey && keyedLiveTrack(props, options?.telemetry)) body["idempotency_key"] = idempotencyKey;
|
|
3342
3627
|
const outboxTicket = s.offlineEnabled ? getOfflineQueue(s).ticket() : void 0;
|
|
3343
3628
|
try {
|
|
3344
3629
|
await s.http.post(P.events, body);
|
|
@@ -3347,9 +3632,10 @@ const Amba = {
|
|
|
3347
3632
|
await getOfflineQueue(s).enqueue({
|
|
3348
3633
|
kind: "track",
|
|
3349
3634
|
event,
|
|
3350
|
-
properties,
|
|
3635
|
+
properties: props,
|
|
3351
3636
|
telemetry: options?.telemetry,
|
|
3352
|
-
occurredAt
|
|
3637
|
+
occurredAt,
|
|
3638
|
+
idempotencyKey
|
|
3353
3639
|
}, outboxTicket);
|
|
3354
3640
|
return;
|
|
3355
3641
|
}
|
|
@@ -3811,21 +4097,25 @@ const Amba = {
|
|
|
3811
4097
|
await getState().http.delete(`${P.storage}/${encodePathSegment(assetId)}`);
|
|
3812
4098
|
},
|
|
3813
4099
|
download: async (assetId) => {
|
|
3814
|
-
const
|
|
4100
|
+
const s = getState();
|
|
4101
|
+
const assetEnv = await s.http.get(`${P.storage}/${encodePathSegment(assetId)}`);
|
|
3815
4102
|
const cdnUrl = assetEnv.data.url ?? assetEnv.data.storage_url ?? "";
|
|
3816
4103
|
if (!cdnUrl) throw new AmbaApiError("NOT_FOUND", "asset has no resolvable CDN url");
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
response
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
}
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
4104
|
+
const sizeBytes = assetEnv.data.size_bytes;
|
|
4105
|
+
const { response, blob, text } = await s.http.send(cdnUrl, { method: "GET" }, async (res) => res.ok ? {
|
|
4106
|
+
response: res,
|
|
4107
|
+
blob: await res.blob(),
|
|
4108
|
+
text: ""
|
|
4109
|
+
} : {
|
|
4110
|
+
response: res,
|
|
4111
|
+
blob: null,
|
|
4112
|
+
text: await res.text()
|
|
4113
|
+
}, {
|
|
4114
|
+
timeoutMs: s.http.longRequestTimeoutMs,
|
|
4115
|
+
responseBytes: typeof sizeBytes === "number" ? sizeBytes : void 0
|
|
4116
|
+
});
|
|
4117
|
+
if (!blob) throw mapHttpError(response, text, (response.headers.get("content-type") ?? "").includes("application/json"));
|
|
4118
|
+
return blob;
|
|
3829
4119
|
}
|
|
3830
4120
|
},
|
|
3831
4121
|
push: {
|
|
@@ -3873,27 +4163,30 @@ const Amba = {
|
|
|
3873
4163
|
functions: { invoke: async (name, body, options) => {
|
|
3874
4164
|
const s = getState();
|
|
3875
4165
|
if (!name || name.trim() === "") throw new AmbaApiError("INVALID_ARGUMENT", "Amba.functions.invoke: a function name is required.");
|
|
4166
|
+
let timeoutMs;
|
|
4167
|
+
try {
|
|
4168
|
+
timeoutMs = readTimeout("Amba.functions.invoke: timeoutMs", options?.timeoutMs, s.http.longRequestTimeoutMs);
|
|
4169
|
+
} catch (err) {
|
|
4170
|
+
throw new AmbaApiError("INVALID_ARGUMENT", err.message);
|
|
4171
|
+
}
|
|
3876
4172
|
const url = await resolveFunctionUrl(s, name);
|
|
3877
4173
|
const method = options?.method ?? "POST";
|
|
3878
4174
|
const headers = s.http.buildHeaders(options?.headers);
|
|
3879
4175
|
let requestBody;
|
|
3880
4176
|
if (body !== void 0 && method !== "GET") requestBody = JSON.stringify(body);
|
|
3881
4177
|
else delete headers["Content-Type"];
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
}
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
if (!response.ok)
|
|
3894
|
-
const ct = response.headers.get("content-type") ?? "";
|
|
3895
|
-
throw mapHttpError(response, text, ct.includes("application/json"));
|
|
3896
|
-
}
|
|
4178
|
+
const { response, text } = await s.http.send(url, {
|
|
4179
|
+
method,
|
|
4180
|
+
headers,
|
|
4181
|
+
body: requestBody
|
|
4182
|
+
}, async (res) => ({
|
|
4183
|
+
response: res,
|
|
4184
|
+
text: await res.text()
|
|
4185
|
+
}), {
|
|
4186
|
+
timeoutMs,
|
|
4187
|
+
label: `Amba.functions.invoke("${name}") network error`
|
|
4188
|
+
});
|
|
4189
|
+
if (!response.ok) throw mapHttpError(response, text, (response.headers.get("content-type") ?? "").includes("application/json"));
|
|
3897
4190
|
if (!text) return void 0;
|
|
3898
4191
|
try {
|
|
3899
4192
|
return JSON.parse(text);
|
|
@@ -3907,20 +4200,14 @@ const Amba = {
|
|
|
3907
4200
|
const ticket = s.remoteValues.ticket();
|
|
3908
4201
|
const url = s.http.url(P.config);
|
|
3909
4202
|
const headers = s.http.buildHeaders();
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
}
|
|
3917
|
-
|
|
3918
|
-
}
|
|
3919
|
-
const text = await response.text();
|
|
3920
|
-
if (!response.ok) {
|
|
3921
|
-
const ct = response.headers.get("content-type") ?? "";
|
|
3922
|
-
throw mapHttpError(response, text, ct.includes("application/json"));
|
|
3923
|
-
}
|
|
4203
|
+
const { response, text } = await s.http.send(url, {
|
|
4204
|
+
method: "GET",
|
|
4205
|
+
headers
|
|
4206
|
+
}, async (res) => ({
|
|
4207
|
+
response: res,
|
|
4208
|
+
text: await res.text()
|
|
4209
|
+
}));
|
|
4210
|
+
if (!response.ok) throw mapHttpError(response, text, (response.headers.get("content-type") ?? "").includes("application/json"));
|
|
3924
4211
|
let parsed = {};
|
|
3925
4212
|
if (text) try {
|
|
3926
4213
|
parsed = JSON.parse(text);
|
|
@@ -4143,7 +4430,8 @@ const Amba = {
|
|
|
4143
4430
|
messaging: {
|
|
4144
4431
|
conversation: (conversationId) => new ConversationSubscription({
|
|
4145
4432
|
baseUrl: getState().baseUrl,
|
|
4146
|
-
authHeaders: realtimeAuthHeaders
|
|
4433
|
+
authHeaders: realtimeAuthHeaders,
|
|
4434
|
+
connectTimeoutMs: getState().http.requestTimeoutMs
|
|
4147
4435
|
}, conversationId),
|
|
4148
4436
|
createConversation: async (request) => {
|
|
4149
4437
|
return (await getState().http.post(`${P.messaging}/conversations`, request)).data;
|