@electric-sql/client 1.3.0 → 1.4.0

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,
@@ -502,6 +504,8 @@ var SUBSET_PARAM_LIMIT = `subset__limit`;
502
504
  var SUBSET_PARAM_OFFSET = `subset__offset`;
503
505
  var SUBSET_PARAM_ORDER_BY = `subset__order_by`;
504
506
  var SUBSET_PARAM_WHERE_PARAMS = `subset__params`;
507
+ var SUBSET_PARAM_WHERE_EXPR = `subset__where_expr`;
508
+ var SUBSET_PARAM_ORDER_BY_EXPR = `subset__order_by_expr`;
505
509
  var ELECTRIC_PROTOCOL_QUERY_PARAMS = [
506
510
  LIVE_QUERY_PARAM,
507
511
  LIVE_SSE_QUERY_PARAM,
@@ -514,7 +518,9 @@ var ELECTRIC_PROTOCOL_QUERY_PARAMS = [
514
518
  SUBSET_PARAM_LIMIT,
515
519
  SUBSET_PARAM_OFFSET,
516
520
  SUBSET_PARAM_ORDER_BY,
517
- SUBSET_PARAM_WHERE_PARAMS
521
+ SUBSET_PARAM_WHERE_PARAMS,
522
+ SUBSET_PARAM_WHERE_EXPR,
523
+ SUBSET_PARAM_ORDER_BY_EXPR
518
524
  ];
519
525
 
520
526
  // src/fetch.ts
@@ -768,6 +774,13 @@ function getNextChunkUrl(url, res) {
768
774
  if (!shapeHandle || !lastOffset || isUpToDate) return;
769
775
  const nextUrl = new URL(url);
770
776
  if (nextUrl.searchParams.has(LIVE_QUERY_PARAM)) return;
777
+ const expiredHandle = nextUrl.searchParams.get(EXPIRED_HANDLE_QUERY_PARAM);
778
+ if (expiredHandle && shapeHandle === expiredHandle) {
779
+ console.warn(
780
+ `[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. Skipping prefetch to prevent infinite 409 loop.`
781
+ );
782
+ return;
783
+ }
771
784
  nextUrl.searchParams.set(SHAPE_HANDLE_QUERY_PARAM, shapeHandle);
772
785
  nextUrl.searchParams.set(OFFSET_QUERY_PARAM, lastOffset);
773
786
  nextUrl.searchParams.sort();
@@ -794,6 +807,81 @@ function chainAborter(aborter, sourceSignal) {
794
807
  function noop() {
795
808
  }
796
809
 
810
+ // src/expression-compiler.ts
811
+ function compileExpression(expr, columnMapper) {
812
+ switch (expr.type) {
813
+ case `ref`: {
814
+ const mappedColumn = columnMapper ? columnMapper(expr.column) : expr.column;
815
+ return quoteIdentifier(mappedColumn);
816
+ }
817
+ case `val`:
818
+ return `$${expr.paramIndex}`;
819
+ case `func`:
820
+ return compileFunction(expr, columnMapper);
821
+ default: {
822
+ const _exhaustive = expr;
823
+ throw new Error(`Unknown expression type: ${JSON.stringify(_exhaustive)}`);
824
+ }
825
+ }
826
+ }
827
+ function compileFunction(expr, columnMapper) {
828
+ const args = expr.args.map((arg) => compileExpression(arg, columnMapper));
829
+ switch (expr.name) {
830
+ // Binary comparison operators
831
+ case `eq`:
832
+ return `${args[0]} = ${args[1]}`;
833
+ case `gt`:
834
+ return `${args[0]} > ${args[1]}`;
835
+ case `gte`:
836
+ return `${args[0]} >= ${args[1]}`;
837
+ case `lt`:
838
+ return `${args[0]} < ${args[1]}`;
839
+ case `lte`:
840
+ return `${args[0]} <= ${args[1]}`;
841
+ // Logical operators
842
+ case `and`:
843
+ return args.map((a) => `(${a})`).join(` AND `);
844
+ case `or`:
845
+ return args.map((a) => `(${a})`).join(` OR `);
846
+ case `not`:
847
+ return `NOT (${args[0]})`;
848
+ // Special operators
849
+ case `in`:
850
+ return `${args[0]} = ANY(${args[1]})`;
851
+ case `like`:
852
+ return `${args[0]} LIKE ${args[1]}`;
853
+ case `ilike`:
854
+ return `${args[0]} ILIKE ${args[1]}`;
855
+ case `isNull`:
856
+ case `isUndefined`:
857
+ return `${args[0]} IS NULL`;
858
+ // String functions
859
+ case `upper`:
860
+ return `UPPER(${args[0]})`;
861
+ case `lower`:
862
+ return `LOWER(${args[0]})`;
863
+ case `length`:
864
+ return `LENGTH(${args[0]})`;
865
+ case `concat`:
866
+ return `CONCAT(${args.join(`, `)})`;
867
+ // Other functions
868
+ case `coalesce`:
869
+ return `COALESCE(${args.join(`, `)})`;
870
+ default:
871
+ throw new Error(`Unknown function: ${expr.name}`);
872
+ }
873
+ }
874
+ function compileOrderBy(clauses, columnMapper) {
875
+ return clauses.map((clause) => {
876
+ const mappedColumn = columnMapper ? columnMapper(clause.column) : clause.column;
877
+ let sql = quoteIdentifier(mappedColumn);
878
+ if (clause.direction === `desc`) sql += ` DESC`;
879
+ if (clause.nulls === `first`) sql += ` NULLS FIRST`;
880
+ if (clause.nulls === `last`) sql += ` NULLS LAST`;
881
+ return sql;
882
+ }).join(`, `);
883
+ }
884
+
797
885
  // src/client.ts
798
886
  var import_fetch_event_source = require("@microsoft/fetch-event-source");
799
887
 
@@ -1459,7 +1547,7 @@ requestShape_fn = async function() {
1459
1547
  return __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
1460
1548
  };
1461
1549
  constructUrl_fn = async function(url, resumingFromPause, subsetParams) {
1462
- var _a, _b, _c, _d;
1550
+ var _a, _b, _c, _d, _e, _f;
1463
1551
  const [requestHeaders, params] = await Promise.all([
1464
1552
  resolveHeaders(this.options.headers),
1465
1553
  this.options.params ? toInternalParams(convertWhereParamsToObj(this.options.params)) : void 0
@@ -1504,10 +1592,20 @@ constructUrl_fn = async function(url, resumingFromPause, subsetParams) {
1504
1592
  }
1505
1593
  }
1506
1594
  if (subsetParams) {
1507
- if (subsetParams.where && typeof subsetParams.where === `string`) {
1595
+ if (subsetParams.whereExpr) {
1596
+ const compiledWhere = compileExpression(
1597
+ subsetParams.whereExpr,
1598
+ (_c = this.options.columnMapper) == null ? void 0 : _c.encode
1599
+ );
1600
+ setQueryParam(fetchUrl, SUBSET_PARAM_WHERE, compiledWhere);
1601
+ fetchUrl.searchParams.set(
1602
+ SUBSET_PARAM_WHERE_EXPR,
1603
+ JSON.stringify(subsetParams.whereExpr)
1604
+ );
1605
+ } else if (subsetParams.where && typeof subsetParams.where === `string`) {
1508
1606
  const encodedWhere = encodeWhereClause(
1509
1607
  subsetParams.where,
1510
- (_c = this.options.columnMapper) == null ? void 0 : _c.encode
1608
+ (_d = this.options.columnMapper) == null ? void 0 : _d.encode
1511
1609
  );
1512
1610
  setQueryParam(fetchUrl, SUBSET_PARAM_WHERE, encodedWhere);
1513
1611
  }
@@ -1520,10 +1618,20 @@ constructUrl_fn = async function(url, resumingFromPause, subsetParams) {
1520
1618
  setQueryParam(fetchUrl, SUBSET_PARAM_LIMIT, subsetParams.limit);
1521
1619
  if (subsetParams.offset)
1522
1620
  setQueryParam(fetchUrl, SUBSET_PARAM_OFFSET, subsetParams.offset);
1523
- if (subsetParams.orderBy && typeof subsetParams.orderBy === `string`) {
1621
+ if (subsetParams.orderByExpr) {
1622
+ const compiledOrderBy = compileOrderBy(
1623
+ subsetParams.orderByExpr,
1624
+ (_e = this.options.columnMapper) == null ? void 0 : _e.encode
1625
+ );
1626
+ setQueryParam(fetchUrl, SUBSET_PARAM_ORDER_BY, compiledOrderBy);
1627
+ fetchUrl.searchParams.set(
1628
+ SUBSET_PARAM_ORDER_BY_EXPR,
1629
+ JSON.stringify(subsetParams.orderByExpr)
1630
+ );
1631
+ } else if (subsetParams.orderBy && typeof subsetParams.orderBy === `string`) {
1524
1632
  const encodedOrderBy = encodeWhereClause(
1525
1633
  subsetParams.orderBy,
1526
- (_d = this.options.columnMapper) == null ? void 0 : _d.encode
1634
+ (_f = this.options.columnMapper) == null ? void 0 : _f.encode
1527
1635
  );
1528
1636
  setQueryParam(fetchUrl, SUBSET_PARAM_ORDER_BY, encodedOrderBy);
1529
1637
  }
@@ -1574,7 +1682,15 @@ onInitialResponse_fn = async function(response) {
1574
1682
  const { headers, status } = response;
1575
1683
  const shapeHandle = headers.get(SHAPE_HANDLE_HEADER);
1576
1684
  if (shapeHandle) {
1577
- __privateSet(this, _shapeHandle, shapeHandle);
1685
+ const shapeKey = __privateGet(this, _currentFetchUrl) ? canonicalShapeKey(__privateGet(this, _currentFetchUrl)) : null;
1686
+ const expiredHandle = shapeKey ? expiredShapesCache.getExpiredHandle(shapeKey) : null;
1687
+ if (shapeHandle !== expiredHandle) {
1688
+ __privateSet(this, _shapeHandle, shapeHandle);
1689
+ } else {
1690
+ console.warn(
1691
+ `[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)}".`
1692
+ );
1693
+ }
1578
1694
  }
1579
1695
  const lastOffset = headers.get(CHUNK_LAST_OFFSET_HEADER);
1580
1696
  if (lastOffset) {
@@ -2097,6 +2213,8 @@ notify_fn = function() {
2097
2213
  Shape,
2098
2214
  ShapeStream,
2099
2215
  camelToSnake,
2216
+ compileExpression,
2217
+ compileOrderBy,
2100
2218
  createColumnMapper,
2101
2219
  isChangeMessage,
2102
2220
  isControlMessage,