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
@@ -41,16 +41,16 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
41
41
  // src/core/types.ts
42
42
  var exports_types = {};
43
43
  __export(exports_types, {
44
- reasonFromError: () => reasonFromError,
45
- isErrorReason: () => isErrorReason,
46
- TransportSendError: () => TransportSendError,
47
- TOMBSTONE_RETENTION_MS: () => TOMBSTONE_RETENTION_MS,
48
- SUPPORTED_PROTOCOL_VERSIONS: () => SUPPORTED_PROTOCOL_VERSIONS,
49
- SERVER_TOMBSTONE_RETENTION_MS: () => SERVER_TOMBSTONE_RETENTION_MS,
50
- PROTOCOL_VERSION: () => PROTOCOL_VERSION,
51
- MutationError: () => MutationError,
44
+ MAX_BATCH_SIZE: () => MAX_BATCH_SIZE,
52
45
  MAX_CLOCK_DRIFT_MS: () => MAX_CLOCK_DRIFT_MS,
53
- MAX_BATCH_SIZE: () => MAX_BATCH_SIZE
46
+ MutationError: () => MutationError,
47
+ PROTOCOL_VERSION: () => PROTOCOL_VERSION,
48
+ SERVER_TOMBSTONE_RETENTION_MS: () => SERVER_TOMBSTONE_RETENTION_MS,
49
+ SUPPORTED_PROTOCOL_VERSIONS: () => SUPPORTED_PROTOCOL_VERSIONS,
50
+ TOMBSTONE_RETENTION_MS: () => TOMBSTONE_RETENTION_MS,
51
+ TransportSendError: () => TransportSendError,
52
+ isErrorReason: () => isErrorReason,
53
+ reasonFromError: () => reasonFromError
54
54
  });
