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