@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.
package/dist/index.mjs CHANGED
@@ -429,7 +429,7 @@ function isChangeMessage(message) {
429
429
  return message != null && `key` in message;
430
430
  }
431
431
  function isControlMessage(message) {
432
- return message != null && !isChangeMessage(message);
432
+ return message != null && `headers` in message && `control` in message.headers;
433
433
  }
434
434
  function isUpToDateMessage(message) {
435
435
  return isControlMessage(message) && message.headers.control === `up-to-date`;
@@ -504,10 +504,9 @@ var ELECTRIC_PROTOCOL_QUERY_PARAMS = [
504
504
  // src/fetch.ts
505
505
  var HTTP_RETRY_STATUS_CODES = [429];
506
506
  var BackoffDefaults = {
507
- initialDelay: 100,
508
- maxDelay: 6e4,
509
- // Cap at 60s - reasonable for long-lived connections
510
- multiplier: 1.3,
507
+ initialDelay: 1e3,
508
+ maxDelay: 32e3,
509
+ multiplier: 2,
511
510
  maxRetries: Infinity
512
511
  // Retry forever - clients may go offline and come back
513
512
  };
@@ -1122,6 +1121,10 @@ var ExpiredShapesCache = class {
1122
1121
  this.data = {};
1123
1122
  this.save();
1124
1123
  }
1124
+ delete(shapeUrl) {
1125
+ delete this.data[shapeUrl];
1126
+ this.save();
1127
+ }
1125
1128
  };
1126
1129
  var expiredShapesCache = new ExpiredShapesCache();
1127
1130
 
@@ -1243,6 +1246,10 @@ var UpToDateTracker = class {
1243
1246
  }
1244
1247
  this.save();
1245
1248
  }
1249
+ delete(shapeKey) {
1250
+ delete this.data[shapeKey];
1251
+ this.save();
1252
+ }
1246
1253
  };
1247
1254
  var upToDateTracker = new UpToDateTracker();
1248
1255
 
@@ -1867,6 +1874,7 @@ var RESERVED_PARAMS = /* @__PURE__ */ new Set([
1867
1874
  OFFSET_QUERY_PARAM,
1868
1875
  CACHE_BUSTER_QUERY_PARAM
1869
1876
  ]);
