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.
- package/README.md +500 -40
- package/dist/cjs/client/index.cjs +32 -17
- package/dist/cjs/client/index.d.cts +18 -3
- package/dist/cjs/client/storage/indexeddb.d.cts +1 -1
- package/dist/cjs/core/index.cjs +28 -28
- package/dist/cjs/core/index.d.cts +21 -9
- package/dist/cjs/react/index.cjs +48 -33
- package/dist/cjs/react/index.d.cts +23 -5
- package/dist/cjs/server/drizzle.cjs +2 -2
- package/dist/cjs/server/drizzle.d.cts +1 -1
- package/dist/cjs/server/ephemeral/index.cjs +180 -0
- package/dist/cjs/server/ephemeral/index.d.cts +102 -0
- package/dist/cjs/server/ephemeral/redis.cjs +226 -0
- package/dist/cjs/server/ephemeral/redis.d.cts +136 -0
- package/dist/cjs/server/index.cjs +238 -87
- package/dist/cjs/server/index.d.cts +196 -5
- package/dist/cjs/svelte/index.cjs +34 -19
- package/dist/cjs/svelte/index.d.cts +18 -3
- package/dist/cjs/transport/bun-ws.cjs +9 -9
- package/dist/cjs/transport/bun-ws.d.cts +1 -1
- package/dist/cjs/transport/polling.cjs +11 -11
- package/dist/cjs/transport/polling.d.cts +1 -1
- package/dist/cjs/transport/sse.cjs +11 -11
- package/dist/cjs/transport/sse.d.cts +1 -1
- package/dist/cjs/transport/ws.cjs +11 -11
- package/dist/cjs/transport/ws.d.cts +1 -1
- package/dist/cjs/vanilla/index.cjs +34 -19
- package/dist/cjs/vanilla/index.d.cts +18 -3
- package/dist/client/index.d.ts +18 -3
- package/dist/client/index.js +7 -7
- package/dist/client/storage/indexeddb.d.ts +1 -1
- package/dist/client/storage/indexeddb.js +1 -1
- package/dist/core/index.d.ts +21 -9
- package/dist/core/index.js +20 -20
- package/dist/react/index.d.ts +23 -5
- package/dist/react/index.js +23 -23
- package/dist/server/drizzle.d.ts +1 -1
- package/dist/server/drizzle.js +3 -3
- package/dist/server/ephemeral/index.d.ts +102 -0
- package/dist/server/ephemeral/index.js +9 -0
- package/dist/server/ephemeral/redis.d.ts +136 -0
- package/dist/server/ephemeral/redis.js +186 -0
- package/dist/server/index.d.ts +196 -5
- package/dist/server/index.js +184 -163
- package/dist/shared/{esm-ytrd3hbq.js → esm-8qbr4y0d.js} +18 -3
- package/dist/shared/{esm-wkwx6bd9.js → esm-ck88h30s.js} +10 -10
- package/dist/shared/esm-g0marxk7.js +134 -0
- package/dist/svelte/index.d.ts +18 -3
- package/dist/svelte/index.js +9 -9
- package/dist/transport/bun-ws.d.ts +1 -1
- package/dist/transport/bun-ws.js +1 -1
- package/dist/transport/polling.d.ts +1 -1
- package/dist/transport/polling.js +3 -3
- package/dist/transport/sse.d.ts +1 -1
- package/dist/transport/sse.js +3 -3
- package/dist/transport/ws.d.ts +1 -1
- package/dist/transport/ws.js +3 -3
- package/dist/vanilla/index.d.ts +18 -3
- package/dist/vanilla/index.js +9 -9
- package/package.json +66 -44
- /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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1947
|
+
// src/server/ephemeral/memory.ts
|
|
1929
1948
|
var DEFAULT_MAX_ENTRIES = 1e4;
|
|
1930
|
-
function indexKey(room, key,
|
|
1931
|
-
return `${room}\x00${key}\x00${
|
|
1949
|
+
function indexKey(room, key, clientId) {
|
|
1950
|
+
return `${room}\x00${key}\x00${clientId}`;
|
|
1932
1951
|
}
|
|
1933
1952
|
function parseIndexKey(packed) {
|
|
1934
|
-
const [room, key,
|
|
1935
|
-
return { room, key,
|
|
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
|
|
1950
|
-
if (!
|
|
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(
|
|
1980
|
+
keyMap.set(clientId, state);
|
|
1962
1981
|
roomMap.set(key, keyMap);
|
|
1963
1982
|
this.store.set(room, roomMap);
|
|
1964
|
-
if (!
|
|
1983
|
+
if (!existing) {
|
|
1965
1984
|
this.entryCount++;
|
|
1966
|
-
|
|
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
|
-
|
|
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(
|
|
1989
|
-
keyMap.delete(
|
|
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,
|
|
2006
|
-
this.remove(room, key,
|
|
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 [
|
|
2056
|
+
for (const [clientId, state] of keyMap) {
|
|
2016
2057
|
if (state.ttlMs && now - state.updatedAt > state.ttlMs) {
|
|
2017
|
-
toRemove.push({ room, key,
|
|
2058
|
+
toRemove.push({ room, key, clientId });
|
|
2018
2059
|
}
|
|
2019
2060
|
}
|
|
2020
2061
|
}
|
|
2021
2062
|
}
|
|
2022
|
-
for (const { room, key,
|
|
2023
|
-
this.remove(room, key,
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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({
|
|
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
|
-
|
|
3317
|
-
|
|
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 !==
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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;
|