@electric-sql/client 1.1.4 → 1.2.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.
@@ -90,10 +90,14 @@ __export(src_exports, {
90
90
  FetchError: () => FetchError,
91
91
  Shape: () => Shape,
92
92
  ShapeStream: () => ShapeStream,
93
+ camelToSnake: () => camelToSnake,
94
+ createColumnMapper: () => createColumnMapper,
93
95
  isChangeMessage: () => isChangeMessage,
94
96
  isControlMessage: () => isControlMessage,
95
97
  isVisibleInSnapshot: () => isVisibleInSnapshot,
96
- resolveValue: () => resolveValue
98
+ resolveValue: () => resolveValue,
99
+ snakeCamelMapper: () => snakeCamelMapper,
100
+ snakeToCamel: () => snakeToCamel
97
101
  });
98
102
  module.exports = __toCommonJS(src_exports);
99
103
 
@@ -256,15 +260,37 @@ var MessageParser = class {
256
260
  parse(messages, schema) {
257
261
  return JSON.parse(messages, (key, value) => {
258
262
  if ((key === `value` || key === `old_value`) && typeof value === `object` && value !== null) {
259
- const row = value;
260
- Object.keys(row).forEach((key2) => {
261
- row[key2] = this.parseRow(key2, row[key2], schema);
262
- });
263
- if (this.transformer) value = this.transformer(value);
263
+ return this.transformMessageValue(value, schema);
264
264
  }
265
265
  return value;
266
266
  });
267
267
  }
268
+ /**
269
+ * Parse an array of ChangeMessages from a snapshot response.
270
+ * Applies type parsing and transformations to the value and old_value properties.
271
+ */
272
+ parseSnapshotData(messages, schema) {
273
+ return messages.map((message) => {
274
+ const msg = message;
275
+ if (msg.value && typeof msg.value === `object` && msg.value !== null) {
276
+ msg.value = this.transformMessageValue(msg.value, schema);
277
+ }
278
+ if (msg.old_value && typeof msg.old_value === `object` && msg.old_value !== null) {
279
+ msg.old_value = this.transformMessageValue(msg.old_value, schema);
280
+ }
281
+ return msg;
282
+ });
283
+ }
284
+ /**
285
+ * Transform a message value or old_value object by parsing its columns.
286
+ */
287
+ transformMessageValue(value, schema) {
288
+ const row = value;
289
+ Object.keys(row).forEach((key) => {
290
+ row[key] = this.parseRow(key, row[key], schema);
291
+ });
292
+ return this.transformer ? this.transformer(row) : row;
293
+ }
268
294
  // Parses the message values using the provided parser based on the schema information
269
295
  parseRow(key, value, schema) {
270
296
  var _b;
@@ -300,6 +326,151 @@ function makeNullableParser(parser, columnInfo, columnName) {
300
326
  };
301
327
  }
302
328
 
329
+ // src/column-mapper.ts
330
+ function snakeToCamel(str) {
331
+ var _a, _b, _c, _d;
332
+ const leadingUnderscores = (_b = (_a = str.match(/^_+/)) == null ? void 0 : _a[0]) != null ? _b : ``;
333
+ const withoutLeading = str.slice(leadingUnderscores.length);
334
+ const trailingUnderscores = (_d = (_c = withoutLeading.match(/_+$/)) == null ? void 0 : _c[0]) != null ? _d : ``;
335
+ const core = trailingUnderscores ? withoutLeading.slice(
336
+ 0,
337
+ withoutLeading.length - trailingUnderscores.length
338
+ ) : withoutLeading;
339
+ const normalized = core.toLowerCase();
340
+ const camelCased = normalized.replace(
341
+ /_+([a-z])/g,
342
+ (_, letter) => letter.toUpperCase()
343
+ );
344
+ return leadingUnderscores + camelCased + trailingUnderscores;
345
+ }
346
+ function camelToSnake(str) {
347
+ return str.replace(/([a-z])([A-Z])/g, `$1_$2`).replace(/([A-Z]+)([A-Z][a-z])/g, `$1_$2`).toLowerCase();
348
+ }
349
+ function createColumnMapper(mapping) {
350
+ const reverseMapping = {};
351
+ for (const [dbName, appName] of Object.entries(mapping)) {
352
+ reverseMapping[appName] = dbName;
353
+ }
354
+ return {
355
+ decode: (dbColumnName) => {
356
+ var _a;
357
+ return (_a = mapping[dbColumnName]) != null ? _a : dbColumnName;
358
+ },
359
+ encode: (appColumnName) => {
360
+ var _a;
361
+ return (_a = reverseMapping[appColumnName]) != null ? _a : appColumnName;
362
+ }
363
+ };
364
+ }
365
+ function encodeWhereClause(whereClause, encode) {
366
+ if (!whereClause || !encode) return whereClause != null ? whereClause : ``;
367
+ const sqlKeywords = /* @__PURE__ */ new Set([
368
+ `SELECT`,
369
+ `FROM`,
370
+ `WHERE`,
371
+ `AND`,
372
+ `OR`,
373
+ `NOT`,
374
+ `IN`,
375
+ `IS`,
376
+ `NULL`,
377
+ `NULLS`,
378
+ `FIRST`,
379
+ `LAST`,
380
+ `TRUE`,
381
+ `FALSE`,
382
+ `LIKE`,
383
+ `ILIKE`,
384
+ `BETWEEN`,
385
+ `ASC`,
386
+ `DESC`,
387
+ `LIMIT`,
388
+ `OFFSET`,
389
+ `ORDER`,
390
+ `BY`,
391
+ `GROUP`,
392
+ `HAVING`,
393
+ `DISTINCT`,
394
+ `AS`,
395
+ `ON`,
396
+ `JOIN`,
397
+ `LEFT`,
398
+ `RIGHT`,
399
+ `INNER`,
400
+ `OUTER`,
401
+ `CROSS`,
402
+ `CASE`,
403
+ `WHEN`,
404
+ `THEN`,
405
+ `ELSE`,
406
+ `END`,
407
+ `CAST`,
408
+ `LOWER`,
409
+ `UPPER`,
410
+ `COALESCE`,
411
+ `NULLIF`
412
+ ]);
413
+ const quotedRanges = [];
414
+ let pos = 0;
415
+ while (pos < whereClause.length) {
416
+ const ch = whereClause[pos];
417
+ if (ch === `'` || ch === `"`) {
418
+ const start = pos;
419
+ const quoteChar = ch;
420
+ pos++;
421
+ while (pos < whereClause.length) {
422
+ if (whereClause[pos] === quoteChar) {
423
+ if (whereClause[pos + 1] === quoteChar) {
424
+ pos += 2;
425
+ } else {
426
+ pos++;
427
+ break;
428
+ }
429
+ } else {
430
+ pos++;
431
+ }
432
+ }
433
+ quotedRanges.push({ start, end: pos });
434
+ } else {
435
+ pos++;
436
+ }
437
+ }
438
+ const isInQuotedString = (pos2) => {
439
+ return quotedRanges.some((range) => pos2 >= range.start && pos2 < range.end);
440
+ };
441
+ const identifierPattern = new RegExp("(?<![a-zA-Z0-9_])([a-zA-Z_][a-zA-Z0-9_]*)(?![a-zA-Z0-9_])", "g");
442
+ return whereClause.replace(identifierPattern, (match, _p1, offset) => {
443
+ if (isInQuotedString(offset)) {
444
+ return match;
445
+ }
446
+ if (sqlKeywords.has(match.toUpperCase())) {
447
+ return match;
448
+ }
449
+ if (match.startsWith(`$`)) {
450
+ return match;
451
+ }
452
+ const encoded = encode(match);
453
+ return encoded;
454
+ });
455
+ }
456
+ function snakeCamelMapper(schema) {
457
+ if (schema) {
458
+ const mapping = {};
459
+ for (const dbColumn of Object.keys(schema)) {
460
+ mapping[dbColumn] = snakeToCamel(dbColumn);
461
+ }
462
+ return createColumnMapper(mapping);
463
+ }
464
+ return {
465
+ decode: (dbColumnName) => {
466
+ return snakeToCamel(dbColumnName);
467
+ },
468
+ encode: (appColumnName) => {
469
+ return camelToSnake(appColumnName);
470
+ }
471
+ };
472
+ }
473
+
303
474
  // src/helpers.ts
304
475
  function isChangeMessage(message) {
305
476
  return `key` in message;
@@ -693,6 +864,127 @@ var ExpiredShapesCache = class {
693
864
  };
694
865
  var expiredShapesCache = new ExpiredShapesCache();
695
866
 
867
+ // src/up-to-date-tracker.ts
868
+ var UpToDateTracker = class {
869
+ constructor() {
870
+ this.data = {};
871
+ this.storageKey = `electric_up_to_date_tracker`;
872
+ this.cacheTTL = 6e4;
873
+ // 60s to match typical CDN s-maxage cache duration
874
+ this.maxEntries = 250;
875
+ this.writeThrottleMs = 6e4;
876
+ // Throttle localStorage writes to once per 60s
877
+ this.lastWriteTime = 0;
878
+ this.load();
879
+ this.cleanup();
880
+ }
881
+ /**
882
+ * Records that a shape received an up-to-date message with a specific cursor.
883
+ * This timestamp and cursor are used to detect cache replay scenarios.
884
+ * Updates in-memory immediately, but throttles localStorage writes.
885
+ */
886
+ recordUpToDate(shapeKey, cursor) {
887
+ this.data[shapeKey] = {
888
+ timestamp: Date.now(),
889
+ cursor
890
+ };
891
+ const keys = Object.keys(this.data);
892
+ if (keys.length > this.maxEntries) {
893
+ const oldest = keys.reduce(
894
+ (min, k) => this.data[k].timestamp < this.data[min].timestamp ? k : min
895
+ );
896
+ delete this.data[oldest];
897
+ }
898
+ this.scheduleSave();
899
+ }
900
+ /**
901
+ * Schedules a throttled save to localStorage.
902
+ * Writes immediately if enough time has passed, otherwise schedules for later.
903
+ */
904
+ scheduleSave() {
905
+ const now = Date.now();
906
+ const timeSinceLastWrite = now - this.lastWriteTime;
907
+ if (timeSinceLastWrite >= this.writeThrottleMs) {
908
+ this.lastWriteTime = now;
909
+ this.save();
910
+ } else if (!this.pendingSaveTimer) {
911
+ const delay = this.writeThrottleMs - timeSinceLastWrite;
912
+ this.pendingSaveTimer = setTimeout(() => {
913
+ this.lastWriteTime = Date.now();
914
+ this.pendingSaveTimer = void 0;
915
+ this.save();
916
+ }, delay);
917
+ }
918
+ }
919
+ /**
920
+ * Checks if we should enter replay mode for this shape.
921
+ * Returns the last seen cursor if there's a recent up-to-date (< 60s),
922
+ * which means we'll likely be replaying cached responses.
923
+ * Returns null if no recent up-to-date exists.
924
+ */
925
+ shouldEnterReplayMode(shapeKey) {
926
+ const entry = this.data[shapeKey];
927
+ if (!entry) {
928
+ return null;
929
+ }
930
+ const age = Date.now() - entry.timestamp;
931
+ if (age >= this.cacheTTL) {
932
+ return null;
933
+ }
934
+ return entry.cursor;
935
+ }
936
+ /**
937
+ * Cleans up expired entries from the cache.
938
+ * Called on initialization and can be called periodically.
939
+ */
940
+ cleanup() {
941
+ const now = Date.now();
942
+ const keys = Object.keys(this.data);
943
+ let modified = false;
944
+ for (const key of keys) {
945
+ const age = now - this.data[key].timestamp;
946
+ if (age > this.cacheTTL) {
947
+ delete this.data[key];
948
+ modified = true;
949
+ }
950
+ }
951
+ if (modified) {
952
+ this.save();
953
+ }
954
+ }
955
+ save() {
956
+ if (typeof localStorage === `undefined`) return;
957
+ try {
958
+ localStorage.setItem(this.storageKey, JSON.stringify(this.data));
959
+ } catch (e) {
960
+ }
961
+ }
962
+ load() {
963
+ if (typeof localStorage === `undefined`) return;
964
+ try {
965
+ const stored = localStorage.getItem(this.storageKey);
966
+ if (stored) {
967
+ this.data = JSON.parse(stored);
968
+ }
969
+ } catch (e) {
970
+ this.data = {};
971
+ }
972
+ }
973
+ /**
974
+ * Clears all tracked up-to-date timestamps.
975
+ * Useful for testing or manual cache invalidation.
976
+ */
977
+ clear() {
978
+ this.data = {};
979
+ if (this.pendingSaveTimer) {
980
+ clearTimeout(this.pendingSaveTimer);
981
+ this.pendingSaveTimer = void 0;
982
+ }
983
+ this.save();
984
+ }
985
+ };
986
+ var upToDateTracker = new UpToDateTracker();
987
+
696
988
  // src/snapshot-tracker.ts
697
989
  var SnapshotTracker = class {
698
990
  constructor() {
@@ -810,7 +1102,7 @@ function canonicalShapeKey(url) {
810
1102
  cleanUrl.searchParams.sort();
811
1103
  return cleanUrl.toString();
812
1104
  }
813
- 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, _lastSseConnectionStartTime, _minSseConnectionDuration, _consecutiveShortSseConnections, _maxShortSseConnections, _sseFallbackToLongPolling, _sseBackoffBaseDelay, _sseBackoffMaxDelay, _unsubscribeFromVisibilityChanges, _ShapeStream_instances, 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, fetchSnapshot_fn;
1105
+ 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;
814
1106
  var ShapeStream = class {
815
1107
  constructor(options) {
816
1108
  __privateAdd(this, _ShapeStream_instances);
@@ -845,6 +1137,10 @@ var ShapeStream = class {
845
1137
  // counter for concurrent snapshot requests
846
1138
  __privateAdd(this, _midStreamPromise);
847
1139
  __privateAdd(this, _midStreamPromiseResolver);
1140
+ __privateAdd(this, _lastSeenCursor);
1141
+ // Last seen cursor from previous session (used to detect cached responses)
1142
+ __privateAdd(this, _currentFetchUrl);
1143
+ // Current fetch URL for computing shape key
848
1144
  __privateAdd(this, _lastSseConnectionStartTime);
849
1145
  __privateAdd(this, _minSseConnectionDuration, 1e3);
850
1146
  // Minimum expected SSE connection duration (1 second)
@@ -863,10 +1159,21 @@ var ShapeStream = class {
863
1159
  __privateSet(this, _lastOffset, (_a = this.options.offset) != null ? _a : `-1`);
864
1160
  __privateSet(this, _liveCacheBuster, ``);
865
1161
  __privateSet(this, _shapeHandle, this.options.handle);
866
- __privateSet(this, _messageParser, new MessageParser(
867
- options.parser,
868
- options.transformer
869
- ));
1162
+ let transformer;
1163
+ if (options.columnMapper) {
1164
+ const applyColumnMapper = (row) => {
1165
+ const result = {};
1166
+ for (const [dbKey, value] of Object.entries(row)) {
1167
+ const appKey = options.columnMapper.decode(dbKey);
1168
+ result[appKey] = value;
1169
+ }
1170
+ return result;
1171
+ };
1172
+ transformer = options.transformer ? (row) => options.transformer(applyColumnMapper(row)) : applyColumnMapper;
1173
+ } else {
1174
+ transformer = options.transformer;
1175
+ }
1176
+ __privateSet(this, _messageParser, new MessageParser(options.parser, transformer));
870
1177
  __privateSet(this, _onError, this.options.onError);
871
1178
  __privateSet(this, _mode, (_b = this.options.log) != null ? _b : `full`);
872
1179
  const baseFetchClient = (_c = options.fetchClient) != null ? _c : (...args) => fetch(...args);
@@ -957,7 +1264,7 @@ var ShapeStream = class {
957
1264
  });
958
1265
  }
959
1266
  /**
960
- * Request a snapshot for subset of data.
1267
+ * Request a snapshot for subset of data and inject it into the subscribed data stream.
961
1268
  *
962
1269
  * Only available when mode is `changes_only`.
963
1270
  * Returns the insertion point & the data, but more importantly injects the data
@@ -984,8 +1291,7 @@ var ShapeStream = class {
984
1291
  if (__privateGet(this, _activeSnapshotRequests) === 1) {
985
1292
  __privateMethod(this, _ShapeStream_instances, pause_fn).call(this);
986
1293
  }
987
- const { fetchUrl, requestHeaders } = yield __privateMethod(this, _ShapeStream_instances, constructUrl_fn).call(this, this.options.url, true, opts);
988
- const { metadata, data } = yield __privateMethod(this, _ShapeStream_instances, fetchSnapshot_fn).call(this, fetchUrl, requestHeaders);
1294
+ const { metadata, data } = yield this.fetchSnapshot(opts);
989
1295
  const dataWithEndBoundary = data.concat([
990
1296
  { headers: __spreadValues({ control: `snapshot-end` }, metadata) }
991
1297
  ]);
@@ -1006,6 +1312,44 @@ var ShapeStream = class {
1006
1312
  }
1007
1313
  });
1008
1314
  }
1315
+ /**
1316
+ * Fetch a snapshot for subset of data.
1317
+ * Returns the metadata and the data, but does not inject it into the subscribed data stream.
1318
+ *
1319
+ * @param opts - The options for the snapshot request.
1320
+ * @returns The metadata and the data for the snapshot.
1321
+ */
1322
+ fetchSnapshot(opts) {
1323
+ return __async(this, null, function* () {
1324
+ var _a;
1325
+ const { fetchUrl, requestHeaders } = yield __privateMethod(this, _ShapeStream_instances, constructUrl_fn).call(this, this.options.url, true, opts);
1326
+ const response = yield __privateGet(this, _fetchClient2).call(this, fetchUrl.toString(), {
1327
+ headers: requestHeaders
1328
+ });
1329
+ if (!response.ok) {
1330
+ throw new FetchError(
1331
+ response.status,
1332
+ void 0,
1333
+ void 0,
1334
+ Object.fromEntries([...response.headers.entries()]),
1335
+ fetchUrl.toString()
1336
+ );
1337
+ }
1338
+ const schema = (_a = __privateGet(this, _schema)) != null ? _a : getSchemaFromHeaders(response.headers, {
1339
+ required: true,
1340
+ url: fetchUrl.toString()
1341
+ });
1342
+ const { metadata, data: rawData } = yield response.json();
1343
+ const data = __privateGet(this, _messageParser).parseSnapshotData(
1344
+ rawData,
1345
+ schema
1346
+ );
1347
+ return {
1348
+ metadata,
1349
+ data
1350
+ };
1351
+ });
1352
+ }
1009
1353
  };
1010
1354
  _error = new WeakMap();
1011
1355
  _fetchClient2 = new WeakMap();
@@ -1034,6 +1378,8 @@ _snapshotTracker = new WeakMap();
1034
1378
  _activeSnapshotRequests = new WeakMap();
1035
1379
  _midStreamPromise = new WeakMap();
1036
1380
  _midStreamPromiseResolver = new WeakMap();
1381
+ _lastSeenCursor = new WeakMap();
1382
+ _currentFetchUrl = new WeakMap();
1037
1383
  _lastSseConnectionStartTime = new WeakMap();
1038
1384
  _minSseConnectionDuration = new WeakMap();
1039
1385
  _consecutiveShortSseConnections = new WeakMap();
@@ -1043,6 +1389,9 @@ _sseBackoffBaseDelay = new WeakMap();
1043
1389
  _sseBackoffMaxDelay = new WeakMap();
1044
1390
  _unsubscribeFromVisibilityChanges = new WeakMap();
1045
1391
  _ShapeStream_instances = new WeakSet();
1392
+ replayMode_get = function() {
1393
+ return __privateGet(this, _lastSeenCursor) !== void 0;
1394
+ };
1046
1395
  start_fn = function() {
1047
1396
  return __async(this, null, function* () {
1048
1397
  var _a, _b, _c, _d, _e;
@@ -1142,6 +1491,7 @@ requestShape_fn = function() {
1142
1491
  };
1143
1492
  constructUrl_fn = function(url, resumingFromPause, subsetParams) {
1144
1493
  return __async(this, null, function* () {
1494
+ var _a, _b, _c;
1145
1495
  const [requestHeaders, params] = yield Promise.all([
1146
1496
  resolveHeaders(this.options.headers),
1147
1497
  this.options.params ? toInternalParams(convertWhereParamsToObj(this.options.params)) : void 0
@@ -1150,7 +1500,13 @@ constructUrl_fn = function(url, resumingFromPause, subsetParams) {
1150
1500
  const fetchUrl = new URL(url);
1151
1501
  if (params) {
1152
1502
  if (params.table) setQueryParam(fetchUrl, TABLE_QUERY_PARAM, params.table);
1153
- if (params.where) setQueryParam(fetchUrl, WHERE_QUERY_PARAM, params.where);
1503
+ if (params.where && typeof params.where === `string`) {
1504
+ const encodedWhere = encodeWhereClause(
1505
+ params.where,
1506
+ (_a = this.options.columnMapper) == null ? void 0 : _a.encode
1507
+ );
1508
+ setQueryParam(fetchUrl, WHERE_QUERY_PARAM, encodedWhere);
1509
+ }
1154
1510
  if (params.columns)
1155
1511
  setQueryParam(fetchUrl, COLUMNS_QUERY_PARAM, params.columns);
1156
1512
  if (params.replica) setQueryParam(fetchUrl, REPLICA_PARAM, params.replica);
@@ -1167,20 +1523,34 @@ constructUrl_fn = function(url, resumingFromPause, subsetParams) {
1167
1523
  }
1168
1524
  }
1169
1525
  if (subsetParams) {
1170
- if (subsetParams.where)
1171
- setQueryParam(fetchUrl, SUBSET_PARAM_WHERE, subsetParams.where);
1526
+ if (subsetParams.where && typeof subsetParams.where === `string`) {
1527
+ const encodedWhere = encodeWhereClause(
1528
+ subsetParams.where,
1529
+ (_b = this.options.columnMapper) == null ? void 0 : _b.encode
1530
+ );
1531
+ setQueryParam(fetchUrl, SUBSET_PARAM_WHERE, encodedWhere);
1532
+ }
1172
1533
  if (subsetParams.params)
1173
- setQueryParam(fetchUrl, SUBSET_PARAM_WHERE_PARAMS, subsetParams.params);
1534
+ fetchUrl.searchParams.set(
1535
+ SUBSET_PARAM_WHERE_PARAMS,
1536
+ JSON.stringify(subsetParams.params)
1537
+ );
1174
1538
  if (subsetParams.limit)
1175
1539
  setQueryParam(fetchUrl, SUBSET_PARAM_LIMIT, subsetParams.limit);
1176
1540
  if (subsetParams.offset)
1177
1541
  setQueryParam(fetchUrl, SUBSET_PARAM_OFFSET, subsetParams.offset);
1178
- if (subsetParams.orderBy)
1179
- setQueryParam(fetchUrl, SUBSET_PARAM_ORDER_BY, subsetParams.orderBy);
1542
+ if (subsetParams.orderBy && typeof subsetParams.orderBy === `string`) {
1543
+ const encodedOrderBy = encodeWhereClause(
1544
+ subsetParams.orderBy,
1545
+ (_c = this.options.columnMapper) == null ? void 0 : _c.encode
1546
+ );
1547
+ setQueryParam(fetchUrl, SUBSET_PARAM_ORDER_BY, encodedOrderBy);
1548
+ }
1180
1549
  }
1181
1550
  fetchUrl.searchParams.set(OFFSET_QUERY_PARAM, __privateGet(this, _lastOffset));
1182
1551
  fetchUrl.searchParams.set(LOG_MODE_QUERY_PARAM, __privateGet(this, _mode));
1183
- if (__privateGet(this, _isUpToDate)) {
1552
+ const isSnapshotRequest = subsetParams !== void 0;
1553
+ if (__privateGet(this, _isUpToDate) && !isSnapshotRequest) {
1184
1554
  if (!__privateGet(this, _isRefreshing) && !resumingFromPause) {
1185
1555
  fetchUrl.searchParams.set(LIVE_QUERY_PARAM, `true`);
1186
1556
  }
@@ -1237,11 +1607,7 @@ onInitialResponse_fn = function(response) {
1237
1607
  if (liveCacheBuster) {
1238
1608
  __privateSet(this, _liveCacheBuster, liveCacheBuster);
1239
1609
  }
1240
- const getSchema = () => {
1241
- const schemaHeader = headers.get(SHAPE_SCHEMA_HEADER);
1242
- return schemaHeader ? JSON.parse(schemaHeader) : {};
1243
- };
1244
- __privateSet(this, _schema, (_a = __privateGet(this, _schema)) != null ? _a : getSchema());
1610
+ __privateSet(this, _schema, (_a = __privateGet(this, _schema)) != null ? _a : getSchemaFromHeaders(headers));
1245
1611
  if (status === 204) {
1246
1612
  __privateSet(this, _lastSyncedAt, Date.now());
1247
1613
  }
@@ -1264,6 +1630,17 @@ onMessages_fn = function(batch, isSseMessage = false) {
1264
1630
  __privateSet(this, _isUpToDate, true);
1265
1631
  __privateSet(this, _isMidStream, false);
1266
1632
  (_a = __privateGet(this, _midStreamPromiseResolver)) == null ? void 0 : _a.call(this);
1633
+ if (__privateGet(this, _ShapeStream_instances, replayMode_get) && !isSseMessage) {
1634
+ const currentCursor = __privateGet(this, _liveCacheBuster);
1635
+ if (currentCursor === __privateGet(this, _lastSeenCursor)) {
1636
+ return;
1637
+ }
1638
+ }
1639
+ __privateSet(this, _lastSeenCursor, void 0);
1640
+ if (__privateGet(this, _currentFetchUrl)) {
1641
+ const shapeKey = canonicalShapeKey(__privateGet(this, _currentFetchUrl));
1642
+ upToDateTracker.recordUpToDate(shapeKey, __privateGet(this, _liveCacheBuster));
1643
+ }
1267
1644
  }
1268
1645
  const messagesToProcess = batch.filter((message) => {
1269
1646
  if (isChangeMessage(message)) {
@@ -1278,6 +1655,14 @@ onMessages_fn = function(batch, isSseMessage = false) {
1278
1655
  fetchShape_fn = function(opts) {
1279
1656
  return __async(this, null, function* () {
1280
1657
  var _a;
1658
+ __privateSet(this, _currentFetchUrl, opts.fetchUrl);
1659
+ if (!__privateGet(this, _isUpToDate) && !__privateGet(this, _ShapeStream_instances, replayMode_get)) {
1660
+ const shapeKey = canonicalShapeKey(opts.fetchUrl);
1661
+ const lastSeenCursor = upToDateTracker.shouldEnterReplayMode(shapeKey);
1662
+ if (lastSeenCursor) {
1663
+ __privateSet(this, _lastSeenCursor, lastSeenCursor);
1664
+ }
1665
+ }
1281
1666
  const useSse = (_a = this.options.liveSse) != null ? _a : this.options.experimentalLiveSse;
1282
1667
  if (__privateGet(this, _isUpToDate) && useSse && !__privateGet(this, _isRefreshing) && !opts.resumingFromPause && !__privateGet(this, _sseFallbackToLongPolling)) {
1283
1668
  opts.fetchUrl.searchParams.set(EXPERIMENTAL_LIVE_SSE_QUERY_PARAM, `true`);
@@ -1469,33 +1854,20 @@ reset_fn = function(handle) {
1469
1854
  __privateSet(this, _consecutiveShortSseConnections, 0);
1470
1855
  __privateSet(this, _sseFallbackToLongPolling, false);
1471
1856
  };
1472
- fetchSnapshot_fn = function(url, headers) {
1473
- return __async(this, null, function* () {
1474
- const response = yield __privateGet(this, _fetchClient2).call(this, url.toString(), { headers });
1475
- if (!response.ok) {
1476
- throw new FetchError(
1477
- response.status,
1478
- void 0,
1479
- void 0,
1480
- Object.fromEntries([...response.headers.entries()]),
1481
- url.toString()
1482
- );
1483
- }
1484
- const { metadata, data } = yield response.json();
1485
- const batch = __privateGet(this, _messageParser).parse(
1486
- JSON.stringify(data),
1487
- __privateGet(this, _schema)
1488
- );
1489
- return {
1490
- metadata,
1491
- data: batch
1492
- };
1493
- });
1494
- };
1495
1857
  ShapeStream.Replica = {
1496
1858
  FULL: `full`,
1497
1859
  DEFAULT: `default`
1498
1860
  };
1861
+ function getSchemaFromHeaders(headers, options) {
1862
+ const schemaHeader = headers.get(SHAPE_SCHEMA_HEADER);
1863
+ if (!schemaHeader) {
1864
+ if ((options == null ? void 0 : options.required) && (options == null ? void 0 : options.url)) {
1865
+ throw new MissingHeadersError(options.url, [SHAPE_SCHEMA_HEADER]);
1866
+ }
1867
+ return {};
1868
+ }
1869
+ return JSON.parse(schemaHeader);
1870
+ }
1499
1871
  function validateParams(params) {
1500
1872
  if (!params) return;
1501
1873
  const reservedParams = Object.keys(params).filter(
@@ -1761,9 +2133,13 @@ notify_fn = function() {
1761
2133
  FetchError,
1762
2134
  Shape,
1763
2135
  ShapeStream,
2136
+ camelToSnake,
2137
+ createColumnMapper,
1764
2138
  isChangeMessage,
1765
2139
  isControlMessage,
1766
2140
  isVisibleInSnapshot,
1767
- resolveValue
2141
+ resolveValue,
2142
+ snakeCamelMapper,
2143
+ snakeToCamel
1768
2144
  });
1769
2145
  //# sourceMappingURL=index.cjs.map