@bod.ee/db 0.12.2 → 0.12.6
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/.claude/skills/developing-bod-db.md +1 -1
- package/.claude/skills/using-bod-db.md +19 -1
- package/CLAUDE.md +1 -1
- package/admin/ui.html +7 -6
- package/docs/para-chat-integration.md +7 -6
- package/package.json +1 -1
- package/src/client/BodClient.ts +9 -5
- package/src/server/BodDB.ts +6 -1
- package/src/server/ReplicationEngine.ts +80 -62
- package/src/server/Transport.ts +7 -2
- package/src/shared/logger.ts +40 -0
- package/src/shared/protocol.ts +1 -1
- package/tests/optimization.test.ts +4 -2
- package/tests/repl-load.test.ts +372 -0
- package/tests/repl-stream-bloat.test.ts +65 -35
- package/tests/replication-topology.test.ts +49 -7
- package/tests/replication.test.ts +16 -7
|
@@ -165,7 +165,7 @@ describe('Per-path replication topology', () => {
|
|
|
165
165
|
|
|
166
166
|
// --- Emit filtering ---
|
|
167
167
|
|
|
168
|
-
it('primary-mode path emits to _repl', () => {
|
|
168
|
+
it('primary-mode path emits to _repl', async () => {
|
|
169
169
|
const db = new BodDB({
|
|
170
170
|
path: ':memory:',
|
|
171
171
|
sweepInterval: 0,
|
|
@@ -175,16 +175,17 @@ describe('Per-path replication topology', () => {
|
|
|
175
175
|
},
|
|
176
176
|
});
|
|
177
177
|
instances.push(db);
|
|
178
|
-
db.replication!.start();
|
|
178
|
+
await db.replication!.start();
|
|
179
179
|
|
|
180
180
|
db.set('_vfs/file1', { data: 'hello' });
|
|
181
|
+
await new Promise(r => setTimeout(r, 50));
|
|
181
182
|
const repl = db.get('_repl');
|
|
182
183
|
expect(repl).toBeTruthy();
|
|
183
184
|
const events = Object.values(repl as Record<string, any>);
|
|
184
185
|
expect(events.some((e: any) => e.path === '_vfs/file1')).toBe(true);
|
|
185
186
|
});
|
|
186
187
|
|
|
187
|
-
it('
|
|
188
|
+
it('emitting modes + fallback all emit to _repl', async () => {
|
|
188
189
|
const db = new BodDB({
|
|
189
190
|
path: ':memory:',
|
|
190
191
|
sweepInterval: 0,
|
|
@@ -197,12 +198,15 @@ describe('Per-path replication topology', () => {
|
|
|
197
198
|
},
|
|
198
199
|
});
|
|
199
200
|
instances.push(db);
|
|
200
|
-
db.replication!.start();
|
|
201
|
+
await db.replication!.start();
|
|
201
202
|
|
|
202
203
|
db.set('local/data', { v: 1 });
|
|
203
204
|
db.set('telemetry/t1', { event: 'click' });
|
|
204
205
|
db.set('other/data', { v: 2 });
|
|
205
206
|
|
|
207
|
+
// Wait for deferred setTimeout emits
|
|
208
|
+
await new Promise(r => setTimeout(r, 50));
|
|
209
|
+
|
|
206
210
|
const repl = db.get('_repl');
|
|
207
211
|
expect(repl).toBeTruthy();
|
|
208
212
|
const events = Object.values(repl as Record<string, any>);
|
|
@@ -211,7 +215,43 @@ describe('Per-path replication topology', () => {
|
|
|
211
215
|
expect(events.some((e: any) => e.path === 'other/data')).toBe(true);
|
|
212
216
|
});
|
|
213
217
|
|
|
214
|
-
it('
|
|
218
|
+
it('replica and readonly modes do NOT emit to _repl', async () => {
|
|
219
|
+
const pPort = getPort();
|
|
220
|
+
const primary = createNode({ port: pPort, replication: { role: 'primary' } });
|
|
221
|
+
await primary.replication!.start();
|
|
222
|
+
|
|
223
|
+
const db = new BodDB({
|
|
224
|
+
path: ':memory:',
|
|
225
|
+
sweepInterval: 0,
|
|
226
|
+
replication: {
|
|
227
|
+
role: 'primary',
|
|
228
|
+
primaryUrl: `ws://localhost:${pPort}`,
|
|
229
|
+
paths: [
|
|
230
|
+
{ path: 'cached', mode: 'replica' },
|
|
231
|
+
{ path: 'feeds', mode: 'readonly' },
|
|
232
|
+
{ path: 'local', mode: 'primary' },
|
|
233
|
+
],
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
instances.push(db);
|
|
237
|
+
await db.replication!.start();
|
|
238
|
+
|
|
239
|
+
// Write directly (bypassing transport proxy) to test emit filtering
|
|
240
|
+
db.set('local/data', { v: 1 });
|
|
241
|
+
db.set('cached/data', { v: 2 });
|
|
242
|
+
db.set('feeds/data', { v: 3 });
|
|
243
|
+
|
|
244
|
+
// Wait for deferred emits
|
|
245
|
+
await new Promise(r => setTimeout(r, 50));
|
|
246
|
+
|
|
247
|
+
const repl = db.get('_repl');
|
|
248
|
+
const events = repl ? Object.values(repl as Record<string, any>) : [];
|
|
249
|
+
expect(events.some((e: any) => e.path === 'local/data')).toBe(true);
|
|
250
|
+
expect(events.some((e: any) => e.path === 'cached/data')).toBe(false);
|
|
251
|
+
expect(events.some((e: any) => e.path === 'feeds/data')).toBe(false);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('writeonly path emits', async () => {
|
|
215
255
|
const db = new BodDB({
|
|
216
256
|
path: ':memory:',
|
|
217
257
|
sweepInterval: 0,
|
|
@@ -221,9 +261,10 @@ describe('Per-path replication topology', () => {
|
|
|
221
261
|
},
|
|
222
262
|
});
|
|
223
263
|
instances.push(db);
|
|
224
|
-
db.replication!.start();
|
|
264
|
+
await db.replication!.start();
|
|
225
265
|
|
|
226
266
|
db.set('telemetry/t1', { event: 'click' });
|
|
267
|
+
await new Promise(r => setTimeout(r, 50));
|
|
227
268
|
const repl = db.get('_repl');
|
|
228
269
|
expect(repl).toBeTruthy();
|
|
229
270
|
const events = Object.values(repl as Record<string, any>);
|
|
@@ -796,7 +837,7 @@ describe('Per-path replication topology', () => {
|
|
|
796
837
|
it('sync path written locally AND emitted (end-to-end)', async () => {
|
|
797
838
|
const pPort = getPort();
|
|
798
839
|
const primary = createNode({ port: pPort, replication: { role: 'primary' } });
|
|
799
|
-
primary.replication!.start();
|
|
840
|
+
await primary.replication!.start();
|
|
800
841
|
|
|
801
842
|
const rPort = getPort();
|
|
802
843
|
const replica = createNode({
|
|
@@ -813,6 +854,7 @@ describe('Per-path replication topology', () => {
|
|
|
813
854
|
replica.set('config/theme', 'dark');
|
|
814
855
|
expect(replica.get('config/theme')).toBe('dark');
|
|
815
856
|
|
|
857
|
+
await new Promise(r => setTimeout(r, 50));
|
|
816
858
|
const repl = replica.get('_repl');
|
|
817
859
|
expect(repl).toBeTruthy();
|
|
818
860
|
const events = Object.values(repl as Record<string, any>);
|
|
@@ -8,6 +8,7 @@ let nextPort = 24400 + Math.floor(Math.random() * 1000);
|
|
|
8
8
|
describe('ReplicationEngine', () => {
|
|
9
9
|
const instances: BodDB[] = [];
|
|
10
10
|
const clients: BodClient[] = [];
|
|
11
|
+
const tick = () => new Promise(r => setTimeout(r, 50));
|
|
11
12
|
|
|
12
13
|
afterEach(() => {
|
|
13
14
|
for (const c of clients) c.disconnect();
|
|
@@ -48,7 +49,7 @@ describe('ReplicationEngine', () => {
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
// 1. Primary emits events to _repl on set/delete/update/push
|
|
51
|
-
it('primary emits replication events on set/delete/update/push', () => {
|
|
52
|
+
it('primary emits replication events on set/delete/update/push', async () => {
|
|
52
53
|
const db = new BodDB({
|
|
53
54
|
path: ':memory:',
|
|
54
55
|
sweepInterval: 0,
|
|
@@ -61,6 +62,7 @@ describe('ReplicationEngine', () => {
|
|
|
61
62
|
db.update({ 'users/u2/age': 30 });
|
|
62
63
|
db.delete('users/u1');
|
|
63
64
|
db.push('logs', { msg: 'hello' });
|
|
65
|
+
await tick();
|
|
64
66
|
|
|
65
67
|
const replData = db.get('_repl');
|
|
66
68
|
expect(replData).toBeTruthy();
|
|
@@ -76,7 +78,7 @@ describe('ReplicationEngine', () => {
|
|
|
76
78
|
});
|
|
77
79
|
|
|
78
80
|
// 2. Loop prevention — _replaying flag prevents re-emission
|
|
79
|
-
it('does not emit events when replaying', () => {
|
|
81
|
+
it('does not emit events when replaying', async () => {
|
|
80
82
|
const db = new BodDB({
|
|
81
83
|
path: ':memory:',
|
|
82
84
|
sweepInterval: 0,
|
|
@@ -88,13 +90,14 @@ describe('ReplicationEngine', () => {
|
|
|
88
90
|
db.setReplaying(true);
|
|
89
91
|
db.set('users/u1', { name: 'Test' });
|
|
90
92
|
db.setReplaying(false);
|
|
93
|
+
await tick();
|
|
91
94
|
|
|
92
95
|
const replData = db.get('_repl');
|
|
93
96
|
expect(replData).toBeNull();
|
|
94
97
|
});
|
|
95
98
|
|
|
96
99
|
// 3. Excluded prefixes are not replicated
|
|
97
|
-
it('skips excluded prefixes', () => {
|
|
100
|
+
it('skips excluded prefixes', async () => {
|
|
98
101
|
const db = new BodDB({
|
|
99
102
|
path: ':memory:',
|
|
100
103
|
sweepInterval: 0,
|
|
@@ -105,6 +108,7 @@ describe('ReplicationEngine', () => {
|
|
|
105
108
|
|
|
106
109
|
db.set('internal/config', { foo: 1 });
|
|
107
110
|
db.set('users/u1', { name: 'Eli' });
|
|
111
|
+
await tick();
|
|
108
112
|
|
|
109
113
|
const replData = db.get('_repl') as Record<string, any>;
|
|
110
114
|
const entries = Object.values(replData);
|
|
@@ -160,7 +164,7 @@ describe('ReplicationEngine', () => {
|
|
|
160
164
|
});
|
|
161
165
|
|
|
162
166
|
// 7. Transaction events are replicated
|
|
163
|
-
it('transaction write events are replicated', () => {
|
|
167
|
+
it('transaction write events are replicated', async () => {
|
|
164
168
|
const db = new BodDB({
|
|
165
169
|
path: ':memory:',
|
|
166
170
|
sweepInterval: 0,
|
|
@@ -173,6 +177,7 @@ describe('ReplicationEngine', () => {
|
|
|
173
177
|
tx.set('a/1', 'one');
|
|
174
178
|
tx.set('a/2', 'two');
|
|
175
179
|
});
|
|
180
|
+
await tick();
|
|
176
181
|
|
|
177
182
|
const replData = db.get('_repl') as Record<string, any>;
|
|
178
183
|
const entries = Object.values(replData);
|
|
@@ -211,7 +216,7 @@ describe('ReplicationEngine', () => {
|
|
|
211
216
|
});
|
|
212
217
|
|
|
213
218
|
// 10. Multi-path update exclusion check (all paths checked, not just first)
|
|
214
|
-
it('update with mixed excluded/non-excluded paths emits only non-excluded', () => {
|
|
219
|
+
it('update with mixed excluded/non-excluded paths emits only non-excluded', async () => {
|
|
215
220
|
const db = new BodDB({
|
|
216
221
|
path: ':memory:',
|
|
217
222
|
sweepInterval: 0,
|
|
@@ -221,6 +226,7 @@ describe('ReplicationEngine', () => {
|
|
|
221
226
|
db.replication!.start();
|
|
222
227
|
|
|
223
228
|
db.update({ '_internal/x': 1, 'data/y': 2 });
|
|
229
|
+
await tick();
|
|
224
230
|
|
|
225
231
|
const replData = db.get('_repl') as Record<string, any>;
|
|
226
232
|
const entries = Object.values(replData);
|
|
@@ -229,7 +235,7 @@ describe('ReplicationEngine', () => {
|
|
|
229
235
|
});
|
|
230
236
|
|
|
231
237
|
// 11. Sweep fires delete events for expired paths
|
|
232
|
-
it('sweep expired paths are replicated as deletes', () => {
|
|
238
|
+
it('sweep expired paths are replicated as deletes', async () => {
|
|
233
239
|
const db = new BodDB({
|
|
234
240
|
path: ':memory:',
|
|
235
241
|
sweepInterval: 0,
|
|
@@ -242,6 +248,7 @@ describe('ReplicationEngine', () => {
|
|
|
242
248
|
// Force expiry by manipulating the DB directly
|
|
243
249
|
db.storage.db.prepare('UPDATE nodes SET expires_at = 1 WHERE path LIKE ?').run('sessions/s1%');
|
|
244
250
|
db.sweep();
|
|
251
|
+
await tick();
|
|
245
252
|
|
|
246
253
|
const replData = db.get('_repl') as Record<string, any>;
|
|
247
254
|
const entries = Object.values(replData);
|
|
@@ -251,7 +258,7 @@ describe('ReplicationEngine', () => {
|
|
|
251
258
|
});
|
|
252
259
|
|
|
253
260
|
// 12. Recursion guard — emit doesn't infinite loop
|
|
254
|
-
it('emit does not cause infinite recursion', () => {
|
|
261
|
+
it('emit does not cause infinite recursion', async () => {
|
|
255
262
|
const db = new BodDB({
|
|
256
263
|
path: ':memory:',
|
|
257
264
|
sweepInterval: 0,
|
|
@@ -262,6 +269,7 @@ describe('ReplicationEngine', () => {
|
|
|
262
269
|
|
|
263
270
|
db.set('users/u1', 'test');
|
|
264
271
|
expect(db.get('users/u1')).toBe('test');
|
|
272
|
+
await tick();
|
|
265
273
|
const replData = db.get('_repl') as Record<string, any>;
|
|
266
274
|
expect(Object.values(replData).length).toBe(1);
|
|
267
275
|
});
|
|
@@ -333,6 +341,7 @@ describe('ReplicationEngine', () => {
|
|
|
333
341
|
expect(local.get('ext/users/u1')).toEqual({ name: 'Eli' });
|
|
334
342
|
|
|
335
343
|
local.set('mydata/x', 'hello');
|
|
344
|
+
await tick();
|
|
336
345
|
const replData = local.get('_repl') as Record<string, any>;
|
|
337
346
|
const entries = Object.values(replData);
|
|
338
347
|
expect(entries.some((e: any) => e.path === 'mydata/x')).toBe(true);
|