@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.
package/dist/index.mjs CHANGED
@@ -225,15 +225,37 @@ var MessageParser = class {
225
225
  parse(messages, schema) {
226
226
  return JSON.parse(messages, (key, value) => {
227
227
  if ((key === `value` || key === `old_value`) && typeof value === `object` && value !== null) {
228
- const row = value;
229
- Object.keys(row).forEach((key2) => {
230
- row[key2] = this.parseRow(key2, row[key2], schema);
231
- });
232
- if (this.transformer) value = this.transformer(value);
228
+ return this.transformMessageValue(value, schema);
233
229
  }
234
230
  return value;
235
231
  });
236
232
  }
233
+ /**
234
+ * Parse an array of ChangeMessages from a snapshot response.
235
+ * Applies type parsing and transformations to the value and old_value properties.
236
+ */
237
+ parseSnapshotData(messages, schema) {
238
+ return messages.map((message) => {
239
+ const msg = message;
240
+ if (msg.value && typeof msg.value === `object` && msg.value !== null) {
241
+ msg.value = this.transformMessageValue(msg.value, schema);
242
+ }
243
+ if (msg.old_value && typeof msg.old_value === `object` && msg.old_value !== null) {
244
+ msg.old_value = this.transformMessageValue(msg.old_value, schema);
245
+ }
246
+ return msg;
247
+ });
248
+ }
249
+ /**
250
+ * Transform a message value or old_value object by parsing its columns.
251
+ */
252
+ transformMessageValue(value, schema) {
253
+ const row = value;
254
+ Object.keys(row).forEach((key) => {
255
+ row[key] = this.parseRow(key, row[key], schema);
256
+ });
257
+ return this.transformer ? this.transformer(row) : row;
258
+ }
237
259
  // Parses the message values using the provided parser based on the schema information
238
260
  parseRow(key, value, schema) {
239
261
  var _b;
@@ -269,6 +291,151 @@ function makeNullableParser(parser, columnInfo, columnName) {
269
291
  };
270
292
  }
271
293
 
294
+ // src/column-mapper.ts
295
+ function snakeToCamel(str) {
296
+ var _a, _b, _c, _d;
297
+ const leadingUnderscores = (_b = (_a = str.match(/^_+/)) == null ? void 0 : _a[0]) != null ? _b : ``;
298
+ const withoutLeading = str.slice(leadingUnderscores.length);
299
+ const trailingUnderscores = (_d = (_c = withoutLeading.match(/_+$/)) == null ? void 0 : _c[0]) != null ? _d : ``;
300
+ const core = trailingUnderscores ? withoutLeading.slice(
301
+ 0,
302
+ withoutLeading.length - trailingUnderscores.length
303
+ ) : withoutLeading;
304
+ const normalized = core.toLowerCase();
305
+ const camelCased = normalized.replace(
306
+ /_+([a-z])/g,
307
+ (_, letter) => letter.toUpperCase()
308
+ );
309
+ return leadingUnderscores + camelCased + trailingUnderscores;
310
+ }
311
+ function camelToSnake(str) {
312
+ return str.replace(/([a-z])([A-Z])/g, `$1_$2`).replace(/([A-Z]+)([A-Z][a-z])/g, `$1_$2`).toLowerCase();
313
+ }
314
+ function createColumnMapper(mapping) {
315
+ const reverseMapping = {};
316
+ for (const [dbName, appName] of Object.entries(mapping)) {
317
+ reverseMapping[appName] = dbName;
318
+ }
319
+ return {
320
+ decode: (dbColumnName) => {
321
+ var _a;
322
+ return (_a = mapping[dbColumnName]) != null ? _a : dbColumnName;
323
+ },
324
+ encode: (appColumnName) => {
325
+ var _a;
326
+ return (_a = reverseMapping[appColumnName]) != null ? _a : appColumnName;
327
+ }
328
+ };
329
+ }
330
+ function encodeWhereClause(whereClause, encode) {
331
+ if (!whereClause || !encode) return whereClause != null ? whereClause : ``;
332
+ const sqlKeywords = /* @__PURE__ */ new Set([
333
+ `SELECT`,
334
+ `FROM`,
335
+ `WHERE`,
336
+ `AND`,
337
+ `OR`,
338
+ `NOT`,
339
+ `IN`,
340
+ `IS`,
341
+ `NULL`,
342
+ `NULLS`,
343
+ `FIRST`,
344
+ `LAST`,
345
+ `TRUE`,
346
+ `FALSE`,
347
+ `LIKE`,
348
+ `ILIKE`,
349
+ `BETWEEN`,
350
+ `ASC`,
351
+ `DESC`,
352
+ `LIMIT`,
353
+ `OFFSET`,
354
+ `ORDER`,
355
+ `BY`,
356
+ `GROUP`,
357
+ `HAVING`,
358
+ `DISTINCT`,
359
+ `AS`,
360
+ `ON`,
361
+ `JOIN`,
362
+ `LEFT`,
363
+ `RIGHT`,
364
+ `INNER`,
365
+ `OUTER`,
366
+ `CROSS`,
367
+ `CASE`,
368
+ `WHEN`,
369
+ `THEN`,
370
+ `ELSE`,
371
+ `END`,
372
+ `CAST`,
373
+ `LOWER`,
374
+ `UPPER`,
375
+ `COALESCE`,
376
+ `NULLIF`
377
+ ]);
378
+ const quotedRanges = [];
379
+ let pos = 0;
380
+ while (pos < whereClause.length) {
381
+ const ch = whereClause[pos];
382
+ if (ch === `'` || ch === `"`) {
383
+ const start = pos;
384
+ const quoteChar = ch;
385
+ pos++;
386
+ while (pos < whereClause.length) {
387
+ if (whereClause[pos] === quoteChar) {
388
+ if (whereClause[pos + 1] === quoteChar) {
389
+ pos += 2;
390
+ } else {
391
+ pos++;
392
+ break;
393
+ }
394
+ } else {
395
+ pos++;
396
+ }
397
+ }
398
+ quotedRanges.push({ start, end: pos });
399
+ } else {
400
+ pos++;
401
+ }
402
+ }
403
+ const isInQuotedString = (pos2) => {
404
+ return quotedRanges.some((range) => pos2 >= range.start && pos2 < range.end);
405
+ };
406
+ const identifierPattern = new RegExp("(?<![a-zA-Z0-9_])([a-zA-Z_][a-zA-Z0-9_]*)(?![a-zA-Z0-9_])", "g");
407
+ return whereClause.replace(identifierPattern, (match, _p1, offset) => {
408
+ if (isInQuotedString(offset)) {
409
+ return match;
410
+ }
411
+ if (sqlKeywords.has(match.toUpperCase())) {
412
+ return match;
413
+ }
414
+ if (match.startsWith(`$`)) {
415
+ return match;
416
+ }
417
+ const encoded = encode(match);
418
+ return encoded;
419
+ });
420
+ }
421
+ function snakeCamelMapper(schema) {
422
+ if (schema) {
423
+ const mapping = {};
424
+ for (const dbColumn of Object.keys(schema)) {
425
+ mapping[dbColumn] = snakeToCamel(dbColumn);
426
+ }
427
+ return createColumnMapper(mapping);
428
+ }
429
+ return {
430
+ decode: (dbColumnName) => {
431
+ return snakeToCamel(dbColumnName);
432
+ },
433
+ encode: (appColumnName) => {
434
+ return camelToSnake(appColumnName);
435
+ }
436
+ };
437
+ }
438
+
272
439
  // src/helpers.ts
273
440
  function isChangeMessage(message) {
274
441
  return `key` in message;
@@ -664,6 +831,127 @@ var ExpiredShapesCache = class {
664
831
  };
665
832
  var expiredShapesCache = new ExpiredShapesCache();
666
833
 
834
+ // src/up-to-date-tracker.ts
835
+ var UpToDateTracker = class {
836
+ constructor() {
837
+ this.data = {};
838
+ this.storageKey = `electric_up_to_date_tracker`;
839
+ this.cacheTTL = 6e4;
840
+ // 60s to match typical CDN s-maxage cache duration
841
+ this.maxEntries = 250;
842
+ this.writeThrottleMs = 6e4;
843
+ // Throttle localStorage writes to once per 60s
844
+ this.lastWriteTime = 0;
845
+ this.load();
846
+ this.cleanup();
847
+ }
848
+ /**
849
+ * Records that a shape received an up-to-date message with a specific cursor.
850
+ * This timestamp and cursor are used to detect cache replay scenarios.
851
+ * Updates in-memory immediately, but throttles localStorage writes.
852
+ */
853
+ recordUpToDate(shapeKey, cursor) {
854
+ this.data[shapeKey] = {
855
+ timestamp: Date.now(),
856
+ cursor
857
+ };
858
+ const keys = Object.keys(this.data);
859
+ if (keys.length > this.maxEntries) {
860
+ const oldest = keys.reduce(
861
+ (min, k) => this.data[k].timestamp < this.data[min].timestamp ? k : min
862
+ );
863
+ delete this.data[oldest];
864
+ }
865
+ this.scheduleSave();
866
+ }
867
+ /**
868
+ * Schedules a throttled save to localStorage.
869
+ * Writes immediately if enough time has passed, otherwise schedules for later.
870
+ */
871
+ scheduleSave() {
872
+ const now = Date.now();
873
+ const timeSinceLastWrite = now - this.lastWriteTime;
874
+ if (timeSinceLastWrite >= this.writeThrottleMs) {
875
+ this.lastWriteTime = now;
876
+ this.save();
877
+ } else if (!this.pendingSaveTimer) {
878
+ const delay = this.writeThrottleMs - timeSinceLastWrite;
879
+ this.pendingSaveTimer = setTimeout(() => {
880
+ this.lastWriteTime = Date.now();
881
+ this.pendingSaveTimer = void 0;
882
+ this.save();
883
+ }, delay);
884
+ }
885
+ }
886
+ /**
887
+ * Checks if we should enter replay mode for this shape.
888
+ * Returns the last seen cursor if there's a recent up-to-date (< 60s),
889
+ * which means we'll likely be replaying cached responses.
890
+ * Returns null if no recent up-to-date exists.
891
+ */
892
+ shouldEnterReplayMode(shapeKey) {
893
+ const entry = this.data[shapeKey];
894
+ if (!entry) {
895
+ return null;
896
+ }
897
+ const age = Date.now() - entry.timestamp;
898
+ if (age >= this.cacheTTL) {
899
+ return null;
900
+ }
901
+ return entry.cursor;
902
+ }
903
+ /**
904
+ * Cleans up expired entries from the cache.
905
+ * Called on initialization and can be called periodically.
906
+ */
907
+ cleanup() {
908
+ const now = Date.now();
909
+ const keys = Object.keys(this.data);
910
+ let modified = false;
911
+ for (const key of keys) {
912
+ const age = now - this.data[key].timestamp;
913
+ if (age > this.cacheTTL) {
914
+ delete this.data[key];
915
+ modified = true;
916
+ }
917
+ }
918
+ if (modified) {
919
+ this.save();
920
+ }
921
+ }
922
+ save() {
923
+ if (typeof localStorage === `undefined`) return;
924
+ try {
925
+ localStorage.setItem(this.storageKey, JSON.stringify(this.data));
926
+ } catch (e) {
927
+ }
928
+ }
929
+ load() {
930
+ if (typeof localStorage === `undefined`) return;
931
+ try {
932
+ const stored = localStorage.getItem(this.storageKey);
933
+ if (stored) {
934
+ this.data = JSON.parse(stored);
935
+ }
936
+ } catch (e) {
937
+ this.data = {};
938
+ }
939
+ }
940
+ /**
941
+ * Clears all tracked up-to-date timestamps.
942
+ * Useful for testing or manual cache invalidation.
943
+ */
944
+ clear() {
945
+ this.data = {};
946
+ if (this.pendingSaveTimer) {
947
+ clearTimeout(this.pendingSaveTimer);
948
+ this.pendingSaveTimer = void 0;
949
+ }
950
+ this.save();
951
+ }
952
+ };
953
+ var upToDateTracker = new UpToDateTracker();
954
+
667
955
  // src/snapshot-tracker.ts
668
956
  var SnapshotTracker = class {
669
957
  constructor() {
@@ -781,7 +1069,7 @@ function canonicalShapeKey(url) {
781
1069
  cleanUrl.searchParams.sort();
782
1070
  return cleanUrl.toString();
783
1071
  }
784
- 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;
1072
+ 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;
785
1073
  var ShapeStream = class {
786
1074
  constructor(options) {
787
1075
  __privateAdd(this, _ShapeStream_instances);
@@ -816,6 +1104,10 @@ var ShapeStream = class {
816
1104
  // counter for concurrent snapshot requests
817
1105
  __privateAdd(this, _midStreamPromise);
818
1106
  __privateAdd(this, _midStreamPromiseResolver);
1107
+ __privateAdd(this, _lastSeenCursor);
1108
+ // Last seen cursor from previous session (used to detect cached responses)
1109
+ __privateAdd(this, _currentFetchUrl);
1110
+ // Current fetch URL for computing shape key
819
1111
  __privateAdd(this, _lastSseConnectionStartTime);
820
1112
  __privateAdd(this, _minSseConnectionDuration, 1e3);
821
1113
  // Minimum expected SSE connection duration (1 second)
@@ -834,10 +1126,21 @@ var ShapeStream = class {
834
1126
  __privateSet(this, _lastOffset, (_a = this.options.offset) != null ? _a : `-1`);
835
1127
  __privateSet(this, _liveCacheBuster, ``);
836
1128
  __privateSet(this, _shapeHandle, this.options.handle);
837
- __privateSet(this, _messageParser, new MessageParser(
838
- options.parser,
839
- options.transformer
840
- ));
1129
+ let transformer;
1130
+ if (options.columnMapper) {
1131
+ const applyColumnMapper = (row) => {
1132
+ const result = {};
1133
+ for (const [dbKey, value] of Object.entries(row)) {
1134
+ const appKey = options.columnMapper.decode(dbKey);
1135
+ result[appKey] = value;
1136
+ }
1137
+ return result;
1138
+ };
1139
+ transformer = options.transformer ? (row) => options.transformer(applyColumnMapper(row)) : applyColumnMapper;
1140
+ } else {
1141
+ transformer = options.transformer;
1142
+ }
1143
+ __privateSet(this, _messageParser, new MessageParser(options.parser, transformer));
841
1144
  __privateSet(this, _onError, this.options.onError);
842
1145
  __privateSet(this, _mode, (_b = this.options.log) != null ? _b : `full`);
843
1146
  const baseFetchClient = (_c = options.fetchClient) != null ? _c : (...args) => fetch(...args);
@@ -928,7 +1231,7 @@ var ShapeStream = class {
928
1231
  });
929
1232
  }
930
1233
  /**
931
- * Request a snapshot for subset of data.
1234
+ * Request a snapshot for subset of data and inject it into the subscribed data stream.
932
1235
  *
933
1236
  * Only available when mode is `changes_only`.
934
1237
  * Returns the insertion point & the data, but more importantly injects the data
@@ -955,8 +1258,7 @@ var ShapeStream = class {
955
1258
  if (__privateGet(this, _activeSnapshotRequests) === 1) {
956
1259
  __privateMethod(this, _ShapeStream_instances, pause_fn).call(this);
957
1260
  }
958
- const { fetchUrl, requestHeaders } = yield __privateMethod(this, _ShapeStream_instances, constructUrl_fn).call(this, this.options.url, true, opts);
959
- const { metadata, data } = yield __privateMethod(this, _ShapeStream_instances, fetchSnapshot_fn).call(this, fetchUrl, requestHeaders);
1261
+ const { metadata, data } = yield this.fetchSnapshot(opts);
960
1262
  const dataWithEndBoundary = data.concat([
961
1263
  { headers: __spreadValues({ control: `snapshot-end` }, metadata) }
962
1264
  ]);
@@ -977,6 +1279,44 @@ var ShapeStream = class {
977
1279
  }
978
1280
  });
979
1281
  }
1282
+ /**
1283
+ * Fetch a snapshot for subset of data.
1284
+ * Returns the metadata and the data, but does not inject it into the subscribed data stream.
1285
+ *
1286
+ * @param opts - The options for the snapshot request.
1287
+ * @returns The metadata and the data for the snapshot.
1288
+ */
1289
+ fetchSnapshot(opts) {
1290
+ return __async(this, null, function* () {
1291
+ var _a;
1292
+ const { fetchUrl, requestHeaders } = yield __privateMethod(this, _ShapeStream_instances, constructUrl_fn).call(this, this.options.url, true, opts);
1293
+ const response = yield __privateGet(this, _fetchClient2).call(this, fetchUrl.toString(), {
1294
+ headers: requestHeaders
1295
+ });
1296
+ if (!response.ok) {
1297
+ throw new FetchError(
1298
+ response.status,
1299
+ void 0,
1300
+ void 0,
1301
+ Object.fromEntries([...response.headers.entries()]),
1302
+ fetchUrl.toString()
1303
+ );
1304
+ }
1305
+ const schema = (_a = __privateGet(this, _schema)) != null ? _a : getSchemaFromHeaders(response.headers, {
1306
+ required: true,
1307
+ url: fetchUrl.toString()
1308
+ });
1309
+ const { metadata, data: rawData } = yield response.json();
1310
+ const data = __privateGet(this, _messageParser).parseSnapshotData(
1311
+ rawData,
1312
+ schema
1313
+ );
1314
+ return {
1315
+ metadata,
1316
+ data
1317
+ };
1318
+ });
1319
+ }
980
1320
  };
981
1321
  _error = new WeakMap();
982
1322
  _fetchClient2 = new WeakMap();
@@ -1005,6 +1345,8 @@ _snapshotTracker = new WeakMap();
1005
1345
  _activeSnapshotRequests = new WeakMap();
1006
1346
  _midStreamPromise = new WeakMap();
1007
1347
  _midStreamPromiseResolver = new WeakMap();
1348
+ _lastSeenCursor = new WeakMap();
1349
+ _currentFetchUrl = new WeakMap();
1008
1350
  _lastSseConnectionStartTime = new WeakMap();
1009
1351
  _minSseConnectionDuration = new WeakMap();
1010
1352
  _consecutiveShortSseConnections = new WeakMap();
@@ -1014,6 +1356,9 @@ _sseBackoffBaseDelay = new WeakMap();
1014
1356
  _sseBackoffMaxDelay = new WeakMap();
1015
1357
  _unsubscribeFromVisibilityChanges = new WeakMap();
1016
1358
  _ShapeStream_instances = new WeakSet();
1359
+ replayMode_get = function() {
1360
+ return __privateGet(this, _lastSeenCursor) !== void 0;
1361
+ };
1017
1362
  start_fn = function() {
1018
1363
  return __async(this, null, function* () {
1019
1364
  var _a, _b, _c, _d, _e;
@@ -1113,6 +1458,7 @@ requestShape_fn = function() {
1113
1458
  };
1114
1459
  constructUrl_fn = function(url, resumingFromPause, subsetParams) {
1115
1460
  return __async(this, null, function* () {
1461
+ var _a, _b, _c;
1116
1462
  const [requestHeaders, params] = yield Promise.all([
1117
1463
  resolveHeaders(this.options.headers),
1118
1464
  this.options.params ? toInternalParams(convertWhereParamsToObj(this.options.params)) : void 0
@@ -1121,7 +1467,13 @@ constructUrl_fn = function(url, resumingFromPause, subsetParams) {
1121
1467
  const fetchUrl = new URL(url);
1122
1468
  if (params) {
1123
1469
  if (params.table) setQueryParam(fetchUrl, TABLE_QUERY_PARAM, params.table);
1124
- if (params.where) setQueryParam(fetchUrl, WHERE_QUERY_PARAM, params.where);
1470
+ if (params.where && typeof params.where === `string`) {
1471
+ const encodedWhere = encodeWhereClause(
1472
+ params.where,
1473
+ (_a = this.options.columnMapper) == null ? void 0 : _a.encode
1474
+ );
1475
+ setQueryParam(fetchUrl, WHERE_QUERY_PARAM, encodedWhere);
1476
+ }
1125
1477
  if (params.columns)
1126
1478
  setQueryParam(fetchUrl, COLUMNS_QUERY_PARAM, params.columns);
1127
1479
  if (params.replica) setQueryParam(fetchUrl, REPLICA_PARAM, params.replica);
@@ -1138,20 +1490,34 @@ constructUrl_fn = function(url, resumingFromPause, subsetParams) {
1138
1490
  }
1139
1491
  }
1140
1492
  if (subsetParams) {
1141
- if (subsetParams.where)
1142
- setQueryParam(fetchUrl, SUBSET_PARAM_WHERE, subsetParams.where);
1493
+ if (subsetParams.where && typeof subsetParams.where === `string`) {
1494
+ const encodedWhere = encodeWhereClause(
1495
+ subsetParams.where,
1496
+ (_b = this.options.columnMapper) == null ? void 0 : _b.encode
1497
+ );
1498
+ setQueryParam(fetchUrl, SUBSET_PARAM_WHERE, encodedWhere);
1499
+ }
1143
1500
  if (subsetParams.params)
1144
- setQueryParam(fetchUrl, SUBSET_PARAM_WHERE_PARAMS, subsetParams.params);
1501
+ fetchUrl.searchParams.set(
1502
+ SUBSET_PARAM_WHERE_PARAMS,
1503
+ JSON.stringify(subsetParams.params)
1504
+ );
1145
1505
  if (subsetParams.limit)
1146
1506
  setQueryParam(fetchUrl, SUBSET_PARAM_LIMIT, subsetParams.limit);
1147
1507
  if (subsetParams.offset)
1148
1508
  setQueryParam(fetchUrl, SUBSET_PARAM_OFFSET, subsetParams.offset);
1149
- if (subsetParams.orderBy)
1150
- setQueryParam(fetchUrl, SUBSET_PARAM_ORDER_BY, subsetParams.orderBy);
1509
+ if (subsetParams.orderBy && typeof subsetParams.orderBy === `string`) {
1510
+ const encodedOrderBy = encodeWhereClause(
1511
+ subsetParams.orderBy,
1512
+ (_c = this.options.columnMapper) == null ? void 0 : _c.encode
1513
+ );
1514
+ setQueryParam(fetchUrl, SUBSET_PARAM_ORDER_BY, encodedOrderBy);
1515
+ }
1151
1516
  }
1152
1517
  fetchUrl.searchParams.set(OFFSET_QUERY_PARAM, __privateGet(this, _lastOffset));
1153
1518
  fetchUrl.searchParams.set(LOG_MODE_QUERY_PARAM, __privateGet(this, _mode));
1154
- if (__privateGet(this, _isUpToDate)) {
1519
+ const isSnapshotRequest = subsetParams !== void 0;
1520
+ if (__privateGet(this, _isUpToDate) && !isSnapshotRequest) {
1155
1521
  if (!__privateGet(this, _isRefreshing) && !resumingFromPause) {
1156
1522
  fetchUrl.searchParams.set(LIVE_QUERY_PARAM, `true`);
1157
1523
  }
@@ -1208,11 +1574,7 @@ onInitialResponse_fn = function(response) {
1208
1574
  if (liveCacheBuster) {
1209
1575
  __privateSet(this, _liveCacheBuster, liveCacheBuster);
1210
1576
  }
1211
- const getSchema = () => {
1212
- const schemaHeader = headers.get(SHAPE_SCHEMA_HEADER);
1213
- return schemaHeader ? JSON.parse(schemaHeader) : {};
1214
- };
1215
- __privateSet(this, _schema, (_a = __privateGet(this, _schema)) != null ? _a : getSchema());
1577
+ __privateSet(this, _schema, (_a = __privateGet(this, _schema)) != null ? _a : getSchemaFromHeaders(headers));
1216
1578
  if (status === 204) {
1217
1579
  __privateSet(this, _lastSyncedAt, Date.now());
1218
1580
  }
@@ -1235,6 +1597,17 @@ onMessages_fn = function(batch, isSseMessage = false) {
1235
1597
  __privateSet(this, _isUpToDate, true);
1236
1598
  __privateSet(this, _isMidStream, false);
1237
1599
  (_a = __privateGet(this, _midStreamPromiseResolver)) == null ? void 0 : _a.call(this);
1600
+ if (__privateGet(this, _ShapeStream_instances, replayMode_get) && !isSseMessage) {
1601
+ const currentCursor = __privateGet(this, _liveCacheBuster);
1602
+ if (currentCursor === __privateGet(this, _lastSeenCursor)) {
1603
+ return;
1604
+ }
1605
+ }
1606
+ __privateSet(this, _lastSeenCursor, void 0);
1607
+ if (__privateGet(this, _currentFetchUrl)) {
1608
+ const shapeKey = canonicalShapeKey(__privateGet(this, _currentFetchUrl));
1609
+ upToDateTracker.recordUpToDate(shapeKey, __privateGet(this, _liveCacheBuster));
1610
+ }
1238
1611
  }
1239
1612
  const messagesToProcess = batch.filter((message) => {
1240
1613
  if (isChangeMessage(message)) {
@@ -1249,6 +1622,14 @@ onMessages_fn = function(batch, isSseMessage = false) {
1249
1622
  fetchShape_fn = function(opts) {
1250
1623
  return __async(this, null, function* () {
1251
1624
  var _a;
1625
+ __privateSet(this, _currentFetchUrl, opts.fetchUrl);
1626
+ if (!__privateGet(this, _isUpToDate) && !__privateGet(this, _ShapeStream_instances, replayMode_get)) {
1627
+ const shapeKey = canonicalShapeKey(opts.fetchUrl);
1628
+ const lastSeenCursor = upToDateTracker.shouldEnterReplayMode(shapeKey);
1629
+ if (lastSeenCursor) {
1630
+ __privateSet(this, _lastSeenCursor, lastSeenCursor);
1631
+ }
1632
+ }
1252
1633
  const useSse = (_a = this.options.liveSse) != null ? _a : this.options.experimentalLiveSse;
1253
1634
  if (__privateGet(this, _isUpToDate) && useSse && !__privateGet(this, _isRefreshing) && !opts.resumingFromPause && !__privateGet(this, _sseFallbackToLongPolling)) {
1254
1635
  opts.fetchUrl.searchParams.set(EXPERIMENTAL_LIVE_SSE_QUERY_PARAM, `true`);
@@ -1440,33 +1821,20 @@ reset_fn = function(handle) {
1440
1821
  __privateSet(this, _consecutiveShortSseConnections, 0);
1441
1822
  __privateSet(this, _sseFallbackToLongPolling, false);
1442
1823
  };
1443
- fetchSnapshot_fn = function(url, headers) {
1444
- return __async(this, null, function* () {
1445
- const response = yield __privateGet(this, _fetchClient2).call(this, url.toString(), { headers });
1446
- if (!response.ok) {
1447
- throw new FetchError(
1448
- response.status,
1449
- void 0,
1450
- void 0,
1451
- Object.fromEntries([...response.headers.entries()]),
1452
- url.toString()
1453
- );
1454
- }
1455
- const { metadata, data } = yield response.json();
1456
- const batch = __privateGet(this, _messageParser).parse(
1457
- JSON.stringify(data),
1458
- __privateGet(this, _schema)
1459
- );
1460
- return {
1461
- metadata,
1462
- data: batch
1463
- };
1464
- });
1465
- };
1466
1824
  ShapeStream.Replica = {
1467
1825
  FULL: `full`,
1468
1826
  DEFAULT: `default`
1469
1827
  };
1828
+ function getSchemaFromHeaders(headers, options) {
1829
+ const schemaHeader = headers.get(SHAPE_SCHEMA_HEADER);
1830
+ if (!schemaHeader) {
1831
+ if ((options == null ? void 0 : options.required) && (options == null ? void 0 : options.url)) {
1832
+ throw new MissingHeadersError(options.url, [SHAPE_SCHEMA_HEADER]);
1833
+ }
1834
+ return {};
1835
+ }
1836
+ return JSON.parse(schemaHeader);
1837
+ }
1470
1838
  function validateParams(params) {
1471
1839
  if (!params) return;
1472
1840
  const reservedParams = Object.keys(params).filter(
@@ -1731,9 +2099,13 @@ export {
1731
2099
  FetchError,
1732
2100
  Shape,
1733
2101
  ShapeStream,
2102
+ camelToSnake,
2103
+ createColumnMapper,
1734
2104
  isChangeMessage,
1735
2105
  isControlMessage,
1736
2106
  isVisibleInSnapshot,
1737
- resolveValue
2107
+ resolveValue,
2108
+ snakeCamelMapper,
2109
+ snakeToCamel
1738
2110
  };
1739
2111
  //# sourceMappingURL=index.mjs.map