@electric-sql/client 1.1.0 → 1.1.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/cjs/index.cjs +76 -8
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +22 -0
- package/dist/index.browser.mjs +3 -3
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.ts +22 -0
- package/dist/index.legacy-esm.js +76 -8
- package/dist/index.legacy-esm.js.map +1 -1
- package/dist/index.mjs +76 -8
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/fetch.ts +126 -9
package/dist/index.d.ts
CHANGED
|
@@ -135,16 +135,38 @@ interface BackoffOptions {
|
|
|
135
135
|
initialDelay: number;
|
|
136
136
|
/**
|
|
137
137
|
* Maximum retry delay in milliseconds
|
|
138
|
+
* After reaching this, delay stays constant (e.g., retry every 60s)
|
|
138
139
|
*/
|
|
139
140
|
maxDelay: number;
|
|
140
141
|
multiplier: number;
|
|
141
142
|
onFailedAttempt?: () => void;
|
|
142
143
|
debug?: boolean;
|
|
144
|
+
/**
|
|
145
|
+
* Maximum number of retry attempts before giving up.
|
|
146
|
+
* Set to Infinity (default) for indefinite retries - needed for offline scenarios
|
|
147
|
+
* where clients may go offline and come back later.
|
|
148
|
+
*
|
|
149
|
+
* The retry budget provides protection against retry storms even with infinite retries.
|
|
150
|
+
*/
|
|
151
|
+
maxRetries?: number;
|
|
152
|
+
/**
|
|
153
|
+
* Percentage of requests that can be retries (0.1 = 10%)
|
|
154
|
+
*
|
|
155
|
+
* This is the primary load shedding mechanism. It limits the *rate* of retries,
|
|
156
|
+
* not the total count. Even with infinite retries, at most 10% of your traffic
|
|
157
|
+
* will be retries, preventing retry storms from amplifying server load.
|
|
158
|
+
*
|
|
159
|
+
* The budget resets every 60 seconds, so a temporary spike of errors won't
|
|
160
|
+
* permanently exhaust the budget.
|
|
161
|
+
*/
|
|
162
|
+
retryBudgetPercent?: number;
|
|
143
163
|
}
|
|
144
164
|
declare const BackoffDefaults: {
|
|
145
165
|
initialDelay: number;
|
|
146
166
|
maxDelay: number;
|
|
147
167
|
multiplier: number;
|
|
168
|
+
maxRetries: number;
|
|
169
|
+
retryBudgetPercent: number;
|
|
148
170
|
};
|
|
149
171
|
|
|
150
172
|
declare const LIVE_CACHE_BUSTER_QUERY_PARAM = "cursor";
|
package/dist/index.legacy-esm.js
CHANGED
|
@@ -316,8 +316,13 @@ var ELECTRIC_PROTOCOL_QUERY_PARAMS = [
|
|
|
316
316
|
var HTTP_RETRY_STATUS_CODES = [429];
|
|
317
317
|
var BackoffDefaults = {
|
|
318
318
|
initialDelay: 100,
|
|
319
|
-
maxDelay:
|
|
320
|
-
|
|
319
|
+
maxDelay: 6e4,
|
|
320
|
+
// Cap at 60s - reasonable for long-lived connections
|
|
321
|
+
multiplier: 1.3,
|
|
322
|
+
maxRetries: Infinity,
|
|
323
|
+
// Retry forever - clients may go offline and come back
|
|
324
|
+
retryBudgetPercent: 0.1
|
|
325
|
+
// 10% retry budget prevents amplification
|
|
321
326
|
};
|
|
322
327
|
function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
|
|
323
328
|
const {
|
|
@@ -325,8 +330,29 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
|
|
|
325
330
|
maxDelay,
|
|
326
331
|
multiplier,
|
|
327
332
|
debug = false,
|
|
328
|
-
onFailedAttempt
|
|
333
|
+
onFailedAttempt,
|
|
334
|
+
maxRetries = Infinity,
|
|
335
|
+
retryBudgetPercent = 0.1
|
|
329
336
|
} = backoffOptions;
|
|
337
|
+
let totalRequests = 0;
|
|
338
|
+
let totalRetries = 0;
|
|
339
|
+
let budgetResetTime = Date.now() + 6e4;
|
|
340
|
+
function checkRetryBudget(percent) {
|
|
341
|
+
const now = Date.now();
|
|
342
|
+
if (now > budgetResetTime) {
|
|
343
|
+
totalRequests = 0;
|
|
344
|
+
totalRetries = 0;
|
|
345
|
+
budgetResetTime = now + 6e4;
|
|
346
|
+
}
|
|
347
|
+
totalRequests++;
|
|
348
|
+
if (totalRequests < 10) return true;
|
|
349
|
+
const currentRetryRate = totalRetries / totalRequests;
|
|
350
|
+
const hasCapacity = currentRetryRate < percent;
|
|
351
|
+
if (hasCapacity) {
|
|
352
|
+
totalRetries++;
|
|
353
|
+
}
|
|
354
|
+
return hasCapacity;
|
|
355
|
+
}
|
|
330
356
|
return async (...args) => {
|
|
331
357
|
var _a;
|
|
332
358
|
const url = args[0];
|
|
@@ -336,7 +362,10 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
|
|
|
336
362
|
while (true) {
|
|
337
363
|
try {
|
|
338
364
|
const result = await fetchClient(...args);
|
|
339
|
-
if (result.ok)
|
|
365
|
+
if (result.ok) {
|
|
366
|
+
delay = initialDelay;
|
|
367
|
+
return result;
|
|
368
|
+
}
|
|
340
369
|
const err = await FetchError.fromResponse(result, url.toString());
|
|
341
370
|
throw err;
|
|
342
371
|
} catch (e) {
|
|
@@ -346,12 +375,51 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
|
|
|
346
375
|
} else if (e instanceof FetchError && !HTTP_RETRY_STATUS_CODES.includes(e.status) && e.status >= 400 && e.status < 500) {
|
|
347
376
|
throw e;
|
|
348
377
|
} else {
|
|
349
|
-
|
|
350
|
-
|
|
378
|
+
attempt++;
|
|
379
|
+
if (attempt >= maxRetries) {
|
|
380
|
+
if (debug) {
|
|
381
|
+
console.log(
|
|
382
|
+
`Max retries reached (${attempt}/${maxRetries}), giving up`
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
throw e;
|
|
386
|
+
}
|
|
387
|
+
if (!checkRetryBudget(retryBudgetPercent)) {
|
|
388
|
+
if (debug) {
|
|
389
|
+
console.log(
|
|
390
|
+
`Retry budget exhausted (attempt ${attempt}), backing off`
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
await new Promise((resolve) => setTimeout(resolve, maxDelay));
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
let serverMinimumMs = 0;
|
|
397
|
+
if (e instanceof FetchError && e.headers) {
|
|
398
|
+
const retryAfter = e.headers[`retry-after`];
|
|
399
|
+
if (retryAfter) {
|
|
400
|
+
const retryAfterSec = Number(retryAfter);
|
|
401
|
+
if (Number.isFinite(retryAfterSec) && retryAfterSec > 0) {
|
|
402
|
+
serverMinimumMs = retryAfterSec * 1e3;
|
|
403
|
+
} else {
|
|
404
|
+
const retryDate = Date.parse(retryAfter);
|
|
405
|
+
if (!isNaN(retryDate)) {
|
|
406
|
+
const deltaMs = retryDate - Date.now();
|
|
407
|
+
serverMinimumMs = Math.max(0, Math.min(deltaMs, 36e5));
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const jitter = Math.random() * delay;
|
|
413
|
+
const clientBackoffMs = Math.min(jitter, maxDelay);
|
|
414
|
+
const waitMs = Math.max(serverMinimumMs, clientBackoffMs);
|
|
351
415
|
if (debug) {
|
|
352
|
-
|
|
353
|
-
console.log(
|
|
416
|
+
const source = serverMinimumMs > 0 ? `server+client` : `client`;
|
|
417
|
+
console.log(
|
|
418
|
+
`Retry attempt #${attempt} after ${waitMs}ms (${source}, serverMin=${serverMinimumMs}ms, clientBackoff=${clientBackoffMs}ms)`
|
|
419
|
+
);
|
|
354
420
|
}
|
|
421
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
422
|
+
delay = Math.min(delay * multiplier, maxDelay);
|
|
355
423
|
}
|
|
356
424
|
}
|
|
357
425
|
}
|