1877
+ var TROUBLESHOOTING_URL = `https://electric-sql.com/docs/guides/troubleshooting`;
1870
1878
  async function resolveValue(value) {
1871
1879
  if (typeof value === `function`) {
1872
1880
  return value();
@@ -1907,7 +1915,7 @@ function canonicalShapeKey(url) {
1907
1915
  cleanUrl.searchParams.sort();
1908
1916
  return cleanUrl.toString();
1909
1917
  }
1910
- 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;
1918
+ 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;
1911
1919
  var ShapeStream = class {
1912
1920
  constructor(options) {
1913
1921
  __privateAdd(this, _ShapeStream_instances);
@@ -1945,6 +1953,15 @@ var ShapeStream = class {
1945
1953
  __privateAdd(this, _unsubscribeFromVisibilityChanges);
1946
1954
  __privateAdd(this, _unsubscribeFromWakeDetection);
1947
1955
  __privateAdd(this, _maxStaleCacheRetries, 3);
1956
+ // Fast-loop detection: track recent non-live requests to detect tight retry
1957
+ // loops caused by proxy/CDN misconfiguration or stale client-side caches
1958
+ __privateAdd(this, _recentRequestEntries, []);
1959
+ __privateAdd(this, _fastLoopWindowMs, 500);
1960
+ __privateAdd(this, _fastLoopThreshold, 5);
1961
+ __privateAdd(this, _fastLoopBackoffBaseMs, 100);
1962
+ __privateAdd(this, _fastLoopBackoffMaxMs, 5e3);
1963
+ __privateAdd(this, _fastLoopConsecutiveCount, 0);
1964
+ __privateAdd(this, _fastLoopMaxCount, 5);
1948
1965
  var _a, _b, _c, _d;
1949
1966
  this.options = __spreadValues({ subscribe: true }, options);
1950
1967
  validateOptions(this.options);
@@ -2002,7 +2019,6 @@ var ShapeStream = class {
2002
2019
  ));
2003
2020
  __privateSet(this, _fetchClient2, createFetchWithConsumedMessages(__privateGet(this, _sseFetchClient)));
2004
2021
  __privateMethod(this, _ShapeStream_instances, subscribeToVisibilityChanges_fn).call(this);
2005
- __privateMethod(this, _ShapeStream_instances, subscribeToWakeDetection_fn).call(this);
2006
2022
  }
2007
2023
  get shapeHandle() {
2008
2024
  return __privateGet(this, _syncState).handle;
@@ -2242,9 +2258,17 @@ _sseBackoffMaxDelay = new WeakMap();
2242
2258
  _unsubscribeFromVisibilityChanges = new WeakMap();
2243
2259
  _unsubscribeFromWakeDetection = new WeakMap();
2244
2260
  _maxStaleCacheRetries = new WeakMap();
2261
+ _recentRequestEntries = new WeakMap();
2262
+ _fastLoopWindowMs = new WeakMap();
2263
+ _fastLoopThreshold = new WeakMap();
2264
+ _fastLoopBackoffBaseMs = new WeakMap();
2265
+ _fastLoopBackoffMaxMs = new WeakMap();
2266
+ _fastLoopConsecutiveCount = new WeakMap();
2267
+ _fastLoopMaxCount = new WeakMap();
2245
2268
  start_fn = async function() {
2246
2269
  var _a, _b;
2247
2270
  __privateSet(this, _started, true);
2271
+ __privateMethod(this, _ShapeStream_instances, subscribeToWakeDetection_fn).call(this);
2248
2272
  try {
2249
2273
  await __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
2250
2274
  } catch (err) {
@@ -2266,6 +2290,8 @@ start_fn = async function() {
2266
2290
  if (__privateGet(this, _syncState) instanceof ErrorState) {
2267
2291
  __privateSet(this, _syncState, __privateGet(this, _syncState).retry());
2268
2292
  }
2293
+ __privateSet(this, _fastLoopConsecutiveCount, 0);
2294
+ __privateSet(this, _recentRequestEntries, []);
2269
2295
  __privateSet(this, _started, false);
2270
2296
  await __privateMethod(this, _ShapeStream_instances, start_fn).call(this);
2271
2297
  return;
@@ -2296,6 +2322,12 @@ requestShape_fn = async function() {
2296
2322
  if (!this.options.subscribe && (((_a = this.options.signal) == null ? void 0 : _a.aborted) || __privateGet(this, _syncState).isUpToDate)) {
2297
2323
  return;
2298
2324
  }
2325
+ if (!__privateGet(this, _syncState).isUpToDate) {
2326
+ await __privateMethod(this, _ShapeStream_instances, checkFastLoop_fn).call(this);
2327
+ } else {
2328
+ __privateSet(this, _fastLoopConsecutiveCount, 0);
2329
+ __privateSet(this, _recentRequestEntries, []);
2330
+ }
2299
2331
  let resumingFromPause = false;
2300
2332
  if (__privateGet(this, _syncState) instanceof PausedState) {
2301
2333
  resumingFromPause = true;
@@ -2354,6 +2386,56 @@ requestShape_fn = async function() {
2354
2386
  (_b = __privateGet(this, _tickPromiseResolver)) == null ? void 0 : _b.call(this);
2355
2387
  return __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
2356
2388
  };
2389
+ checkFastLoop_fn = async function() {
2390
+ const now = Date.now();
2391
+ const currentOffset = __privateGet(this, _syncState).offset;
2392
+ __privateSet(this, _recentRequestEntries, __privateGet(this, _recentRequestEntries).filter(
2393
+ (e) => now - e.timestamp < __privateGet(this, _fastLoopWindowMs)
2394
+ ));
2395
+ __privateGet(this, _recentRequestEntries).push({ timestamp: now, offset: currentOffset });
2396
+ const sameOffsetCount = __privateGet(this, _recentRequestEntries).filter(
2397
+ (e) => e.offset === currentOffset
2398
+ ).length;
2399
+ if (sameOffsetCount < __privateGet(this, _fastLoopThreshold)) return;
2400
+ __privateWrapper(this, _fastLoopConsecutiveCount)._++;
2401
+ if (__privateGet(this, _fastLoopConsecutiveCount) >= __privateGet(this, _fastLoopMaxCount)) {
2402
+ throw new FetchError(
2403
+ 502,
2404
+ void 0,
2405
+ void 0,
2406
+ {},
2407
+ this.options.url,
2408
+ `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:
2409
+ - Proxy is not including query parameters (handle, offset) in its cache key
2410
+ - CDN is serving stale 409 responses
2411
+ - Proxy is stripping required Electric headers from responses
2412
+ For more information visit the troubleshooting guide: ${TROUBLESHOOTING_URL}`
2413
+ );
2414
+ }
2415
+ if (__privateGet(this, _fastLoopConsecutiveCount) === 1) {
2416
+ console.warn(
2417
+ `[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}`
2418
+ );
2419
+ if (__privateGet(this, _currentFetchUrl)) {
2420
+ const shapeKey = canonicalShapeKey(__privateGet(this, _currentFetchUrl));
2421
+ expiredShapesCache.delete(shapeKey);
2422
+ upToDateTracker.delete(shapeKey);
2423
+ } else {
2424
+ expiredShapesCache.clear();
2425
+ upToDateTracker.clear();
2426
+ }
2427
+ __privateMethod(this, _ShapeStream_instances, reset_fn).call(this);
2428
+ __privateSet(this, _recentRequestEntries, []);
2429
+ return;
2430
+ }
2431
+ const maxDelay = Math.min(
2432
+ __privateGet(this, _fastLoopBackoffMaxMs),
2433
+ __privateGet(this, _fastLoopBackoffBaseMs) * Math.pow(2, __privateGet(this, _fastLoopConsecutiveCount))
2434
+ );
2435
+ const delayMs = Math.floor(Math.random() * maxDelay);
2436
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
2437
+ __privateSet(this, _recentRequestEntries, []);
2438
+ };
2357
2439
  constructUrl_fn = async function(url, resumingFromPause, subsetParams) {
2358
2440
  var _a, _b, _c, _d, _e, _f;
2359
2441
  const [requestHeaders, params] = await Promise.all([
@@ -2504,11 +2586,11 @@ onInitialResponse_fn = async function(response) {
2504
2586
  void 0,
2505
2587
  {},
2506
2588
  (_c = (_b = __privateGet(this, _currentFetchUrl)) == null ? void 0 : _b.toString()) != null ? _c : ``,
2507
- `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`
2589
+ `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}`
2508
2590
  );
2509
2591
  }
2510
2592
  console.warn(
2511
- `[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)}).`
2593
+ `[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)}).`
2512
2594
  );
2513
2595
  throw new StaleCacheError(
2514
2596
  `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.`
@@ -2740,6 +2822,7 @@ subscribeToVisibilityChanges_fn = function() {
2740
2822
  document.addEventListener(`visibilitychange`, visibilityHandler);
2741
2823
  __privateSet(this, _unsubscribeFromVisibilityChanges, () => {
2742
2824
  document.removeEventListener(`visibilitychange`, visibilityHandler);
2825
+ __privateSet(this, _unsubscribeFromVisibilityChanges, void 0);
2743
2826
  });
2744
2827
  }
2745
2828
  };
@@ -2757,6 +2840,7 @@ subscribeToVisibilityChanges_fn = function() {
2757
2840
  */
2758
2841
  subscribeToWakeDetection_fn = function() {
2759
2842
  if (__privateMethod(this, _ShapeStream_instances, hasBrowserVisibilityAPI_fn).call(this)) return;
2843
+ if (__privateGet(this, _unsubscribeFromWakeDetection)) return;
2760
2844
  const INTERVAL_MS = 2e3;
2761
2845
  const WAKE_THRESHOLD_MS = 4e3;
2762
2846
  let lastTickTime = Date.now();
@@ -2779,6 +2863,7 @@ subscribeToWakeDetection_fn = function() {
2779
2863
  }
2780
2864
  __privateSet(this, _unsubscribeFromWakeDetection, () => {
2781
2865
  clearInterval(timer);
2866
+ __privateSet(this, _unsubscribeFromWakeDetection, void 0);
2782
2867
  });
2783
2868
  };
2784
2869
  /**