@electric-sql/client 1.1.2 → 1.1.3
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/README.md +74 -12
- package/dist/cjs/index.cjs +43 -8
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +53 -4
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.ts +53 -4
- package/dist/index.legacy-esm.js +43 -8
- package/dist/index.legacy-esm.js.map +1 -1
- package/dist/index.mjs +43 -8
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +45 -4
- package/src/constants.ts +1 -0
- package/src/fetch.ts +75 -9
package/dist/index.d.ts
CHANGED
|
@@ -135,16 +135,24 @@ 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
|
+
maxRetries?: number;
|
|
143
150
|
}
|
|
144
151
|
declare const BackoffDefaults: {
|
|
145
152
|
initialDelay: number;
|
|
146
153
|
maxDelay: number;
|
|
147
154
|
multiplier: number;
|
|
155
|
+
maxRetries: number;
|
|
148
156
|
};
|
|
149
157
|
|
|
150
158
|
declare const LIVE_CACHE_BUSTER_QUERY_PARAM = "cursor";
|
|
@@ -296,10 +304,51 @@ interface ShapeStreamOptions<T = never> {
|
|
|
296
304
|
transformer?: TransformFunction<T>;
|
|
297
305
|
/**
|
|
298
306
|
* A function for handling shapestream errors.
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
307
|
+
*
|
|
308
|
+
* **Automatic retries**: The client automatically retries 5xx server errors, network
|
|
309
|
+
* errors, and 429 rate limits with exponential backoff. The `onError` callback is
|
|
310
|
+
* only invoked after these automatic retries are exhausted, or for non-retryable
|
|
311
|
+
* errors like 4xx client errors.
|
|
312
|
+
*
|
|
313
|
+
* When not provided, non-retryable errors will be thrown and syncing will stop.
|
|
314
|
+
*
|
|
315
|
+
* **Return value behavior**:
|
|
316
|
+
* - Return an **object** (RetryOpts or empty `{}`) to retry syncing:
|
|
317
|
+
* - `{}` - Retry with the same params and headers
|
|
318
|
+
* - `{ params }` - Retry with modified params
|
|
319
|
+
* - `{ headers }` - Retry with modified headers (e.g., refreshed auth token)
|
|
320
|
+
* - `{ params, headers }` - Retry with both modified
|
|
321
|
+
* - Return **void** or **undefined** to stop the stream permanently
|
|
322
|
+
*
|
|
323
|
+
* **Important**: If you want syncing to continue after an error (e.g., to retry
|
|
324
|
+
* on network failures), you MUST return at least an empty object `{}`. Simply
|
|
325
|
+
* logging the error and returning nothing will stop syncing.
|
|
326
|
+
*
|
|
327
|
+
* Supports async functions that return `Promise<void | RetryOpts>`.
|
|
328
|
+
*
|
|
329
|
+
* @example
|
|
330
|
+
* ```typescript
|
|
331
|
+
* // Retry on network errors, stop on others
|
|
332
|
+
* onError: (error) => {
|
|
333
|
+
* console.error('Stream error:', error)
|
|
334
|
+
* if (error instanceof FetchError && error.status >= 500) {
|
|
335
|
+
* return {} // Retry with same params
|
|
336
|
+
* }
|
|
337
|
+
* // Return void to stop on other errors
|
|
338
|
+
* }
|
|
339
|
+
* ```
|
|
340
|
+
*
|
|
341
|
+
* @example
|
|
342
|
+
* ```typescript
|
|
343
|
+
* // Refresh auth token on 401
|
|
344
|
+
* onError: async (error) => {
|
|
345
|
+
* if (error instanceof FetchError && error.status === 401) {
|
|
346
|
+
* const newToken = await refreshAuthToken()
|
|
347
|
+
* return { headers: { Authorization: `Bearer ${newToken}` } }
|
|
348
|
+
* }
|
|
349
|
+
* return {} // Retry other errors
|
|
350
|
+
* }
|
|
351
|
+
* ```
|
|
303
352
|
*/
|
|
304
353
|
onError?: ShapeStreamErrorHandler;
|
|
305
354
|
}
|
package/dist/index.legacy-esm.js
CHANGED
|
@@ -300,6 +300,7 @@ var SUBSET_PARAM_ORDER_BY = `subset__order_by`;
|
|
|
300
300
|
var SUBSET_PARAM_WHERE_PARAMS = `subset__params`;
|
|
301
301
|
var ELECTRIC_PROTOCOL_QUERY_PARAMS = [
|
|
302
302
|
LIVE_QUERY_PARAM,
|
|
303
|
+
LIVE_SSE_QUERY_PARAM,
|
|
303
304
|
SHAPE_HANDLE_QUERY_PARAM,
|
|
304
305
|
OFFSET_QUERY_PARAM,
|
|
305
306
|
LIVE_CACHE_BUSTER_QUERY_PARAM,
|
|
@@ -316,16 +317,33 @@ var ELECTRIC_PROTOCOL_QUERY_PARAMS = [
|
|
|
316
317
|
var HTTP_RETRY_STATUS_CODES = [429];
|
|
317
318
|
var BackoffDefaults = {
|
|
318
319
|
initialDelay: 100,
|
|
319
|
-
maxDelay:
|
|
320
|
-
|
|
320
|
+
maxDelay: 6e4,
|
|
321
|
+
// Cap at 60s - reasonable for long-lived connections
|
|
322
|
+
multiplier: 1.3,
|
|
323
|
+
maxRetries: Infinity
|
|
324
|
+
// Retry forever - clients may go offline and come back
|
|
321
325
|
};
|
|
326
|
+
function parseRetryAfterHeader(retryAfter) {
|
|
327
|
+
if (!retryAfter) return 0;
|
|
328
|
+
const retryAfterSec = Number(retryAfter);
|
|
329
|
+
if (Number.isFinite(retryAfterSec) && retryAfterSec > 0) {
|
|
330
|
+
return retryAfterSec * 1e3;
|
|
331
|
+
}
|
|
332
|
+
const retryDate = Date.parse(retryAfter);
|
|
333
|
+
if (!isNaN(retryDate)) {
|
|
334
|
+
const deltaMs = retryDate - Date.now();
|
|
335
|
+
return Math.max(0, Math.min(deltaMs, 36e5));
|
|
336
|
+
}
|
|
337
|
+
return 0;
|
|
338
|
+
}
|
|
322
339
|
function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
|
|
323
340
|
const {
|
|
324
341
|
initialDelay,
|
|
325
342
|
maxDelay,
|
|
326
343
|
multiplier,
|
|
327
344
|
debug = false,
|
|
328
|
-
onFailedAttempt
|
|
345
|
+
onFailedAttempt,
|
|
346
|
+
maxRetries = Infinity
|
|
329
347
|
} = backoffOptions;
|
|
330
348
|
return async (...args) => {
|
|
331
349
|
var _a;
|
|
@@ -336,7 +354,9 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
|
|
|
336
354
|
while (true) {
|
|
337
355
|
try {
|
|
338
356
|
const result = await fetchClient(...args);
|
|
339
|
-
if (result.ok)
|
|
357
|
+
if (result.ok) {
|
|
358
|
+
return result;
|
|
359
|
+
}
|
|
340
360
|
const err = await FetchError.fromResponse(result, url.toString());
|
|
341
361
|
throw err;
|
|
342
362
|
} catch (e) {
|
|
@@ -346,12 +366,27 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
|
|
|
346
366
|
} else if (e instanceof FetchError && !HTTP_RETRY_STATUS_CODES.includes(e.status) && e.status >= 400 && e.status < 500) {
|
|
347
367
|
throw e;
|
|
348
368
|
} else {
|
|
349
|
-
|
|
350
|
-
|
|
369
|
+
attempt++;
|
|
370
|
+
if (attempt > maxRetries) {
|
|
371
|
+
if (debug) {
|
|
372
|
+
console.log(
|
|
373
|
+
`Max retries reached (${attempt}/${maxRetries}), giving up`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
throw e;
|
|
377
|
+
}
|
|
378
|
+
const serverMinimumMs = e instanceof FetchError && e.headers ? parseRetryAfterHeader(e.headers[`retry-after`]) : 0;
|
|
379
|
+
const jitter = Math.random() * delay;
|
|
380
|
+
const clientBackoffMs = Math.min(jitter, maxDelay);
|
|
381
|
+
const waitMs = Math.max(serverMinimumMs, clientBackoffMs);
|
|
351
382
|
if (debug) {
|
|
352
|
-
|
|
353
|
-
console.log(
|
|
383
|
+
const source = serverMinimumMs > 0 ? `server+client` : `client`;
|
|
384
|
+
console.log(
|
|
385
|
+
`Retry attempt #${attempt} after ${waitMs}ms (${source}, serverMin=${serverMinimumMs}ms, clientBackoff=${clientBackoffMs}ms)`
|
|
386
|
+
);
|
|
354
387
|
}
|
|
388
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
389
|
+
delay = Math.min(delay * multiplier, maxDelay);
|
|
355
390
|
}
|
|
356
391
|
}
|
|
357
392
|
}
|