@electric-sql/client 1.5.7 → 1.5.9

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.
@@ -466,7 +466,7 @@ function isChangeMessage(message) {
466
466
  return message != null && `key` in message;
467
467
  }
468
468
  function isControlMessage(message) {
469
- return message != null && !isChangeMessage(message);
469
+ return message != null && `headers` in message && `control` in message.headers;
470
470
  }
471
471
  function isUpToDateMessage(message) {
472
472
  return isControlMessage(message) && message.headers.control === `up-to-date`;
@@ -541,10 +541,9 @@ var ELECTRIC_PROTOCOL_QUERY_PARAMS = [
541
541
  // src/fetch.ts
542
542
  var HTTP_RETRY_STATUS_CODES = [429];
543
543
  var BackoffDefaults = {
544
- initialDelay: 100,
545
- maxDelay: 6e4,
546
- // Cap at 60s - reasonable for long-lived connections
547
- multiplier: 1.3,
544
+ initialDelay: 1e3,
545
+ maxDelay: 32e3,
546
+ multiplier: 2,
548
547
  maxRetries: Infinity
549
548
  // Retry forever - clients may go offline and come back
550
549
  };
@@ -1159,6 +1158,10 @@ var ExpiredShapesCache = class {
1159
1158
  this.data = {};
1160
1159
  this.save();
1161
1160
  }
1161
+ delete(shapeUrl) {
1162
+ delete this.data[shapeUrl];
1163
+ this.save();
1164
+ }
1162
1165
  };
1163
1166
  var expiredShapesCache = new ExpiredShapesCache();
1164
1167
 
@@ -1280,6 +1283,10 @@ var UpToDateTracker = class {
1280
1283
  }
1281
1284
  this.save();
1282
1285
  }
1286
+ delete(shapeKey) {
1287
+ delete this.data[shapeKey];
1288
+ this.save();
1289
+ }
1283
1290
  };
1284
1291
  var upToDateTracker = new UpToDateTracker();
1285
1292
 
@@ -1904,6 +1911,7 @@ var RESERVED_PARAMS = /* @__PURE__ */ new Set([
1904
1911
  OFFSET_QUERY_PARAM,
1905
1912
  CACHE_BUSTER_QUERY_PARAM
1906
1913
  ]);
