@electric-sql/client 1.1.2 → 1.1.4
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 +58 -12
- 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 +58 -12
- package/dist/index.legacy-esm.js.map +1 -1
- package/dist/index.mjs +58 -12
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +70 -6
- 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
|
}
|
|
@@ -716,9 +751,8 @@ function canonicalShapeKey(url) {
|
|
|
716
751
|
cleanUrl.searchParams.sort();
|
|
717
752
|
return cleanUrl.toString();
|
|
718
753
|
}
|
|
719
|
-
var _error, _fetchClient2, _sseFetchClient, _messageParser, _subscribers, _started, _state, _lastOffset, _liveCacheBuster, _lastSyncedAt, _isUpToDate, _isMidStream, _connected, _shapeHandle, _mode, _schema, _onError, _requestAbortController, _isRefreshing, _tickPromise, _tickPromiseResolver, _tickPromiseRejecter, _messageChain, _snapshotTracker, _activeSnapshotRequests, _midStreamPromise, _midStreamPromiseResolver, _lastSseConnectionStartTime, _minSseConnectionDuration, _consecutiveShortSseConnections, _maxShortSseConnections, _sseFallbackToLongPolling, _sseBackoffBaseDelay, _sseBackoffMaxDelay, _ShapeStream_instances, start_fn, requestShape_fn, constructUrl_fn, createAbortListener_fn, onInitialResponse_fn, onMessages_fn, fetchShape_fn, requestShapeLongPoll_fn, requestShapeSSE_fn, pause_fn, resume_fn, nextTick_fn, waitForStreamEnd_fn, publish_fn, sendErrorToSubscribers_fn, subscribeToVisibilityChanges_fn, reset_fn, fetchSnapshot_fn;
|
|
754
|
+
var _error, _fetchClient2, _sseFetchClient, _messageParser, _subscribers, _started, _state, _lastOffset, _liveCacheBuster, _lastSyncedAt, _isUpToDate, _isMidStream, _connected, _shapeHandle, _mode, _schema, _onError, _requestAbortController, _isRefreshing, _tickPromise, _tickPromiseResolver, _tickPromiseRejecter, _messageChain, _snapshotTracker, _activeSnapshotRequests, _midStreamPromise, _midStreamPromiseResolver, _lastSseConnectionStartTime, _minSseConnectionDuration, _consecutiveShortSseConnections, _maxShortSseConnections, _sseFallbackToLongPolling, _sseBackoffBaseDelay, _sseBackoffMaxDelay, _unsubscribeFromVisibilityChanges, _ShapeStream_instances, start_fn, requestShape_fn, constructUrl_fn, createAbortListener_fn, onInitialResponse_fn, onMessages_fn, fetchShape_fn, requestShapeLongPoll_fn, requestShapeSSE_fn, pause_fn, resume_fn, nextTick_fn, waitForStreamEnd_fn, publish_fn, sendErrorToSubscribers_fn, subscribeToVisibilityChanges_fn, reset_fn, fetchSnapshot_fn;
|
|
720
755
|
var ShapeStream = class {
|
|
721
|
-
// Maximum delay cap (ms)
|
|
722
756
|
constructor(options) {
|
|
723
757
|
__privateAdd(this, _ShapeStream_instances);
|
|
724
758
|
__privateAdd(this, _error, null);
|
|
@@ -762,6 +796,8 @@ var ShapeStream = class {
|
|
|
762
796
|
__privateAdd(this, _sseBackoffBaseDelay, 100);
|
|
763
797
|
// Base delay for exponential backoff (ms)
|
|
764
798
|
__privateAdd(this, _sseBackoffMaxDelay, 5e3);
|
|
799
|
+
// Maximum delay cap (ms)
|
|
800
|
+
__privateAdd(this, _unsubscribeFromVisibilityChanges);
|
|
765
801
|
var _a, _b, _c, _d;
|
|
766
802
|
this.options = __spreadValues({ subscribe: true }, options);
|
|
767
803
|
validateOptions(this.options);
|
|
@@ -817,7 +853,9 @@ var ShapeStream = class {
|
|
|
817
853
|
};
|
|
818
854
|
}
|
|
819
855
|
unsubscribeAll() {
|
|
856
|
+
var _a;
|
|
820
857
|
__privateGet(this, _subscribers).clear();
|
|
858
|
+
(_a = __privateGet(this, _unsubscribeFromVisibilityChanges)) == null ? void 0 : _a.call(this);
|
|
821
859
|
}
|
|
822
860
|
/** Unix time at which we last synced. Undefined when `isLoading` is true. */
|
|
823
861
|
lastSyncedAt() {
|
|
@@ -940,6 +978,7 @@ _maxShortSseConnections = new WeakMap();
|
|
|
940
978
|
_sseFallbackToLongPolling = new WeakMap();
|
|
941
979
|
_sseBackoffBaseDelay = new WeakMap();
|
|
942
980
|
_sseBackoffMaxDelay = new WeakMap();
|
|
981
|
+
_unsubscribeFromVisibilityChanges = new WeakMap();
|
|
943
982
|
_ShapeStream_instances = new WeakSet();
|
|
944
983
|
start_fn = async function() {
|
|
945
984
|
var _a, _b, _c, _d, _e;
|
|
@@ -1006,7 +1045,8 @@ requestShape_fn = async function() {
|
|
|
1006
1045
|
return __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
|
|
1007
1046
|
}
|
|
1008
1047
|
if (e instanceof FetchBackoffAbortError) {
|
|
1009
|
-
|
|
1048
|
+
const currentState = __privateGet(this, _state);
|
|
1049
|
+
if (requestAbortController.signal.aborted && requestAbortController.signal.reason === PAUSE_STREAM && currentState === `pause-requested`) {
|
|
1010
1050
|
__privateSet(this, _state, `paused`);
|
|
1011
1051
|
}
|
|
1012
1052
|
return;
|
|
@@ -1252,7 +1292,10 @@ pause_fn = function() {
|
|
|
1252
1292
|
}
|
|
1253
1293
|
};
|
|
1254
1294
|
resume_fn = function() {
|
|
1255
|
-
if (__privateGet(this, _started) && __privateGet(this, _state) === `paused`) {
|
|
1295
|
+
if (__privateGet(this, _started) && (__privateGet(this, _state) === `paused` || __privateGet(this, _state) === `pause-requested`)) {
|
|
1296
|
+
if (__privateGet(this, _state) === `pause-requested`) {
|
|
1297
|
+
__privateSet(this, _state, `active`);
|
|
1298
|
+
}
|
|
1256
1299
|
__privateMethod(this, _ShapeStream_instances, start_fn).call(this);
|
|
1257
1300
|
}
|
|
1258
1301
|
};
|
|
@@ -1318,6 +1361,9 @@ subscribeToVisibilityChanges_fn = function() {
|
|
|
1318
1361
|
}
|
|
1319
1362
|
};
|
|
1320
1363
|
document.addEventListener(`visibilitychange`, visibilityHandler);
|
|
1364
|
+
__privateSet(this, _unsubscribeFromVisibilityChanges, () => {
|
|
1365
|
+
document.removeEventListener(`visibilitychange`, visibilityHandler);
|
|
1366
|
+
});
|
|
1321
1367
|
}
|
|
1322
1368
|
};
|
|
1323
1369
|
/**
|