55
55
  function isErrorReason(value) {
56
56
  return typeof value === "string" && VALID_ERROR_REASONS.has(value);
@@ -107,8 +107,8 @@ var init_types = __esm(() => {
107
107
  // src/server/drizzle.ts
108
108
  var exports_drizzle = {};
109
109
  __export(exports_drizzle, {
110
- drizzleTxAtomic: () => drizzleTxAtomic,
111
- drizzleTable: () => drizzleTable
110
+ drizzleTable: () => drizzleTable,
111
+ drizzleTxAtomic: () => drizzleTxAtomic
112
112
  });
113
113
  module.exports = __toCommonJS(exports_drizzle);
114
114
  var import_node_module2 = require("node:module");
@@ -481,30 +481,30 @@ var drizzleTxAtomic = {
481
481
  // src/server/index.ts
482
482
  var exports_server = {};
483
483
  __export(exports_server, {
484
- stableStringify: () => stableStringify,
485
- resolveRoomKey: () => resolveRoomKey,
486
- resolveConflict: () => resolveConflict,
487
- processOp: () => processOp,
488
- enforceServerSet: () => enforceServerSet,
489
- enforceReadonly: () => enforceReadonly,
490
- enforceClockDrift: () => enforceClockDrift,
491
- enforceBatchSize: () => enforceBatchSize,
492
- drizzleTxAtomic: () => drizzleTxAtomic,
493
- drizzleTable: () => drizzleTable,
494
- defineTable: () => defineTable,
495
- createSyncServer: () => createSyncServer,
496
- createSqliteStorage: () => createSqliteStorage,
497
- createServer: () => createServer,
498
- createRateLimiter: () => createRateLimiter,
499
- createPostgresStorage: () => createPostgresStorage,
500
- SessionManager: () => SessionManager,
501
- ServerClock: () => ServerClock,
502
- SERVER_HLC_META_KEY: () => SERVER_HLC_META_KEY,
503
- ResultCache: () => ResultCache,
504
- OpProcessor: () => OpProcessor,
505
- MutationError: () => MutationError,
484
+ BroadcastEngine: () => BroadcastEngine,
506
485
  MessageHandler: () => MessageHandler,
507
- BroadcastEngine: () => BroadcastEngine
486
+ MutationError: () => MutationError,
487
+ OpProcessor: () => OpProcessor,
488
+ ResultCache: () => ResultCache,
489
+ SERVER_HLC_META_KEY: () => SERVER_HLC_META_KEY,
490
+ ServerClock: () => ServerClock,
491
+ SessionManager: () => SessionManager,
492
+ createPostgresStorage: () => createPostgresStorage,
493
+ createRateLimiter: () => createRateLimiter,
494
+ createServer: () => createServer,
495
+ createSqliteStorage: () => createSqliteStorage,
496
+ createSyncServer: () => createSyncServer,
497
+ defineTable: () => defineTable,
498
+ drizzleTable: () => drizzleTable,
499
+ drizzleTxAtomic: () => drizzleTxAtomic,
500
+ enforceBatchSize: () => enforceBatchSize,
501
+ enforceClockDrift: () => enforceClockDrift,
502
+ enforceReadonly: () => enforceReadonly,
503
+ enforceServerSet: () => enforceServerSet,
504
+ processOp: () => processOp,
505
+ resolveConflict: () => resolveConflict,
506
+ resolveRoomKey: () => resolveRoomKey,
507
+ stableStringify: () => stableStringify
508
508
  });
509
509
  module.exports = __toCommonJS(exports_server);
510
510
 
@@ -1007,7 +1007,7 @@ class ResultCache {
1007
1007
  for (const row of rows) {
1008
1008
  const rowId = String(row[idField] ?? "");
1009
1009
  if (rowId) {
1010
- newRows.set(rowId, row);
1010
+ newRows.set(rowId, { ...row });
1011
1011
  }
1012
1012
  }
1013
1013
  const diff = this.diff(oldRows, newRows);
@@ -1027,6 +1027,9 @@ class ResultCache {
1027
1027
  }
1028
1028
  return this.diff(oldRows, newRows);
1029
1029
  }
1030
+ evictRow(clientId, queryName, rowId) {
1031
+ this.cache.get(clientId)?.get(queryName)?.delete(rowId);
1032
+ }
1030
1033
  clear(clientId, queryName) {
1031
1034
  this.cache.get(clientId)?.delete(queryName);
1032
1035
  }
@@ -1140,6 +1143,14 @@ class BroadcastEngine {
1140
1143
  clearTimeout(timer);
1141
1144
  }
1142
1145
  }
1146
+ forgetWriterRow(tableName, clientId, rowId) {
1147
+ const affectedQueries = this.deps.tableDependencyIndex.get(tableName);
1148
+ if (!affectedQueries)
1149
+ return;
1150
+ for (const queryName of affectedQueries) {
1151
+ this.deps.resultCache.evictRow(clientId, queryName, rowId);
1152
+ }
1153
+ }
1143
1154
  async broadcastChanges(tableName, excludeClientId, overrideRoomKey) {
1144
1155
  const affectedQueries = this.deps.tableDependencyIndex.get(tableName);
1145
1156
  if (!affectedQueries)
@@ -1798,6 +1809,8 @@ class OpProcessor {
1798
1809
  if (queryReg.options.broadcast === "eager-durable" && this.storage) {
1799
1810
  await this.storage.applyOp(op.table, op.rowId, mergedRow, colClocks, hlcStr, op.op, payload);
1800
1811
  this.recordBatchWrite(batchCtx, op.table, op.rowId, mergedRow, colClocks, hlcStr);
1812
+ } else if (!this.storage) {
1813
+ this.recordBatchWrite(batchCtx, op.table, op.rowId, mergedRow, colClocks, hlcStr);
1801
1814
  } else {
1802
1815
  const pushed = this.eagerBuffer.push(op.table, bufferEntry, queryReg.options.maxBufferSize);
1803
1816
  this.recordBatchWrite(batchCtx, op.table, op.rowId, mergedRow, colClocks, hlcStr);
@@ -1809,6 +1822,9 @@ class OpProcessor {
1809
1822
  }
1810
1823
  }
1811
1824
  }
1825
+ if (op.op === "delete") {
1826
+ this.deps.forgetWriterRow(op.table, session.clientId, op.rowId);
1827
+ }
1812
1828
  const senderSession = this.sessions.get(session.clientId);
1813
1829
  const senderSub = senderSession?.subscriptions.get(op.table);
1814
1830
  const senderRoomKey = senderSub ? senderSub.roomKey : undefined;
@@ -1898,6 +1914,9 @@ class OpProcessor {
1898
1914
  await this.storage.applyOp(op.table, op.rowId, result.resolvedRow, result.updatedColClocks, op.hlc, op.op, result.resolvedRow);
1899
1915
  this.recordBatchWrite(batchCtx, op.table, op.rowId, result.resolvedRow, result.updatedColClocks, op.hlc);
1900
1916
  }
1917
+ if (op.op === "delete") {
1918
+ this.deps.forgetWriterRow(op.table, session.clientId, op.rowId);
1919
+ }
1901
1920
  if (deferredBroadcastTables) {
1902
1921
  deferredBroadcastTables.add(op.table);
1903
1922
  if (mutateResult?.affected) {
@@ -1925,14 +1944,14 @@ class OpProcessor {
1925
1944
  }
1926
1945
  }
1927
1946
 
1928
- // src/server/ephemeral-manager.ts
1947
+ // src/server/ephemeral/memory.ts
1929
1948
  var DEFAULT_MAX_ENTRIES = 1e4;
1930
- function indexKey(room, key, userId) {
1931
- return `${room}\x00${key}\x00${userId}`;
1949
+ function indexKey(room, key, clientId) {
1950
+ return `${room}\x00${key}\x00${clientId}`;
1932
1951
  }
1933
1952
  function parseIndexKey(packed) {
1934
- const [room, key, userId] = packed.split("\x00");
1935
- return { room, key, userId };
1953
+ const [room, key, clientId] = packed.split("\x00");
1954
+ return { room, key, clientId };
1936
1955
  }
1937
1956
 
1938
1957
  class EphemeralManager {
@@ -1946,8 +1965,8 @@ class EphemeralManager {
1946
1965
  set(room, key, clientId, userId, data, ttlMs) {
1947
1966
  const roomMap = this.store.get(room) ?? new Map;
1948
1967
  const keyMap = roomMap.get(key) ?? new Map;
1949
- const isUpdate = keyMap.has(userId);
1950
- if (!isUpdate && this.entryCount >= this.maxEntries) {
1968
+ const existing = keyMap.get(clientId);
1969
+ if (!existing && this.entryCount >= this.maxEntries) {
1951
1970
  return false;
1952
1971
  }
1953
1972
  const state = {
@@ -1958,17 +1977,28 @@ class EphemeralManager {
1958
1977
  updatedAt: Date.now(),
1959
1978
  ttlMs
1960
1979
  };
1961
- keyMap.set(userId, state);
1980
+ keyMap.set(clientId, state);
1962
1981
  roomMap.set(key, keyMap);
1963
1982
  this.store.set(room, roomMap);
1964
- if (!isUpdate) {
1983
+ if (!existing) {
1965
1984
  this.entryCount++;
1966
- const clientSet = this.clientIndex.get(clientId) ?? new Set;
1967
- clientSet.add(indexKey(room, key, userId));
1968
- this.clientIndex.set(clientId, clientSet);
1985
+ this.addToClientIndex(clientId, indexKey(room, key, clientId));
1969
1986
  }
1970
1987
  return true;
1971
1988
  }
1989
+ addToClientIndex(clientId, packed) {
1990
+ const clientSet = this.clientIndex.get(clientId) ?? new Set;
1991
+ clientSet.add(packed);
1992
+ this.clientIndex.set(clientId, clientSet);
1993
+ }
1994
+ removeFromClientIndex(clientId, packed) {
1995
+ const clientSet = this.clientIndex.get(clientId);
1996
+ if (!clientSet)
1997
+ return;
1998
+ clientSet.delete(packed);
1999
+ if (clientSet.size === 0)
2000
+ this.clientIndex.delete(clientId);
2001
+ }
1972
2002
  get(room, key) {
1973
2003
  const roomMap = this.store.get(room);
1974
2004
  if (!roomMap)
@@ -1978,16 +2008,27 @@ class EphemeralManager {
1978
2008
  return {};
1979
2009
  return Object.fromEntries(keyMap.entries());
1980
2010
  }
1981
- remove(room, key, userId) {
2011
+ getRoom(room) {
2012
+ const roomMap = this.store.get(room);
2013
+ if (!roomMap)
2014
+ return {};
2015
+ const out = {};
2016
+ for (const [key, keyMap] of roomMap) {
2017
+ out[key] = Object.fromEntries(keyMap.entries());
2018
+ }
2019
+ return out;
2020
+ }
2021
+ remove(room, key, clientId) {
1982
2022
  const roomMap = this.store.get(room);
1983
2023
  if (!roomMap)
1984
2024
  return;
1985
2025
  const keyMap = roomMap.get(key);
1986
2026
  if (!keyMap)
1987
2027
  return;
1988
- if (keyMap.has(userId)) {
1989
- keyMap.delete(userId);
2028
+ if (keyMap.has(clientId)) {
2029
+ keyMap.delete(clientId);
1990
2030
  this.entryCount--;
2031
+ this.removeFromClientIndex(clientId, indexKey(room, key, clientId));
1991
2032
  }
1992
2033
  if (keyMap.size === 0) {
1993
2034
  roomMap.delete(key);
@@ -2002,8 +2043,8 @@ class EphemeralManager {
2002
2043
  return;
2003
2044
  const entries = [...indices];
2004
2045
  for (const packed of entries) {
2005
- const { room, key, userId } = parseIndexKey(packed);
2006
- this.remove(room, key, userId);
2046
+ const { room, key, clientId: owner } = parseIndexKey(packed);
2047
+ this.remove(room, key, owner);
2007
2048
  }
2008
2049
  this.clientIndex.delete(clientId);
2009
2050
  }
@@ -2012,25 +2053,18 @@ class EphemeralManager {
2012
2053
  const toRemove = [];
2013
2054
  for (const [room, roomMap] of this.store) {
2014
2055
  for (const [key, keyMap] of roomMap) {
2015
- for (const [userId, state] of keyMap) {
2056
+ for (const [clientId, state] of keyMap) {
2016
2057
  if (state.ttlMs && now - state.updatedAt > state.ttlMs) {
2017
- toRemove.push({ room, key, userId, clientId: state.clientId });
2058
+ toRemove.push({ room, key, clientId });
2018
2059
  }
2019
2060
  }
2020
2061
  }
2021
2062
  }
2022
- for (const { room, key, userId, clientId } of toRemove) {
2023
- this.remove(room, key, userId);
2024
- const clientSet = this.clientIndex.get(clientId);
2025
- if (clientSet) {
2026
- clientSet.delete(indexKey(room, key, userId));
2027
- if (clientSet.size === 0) {
2028
- this.clientIndex.delete(clientId);
2029
- }
2030
- }
2063
+ for (const { room, key, clientId } of toRemove) {
2064
+ this.remove(room, key, clientId);
2031
2065
  }
2032
2066
  }
2033
- get size() {
2067
+ size() {
2034
2068
  return this.entryCount;
2035
2069
  }
2036
2070
  destroy() {
@@ -2039,6 +2073,9 @@ class EphemeralManager {
2039
2073
  this.entryCount = 0;
2040
2074
  }
2041
2075
  }
2076
+ function createMemoryEphemeral(maxEntries) {
2077
+ return new EphemeralManager(maxEntries);
2078
+ }
2042
2079
 
2043
2080
  // src/server/eager-buffer.ts
2044
2081
  var DEFAULT_FLUSH_INTERVAL = 200;
@@ -2483,6 +2520,7 @@ class MessageHandler {
2483
2520
  broadcast;
2484
2521
  ops;
2485
2522
  ephemeralManager = new EphemeralManager;
2523
+ ephemeralBusReady = false;
2486
2524
  ephemeralCleanupTimer = null;
2487
2525
  eagerBuffer = new EagerBuffer;
2488
2526
  replay;
@@ -2553,7 +2591,8 @@ class MessageHandler {
2553
2591
  stampHlc: () => this.clock.stamp(),
2554
2592
  receiveClientHlc: (hlc) => this.clock.receive(hlc),
2555
2593
  send: (clientId, message) => this.transport.send(clientId, message),
2556
- broadcastChanges: (table, excludeClientId) => this.broadcastChanges(table, excludeClientId)
2594
+ broadcastChanges: (table, excludeClientId) => this.broadcastChanges(table, excludeClientId),
2595
+ forgetWriterRow: (table, clientId, rowId) => this.broadcast.forgetWriterRow(table, clientId, rowId)
2557
2596
  });
2558
2597
  this.transport.onConnect((clientId) => {
2559
2598
  this.sessions.connect(clientId);
@@ -2564,7 +2603,7 @@ class MessageHandler {
2564
2603
  this.messageQueues.delete(clientId);
2565
2604
  this.messageQueueDepths.delete(clientId);
2566
2605
  this.resultCache.clearClient(clientId);
2567
- this.ephemeralManager.removeClient(clientId);
2606
+ this.forgetEphemeralClient(clientId);
2568
2607
  this.ephemeralBuckets.delete(clientId);
2569
2608
  this.emit({ type: "client_disconnected", clientId });
2570
2609
  });
@@ -2605,7 +2644,9 @@ class MessageHandler {
2605
2644
  this.messageQueues.set(clientId, next);
2606
2645
  });
2607
2646
  this.ephemeralCleanupTimer = setInterval(() => {
2608
- this.ephemeralManager.cleanupExpired();
2647
+ Promise.resolve(this.ephemeralManager.cleanupExpired()).catch((err) => {
2648
+ console.error("[reflectdb] ephemeral cleanup failed:", err);
2649
+ });
2609
2650
  }, 5000);
2610
2651
  this.eagerBuffer.setOnFlushError((err, op) => {
2611
2652
  this.emit({
@@ -2639,8 +2680,10 @@ class MessageHandler {
2639
2680
  }
2640
2681
  queryNames.add(name);
2641
2682
  }
2642
- if (registration.options.broadcast === "eager") {
2683
+ if (registration.options.broadcast === "eager" || registration.options.broadcast === "eager-durable") {
2643
2684
  this.eagerBuffer.start(registration.options.flushInterval);
2685
+ }
2686
+ if (registration.options.broadcast === "eager") {
2644
2687
  if (!MessageHandler.eagerWarned) {
2645
2688
  MessageHandler.eagerWarned = true;
2646
2689
  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.`);
@@ -2662,6 +2705,26 @@ class MessageHandler {
2662
2705
  setMaxEphemeralEntries(max) {
2663
2706
  this.ephemeralManager = new EphemeralManager(max);
2664
2707
  }
2708
+ setEphemeralAdapter(adapter) {
2709
+ this.ephemeralManager = adapter;
2710
+ if (!adapter.subscribe || this.ephemeralBusReady)
2711
+ return;
2712
+ this.ephemeralBusReady = true;
2713
+ Promise.resolve(adapter.subscribe((event) => {
2714
+ this.deliverRemoteEphemeral(event).catch((err) => {
2715
+ console.error("[reflectdb] remote ephemeral delivery failed:", err);
2716
+ });
2717
+ })).catch((err) => {
2718
+ console.error("[reflectdb] ephemeral bus subscribe failed:", err);
2719
+ });
2720
+ }
2721
+ async forgetEphemeralClient(clientId) {
2722
+ try {
2723
+ await this.ephemeralManager.removeClient(clientId);
2724
+ } catch (err) {
2725
+ console.error("[reflectdb] ephemeral client cleanup failed:", err);
2726
+ }
2727
+ }
2665
2728
  setMinSchemaVersion(version) {
2666
2729
  this.minSchemaVersion = version;
2667
2730
  }
@@ -2686,7 +2749,7 @@ class MessageHandler {
2686
2749
  this.messageQueues.delete(oldest.clientId);
2687
2750
  this.messageQueueDepths.delete(oldest.clientId);
2688
2751
  this.resultCache.clearClient(oldest.clientId);
2689
- this.ephemeralManager.removeClient(oldest.clientId);
2752
+ await this.forgetEphemeralClient(oldest.clientId);
2690
2753
  this.ephemeralBuckets.delete(oldest.clientId);
2691
2754
  }
2692
2755
  }
@@ -3189,6 +3252,9 @@ class MessageHandler {
3189
3252
  }
3190
3253
  }
3191
3254
  this.sessions.subscribe(clientId, message.table, message.params ?? {}, queryReg.options, roomKey, message.window ?? null);
3255
+ if (roomKey) {
3256
+ await this.sendPresenceSnapshot(clientId, roomKey);
3257
+ }
3192
3258
  }
3193
3259
  async handleLoadMore(clientId, message) {
3194
3260
  const session = this.sessions.get(clientId);
@@ -3302,21 +3368,45 @@ class MessageHandler {
3302
3368
  data,
3303
3369
  ttlMs
3304
3370
  };
3305
- const sent = new Set;
3306
- const recipients = [];
3307
3371
  for (const roomKey of roomKeys) {
3308
- const accepted = this.ephemeralManager.set(roomKey, key, clientId, userId, data, ttlMs);
3372
+ const accepted = await this.ephemeralManager.set(roomKey, key, clientId, userId, data, ttlMs);
3309
3373
  if (!accepted) {
3310
- this.emit({ type: "ephemeral_full", currentSize: this.ephemeralManager.size });
3374
+ this.emit({
3375
+ type: "ephemeral_full",
3376
+ currentSize: await this.ephemeralManager.size()
3377
+ });
3311
3378
  return;
3312
3379
  }
3313
3380
  }
3381
+ const targets = [];
3314
3382
  for (const queryName of queryNames) {
3315
3383
  const senderSub = session.subscriptions.get(queryName);
3316
- const senderRoomKey = senderSub?.roomKey ?? null;
3317
- const subscribers = this.sessions.getSubscribersForQuery(queryName, senderRoomKey);
3384
+ targets.push({ query: queryName, room: senderSub?.roomKey ?? null });
3385
+ }
3386
+ await this.fanOutEphemeral(event, targets, clientId);
3387
+ if (this.ephemeralManager.publish) {
3388
+ try {
3389
+ await this.ephemeralManager.publish({
3390
+ serverId: this.serverId,
3391
+ key,
3392
+ clientId,
3393
+ userId,
3394
+ data,
3395
+ ttlMs,
3396
+ targets
3397
+ });
3398
+ } catch (err) {
3399
+ console.error("[reflectdb] ephemeral publish failed:", err);
3400
+ }
3401
+ }
3402
+ }
3403
+ async fanOutEphemeral(event, targets, exclude) {
3404
+ const sent = new Set;
3405
+ const recipients = [];
3406
+ for (const target of targets) {
3407
+ const subscribers = this.sessions.getSubscribersForQuery(target.query, target.room);
3318
3408
  for (const sub of subscribers) {
3319
- if (sub.clientId !== clientId && !sent.has(sub.clientId)) {
3409
+ if (sub.clientId !== exclude && !sent.has(sub.clientId)) {
3320
3410
  sent.add(sub.clientId);
3321
3411
  recipients.push(sub.clientId);
3322
3412
  }
@@ -3324,6 +3414,41 @@ class MessageHandler {
3324
3414
  }
3325
3415
  await Promise.all(recipients.map((recipient) => this.trySend(recipient, event)));
3326
3416
  }
3417
+ async deliverRemoteEphemeral(remote) {
3418
+ if (remote.serverId === this.serverId)
3419
+ return;
3420
+ await this.fanOutEphemeral({
3421
+ type: "ephemeral",
3422
+ key: remote.key,
3423
+ clientId: remote.clientId,
3424
+ userId: remote.userId,
3425
+ data: remote.data,
3426
+ ttlMs: remote.ttlMs
3427
+ }, remote.targets);
3428
+ }
3429
+ async sendPresenceSnapshot(clientId, roomKey) {
3430
+ let channels;
3431
+ try {
3432
+ channels = await this.ephemeralManager.getRoom(roomKey);
3433
+ } catch (err) {
3434
+ console.error("[reflectdb] presence snapshot failed:", err);
3435
+ return;
3436
+ }
3437
+ for (const byClient of Object.values(channels)) {
3438
+ for (const state of Object.values(byClient)) {
3439
+ if (state.clientId === clientId)
3440
+ continue;
3441
+ await this.trySend(clientId, {
3442
+ type: "ephemeral",
3443
+ key: state.key,
3444
+ clientId: state.clientId,
3445
+ userId: state.userId,
3446
+ data: state.data,
3447
+ ttlMs: state.ttlMs
3448
+ });
3449
+ }
3450
+ }
3451
+ }
3327
3452
  async reserveOpId(opId) {
3328
3453
  return this.replay.reserve(opId);
3329
3454
  }
@@ -3430,7 +3555,7 @@ class MessageHandler {
3430
3555
  }
3431
3556
  this.messageQueues.clear();
3432
3557
  this.messageQueueDepths.clear();
3433
- this.ephemeralManager.destroy();
3558
+ Promise.resolve(this.ephemeralManager.destroy()).catch(() => {});
3434
3559
  await this.eagerBuffer.close();
3435
3560
  }
3436
3561
  }
@@ -3505,6 +3630,12 @@ function createServer(config) {
3505
3630
  if (config.storage) {
3506
3631
  handler.setStorage(config.storage);
3507
3632
  }
3633
+ if (config.ephemeral?.maxEntries !== undefined) {
3634
+ handler.setMaxEphemeralEntries(config.ephemeral.maxEntries);
3635
+ }
3636
+ if (config.ephemeral?.adapter) {
3637
+ handler.setEphemeralAdapter(config.ephemeral.adapter);
3638
+ }
3508
3639
  let compactionConfig = null;
3509
3640
  let pollTimer = null;
3510
3641
  const lockChains = new Map;
@@ -3737,7 +3868,7 @@ function createServer(config) {
3737
3868
  return server;
3738
3869
  }
3739
3870
  function registerHotDispose(fn) {
3740
- if (true) {
3871
+ if (typeof {} !== "undefined") {
3741
3872
  const hot = {}.hot;
3742
3873
  if (hot && typeof hot.dispose === "function") {
3743
3874
  hot.dispose(fn);
@@ -3778,6 +3909,13 @@ function loadDatabase() {
3778
3909
  throw new Error("createSqliteStorage requires the Bun runtime (bun:sqlite is unavailable). Use createPostgresStorage on Node.");
3779
3910
  }
3780
3911
  }
3912
+ function withStatement(statement, use) {
3913
+ try {
3914
+ return use(statement);
3915
+ } finally {
3916
+ statement.finalize?.();
3917
+ }
3918
+ }
3781
3919
  var OP_ID_RETENTION_MS = 24 * 60 * 60 * 1000;
3782
3920
  function createSqliteStorage(config = {}) {
3783
3921
  const db = config.db ?? new (loadDatabase())(config.path ?? ":memory:");
@@ -3835,6 +3973,18 @@ function createSqliteStorage(config = {}) {
3835
3973
  const cleanupOpsStmt = db.prepare("DELETE FROM _reflectdb_processed_ops WHERE created_at < ?");
3836
3974
  const getMetaStmt = db.prepare("SELECT value FROM _reflectdb_meta WHERE key = ?");
3837
3975
  const setMetaStmt = db.prepare("INSERT OR REPLACE INTO _reflectdb_meta (key, value) VALUES (?, ?)");
3976
+ const persistentStatements = [
3977
+ getRowStmt,
3978
+ putRowStmt,
3979
+ deleteRowStmt,
3980
+ getRowsStmt,
3981
+ appendOpStmt,
3982
+ deleteOpsStmt,
3983
+ reserveOpStmt,
3984
+ cleanupOpsStmt,
3985
+ getMetaStmt,
3986
+ setMetaStmt
3987
+ ];
3838
3988
  return {
3839
3989
  async getRow(table, rowId) {
3840
3990
  const result = getRowStmt.get(table, rowId);
@@ -3851,9 +4001,9 @@ function createSqliteStorage(config = {}) {
3851
4001
  if (rowIds.length === 0)
3852
4002
  return {};
3853
4003
  const placeholders = rowIds.map(() => "?").join(", ");
3854
- const stmt = db.prepare(`SELECT row_id, data, col_clocks, hlc FROM _reflectdb_rows WHERE tbl = ? AND row_id IN (${placeholders})`);
4004
+ 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));
3855
4005
  const out = {};
3856
- for (const r of stmt.all(table, ...rowIds)) {
4006
+ for (const r of results) {
3857
4007
  out[r.row_id] = {
3858
4008
  row: JSON.parse(r.data),
3859
4009
  rowHlc: r.hlc,
@@ -3898,8 +4048,7 @@ function createSqliteStorage(config = {}) {
3898
4048
  if (tables.length === 0)
3899
4049
  return [];
3900
4050
  const placeholders = tables.map(() => "?").join(", ");
3901
- 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`);
3902
- const results = stmt.all(since, ...tables);
4051
+ 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));
3903
4052
  return results.map((r) => ({
3904
4053
  table: r.tbl,
3905
4054
  op: r.op,
@@ -3913,15 +4062,13 @@ function createSqliteStorage(config = {}) {
3913
4062
  if (tables.length === 0)
3914
4063
  return [];
3915
4064
  const placeholders = tables.map(() => "?").join(", ");
3916
- const stmt = db.prepare(`SELECT DISTINCT tbl FROM _reflectdb_oplog WHERE hlc > ? AND tbl IN (${placeholders})`);
3917
- return stmt.all(since, ...tables).map((r) => r.tbl);
4065
+ 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));
3918
4066
  },
3919
4067
  async getOplogHead(tables) {
3920
4068
  if (tables.length === 0)
3921
4069
  return null;
3922
4070
  const placeholders = tables.map(() => "?").join(", ");
3923
- const stmt = db.prepare(`SELECT MAX(hlc) AS head FROM _reflectdb_oplog WHERE tbl IN (${placeholders})`);
3924
- return stmt.get(...tables)?.head ?? null;
4071
+ return withStatement(db.prepare(`SELECT MAX(hlc) AS head FROM _reflectdb_oplog WHERE tbl IN (${placeholders})`), (stmt) => stmt.get(...tables)?.head ?? null);
3925
4072
  },
3926
4073
  async deleteOpsBefore(hlc) {
3927
4074
  const result = deleteOpsStmt.run(hlc);
@@ -3973,6 +4120,9 @@ function createSqliteStorage(config = {}) {
3973
4120
  db.run("DELETE FROM _reflectdb_meta WHERE key = ?", [`lock:${key}`]);
3974
4121
  },
3975
4122
  close() {
4123
+ for (const statement of persistentStatements) {
4124
+ statement.finalize?.();
4125
+ }
3976
4126
  db.close();
3977
4127
  }
3978
4128
  };
@@ -4246,7 +4396,8 @@ function createSyncServer(config) {
4246
4396
  allowAnonymous: config.allowAnonymous,
4247
4397
  queryTimeoutMs: config.queryTimeoutMs,
4248
4398
  maxBroadcastConcurrency: config.maxBroadcastConcurrency,
4249
- txAtomic: config.txAtomic
4399
+ txAtomic: config.txAtomic,
4400
+ ephemeral: config.ephemeral
4250
4401
  });
4251
4402
  const implementations = new Map;
4252
4403
  let authCallback = null;