@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.
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);
@@ -467,6 +473,9 @@ var SUBSET_PARAM_LIMIT = `subset__limit`;
467
473
  var SUBSET_PARAM_OFFSET = `subset__offset`;
468
474
  var SUBSET_PARAM_ORDER_BY = `subset__order_by`;
469
475
  var SUBSET_PARAM_WHERE_PARAMS = `subset__params`;
476
+ var SUBSET_PARAM_WHERE_EXPR = `subset__where_expr`;
477
+ var SUBSET_PARAM_ORDER_BY_EXPR = `subset__order_by_expr`;
478
+ var CACHE_BUSTER_QUERY_PARAM = `cache-buster`;
470
479
  var ELECTRIC_PROTOCOL_QUERY_PARAMS = [
471
480
  LIVE_QUERY_PARAM,
472
481
  LIVE_SSE_QUERY_PARAM,
@@ -479,7 +488,10 @@ var ELECTRIC_PROTOCOL_QUERY_PARAMS = [
479
488
  SUBSET_PARAM_LIMIT,
480
489
  SUBSET_PARAM_OFFSET,
481
490
  SUBSET_PARAM_ORDER_BY,
482
- SUBSET_PARAM_WHERE_PARAMS
491
+ SUBSET_PARAM_WHERE_PARAMS,
492
+ SUBSET_PARAM_WHERE_EXPR,
493
+ SUBSET_PARAM_ORDER_BY_EXPR,
494
+ CACHE_BUSTER_QUERY_PARAM
483
495
  ];
484
496
 
485
497
  // src/fetch.ts
@@ -766,6 +778,81 @@ function chainAborter(aborter, sourceSignal) {
766
778
  function noop() {
767
779
  }
768
780
 
781
+ // src/expression-compiler.ts
782
+ function compileExpression(expr, columnMapper) {
783
+ switch (expr.type) {
784
+ case `ref`: {
785
+ const mappedColumn = columnMapper ? columnMapper(expr.column) : expr.column;
786
+ return quoteIdentifier(mappedColumn);
787
+ }
788
+ case `val`:
789
+ return `$${expr.paramIndex}`;
790
+ case `func`:
791
+ return compileFunction(expr, columnMapper);
792
+ default: {
793
+ const _exhaustive = expr;
794
+ throw new Error(`Unknown expression type: ${JSON.stringify(_exhaustive)}`);
795
+ }
796
+ }
797
+ }
798
+ function compileFunction(expr, columnMapper) {
799
+ const args = expr.args.map((arg) => compileExpression(arg, columnMapper));
800
+ switch (expr.name) {
801
+ // Binary comparison operators
802
+ case `eq`:
803
+ return `${args[0]} = ${args[1]}`;
804
+ case `gt`:
805
+ return `${args[0]} > ${args[1]}`;
806
+ case `gte`:
807
+ return `${args[0]} >= ${args[1]}`;
808
+ case `lt`:
809
+ return `${args[0]} < ${args[1]}`;
810
+ case `lte`:
811
+ return `${args[0]} <= ${args[1]}`;
812
+ // Logical operators
813
+ case `and`:
814
+ return args.map((a) => `(${a})`).join(` AND `);
815
+ case `or`:
816
+ return args.map((a) => `(${a})`).join(` OR `);
817
+ case `not`:
818
+ return `NOT (${args[0]})`;
819
+ // Special operators
820
+ case `in`:
821
+ return `${args[0]} = ANY(${args[1]})`;
822
+ case `like`:
823
+ return `${args[0]} LIKE ${args[1]}`;
824
+ case `ilike`:
825
+ return `${args[0]} ILIKE ${args[1]}`;
826
+ case `isNull`:
827
+ case `isUndefined`:
828
+ return `${args[0]} IS NULL`;
829
+ // String functions
830
+ case `upper`:
831
+ return `UPPER(${args[0]})`;
832
+ case `lower`:
833
+ return `LOWER(${args[0]})`;
834
+ case `length`:
835
+ return `LENGTH(${args[0]})`;
836
+ case `concat`:
837
+ return `CONCAT(${args.join(`, `)})`;
838
+ // Other functions
839
+ case `coalesce`:
840
+ return `COALESCE(${args.join(`, `)})`;
841
+ default:
842
+ throw new Error(`Unknown function: ${expr.name}`);
843
+ }
844
+ }
845
+ function compileOrderBy(clauses, columnMapper) {
846
+ return clauses.map((clause) => {
847
+ const mappedColumn = columnMapper ? columnMapper(clause.column) : clause.column;
848
+ let sql = quoteIdentifier(mappedColumn);
849
+ if (clause.direction === `desc`) sql += ` DESC`;
850
+ if (clause.nulls === `first`) sql += ` NULLS FIRST`;
851
+ if (clause.nulls === `last`) sql += ` NULLS LAST`;
852
+ return sql;
853
+ }).join(`, `);
854
+ }
855
+
769
856
  // src/client.ts
770
857
  import {
771
858
  fetchEventSource
@@ -1012,7 +1099,8 @@ var RESERVED_PARAMS = /* @__PURE__ */ new Set([
1012
1099
  LIVE_CACHE_BUSTER_QUERY_PARAM,
1013
1100
  SHAPE_HANDLE_QUERY_PARAM,
1014
1101
  LIVE_QUERY_PARAM,
1015
- OFFSET_QUERY_PARAM
1102
+ OFFSET_QUERY_PARAM,
1103
+ CACHE_BUSTER_QUERY_PARAM
1016
1104
  ]);
1017
1105
  async function resolveValue(value) {
1018
1106
  if (typeof value === `function`) {
@@ -1054,7 +1142,7 @@ function canonicalShapeKey(url) {
1054
1142
  cleanUrl.searchParams.sort();
1055
1143
  return cleanUrl.toString();
1056
1144
  }
1057
- 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;
1058
1146
  var ShapeStream = class {
1059
1147
  constructor(options) {
1060
1148
  __privateAdd(this, _ShapeStream_instances);
@@ -1105,6 +1193,10 @@ var ShapeStream = class {
1105
1193
  __privateAdd(this, _sseBackoffMaxDelay, 5e3);
1106
1194
  // Maximum delay cap (ms)
1107
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);
1108
1200
  var _a, _b, _c, _d;
1109
1201
  this.options = __spreadValues({ subscribe: true }, options);
1110
1202
  validateOptions(this.options);
@@ -1335,6 +1427,9 @@ _sseFallbackToLongPolling = new WeakMap();
1335
1427
  _sseBackoffBaseDelay = new WeakMap();
1336
1428
  _sseBackoffMaxDelay = new WeakMap();
1337
1429
  _unsubscribeFromVisibilityChanges = new WeakMap();
1430
+ _staleCacheBuster = new WeakMap();
1431
+ _staleCacheRetryCount = new WeakMap();
1432
+ _maxStaleCacheRetries = new WeakMap();
1338
1433
  _ShapeStream_instances = new WeakSet();
1339
1434
  replayMode_get = function() {
1340
1435
  return __privateGet(this, _lastSeenCursor) !== void 0;
@@ -1348,7 +1443,8 @@ start_fn = async function() {
1348
1443
  __privateSet(this, _error, err);
1349
1444
  if (__privateGet(this, _onError)) {
1350
1445
  const retryOpts = await __privateGet(this, _onError).call(this, err);
1351
- if (retryOpts && typeof retryOpts === `object`) {
1446
+ const isRetryable = !(err instanceof MissingHeadersError);
1447
+ if (retryOpts && typeof retryOpts === `object` && isRetryable) {
1352
1448
  if (retryOpts.params) {
1353
1449
  this.options.params = __spreadValues(__spreadValues({}, (_a = this.options.params) != null ? _a : {}), retryOpts.params);
1354
1450
  }
@@ -1410,6 +1506,9 @@ requestShape_fn = async function() {
1410
1506
  }
1411
1507
  return;
1412
1508
  }
1509
+ if (e instanceof StaleCacheError) {
1510
+ return __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
1511
+ }
1413
1512
  if (!(e instanceof FetchError)) throw e;
1414
1513
  if (e.status == 409) {
1415
1514
  if (__privateGet(this, _shapeHandle)) {
@@ -1433,7 +1532,7 @@ requestShape_fn = async function() {
1433
1532
  return __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
1434
1533
  };
1435
1534
  constructUrl_fn = async function(url, resumingFromPause, subsetParams) {
1436
- var _a, _b, _c, _d;
1535
+ var _a, _b, _c, _d, _e, _f;
1437
1536
  const [requestHeaders, params] = await Promise.all([
1438
1537
  resolveHeaders(this.options.headers),
1439
1538
  this.options.params ? toInternalParams(convertWhereParamsToObj(this.options.params)) : void 0
@@ -1478,10 +1577,20 @@ constructUrl_fn = async function(url, resumingFromPause, subsetParams) {
1478
1577
  }
1479
1578
  }
1480
1579
  if (subsetParams) {
1481
- if (subsetParams.where && typeof subsetParams.where === `string`) {
1580
+ if (subsetParams.whereExpr) {
1581
+ const compiledWhere = compileExpression(
1582
+ subsetParams.whereExpr,
1583
+ (_c = this.options.columnMapper) == null ? void 0 : _c.encode
1584
+ );
1585
+ setQueryParam(fetchUrl, SUBSET_PARAM_WHERE, compiledWhere);
1586
+ fetchUrl.searchParams.set(
1587
+ SUBSET_PARAM_WHERE_EXPR,
1588
+ JSON.stringify(subsetParams.whereExpr)
1589
+ );
1590
+ } else if (subsetParams.where && typeof subsetParams.where === `string`) {
1482
1591
  const encodedWhere = encodeWhereClause(
1483
1592
  subsetParams.where,
1484
- (_c = this.options.columnMapper) == null ? void 0 : _c.encode
1593
+ (_d = this.options.columnMapper) == null ? void 0 : _d.encode
1485
1594
  );
1486
1595
  setQueryParam(fetchUrl, SUBSET_PARAM_WHERE, encodedWhere);
1487
1596
  }
@@ -1494,10 +1603,20 @@ constructUrl_fn = async function(url, resumingFromPause, subsetParams) {
1494
1603
  setQueryParam(fetchUrl, SUBSET_PARAM_LIMIT, subsetParams.limit);
1495
1604
  if (subsetParams.offset)
1496
1605
  setQueryParam(fetchUrl, SUBSET_PARAM_OFFSET, subsetParams.offset);
1497
- if (subsetParams.orderBy && typeof subsetParams.orderBy === `string`) {
1606
+ if (subsetParams.orderByExpr) {
1607
+ const compiledOrderBy = compileOrderBy(
1608
+ subsetParams.orderByExpr,
1609
+ (_e = this.options.columnMapper) == null ? void 0 : _e.encode
1610
+ );
1611
+ setQueryParam(fetchUrl, SUBSET_PARAM_ORDER_BY, compiledOrderBy);
1612
+ fetchUrl.searchParams.set(
1613
+ SUBSET_PARAM_ORDER_BY_EXPR,
1614
+ JSON.stringify(subsetParams.orderByExpr)
1615
+ );
1616
+ } else if (subsetParams.orderBy && typeof subsetParams.orderBy === `string`) {
1498
1617
  const encodedOrderBy = encodeWhereClause(
1499
1618
  subsetParams.orderBy,
1500
- (_d = this.options.columnMapper) == null ? void 0 : _d.encode
1619
+ (_f = this.options.columnMapper) == null ? void 0 : _f.encode
1501
1620
  );
1502
1621
  setQueryParam(fetchUrl, SUBSET_PARAM_ORDER_BY, encodedOrderBy);
1503
1622
  }
@@ -1522,6 +1641,12 @@ constructUrl_fn = async function(url, resumingFromPause, subsetParams) {
1522
1641
  if (expiredHandle) {
1523
1642
  fetchUrl.searchParams.set(EXPIRED_HANDLE_QUERY_PARAM, expiredHandle);
1524
1643
  }
1644
+ if (__privateGet(this, _staleCacheBuster)) {
1645
+ fetchUrl.searchParams.set(
1646
+ CACHE_BUSTER_QUERY_PARAM,
1647
+ __privateGet(this, _staleCacheBuster)
1648
+ );
1649
+ }
1525
1650
  fetchUrl.searchParams.sort();
1526
1651
  return {
1527
1652
  fetchUrl,
@@ -1544,7 +1669,7 @@ createAbortListener_fn = async function(signal) {
1544
1669
  }
1545
1670
  };
1546
1671
  onInitialResponse_fn = async function(response) {
1547
- var _a;
1672
+ var _a, _b, _c, _d;
1548
1673
  const { headers, status } = response;
1549
1674
  const shapeHandle = headers.get(SHAPE_HANDLE_HEADER);
1550
1675
  if (shapeHandle) {
@@ -1552,6 +1677,30 @@ onInitialResponse_fn = async function(response) {
1552
1677
  const expiredHandle = shapeKey ? expiredShapesCache.getExpiredHandle(shapeKey) : null;
1553
1678
  if (shapeHandle !== expiredHandle) {
1554
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
+ );
1555
1704
  } else {
1556
1705
  console.warn(
1557
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)}".`
@@ -1566,7 +1715,7 @@ onInitialResponse_fn = async function(response) {
1566
1715
  if (liveCacheBuster) {
1567
1716
  __privateSet(this, _liveCacheBuster, liveCacheBuster);
1568
1717
  }
1569
- __privateSet(this, _schema, (_a = __privateGet(this, _schema)) != null ? _a : getSchemaFromHeaders(headers));
1718
+ __privateSet(this, _schema, (_d = __privateGet(this, _schema)) != null ? _d : getSchemaFromHeaders(headers));
1570
1719
  if (status === 204) {
1571
1720
  __privateSet(this, _lastSyncedAt, Date.now());
1572
1721
  }
@@ -1804,6 +1953,8 @@ reset_fn = function(handle) {
1804
1953
  __privateSet(this, _activeSnapshotRequests, 0);
1805
1954
  __privateSet(this, _consecutiveShortSseConnections, 0);
1806
1955
  __privateSet(this, _sseFallbackToLongPolling, false);
1956
+ __privateSet(this, _staleCacheBuster, void 0);
1957
+ __privateSet(this, _staleCacheRetryCount, 0);
1807
1958
  };
1808
1959
  ShapeStream.Replica = {
1809
1960
  FULL: `full`,
@@ -2078,6 +2229,8 @@ export {
2078
2229
  Shape,
2079
2230
  ShapeStream,
2080
2231
  camelToSnake,
2232
+ compileExpression,
2233
+ compileOrderBy,
2081
2234
  createColumnMapper,
2082
2235
  isChangeMessage,
2083
2236
  isControlMessage,