@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.
@@ -203,15 +203,37 @@ var MessageParser = class {
203
203
  parse(messages, schema) {
204
204
  return JSON.parse(messages, (key, value) => {
205
205
  if ((key === `value` || key === `old_value`) && typeof value === `object` && value !== null) {
206
- const row = value;
207
- Object.keys(row).forEach((key2) => {
208
- row[key2] = this.parseRow(key2, row[key2], schema);
209
- });
210
- if (this.transformer) value = this.transformer(value);
206
+ return this.transformMessageValue(value, schema);
211
207
  }
212
208
  return value;
213
209
  });
214
210
  }
211
+ /**
212
+ * Parse an array of ChangeMessages from a snapshot response.
213
+ * Applies type parsing and transformations to the value and old_value properties.
214
+ */
215
+ parseSnapshotData(messages, schema) {
216
+ return messages.map((message) => {
217
+ const msg = message;
218
+ if (msg.value && typeof msg.value === `object` && msg.value !== null) {
219
+ msg.value = this.transformMessageValue(msg.value, schema);
220
+ }
221
+ if (msg.old_value && typeof msg.old_value === `object` && msg.old_value !== null) {
222
+ msg.old_value = this.transformMessageValue(msg.old_value, schema);
223
+ }
224
+ return msg;
225
+ });
226
+ }
227
+ /**
228
+ * Transform a message value or old_value object by parsing its columns.
229
+ */
230
+ transformMessageValue(value, schema) {
231
+ const row = value;
232
+ Object.keys(row).forEach((key) => {
233
+ row[key] = this.parseRow(key, row[key], schema);
234
+ });
235
+ return this.transformer ? this.transformer(row) : row;
236
+ }
215
237
  // Parses the message values using the provided parser based on the schema information
216
238
  parseRow(key, value, schema) {
217
239
  var _b;
@@ -247,6 +269,151 @@ function makeNullableParser(parser, columnInfo, columnName) {
247
269
  };
248
270
  }
249
271
 
272
+ // src/column-mapper.ts
273
+ function snakeToCamel(str) {
274
+ var _a, _b, _c, _d;
275
+ const leadingUnderscores = (_b = (_a = str.match(/^_+/)) == null ? void 0 : _a[0]) != null ? _b : ``;
276
+ const withoutLeading = str.slice(leadingUnderscores.length);
277
+ const trailingUnderscores = (_d = (_c = withoutLeading.match(/_+$/)) == null ? void 0 : _c[0]) != null ? _d : ``;
278
+ const core = trailingUnderscores ? withoutLeading.slice(
279
+ 0,
280
+ withoutLeading.length - trailingUnderscores.length
281
+ ) : withoutLeading;
282
+ const normalized = core.toLowerCase();
283
+ const camelCased = normalized.replace(
284
+ /_+([a-z])/g,
285
+ (_, letter) => letter.toUpperCase()
286
+ );
287
+ return leadingUnderscores + camelCased + trailingUnderscores;
288
+ }
289
+ function camelToSnake(str) {
290
+ return str.replace(/([a-z])([A-Z])/g, `$1_$2`).replace(/([A-Z]+)([A-Z][a-z])/g, `$1_$2`).toLowerCase();
291
+ }
292
+ function createColumnMapper(mapping) {
293
+ const reverseMapping = {};
294
+ for (const [dbName, appName] of Object.entries(mapping)) {
295
+ reverseMapping[appName] = dbName;
296
+ }
297
+ return {
298
+ decode: (dbColumnName) => {
299
+ var _a;
300
+ return (_a = mapping[dbColumnName]) != null ? _a : dbColumnName;
301
+ },
302
+ encode: (appColumnName) => {
303
+ var _a;
304
+ return (_a = reverseMapping[appColumnName]) != null ? _a : appColumnName;
305
+ }
306
+ };
307
+ }
308
+ function encodeWhereClause(whereClause, encode) {
309
+ if (!whereClause || !encode) return whereClause != null ? whereClause : ``;
310
+ const sqlKeywords = /* @__PURE__ */ new Set([
311
+ `SELECT`,
312
+ `FROM`,
313
+ `WHERE`,
314
+ `AND`,
315
+ `OR`,
316
+ `NOT`,
317
+ `IN`,
318
+ `IS`,
319
+ `NULL`,
320
+ `NULLS`,
321
+ `FIRST`,
322
+ `LAST`,
323
+ `TRUE`,
324
+ `FALSE`,
325
+ `LIKE`,
326
+ `ILIKE`,
327
+ `BETWEEN`,
328
+ `ASC`,
329
+ `DESC`,
330
+ `LIMIT`,
331
+ `OFFSET`,
332
+ `ORDER`,
333
+ `BY`,
334
+ `GROUP`,
335
+ `HAVING`,
336
+ `DISTINCT`,
337
+ `AS`,
338
+ `ON`,
339
+ `JOIN`,
340
+ `LEFT`,
341
+ `RIGHT`,
342
+ `INNER`,
343
+ `OUTER`,
344
+ `CROSS`,
345
+ `CASE`,
346
+ `WHEN`,
347
+ `THEN`,
348
+ `ELSE`,
349
+ `END`,
350
+ `CAST`,
351
+ `LOWER`,
352
+ `UPPER`,
353
+ `COALESCE`,
354
+ `NULLIF`
355
+ ]);
356
+ const quotedRanges = [];
357
+ let pos = 0;
358
+ while (pos < whereClause.length) {
359
+ const ch = whereClause[pos];
360
+ if (ch === `'` || ch === `"`) {
361
+ const start = pos;
362
+ const quoteChar = ch;
363
+ pos++;
364
+ while (pos < whereClause.length) {
365
+ if (whereClause[pos] === quoteChar) {
366
+ if (whereClause[pos + 1] === quoteChar) {
367
+ pos += 2;
368
+ } else {
369
+ pos++;
370
+ break;
371
+ }
372
+ } else {
373
+ pos++;
374
+ }
375
+ }
376
+ quotedRanges.push({ start, end: pos });
377
+ } else {
378
+ pos++;
379
+ }
380
+ }
381
+ const isInQuotedString = (pos2) => {
382
+ return quotedRanges.some((range) => pos2 >= range.start && pos2 < range.end);
383
+ };
384
+ const identifierPattern = new RegExp("(?<![a-zA-Z0-9_])([a-zA-Z_][a-zA-Z0-9_]*)(?![a-zA-Z0-9_])", "g");
385
+ return whereClause.replace(identifierPattern, (match, _p1, offset) => {
386
+ if (isInQuotedString(offset)) {
387
+ return match;
388
+ }
389
+ if (sqlKeywords.has(match.toUpperCase())) {
390
+ return match;
391
+ }
392
+ if (match.startsWith(`$`)) {
393
+ return match;
394
+ }
395
+ const encoded = encode(match);
396
+ return encoded;
397
+ });
398
+ }
399
+ function snakeCamelMapper(schema) {
400
+ if (schema) {
401
+ const mapping = {};
402
+ for (const dbColumn of Object.keys(schema)) {
403
+ mapping[dbColumn] = snakeToCamel(dbColumn);
404
+ }
405
+ return createColumnMapper(mapping);
406
+ }
407
+ return {
408
+ decode: (dbColumnName) => {
409
+ return snakeToCamel(dbColumnName);
410
+ },
411
+ encode: (appColumnName) => {
412
+ return camelToSnake(appColumnName);
413
+ }
414
+ };
415
+ }
416
+
250
417
  // src/helpers.ts
251
418
  function isChangeMessage(message) {
252
419
  return `key` in message;
@@ -642,6 +809,127 @@ var ExpiredShapesCache = class {
642
809
  };
643
810
  var expiredShapesCache = new ExpiredShapesCache();
644
811
 
812
+ // src/up-to-date-tracker.ts
813
+ var UpToDateTracker = class {
814
+ constructor() {
815
+ this.data = {};
816
+ this.storageKey = `electric_up_to_date_tracker`;
817
+ this.cacheTTL = 6e4;
818
+ // 60s to match typical CDN s-maxage cache duration
819
+ this.maxEntries = 250;
820
+ this.writeThrottleMs = 6e4;
821
+ // Throttle localStorage writes to once per 60s
822
+ this.lastWriteTime = 0;
823
+ this.load();
824
+ this.cleanup();
825
+ }
826
+ /**
827
+ * Records that a shape received an up-to-date message with a specific cursor.
828
+ * This timestamp and cursor are used to detect cache replay scenarios.
829
+ * Updates in-memory immediately, but throttles localStorage writes.
830
+ */
831
+ recordUpToDate(shapeKey, cursor) {
832
+ this.data[shapeKey] = {
833
+ timestamp: Date.now(),
834
+ cursor
835
+ };
836
+ const keys = Object.keys(this.data);
837
+ if (keys.length > this.maxEntries) {
838
+ const oldest = keys.reduce(
839
+ (min, k) => this.data[k].timestamp < this.data[min].timestamp ? k : min
840
+ );
841
+ delete this.data[oldest];
842
+ }
843
+ this.scheduleSave();
844
+ }
845
+ /**
846
+ * Schedules a throttled save to localStorage.
847
+ * Writes immediately if enough time has passed, otherwise schedules for later.
848
+ */
849
+ scheduleSave() {
850
+ const now = Date.now();
851
+ const timeSinceLastWrite = now - this.lastWriteTime;
852
+ if (timeSinceLastWrite >= this.writeThrottleMs) {
853
+ this.lastWriteTime = now;
854
+ this.save();
855
+ } else if (!this.pendingSaveTimer) {
856
+ const delay = this.writeThrottleMs - timeSinceLastWrite;
857
+ this.pendingSaveTimer = setTimeout(() => {
858
+ this.lastWriteTime = Date.now();
859
+ this.pendingSaveTimer = void 0;
860
+ this.save();
861
+ }, delay);
862
+ }
863
+ }
864
+ /**
865
+ * Checks if we should enter replay mode for this shape.
866
+ * Returns the last seen cursor if there's a recent up-to-date (< 60s),
867
+ * which means we'll likely be replaying cached responses.
868
+ * Returns null if no recent up-to-date exists.
869
+ */
870
+ shouldEnterReplayMode(shapeKey) {
871
+ const entry = this.data[shapeKey];
872
+ if (!entry) {
873
+ return null;
874
+ }
875
+ const age = Date.now() - entry.timestamp;
876
+ if (age >= this.cacheTTL) {
877
+ return null;
878
+ }
879
+ return entry.cursor;
880
+ }
881
+ /**
882
+ * Cleans up expired entries from the cache.
883
+ * Called on initialization and can be called periodically.
884
+ */
885
+ cleanup() {
886
+ const now = Date.now();
887
+ const keys = Object.keys(this.data);
888
+ let modified = false;
889
+ for (const key of keys) {
890
+ const age = now - this.data[key].timestamp;
891
+ if (age > this.cacheTTL) {
892
+ delete this.data[key];
893
+ modified = true;
894
+ }
895
+ }
896
+ if (modified) {
897
+ this.save();
898
+ }
899
+ }
900
+ save() {
901
+ if (typeof localStorage === `undefined`) return;
902
+ try {
903
+ localStorage.setItem(this.storageKey, JSON.stringify(this.data));
904
+ } catch (e) {
905
+ }
906
+ }
907
+ load() {
908
+ if (typeof localStorage === `undefined`) return;
909
+ try {
910
+ const stored = localStorage.getItem(this.storageKey);
911
+ if (stored) {
912
+ this.data = JSON.parse(stored);
913
+ }
914
+ } catch (e) {
915
+ this.data = {};
916
+ }
917
+ }
918
+ /**
919
+ * Clears all tracked up-to-date timestamps.
920
+ * Useful for testing or manual cache invalidation.
921
+ */
922
+ clear() {
923
+ this.data = {};
924
+ if (this.pendingSaveTimer) {
925
+ clearTimeout(this.pendingSaveTimer);
926
+ this.pendingSaveTimer = void 0;
927
+ }
928
+ this.save();
929
+ }
930
+ };
931
+ var upToDateTracker = new UpToDateTracker();
932
+
645
933
  // src/snapshot-tracker.ts
646
934
  var SnapshotTracker = class {
647
935
  constructor() {
@@ -751,7 +1039,7 @@ function canonicalShapeKey(url) {
751
1039
  cleanUrl.searchParams.sort();
752
1040
  return cleanUrl.toString();
753
1041
  }
754
- 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;
1042
+ 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;
755
1043
  var ShapeStream = class {
756
1044
  constructor(options) {
757
1045
  __privateAdd(this, _ShapeStream_instances);
@@ -786,6 +1074,10 @@ var ShapeStream = class {
786
1074
  // counter for concurrent snapshot requests
787
1075
  __privateAdd(this, _midStreamPromise);
788
1076
  __privateAdd(this, _midStreamPromiseResolver);
1077
+ __privateAdd(this, _lastSeenCursor);
1078
+ // Last seen cursor from previous session (used to detect cached responses)
1079
+ __privateAdd(this, _currentFetchUrl);
1080
+ // Current fetch URL for computing shape key
789
1081
  __privateAdd(this, _lastSseConnectionStartTime);
790
1082
  __privateAdd(this, _minSseConnectionDuration, 1e3);
791
1083
  // Minimum expected SSE connection duration (1 second)
@@ -804,10 +1096,21 @@ var ShapeStream = class {
804
1096
  __privateSet(this, _lastOffset, (_a = this.options.offset) != null ? _a : `-1`);
805
1097
  __privateSet(this, _liveCacheBuster, ``);
806
1098
  __privateSet(this, _shapeHandle, this.options.handle);
807
- __privateSet(this, _messageParser, new MessageParser(
808
- options.parser,
809
- options.transformer
810
- ));
1099
+ let transformer;
1100
+ if (options.columnMapper) {
1101
+ const applyColumnMapper = (row) => {
1102
+ const result = {};
1103
+ for (const [dbKey, value] of Object.entries(row)) {
1104
+ const appKey = options.columnMapper.decode(dbKey);
1105
+ result[appKey] = value;
1106
+ }
1107
+ return result;
1108
+ };
1109
+ transformer = options.transformer ? (row) => options.transformer(applyColumnMapper(row)) : applyColumnMapper;
1110
+ } else {
1111
+ transformer = options.transformer;
1112
+ }
1113
+ __privateSet(this, _messageParser, new MessageParser(options.parser, transformer));
811
1114
  __privateSet(this, _onError, this.options.onError);
812
1115
  __privateSet(this, _mode, (_b = this.options.log) != null ? _b : `full`);
813
1116
  const baseFetchClient = (_c = options.fetchClient) != null ? _c : (...args) => fetch(...args);
@@ -896,7 +1199,7 @@ var ShapeStream = class {
896
1199
  __privateSet(this, _isRefreshing, false);
897
1200
  }
898
1201
  /**
899
- * Request a snapshot for subset of data.
1202
+ * Request a snapshot for subset of data and inject it into the subscribed data stream.
900
1203
  *
901
1204
  * Only available when mode is `changes_only`.
902
1205
  * Returns the insertion point & the data, but more importantly injects the data
@@ -922,8 +1225,7 @@ var ShapeStream = class {
922
1225
  if (__privateGet(this, _activeSnapshotRequests) === 1) {
923
1226
  __privateMethod(this, _ShapeStream_instances, pause_fn).call(this);
924
1227
  }
925
- const { fetchUrl, requestHeaders } = await __privateMethod(this, _ShapeStream_instances, constructUrl_fn).call(this, this.options.url, true, opts);
926
- const { metadata, data } = await __privateMethod(this, _ShapeStream_instances, fetchSnapshot_fn).call(this, fetchUrl, requestHeaders);
1228
+ const { metadata, data } = await this.fetchSnapshot(opts);
927
1229
  const dataWithEndBoundary = data.concat([
928
1230
  { headers: __spreadValues({ control: `snapshot-end` }, metadata) }
929
1231
  ]);
@@ -943,6 +1245,42 @@ var ShapeStream = class {
943
1245
  }
944
1246
  }
945
1247
  }
1248
+ /**
1249
+ * Fetch a snapshot for subset of data.
1250
+ * Returns the metadata and the data, but does not inject it into the subscribed data stream.
1251
+ *
1252
+ * @param opts - The options for the snapshot request.
1253
+ * @returns The metadata and the data for the snapshot.
1254
+ */
1255
+ async fetchSnapshot(opts) {
1256
+ var _a;
1257
+ const { fetchUrl, requestHeaders } = await __privateMethod(this, _ShapeStream_instances, constructUrl_fn).call(this, this.options.url, true, opts);
1258
+ const response = await __privateGet(this, _fetchClient2).call(this, fetchUrl.toString(), {
1259
+ headers: requestHeaders
1260
+ });
1261
+ if (!response.ok) {
1262
+ throw new FetchError(
1263
+ response.status,
1264
+ void 0,
1265
+ void 0,
1266
+ Object.fromEntries([...response.headers.entries()]),
1267
+ fetchUrl.toString()
1268
+ );
1269
+ }
1270
+ const schema = (_a = __privateGet(this, _schema)) != null ? _a : getSchemaFromHeaders(response.headers, {
1271
+ required: true,
1272
+ url: fetchUrl.toString()
1273
+ });
1274
+ const { metadata, data: rawData } = await response.json();
1275
+ const data = __privateGet(this, _messageParser).parseSnapshotData(
1276
+ rawData,
1277
+ schema
1278
+ );
1279
+ return {
1280
+ metadata,
1281
+ data
1282
+ };
1283
+ }
946
1284
  };
947
1285
  _error = new WeakMap();
948
1286
  _fetchClient2 = new WeakMap();
@@ -971,6 +1309,8 @@ _snapshotTracker = new WeakMap();
971
1309
  _activeSnapshotRequests = new WeakMap();
972
1310
  _midStreamPromise = new WeakMap();
973
1311
  _midStreamPromiseResolver = new WeakMap();
1312
+ _lastSeenCursor = new WeakMap();
1313
+ _currentFetchUrl = new WeakMap();
974
1314
  _lastSseConnectionStartTime = new WeakMap();
975
1315
  _minSseConnectionDuration = new WeakMap();
976
1316
  _consecutiveShortSseConnections = new WeakMap();
@@ -980,6 +1320,9 @@ _sseBackoffBaseDelay = new WeakMap();
980
1320
  _sseBackoffMaxDelay = new WeakMap();
981
1321
  _unsubscribeFromVisibilityChanges = new WeakMap();
982
1322
  _ShapeStream_instances = new WeakSet();
1323
+ replayMode_get = function() {
1324
+ return __privateGet(this, _lastSeenCursor) !== void 0;
1325
+ };
983
1326
  start_fn = async function() {
984
1327
  var _a, _b, _c, _d, _e;
985
1328
  __privateSet(this, _started, true);
@@ -1074,6 +1417,7 @@ requestShape_fn = async function() {
1074
1417
  return __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
1075
1418
  };
1076
1419
  constructUrl_fn = async function(url, resumingFromPause, subsetParams) {
1420
+ var _a, _b, _c;
1077
1421
  const [requestHeaders, params] = await Promise.all([
1078
1422
  resolveHeaders(this.options.headers),
1079
1423
  this.options.params ? toInternalParams(convertWhereParamsToObj(this.options.params)) : void 0
@@ -1082,7 +1426,13 @@ constructUrl_fn = async function(url, resumingFromPause, subsetParams) {
1082
1426
  const fetchUrl = new URL(url);
1083
1427
  if (params) {
1084
1428
  if (params.table) setQueryParam(fetchUrl, TABLE_QUERY_PARAM, params.table);
1085
- if (params.where) setQueryParam(fetchUrl, WHERE_QUERY_PARAM, params.where);
1429
+ if (params.where && typeof params.where === `string`) {
1430
+ const encodedWhere = encodeWhereClause(
1431
+ params.where,
1432
+ (_a = this.options.columnMapper) == null ? void 0 : _a.encode
1433
+ );
1434
+ setQueryParam(fetchUrl, WHERE_QUERY_PARAM, encodedWhere);
1435
+ }
1086
1436
  if (params.columns)
1087
1437
  setQueryParam(fetchUrl, COLUMNS_QUERY_PARAM, params.columns);
1088
1438
  if (params.replica) setQueryParam(fetchUrl, REPLICA_PARAM, params.replica);
@@ -1099,20 +1449,34 @@ constructUrl_fn = async function(url, resumingFromPause, subsetParams) {
1099
1449
  }
1100
1450
  }
1101
1451
  if (subsetParams) {
1102
- if (subsetParams.where)
1103
- setQueryParam(fetchUrl, SUBSET_PARAM_WHERE, subsetParams.where);
1452
+ if (subsetParams.where && typeof subsetParams.where === `string`) {
1453
+ const encodedWhere = encodeWhereClause(
1454
+ subsetParams.where,
1455
+ (_b = this.options.columnMapper) == null ? void 0 : _b.encode
1456
+ );
1457
+ setQueryParam(fetchUrl, SUBSET_PARAM_WHERE, encodedWhere);
1458
+ }
1104
1459
  if (subsetParams.params)
1105
- setQueryParam(fetchUrl, SUBSET_PARAM_WHERE_PARAMS, subsetParams.params);
1460
+ fetchUrl.searchParams.set(
1461
+ SUBSET_PARAM_WHERE_PARAMS,
1462
+ JSON.stringify(subsetParams.params)
1463
+ );
1106
1464
  if (subsetParams.limit)
1107
1465
  setQueryParam(fetchUrl, SUBSET_PARAM_LIMIT, subsetParams.limit);
1108
1466
  if (subsetParams.offset)
1109
1467
  setQueryParam(fetchUrl, SUBSET_PARAM_OFFSET, subsetParams.offset);
1110
- if (subsetParams.orderBy)
1111
- setQueryParam(fetchUrl, SUBSET_PARAM_ORDER_BY, subsetParams.orderBy);
1468
+ if (subsetParams.orderBy && typeof subsetParams.orderBy === `string`) {
1469
+ const encodedOrderBy = encodeWhereClause(
1470
+ subsetParams.orderBy,
1471
+ (_c = this.options.columnMapper) == null ? void 0 : _c.encode
1472
+ );
1473
+ setQueryParam(fetchUrl, SUBSET_PARAM_ORDER_BY, encodedOrderBy);
1474
+ }
1112
1475
  }
1113
1476
  fetchUrl.searchParams.set(OFFSET_QUERY_PARAM, __privateGet(this, _lastOffset));
1114
1477
  fetchUrl.searchParams.set(LOG_MODE_QUERY_PARAM, __privateGet(this, _mode));
1115
- if (__privateGet(this, _isUpToDate)) {
1478
+ const isSnapshotRequest = subsetParams !== void 0;
1479
+ if (__privateGet(this, _isUpToDate) && !isSnapshotRequest) {
1116
1480
  if (!__privateGet(this, _isRefreshing) && !resumingFromPause) {
1117
1481
  fetchUrl.searchParams.set(LIVE_QUERY_PARAM, `true`);
1118
1482
  }
@@ -1165,11 +1529,7 @@ onInitialResponse_fn = async function(response) {
1165
1529
  if (liveCacheBuster) {
1166
1530
  __privateSet(this, _liveCacheBuster, liveCacheBuster);
1167
1531
  }
1168
- const getSchema = () => {
1169
- const schemaHeader = headers.get(SHAPE_SCHEMA_HEADER);
1170
- return schemaHeader ? JSON.parse(schemaHeader) : {};
1171
- };
1172
- __privateSet(this, _schema, (_a = __privateGet(this, _schema)) != null ? _a : getSchema());
1532
+ __privateSet(this, _schema, (_a = __privateGet(this, _schema)) != null ? _a : getSchemaFromHeaders(headers));
1173
1533
  if (status === 204) {
1174
1534
  __privateSet(this, _lastSyncedAt, Date.now());
1175
1535
  }
@@ -1190,6 +1550,17 @@ onMessages_fn = async function(batch, isSseMessage = false) {
1190
1550
  __privateSet(this, _isUpToDate, true);
1191
1551
  __privateSet(this, _isMidStream, false);
1192
1552
  (_a = __privateGet(this, _midStreamPromiseResolver)) == null ? void 0 : _a.call(this);
1553
+ if (__privateGet(this, _ShapeStream_instances, replayMode_get) && !isSseMessage) {
1554
+ const currentCursor = __privateGet(this, _liveCacheBuster);
1555
+ if (currentCursor === __privateGet(this, _lastSeenCursor)) {
1556
+ return;
1557
+ }
1558
+ }
1559
+ __privateSet(this, _lastSeenCursor, void 0);
1560
+ if (__privateGet(this, _currentFetchUrl)) {
1561
+ const shapeKey = canonicalShapeKey(__privateGet(this, _currentFetchUrl));
1562
+ upToDateTracker.recordUpToDate(shapeKey, __privateGet(this, _liveCacheBuster));
1563
+ }
1193
1564
  }
1194
1565
  const messagesToProcess = batch.filter((message) => {
1195
1566
  if (isChangeMessage(message)) {
@@ -1202,6 +1573,14 @@ onMessages_fn = async function(batch, isSseMessage = false) {
1202
1573
  };
1203
1574
  fetchShape_fn = async function(opts) {
1204
1575
  var _a;
1576
+ __privateSet(this, _currentFetchUrl, opts.fetchUrl);
1577
+ if (!__privateGet(this, _isUpToDate) && !__privateGet(this, _ShapeStream_instances, replayMode_get)) {
1578
+ const shapeKey = canonicalShapeKey(opts.fetchUrl);
1579
+ const lastSeenCursor = upToDateTracker.shouldEnterReplayMode(shapeKey);
1580
+ if (lastSeenCursor) {
1581
+ __privateSet(this, _lastSeenCursor, lastSeenCursor);
1582
+ }
1583
+ }
1205
1584
  const useSse = (_a = this.options.liveSse) != null ? _a : this.options.experimentalLiveSse;
1206
1585
  if (__privateGet(this, _isUpToDate) && useSse && !__privateGet(this, _isRefreshing) && !opts.resumingFromPause && !__privateGet(this, _sseFallbackToLongPolling)) {
1207
1586
  opts.fetchUrl.searchParams.set(EXPERIMENTAL_LIVE_SSE_QUERY_PARAM, `true`);
@@ -1382,31 +1761,20 @@ reset_fn = function(handle) {
1382
1761
  __privateSet(this, _consecutiveShortSseConnections, 0);
1383
1762
  __privateSet(this, _sseFallbackToLongPolling, false);
1384
1763
  };
1385
- fetchSnapshot_fn = async function(url, headers) {
1386
- const response = await __privateGet(this, _fetchClient2).call(this, url.toString(), { headers });
1387
- if (!response.ok) {
1388
- throw new FetchError(
1389
- response.status,
1390
- void 0,
1391
- void 0,
1392
- Object.fromEntries([...response.headers.entries()]),
1393
- url.toString()
1394
- );
1395
- }
1396
- const { metadata, data } = await response.json();
1397
- const batch = __privateGet(this, _messageParser).parse(
1398
- JSON.stringify(data),
1399
- __privateGet(this, _schema)
1400
- );
1401
- return {
1402
- metadata,
1403
- data: batch
1404
- };
1405
- };
1406
1764
  ShapeStream.Replica = {
1407
1765
  FULL: `full`,
1408
1766
  DEFAULT: `default`
1409
1767
  };
1768
+ function getSchemaFromHeaders(headers, options) {
1769
+ const schemaHeader = headers.get(SHAPE_SCHEMA_HEADER);
1770
+ if (!schemaHeader) {
1771
+ if ((options == null ? void 0 : options.required) && (options == null ? void 0 : options.url)) {
1772
+ throw new MissingHeadersError(options.url, [SHAPE_SCHEMA_HEADER]);
1773
+ }
1774
+ return {};
1775
+ }
1776
+ return JSON.parse(schemaHeader);
1777
+ }
1410
1778
  function validateParams(params) {
1411
1779
  if (!params) return;
1412
1780
  const reservedParams = Object.keys(params).filter(
@@ -1665,9 +2033,13 @@ export {
1665
2033
  FetchError,
1666
2034
  Shape,
1667
2035
  ShapeStream,
2036
+ camelToSnake,
2037
+ createColumnMapper,
1668
2038
  isChangeMessage,
1669
2039
  isControlMessage,
1670
2040
  isVisibleInSnapshot,
1671
- resolveValue
2041
+ resolveValue,
2042
+ snakeCamelMapper,
2043
+ snakeToCamel
1672
2044
  };
1673
2045
  //# sourceMappingURL=index.legacy-esm.js.map