@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.
@@ -369,13 +369,8 @@ var ELECTRIC_PROTOCOL_QUERY_PARAMS = [
369
369
  var HTTP_RETRY_STATUS_CODES = [429];
370
370
  var BackoffDefaults = {
371
371
  initialDelay: 100,
372
- maxDelay: 6e4,
373
- // Cap at 60s - reasonable for long-lived connections
374
- multiplier: 1.3,
375
- maxRetries: Infinity,
376
- // Retry forever - clients may go offline and come back
377
- retryBudgetPercent: 0.1
378
- // 10% retry budget prevents amplification
372
+ maxDelay: 1e4,
373
+ multiplier: 1.3
379
374
  };
380
375
  function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
381
376
  const {
@@ -383,29 +378,8 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
383
378
  maxDelay,
384
379
  multiplier,
385
380
  debug = false,
386
- onFailedAttempt,
387
- maxRetries = Infinity,
388
- retryBudgetPercent = 0.1
381
+ onFailedAttempt
389
382
  } = backoffOptions;
390
- let totalRequests = 0;
391
- let totalRetries = 0;
392
- let budgetResetTime = Date.now() + 6e4;
393
- function checkRetryBudget(percent) {
394
- const now = Date.now();
395
- if (now > budgetResetTime) {
396
- totalRequests = 0;
397
- totalRetries = 0;
398
- budgetResetTime = now + 6e4;
399
- }
400
- totalRequests++;
401
- if (totalRequests < 10) return true;
402
- const currentRetryRate = totalRetries / totalRequests;
403
- const hasCapacity = currentRetryRate < percent;
404
- if (hasCapacity) {
405
- totalRetries++;
406
- }
407
- return hasCapacity;
408
- }
409
383
  return (...args) => __async(this, null, function* () {
410
384
  var _a;
411
385
  const url = args[0];
@@ -415,10 +389,7 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
415
389
  while (true) {
416
390
  try {
417
391
  const result = yield fetchClient(...args);
418
- if (result.ok) {
419
- delay = initialDelay;
420
- return result;
421
- }
392
+ if (result.ok) return result;
422
393
  const err = yield FetchError.fromResponse(result, url.toString());
423
394
  throw err;
424
395
  } catch (e) {
@@ -428,51 +399,12 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
428
399
  } else if (e instanceof FetchError && !HTTP_RETRY_STATUS_CODES.includes(e.status) && e.status >= 400 && e.status < 500) {
429
400
  throw e;
430
401
  } else {
431
- attempt++;
432
- if (attempt >= maxRetries) {
433
- if (debug) {
434
- console.log(
435
- `Max retries reached (${attempt}/${maxRetries}), giving up`
436
- );
437
- }
438
- throw e;
439
- }
440
- if (!checkRetryBudget(retryBudgetPercent)) {
441
- if (debug) {
442
- console.log(
443
- `Retry budget exhausted (attempt ${attempt}), backing off`
444
- );
445
- }
446
- yield new Promise((resolve) => setTimeout(resolve, maxDelay));
447
- continue;
448
- }
449
- let serverMinimumMs = 0;
450
- if (e instanceof FetchError && e.headers) {
451
- const retryAfter = e.headers[`retry-after`];
452
- if (retryAfter) {
453
- const retryAfterSec = Number(retryAfter);
454
- if (Number.isFinite(retryAfterSec) && retryAfterSec > 0) {
455
- serverMinimumMs = retryAfterSec * 1e3;
456
- } else {
457
- const retryDate = Date.parse(retryAfter);
458
- if (!isNaN(retryDate)) {
459
- const deltaMs = retryDate - Date.now();
460
- serverMinimumMs = Math.max(0, Math.min(deltaMs, 36e5));
461
- }
462
- }
463
- }
464
- }
465
- const jitter = Math.random() * delay;
466
- const clientBackoffMs = Math.min(jitter, maxDelay);
467
- const waitMs = Math.max(serverMinimumMs, clientBackoffMs);
402
+ yield new Promise((resolve) => setTimeout(resolve, delay));
403
+ delay = Math.min(delay * multiplier, maxDelay);
468
404
  if (debug) {
469
- const source = serverMinimumMs > 0 ? `server+client` : `client`;
470
- console.log(
471
- `Retry attempt #${attempt} after ${waitMs}ms (${source}, serverMin=${serverMinimumMs}ms, clientBackoff=${clientBackoffMs}ms)`
472
- );
405
+ attempt++;
406
+ console.log(`Retry attempt #${attempt} after ${delay}ms`);
473
407
  }
474
- yield new Promise((resolve) => setTimeout(resolve, waitMs));
475
- delay = Math.min(delay * multiplier, maxDelay);
476
408
  }
477
409
  }
478
410
  }
@@ -843,8 +775,9 @@ function canonicalShapeKey(url) {
843
775
  cleanUrl.searchParams.sort();
844
776
  return cleanUrl.toString();
845
777
  }
846
- 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;
778
+ 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;
847
779
  var ShapeStream = class {
780
+ // Maximum delay cap (ms)
848
781
  constructor(options) {
849
782
  __privateAdd(this, _ShapeStream_instances);
850
783
  __privateAdd(this, _error, null);
@@ -878,6 +811,16 @@ var ShapeStream = class {
878
811
  // counter for concurrent snapshot requests
879
812
  __privateAdd(this, _midStreamPromise);
880
813
  __privateAdd(this, _midStreamPromiseResolver);
814
+ __privateAdd(this, _lastSseConnectionStartTime);
815
+ __privateAdd(this, _minSseConnectionDuration, 1e3);
816
+ // Minimum expected SSE connection duration (1 second)
817
+ __privateAdd(this, _consecutiveShortSseConnections, 0);
818
+ __privateAdd(this, _maxShortSseConnections, 3);
819
+ // Fall back to long polling after this many short connections
820
+ __privateAdd(this, _sseFallbackToLongPolling, false);
821
+ __privateAdd(this, _sseBackoffBaseDelay, 100);
822
+ // Base delay for exponential backoff (ms)
823
+ __privateAdd(this, _sseBackoffMaxDelay, 5e3);
881
824
  var _a, _b, _c, _d;
882
825
  this.options = __spreadValues({ subscribe: true }, options);
883
826
  validateOptions(this.options);
@@ -1053,10 +996,17 @@ _snapshotTracker = new WeakMap();
1053
996
  _activeSnapshotRequests = new WeakMap();
1054
997
  _midStreamPromise = new WeakMap();
1055
998
  _midStreamPromiseResolver = new WeakMap();
999
+ _lastSseConnectionStartTime = new WeakMap();
1000
+ _minSseConnectionDuration = new WeakMap();
1001
+ _consecutiveShortSseConnections = new WeakMap();
1002
+ _maxShortSseConnections = new WeakMap();
1003
+ _sseFallbackToLongPolling = new WeakMap();
1004
+ _sseBackoffBaseDelay = new WeakMap();
1005
+ _sseBackoffMaxDelay = new WeakMap();
1056
1006
  _ShapeStream_instances = new WeakSet();
1057
1007
  start_fn = function() {
1058
1008
  return __async(this, null, function* () {
1059
- var _a;
1009
+ var _a, _b, _c, _d, _e;
1060
1010
  __privateSet(this, _started, true);
1061
1011
  try {
1062
1012
  yield __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
@@ -1064,24 +1014,34 @@ start_fn = function() {
1064
1014
  __privateSet(this, _error, err);
1065
1015
  if (__privateGet(this, _onError)) {
1066
1016
  const retryOpts = yield __privateGet(this, _onError).call(this, err);
1067
- if (typeof retryOpts === `object`) {
1068
- __privateMethod(this, _ShapeStream_instances, reset_fn).call(this);
1069
- if (`params` in retryOpts) {
1070
- this.options.params = retryOpts.params;
1017
+ if (retryOpts && typeof retryOpts === `object`) {
1018
+ if (retryOpts.params) {
1019
+ this.options.params = __spreadValues(__spreadValues({}, (_a = this.options.params) != null ? _a : {}), retryOpts.params);
1071
1020
  }
1072
- if (`headers` in retryOpts) {
1073
- this.options.headers = retryOpts.headers;
1021
+ if (retryOpts.headers) {
1022
+ this.options.headers = __spreadValues(__spreadValues({}, (_b = this.options.headers) != null ? _b : {}), retryOpts.headers);
1074
1023
  }
1024
+ __privateSet(this, _error, null);
1075
1025
  __privateSet(this, _started, false);
1076
- __privateMethod(this, _ShapeStream_instances, start_fn).call(this);
1026
+ yield __privateMethod(this, _ShapeStream_instances, start_fn).call(this);
1027
+ return;
1028
+ }
1029
+ if (err instanceof Error) {
1030
+ __privateMethod(this, _ShapeStream_instances, sendErrorToSubscribers_fn).call(this, err);
1077
1031
  }
1032
+ __privateSet(this, _connected, false);
1033
+ (_c = __privateGet(this, _tickPromiseRejecter)) == null ? void 0 : _c.call(this);
1078
1034
  return;
1079
1035
  }
1080
- throw err;
1081
- } finally {
1036
+ if (err instanceof Error) {
1037
+ __privateMethod(this, _ShapeStream_instances, sendErrorToSubscribers_fn).call(this, err);
1038
+ }
1082
1039
  __privateSet(this, _connected, false);
1083
- (_a = __privateGet(this, _tickPromiseRejecter)) == null ? void 0 : _a.call(this);
1040
+ (_d = __privateGet(this, _tickPromiseRejecter)) == null ? void 0 : _d.call(this);
1041
+ throw err;
1084
1042
  }
1043
+ __privateSet(this, _connected, false);
1044
+ (_e = __privateGet(this, _tickPromiseRejecter)) == null ? void 0 : _e.call(this);
1085
1045
  });
1086
1046
  };
1087
1047
  requestShape_fn = function() {
@@ -1128,7 +1088,6 @@ requestShape_fn = function() {
1128
1088
  yield __privateMethod(this, _ShapeStream_instances, publish_fn).call(this, Array.isArray(e.json) ? e.json : [e.json]);
1129
1089
  return __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
1130
1090
  } else {
1131
- __privateMethod(this, _ShapeStream_instances, sendErrorToSubscribers_fn).call(this, e);
1132
1091
  throw e;
1133
1092
  }
1134
1093
  } finally {
@@ -1280,7 +1239,7 @@ fetchShape_fn = function(opts) {
1280
1239
  return __async(this, null, function* () {
1281
1240
  var _a;
1282
1241
  const useSse = (_a = this.options.liveSse) != null ? _a : this.options.experimentalLiveSse;
1283
- if (__privateGet(this, _isUpToDate) && useSse && !__privateGet(this, _isRefreshing) && !opts.resumingFromPause) {
1242
+ if (__privateGet(this, _isUpToDate) && useSse && !__privateGet(this, _isRefreshing) && !opts.resumingFromPause && !__privateGet(this, _sseFallbackToLongPolling)) {
1284
1243
  opts.fetchUrl.searchParams.set(EXPERIMENTAL_LIVE_SSE_QUERY_PARAM, `true`);
1285
1244
  opts.fetchUrl.searchParams.set(LIVE_SSE_QUERY_PARAM, `true`);
1286
1245
  return __privateMethod(this, _ShapeStream_instances, requestShapeSSE_fn).call(this, opts);
@@ -1308,6 +1267,7 @@ requestShapeSSE_fn = function(opts) {
1308
1267
  return __async(this, null, function* () {
1309
1268
  const { fetchUrl, requestAbortController, headers } = opts;
1310
1269
  const fetch2 = __privateGet(this, _sseFetchClient);
1270
+ __privateSet(this, _lastSseConnectionStartTime, Date.now());
1311
1271
  try {
1312
1272
  let buffer = [];
1313
1273
  yield (0, import_fetch_event_source.fetchEventSource)(fetchUrl.toString(), {
@@ -1341,6 +1301,27 @@ requestShapeSSE_fn = function(opts) {
1341
1301
  throw new FetchBackoffAbortError();
1342
1302
  }
1343
1303
  throw error;
1304
+ } finally {
1305
+ const connectionDuration = Date.now() - __privateGet(this, _lastSseConnectionStartTime);
1306
+ const wasAborted = requestAbortController.signal.aborted;
1307
+ if (connectionDuration < __privateGet(this, _minSseConnectionDuration) && !wasAborted) {
1308
+ __privateWrapper(this, _consecutiveShortSseConnections)._++;
1309
+ if (__privateGet(this, _consecutiveShortSseConnections) >= __privateGet(this, _maxShortSseConnections)) {
1310
+ __privateSet(this, _sseFallbackToLongPolling, true);
1311
+ console.warn(
1312
+ `[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.`
1313
+ );
1314
+ } else {
1315
+ const maxDelay = Math.min(
1316
+ __privateGet(this, _sseBackoffMaxDelay),
1317
+ __privateGet(this, _sseBackoffBaseDelay) * Math.pow(2, __privateGet(this, _consecutiveShortSseConnections))
1318
+ );
1319
+ const delayMs = Math.floor(Math.random() * maxDelay);
1320
+ yield new Promise((resolve) => setTimeout(resolve, delayMs));
1321
+ }
1322
+ } else if (connectionDuration >= __privateGet(this, _minSseConnectionDuration)) {
1323
+ __privateSet(this, _consecutiveShortSseConnections, 0);
1324
+ }
1344
1325
  }
1345
1326
  });
1346
1327
  };
@@ -1439,6 +1420,8 @@ reset_fn = function(handle) {
1439
1420
  __privateSet(this, _connected, false);
1440
1421
  __privateSet(this, _schema, void 0);
1441
1422
  __privateSet(this, _activeSnapshotRequests, 0);
1423
+ __privateSet(this, _consecutiveShortSseConnections, 0);
1424
+ __privateSet(this, _sseFallbackToLongPolling, false);
1442
1425
  };
1443
1426
  fetchSnapshot_fn = function(url, headers) {
1444
1427
  return __async(this, null, function* () {