@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.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
|
|
307
|
-
|
|
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
|
-
|
|
310
|
-
|
|
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 {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|