@electric-sql/client 1.1.1 → 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 +83 -65
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +45 -18
- package/dist/index.browser.mjs +3 -3
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.ts +45 -18
- package/dist/index.legacy-esm.js +83 -65
- package/dist/index.legacy-esm.js.map +1 -1
- package/dist/index.mjs +83 -65
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +142 -21
- package/src/constants.ts +1 -0
- package/src/fetch.ts +37 -88
package/dist/index.mjs
CHANGED
|
@@ -322,6 +322,7 @@ var SUBSET_PARAM_ORDER_BY = `subset__order_by`;
|
|
|
322
322
|
var SUBSET_PARAM_WHERE_PARAMS = `subset__params`;
|
|
323
323
|
var ELECTRIC_PROTOCOL_QUERY_PARAMS = [
|
|
324
324
|
LIVE_QUERY_PARAM,
|
|
325
|
+
LIVE_SSE_QUERY_PARAM,
|
|
325
326
|
SHAPE_HANDLE_QUERY_PARAM,
|
|
326
327
|
OFFSET_QUERY_PARAM,
|
|
327
328
|
LIVE_CACHE_BUSTER_QUERY_PARAM,
|
|
@@ -341,11 +342,22 @@ var BackoffDefaults = {
|
|
|
341
342
|
maxDelay: 6e4,
|
|
342
343
|
// Cap at 60s - reasonable for long-lived connections
|
|
343
344
|
multiplier: 1.3,
|
|
344
|
-
maxRetries: Infinity
|
|
345
|
+
maxRetries: Infinity
|
|
345
346
|
// Retry forever - clients may go offline and come back
|
|
346
|
-
retryBudgetPercent: 0.1
|
|
347
|
-
// 10% retry budget prevents amplification
|
|
348
347
|
};
|
|
348
|
+
function parseRetryAfterHeader(retryAfter) {
|
|
349
|
+
if (!retryAfter) return 0;
|
|
350
|
+
const retryAfterSec = Number(retryAfter);
|
|
351
|
+
if (Number.isFinite(retryAfterSec) && retryAfterSec > 0) {
|
|
352
|
+
return retryAfterSec * 1e3;
|
|
353
|
+
}
|
|
354
|
+
const retryDate = Date.parse(retryAfter);
|
|
355
|
+
if (!isNaN(retryDate)) {
|
|
356
|
+
const deltaMs = retryDate - Date.now();
|
|
357
|
+
return Math.max(0, Math.min(deltaMs, 36e5));
|
|
358
|
+
}
|
|
359
|
+
return 0;
|
|
360
|
+
}
|
|
349
361
|
function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
|
|
350
362
|
const {
|
|
351
363
|
initialDelay,
|
|
@@ -353,28 +365,8 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
|
|
|
353
365
|
multiplier,
|
|
354
366
|
debug = false,
|
|
355
367
|
onFailedAttempt,
|
|
356
|
-
maxRetries = Infinity
|
|
357
|
-
retryBudgetPercent = 0.1
|
|
368
|
+
maxRetries = Infinity
|
|
358
369
|
} = backoffOptions;
|
|
359
|
-
let totalRequests = 0;
|
|
360
|
-
let totalRetries = 0;
|
|
361
|
-
let budgetResetTime = Date.now() + 6e4;
|
|
362
|
-
function checkRetryBudget(percent) {
|
|
363
|
-
const now = Date.now();
|
|
364
|
-
if (now > budgetResetTime) {
|
|
365
|
-
totalRequests = 0;
|
|
366
|
-
totalRetries = 0;
|
|
367
|
-
budgetResetTime = now + 6e4;
|
|
368
|
-
}
|
|
369
|
-
totalRequests++;
|
|
370
|
-
if (totalRequests < 10) return true;
|
|
371
|
-
const currentRetryRate = totalRetries / totalRequests;
|
|
372
|
-
const hasCapacity = currentRetryRate < percent;
|
|
373
|
-
if (hasCapacity) {
|
|
374
|
-
totalRetries++;
|
|
375
|
-
}
|
|
376
|
-
return hasCapacity;
|
|
377
|
-
}
|
|
378
370
|
return (...args) => __async(this, null, function* () {
|
|
379
371
|
var _a;
|
|
380
372
|
const url = args[0];
|
|
@@ -385,7 +377,6 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
|
|
|
385
377
|
try {
|
|
386
378
|
const result = yield fetchClient(...args);
|
|
387
379
|
if (result.ok) {
|
|
388
|
-
delay = initialDelay;
|
|
389
380
|
return result;
|
|
390
381
|
}
|
|
391
382
|
const err = yield FetchError.fromResponse(result, url.toString());
|
|
@@ -398,7 +389,7 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
|
|
|
398
389
|
throw e;
|
|
399
390
|
} else {
|
|
400
391
|
attempt++;
|
|
401
|
-
if (attempt
|
|
392
|
+
if (attempt > maxRetries) {
|
|
402
393
|
if (debug) {
|
|
403
394
|
console.log(
|
|
404
395
|
`Max retries reached (${attempt}/${maxRetries}), giving up`
|
|
@@ -406,31 +397,7 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
|
|
|
406
397
|
}
|
|
407
398
|
throw e;
|
|
408
399
|
}
|
|
409
|
-
|
|
410
|
-
if (debug) {
|
|
411
|
-
console.log(
|
|
412
|
-
`Retry budget exhausted (attempt ${attempt}), backing off`
|
|
413
|
-
);
|
|
414
|
-
}
|
|
415
|
-
yield new Promise((resolve) => setTimeout(resolve, maxDelay));
|
|
416
|
-
continue;
|
|
417
|
-
}
|
|
418
|
-
let serverMinimumMs = 0;
|
|
419
|
-
if (e instanceof FetchError && e.headers) {
|
|
420
|
-
const retryAfter = e.headers[`retry-after`];
|
|
421
|
-
if (retryAfter) {
|
|
422
|
-
const retryAfterSec = Number(retryAfter);
|
|
423
|
-
if (Number.isFinite(retryAfterSec) && retryAfterSec > 0) {
|
|
424
|
-
serverMinimumMs = retryAfterSec * 1e3;
|
|
425
|
-
} else {
|
|
426
|
-
const retryDate = Date.parse(retryAfter);
|
|
427
|
-
if (!isNaN(retryDate)) {
|
|
428
|
-
const deltaMs = retryDate - Date.now();
|
|
429
|
-
serverMinimumMs = Math.max(0, Math.min(deltaMs, 36e5));
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
}
|
|
400
|
+
const serverMinimumMs = e instanceof FetchError && e.headers ? parseRetryAfterHeader(e.headers[`retry-after`]) : 0;
|
|
434
401
|
const jitter = Math.random() * delay;
|
|
435
402
|
const clientBackoffMs = Math.min(jitter, maxDelay);
|
|
436
403
|
const waitMs = Math.max(serverMinimumMs, clientBackoffMs);
|
|
@@ -814,8 +781,9 @@ function canonicalShapeKey(url) {
|
|
|
814
781
|
cleanUrl.searchParams.sort();
|
|
815
782
|
return cleanUrl.toString();
|
|
816
783
|
}
|
|
817
|
-
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, _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;
|
|
784
|
+
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;
|
|
818
785
|
var ShapeStream = class {
|
|
786
|
+
// Maximum delay cap (ms)
|
|
819
787
|
constructor(options) {
|
|
820
788
|
__privateAdd(this, _ShapeStream_instances);
|
|
821
789
|
__privateAdd(this, _error, null);
|
|
@@ -849,6 +817,16 @@ var ShapeStream = class {
|
|
|
849
817
|
// counter for concurrent snapshot requests
|
|
850
818
|
__privateAdd(this, _midStreamPromise);
|
|
851
819
|
__privateAdd(this, _midStreamPromiseResolver);
|
|
820
|
+
__privateAdd(this, _lastSseConnectionStartTime);
|
|
821
|
+
__privateAdd(this, _minSseConnectionDuration, 1e3);
|
|
822
|
+
// Minimum expected SSE connection duration (1 second)
|
|
823
|
+
__privateAdd(this, _consecutiveShortSseConnections, 0);
|
|
824
|
+
__privateAdd(this, _maxShortSseConnections, 3);
|
|
825
|
+
// Fall back to long polling after this many short connections
|
|
826
|
+
__privateAdd(this, _sseFallbackToLongPolling, false);
|
|
827
|
+
__privateAdd(this, _sseBackoffBaseDelay, 100);
|
|
828
|
+
// Base delay for exponential backoff (ms)
|
|
829
|
+
__privateAdd(this, _sseBackoffMaxDelay, 5e3);
|
|
852
830
|
var _a, _b, _c, _d;
|
|
853
831
|
this.options = __spreadValues({ subscribe: true }, options);
|
|
854
832
|
validateOptions(this.options);
|
|
@@ -1024,10 +1002,17 @@ _snapshotTracker = new WeakMap();
|
|
|
1024
1002
|
_activeSnapshotRequests = new WeakMap();
|
|
1025
1003
|
_midStreamPromise = new WeakMap();
|
|
1026
1004
|
_midStreamPromiseResolver = new WeakMap();
|
|
1005
|
+
_lastSseConnectionStartTime = new WeakMap();
|
|
1006
|
+
_minSseConnectionDuration = new WeakMap();
|
|
1007
|
+
_consecutiveShortSseConnections = new WeakMap();
|
|
1008
|
+
_maxShortSseConnections = new WeakMap();
|
|
1009
|
+
_sseFallbackToLongPolling = new WeakMap();
|
|
1010
|
+
_sseBackoffBaseDelay = new WeakMap();
|
|
1011
|
+
_sseBackoffMaxDelay = new WeakMap();
|
|
1027
1012
|
_ShapeStream_instances = new WeakSet();
|
|
1028
1013
|
start_fn = function() {
|
|
1029
1014
|
return __async(this, null, function* () {
|
|
1030
|
-
var _a;
|
|
1015
|
+
var _a, _b, _c, _d, _e;
|
|
1031
1016
|
__privateSet(this, _started, true);
|
|
1032
1017
|
try {
|
|
1033
1018
|
yield __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
|
|
@@ -1035,24 +1020,34 @@ start_fn = function() {
|
|
|
1035
1020
|
__privateSet(this, _error, err);
|
|
1036
1021
|
if (__privateGet(this, _onError)) {
|
|
1037
1022
|
const retryOpts = yield __privateGet(this, _onError).call(this, err);
|
|
1038
|
-
if (typeof retryOpts === `object`) {
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
this.options.params = retryOpts.params;
|
|
1023
|
+
if (retryOpts && typeof retryOpts === `object`) {
|
|
1024
|
+
if (retryOpts.params) {
|
|
1025
|
+
this.options.params = __spreadValues(__spreadValues({}, (_a = this.options.params) != null ? _a : {}), retryOpts.params);
|
|
1042
1026
|
}
|
|
1043
|
-
if (
|
|
1044
|
-
this.options.headers = retryOpts.headers;
|
|
1027
|
+
if (retryOpts.headers) {
|
|
1028
|
+
this.options.headers = __spreadValues(__spreadValues({}, (_b = this.options.headers) != null ? _b : {}), retryOpts.headers);
|
|
1045
1029
|
}
|
|
1030
|
+
__privateSet(this, _error, null);
|
|
1046
1031
|
__privateSet(this, _started, false);
|
|
1047
|
-
__privateMethod(this, _ShapeStream_instances, start_fn).call(this);
|
|
1032
|
+
yield __privateMethod(this, _ShapeStream_instances, start_fn).call(this);
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
if (err instanceof Error) {
|
|
1036
|
+
__privateMethod(this, _ShapeStream_instances, sendErrorToSubscribers_fn).call(this, err);
|
|
1048
1037
|
}
|
|
1038
|
+
__privateSet(this, _connected, false);
|
|
1039
|
+
(_c = __privateGet(this, _tickPromiseRejecter)) == null ? void 0 : _c.call(this);
|
|
1049
1040
|
return;
|
|
1050
1041
|
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1042
|
+
if (err instanceof Error) {
|
|
1043
|
+
__privateMethod(this, _ShapeStream_instances, sendErrorToSubscribers_fn).call(this, err);
|
|
1044
|
+
}
|
|
1053
1045
|
__privateSet(this, _connected, false);
|
|
1054
|
-
(
|
|
1046
|
+
(_d = __privateGet(this, _tickPromiseRejecter)) == null ? void 0 : _d.call(this);
|
|
1047
|
+
throw err;
|
|
1055
1048
|
}
|
|
1049
|
+
__privateSet(this, _connected, false);
|
|
1050
|
+
(_e = __privateGet(this, _tickPromiseRejecter)) == null ? void 0 : _e.call(this);
|
|
1056
1051
|
});
|
|
1057
1052
|
};
|
|
1058
1053
|
requestShape_fn = function() {
|
|
@@ -1099,7 +1094,6 @@ requestShape_fn = function() {
|
|
|
1099
1094
|
yield __privateMethod(this, _ShapeStream_instances, publish_fn).call(this, Array.isArray(e.json) ? e.json : [e.json]);
|
|
1100
1095
|
return __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
|
|
1101
1096
|
} else {
|
|
1102
|
-
__privateMethod(this, _ShapeStream_instances, sendErrorToSubscribers_fn).call(this, e);
|
|
1103
1097
|
throw e;
|
|
1104
1098
|
}
|
|
1105
1099
|
} finally {
|
|
@@ -1251,7 +1245,7 @@ fetchShape_fn = function(opts) {
|
|
|
1251
1245
|
return __async(this, null, function* () {
|
|
1252
1246
|
var _a;
|
|
1253
1247
|
const useSse = (_a = this.options.liveSse) != null ? _a : this.options.experimentalLiveSse;
|
|
1254
|
-
if (__privateGet(this, _isUpToDate) && useSse && !__privateGet(this, _isRefreshing) && !opts.resumingFromPause) {
|
|
1248
|
+
if (__privateGet(this, _isUpToDate) && useSse && !__privateGet(this, _isRefreshing) && !opts.resumingFromPause && !__privateGet(this, _sseFallbackToLongPolling)) {
|
|
1255
1249
|
opts.fetchUrl.searchParams.set(EXPERIMENTAL_LIVE_SSE_QUERY_PARAM, `true`);
|
|
1256
1250
|
opts.fetchUrl.searchParams.set(LIVE_SSE_QUERY_PARAM, `true`);
|
|
1257
1251
|
return __privateMethod(this, _ShapeStream_instances, requestShapeSSE_fn).call(this, opts);
|
|
@@ -1279,6 +1273,7 @@ requestShapeSSE_fn = function(opts) {
|
|
|
1279
1273
|
return __async(this, null, function* () {
|
|
1280
1274
|
const { fetchUrl, requestAbortController, headers } = opts;
|
|
1281
1275
|
const fetch2 = __privateGet(this, _sseFetchClient);
|
|
1276
|
+
__privateSet(this, _lastSseConnectionStartTime, Date.now());
|
|
1282
1277
|
try {
|
|
1283
1278
|
let buffer = [];
|
|
1284
1279
|
yield fetchEventSource(fetchUrl.toString(), {
|
|
@@ -1312,6 +1307,27 @@ requestShapeSSE_fn = function(opts) {
|
|
|
1312
1307
|
throw new FetchBackoffAbortError();
|
|
1313
1308
|
}
|
|
1314
1309
|
throw error;
|
|
1310
|
+
} finally {
|
|
1311
|
+
const connectionDuration = Date.now() - __privateGet(this, _lastSseConnectionStartTime);
|
|
1312
|
+
const wasAborted = requestAbortController.signal.aborted;
|
|
1313
|
+
if (connectionDuration < __privateGet(this, _minSseConnectionDuration) && !wasAborted) {
|
|
1314
|
+
__privateWrapper(this, _consecutiveShortSseConnections)._++;
|
|
1315
|
+
if (__privateGet(this, _consecutiveShortSseConnections) >= __privateGet(this, _maxShortSseConnections)) {
|
|
1316
|
+
__privateSet(this, _sseFallbackToLongPolling, true);
|
|
1317
|
+
console.warn(
|
|
1318
|
+
`[Electric] SSE connections are closing immediately (possibly due to proxy buffering or misconfiguration). Falling back to long polling. Your proxy must support streaming SSE responses (not buffer the complete response). Configuration: Nginx add 'X-Accel-Buffering: no', Caddy add 'flush_interval -1' to reverse_proxy. Note: Do NOT disable caching entirely - Electric uses cache headers to enable request collapsing for efficiency.`
|
|
1319
|
+
);
|
|
1320
|
+
} else {
|
|
1321
|
+
const maxDelay = Math.min(
|
|
1322
|
+
__privateGet(this, _sseBackoffMaxDelay),
|
|
1323
|
+
__privateGet(this, _sseBackoffBaseDelay) * Math.pow(2, __privateGet(this, _consecutiveShortSseConnections))
|
|
1324
|
+
);
|
|
1325
|
+
const delayMs = Math.floor(Math.random() * maxDelay);
|
|
1326
|
+
yield new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1327
|
+
}
|
|
1328
|
+
} else if (connectionDuration >= __privateGet(this, _minSseConnectionDuration)) {
|
|
1329
|
+
__privateSet(this, _consecutiveShortSseConnections, 0);
|
|
1330
|
+
}
|
|
1315
1331
|
}
|
|
1316
1332
|
});
|
|
1317
1333
|
};
|
|
@@ -1410,6 +1426,8 @@ reset_fn = function(handle) {
|
|
|
1410
1426
|
__privateSet(this, _connected, false);
|
|
1411
1427
|
__privateSet(this, _schema, void 0);
|
|
1412
1428
|
__privateSet(this, _activeSnapshotRequests, 0);
|
|
1429
|
+
__privateSet(this, _consecutiveShortSseConnections, 0);
|
|
1430
|
+
__privateSet(this, _sseFallbackToLongPolling, false);
|
|
1413
1431
|
};
|
|
1414
1432
|
fetchSnapshot_fn = function(url, headers) {
|
|
1415
1433
|
return __async(this, null, function* () {
|