@electric-sql/client 1.0.14 → 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 +86 -10
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +28 -2
- package/dist/index.browser.mjs +3 -3
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.ts +28 -2
- package/dist/index.legacy-esm.js +86 -10
- package/dist/index.legacy-esm.js.map +1 -1
- package/dist/index.mjs +86 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +18 -4
- package/src/constants.ts +4 -0
- package/src/fetch.ts +130 -15
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";
|
|
@@ -278,9 +300,13 @@ interface ShapeStreamOptions<T = never> {
|
|
|
278
300
|
*/
|
|
279
301
|
subscribe?: boolean;
|
|
280
302
|
/**
|
|
281
|
-
*
|
|
303
|
+
* @deprecated No longer experimental, use {@link liveSse} instead.
|
|
282
304
|
*/
|
|
283
305
|
experimentalLiveSse?: boolean;
|
|
306
|
+
/**
|
|
307
|
+
* Use Server-Sent Events (SSE) for live updates.
|
|
308
|
+
*/
|
|
309
|
+
liveSse?: boolean;
|
|
284
310
|
/**
|
|
285
311
|
* Initial data loading mode
|
|
286
312
|
*/
|
|
@@ -348,7 +374,7 @@ interface ShapeStreamInterface<T extends Row<unknown> = Row> {
|
|
|
348
374
|
* ```
|
|
349
375
|
* const stream = new ShapeStream({
|
|
350
376
|
* url: `http://localhost:3000/v1/shape`,
|
|
351
|
-
*
|
|
377
|
+
* liveSse: true
|
|
352
378
|
* })
|
|
353
379
|
* ```
|
|
354
380
|
*
|
package/dist/index.legacy-esm.js
CHANGED
|
@@ -289,6 +289,7 @@ var WHERE_QUERY_PARAM = `where`;
|
|
|
289
289
|
var REPLICA_PARAM = `replica`;
|
|
290
290
|
var WHERE_PARAMS_PARAM = `params`;
|
|
291
291
|
var EXPERIMENTAL_LIVE_SSE_QUERY_PARAM = `experimental_live_sse`;
|
|
292
|
+
var LIVE_SSE_QUERY_PARAM = `live_sse`;
|
|
292
293
|
var FORCE_DISCONNECT_AND_REFRESH = `force-disconnect-and-refresh`;
|
|
293
294
|
var PAUSE_STREAM = `pause-stream`;
|
|
294
295
|
var LOG_MODE_QUERY_PARAM = `log`;
|
|
@@ -315,8 +316,13 @@ var ELECTRIC_PROTOCOL_QUERY_PARAMS = [
|
|
|
315
316
|
var HTTP_RETRY_STATUS_CODES = [429];
|
|
316
317
|
var BackoffDefaults = {
|
|
317
318
|
initialDelay: 100,
|
|
318
|
-
maxDelay:
|
|
319
|
-
|
|
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
|
|
320
326
|
};
|
|
321
327
|
function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
|
|
322
328
|
const {
|
|
@@ -324,8 +330,29 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
|
|
|
324
330
|
maxDelay,
|
|
325
331
|
multiplier,
|
|
326
332
|
debug = false,
|
|
327
|
-
onFailedAttempt
|
|
333
|
+
onFailedAttempt,
|
|
334
|
+
maxRetries = Infinity,
|
|
335
|
+
retryBudgetPercent = 0.1
|
|
328
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
|
+
}
|
|
329
356
|
return async (...args) => {
|
|
330
357
|
var _a;
|
|
331
358
|
const url = args[0];
|
|
@@ -335,7 +362,10 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
|
|
|
335
362
|
while (true) {
|
|
336
363
|
try {
|
|
337
364
|
const result = await fetchClient(...args);
|
|
338
|
-
if (result.ok)
|
|
365
|
+
if (result.ok) {
|
|
366
|
+
delay = initialDelay;
|
|
367
|
+
return result;
|
|
368
|
+
}
|
|
339
369
|
const err = await FetchError.fromResponse(result, url.toString());
|
|
340
370
|
throw err;
|
|
341
371
|
} catch (e) {
|
|
@@ -345,12 +375,51 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
|
|
|
345
375
|
} else if (e instanceof FetchError && !HTTP_RETRY_STATUS_CODES.includes(e.status) && e.status >= 400 && e.status < 500) {
|
|
346
376
|
throw e;
|
|
347
377
|
} else {
|
|
348
|
-
|
|
349
|
-
|
|
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);
|
|
350
415
|
if (debug) {
|
|
351
|
-
|
|
352
|
-
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
|
+
);
|
|
353
420
|
}
|
|
421
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
422
|
+
delay = Math.min(delay * multiplier, maxDelay);
|
|
354
423
|
}
|
|
355
424
|
}
|
|
356
425
|
}
|
|
@@ -359,6 +428,7 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
|
|
|
359
428
|
var NO_BODY_STATUS_CODES = [201, 204, 205];
|
|
360
429
|
function createFetchWithConsumedMessages(fetchClient) {
|
|
361
430
|
return async (...args) => {
|
|
431
|
+
var _a, _b;
|
|
362
432
|
const url = args[0];
|
|
363
433
|
const res = await fetchClient(...args);
|
|
364
434
|
try {
|
|
@@ -368,6 +438,9 @@ function createFetchWithConsumedMessages(fetchClient) {
|
|
|
368
438
|
const text = await res.text();
|
|
369
439
|
return new Response(text, res);
|
|
370
440
|
} catch (err) {
|
|
441
|
+
if ((_b = (_a = args[1]) == null ? void 0 : _a.signal) == null ? void 0 : _b.aborted) {
|
|
442
|
+
throw new FetchBackoffAbortError();
|
|
443
|
+
}
|
|
371
444
|
throw new FetchError(
|
|
372
445
|
res.status,
|
|
373
446
|
void 0,
|
|
@@ -986,7 +1059,7 @@ requestShape_fn = async function() {
|
|
|
986
1059
|
}
|
|
987
1060
|
const newShapeHandle = e.headers[SHAPE_HANDLE_HEADER] || `${__privateGet(this, _shapeHandle)}-next`;
|
|
988
1061
|
__privateMethod(this, _ShapeStream_instances, reset_fn).call(this, newShapeHandle);
|
|
989
|
-
await __privateMethod(this, _ShapeStream_instances, publish_fn).call(this, e.json);
|
|
1062
|
+
await __privateMethod(this, _ShapeStream_instances, publish_fn).call(this, Array.isArray(e.json) ? e.json : [e.json]);
|
|
990
1063
|
return __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
|
|
991
1064
|
} else {
|
|
992
1065
|
__privateMethod(this, _ShapeStream_instances, sendErrorToSubscribers_fn).call(this, e);
|
|
@@ -1129,8 +1202,11 @@ onMessages_fn = async function(batch, isSseMessage = false) {
|
|
|
1129
1202
|
}
|
|
1130
1203
|
};
|
|
1131
1204
|
fetchShape_fn = async function(opts) {
|
|
1132
|
-
|
|
1205
|
+
var _a;
|
|
1206
|
+
const useSse = (_a = this.options.liveSse) != null ? _a : this.options.experimentalLiveSse;
|
|
1207
|
+
if (__privateGet(this, _isUpToDate) && useSse && !__privateGet(this, _isRefreshing) && !opts.resumingFromPause) {
|
|
1133
1208
|
opts.fetchUrl.searchParams.set(EXPERIMENTAL_LIVE_SSE_QUERY_PARAM, `true`);
|
|
1209
|
+
opts.fetchUrl.searchParams.set(LIVE_SSE_QUERY_PARAM, `true`);
|
|
1134
1210
|
return __privateMethod(this, _ShapeStream_instances, requestShapeSSE_fn).call(this, opts);
|
|
1135
1211
|
}
|
|
1136
1212
|
return __privateMethod(this, _ShapeStream_instances, requestShapeLongPoll_fn).call(this, opts);
|