@electric-sql/client 1.3.1 → 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.
@@ -71,6 +71,8 @@ __export(src_exports, {
71
71
  Shape: () => Shape,
72
72
  ShapeStream: () => ShapeStream,
73
73
  camelToSnake: () => camelToSnake,
74
+ compileExpression: () => compileExpression,
75
+ compileOrderBy: () => compileOrderBy,
74
76
  createColumnMapper: () => createColumnMapper,
75
77
  isChangeMessage: () => isChangeMessage,
76
78
  isControlMessage: () => isControlMessage,
@@ -165,6 +167,12 @@ For more information visit the troubleshooting guide: /docs/guides/troubleshooti
165
167
  super(msg);
166
168
  }
167
169
  };
170
+ var StaleCacheError = class extends Error {
171
+ constructor(message) {
172
+ super(message);
173
+ this.name = `StaleCacheError`;
174
+ }
175
+ };
168
176
 
169
177
  // src/parser.ts
170
178
  var parseNumber = (value) => Number(value);
@@ -502,6 +510,9 @@ var SUBSET_PARAM_LIMIT = `subset__limit`;
502
510
  var SUBSET_PARAM_OFFSET = `subset__offset`;
503
511
  var SUBSET_PARAM_ORDER_BY = `subset__order_by`;
504
512
  var SUBSET_PARAM_WHERE_PARAMS = `subset__params`;
