reflectdb 0.1.0 → 0.1.2

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.
Files changed (61) hide show
  1. package/README.md +500 -40
  2. package/dist/cjs/client/index.cjs +32 -17
  3. package/dist/cjs/client/index.d.cts +18 -3
  4. package/dist/cjs/client/storage/indexeddb.d.cts +1 -1
  5. package/dist/cjs/core/index.cjs +28 -28
  6. package/dist/cjs/core/index.d.cts +21 -9
  7. package/dist/cjs/react/index.cjs +48 -33
  8. package/dist/cjs/react/index.d.cts +23 -5
  9. package/dist/cjs/server/drizzle.cjs +2 -2
  10. package/dist/cjs/server/drizzle.d.cts +1 -1
  11. package/dist/cjs/server/ephemeral/index.cjs +180 -0
  12. package/dist/cjs/server/ephemeral/index.d.cts +102 -0
  13. package/dist/cjs/server/ephemeral/redis.cjs +226 -0
  14. package/dist/cjs/server/ephemeral/redis.d.cts +136 -0
  15. package/dist/cjs/server/index.cjs +238 -87
  16. package/dist/cjs/server/index.d.cts +196 -5
  17. package/dist/cjs/svelte/index.cjs +34 -19
  18. package/dist/cjs/svelte/index.d.cts +18 -3
  19. package/dist/cjs/transport/bun-ws.cjs +9 -9
  20. package/dist/cjs/transport/bun-ws.d.cts +1 -1
  21. package/dist/cjs/transport/polling.cjs +11 -11
  22. package/dist/cjs/transport/polling.d.cts +1 -1
  23. package/dist/cjs/transport/sse.cjs +11 -11
  24. package/dist/cjs/transport/sse.d.cts +1 -1
  25. package/dist/cjs/transport/ws.cjs +11 -11
  26. package/dist/cjs/transport/ws.d.cts +1 -1
  27. package/dist/cjs/vanilla/index.cjs +34 -19
  28. package/dist/cjs/vanilla/index.d.cts +18 -3
  29. package/dist/client/index.d.ts +18 -3
  30. package/dist/client/index.js +7 -7
  31. package/dist/client/storage/indexeddb.d.ts +1 -1
  32. package/dist/client/storage/indexeddb.js +1 -1
  33. package/dist/core/index.d.ts +21 -9
  34. package/dist/core/index.js +20 -20
  35. package/dist/react/index.d.ts +23 -5
  36. package/dist/react/index.js +23 -23
  37. package/dist/server/drizzle.d.ts +1 -1
  38. package/dist/server/drizzle.js +3 -3
  39. package/dist/server/ephemeral/index.d.ts +102 -0
  40. package/dist/server/ephemeral/index.js +9 -0
  41. package/dist/server/ephemeral/redis.d.ts +136 -0
  42. package/dist/server/ephemeral/redis.js +186 -0
  43. package/dist/server/index.d.ts +196 -5
  44. package/dist/server/index.js +184 -163
  45. package/dist/shared/{esm-ytrd3hbq.js → esm-8qbr4y0d.js} +18 -3
  46. package/dist/shared/{esm-wkwx6bd9.js → esm-ck88h30s.js} +10 -10
  47. package/dist/shared/esm-g0marxk7.js +134 -0
  48. package/dist/svelte/index.d.ts +18 -3
  49. package/dist/svelte/index.js +9 -9
  50. package/dist/transport/bun-ws.d.ts +1 -1
  51. package/dist/transport/bun-ws.js +1 -1
  52. package/dist/transport/polling.d.ts +1 -1
  53. package/dist/transport/polling.js +3 -3
  54. package/dist/transport/sse.d.ts +1 -1
  55. package/dist/transport/sse.js +3 -3
  56. package/dist/transport/ws.d.ts +1 -1
  57. package/dist/transport/ws.js +3 -3
  58. package/dist/vanilla/index.d.ts +18 -3
  59. package/dist/vanilla/index.js +9 -9
  60. package/package.json +66 -44
  61. /package/dist/shared/{esm-b7xs9cde.js → esm-k7kedp3y.js} +0 -0
@@ -1,3 +1,6 @@
1
+ import {
2
+ EphemeralManager
3
+ } from "../shared/esm-g0marxk7.js";
1
4
  import {
2
5
  compareHlc,
3
6
  createHlc,
@@ -21,7 +24,7 @@ import {
21
24
  } from "../shared/esm-3tkwvysa.js";
22
25
  import {
23
26
  __require
24
- } from "../shared/esm-b7xs9cde.js";
27
+ } from "../shared/esm-k7kedp3y.js";
25
28
 
