@electric-sql/client 1.4.0 → 1.4.1

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
@@ -130,6 +130,12 @@ For more information visit the troubleshooting guide: /docs/guides/troubleshooti
130
130
  super(msg);
131
131
  }
132
132
  };
133
+ var StaleCacheError = class extends Error {
134
+ constructor(message) {
135
+ super(message);
136
+ this.name = `StaleCacheError`;
137
+ }
138
+ };
133
139
 
134
140
  // src/parser.ts
135
141
  var parseNumber = (value) => Number(value);
@@ -469,6 +475,7 @@ var SUBSET_PARAM_ORDER_BY = `subset__order_by`;
469
475
  var SUBSET_PARAM_WHERE_PARAMS = `subset__params`;
470
476
  var SUBSET_PARAM_WHERE_EXPR = `subset__where_expr`;
471
477
  var SUBSET_PARAM_ORDER_BY_EXPR = `subset__order_by_expr`;
478
+ var CACHE_BUSTER_QUERY_PARAM = `cache-buster`;
472
479
  var ELECTRIC_PROTOCOL_QUERY_PARAMS = [
473
480
  LIVE_QUERY_PARAM,
474
481
  LIVE_SSE_QUERY_PARAM,
@@ -483,7 +490,8 @@ var ELECTRIC_PROTOCOL_QUERY_PARAMS = [
483
490
  SUBSET_PARAM_ORDER_BY,
484
491
  SUBSET_PARAM_WHERE_PARAMS,
485
492
  SUBSET_PARAM_WHERE_EXPR,
486
- SUBSET_PARAM_ORDER_BY_EXPR
493
+ SUBSET_PARAM_ORDER_BY_EXPR,
494
+ CACHE_BUSTER_QUERY_PARAM
487
495
  ];
488
496
 
489
497
  // src/fetch.ts
@@ -1091,7 +1099,8 @@ var RESERVED_PARAMS = /* @__PURE__ */ new Set([
1091
1099
  LIVE_CACHE_BUSTER_QUERY_PARAM,
1092
1100
  SHAPE_HANDLE_QUERY_PARAM,
1093
1101
  LIVE_QUERY_PARAM,
1094
- OFFSET_QUERY_PARAM
1102
+ OFFSET_QUERY_PARAM,
1103
+ CACHE_BUSTER_QUERY_PARAM
1095
1104
  ]);
1096
1105
  async function resolveValue(value) {
1097
1106
  if (typeof value === `function`) {
@@ -1133,7 +1142,7 @@ function canonicalShapeKey(url) {
1133
1142
  cleanUrl.searchParams.sort();
1134
1143
  return cleanUrl.toString();
1135
1144
  }
1136
- 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, _lastSeenCursor, _currentFetchUrl, _lastSseConnectionStartTime, _minSseConnectionDuration, _consecutiveShortSseConnections, _maxShortSseConnections, _sseFallbackToLongPolling, _sseBackoffBaseDelay, _sseBackoffMaxDelay, _unsubscribeFromVisibilityChanges, _ShapeStream_instances, replayMode_get, 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;
1145
+ 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, _lastSeenCursor, _currentFetchUrl, _lastSseConnectionStartTime, _minSseConnectionDuration, _consecutiveShortSseConnections, _maxShortSseConnections, _sseFallbackToLongPolling, _sseBackoffBaseDelay, _sseBackoffMaxDelay, _unsubscribeFromVisibilityChanges, _staleCacheBuster, _staleCacheRetryCount, _maxStaleCacheRetries, _ShapeStream_instances, replayMode_get, 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;
1137
1146
  var ShapeStream = class {
1138
1147
  constructor(options) {
1139
1148
  __privateAdd(this, _ShapeStream_instances);
@@ -1184,6 +1193,10 @@ var ShapeStream = class {
1184
1193
  __privateAdd(this, _sseBackoffMaxDelay, 5e3);
1185
1194
  // Maximum delay cap (ms)
1186
1195
  __privateAdd(this, _unsubscribeFromVisibilityChanges);
1196
+ __privateAdd(this, _staleCacheBuster);
1197
+ // Cache buster set when stale CDN response detected, used on retry requests to bypass cache
1198
+ __privateAdd(this, _staleCacheRetryCount, 0);
1199
+ __privateAdd(this, _maxStaleCacheRetries, 3);
1187
1200
  var _a, _b, _c, _d;
1188
1201
  this.options = __spreadValues({ subscribe: true }, options);
1189
1202
  validateOptions(this.options);
@@ -1414,6 +1427,9 @@ _sseFallbackToLongPolling = new WeakMap();
1414
1427
  _sseBackoffBaseDelay = new WeakMap();
1415
1428
  _sseBackoffMaxDelay = new WeakMap();
1416
1429
  _unsubscribeFromVisibilityChanges = new WeakMap();
1430
+ _staleCacheBuster = new WeakMap();
1431
+ _staleCacheRetryCount = new WeakMap();
1432
+ _maxStaleCacheRetries = new WeakMap();
1417
1433
  _ShapeStream_instances = new WeakSet();
1418
1434
  replayMode_get = function() {
1419
1435
  return __privateGet(this, _lastSeenCursor) !== void 0;
@@ -1427,7 +1443,8 @@ start_fn = async function() {
1427
1443
  __privateSet(this, _error, err);
1428
1444
  if (__privateGet(this, _onError)) {
1429
1445
  const retryOpts = await __privateGet(this, _onError).call(this, err);
1430
- if (retryOpts && typeof retryOpts === `object`) {
1446
+ const isRetryable = !(err instanceof MissingHeadersError);
1447
+ if (retryOpts && typeof retryOpts === `object` && isRetryable) {
1431
1448
  if (retryOpts.params) {
1432
1449
  this.options.params = __spreadValues(__spreadValues({}, (_a = this.options.params) != null ? _a : {}), retryOpts.params);
1433
1450
  }
@@ -1489,6 +1506,9 @@ requestShape_fn = async function() {
1489
1506
  }
1490
1507
  return;
1491
1508
  }
1509
+ if (e instanceof StaleCacheError) {
1510
+ return __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
1511
+ }
1492
1512
  if (!(e instanceof FetchError)) throw e;
1493
1513
  if (e.status == 409) {
1494
1514
  if (__privateGet(this, _shapeHandle)) {
@@ -1621,6 +1641,12 @@ constructUrl_fn = async function(url, resumingFromPause, subsetParams) {
1621
1641
  if (expiredHandle) {
1622
1642
  fetchUrl.searchParams.set(EXPIRED_HANDLE_QUERY_PARAM, expiredHandle);
1623
1643
  }
1644
+ if (__privateGet(this, _staleCacheBuster)) {
1645
+ fetchUrl.searchParams.set(
1646
+ CACHE_BUSTER_QUERY_PARAM,
1647
+ __privateGet(this, _staleCacheBuster)
1648
+ );
1649
+ }
1624
1650
  fetchUrl.searchParams.sort();
1625
1651
  return {
1626
1652
  fetchUrl,
@@ -1643,7 +1669,7 @@ createAbortListener_fn = async function(signal) {
1643
1669
  }
1644
1670
  };
1645
1671
  onInitialResponse_fn = async function(response) {
1646
- var _a;
1672
+ var _a, _b, _c, _d;
1647
1673
  const { headers, status } = response;
1648
1674
  const shapeHandle = headers.get(SHAPE_HANDLE_HEADER);
1649
1675
  if (shapeHandle) {
@@ -1651,6 +1677,30 @@ onInitialResponse_fn = async function(response) {
1651
1677
  const expiredHandle = shapeKey ? expiredShapesCache.getExpiredHandle(shapeKey) : null;
1652
1678
  if (shapeHandle !== expiredHandle) {
1653
1679
  __privateSet(this, _shapeHandle, shapeHandle);
1680
+ if (__privateGet(this, _staleCacheBuster)) {
1681
+ __privateSet(this, _staleCacheBuster, void 0);
1682
+ __privateSet(this, _staleCacheRetryCount, 0);
1683
+ }
1684
+ } else if (__privateGet(this, _shapeHandle) === void 0) {
1685
+ __privateWrapper(this, _staleCacheRetryCount)._++;
1686
+ await ((_a = response.body) == null ? void 0 : _a.cancel());
1687
+ if (__privateGet(this, _staleCacheRetryCount) > __privateGet(this, _maxStaleCacheRetries)) {
1688
+ throw new FetchError(
1689
+ 502,
1690
+ void 0,
1691
+ void 0,
1692
+ {},
1693
+ (_c = (_b = __privateGet(this, _currentFetchUrl)) == null ? void 0 : _b.toString()) != null ? _c : ``,
1694
+ `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`
1695
+ );
1696
+ }
1697
+ console.warn(
1698
+ `[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, _staleCacheRetryCount)}/${__privateGet(this, _maxStaleCacheRetries)}).`
1699
+ );
1700
+ __privateSet(this, _staleCacheBuster, `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`);
1701
+ throw new StaleCacheError(
1702
+ `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.`
1703
+ );
1654
1704
  } else {
1655
1705
  console.warn(
1656
1706
  `[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. Ignoring the stale handle and continuing with handle "${__privateGet(this, _shapeHandle)}".`
@@ -1665,7 +1715,7 @@ onInitialResponse_fn = async function(response) {
1665
1715
  if (liveCacheBuster) {
1666
1716
  __privateSet(this, _liveCacheBuster, liveCacheBuster);
1667
1717
  }
1668
- __privateSet(this, _schema, (_a = __privateGet(this, _schema)) != null ? _a : getSchemaFromHeaders(headers));
1718
+ __privateSet(this, _schema, (_d = __privateGet(this, _schema)) != null ? _d : getSchemaFromHeaders(headers));
1669
1719
  if (status === 204) {
1670
1720
  __privateSet(this, _lastSyncedAt, Date.now());
1671
1721
  }
@@ -1903,6 +1953,8 @@ reset_fn = function(handle) {
1903
1953
  __privateSet(this, _activeSnapshotRequests, 0);
1904
1954
  __privateSet(this, _consecutiveShortSseConnections, 0);
1905
1955
  __privateSet(this, _sseFallbackToLongPolling, false);
1956
+ __privateSet(this, _staleCacheBuster, void 0);
1957
+ __privateSet(this, _staleCacheRetryCount, 0);
1906
1958
  };
1907
1959
  ShapeStream.Replica = {
1908
1960
  FULL: `full`,