@viso-lol/sdk 0.0.0-stage → 0.1.0

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.js ADDED
@@ -0,0 +1,685 @@
1
+ // src/bytes.ts
2
+ /** Copies a view's bytes: a view may cover only part of a buffer, or a shared buffer. */
3
+ function viewBytes(view) {
4
+ return new Uint8Array(view.buffer, view.byteOffset, view.byteLength).slice();
5
+ }
6
+ /** A function, not an inline check, so TypeScript does not narrow `aborted` across awaits. */
7
+ function isAborted(signal) {
8
+ return signal?.aborted === true;
9
+ }
10
+ // src/errors.ts
11
+ /** An error response from the API: `{ error: { code, message, details?, requestId } }`. */
12
+ var VisoError = class extends Error {
13
+ /** Stable machine-readable code, for example `images.file_exists`. */
14
+ code;
15
+ /** HTTP status. */
16
+ status;
17
+ /** Include it when reporting a problem. */
18
+ requestId;
19
+ details;
20
+ headers;
21
+ constructor(init) {
22
+ super(init.message);
23
+ this.name = "VisoError";
24
+ this.code = init.code;
25
+ this.status = init.status;
26
+ this.requestId = init.requestId;
27
+ this.details = init.details;
28
+ this.headers = init.headers ?? new Headers();
29
+ }
30
+ };
31
+ /** The request never got a response: network failure or timeout. */
32
+ var VisoConnectionError = class extends Error {
33
+ /** True when the SDK gave up after `timeoutMs`. */
34
+ timeout;
35
+ constructor(message, options = {}) {
36
+ super(message, { cause: options.cause });
37
+ this.name = "VisoConnectionError";
38
+ this.timeout = options.timeout ?? false;
39
+ }
40
+ };
41
+ function isRecord(value) {
42
+ return typeof value === "object" && value !== null && !Array.isArray(value);
43
+ }
44
+ /** Builds a `VisoError` from any failed response, including non-JSON ones from proxies. */
45
+ async function errorFromResponse(response) {
46
+ const headerRequestId = response.headers.get("x-request-id");
47
+ const text = await response.text().catch(() => "");
48
+ let body;
49
+ try {
50
+ body = JSON.parse(text);
51
+ } catch {
52
+ body = void 0;
53
+ }
54
+ const envelope = isRecord(body) && isRecord(body["error"]) ? body["error"] : void 0;
55
+ const code = envelope?.["code"];
56
+ const message = envelope?.["message"];
57
+ const requestId = envelope?.["requestId"];
58
+ const details = envelope?.["details"];
59
+ return new VisoError({
60
+ code: typeof code === "string" ? code : "internal",
61
+ status: response.status,
62
+ message: typeof message === "string" ? message : `The API answered ${response.status} ${response.statusText}`.trim(),
63
+ requestId: typeof requestId === "string" ? requestId : headerRequestId,
64
+ details: isRecord(details) ? details : void 0,
65
+ headers: response.headers
66
+ });
67
+ }
68
+ // src/images-input.ts
69
+ function isBinary(source) {
70
+ return source instanceof Blob || source instanceof ArrayBuffer || ArrayBuffer.isView(source);
71
+ }
72
+ function toBlob(source) {
73
+ if (source instanceof Blob) return source;
74
+ if (source instanceof ArrayBuffer) return new Blob([source]);
75
+ return new Blob([viewBytes(source)]);
76
+ }
77
+ function base64ToBlob(base64) {
78
+ const data = base64.startsWith("data:") ? base64.slice(base64.indexOf(",") + 1) : base64;
79
+ const binary = atob(data);
80
+ const bytes = new Uint8Array(binary.length);
81
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
82
+ return new Blob([bytes]);
83
+ }
84
+ /**
85
+ * Face endpoints take JSON (`{ base64 }` or `{ fileId }` images) or multipart (image
86
+ * files). Bytes go as multipart, which avoids base64's 33% overhead; options then travel
87
+ * as JSON-encoded form fields so that, for example, the external ID `"123"` stays a string.
88
+ */
89
+ function faceBody(images, options) {
90
+ const sources = Object.entries(images);
91
+ if (!sources.some(([, source]) => isBinary(source))) return { json: {
92
+ ...options,
93
+ ...images
94
+ } };
95
+ const form = new FormData();
96
+ for (const [field, source] of sources) if (isBinary(source)) form.append(field, toBlob(source), field);
97
+ else if ("fileId" in source) form.append(field, source.fileId);
98
+ else form.append(field, base64ToBlob(source.base64), field);
99
+ for (const [name, value] of Object.entries(options)) if (value !== void 0) form.append(name, JSON.stringify(value));
100
+ return { form };
101
+ }
102
+ // src/pagination.ts
103
+ function paginate(fetchPage, firstCursor) {
104
+ const first = fetchPage(firstCursor);
105
+ async function* items() {
106
+ let page = await first;
107
+ while (true) {
108
+ yield* page.data;
109
+ if (page.nextCursor === null) return;
110
+ page = await fetchPage(page.nextCursor);
111
+ }
112
+ }
113
+ return Object.assign(first, { [Symbol.asyncIterator]: items });
114
+ }
115
+ // src/resources/face.ts
116
+ const segment$1 = encodeURIComponent;
117
+ var FacesResource = class {
118
+ #client;
119
+ constructor(client) {
120
+ this.#client = client;
121
+ }
122
+ /** Detects faces in the image and stores them in the collection, largest first. */
123
+ index(collectionId, params, options) {
124
+ const { image, ...rest } = params;
125
+ return this.#client.request({
126
+ method: "POST",
127
+ path: `/face/collections/${segment$1(collectionId)}/faces`,
128
+ ...faceBody({ image }, rest),
129
+ options
130
+ });
131
+ }
132
+ /** Faces of a collection, oldest first. Iterate with `for await` to read every page. */
133
+ list(collectionId, params = {}, options) {
134
+ return paginate((cursor) => this.#client.request({
135
+ method: "GET",
136
+ path: `/face/collections/${segment$1(collectionId)}/faces`,
137
+ query: {
138
+ externalId: params.externalId,
139
+ limit: params.limit,
140
+ cursor
141
+ },
142
+ options
143
+ }), params.cursor);
144
+ }
145
+ delete(collectionId, faceId, options) {
146
+ return this.#client.request({
147
+ method: "DELETE",
148
+ path: `/face/collections/${segment$1(collectionId)}/faces/${segment$1(faceId)}`,
149
+ options
150
+ });
151
+ }
152
+ /** Searches the collection for faces similar to a stored face. */
153
+ search(collectionId, faceId, params = {}, options) {
154
+ return this.#client.request({
155
+ method: "POST",
156
+ path: `/face/collections/${segment$1(collectionId)}/faces/${segment$1(faceId)}/search`,
157
+ json: params,
158
+ options
159
+ });
160
+ }
161
+ };
162
+ var CollectionsResource = class {
163
+ faces;
164
+ #client;
165
+ constructor(client) {
166
+ this.#client = client;
167
+ this.faces = new FacesResource(client);
168
+ }
169
+ create(params, options) {
170
+ return this.#client.request({
171
+ method: "POST",
172
+ path: "/face/collections",
173
+ json: params,
174
+ options
175
+ });
176
+ }
177
+ list(options) {
178
+ return paginate(() => this.#client.request({
179
+ method: "GET",
180
+ path: "/face/collections",
181
+ options
182
+ }));
183
+ }
184
+ get(collectionId, options) {
185
+ return this.#client.request({
186
+ method: "GET",
187
+ path: `/face/collections/${segment$1(collectionId)}`,
188
+ options
189
+ });
190
+ }
191
+ /** Deletion is accepted at once and finishes in the background. */
192
+ delete(collectionId, options) {
193
+ return this.#client.request({
194
+ method: "DELETE",
195
+ path: `/face/collections/${segment$1(collectionId)}`,
196
+ options
197
+ });
198
+ }
199
+ /** Searches the collection for the largest face in the image. */
200
+ search(collectionId, params, options) {
201
+ const { image, ...rest } = params;
202
+ return this.#client.request({
203
+ method: "POST",
204
+ path: `/face/collections/${segment$1(collectionId)}/search`,
205
+ ...faceBody({ image }, rest),
206
+ options
207
+ });
208
+ }
209
+ };
210
+ var FaceResource = class {
211
+ collections;
212
+ #client;
213
+ constructor(client) {
214
+ this.#client = client;
215
+ this.collections = new CollectionsResource(client);
216
+ }
217
+ /** Finds faces with their box, landmarks, pose and quality, plus requested attributes. */
218
+ detect(params, options) {
219
+ const { image, ...rest } = params;
220
+ return this.#client.request({
221
+ method: "POST",
222
+ path: "/face/detect",
223
+ ...faceBody({ image }, rest),
224
+ options
225
+ });
226
+ }
227
+ /** Compares the largest face in `source` with every face in `target`. */
228
+ compare(params, options) {
229
+ const { source, target, ...rest } = params;
230
+ return this.#client.request({
231
+ method: "POST",
232
+ path: "/face/compare",
233
+ ...faceBody({
234
+ source,
235
+ target
236
+ }, rest),
237
+ options
238
+ });
239
+ }
240
+ };
241
+ // src/resources/images.ts
242
+ const segment = encodeURIComponent;
243
+ var BucketsResource = class {
244
+ #client;
245
+ constructor(client) {
246
+ this.#client = client;
247
+ }
248
+ create(params, options) {
249
+ return this.#client.request({
250
+ method: "POST",
251
+ path: "/images/buckets",
252
+ json: params,
253
+ options
254
+ });
255
+ }
256
+ list(options) {
257
+ return paginate(() => this.#client.request({
258
+ method: "GET",
259
+ path: "/images/buckets",
260
+ options
261
+ }));
262
+ }
263
+ get(bucketId, options) {
264
+ return this.#client.request({
265
+ method: "GET",
266
+ path: `/images/buckets/${segment(bucketId)}`,
267
+ options
268
+ });
269
+ }
270
+ /** Changing visibility or cache lifetime purges the bucket's cached files. */
271
+ update(bucketId, params, options) {
272
+ return this.#client.request({
273
+ method: "PATCH",
274
+ path: `/images/buckets/${segment(bucketId)}`,
275
+ json: params,
276
+ options
277
+ });
278
+ }
279
+ /** Deletes the bucket and every file in it; stored bytes are removed in the background. */
280
+ delete(bucketId, options) {
281
+ return this.#client.request({
282
+ method: "DELETE",
283
+ path: `/images/buckets/${segment(bucketId)}`,
284
+ options
285
+ });
286
+ }
287
+ };
288
+ var FilesResource = class {
289
+ #client;
290
+ constructor(client) {
291
+ this.#client = client;
292
+ }
293
+ /**
294
+ * Files ordered by path. The promise resolves to the first page (with `folders` when
295
+ * `delimiter` is `/`); `for await` iterates the files of every page.
296
+ */
297
+ list(params, options) {
298
+ const { bucketId, cursor: firstCursor, ...query } = params;
299
+ return paginate((cursor) => this.#client.request({
300
+ method: "GET",
301
+ path: `/images/buckets/${segment(bucketId)}/files`,
302
+ query: {
303
+ ...query,
304
+ cursor
305
+ },
306
+ options
307
+ }), firstCursor);
308
+ }
309
+ get(fileId, options) {
310
+ return this.#client.request({
311
+ method: "GET",
312
+ path: `/images/files/${segment(fileId)}`,
313
+ options
314
+ });
315
+ }
316
+ delete(fileId, options) {
317
+ return this.#client.request({
318
+ method: "DELETE",
319
+ path: `/images/files/${segment(fileId)}`,
320
+ options
321
+ });
322
+ }
323
+ /** Deletes up to 100 files of a bucket at once. */
324
+ deleteMany(params, options) {
325
+ const { bucketId, ...json } = params;
326
+ return this.#client.request({
327
+ method: "POST",
328
+ path: `/images/buckets/${segment(bucketId)}/files/delete`,
329
+ json,
330
+ options
331
+ });
332
+ }
333
+ /** A time-limited CDN URL, required for files in private buckets. */
334
+ createSignedUrl(fileId, params = {}, options) {
335
+ return this.#client.request({
336
+ method: "POST",
337
+ path: `/images/files/${segment(fileId)}/signed-url`,
338
+ json: params,
339
+ options
340
+ });
341
+ }
342
+ };
343
+ function contentTypeOf(params) {
344
+ if (params.contentType !== void 0) return params.contentType;
345
+ return params.body instanceof Blob && params.body.type !== "" ? params.body.type : void 0;
346
+ }
347
+ var ImagesResource = class {
348
+ buckets;
349
+ files;
350
+ #client;
351
+ constructor(client) {
352
+ this.#client = client;
353
+ this.buckets = new BucketsResource(client);
354
+ this.files = new FilesResource(client);
355
+ }
356
+ /**
357
+ * Uploads a file from your server. Bytes are sent as the raw request body; streams are
358
+ * forwarded as they are read. The file type is detected from its content.
359
+ */
360
+ upload(params, options) {
361
+ return this.#client.request({
362
+ method: "POST",
363
+ path: `/images/buckets/${segment(params.bucketId)}/files`,
364
+ query: {
365
+ path: params.path,
366
+ name: params.name,
367
+ overwrite: params.overwrite === void 0 ? void 0 : String(params.overwrite)
368
+ },
369
+ body: ArrayBuffer.isView(params.body) ? viewBytes(params.body) : params.body,
370
+ contentType: contentTypeOf(params),
371
+ options
372
+ });
373
+ }
374
+ /**
375
+ * A single-use URL a browser can `PUT` one image to without an API key. Create it on
376
+ * your server and pass `uploadUrl` to `uploadToSignedUrl` in the browser.
377
+ */
378
+ createUploadUrl(params, options) {
379
+ const { bucketId, ...json } = params;
380
+ return this.#client.request({
381
+ method: "POST",
382
+ path: `/images/buckets/${segment(bucketId)}/upload-urls`,
383
+ json,
384
+ options
385
+ });
386
+ }
387
+ };
388
+ // src/resources/usage.ts
389
+ var UsageResource = class {
390
+ #client;
391
+ constructor(client) {
392
+ this.#client = client;
393
+ }
394
+ /** The team's quotas, the project's recent requests and the per-key rate limit. */
395
+ get(options) {
396
+ return this.#client.request({
397
+ method: "GET",
398
+ path: "/usage",
399
+ options
400
+ });
401
+ }
402
+ };
403
+ // src/retry.ts
404
+ const BASE_DELAY_MS = 500;
405
+ const MAX_BACKOFF_MS = 8e3;
406
+ /** Never wait longer than this for one retry, whatever `Retry-After` says. */
407
+ const MAX_RETRY_AFTER_MS = 6e4;
408
+ /** Methods that are safe to repeat when the outcome of a request is unknown. */
409
+ const IDEMPOTENT_METHODS = /* @__PURE__ */ new Set([
410
+ "GET",
411
+ "HEAD",
412
+ "PUT",
413
+ "DELETE"
414
+ ]);
415
+ /**
416
+ * 429 and 503 mean the request was not processed (the API refunds quota on 503), so they
417
+ * are retried for any method. Other 5xx and network errors may have had side effects, so
418
+ * only idempotent methods retry them.
419
+ */
420
+ function isRetryableStatus(status, method) {
421
+ if (status === 429 || status === 503) return true;
422
+ return status >= 500 && IDEMPOTENT_METHODS.has(method);
423
+ }
424
+ function isRetryableConnectionError(method) {
425
+ return IDEMPOTENT_METHODS.has(method);
426
+ }
427
+ /** `Retry-After` in milliseconds (delta seconds or an HTTP date), or `undefined`. */
428
+ function parseRetryAfter(value, now = Date.now()) {
429
+ if (value === null || value.trim() === "") return;
430
+ const seconds = Number(value);
431
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
432
+ const date = Date.parse(value);
433
+ return Number.isNaN(date) ? void 0 : Math.max(0, date - now);
434
+ }
435
+ /** Exponential backoff with jitter for retry `attempt` (0-based), honoring `Retry-After`. */
436
+ function retryDelay(attempt, retryAfter = null, random = Math.random) {
437
+ const fromHeader = parseRetryAfter(retryAfter);
438
+ if (fromHeader !== void 0) return Math.min(fromHeader, MAX_RETRY_AFTER_MS);
439
+ const backoff = Math.min(MAX_BACKOFF_MS, BASE_DELAY_MS * 2 ** attempt);
440
+ return Math.round(backoff * (.75 + random() * .25));
441
+ }
442
+ function sleep(ms, signal) {
443
+ return new Promise((resolve, reject) => {
444
+ if (signal?.aborted === true) {
445
+ reject(signal.reason);
446
+ return;
447
+ }
448
+ const onAbort = () => {
449
+ clearTimeout(timer);
450
+ reject(signal?.reason);
451
+ };
452
+ const timer = setTimeout(() => {
453
+ signal?.removeEventListener("abort", onAbort);
454
+ resolve();
455
+ }, ms);
456
+ signal?.addEventListener("abort", onAbort, { once: true });
457
+ });
458
+ }
459
+ // src/client.ts
460
+ const VERSION = "0.1.0";
461
+ const DEFAULT_BASE_URL = "https://api.viso.lol/v1";
462
+ function environmentVariable(name) {
463
+ const process = Reflect.get(globalThis, "process");
464
+ if (typeof process !== "object" || process === null) return;
465
+ const env = Reflect.get(process, "env");
466
+ if (typeof env !== "object" || env === null) return;
467
+ const value = Reflect.get(env, name);
468
+ return typeof value === "string" && value !== "" ? value : void 0;
469
+ }
470
+ /** Read through `globalThis` so the SDK type-checks without the DOM library (Workers). */
471
+ function isBrowser() {
472
+ const browserWindow = Reflect.get(globalThis, "window");
473
+ return typeof browserWindow === "object" && browserWindow !== null && Reflect.get(browserWindow, "document") !== void 0;
474
+ }
475
+ /** Streams can be read once, so requests that send one are never retried. */
476
+ function isReplayable(body) {
477
+ return !(typeof ReadableStream !== "undefined" && body instanceof ReadableStream);
478
+ }
479
+ /**
480
+ * Combines the caller's signal with a per-attempt timeout. `timedOut()` tells our own
481
+ * timeout apart from the caller aborting.
482
+ */
483
+ function attemptSignal(timeoutMs, signal) {
484
+ const controller = new AbortController();
485
+ let timedOut = false;
486
+ const timer = setTimeout(() => {
487
+ timedOut = true;
488
+ controller.abort(/* @__PURE__ */ new Error(`Request timed out after ${timeoutMs} ms.`));
489
+ }, timeoutMs);
490
+ const onAbort = () => controller.abort(signal?.reason);
491
+ if (signal?.aborted === true) onAbort();
492
+ else signal?.addEventListener("abort", onAbort, { once: true });
493
+ return {
494
+ signal: controller.signal,
495
+ timedOut: () => timedOut,
496
+ done: () => {
497
+ clearTimeout(timer);
498
+ signal?.removeEventListener("abort", onAbort);
499
+ }
500
+ };
501
+ }
502
+ /**
503
+ * The viso.lol API client.
504
+ *
505
+ * ```ts
506
+ * const viso = new Viso({ apiKey: process.env.VISO_API_KEY });
507
+ * const { faces } = await viso.face.detect({ image: await file.arrayBuffer() });
508
+ * ```
509
+ */
510
+ var Viso = class {
511
+ face;
512
+ images;
513
+ usage;
514
+ baseUrl;
515
+ maxRetries;
516
+ timeoutMs;
517
+ #apiKey;
518
+ #fetch;
519
+ constructor(options = {}) {
520
+ const apiKey = options.apiKey ?? environmentVariable("VISO_API_KEY");
521
+ if (apiKey === void 0 || apiKey === "") throw new Error("Missing API key. Pass `apiKey` or set the VISO_API_KEY environment variable.");
522
+ if (isBrowser() && options.dangerouslyAllowBrowser !== true) throw new Error("The viso.lol SDK is running in a browser, which would expose your secret API key. Call the API from your server, and upload from browsers with signed upload URLs (`uploadToSignedUrl`). For local experiments only, pass `dangerouslyAllowBrowser: true`.");
523
+ this.#apiKey = apiKey;
524
+ this.baseUrl = (options.baseUrl ?? environmentVariable("VISO_BASE_URL") ?? "https://api.viso.lol/v1").trim().replace(/\/+$/, "");
525
+ this.maxRetries = options.maxRetries ?? 2;
526
+ this.timeoutMs = options.timeoutMs ?? 6e4;
527
+ const customFetch = options.fetch;
528
+ this.#fetch = customFetch ?? ((input, init) => fetch(input, init));
529
+ this.face = new FaceResource(this);
530
+ this.images = new ImagesResource(this);
531
+ this.usage = new UsageResource(this);
532
+ }
533
+ /** Sends a request, retrying when it is safe, and returns the parsed JSON body. */
534
+ request(request) {
535
+ const url = new URL(`${this.baseUrl}${request.path}`);
536
+ for (const [name, value] of Object.entries(request.query ?? {})) if (value !== void 0) url.searchParams.set(name, String(value));
537
+ const headers = new Headers({
538
+ Authorization: `Bearer ${this.#apiKey}`,
539
+ Accept: "application/json"
540
+ });
541
+ let body;
542
+ if (request.json !== void 0) {
543
+ headers.set("Content-Type", "application/json");
544
+ body = JSON.stringify(request.json);
545
+ } else if (request.form !== void 0) body = request.form;
546
+ else if (request.body !== void 0) {
547
+ headers.set("Content-Type", request.contentType ?? "application/octet-stream");
548
+ body = request.body;
549
+ }
550
+ const options = request.options ?? {};
551
+ return this.#attempt({
552
+ url,
553
+ method: request.method,
554
+ headers,
555
+ body,
556
+ signal: options.signal,
557
+ timeoutMs: options.timeoutMs ?? this.timeoutMs,
558
+ maxRetries: isReplayable(body) ? options.maxRetries ?? this.maxRetries : 0,
559
+ attempt: 0
560
+ });
561
+ }
562
+ async #attempt(state) {
563
+ const { url, method, body, signal } = state;
564
+ const abort = attemptSignal(state.timeoutMs, signal);
565
+ const init = {
566
+ method,
567
+ headers: state.headers,
568
+ signal: abort.signal
569
+ };
570
+ if (body !== void 0) {
571
+ init.body = body;
572
+ if (!isReplayable(body)) init.duplex = "half";
573
+ }
574
+ const canRetry = state.attempt < state.maxRetries;
575
+ const retry = async (retryAfter = null) => {
576
+ await sleep(retryDelay(state.attempt, retryAfter), signal);
577
+ return this.#attempt({
578
+ ...state,
579
+ attempt: state.attempt + 1
580
+ });
581
+ };
582
+ let response;
583
+ try {
584
+ response = await this.#fetch(url, init);
585
+ } catch (error) {
586
+ abort.done();
587
+ if (isAborted(signal)) throw signal?.reason;
588
+ if (canRetry && isRetryableConnectionError(method)) return retry();
589
+ const timeout = abort.timedOut();
590
+ throw new VisoConnectionError(timeout ? `The request to ${url.pathname} timed out after ${state.timeoutMs} ms.` : `Could not reach the viso.lol API at ${url.origin}.`, {
591
+ cause: error,
592
+ timeout
593
+ });
594
+ }
595
+ try {
596
+ if (response.ok) return await response.json();
597
+ if (!canRetry || !isRetryableStatus(response.status, method)) throw await errorFromResponse(response);
598
+ await response.body?.cancel();
599
+ } finally {
600
+ abort.done();
601
+ }
602
+ return retry(response.headers.get("Retry-After"));
603
+ }
604
+ };
605
+ // src/upload.ts
606
+ function withName(uploadUrl, body, name) {
607
+ const fileName = name ?? (typeof File !== "undefined" && body instanceof File ? body.name : "");
608
+ if (fileName === "") return uploadUrl;
609
+ const url = new URL(uploadUrl);
610
+ url.searchParams.set("name", fileName.slice(0, 255));
611
+ return url.toString();
612
+ }
613
+ function contentType(body) {
614
+ return body instanceof Blob && body.type !== "" ? body.type : "application/octet-stream";
615
+ }
616
+ function isXhrConstructor(value) {
617
+ return typeof value === "function";
618
+ }
619
+ /** `XMLHttpRequest` reports upload progress; `fetch` cannot. */
620
+ function browserXhr() {
621
+ const value = Reflect.get(globalThis, "XMLHttpRequest");
622
+ return isXhrConstructor(value) ? value : void 0;
623
+ }
624
+ function viaXhr(XhrClass, url, body, options) {
625
+ return new Promise((resolve, reject) => {
626
+ const xhr = new XhrClass();
627
+ xhr.open("PUT", url);
628
+ xhr.setRequestHeader("Content-Type", contentType(body));
629
+ xhr.upload.addEventListener("progress", (event) => {
630
+ options.onProgress?.({
631
+ loaded: event.loaded,
632
+ total: event.lengthComputable ? event.total : null
633
+ });
634
+ });
635
+ xhr.addEventListener("load", () => {
636
+ const headers = new Headers();
637
+ const requestId = xhr.getResponseHeader("x-request-id");
638
+ if (requestId !== null) headers.set("x-request-id", requestId);
639
+ resolve(new Response(xhr.responseText, {
640
+ status: xhr.status,
641
+ headers
642
+ }));
643
+ });
644
+ xhr.addEventListener("error", () => reject(new VisoConnectionError("The upload failed.")));
645
+ xhr.addEventListener("timeout", () => reject(new VisoConnectionError("The upload timed out.", { timeout: true })));
646
+ xhr.addEventListener("abort", () => reject(options.signal?.reason));
647
+ options.signal?.addEventListener("abort", () => xhr.abort(), { once: true });
648
+ xhr.send(body instanceof Blob ? body : new Blob([toBytes(body)]));
649
+ });
650
+ }
651
+ function toBytes(body) {
652
+ return body instanceof ArrayBuffer ? new Uint8Array(body) : viewBytes(body);
653
+ }
654
+ /**
655
+ * Uploads one file to a signed upload URL created on your server with
656
+ * `viso.images.createUploadUrl()`. Safe in browsers: the URL is the credential and it
657
+ * works once. Uploads are not retried automatically; a rejected upload (wrong type, too
658
+ * large) leaves the URL usable for another attempt.
659
+ */
660
+ async function uploadToSignedUrl(uploadUrl, body, options = {}) {
661
+ if (isAborted(options.signal)) throw options.signal?.reason;
662
+ const url = withName(uploadUrl, body, options.name);
663
+ let response;
664
+ const XhrClass = options.fetch === void 0 ? browserXhr() : void 0;
665
+ if (XhrClass !== void 0) response = await viaXhr(XhrClass, url, body, options);
666
+ else {
667
+ const send = options.fetch ?? ((input, init) => fetch(input, init));
668
+ try {
669
+ response = await send(url, {
670
+ method: "PUT",
671
+ headers: { "Content-Type": contentType(body) },
672
+ body: body instanceof Blob ? body : toBytes(body),
673
+ signal: options.signal ?? null
674
+ });
675
+ } catch (error) {
676
+ if (isAborted(options.signal)) throw options.signal?.reason;
677
+ throw new VisoConnectionError("The upload failed.", { cause: error });
678
+ }
679
+ }
680
+ if (!response.ok) throw await errorFromResponse(response);
681
+ return await response.json();
682
+ }
683
+ export { DEFAULT_BASE_URL, VERSION, Viso, VisoConnectionError, VisoError, uploadToSignedUrl };
684
+
685
+ //# sourceMappingURL=index.js.map