26
29
  // src/server/enforcement.ts
27
30
  function enforceClockDrift(hlc) {
@@ -462,7 +465,7 @@ class ResultCache {
462
465
  for (const row of rows) {
463
466
  const rowId = String(row[idField] ?? "");
464
467
  if (rowId) {
465
- newRows.set(rowId, row);
468
+ newRows.set(rowId, { ...row });
466
469
  }
467
470
  }
468
471
  const diff = this.diff(oldRows, newRows);
@@ -482,6 +485,9 @@ class ResultCache {
482
485
  }
483
486
  return this.diff(oldRows, newRows);
484
487
  }
488
+ evictRow(clientId, queryName, rowId) {
489
+ this.cache.get(clientId)?.get(queryName)?.delete(rowId);
490
+ }
485
491
  clear(clientId, queryName) {
486
492
  this.cache.get(clientId)?.delete(queryName);
487
493
  }
@@ -595,6 +601,14 @@ class BroadcastEngine {
595
601
  clearTimeout(timer);
596
602
  }
597
603
  }
604
+ forgetWriterRow(tableName, clientId, rowId) {
605
+ const affectedQueries = this.deps.tableDependencyIndex.get(tableName);
606
+ if (!affectedQueries)
607
+ return;
608
+ for (const queryName of affectedQueries) {
609
+ this.deps.resultCache.evictRow(clientId, queryName, rowId);
610
+ }
611
+ }
598
612
  async broadcastChanges(tableName, excludeClientId, overrideRoomKey) {
599
613
  const affectedQueries = this.deps.tableDependencyIndex.get(tableName);
600
614
  if (!affectedQueries)
@@ -1249,6 +1263,8 @@ class OpProcessor {
1249
1263
  if (queryReg.options.broadcast === "eager-durable" && this.storage) {
1250
1264
  await this.storage.applyOp(op.table, op.rowId, mergedRow, colClocks, hlcStr, op.op, payload);
1251
1265
  this.recordBatchWrite(batchCtx, op.table, op.rowId, mergedRow, colClocks, hlcStr);
1266
+ } else if (!this.storage) {
1267
+ this.recordBatchWrite(batchCtx, op.table, op.rowId, mergedRow, colClocks, hlcStr);
1252
1268
  } else {
1253
1269
  const pushed = this.eagerBuffer.push(op.table, bufferEntry, queryReg.options.maxBufferSize);
1254
1270
  this.recordBatchWrite(batchCtx, op.table, op.rowId, mergedRow, colClocks, hlcStr);
@@ -1260,6 +1276,9 @@ class OpProcessor {
1260
1276
  }
1261
1277
  }
1262
1278
  }
1279
+ if (op.op === "delete") {
1280
+ this.deps.forgetWriterRow(op.table, session.clientId, op.rowId);
1281
+ }
1263
1282
  const senderSession = this.sessions.get(session.clientId);
1264
1283
  const senderSub = senderSession?.subscriptions.get(op.table);
1265
1284
  const senderRoomKey = senderSub ? senderSub.roomKey : undefined;
@@ -1349,6 +1368,9 @@ class OpProcessor {
1349
1368
  await this.storage.applyOp(op.table, op.rowId, result.resolvedRow, result.updatedColClocks, op.hlc, op.op, result.resolvedRow);
1350
1369
  this.recordBatchWrite(batchCtx, op.table, op.rowId, result.resolvedRow, result.updatedColClocks, op.hlc);
1351
1370
  }
1371
+ if (op.op === "delete") {
1372
+ this.deps.forgetWriterRow(op.table, session.clientId, op.rowId);
1373
+ }
1352
1374
  if (deferredBroadcastTables) {
1353
1375
  deferredBroadcastTables.add(op.table);
1354
1376
  if (mutateResult?.affected) {
@@ -1376,121 +1398,6 @@ class OpProcessor {
1376
1398
  }
1377
1399
  }
1378
1400
 
1379
- // src/server/ephemeral-manager.ts
1380
- var DEFAULT_MAX_ENTRIES = 1e4;
1381
- function indexKey(room, key, userId) {
1382
- return `${room}\x00${key}\x00${userId}`;
1383
- }
1384
- function parseIndexKey(packed) {
1385
- const [room, key, userId] = packed.split("\x00");
1386
- return { room, key, userId };
1387
- }
1388
-
1389
- class EphemeralManager {
1390
- store = new Map;
1391
- clientIndex = new Map;
1392
- entryCount = 0;
1393
- maxEntries;
1394
- constructor(maxEntries) {
1395
- this.maxEntries = maxEntries ?? DEFAULT_MAX_ENTRIES;
1396
- }
1397
- set(room, key, clientId, userId, data, ttlMs) {
1398
- const roomMap = this.store.get(room) ?? new Map;
1399
- const keyMap = roomMap.get(key) ?? new Map;
1400
- const isUpdate = keyMap.has(userId);
1401
- if (!isUpdate && this.entryCount >= this.maxEntries) {
1402
- return false;
1403
- }
1404
- const state = {
1405
- clientId,
1406
- userId,
1407
- key,
1408
- data,
1409
- updatedAt: Date.now(),
1410
- ttlMs
1411
- };
1412
- keyMap.set(userId, state);
1413
- roomMap.set(key, keyMap);
1414
- this.store.set(room, roomMap);
1415
- if (!isUpdate) {
1416
- this.entryCount++;
1417
- const clientSet = this.clientIndex.get(clientId) ?? new Set;
1418
- clientSet.add(indexKey(room, key, userId));
1419
- this.clientIndex.set(clientId, clientSet);
1420
- }
1421
- return true;
1422
- }
1423
- get(room, key) {
1424
- const roomMap = this.store.get(room);
1425
- if (!roomMap)
1426
- return {};
1427
- const keyMap = roomMap.get(key);
1428
- if (!keyMap)
1429
- return {};
1430
- return Object.fromEntries(keyMap.entries());
1431
- }
1432
- remove(room, key, userId) {
1433
- const roomMap = this.store.get(room);
1434
- if (!roomMap)
1435
- return;
1436
- const keyMap = roomMap.get(key);
1437
- if (!keyMap)
1438
- return;
1439
- if (keyMap.has(userId)) {
1440
- keyMap.delete(userId);
1441
- this.entryCount--;
1442
- }
1443
- if (keyMap.size === 0) {
1444
- roomMap.delete(key);
1445
- }
1446
- if (roomMap.size === 0) {
1447
- this.store.delete(room);
1448
- }
1449
- }
1450
- removeClient(clientId) {
1451
- const indices = this.clientIndex.get(clientId);
1452
- if (!indices)
1453
- return;
1454
- const entries = [...indices];
1455
- for (const packed of entries) {
1456
- const { room, key, userId } = parseIndexKey(packed);
1457
- this.remove(room, key, userId);
1458
- }
1459
- this.clientIndex.delete(clientId);
1460
- }
1461
- cleanupExpired() {
1462
- const now = Date.now();
1463
- const toRemove = [];
1464
- for (const [room, roomMap] of this.store) {
1465
- for (const [key, keyMap] of roomMap) {
1466
- for (const [userId, state] of keyMap) {
1467
- if (state.ttlMs && now - state.updatedAt > state.ttlMs) {
1468
- toRemove.push({ room, key, userId, clientId: state.clientId });
1469
- }
1470
- }
1471
- }
1472
- }
1473
- for (const { room, key, userId, clientId } of toRemove) {
1474
- this.remove(room, key, userId);
1475
- const clientSet = this.clientIndex.get(clientId);
1476
- if (clientSet) {
1477
- clientSet.delete(indexKey(room, key, userId));
1478
- if (clientSet.size === 0) {
1479
- this.clientIndex.delete(clientId);
1480
- }
1481
- }
1482
- }
1483
- }
1484
- get size() {
1485
- return this.entryCount;
1486
- }
1487
- destroy() {
1488
- this.store.clear();
1489
- this.clientIndex.clear();
1490
- this.entryCount = 0;
1491
- }
1492
- }
1493
-
1494
1401
  // src/server/eager-buffer.ts
1495
1402
  var DEFAULT_FLUSH_INTERVAL = 200;
1496
1403
  var DEFAULT_MAX_BUFFER_SIZE = 1000;
@@ -1934,6 +1841,7 @@ class MessageHandler {
1934
1841
  broadcast;
1935
1842
  ops;
1936
1843
  ephemeralManager = new EphemeralManager;
1844
+ ephemeralBusReady = false;
1937
1845
  ephemeralCleanupTimer = null;
1938
1846
  eagerBuffer = new EagerBuffer;
1939
1847
  replay;
@@ -2004,7 +1912,8 @@ class MessageHandler {
2004
1912
  stampHlc: () => this.clock.stamp(),
2005
1913
  receiveClientHlc: (hlc) => this.clock.receive(hlc),
2006
1914
  send: (clientId, message) => this.transport.send(clientId, message),
2007
- broadcastChanges: (table, excludeClientId) => this.broadcastChanges(table, excludeClientId)
1915
+ broadcastChanges: (table, excludeClientId) => this.broadcastChanges(table, excludeClientId),
1916
+ forgetWriterRow: (table, clientId, rowId) => this.broadcast.forgetWriterRow(table, clientId, rowId)
2008
1917
  });
2009
1918
  this.transport.onConnect((clientId) => {
2010
1919
  this.sessions.connect(clientId);
@@ -2015,7 +1924,7 @@ class MessageHandler {
2015
1924
  this.messageQueues.delete(clientId);
2016
1925
  this.messageQueueDepths.delete(clientId);
2017
1926
  this.resultCache.clearClient(clientId);
2018
- this.ephemeralManager.removeClient(clientId);
1927
+ this.forgetEphemeralClient(clientId);
2019
1928
  this.ephemeralBuckets.delete(clientId);
2020
1929
  this.emit({ type: "client_disconnected", clientId });
2021
1930
  });
@@ -2056,7 +1965,9 @@ class MessageHandler {
2056
1965
  this.messageQueues.set(clientId, next);
2057
1966
  });
2058
1967
  this.ephemeralCleanupTimer = setInterval(() => {
2059
- this.ephemeralManager.cleanupExpired();
1968
+ Promise.resolve(this.ephemeralManager.cleanupExpired()).catch((err) => {
1969
+ console.error("[reflectdb] ephemeral cleanup failed:", err);
1970
+ });
2060
1971
  }, 5000);
2061
1972
  this.eagerBuffer.setOnFlushError((err, op) => {
2062
1973
  this.emit({
@@ -2090,8 +2001,10 @@ class MessageHandler {
2090
2001
  }
2091
2002
  queryNames.add(name);
2092
2003
  }
2093
- if (registration.options.broadcast === "eager") {
2004
+ if (registration.options.broadcast === "eager" || registration.options.broadcast === "eager-durable") {
2094
2005
  this.eagerBuffer.start(registration.options.flushInterval);
2006
+ }
2007
+ if (registration.options.broadcast === "eager") {
2095
2008
  if (!MessageHandler.eagerWarned) {
2096
2009
  MessageHandler.eagerWarned = true;
2097
2010
  console.warn(`[reflectdb] Query "${name}" uses broadcast: "eager" — at-most-once across server crash for the oplog. ` + `Use "eager-durable" unless your mutate hook writes durably to the user DB and you can tolerate resume-via-snapshot.`);
@@ -2113,6 +2026,26 @@ class MessageHandler {
2113
2026
  setMaxEphemeralEntries(max) {
2114
2027
  this.ephemeralManager = new EphemeralManager(max);
2115
2028
  }
2029
+ setEphemeralAdapter(adapter) {
2030
+ this.ephemeralManager = adapter;
2031
+ if (!adapter.subscribe || this.ephemeralBusReady)
2032
+ return;
2033
+ this.ephemeralBusReady = true;
2034
+ Promise.resolve(adapter.subscribe((event) => {
2035
+ this.deliverRemoteEphemeral(event).catch((err) => {
2036
+ console.error("[reflectdb] remote ephemeral delivery failed:", err);
2037
+ });
2038
+ })).catch((err) => {
2039
+ console.error("[reflectdb] ephemeral bus subscribe failed:", err);
2040
+ });
2041
+ }
2042
+ async forgetEphemeralClient(clientId) {
2043
+ try {
2044
+ await this.ephemeralManager.removeClient(clientId);
2045
+ } catch (err) {
2046
+ console.error("[reflectdb] ephemeral client cleanup failed:", err);
2047
+ }
2048
+ }
2116
2049
  setMinSchemaVersion(version) {
2117
2050
  this.minSchemaVersion = version;
2118
2051
  }
@@ -2137,7 +2070,7 @@ class MessageHandler {
2137
2070
  this.messageQueues.delete(oldest.clientId);
2138
2071
  this.messageQueueDepths.delete(oldest.clientId);
2139
2072
  this.resultCache.clearClient(oldest.clientId);
2140
- this.ephemeralManager.removeClient(oldest.clientId);
2073
+ await this.forgetEphemeralClient(oldest.clientId);
2141
2074
  this.ephemeralBuckets.delete(oldest.clientId);
2142
2075
  }
2143
2076
  }
@@ -2640,6 +2573,9 @@ class MessageHandler {
2640
2573
  }
2641
2574
  }
2642
2575
  this.sessions.subscribe(clientId, message.table, message.params ?? {}, queryReg.options, roomKey, message.window ?? null);
2576
+ if (roomKey) {
2577
+ await this.sendPresenceSnapshot(clientId, roomKey);
2578
+ }
2643
2579
  }
2644
2580
  async handleLoadMore(clientId, message) {
2645
2581
  const session = this.sessions.get(clientId);
@@ -2753,21 +2689,45 @@ class MessageHandler {
2753
2689
  data,
2754
2690
  ttlMs
2755
2691
  };
2756
- const sent = new Set;
2757
- const recipients = [];
2758
2692
  for (const roomKey of roomKeys) {
2759
- const accepted = this.ephemeralManager.set(roomKey, key, clientId, userId, data, ttlMs);
2693
+ const accepted = await this.ephemeralManager.set(roomKey, key, clientId, userId, data, ttlMs);
2760
2694
  if (!accepted) {
2761
- this.emit({ type: "ephemeral_full", currentSize: this.ephemeralManager.size });
2695
+ this.emit({
2696
+ type: "ephemeral_full",
2697
+ currentSize: await this.ephemeralManager.size()
2698
+ });
2762
2699
  return;
2763
2700
  }
2764
2701
  }
2702
+ const targets = [];
2765
2703
  for (const queryName of queryNames) {
2766
2704
  const senderSub = session.subscriptions.get(queryName);
2767
- const senderRoomKey = senderSub?.roomKey ?? null;
2768
- const subscribers = this.sessions.getSubscribersForQuery(queryName, senderRoomKey);
2705
+ targets.push({ query: queryName, room: senderSub?.roomKey ?? null });
2706
+ }
2707
+ await this.fanOutEphemeral(event, targets, clientId);
2708
+ if (this.ephemeralManager.publish) {
2709
+ try {
2710
+ await this.ephemeralManager.publish({
2711
+ serverId: this.serverId,
2712
+ key,
2713
+ clientId,
2714
+ userId,
2715
+ data,
2716
+ ttlMs,
2717
+ targets
2718
+ });
2719
+ } catch (err) {
2720
+ console.error("[reflectdb] ephemeral publish failed:", err);
2721
+ }
2722
+ }
2723
+ }
2724
+ async fanOutEphemeral(event, targets, exclude) {
2725
+ const sent = new Set;
2726
+ const recipients = [];
2727
+ for (const target of targets) {
2728
+ const subscribers = this.sessions.getSubscribersForQuery(target.query, target.room);
2769
2729
  for (const sub of subscribers) {
2770
- if (sub.clientId !== clientId && !sent.has(sub.clientId)) {
2730
+ if (sub.clientId !== exclude && !sent.has(sub.clientId)) {
2771
2731
  sent.add(sub.clientId);
2772
2732
  recipients.push(sub.clientId);
2773
2733
  }
@@ -2775,6 +2735,41 @@ class MessageHandler {
2775
2735
  }
2776
2736
  await Promise.all(recipients.map((recipient) => this.trySend(recipient, event)));
2777
2737
  }
2738
+ async deliverRemoteEphemeral(remote) {
2739
+ if (remote.serverId === this.serverId)
2740
+ return;
2741
+ await this.fanOutEphemeral({
2742
+ type: "ephemeral",
2743
+ key: remote.key,
2744
+ clientId: remote.clientId,
2745
+ userId: remote.userId,
2746
+ data: remote.data,
2747
+ ttlMs: remote.ttlMs
2748
+ }, remote.targets);
2749
+ }
2750
+ async sendPresenceSnapshot(clientId, roomKey) {
2751
+ let channels;
2752
+ try {
2753
+ channels = await this.ephemeralManager.getRoom(roomKey);
2754
+ } catch (err) {
2755
+ console.error("[reflectdb] presence snapshot failed:", err);
2756
+ return;
2757
+ }
2758
+ for (const byClient of Object.values(channels)) {
2759
+ for (const state of Object.values(byClient)) {
2760
+ if (state.clientId === clientId)
2761
+ continue;
2762
+ await this.trySend(clientId, {
2763
+ type: "ephemeral",
2764
+ key: state.key,
2765
+ clientId: state.clientId,
2766
+ userId: state.userId,
2767
+ data: state.data,
2768
+ ttlMs: state.ttlMs
2769
+ });
2770
+ }
2771
+ }
2772
+ }
2778
2773
  async reserveOpId(opId) {
2779
2774
  return this.replay.reserve(opId);
2780
2775
  }
@@ -2881,7 +2876,7 @@ class MessageHandler {
2881
2876
  }
2882
2877
  this.messageQueues.clear();
2883
2878
  this.messageQueueDepths.clear();
2884
- this.ephemeralManager.destroy();
2879
+ Promise.resolve(this.ephemeralManager.destroy()).catch(() => {});
2885
2880
  await this.eagerBuffer.close();
2886
2881
  }
2887
2882
  }
@@ -2956,6 +2951,12 @@ function createServer(config) {
2956
2951
  if (config.storage) {
2957
2952
  handler.setStorage(config.storage);
2958
2953
  }
2954
+ if (config.ephemeral?.maxEntries !== undefined) {
2955
+ handler.setMaxEphemeralEntries(config.ephemeral.maxEntries);
2956
+ }
2957
+ if (config.ephemeral?.adapter) {
2958
+ handler.setEphemeralAdapter(config.ephemeral.adapter);
2959
+ }
2959
2960
  let compactionConfig = null;
2960
2961
  let pollTimer = null;
2961
2962
  const lockChains = new Map;
@@ -3225,6 +3226,13 @@ function loadDatabase() {
3225
3226
  throw new Error("createSqliteStorage requires the Bun runtime (bun:sqlite is unavailable). Use createPostgresStorage on Node.");
3226
3227
  }
3227
3228
  }
3229
+ function withStatement(statement, use) {
3230
+ try {
3231
+ return use(statement);
3232
+ } finally {
3233
+ statement.finalize?.();
3234
+ }
3235
+ }
3228
3236
  var OP_ID_RETENTION_MS = 24 * 60 * 60 * 1000;
3229
3237
  function createSqliteStorage(config = {}) {
3230
3238
  const db = config.db ?? new (loadDatabase())(config.path ?? ":memory:");
@@ -3282,6 +3290,18 @@ function createSqliteStorage(config = {}) {
3282
3290
  const cleanupOpsStmt = db.prepare("DELETE FROM _reflectdb_processed_ops WHERE created_at < ?");
3283
3291
  const getMetaStmt = db.prepare("SELECT value FROM _reflectdb_meta WHERE key = ?");
3284
3292
  const setMetaStmt = db.prepare("INSERT OR REPLACE INTO _reflectdb_meta (key, value) VALUES (?, ?)");
3293
+ const persistentStatements = [
3294
+ getRowStmt,
3295
+ putRowStmt,
3296
+ deleteRowStmt,
3297
+ getRowsStmt,
3298
+ appendOpStmt,
3299
+ deleteOpsStmt,
3300
+ reserveOpStmt,
3301
+ cleanupOpsStmt,
3302
+ getMetaStmt,
3303
+ setMetaStmt
3304
+ ];
3285
3305
  return {
3286
3306
  async getRow(table, rowId) {
3287
3307
  const result = getRowStmt.get(table, rowId);
@@ -3298,9 +3318,9 @@ function createSqliteStorage(config = {}) {
3298
3318
  if (rowIds.length === 0)
3299
3319
  return {};
3300
3320
  const placeholders = rowIds.map(() => "?").join(", ");
3301
- const stmt = db.prepare(`SELECT row_id, data, col_clocks, hlc FROM _reflectdb_rows WHERE tbl = ? AND row_id IN (${placeholders})`);
3321
+ const results = withStatement(db.prepare(`SELECT row_id, data, col_clocks, hlc FROM _reflectdb_rows WHERE tbl = ? AND row_id IN (${placeholders})`), (stmt) => stmt.all(table, ...rowIds));
3302
3322
  const out = {};
3303
- for (const r of stmt.all(table, ...rowIds)) {
3323
+ for (const r of results) {
3304
3324
  out[r.row_id] = {
3305
3325
  row: JSON.parse(r.data),
3306
3326
  rowHlc: r.hlc,
@@ -3345,8 +3365,7 @@ function createSqliteStorage(config = {}) {
3345
3365
  if (tables.length === 0)
3346
3366
  return [];
3347
3367
  const placeholders = tables.map(() => "?").join(", ");
3348
- const stmt = db.prepare(`SELECT tbl, op, row_id, payload, hlc, col_clocks FROM _reflectdb_oplog WHERE hlc > ? AND tbl IN (${placeholders}) ORDER BY hlc ASC`);
3349
- const results = stmt.all(since, ...tables);
3368
+ const results = withStatement(db.prepare(`SELECT tbl, op, row_id, payload, hlc, col_clocks FROM _reflectdb_oplog WHERE hlc > ? AND tbl IN (${placeholders}) ORDER BY hlc ASC`), (stmt) => stmt.all(since, ...tables));
3350
3369
  return results.map((r) => ({
3351
3370
  table: r.tbl,
3352
3371
  op: r.op,
@@ -3360,15 +3379,13 @@ function createSqliteStorage(config = {}) {
3360
3379
  if (tables.length === 0)
3361
3380
  return [];
3362
3381
  const placeholders = tables.map(() => "?").join(", ");
3363
- const stmt = db.prepare(`SELECT DISTINCT tbl FROM _reflectdb_oplog WHERE hlc > ? AND tbl IN (${placeholders})`);
3364
- return stmt.all(since, ...tables).map((r) => r.tbl);
3382
+ return withStatement(db.prepare(`SELECT DISTINCT tbl FROM _reflectdb_oplog WHERE hlc > ? AND tbl IN (${placeholders})`), (stmt) => stmt.all(since, ...tables).map((r) => r.tbl));
3365
3383
  },
3366
3384
  async getOplogHead(tables) {
3367
3385
  if (tables.length === 0)
3368
3386
  return null;
3369
3387
  const placeholders = tables.map(() => "?").join(", ");
3370
- const stmt = db.prepare(`SELECT MAX(hlc) AS head FROM _reflectdb_oplog WHERE tbl IN (${placeholders})`);
3371
- return stmt.get(...tables)?.head ?? null;
3388
+ return withStatement(db.prepare(`SELECT MAX(hlc) AS head FROM _reflectdb_oplog WHERE tbl IN (${placeholders})`), (stmt) => stmt.get(...tables)?.head ?? null);
3372
3389
  },
3373
3390
  async deleteOpsBefore(hlc) {
3374
3391
  const result = deleteOpsStmt.run(hlc);
@@ -3420,6 +3437,9 @@ function createSqliteStorage(config = {}) {
3420
3437
  db.run("DELETE FROM _reflectdb_meta WHERE key = ?", [`lock:${key}`]);
3421
3438
  },
3422
3439
  close() {
3440
+ for (const statement of persistentStatements) {
3441
+ statement.finalize?.();
3442
+ }
3423
3443
  db.close();
3424
3444
  }
3425
3445
  };
@@ -3693,7 +3713,8 @@ function createSyncServer(config) {
3693
3713
  allowAnonymous: config.allowAnonymous,
3694
3714
  queryTimeoutMs: config.queryTimeoutMs,
3695
3715
  maxBroadcastConcurrency: config.maxBroadcastConcurrency,
3696
- txAtomic: config.txAtomic
3716
+ txAtomic: config.txAtomic,
3717
+ ephemeral: config.ephemeral
3697
3718
  });
3698
3719
  const implementations = new Map;
3699
3720
  let authCallback = null;
@@ -3758,7 +3779,7 @@ function createSyncServer(config) {
3758
3779
  server.query(name, fn, {
3759
3780
  tables,
3760
3781
  mutate: async () => {
3761
- const { MutationError: MutationError2 } = await import("../shared/esm-wkwx6bd9.js");
3782
+ const { MutationError: MutationError2 } = await import("../shared/esm-ck88h30s.js");
3762
3783
  throw new MutationError2("readonly_query", `"${name}" is a view and is read-only`);
3763
3784
  }
3764
3785
  });
@@ -3932,28 +3953,28 @@ function applyServerSet(payload, serverSet, ctx) {
3932
3953
  }
3933
3954
  }
3934
3955
  export {
3935
- stableStringify,
3936
- resolveRoomKey,
3937
- resolveConflict,
3938
- processOp,
3939
- enforceServerSet,
3940
- enforceReadonly,
3941
- enforceClockDrift,
3942
- enforceBatchSize,
3943
- drizzleTxAtomic,
3944
- drizzleTable,
3945
- defineTable,
3946
- createSyncServer,
3947
- createSqliteStorage,
3948
- createServer,
3949
- createRateLimiter,
3950
- createPostgresStorage,
3951
- SessionManager,
3952
- ServerClock,
3953
- SERVER_HLC_META_KEY,
3954
- ResultCache,
3955
- OpProcessor,
3956
- MutationError,
3956
+ BroadcastEngine,
3957
3957
  MessageHandler,
3958
- BroadcastEngine
3958
+ MutationError,
3959
+ OpProcessor,
3960
+ ResultCache,
3961
+ SERVER_HLC_META_KEY,
3962
+ ServerClock,
3963
+ SessionManager,
3964
+ createPostgresStorage,
3965
+ createRateLimiter,
3966
+ createServer,
3967
+ createSqliteStorage,
3968
+ createSyncServer,
3969
+ defineTable,
3970
+ drizzleTable,
3971
+ drizzleTxAtomic,
3972
+ enforceBatchSize,
3973
+ enforceClockDrift,
3974
+ enforceReadonly,
3975
+ enforceServerSet,
3976
+ processOp,
3977
+ resolveConflict,
3978
+ resolveRoomKey,
3979
+ stableStringify
3959
3980
  };
@@ -256,7 +256,7 @@ class ClientStore {
256
256
  if (existing?.serverHlc && compareHlc(hlc, existing.serverHlc) <= 0) {
257
257
  return;
258
258
  }
259
- this.setRow(table, rowId, payload ?? {}, colClocks ?? { _row: hlc }, hlc);
259
+ this.setRow(table, rowId, this.withPk(table, rowId, payload ?? {}), colClocks ?? { _row: hlc }, hlc);
260
260
  return;
261
261
  }
262
262
  const incomingClocks = colClocks ?? {};
@@ -277,7 +277,13 @@ class ClientStore {
277
277
  }
278
278
  const newRowHlc = existing.serverHlc && compareHlc(existing.serverHlc, hlc) >= 0 ? existing.serverHlc : hlc;
279
279
  mergedClocks._row = newRowHlc;
280
- this.setRow(table, rowId, mergedData, mergedClocks, newRowHlc);
280
+ this.setRow(table, rowId, this.withPk(table, rowId, mergedData), mergedClocks, newRowHlc);
281
+ }
282
+ withPk(table, rowId, data) {
283
+ const pk = this.tableMeta.get(table)?.pk ?? "id";
284
+ if (data[pk] !== undefined)
285
+ return data;
286
+ return { ...data, [pk]: rowId };
281
287
  }
282
288
  revertOp(opId, serverRow) {
283
289
  const pending = this.pendingOps.find((p) => p.op.id === opId);
@@ -322,10 +328,19 @@ class ClientStore {
322
328
  delete payload[field];
323
329
  }
324
330
  }
331
+ if (op.op === "insert" && payload) {
332
+ payload = this.withPk(op.table, op.rowId, payload);
333
+ }
325
334
  if (op.op === "delete") {
326
335
  this.setRow(op.table, op.rowId, null, { _row: op.hlc }, op.hlc);
327
336
  } else if (op.op === "insert") {
328
- this.setRow(op.table, op.rowId, payload ?? {}, { _row: op.hlc }, op.hlc);
337
+ const existing = this.getRow(op.table, op.rowId);
338
+ if (existing?.data) {
339
+ const merged = { ...existing.data, ...payload };
340
+ this.setRow(op.table, op.rowId, merged, existing.colClocks, existing.serverHlc);
341
+ } else {
342
+ this.setRow(op.table, op.rowId, payload ?? {}, { _row: op.hlc }, op.hlc);
343
+ }
329
344
  } else {
330
345
  const existing = this.getRow(op.table, op.rowId);
331
346
  if (existing?.data) {
@@ -10,16 +10,16 @@ import {
10
10
  isErrorReason,
11
11
  reasonFromError
12
12
  } from "./esm-3tkwvysa.js";
13
- import"./esm-b7xs9cde.js";
13
+ import"./esm-k7kedp3y.js";
14
14
  export {
15
- reasonFromError,
16
- isErrorReason,
17
- TransportSendError,
18
- TOMBSTONE_RETENTION_MS,
19
- SUPPORTED_PROTOCOL_VERSIONS,
20
- SERVER_TOMBSTONE_RETENTION_MS,
21
- PROTOCOL_VERSION,
22
- MutationError,
15
+ MAX_BATCH_SIZE,
23
16
  MAX_CLOCK_DRIFT_MS,
24
- MAX_BATCH_SIZE
17
+ MutationError,
18
+ PROTOCOL_VERSION,
19
+ SERVER_TOMBSTONE_RETENTION_MS,
20
+ SUPPORTED_PROTOCOL_VERSIONS,
21
+ TOMBSTONE_RETENTION_MS,
22
+ TransportSendError,
23
+ isErrorReason,
24
+ reasonFromError
25
25
  };