@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/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 >= maxRetries) {
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
- 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
- }
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
- __privateMethod(this, _ShapeStream_instances, reset_fn).call(this);
1040
- if (`params` in retryOpts) {
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 (`headers` in retryOpts) {
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
- throw err;
1052
- } finally {
1042
+ if (err instanceof Error) {
1043
+ __privateMethod(this, _ShapeStream_instances, sendErrorToSubscribers_fn).call(this, err);
1044
+ }
1053
1045
  __privateSet(this, _connected, false);
1054
- (_a = __privateGet(this, _tickPromiseRejecter)) == null ? void 0 : _a.call(this);
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* () {