@rendobar/sdk 3.0.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 +38 -7
- package/dist/index.mjs +38 -7
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -307,22 +307,53 @@ function toBlob(file) {
|
|
|
307
307
|
return file instanceof Blob ? file : new Blob([file]);
|
|
308
308
|
}
|
|
309
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
|
+
/**
|
|
310
335
|
* PUT a body to a presigned R2 url with bounded retry.
|
|
311
336
|
*
|
|
312
337
|
* Returns the ETag string when the bucket exposes it, or `null` when the
|
|
313
|
-
* response header is absent (e.g. bucket CORS not exposing ETag).
|
|
314
|
-
*
|
|
315
|
-
*
|
|
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
|
|
316
344
|
*/
|
|
317
|
-
async function putWithRetry(url, body, signal, attempts = 3) {
|
|
345
|
+
async function putWithRetry(url, body, signal, sizeBytes, attempts = 3, attemptTimeouts) {
|
|
318
346
|
let lastErr;
|
|
319
347
|
for (let i = 0; i < attempts; i++) {
|
|
320
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;
|
|
321
352
|
try {
|
|
322
353
|
const res = await fetch(url, {
|
|
323
354
|
method: "PUT",
|
|
324
355
|
body,
|
|
325
|
-
signal
|
|
356
|
+
signal: attemptSignal
|
|
326
357
|
});
|
|
327
358
|
if (!res.ok) throw new Error(`Upload PUT failed: ${res.status}`);
|
|
328
359
|
return res.headers.get("etag");
|
|
@@ -378,7 +409,7 @@ function createUploadsResource(request) {
|
|
|
378
409
|
return init.data;
|
|
379
410
|
}
|
|
380
411
|
if (init.status === "presigned") {
|
|
381
|
-
await putWithRetry(init.upload.url, blob, options.signal);
|
|
412
|
+
await putWithRetry(init.upload.url, blob, options.signal, size);
|
|
382
413
|
options.onProgress?.({
|
|
383
414
|
loaded: size,
|
|
384
415
|
total: size
|
|
@@ -399,7 +430,7 @@ function createUploadsResource(request) {
|
|
|
399
430
|
const chunk = blob.slice(start, Math.min(start + partSize, size));
|
|
400
431
|
let etag;
|
|
401
432
|
try {
|
|
402
|
-
etag = await putWithRetry(p.url, chunk, combinedSignal);
|
|
433
|
+
etag = await putWithRetry(p.url, chunk, combinedSignal, chunk.size);
|
|
403
434
|
} catch (e) {
|
|
404
435
|
internal.abort();
|
|
405
436
|
partError = e instanceof Error ? e : new Error(String(e));
|
package/dist/index.mjs
CHANGED
|
@@ -306,22 +306,53 @@ function toBlob(file) {
|
|
|
306
306
|
return file instanceof Blob ? file : new Blob([file]);
|
|
307
307
|
}
|
|
308
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
|
+
/**
|
|
309
334
|
* PUT a body to a presigned R2 url with bounded retry.
|
|
310
335
|
*
|
|
311
336
|
* 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).
|
|
313
|
-
*
|
|
314
|
-
*
|
|
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
|
|
315
343
|
*/
|
|
316
|
-
async function putWithRetry(url, body, signal, attempts = 3) {
|
|
344
|
+
async function putWithRetry(url, body, signal, sizeBytes, attempts = 3, attemptTimeouts) {
|
|
317
345
|
let lastErr;
|
|
318
346
|
for (let i = 0; i < attempts; i++) {
|
|
319
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;
|
|
320
351
|
try {
|
|
321
352
|
const res = await fetch(url, {
|
|
322
353
|
method: "PUT",
|
|
323
354
|
body,
|
|
324
|
-
signal
|
|
355
|
+
signal: attemptSignal
|
|
325
356
|
});
|
|
326
357
|
if (!res.ok) throw new Error(`Upload PUT failed: ${res.status}`);
|
|
327
358
|
return res.headers.get("etag");
|
|
@@ -377,7 +408,7 @@ function createUploadsResource(request) {
|
|
|
377
408
|
return init.data;
|
|
378
409
|
}
|
|
379
410
|
if (init.status === "presigned") {
|
|
380
|
-
await putWithRetry(init.upload.url, blob, options.signal);
|
|
411
|
+
await putWithRetry(init.upload.url, blob, options.signal, size);
|
|
381
412
|
options.onProgress?.({
|
|
382
413
|
loaded: size,
|
|
383
414
|
total: size
|
|
@@ -398,7 +429,7 @@ function createUploadsResource(request) {
|
|
|
398
429
|
const chunk = blob.slice(start, Math.min(start + partSize, size));
|
|
399
430
|
let etag;
|
|
400
431
|
try {
|
|
401
|
-
etag = await putWithRetry(p.url, chunk, combinedSignal);
|
|
432
|
+
etag = await putWithRetry(p.url, chunk, combinedSignal, chunk.size);
|
|
402
433
|
} catch (e) {
|
|
403
434
|
internal.abort();
|
|
404
435
|
partError = e instanceof Error ? e : new Error(String(e));
|