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
package/dist/server/index.js
CHANGED
|
@@ -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-
|
|
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.
|
|
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.
|
|
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({
|
|
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
|
-
|
|
2768
|
-
|
|
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 !==
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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-
|
|
13
|
+
import"./esm-k7kedp3y.js";
|
|
14
14
|
export {
|
|
15
|
-
|
|
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
|
-
|
|
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
|
};
|