1914
+ var TROUBLESHOOTING_URL = `https://electric-sql.com/docs/guides/troubleshooting`;
1907
1915
  async function resolveValue(value) {
1908
1916
  if (typeof value === `function`) {
1909
1917
  return value();
@@ -1944,7 +1952,7 @@ function canonicalShapeKey(url) {
1944
1952
  cleanUrl.searchParams.sort();
1945
1953
  return cleanUrl.toString();
1946
1954
  }
1947
- var _error, _fetchClient2, _sseFetchClient, _messageParser, _subscribers, _started, _syncState, _connected, _mode, _onError, _requestAbortController, _refreshCount, _snapshotCounter, _ShapeStream_instances, isRefreshing_get, _tickPromise, _tickPromiseResolver, _tickPromiseRejecter, _messageChain, _snapshotTracker, _pauseLock, _currentFetchUrl, _lastSseConnectionStartTime, _minSseConnectionDuration, _maxShortSseConnections, _sseBackoffBaseDelay, _sseBackoffMaxDelay, _unsubscribeFromVisibilityChanges, _unsubscribeFromWakeDetection, _maxStaleCacheRetries, start_fn, teardown_fn, requestShape_fn, constructUrl_fn, createAbortListener_fn, onInitialResponse_fn, onMessages_fn, fetchShape_fn, requestShapeLongPoll_fn, requestShapeSSE_fn, nextTick_fn, publish_fn, sendErrorToSubscribers_fn, hasBrowserVisibilityAPI_fn, subscribeToVisibilityChanges_fn, subscribeToWakeDetection_fn, reset_fn, buildSubsetBody_fn;
1955
+ var _error, _fetchClient2, _sseFetchClient, _messageParser, _subscribers, _started, _syncState, _connected, _mode, _onError, _requestAbortController, _refreshCount, _snapshotCounter, _ShapeStream_instances, isRefreshing_get, _tickPromise, _tickPromiseResolver, _tickPromiseRejecter, _messageChain, _snapshotTracker, _pauseLock, _currentFetchUrl, _lastSseConnectionStartTime, _minSseConnectionDuration, _maxShortSseConnections, _sseBackoffBaseDelay, _sseBackoffMaxDelay, _unsubscribeFromVisibilityChanges, _unsubscribeFromWakeDetection, _maxStaleCacheRetries, _recentRequestEntries, _fastLoopWindowMs, _fastLoopThreshold, _fastLoopBackoffBaseMs, _fastLoopBackoffMaxMs, _fastLoopConsecutiveCount, _fastLoopMaxCount, start_fn, teardown_fn, requestShape_fn, checkFastLoop_fn, constructUrl_fn, createAbortListener_fn, onInitialResponse_fn, onMessages_fn, fetchShape_fn, requestShapeLongPoll_fn, requestShapeSSE_fn, nextTick_fn, publish_fn, sendErrorToSubscribers_fn, hasBrowserVisibilityAPI_fn, subscribeToVisibilityChanges_fn, subscribeToWakeDetection_fn, reset_fn, buildSubsetBody_fn;
1948
1956
  var ShapeStream = class {
1949
1957
  constructor(options) {
1950
1958
  __privateAdd(this, _ShapeStream_instances);
@@ -1982,6 +1990,15 @@ var ShapeStream = class {
1982
1990
  __privateAdd(this, _unsubscribeFromVisibilityChanges);
1983
1991
  __privateAdd(this, _unsubscribeFromWakeDetection);
1984
1992
  __privateAdd(this, _maxStaleCacheRetries, 3);
1993
+ // Fast-loop detection: track recent non-live requests to detect tight retry
1994
+ // loops caused by proxy/CDN misconfiguration or stale client-side caches
1995
+ __privateAdd(this, _recentRequestEntries, []);
1996
+ __privateAdd(this, _fastLoopWindowMs, 500);
1997
+ __privateAdd(this, _fastLoopThreshold, 5);
1998
+ __privateAdd(this, _fastLoopBackoffBaseMs, 100);
1999
+ __privateAdd(this, _fastLoopBackoffMaxMs, 5e3);
2000
+ __privateAdd(this, _fastLoopConsecutiveCount, 0);
2001
+ __privateAdd(this, _fastLoopMaxCount, 5);
1985
2002
  var _a, _b, _c, _d;
1986
2003
  this.options = __spreadValues({ subscribe: true }, options);
1987
2004
  validateOptions(this.options);
@@ -2039,7 +2056,6 @@ var ShapeStream = class {
2039
2056
  ));
2040
2057
  __privateSet(this, _fetchClient2, createFetchWithConsumedMessages(__privateGet(this, _sseFetchClient)));
2041
2058
  __privateMethod(this, _ShapeStream_instances, subscribeToVisibilityChanges_fn).call(this);
2042
- __privateMethod(this, _ShapeStream_instances, subscribeToWakeDetection_fn).call(this);
2043
2059
  }
2044
2060
  get shapeHandle() {
2045
2061
  return __privateGet(this, _syncState).handle;
@@ -2279,9 +2295,17 @@ _sseBackoffMaxDelay = new WeakMap();
2279
2295
  _unsubscribeFromVisibilityChanges = new WeakMap();
2280
2296
  _unsubscribeFromWakeDetection = new WeakMap();
2281
2297
  _maxStaleCacheRetries = new WeakMap();
2298
+ _recentRequestEntries = new WeakMap();
2299
+ _fastLoopWindowMs = new WeakMap();
2300
+ _fastLoopThreshold = new WeakMap();
2301
+ _fastLoopBackoffBaseMs = new WeakMap();
2302
+ _fastLoopBackoffMaxMs = new WeakMap();
2303
+ _fastLoopConsecutiveCount = new WeakMap();
2304
+ _fastLoopMaxCount = new WeakMap();
2282
2305
  start_fn = async function() {
2283
2306
  var _a, _b;
2284
2307
  __privateSet(this, _started, true);
2308
+ __privateMethod(this, _ShapeStream_instances, subscribeToWakeDetection_fn).call(this);
2285
2309
  try {
2286
2310
  await __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
2287
2311
  } catch (err) {
@@ -2303,6 +2327,8 @@ start_fn = async function() {
2303
2327
  if (__privateGet(this, _syncState) instanceof ErrorState) {
2304
2328
  __privateSet(this, _syncState, __privateGet(this, _syncState).retry());
2305
2329
  }
2330
+ __privateSet(this, _fastLoopConsecutiveCount, 0);
2331
+ __privateSet(this, _recentRequestEntries, []);
2306
2332
  __privateSet(this, _started, false);
2307
2333
  await __privateMethod(this, _ShapeStream_instances, start_fn).call(this);
2308
2334
  return;
@@ -2333,6 +2359,12 @@ requestShape_fn = async function() {
2333
2359
  if (!this.options.subscribe && (((_a = this.options.signal) == null ? void 0 : _a.aborted) || __privateGet(this, _syncState).isUpToDate)) {
2334
2360
  return;
2335
2361
  }
2362
+ if (!__privateGet(this, _syncState).isUpToDate) {
2363
+ await __privateMethod(this, _ShapeStream_instances, checkFastLoop_fn).call(this);
2364
+ } else {
2365
+ __privateSet(this, _fastLoopConsecutiveCount, 0);
2366
+ __privateSet(this, _recentRequestEntries, []);
2367
+ }
2336
2368
  let resumingFromPause = false;
2337
2369
  if (__privateGet(this, _syncState) instanceof PausedState) {
2338
2370
  resumingFromPause = true;
@@ -2391,6 +2423,56 @@ requestShape_fn = async function() {
2391
2423
  (_b = __privateGet(this, _tickPromiseResolver)) == null ? void 0 : _b.call(this);
2392
2424
  return __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
2393
2425
  };
2426
+ checkFastLoop_fn = async function() {
2427
+ const now = Date.now();
2428
+ const currentOffset = __privateGet(this, _syncState).offset;
2429
+ __privateSet(this, _recentRequestEntries, __privateGet(this, _recentRequestEntries).filter(
2430
+ (e) => now - e.timestamp < __privateGet(this, _fastLoopWindowMs)
2431
+ ));
2432
+ __privateGet(this, _recentRequestEntries).push({ timestamp: now, offset: currentOffset });
2433
+ const sameOffsetCount = __privateGet(this, _recentRequestEntries).filter(
2434
+ (e) => e.offset === currentOffset
2435
+ ).length;
2436
+ if (sameOffsetCount < __privateGet(this, _fastLoopThreshold)) return;
2437
+ __privateWrapper(this, _fastLoopConsecutiveCount)._++;
2438
+ if (__privateGet(this, _fastLoopConsecutiveCount) >= __privateGet(this, _fastLoopMaxCount)) {
2439
+ throw new FetchError(
2440
+ 502,
2441
+ void 0,
2442
+ void 0,
2443
+ {},
2444
+ this.options.url,
2445
+ `Client is stuck in a fast retry loop (${__privateGet(this, _fastLoopThreshold)} requests in ${__privateGet(this, _fastLoopWindowMs)}ms at the same offset, repeated ${__privateGet(this, _fastLoopMaxCount)} times). Client-side caches were cleared automatically on first detection, but the loop persists. This usually indicates a proxy or CDN misconfiguration. Common causes:
2446
+ - Proxy is not including query parameters (handle, offset) in its cache key
2447
+ - CDN is serving stale 409 responses
2448
+ - Proxy is stripping required Electric headers from responses
2449
+ For more information visit the troubleshooting guide: ${TROUBLESHOOTING_URL}`
2450
+ );
2451
+ }
2452
+ if (__privateGet(this, _fastLoopConsecutiveCount) === 1) {
2453
+ console.warn(
2454
+ `[Electric] Detected fast retry loop (${__privateGet(this, _fastLoopThreshold)} requests in ${__privateGet(this, _fastLoopWindowMs)}ms at the same offset). Clearing client-side caches and resetting stream to recover. If this persists, check that your proxy includes all query parameters (especially 'handle' and 'offset') in its cache key, and that required Electric headers are forwarded to the client. For more information visit the troubleshooting guide: ${TROUBLESHOOTING_URL}`
2455
+ );
2456
+ if (__privateGet(this, _currentFetchUrl)) {
2457
+ const shapeKey = canonicalShapeKey(__privateGet(this, _currentFetchUrl));
2458
+ expiredShapesCache.delete(shapeKey);
2459
+ upToDateTracker.delete(shapeKey);
2460
+ } else {
2461
+ expiredShapesCache.clear();
2462
+ upToDateTracker.clear();
2463
+ }
2464
+ __privateMethod(this, _ShapeStream_instances, reset_fn).call(this);
2465
+ __privateSet(this, _recentRequestEntries, []);
2466
+ return;
2467
+ }
2468
+ const maxDelay = Math.min(
2469
+ __privateGet(this, _fastLoopBackoffMaxMs),
2470
+ __privateGet(this, _fastLoopBackoffBaseMs) * Math.pow(2, __privateGet(this, _fastLoopConsecutiveCount))
2471
+ );
2472
+ const delayMs = Math.floor(Math.random() * maxDelay);
2473
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
2474
+ __privateSet(this, _recentRequestEntries, []);
2475
+ };
2394
2476
  constructUrl_fn = async function(url, resumingFromPause, subsetParams) {
2395
2477
  var _a, _b, _c, _d, _e, _f;
2396
2478
  const [requestHeaders, params] = await Promise.all([
@@ -2541,11 +2623,11 @@ onInitialResponse_fn = async function(response) {
2541
2623
  void 0,
2542
2624
  {},
2543
2625
  (_c = (_b = __privateGet(this, _currentFetchUrl)) == null ? void 0 : _b.toString()) != null ? _c : ``,
2544
- `CDN continues serving stale cached responses after ${__privateGet(this, _maxStaleCacheRetries)} retry attempts. This indicates a severe proxy/CDN misconfiguration. Check that your proxy includes all query parameters (especially 'handle' and 'offset') in its cache key. For more information visit the troubleshooting guide: https://electric-sql.com/docs/guides/troubleshooting`
2626
+ `CDN continues serving stale cached responses after ${__privateGet(this, _maxStaleCacheRetries)} retry attempts. This indicates a severe proxy/CDN misconfiguration. Check that your proxy includes all query parameters (especially 'handle' and 'offset') in its cache key. For more information visit the troubleshooting guide: ${TROUBLESHOOTING_URL}`
2545
2627
  );
2546
2628
  }
2547
2629
  console.warn(
2548
- `[Electric] Received stale cached response with expired shape handle. This should not happen and indicates a proxy/CDN caching misconfiguration. The response contained handle "${shapeHandle}" which was previously marked as expired. Check that your proxy includes all query parameters (especially 'handle' and 'offset') in its cache key. For more information visit the troubleshooting guide: https://electric-sql.com/docs/guides/troubleshooting Retrying with a random cache buster to bypass the stale cache (attempt ${__privateGet(this, _syncState).staleCacheRetryCount}/${__privateGet(this, _maxStaleCacheRetries)}).`
2630
+ `[Electric] Received stale cached response with expired shape handle. This should not happen and indicates a proxy/CDN caching misconfiguration. The response contained handle "${shapeHandle}" which was previously marked as expired. Check that your proxy includes all query parameters (especially 'handle' and 'offset') in its cache key. For more information visit the troubleshooting guide: ${TROUBLESHOOTING_URL} Retrying with a random cache buster to bypass the stale cache (attempt ${__privateGet(this, _syncState).staleCacheRetryCount}/${__privateGet(this, _maxStaleCacheRetries)}).`
2549
2631
  );
2550
2632
  throw new StaleCacheError(
2551
2633
  `Received stale cached response with expired handle "${shapeHandle}". This indicates a proxy/CDN caching misconfiguration. Check that your proxy includes all query parameters (especially 'handle' and 'offset') in its cache key.`
@@ -2777,6 +2859,7 @@ subscribeToVisibilityChanges_fn = function() {
2777
2859
  document.addEventListener(`visibilitychange`, visibilityHandler);
2778
2860
  __privateSet(this, _unsubscribeFromVisibilityChanges, () => {
2779
2861
  document.removeEventListener(`visibilitychange`, visibilityHandler);
2862
+ __privateSet(this, _unsubscribeFromVisibilityChanges, void 0);
2780
2863
  });
2781
2864
  }
2782
2865
  };
@@ -2794,6 +2877,7 @@ subscribeToVisibilityChanges_fn = function() {
2794
2877
  */
2795
2878
  subscribeToWakeDetection_fn = function() {
2796
2879
  if (__privateMethod(this, _ShapeStream_instances, hasBrowserVisibilityAPI_fn).call(this)) return;
2880
+ if (__privateGet(this, _unsubscribeFromWakeDetection)) return;
2797
2881
  const INTERVAL_MS = 2e3;
2798
2882
  const WAKE_THRESHOLD_MS = 4e3;
2799
2883
  let lastTickTime = Date.now();
@@ -2816,6 +2900,7 @@ subscribeToWakeDetection_fn = function() {
2816
2900
  }
2817
2901
  __privateSet(this, _unsubscribeFromWakeDetection, () => {
2818
2902
  clearInterval(timer);
2903
+ __privateSet(this, _unsubscribeFromWakeDetection, void 0);
2819
2904
  });
2820
2905
  };
2821
2906
  /**