@electric-sql/client 1.1.1 → 1.1.2

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/index.mjs CHANGED
@@ -338,13 +338,8 @@ var ELECTRIC_PROTOCOL_QUERY_PARAMS = [
338
338
  var HTTP_RETRY_STATUS_CODES = [429];
339
339
  var BackoffDefaults = {
340
340
  initialDelay: 100,
341
- maxDelay: 6e4,
342
- // Cap at 60s - reasonable for long-lived connections
343
- multiplier: 1.3,
344
- maxRetries: Infinity,
345
- // Retry forever - clients may go offline and come back
346
- retryBudgetPercent: 0.1
347
- // 10% retry budget prevents amplification
341
+ maxDelay: 1e4,
342
+ multiplier: 1.3
348
343
  };
349
344
  function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
350
345
  const {
@@ -352,29 +347,8 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
352
347
  maxDelay,
353
348
  multiplier,
354
349
  debug = false,
355
- onFailedAttempt,
356
- maxRetries = Infinity,
357
- retryBudgetPercent = 0.1
350
+ onFailedAttempt
358
351
  } = 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
352
  return (...args) => __async(this, null, function* () {
379
353
  var _a;
380
354
  const url = args[0];
@@ -384,10 +358,7 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
384
358
  while (true) {
385
359
  try {
386
360
  const result = yield fetchClient(...args);
387
- if (result.ok) {
388
- delay = initialDelay;
389
- return result;
390
- }
361
+ if (result.ok) return result;
391
362
  const err = yield FetchError.fromResponse(result, url.toString());
392
363
  throw err;
393
364
  } catch (e) {
@@ -397,51 +368,12 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
397
368
  } else if (e instanceof FetchError && !HTTP_RETRY_STATUS_CODES.includes(e.status) && e.status >= 400 && e.status < 500) {
398
369
  throw e;
399
370
  } else {
400
- attempt++;
401
- if (attempt >= maxRetries) {
402
- if (debug) {
403
- console.log(
404
- `Max retries reached (${attempt}/${maxRetries}), giving up`
405
- );
406
- }
407
- throw e;
408
- }
409
- if (!checkRetryBudget(retryBudgetPercent)) {
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
- }
434
- const jitter = Math.random() * delay;
435
- const clientBackoffMs = Math.min(jitter, maxDelay);
436
- const waitMs = Math.max(serverMinimumMs, clientBackoffMs);
371
+ yield new Promise((resolve) => setTimeout(resolve, delay));
372
+ delay = Math.min(delay * multiplier, maxDelay);
437
373
  if (debug) {
438
- const source = serverMinimumMs > 0 ? `server+client` : `client`;
439
- console.log(
440
- `Retry attempt #${attempt} after ${waitMs}ms (${source}, serverMin=${serverMinimumMs}ms, clientBackoff=${clientBackoffMs}ms)`
441
- );
374
+ attempt++;
375
+ console.log(`Retry attempt #${attempt} after ${delay}ms`);
442
376
  }
443
- yield new Promise((resolve) => setTimeout(resolve, waitMs));
444
- delay = Math.min(delay * multiplier, maxDelay);
445
377
  }
446
378
  }
447
379
  }
@@ -814,8 +746,9 @@ function canonicalShapeKey(url) {
814
746
  cleanUrl.searchParams.sort();
815
747
  return cleanUrl.toString();
816
748
  }
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;
749
+ 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
750
  var ShapeStream = class {
751
+ // Maximum delay cap (ms)
819
752
  constructor(options) {
820
753
  __privateAdd(this, _ShapeStream_instances);
821
754
  __privateAdd(this, _error, null);
@@ -849,6 +782,16 @@ var ShapeStream = class {
849
782
  // counter for concurrent snapshot requests
850
783
  __privateAdd(this, _midStreamPromise);
851
784
  __privateAdd(this, _midStreamPromiseResolver);
785
+ __privateAdd(this, _lastSseConnectionStartTime);
786
+ __privateAdd(this, _minSseConnectionDuration, 1e3);
787
+ // Minimum expected SSE connection duration (1 second)
788
+ __privateAdd(this, _consecutiveShortSseConnections, 0);
789
+ __privateAdd(this, _maxShortSseConnections, 3);
790
+ // Fall back to long polling after this many short connections
791
+ __privateAdd(this, _sseFallbackToLongPolling, false);
792
+ __privateAdd(this, _sseBackoffBaseDelay, 100);
793
+ // Base delay for exponential backoff (ms)
794
+ __privateAdd(this, _sseBackoffMaxDelay, 5e3);
852
795
  var _a, _b, _c, _d;
853
796
  this.options = __spreadValues({ subscribe: true }, options);
854
797
  validateOptions(this.options);
@@ -1024,10 +967,17 @@ _snapshotTracker = new WeakMap();
1024
967
  _activeSnapshotRequests = new WeakMap();
1025
968
  _midStreamPromise = new WeakMap();
1026
969
  _midStreamPromiseResolver = new WeakMap();
970
+ _lastSseConnectionStartTime = new WeakMap();
971
+ _minSseConnectionDuration = new WeakMap();
972
+ _consecutiveShortSseConnections = new WeakMap();
973
+ _maxShortSseConnections = new WeakMap();
974
+ _sseFallbackToLongPolling = new WeakMap();
975
+ _sseBackoffBaseDelay = new WeakMap();
976
+ _sseBackoffMaxDelay = new WeakMap();
1027
977
  _ShapeStream_instances = new WeakSet();
1028
978
  start_fn = function() {
1029
979
  return __async(this, null, function* () {
1030
- var _a;
980
+ var _a, _b, _c, _d, _e;
1031
981
  __privateSet(this, _started, true);
1032
982
  try {
1033
983
  yield __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
@@ -1035,24 +985,34 @@ start_fn = function() {
1035
985
  __privateSet(this, _error, err);
1036
986
  if (__privateGet(this, _onError)) {
1037
987
  const retryOpts = yield __privateGet(this, _onError).call(this, err);
1038
- if (typeof retryOpts === `object`) {
1039
- __privateMethod(this, _ShapeStream_instances, reset_fn).call(this);
1040
- if (`params` in retryOpts) {
1041
- this.options.params = retryOpts.params;
988
+ if (retryOpts && typeof retryOpts === `object`) {
989
+ if (retryOpts.params) {
990
+ this.options.params = __spreadValues(__spreadValues({}, (_a = this.options.params) != null ? _a : {}), retryOpts.params);
1042
991
  }
1043
- if (`headers` in retryOpts) {
1044
- this.options.headers = retryOpts.headers;
992
+ if (retryOpts.headers) {
993
+ this.options.headers = __spreadValues(__spreadValues({}, (_b = this.options.headers) != null ? _b : {}), retryOpts.headers);
1045
994
  }
995
+ __privateSet(this, _error, null);
1046
996
  __privateSet(this, _started, false);
1047
- __privateMethod(this, _ShapeStream_instances, start_fn).call(this);
997
+ yield __privateMethod(this, _ShapeStream_instances, start_fn).call(this);
998
+ return;
999
+ }
1000
+ if (err instanceof Error) {
1001
+ __privateMethod(this, _ShapeStream_instances, sendErrorToSubscribers_fn).call(this, err);
1048
1002
  }
1003
+ __privateSet(this, _connected, false);
1004
+ (_c = __privateGet(this, _tickPromiseRejecter)) == null ? void 0 : _c.call(this);
1049
1005
  return;
1050
1006
  }
1051
- throw err;
1052
- } finally {
1007
+ if (err instanceof Error) {
1008
+ __privateMethod(this, _ShapeStream_instances, sendErrorToSubscribers_fn).call(this, err);
1009
+ }
1053
1010
  __privateSet(this, _connected, false);
1054
- (_a = __privateGet(this, _tickPromiseRejecter)) == null ? void 0 : _a.call(this);
1011
+ (_d = __privateGet(this, _tickPromiseRejecter)) == null ? void 0 : _d.call(this);
1012
+ throw err;
1055
1013
  }
1014
+ __privateSet(this, _connected, false);
1015
+ (_e = __privateGet(this, _tickPromiseRejecter)) == null ? void 0 : _e.call(this);
1056
1016
  });
1057
1017
  };
1058
1018
  requestShape_fn = function() {
@@ -1099,7 +1059,6 @@ requestShape_fn = function() {
1099
1059
  yield __privateMethod(this, _ShapeStream_instances, publish_fn).call(this, Array.isArray(e.json) ? e.json : [e.json]);
1100
1060
  return __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
1101
1061
  } else {
1102
- __privateMethod(this, _ShapeStream_instances, sendErrorToSubscribers_fn).call(this, e);
1103
1062
  throw e;
1104
1063
  }
1105
1064
  } finally {
@@ -1251,7 +1210,7 @@ fetchShape_fn = function(opts) {
1251
1210
  return __async(this, null, function* () {
1252
1211
  var _a;
1253
1212
  const useSse = (_a = this.options.liveSse) != null ? _a : this.options.experimentalLiveSse;
1254
- if (__privateGet(this, _isUpToDate) && useSse && !__privateGet(this, _isRefreshing) && !opts.resumingFromPause) {
1213
+ if (__privateGet(this, _isUpToDate) && useSse && !__privateGet(this, _isRefreshing) && !opts.resumingFromPause && !__privateGet(this, _sseFallbackToLongPolling)) {
1255
1214
  opts.fetchUrl.searchParams.set(EXPERIMENTAL_LIVE_SSE_QUERY_PARAM, `true`);
1256
1215
  opts.fetchUrl.searchParams.set(LIVE_SSE_QUERY_PARAM, `true`);
1257
1216
  return __privateMethod(this, _ShapeStream_instances, requestShapeSSE_fn).call(this, opts);
@@ -1279,6 +1238,7 @@ requestShapeSSE_fn = function(opts) {
1279
1238
  return __async(this, null, function* () {
1280
1239
  const { fetchUrl, requestAbortController, headers } = opts;
1281
1240
  const fetch2 = __privateGet(this, _sseFetchClient);
1241
+ __privateSet(this, _lastSseConnectionStartTime, Date.now());
1282
1242
  try {
1283
1243
  let buffer = [];
1284
1244
  yield fetchEventSource(fetchUrl.toString(), {
@@ -1312,6 +1272,27 @@ requestShapeSSE_fn = function(opts) {
1312
1272
  throw new FetchBackoffAbortError();
1313
1273
  }
1314
1274
  throw error;
1275
+ } finally {
1276
+ const connectionDuration = Date.now() - __privateGet(this, _lastSseConnectionStartTime);
1277
+ const wasAborted = requestAbortController.signal.aborted;
1278
+ if (connectionDuration < __privateGet(this, _minSseConnectionDuration) && !wasAborted) {
1279
+ __privateWrapper(this, _consecutiveShortSseConnections)._++;
1280
+ if (__privateGet(this, _consecutiveShortSseConnections) >= __privateGet(this, _maxShortSseConnections)) {
1281
+ __privateSet(this, _sseFallbackToLongPolling, true);
1282
+ console.warn(
1283
+ `[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.`
1284
+ );
1285
+ } else {
1286
+ const maxDelay = Math.min(
1287
+ __privateGet(this, _sseBackoffMaxDelay),
1288
+ __privateGet(this, _sseBackoffBaseDelay) * Math.pow(2, __privateGet(this, _consecutiveShortSseConnections))
1289
+ );
1290
+ const delayMs = Math.floor(Math.random() * maxDelay);
1291
+ yield new Promise((resolve) => setTimeout(resolve, delayMs));
1292
+ }
1293
+ } else if (connectionDuration >= __privateGet(this, _minSseConnectionDuration)) {
1294
+ __privateSet(this, _consecutiveShortSseConnections, 0);
1295
+ }
1315
1296
  }
1316
1297
  });
1317
1298
  };
@@ -1410,6 +1391,8 @@ reset_fn = function(handle) {
1410
1391
  __privateSet(this, _connected, false);
1411
1392
  __privateSet(this, _schema, void 0);
1412
1393
  __privateSet(this, _activeSnapshotRequests, 0);
1394
+ __privateSet(this, _consecutiveShortSseConnections, 0);
1395
+ __privateSet(this, _sseFallbackToLongPolling, false);
1413
1396
  };
1414
1397
  fetchSnapshot_fn = function(url, headers) {
1415
1398
  return __async(this, null, function* () {