513
+ var SUBSET_PARAM_WHERE_EXPR = `subset__where_expr`;
514
+ var SUBSET_PARAM_ORDER_BY_EXPR = `subset__order_by_expr`;
515
+ var CACHE_BUSTER_QUERY_PARAM = `cache-buster`;
505
516
  var ELECTRIC_PROTOCOL_QUERY_PARAMS = [
506
517
  LIVE_QUERY_PARAM,
507
518
  LIVE_SSE_QUERY_PARAM,
@@ -514,7 +525,10 @@ var ELECTRIC_PROTOCOL_QUERY_PARAMS = [
514
525
  SUBSET_PARAM_LIMIT,
515
526
  SUBSET_PARAM_OFFSET,
516
527
  SUBSET_PARAM_ORDER_BY,
517
- SUBSET_PARAM_WHERE_PARAMS
528
+ SUBSET_PARAM_WHERE_PARAMS,
529
+ SUBSET_PARAM_WHERE_EXPR,
530
+ SUBSET_PARAM_ORDER_BY_EXPR,
531
+ CACHE_BUSTER_QUERY_PARAM
518
532
  ];
519
533
 
520
534
  // src/fetch.ts
@@ -801,6 +815,81 @@ function chainAborter(aborter, sourceSignal) {
801
815
  function noop() {
802
816
  }
803
817
 
818
+ // src/expression-compiler.ts
819
+ function compileExpression(expr, columnMapper) {
820
+ switch (expr.type) {
821
+ case `ref`: {
822
+ const mappedColumn = columnMapper ? columnMapper(expr.column) : expr.column;
823
+ return quoteIdentifier(mappedColumn);
824
+ }
825
+ case `val`:
826
+ return `$${expr.paramIndex}`;
827
+ case `func`:
828
+ return compileFunction(expr, columnMapper);
829
+ default: {
830
+ const _exhaustive = expr;
831
+ throw new Error(`Unknown expression type: ${JSON.stringify(_exhaustive)}`);
832
+ }
833
+ }
834
+ }
835
+ function compileFunction(expr, columnMapper) {
836
+ const args = expr.args.map((arg) => compileExpression(arg, columnMapper));
837
+ switch (expr.name) {
838
+ // Binary comparison operators
839
+ case `eq`:
840
+ return `${args[0]} = ${args[1]}`;
841
+ case `gt`:
842
+ return `${args[0]} > ${args[1]}`;
843
+ case `gte`:
844
+ return `${args[0]} >= ${args[1]}`;
845
+ case `lt`:
846
+ return `${args[0]} < ${args[1]}`;
847
+ case `lte`:
848
+ return `${args[0]} <= ${args[1]}`;
849
+ // Logical operators
850
+ case `and`:
851
+ return args.map((a) => `(${a})`).join(` AND `);
852
+ case `or`:
853
+ return args.map((a) => `(${a})`).join(` OR `);
854
+ case `not`:
855
+ return `NOT (${args[0]})`;
856
+ // Special operators
857
+ case `in`:
858
+ return `${args[0]} = ANY(${args[1]})`;
859
+ case `like`:
860
+ return `${args[0]} LIKE ${args[1]}`;
861
+ case `ilike`:
862
+ return `${args[0]} ILIKE ${args[1]}`;
863
+ case `isNull`:
864
+ case `isUndefined`:
865
+ return `${args[0]} IS NULL`;
866
+ // String functions
867
+ case `upper`:
868
+ return `UPPER(${args[0]})`;
869
+ case `lower`:
870
+ return `LOWER(${args[0]})`;
871
+ case `length`:
872
+ return `LENGTH(${args[0]})`;
873
+ case `concat`:
874
+ return `CONCAT(${args.join(`, `)})`;
875
+ // Other functions
876
+ case `coalesce`:
877
+ return `COALESCE(${args.join(`, `)})`;
878
+ default:
879
+ throw new Error(`Unknown function: ${expr.name}`);
880
+ }
881
+ }
882
+ function compileOrderBy(clauses, columnMapper) {
883
+ return clauses.map((clause) => {
884
+ const mappedColumn = columnMapper ? columnMapper(clause.column) : clause.column;
885
+ let sql = quoteIdentifier(mappedColumn);
886
+ if (clause.direction === `desc`) sql += ` DESC`;
887
+ if (clause.nulls === `first`) sql += ` NULLS FIRST`;
888
+ if (clause.nulls === `last`) sql += ` NULLS LAST`;
889
+ return sql;
890
+ }).join(`, `);
891
+ }
892
+
804
893
  // src/client.ts
805
894
  var import_fetch_event_source = require("@microsoft/fetch-event-source");
806
895
 
@@ -1045,7 +1134,8 @@ var RESERVED_PARAMS = /* @__PURE__ */ new Set([
1045
1134
  LIVE_CACHE_BUSTER_QUERY_PARAM,
1046
1135
  SHAPE_HANDLE_QUERY_PARAM,
1047
1136
  LIVE_QUERY_PARAM,
1048
- OFFSET_QUERY_PARAM
1137
+ OFFSET_QUERY_PARAM,
1138
+ CACHE_BUSTER_QUERY_PARAM
1049
1139
  ]);
1050
1140
  async function resolveValue(value) {
1051
1141
  if (typeof value === `function`) {
@@ -1087,7 +1177,7 @@ function canonicalShapeKey(url) {
1087
1177
  cleanUrl.searchParams.sort();
1088
1178
  return cleanUrl.toString();
1089
1179
  }
1090
- 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;
1091
1181
  var ShapeStream = class {
1092
1182
  constructor(options) {
1093
1183
  __privateAdd(this, _ShapeStream_instances);
@@ -1138,6 +1228,10 @@ var ShapeStream = class {
1138
1228
  __privateAdd(this, _sseBackoffMaxDelay, 5e3);
1139
1229
  // Maximum delay cap (ms)
1140
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);
1141
1235
  var _a, _b, _c, _d;
1142
1236
  this.options = __spreadValues({ subscribe: true }, options);
1143
1237
  validateOptions(this.options);
@@ -1368,6 +1462,9 @@ _sseFallbackToLongPolling = new WeakMap();
1368
1462
  _sseBackoffBaseDelay = new WeakMap();
1369
1463
  _sseBackoffMaxDelay = new WeakMap();
1370
1464
  _unsubscribeFromVisibilityChanges = new WeakMap();
1465
+ _staleCacheBuster = new WeakMap();
1466
+ _staleCacheRetryCount = new WeakMap();
1467
+ _maxStaleCacheRetries = new WeakMap();
1371
1468
  _ShapeStream_instances = new WeakSet();
1372
1469
  replayMode_get = function() {
1373
1470
  return __privateGet(this, _lastSeenCursor) !== void 0;
@@ -1381,7 +1478,8 @@ start_fn = async function() {
1381
1478
  __privateSet(this, _error, err);
1382
1479
  if (__privateGet(this, _onError)) {
1383
1480
  const retryOpts = await __privateGet(this, _onError).call(this, err);
1384
- if (retryOpts && typeof retryOpts === `object`) {
1481
+ const isRetryable = !(err instanceof MissingHeadersError);
1482
+ if (retryOpts && typeof retryOpts === `object` && isRetryable) {
1385
1483
  if (retryOpts.params) {
1386
1484
  this.options.params = __spreadValues(__spreadValues({}, (_a = this.options.params) != null ? _a : {}), retryOpts.params);
1387
1485
  }
@@ -1443,6 +1541,9 @@ requestShape_fn = async function() {
1443
1541
  }
1444
1542
  return;
1445
1543
  }
1544
+ if (e instanceof StaleCacheError) {
1545
+ return __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
1546
+ }
1446
1547
  if (!(e instanceof FetchError)) throw e;
1447
1548
  if (e.status == 409) {
1448
1549
  if (__privateGet(this, _shapeHandle)) {
@@ -1466,7 +1567,7 @@ requestShape_fn = async function() {
1466
1567
  return __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
1467
1568
  };
1468
1569
  constructUrl_fn = async function(url, resumingFromPause, subsetParams) {
1469
- var _a, _b, _c, _d;
1570
+ var _a, _b, _c, _d, _e, _f;
1470
1571
  const [requestHeaders, params] = await Promise.all([
1471
1572
  resolveHeaders(this.options.headers),
1472
1573
  this.options.params ? toInternalParams(convertWhereParamsToObj(this.options.params)) : void 0
@@ -1511,10 +1612,20 @@ constructUrl_fn = async function(url, resumingFromPause, subsetParams) {
1511
1612
  }
1512
1613
  }
1513
1614
  if (subsetParams) {
1514
- if (subsetParams.where && typeof subsetParams.where === `string`) {
1615
+ if (subsetParams.whereExpr) {
1616
+ const compiledWhere = compileExpression(
1617
+ subsetParams.whereExpr,
1618
+ (_c = this.options.columnMapper) == null ? void 0 : _c.encode
1619
+ );
1620
+ setQueryParam(fetchUrl, SUBSET_PARAM_WHERE, compiledWhere);
1621
+ fetchUrl.searchParams.set(
1622
+ SUBSET_PARAM_WHERE_EXPR,
1623
+ JSON.stringify(subsetParams.whereExpr)
1624
+ );
1625
+ } else if (subsetParams.where && typeof subsetParams.where === `string`) {
1515
1626
  const encodedWhere = encodeWhereClause(
1516
1627
  subsetParams.where,
1517
- (_c = this.options.columnMapper) == null ? void 0 : _c.encode
1628
+ (_d = this.options.columnMapper) == null ? void 0 : _d.encode
1518
1629
  );
1519
1630
  setQueryParam(fetchUrl, SUBSET_PARAM_WHERE, encodedWhere);
1520
1631
  }
@@ -1527,10 +1638,20 @@ constructUrl_fn = async function(url, resumingFromPause, subsetParams) {
1527
1638
  setQueryParam(fetchUrl, SUBSET_PARAM_LIMIT, subsetParams.limit);
1528
1639
  if (subsetParams.offset)
1529
1640
  setQueryParam(fetchUrl, SUBSET_PARAM_OFFSET, subsetParams.offset);
1530
- if (subsetParams.orderBy && typeof subsetParams.orderBy === `string`) {
1641
+ if (subsetParams.orderByExpr) {
1642
+ const compiledOrderBy = compileOrderBy(
1643
+ subsetParams.orderByExpr,
1644
+ (_e = this.options.columnMapper) == null ? void 0 : _e.encode
1645
+ );
1646
+ setQueryParam(fetchUrl, SUBSET_PARAM_ORDER_BY, compiledOrderBy);
1647
+ fetchUrl.searchParams.set(
1648
+ SUBSET_PARAM_ORDER_BY_EXPR,
1649
+ JSON.stringify(subsetParams.orderByExpr)
1650
+ );
1651
+ } else if (subsetParams.orderBy && typeof subsetParams.orderBy === `string`) {
1531
1652
  const encodedOrderBy = encodeWhereClause(
1532
1653
  subsetParams.orderBy,
1533
- (_d = this.options.columnMapper) == null ? void 0 : _d.encode
1654
+ (_f = this.options.columnMapper) == null ? void 0 : _f.encode
1534
1655
  );
1535
1656
  setQueryParam(fetchUrl, SUBSET_PARAM_ORDER_BY, encodedOrderBy);
1536
1657
  }
@@ -1555,6 +1676,12 @@ constructUrl_fn = async function(url, resumingFromPause, subsetParams) {
1555
1676
  if (expiredHandle) {
1556
1677
  fetchUrl.searchParams.set(EXPIRED_HANDLE_QUERY_PARAM, expiredHandle);
1557
1678
  }
1679
+ if (__privateGet(this, _staleCacheBuster)) {
1680
+ fetchUrl.searchParams.set(
1681
+ CACHE_BUSTER_QUERY_PARAM,
1682
+ __privateGet(this, _staleCacheBuster)
1683
+ );
1684
+ }
1558
1685
  fetchUrl.searchParams.sort();
1559
1686
  return {
1560
1687
  fetchUrl,
@@ -1577,7 +1704,7 @@ createAbortListener_fn = async function(signal) {
1577
1704
  }
1578
1705
  };
1579
1706
  onInitialResponse_fn = async function(response) {
1580
- var _a;
1707
+ var _a, _b, _c, _d;
1581
1708
  const { headers, status } = response;
1582
1709
  const shapeHandle = headers.get(SHAPE_HANDLE_HEADER);
1583
1710
  if (shapeHandle) {
@@ -1585,6 +1712,30 @@ onInitialResponse_fn = async function(response) {
1585
1712
  const expiredHandle = shapeKey ? expiredShapesCache.getExpiredHandle(shapeKey) : null;
1586
1713
  if (shapeHandle !== expiredHandle) {
1587
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
+ );
1588
1739
  } else {
1589
1740
  console.warn(
1590
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)}".`
@@ -1599,7 +1750,7 @@ onInitialResponse_fn = async function(response) {
1599
1750
  if (liveCacheBuster) {
1600
1751
  __privateSet(this, _liveCacheBuster, liveCacheBuster);
1601
1752
  }
1602
- __privateSet(this, _schema, (_a = __privateGet(this, _schema)) != null ? _a : getSchemaFromHeaders(headers));
1753
+ __privateSet(this, _schema, (_d = __privateGet(this, _schema)) != null ? _d : getSchemaFromHeaders(headers));
1603
1754
  if (status === 204) {
1604
1755
  __privateSet(this, _lastSyncedAt, Date.now());
1605
1756
  }
@@ -1837,6 +1988,8 @@ reset_fn = function(handle) {
1837
1988
  __privateSet(this, _activeSnapshotRequests, 0);
1838
1989
  __privateSet(this, _consecutiveShortSseConnections, 0);
1839
1990
  __privateSet(this, _sseFallbackToLongPolling, false);
1991
+ __privateSet(this, _staleCacheBuster, void 0);
1992
+ __privateSet(this, _staleCacheRetryCount, 0);
1840
1993
  };
1841
1994
  ShapeStream.Replica = {
1842
1995
  FULL: `full`,
@@ -2112,6 +2265,8 @@ notify_fn = function() {
2112
2265
  Shape,
2113
2266
  ShapeStream,
2114
2267
  camelToSnake,
2268
+ compileExpression,
2269
+ compileOrderBy,
2115
2270
  createColumnMapper,
2116
2271
  isChangeMessage,
2117
2272
  isControlMessage,