@rendobar/sdk 2.2.0 → 3.0.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.mjs CHANGED
@@ -302,13 +302,161 @@ function createBillingResource(request) {
302
302
  }
303
303
  //#endregion
304
304
  //#region src/resources/uploads.ts
305
+ function toBlob(file) {
306
+ return file instanceof Blob ? file : new Blob([file]);
307
+ }
308
+ /**
309
+ * Per-attempt upload time budget. fetch() exposes no upload progress, so a
310
+ * stalled socket and a slow link are indistinguishable from the outside; a
311
+ * hard per-attempt budget is the only portable stall cap. The ladder escalates
312
+ * (healthy 500 KiB/s assumption first, then 100, then 50) so a genuinely slow
313
+ * connection that times out on an early attempt still completes on a later,
314
+ * more patient one — while a hung socket gets cut and retried on a fresh
315
+ * connection instead of waiting on the OS TCP timeout.
316
+ *
317
+ * @internal exported for tests
318
+ */
319
+ function attemptTimeoutMs(sizeBytes, attempt) {
320
+ const GRACE_MS = [
321
+ 1e4,
322
+ 3e4,
323
+ 6e4
324
+ ];
325
+ const RATE_BPS = [
326
+ 500 * 1024,
327
+ 100 * 1024,
328
+ 50 * 1024
329
+ ];
330
+ const i = Math.min(attempt, GRACE_MS.length - 1);
331
+ return GRACE_MS[i] + Math.ceil(sizeBytes / RATE_BPS[i] * 1e3);
332
+ }
333
+ /**
334
+ * PUT a body to a presigned R2 url with bounded retry.
335
+ *
336
+ * Returns the ETag string when the bucket exposes it, or `null` when the
337
+ * response header is absent (e.g. bucket CORS not exposing ETag). Network/HTTP
338
+ * failures and per-attempt timeouts trigger retries; a caller abort and a
339
+ * missing ETag do not (the latter is a deterministic misconfiguration, not a
340
+ * transient error).
341
+ *
342
+ * @internal exported for tests
343
+ */
344
+ async function putWithRetry(url, body, signal, sizeBytes, attempts = 3, attemptTimeouts) {
345
+ let lastErr;
346
+ for (let i = 0; i < attempts; i++) {
347
+ if (signal?.aborted) throw new Error("Upload aborted");
348
+ const timeoutMs = attemptTimeouts?.[i] ?? attemptTimeoutMs(sizeBytes, i);
349
+ const timeout = AbortSignal.timeout(timeoutMs);
350
+ const attemptSignal = signal ? AbortSignal.any([signal, timeout]) : timeout;
351
+ try {
352
+ const res = await fetch(url, {
353
+ method: "PUT",
354
+ body,
355
+ signal: attemptSignal
356
+ });
357
+ if (!res.ok) throw new Error(`Upload PUT failed: ${res.status}`);
358
+ return res.headers.get("etag");
359
+ } catch (e) {
360
+ lastErr = e;
361
+ if (signal?.aborted) throw e;
362
+ if (i < attempts - 1) await new Promise((r) => setTimeout(r, 200 * 2 ** i));
363
+ }
364
+ }
365
+ throw lastErr;
366
+ }
367
+ /**
368
+ * Run `fn` over `items` with at most `limit` in flight; preserves order.
369
+ * Workers stop pulling new items once `signal` is aborted, enabling fast
370
+ * cancellation when one part fails.
371
+ */
372
+ async function mapLimit(items, limit, fn, signal) {
373
+ const out = new Array(items.length);
374
+ let next = 0;
375
+ const worker = async () => {
376
+ while (next < items.length) {
377
+ if (signal?.aborted) break;
378
+ const i = next++;
379
+ out[i] = await fn(items[i]);
380
+ }
381
+ };
382
+ const workers = Math.min(Math.max(1, limit), items.length);
383
+ await Promise.all(Array.from({ length: workers }, worker));
384
+ return out;
385
+ }
305
386
  function createUploadsResource(request) {
306
- return { async upload(file, options) {
307
- return request("/uploads", {
387
+ return { async create(file, options) {
388
+ const blob = toBlob(file);
389
+ const size = options.size ?? blob.size;
390
+ if (options.signal?.aborted) throw new Error("Upload aborted");
391
+ const init = await (await request("/assets", {
392
+ method: "POST",
393
+ body: {
394
+ filename: options.filename,
395
+ size,
396
+ contentType: options.contentType,
397
+ checksum: options.checksum,
398
+ lifecycle: options.persist ? "persisted" : "ephemeral"
399
+ },
400
+ raw: true,
401
+ signal: options.signal
402
+ })).json();
403
+ if (init.status === "deduplicated") {
404
+ options.onProgress?.({
405
+ loaded: size,
406
+ total: size
407
+ });
408
+ return init.data;
409
+ }
410
+ if (init.status === "presigned") {
411
+ await putWithRetry(init.upload.url, blob, options.signal, size);
412
+ options.onProgress?.({
413
+ loaded: size,
414
+ total: size
415
+ });
416
+ return request(`/assets/${init.data.id}/complete`, {
417
+ method: "POST",
418
+ body: {},
419
+ signal: options.signal
420
+ });
421
+ }
422
+ const internal = new AbortController();
423
+ const combinedSignal = options.signal ? AbortSignal.any([options.signal, internal.signal]) : internal.signal;
424
+ const { partSize, parts } = init.upload;
425
+ let loaded = 0;
426
+ let partError = null;
427
+ const completed = await mapLimit(parts, options.concurrency ?? 5, async (p) => {
428
+ const start = (p.partNumber - 1) * partSize;
429
+ const chunk = blob.slice(start, Math.min(start + partSize, size));
430
+ let etag;
431
+ try {
432
+ etag = await putWithRetry(p.url, chunk, combinedSignal, chunk.size);
433
+ } catch (e) {
434
+ internal.abort();
435
+ partError = e instanceof Error ? e : new Error(String(e));
436
+ throw partError;
437
+ }
438
+ if (etag === null) {
439
+ internal.abort();
440
+ const corsErr = /* @__PURE__ */ new Error("Multipart upload requires the ETag response header. Configure the R2 bucket CORS to expose 'ETag'.");
441
+ partError = corsErr;
442
+ throw corsErr;
443
+ }
444
+ loaded += chunk.size;
445
+ options.onProgress?.({
446
+ loaded,
447
+ total: size
448
+ });
449
+ return {
450
+ partNumber: p.partNumber,
451
+ etag
452
+ };
453
+ }, combinedSignal);
454
+ if (partError) throw partError;
455
+ completed.sort((a, b) => a.partNumber - b.partNumber);
456
+ return request(`/assets/${init.data.id}/complete`, {
308
457
  method: "POST",
309
- bodyRaw: file,
310
- query: options?.filename ? { filename: options.filename } : void 0,
311
- signal: options?.signal
458
+ body: { parts: completed },
459
+ signal: options.signal
312
460
  });
313
461
  } };
314
462
  }
@@ -448,12 +596,32 @@ function createBatchesResource(request) {
448
596
  //#endregion
449
597
  //#region src/resources/assets.ts
450
598
  function createAssetsResource(request) {
451
- return { async templates(params, options) {
452
- return request("/assets/templates", {
453
- query: params ? { ...params } : void 0,
454
- signal: options?.signal
455
- });
456
- } };
599
+ return {
600
+ async list(params, options) {
601
+ return request("/assets", {
602
+ query: params,
603
+ signal: options?.signal
604
+ });
605
+ },
606
+ async get(id, options) {
607
+ return request(`/assets/${id}`, { signal: options?.signal });
608
+ },
609
+ async delete(id, options) {
610
+ return request(`/assets/${id}`, {
611
+ method: "DELETE",
612
+ signal: options?.signal
613
+ });
614
+ },
615
+ async download(id, options) {
616
+ return request(`/assets/${id}/download`, { signal: options?.signal });
617
+ },
618
+ async templates(params, options) {
619
+ return request("/assets/templates", {
620
+ query: params,
621
+ signal: options?.signal
622
+ });
623
+ }
624
+ };
457
625
  }
458
626
  //#endregion
459
627
  //#region src/resources/team.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rendobar/sdk",
3
- "version": "2.2.0",
3
+ "version": "3.0.1",
4
4
  "type": "module",
5
5
  "description": "TypeScript client for the Rendobar media processing API",
6
6
  "main": "./dist/index.cjs",