@rendobar/sdk 2.1.2 → 3.0.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.mjs CHANGED
@@ -302,13 +302,130 @@ 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
+ * PUT a body to a presigned R2 url with bounded retry.
310
+ *
311
+ * Returns the ETag string when the bucket exposes it, or `null` when the
312
+ * response header is absent (e.g. bucket CORS not exposing ETag). Only
313
+ * network/HTTP failures trigger retries; a missing ETag is not retried because
314
+ * it is a deterministic misconfiguration, not a transient error.
315
+ */
316
+ async function putWithRetry(url, body, signal, attempts = 3) {
317
+ let lastErr;
318
+ for (let i = 0; i < attempts; i++) {
319
+ if (signal?.aborted) throw new Error("Upload aborted");
320
+ try {
321
+ const res = await fetch(url, {
322
+ method: "PUT",
323
+ body,
324
+ signal
325
+ });
326
+ if (!res.ok) throw new Error(`Upload PUT failed: ${res.status}`);
327
+ return res.headers.get("etag");
328
+ } catch (e) {
329
+ lastErr = e;
330
+ if (signal?.aborted) throw e;
331
+ if (i < attempts - 1) await new Promise((r) => setTimeout(r, 200 * 2 ** i));
332
+ }
333
+ }
334
+ throw lastErr;
335
+ }
336
+ /**
337
+ * Run `fn` over `items` with at most `limit` in flight; preserves order.
338
+ * Workers stop pulling new items once `signal` is aborted, enabling fast
339
+ * cancellation when one part fails.
340
+ */
341
+ async function mapLimit(items, limit, fn, signal) {
342
+ const out = new Array(items.length);
343
+ let next = 0;
344
+ const worker = async () => {
345
+ while (next < items.length) {
346
+ if (signal?.aborted) break;
347
+ const i = next++;
348
+ out[i] = await fn(items[i]);
349
+ }
350
+ };
351
+ const workers = Math.min(Math.max(1, limit), items.length);
352
+ await Promise.all(Array.from({ length: workers }, worker));
353
+ return out;
354
+ }
305
355
  function createUploadsResource(request) {
306
- return { async upload(file, options) {
307
- return request("/uploads", {
356
+ return { async create(file, options) {
357
+ const blob = toBlob(file);
358
+ const size = options.size ?? blob.size;
359
+ if (options.signal?.aborted) throw new Error("Upload aborted");
360
+ const init = await (await request("/assets", {
308
361
  method: "POST",
309
- bodyRaw: file,
310
- query: options?.filename ? { filename: options.filename } : void 0,
311
- signal: options?.signal
362
+ body: {
363
+ filename: options.filename,
364
+ size,
365
+ contentType: options.contentType,
366
+ checksum: options.checksum,
367
+ lifecycle: options.persist ? "persisted" : "ephemeral"
368
+ },
369
+ raw: true,
370
+ signal: options.signal
371
+ })).json();
372
+ if (init.status === "deduplicated") {
373
+ options.onProgress?.({
374
+ loaded: size,
375
+ total: size
376
+ });
377
+ return init.data;
378
+ }
379
+ if (init.status === "presigned") {
380
+ await putWithRetry(init.upload.url, blob, options.signal);
381
+ options.onProgress?.({
382
+ loaded: size,
383
+ total: size
384
+ });
385
+ return request(`/assets/${init.data.id}/complete`, {
386
+ method: "POST",
387
+ body: {},
388
+ signal: options.signal
389
+ });
390
+ }
391
+ const internal = new AbortController();
392
+ const combinedSignal = options.signal ? AbortSignal.any([options.signal, internal.signal]) : internal.signal;
393
+ const { partSize, parts } = init.upload;
394
+ let loaded = 0;
395
+ let partError = null;
396
+ const completed = await mapLimit(parts, options.concurrency ?? 5, async (p) => {
397
+ const start = (p.partNumber - 1) * partSize;
398
+ const chunk = blob.slice(start, Math.min(start + partSize, size));
399
+ let etag;
400
+ try {
401
+ etag = await putWithRetry(p.url, chunk, combinedSignal);
402
+ } catch (e) {
403
+ internal.abort();
404
+ partError = e instanceof Error ? e : new Error(String(e));
405
+ throw partError;
406
+ }
407
+ if (etag === null) {
408
+ internal.abort();
409
+ const corsErr = /* @__PURE__ */ new Error("Multipart upload requires the ETag response header. Configure the R2 bucket CORS to expose 'ETag'.");
410
+ partError = corsErr;
411
+ throw corsErr;
412
+ }
413
+ loaded += chunk.size;
414
+ options.onProgress?.({
415
+ loaded,
416
+ total: size
417
+ });
418
+ return {
419
+ partNumber: p.partNumber,
420
+ etag
421
+ };
422
+ }, combinedSignal);
423
+ if (partError) throw partError;
424
+ completed.sort((a, b) => a.partNumber - b.partNumber);
425
+ return request(`/assets/${init.data.id}/complete`, {
426
+ method: "POST",
427
+ body: { parts: completed },
428
+ signal: options.signal
312
429
  });
313
430
  } };
314
431
  }
@@ -448,12 +565,32 @@ function createBatchesResource(request) {
448
565
  //#endregion
449
566
  //#region src/resources/assets.ts
450
567
  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
- } };
568
+ return {
569
+ async list(params, options) {
570
+ return request("/assets", {
571
+ query: params,
572
+ signal: options?.signal
573
+ });
574
+ },
575
+ async get(id, options) {
576
+ return request(`/assets/${id}`, { signal: options?.signal });
577
+ },
578
+ async delete(id, options) {
579
+ return request(`/assets/${id}`, {
580
+ method: "DELETE",
581
+ signal: options?.signal
582
+ });
583
+ },
584
+ async download(id, options) {
585
+ return request(`/assets/${id}/download`, { signal: options?.signal });
586
+ },
587
+ async templates(params, options) {
588
+ return request("/assets/templates", {
589
+ query: params,
590
+ signal: options?.signal
591
+ });
592
+ }
593
+ };
457
594
  }
458
595
  //#endregion
459
596
  //#region src/resources/team.ts
@@ -645,4 +782,40 @@ function createClient(config = {}) {
645
782
  };
646
783
  }
647
784
  //#endregion
648
- export { ApiError, createClient, isApiError };
785
+ //#region src/types.ts
786
+ /**
787
+ * Primary playable/download URL for a completed job, or `undefined`.
788
+ *
789
+ * Returns `output.file.url` — the single download for a file job or the playlist
790
+ * URL for a stream. A pure set (multiple files, no headline) or a data-only job
791
+ * has no single primary URL: inspect `job.output.files` / `job.output.data`.
792
+ */
793
+ function outputUrl(job) {
794
+ if (job.status !== "complete") return void 0;
795
+ return job.output.file?.url;
796
+ }
797
+ /**
798
+ * Typed accessor for a completed job's structured `data`, or `null`.
799
+ *
800
+ * `output.data` is job-type-specific (probe result, detections, transcript) and
801
+ * is `unknown` at the contract level — its shape is documented per job type, not
802
+ * by the API schema. This helper applies the caller-supplied type `T` so you can
803
+ * read `data` without sprinkling assertions at each call site. `T` is your claim
804
+ * about the shape for the job type you submitted; validate it yourself (e.g. with
805
+ * Zod) if the input is untrusted.
806
+ *
807
+ * Returns `null` for non-complete jobs and for jobs with no structured data
808
+ * (pure file transforms).
809
+ *
810
+ * @example
811
+ * type Metadata = { format: string; durationMs: number };
812
+ * const meta = jobData<Metadata>(await client.jobs.wait(id));
813
+ * if (meta) console.log(meta.durationMs);
814
+ */
815
+ function jobData(job) {
816
+ if (job.status !== "complete") return null;
817
+ if (job.output.data == null) return null;
818
+ return job.output.data;
819
+ }
820
+ //#endregion
821
+ export { ApiError, createClient, isApiError, jobData, outputUrl };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rendobar/sdk",
3
- "version": "2.1.2",
3
+ "version": "3.0.0",
4
4
  "type": "module",
5
5
  "description": "TypeScript client for the Rendobar media processing API",
6
6
  "main": "./dist/index.cjs",