@electric-sql/client 1.4.0 → 1.4.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.
@@ -167,6 +167,12 @@ For more information visit the troubleshooting guide: /docs/guides/troubleshooti
167
167
  super(msg);
168
168
  }
169
169
  };
170
+ var StaleCacheError = class extends Error {
171
+ constructor(message) {
172
+ super(message);
173
+ this.name = `StaleCacheError`;
174
+ }
175
+ };
170
176
 
171
177
  // src/parser.ts
172
178
  var parseNumber = (value) => Number(value);
@@ -506,6 +512,7 @@ var SUBSET_PARAM_ORDER_BY = `subset__order_by`;
506
512
  var SUBSET_PARAM_WHERE_PARAMS = `subset__params`;
507
513
  var SUBSET_PARAM_WHERE_EXPR = `subset__where_expr`;
508
514
  var SUBSET_PARAM_ORDER_BY_EXPR = `subset__order_by_expr`;
515
+ var CACHE_BUSTER_QUERY_PARAM = `cache-buster`;
509
516
  var ELECTRIC_PROTOCOL_QUERY_PARAMS = [
510
517
  LIVE_QUERY_PARAM,
511
518
  LIVE_SSE_QUERY_PARAM,
@@ -520,7 +527,8 @@ var ELECTRIC_PROTOCOL_QUERY_PARAMS = [
520
527
  SUBSET_PARAM_ORDER_BY,
521
528
  SUBSET_PARAM_WHERE_PARAMS,
522
529
  SUBSET_PARAM_WHERE_EXPR,
523
- SUBSET_PARAM_ORDER_BY_EXPR
530
+ SUBSET_PARAM_ORDER_BY_EXPR,
531
+ CACHE_BUSTER_QUERY_PARAM
524
532
  ];
525
533
 
526
534
  // src/fetch.ts
@@ -1126,7 +1134,8 @@ var RESERVED_PARAMS = /* @__PURE__ */ new Set([
1126
1134
  LIVE_CACHE_BUSTER_QUERY_PARAM,
1127
1135
  SHAPE_HANDLE_QUERY_PARAM,
1128
1136
  LIVE_QUERY_PARAM,
1129
- OFFSET_QUERY_PARAM
1137
+ OFFSET_QUERY_PARAM,
1138
+ CACHE_BUSTER_QUERY_PARAM
1130
1139
  ]);
1131
1140
  async function resolveValue(value) {
1132
1141
  if (typeof value === `function`) {
@@ -1168,7 +1177,7 @@ function canonicalShapeKey(url) {
1168
1177
  cleanUrl.searchParams.sort();
1169
1178
  return cleanUrl.toString();
1170
1179
  }
1171
- 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;
1180
+ 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;
1172
1181
  var ShapeStream = class {
1173
1182
  constructor(options) {
1174
1183
  __privateAdd(this, _ShapeStream_instances);
@@ -1219,6 +1228,10 @@ var ShapeStream = class {
1219
1228
  __privateAdd(this, _sseBackoffMaxDelay, 5e3);
1220
1229
  // Maximum delay cap (ms)
1221
1230
  __privateAdd(this, _unsubscribeFromVisibilityChanges);
1231
+ __privateAdd(this, _staleCacheBuster);
1232
+ // Cache buster set when stale CDN response detected, used on retry requests to bypass cache
1233
+ __privateAdd(this, _staleCacheRetryCount, 0);
1234
+ __privateAdd(this, _maxStaleCacheRetries, 3);
1222
1235
  var _a, _b, _c, _d;
1223
1236
  this.options = __spreadValues({ subscribe: true }, options);
1224
1237
  validateOptions(this.options);
@@ -1277,7 +1290,7 @@ var ShapeStream = class {
1277
1290
  }
1278
1291
  subscribe(callback, onError = () => {
1279
1292
  }) {
1280
- const subscriptionId = Math.random();
1293
+ const subscriptionId = {};
1281
1294
  __privateGet(this, _subscribers).set(subscriptionId, [callback, onError]);
1282
1295
  if (!__privateGet(this, _started)) __privateMethod(this, _ShapeStream_instances, start_fn).call(this);
1283
1296
  return () => {
@@ -1449,6 +1462,9 @@ _sseFallbackToLongPolling = new WeakMap();
1449
1462
  _sseBackoffBaseDelay = new WeakMap();
1450
1463
  _sseBackoffMaxDelay = new WeakMap();
1451
1464
  _unsubscribeFromVisibilityChanges = new WeakMap();
1465
+ _staleCacheBuster = new WeakMap();
1466
+ _staleCacheRetryCount = new WeakMap();
1467
+ _maxStaleCacheRetries = new WeakMap();
1452
1468
  _ShapeStream_instances = new WeakSet();
1453
1469
  replayMode_get = function() {
1454
1470
  return __privateGet(this, _lastSeenCursor) !== void 0;
@@ -1462,7 +1478,8 @@ start_fn = async function() {
1462
1478
  __privateSet(this, _error, err);
1463
1479
  if (__privateGet(this, _onError)) {
1464
1480
  const retryOpts = await __privateGet(this, _onError).call(this, err);
1465
- if (retryOpts && typeof retryOpts === `object`) {
1481
+ const isRetryable = !(err instanceof MissingHeadersError);
1482
+ if (retryOpts && typeof retryOpts === `object` && isRetryable) {
1466
1483
  if (retryOpts.params) {
1467
1484
  this.options.params = __spreadValues(__spreadValues({}, (_a = this.options.params) != null ? _a : {}), retryOpts.params);
1468
1485
  }
@@ -1524,6 +1541,9 @@ requestShape_fn = async function() {
1524
1541
  }
1525
1542
  return;
1526
1543
  }
1544
+ if (e instanceof StaleCacheError) {
1545
+ return __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
1546
+ }
1527
1547
  if (!(e instanceof FetchError)) throw e;
1528
1548
  if (e.status == 409) {
1529
1549
  if (__privateGet(this, _shapeHandle)) {
@@ -1656,6 +1676,12 @@ constructUrl_fn = async function(url, resumingFromPause, subsetParams) {
1656
1676
  if (expiredHandle) {
1657
1677
  fetchUrl.searchParams.set(EXPIRED_HANDLE_QUERY_PARAM, expiredHandle);
1658
1678
  }
1679
+ if (__privateGet(this, _staleCacheBuster)) {
1680
+ fetchUrl.searchParams.set(
1681
+ CACHE_BUSTER_QUERY_PARAM,
1682
+ __privateGet(this, _staleCacheBuster)
1683
+ );
1684
+ }
1659
1685
  fetchUrl.searchParams.sort();
1660
1686
  return {
1661
1687
  fetchUrl,
@@ -1678,7 +1704,7 @@ createAbortListener_fn = async function(signal) {
1678
1704
  }
1679
1705
  };
1680
1706
  onInitialResponse_fn = async function(response) {
1681
- var _a;
1707
+ var _a, _b, _c, _d;
1682
1708
  const { headers, status } = response;
1683
1709
  const shapeHandle = headers.get(SHAPE_HANDLE_HEADER);
1684
1710
  if (shapeHandle) {
@@ -1686,6 +1712,30 @@ onInitialResponse_fn = async function(response) {
1686
1712
  const expiredHandle = shapeKey ? expiredShapesCache.getExpiredHandle(shapeKey) : null;
1687
1713
  if (shapeHandle !== expiredHandle) {
1688
1714
  __privateSet(this, _shapeHandle, shapeHandle);
1715
+ if (__privateGet(this, _staleCacheBuster)) {
1716
+ __privateSet(this, _staleCacheBuster, void 0);
1717
+ __privateSet(this, _staleCacheRetryCount, 0);
1718
+ }
1719
+ } else if (__privateGet(this, _shapeHandle) === void 0) {
1720
+ __privateWrapper(this, _staleCacheRetryCount)._++;
1721
+ await ((_a = response.body) == null ? void 0 : _a.cancel());
1722
+ if (__privateGet(this, _staleCacheRetryCount) > __privateGet(this, _maxStaleCacheRetries)) {
1723
+ throw new FetchError(
1724
+ 502,
1725
+ void 0,
1726
+ void 0,
1727
+ {},
1728
+ (_c = (_b = __privateGet(this, _currentFetchUrl)) == null ? void 0 : _b.toString()) != null ? _c : ``,
1729
+ `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`
1730
+ );
1731
+ }
1732
+ console.warn(
1733
+ `[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)}).`
1734
+ );
1735
+ __privateSet(this, _staleCacheBuster, `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`);
1736
+ throw new StaleCacheError(
1737
+ `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.`
1738
+ );
1689
1739
  } else {
1690
1740
  console.warn(
1691
1741
  `[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)}".`
@@ -1700,7 +1750,7 @@ onInitialResponse_fn = async function(response) {
1700
1750
  if (liveCacheBuster) {
1701
1751
  __privateSet(this, _liveCacheBuster, liveCacheBuster);
1702
1752
  }
1703
- __privateSet(this, _schema, (_a = __privateGet(this, _schema)) != null ? _a : getSchemaFromHeaders(headers));
1753
+ __privateSet(this, _schema, (_d = __privateGet(this, _schema)) != null ? _d : getSchemaFromHeaders(headers));
1704
1754
  if (status === 204) {
1705
1755
  __privateSet(this, _lastSyncedAt, Date.now());
1706
1756
  }
@@ -1724,6 +1774,7 @@ onMessages_fn = async function(batch, isSseMessage = false) {
1724
1774
  if (__privateGet(this, _ShapeStream_instances, replayMode_get) && !isSseMessage) {
1725
1775
  const currentCursor = __privateGet(this, _liveCacheBuster);
1726
1776
  if (currentCursor === __privateGet(this, _lastSeenCursor)) {
1777
+ __privateSet(this, _lastSeenCursor, void 0);
1727
1778
  return;
1728
1779
  }
1729
1780
  }
@@ -1938,6 +1989,8 @@ reset_fn = function(handle) {
1938
1989
  __privateSet(this, _activeSnapshotRequests, 0);
1939
1990
  __privateSet(this, _consecutiveShortSseConnections, 0);
1940
1991
  __privateSet(this, _sseFallbackToLongPolling, false);
1992
+ __privateSet(this, _staleCacheBuster, void 0);
1993
+ __privateSet(this, _staleCacheRetryCount, 0);
1941
1994
  };
1942
1995
  ShapeStream.Replica = {
1943
1996
  FULL: `full`,
@@ -2080,7 +2133,7 @@ var Shape = class {
2080
2133
  await this.stream.requestSnapshot(params);
2081
2134
  }
2082
2135
  subscribe(callback) {
2083
- const subscriptionId = Math.random();
2136
+ const subscriptionId = {};
2084
2137
  __privateGet(this, _subscribers2).set(subscriptionId, callback);
2085
2138
  return () => {
2086
2139
  __privateGet(this, _subscribers2).delete(subscriptionId);