@bod.ee/db 0.7.0
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/settings.local.json +23 -0
- package/.claude/skills/config-file.md +54 -0
- package/.claude/skills/deploying-bod-db.md +29 -0
- package/.claude/skills/developing-bod-db.md +127 -0
- package/.claude/skills/using-bod-db.md +403 -0
- package/CLAUDE.md +110 -0
- package/README.md +252 -0
- package/admin/rules.ts +12 -0
- package/admin/server.ts +523 -0
- package/admin/ui.html +2281 -0
- package/cli.ts +177 -0
- package/client.ts +2 -0
- package/config.ts +20 -0
- package/deploy/.env.example +1 -0
- package/deploy/base.yaml +18 -0
- package/deploy/boddb-logs.yaml +10 -0
- package/deploy/boddb.yaml +10 -0
- package/deploy/demo.html +196 -0
- package/deploy/deploy.ts +32 -0
- package/deploy/prod-logs.config.ts +15 -0
- package/deploy/prod.config.ts +15 -0
- package/index.ts +20 -0
- package/mcp.ts +78 -0
- package/package.json +29 -0
- package/react.ts +1 -0
- package/src/client/BodClient.ts +515 -0
- package/src/react/hooks.ts +121 -0
- package/src/server/BodDB.ts +319 -0
- package/src/server/ExpressionRules.ts +250 -0
- package/src/server/FTSEngine.ts +76 -0
- package/src/server/FileAdapter.ts +116 -0
- package/src/server/MCPAdapter.ts +409 -0
- package/src/server/MQEngine.ts +286 -0
- package/src/server/QueryEngine.ts +45 -0
- package/src/server/RulesEngine.ts +108 -0
- package/src/server/StorageEngine.ts +464 -0
- package/src/server/StreamEngine.ts +320 -0
- package/src/server/SubscriptionEngine.ts +120 -0
- package/src/server/Transport.ts +479 -0
- package/src/server/VectorEngine.ts +115 -0
- package/src/shared/errors.ts +15 -0
- package/src/shared/pathUtils.ts +94 -0
- package/src/shared/protocol.ts +59 -0
- package/src/shared/transforms.ts +99 -0
- package/tests/batch.test.ts +60 -0
- package/tests/bench.ts +205 -0
- package/tests/e2e.test.ts +284 -0
- package/tests/expression-rules.test.ts +114 -0
- package/tests/file-adapter.test.ts +57 -0
- package/tests/fts.test.ts +58 -0
- package/tests/mq-flow.test.ts +204 -0
- package/tests/mq.test.ts +326 -0
- package/tests/push.test.ts +55 -0
- package/tests/query.test.ts +60 -0
- package/tests/rules.test.ts +78 -0
- package/tests/sse.test.ts +78 -0
- package/tests/storage.test.ts +199 -0
- package/tests/stream.test.ts +385 -0
- package/tests/stress.test.ts +202 -0
- package/tests/subscriptions.test.ts +86 -0
- package/tests/transforms.test.ts +92 -0
- package/tests/transport.test.ts +209 -0
- package/tests/ttl.test.ts +70 -0
- package/tests/vector.test.ts +69 -0
- package/tsconfig.json +27 -0
package/cli.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { BodDB } from './index.ts';
|
|
3
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
4
|
+
import { dirname, resolve } from 'path';
|
|
5
|
+
import { cpus, totalmem } from 'os';
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const flags: Record<string, string> = {};
|
|
9
|
+
let configPath: string | undefined;
|
|
10
|
+
|
|
11
|
+
// Parse args
|
|
12
|
+
for (let i = 0; i < args.length; i++) {
|
|
13
|
+
const arg = args[i];
|
|
14
|
+
if (arg === '--help' || arg === '-h') { printHelp(); process.exit(0); }
|
|
15
|
+
if (arg === '--version' || arg === '-v') { printVersion(); process.exit(0); }
|
|
16
|
+
if (arg === '--init') { await initConfig(args[i + 1]); process.exit(0); }
|
|
17
|
+
if (arg.startsWith('--')) {
|
|
18
|
+
const [key, val] = arg.slice(2).split('=');
|
|
19
|
+
const booleanFlags = ['memory'];
|
|
20
|
+
if (val != null) {
|
|
21
|
+
flags[key] = val;
|
|
22
|
+
} else if (booleanFlags.includes(key)) {
|
|
23
|
+
flags[key] = 'true';
|
|
24
|
+
} else {
|
|
25
|
+
flags[key] = args[++i] ?? '';
|
|
26
|
+
}
|
|
27
|
+
} else if (!configPath) {
|
|
28
|
+
configPath = arg;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Load config file
|
|
33
|
+
let options: Record<string, unknown> = {};
|
|
34
|
+
if (configPath) {
|
|
35
|
+
const abs = resolve(configPath);
|
|
36
|
+
if (configPath.endsWith('.ts') || configPath.endsWith('.js')) {
|
|
37
|
+
const mod = await import(abs);
|
|
38
|
+
options = { ...(mod.default ?? mod.config ?? mod) };
|
|
39
|
+
} else if (configPath.endsWith('.json')) {
|
|
40
|
+
const text = require('fs').readFileSync(abs, 'utf-8');
|
|
41
|
+
const parsed = JSON.parse(text);
|
|
42
|
+
options = { ...(parsed.config ?? parsed) };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// CLI flags override config
|
|
47
|
+
if (flags.port) options.port = parseInt(flags.port, 10);
|
|
48
|
+
if (flags.path) options.path = flags.path;
|
|
49
|
+
if (flags.memory) options.path = ':memory:';
|
|
50
|
+
|
|
51
|
+
// Ensure db directory exists
|
|
52
|
+
const dbPath = (options.path as string) ?? ':memory:';
|
|
53
|
+
if (dbPath !== ':memory:') {
|
|
54
|
+
const dir = dirname(resolve(dbPath));
|
|
55
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const db = await BodDB.create(options as any);
|
|
59
|
+
const server = db.serve();
|
|
60
|
+
const port = server?.port ?? options.port ?? 4400;
|
|
61
|
+
|
|
62
|
+
const line = (s: string) => ` │ ${s.padEnd(36)}│`;
|
|
63
|
+
console.log([
|
|
64
|
+
'',
|
|
65
|
+
' ┌──────────────────────────────────────┐',
|
|
66
|
+
line(` BodDB v${getVersion()}`),
|
|
67
|
+
' ├──────────────────────────────────────┤',
|
|
68
|
+
line(` Port: ${port}`),
|
|
69
|
+
line(` DB: ${dbPath === ':memory:' ? ':memory:' : resolve(dbPath)}`),
|
|
70
|
+
line(` Config: ${configPath ?? 'defaults'}`),
|
|
71
|
+
line(''),
|
|
72
|
+
line(` REST: http://localhost:${port}`),
|
|
73
|
+
line(` WS: ws://localhost:${port}`),
|
|
74
|
+
' └──────────────────────────────────────┘',
|
|
75
|
+
'',
|
|
76
|
+
].join('\n'));
|
|
77
|
+
|
|
78
|
+
// Admin stats (for admin UI stats bar)
|
|
79
|
+
if (options.transport?.staticRoutes) {
|
|
80
|
+
const { statSync } = require('fs');
|
|
81
|
+
let lastCpu = process.cpuUsage();
|
|
82
|
+
let lastTime = performance.now();
|
|
83
|
+
let lastOsCpus = cpus();
|
|
84
|
+
const systemCpuPercent = (): number => {
|
|
85
|
+
const cur = cpus();
|
|
86
|
+
let idleDelta = 0, totalDelta = 0;
|
|
87
|
+
for (let i = 0; i < cur.length; i++) {
|
|
88
|
+
const prev = lastOsCpus[i]?.times ?? cur[i].times;
|
|
89
|
+
const c = cur[i].times;
|
|
90
|
+
idleDelta += c.idle - prev.idle;
|
|
91
|
+
totalDelta += (c.user + c.nice + c.sys + c.irq + c.idle) - (prev.user + prev.nice + prev.sys + prev.irq + prev.idle);
|
|
92
|
+
}
|
|
93
|
+
lastOsCpus = cur;
|
|
94
|
+
return totalDelta > 0 ? +((1 - idleDelta / totalDelta) * 100).toFixed(1) : 0;
|
|
95
|
+
};
|
|
96
|
+
setInterval(() => {
|
|
97
|
+
if (!db.subs.subscriberCount('_admin')) return;
|
|
98
|
+
const now = performance.now();
|
|
99
|
+
const cpu = process.cpuUsage();
|
|
100
|
+
const elapsedUs = (now - lastTime) * 1000;
|
|
101
|
+
const cpuPercent = +((cpu.user - lastCpu.user + cpu.system - lastCpu.system) / elapsedUs * 100).toFixed(1);
|
|
102
|
+
lastCpu = cpu; lastTime = now;
|
|
103
|
+
const mem = process.memoryUsage();
|
|
104
|
+
const nodeCount = (db.storage.db.query('SELECT COUNT(*) as n FROM nodes WHERE mq_status IS NULL').get() as any).n;
|
|
105
|
+
let dbSizeMb = 0;
|
|
106
|
+
try { dbSizeMb = +(statSync(resolve(dbPath)).size / 1024 / 1024).toFixed(2); } catch {}
|
|
107
|
+
db.set('_admin/stats', {
|
|
108
|
+
process: { cpuPercent, heapUsedMb: +(mem.heapUsed / 1024 / 1024).toFixed(2), rssMb: +(mem.rss / 1024 / 1024).toFixed(2), uptimeSec: Math.floor(process.uptime()) },
|
|
109
|
+
db: { nodeCount, sizeMb: dbSizeMb },
|
|
110
|
+
system: { cpuCores: cpus().length, totalMemMb: +(totalmem() / 1024 / 1024).toFixed(0), cpuPercent: systemCpuPercent() },
|
|
111
|
+
subs: db.subs.subscriberCount(),
|
|
112
|
+
clients: db.transport?.clientCount ?? 0,
|
|
113
|
+
ts: Date.now(),
|
|
114
|
+
});
|
|
115
|
+
}, 1000);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Graceful shutdown
|
|
119
|
+
process.on('SIGINT', () => { db.close(); process.exit(0); });
|
|
120
|
+
process.on('SIGTERM', () => { db.close(); process.exit(0); });
|
|
121
|
+
|
|
122
|
+
function getVersion(): string {
|
|
123
|
+
try { return require('./package.json').version; } catch { return '0.0.0'; }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function printVersion() {
|
|
127
|
+
console.log(`bod-db v${getVersion()}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function printHelp() {
|
|
131
|
+
console.log(`
|
|
132
|
+
bod-db — embedded reactive database
|
|
133
|
+
|
|
134
|
+
Usage:
|
|
135
|
+
bod-db [config] Start server with config file (.ts/.js/.json)
|
|
136
|
+
bod-db --init [path] Generate a starter config file
|
|
137
|
+
|
|
138
|
+
Options:
|
|
139
|
+
--port <n> Override port (default: 4400)
|
|
140
|
+
--path <file> Override SQLite path
|
|
141
|
+
--memory Use in-memory database
|
|
142
|
+
-v, --version Print version
|
|
143
|
+
-h, --help Show this help
|
|
144
|
+
|
|
145
|
+
Examples:
|
|
146
|
+
bod-db Start with defaults (memory, port 4400)
|
|
147
|
+
bod-db config.ts Start with config file
|
|
148
|
+
bod-db --port 3000 Start on port 3000
|
|
149
|
+
bod-db config.ts --port 5000 Config + port override
|
|
150
|
+
bod-db --init Generate ./bod.config.ts
|
|
151
|
+
`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function initConfig(dest?: string) {
|
|
155
|
+
const target = resolve(dest ?? 'bod.config.ts');
|
|
156
|
+
if (existsSync(target)) {
|
|
157
|
+
console.log(` ✗ ${target} already exists`);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
const template = `import type { BodDBOptions } from 'bod-db';
|
|
161
|
+
|
|
162
|
+
export default {
|
|
163
|
+
path: './.tmp/bod.db',
|
|
164
|
+
port: 4400,
|
|
165
|
+
sweepInterval: 60000,
|
|
166
|
+
rules: {
|
|
167
|
+
'': { read: true, write: true },
|
|
168
|
+
},
|
|
169
|
+
indexes: {},
|
|
170
|
+
fts: {},
|
|
171
|
+
vectors: { dimensions: 384 },
|
|
172
|
+
mq: { visibilityTimeout: 30, maxDeliveries: 5 },
|
|
173
|
+
} satisfies Partial<BodDBOptions>;
|
|
174
|
+
`;
|
|
175
|
+
require('fs').writeFileSync(target, template);
|
|
176
|
+
console.log(` ✓ Created ${target}`);
|
|
177
|
+
}
|
package/client.ts
ADDED
package/config.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { BodDBOptions } from './src/server/BodDB.ts';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
path: './.tmp/bod.db',
|
|
5
|
+
port: 4400,
|
|
6
|
+
sweepInterval: 60000,
|
|
7
|
+
rules: {
|
|
8
|
+
'': { read: true, write: true },
|
|
9
|
+
},
|
|
10
|
+
indexes: {
|
|
11
|
+
'users': ['role', 'createdAt'],
|
|
12
|
+
'posts': ['authorId', 'createdAt', 'likes'],
|
|
13
|
+
},
|
|
14
|
+
fts: {},
|
|
15
|
+
vectors: { dimensions: 384 },
|
|
16
|
+
mq: { visibilityTimeout: 30, maxDeliveries: 5 },
|
|
17
|
+
compact: {
|
|
18
|
+
'events/logs': { maxAge: 86400 },
|
|
19
|
+
},
|
|
20
|
+
} satisfies Partial<BodDBOptions>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
SSH_KEY_PATH=~/.ssh/id_ed25519
|
package/deploy/base.yaml
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
droplet:
|
|
2
|
+
host: 157.151.221.191
|
|
3
|
+
user: ubuntu
|
|
4
|
+
ssh:
|
|
5
|
+
privateKey: ~/.ssh/oracle_cloud
|
|
6
|
+
app:
|
|
7
|
+
user: ubuntu
|
|
8
|
+
runtime:
|
|
9
|
+
host: 0.0.0.0
|
|
10
|
+
nodeEnv: production
|
|
11
|
+
service:
|
|
12
|
+
restart: always
|
|
13
|
+
restartSec: 2
|
|
14
|
+
killSignal: SIGINT
|
|
15
|
+
https:
|
|
16
|
+
email: admin@livshitz.com
|
|
17
|
+
deploy:
|
|
18
|
+
excludes: [.git, node_modules, .tmp, tests, .claude, .cursor, bun.lock, data]
|
package/deploy/demo.html
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>BodDB Remote Demo</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body { font-family: system-ui, sans-serif; background: #0a0a0a; color: #e0e0e0; padding: 2rem; max-width: 800px; margin: 0 auto; }
|
|
10
|
+
h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
|
|
11
|
+
h2 { font-size: 1.1rem; margin: 1.5rem 0 0.5rem; color: #888; }
|
|
12
|
+
.status { padding: 0.5rem 1rem; border-radius: 6px; margin-bottom: 1rem; font-size: 0.9rem; }
|
|
13
|
+
.status.connected { background: #0a2e0a; border: 1px solid #1a5c1a; }
|
|
14
|
+
.status.disconnected { background: #2e0a0a; border: 1px solid #5c1a1a; }
|
|
15
|
+
.status.connecting { background: #2e2e0a; border: 1px solid #5c5c1a; }
|
|
16
|
+
.log { background: #111; border: 1px solid #333; border-radius: 6px; padding: 1rem; max-height: 200px; overflow-y: auto; font-family: monospace; font-size: 0.85rem; line-height: 1.6; }
|
|
17
|
+
.log .time { color: #666; }
|
|
18
|
+
.log .op { color: #6cf; }
|
|
19
|
+
.log .data { color: #8f8; }
|
|
20
|
+
.log .err { color: #f66; }
|
|
21
|
+
.controls { display: flex; gap: 0.5rem; flex-wrap: wrap; margin: 0.5rem 0; }
|
|
22
|
+
button { background: #222; border: 1px solid #444; color: #e0e0e0; padding: 0.4rem 0.8rem; border-radius: 4px; cursor: pointer; font-size: 0.85rem; }
|
|
23
|
+
button:hover { background: #333; }
|
|
24
|
+
input { background: #111; border: 1px solid #333; color: #e0e0e0; padding: 0.4rem 0.6rem; border-radius: 4px; font-size: 0.85rem; font-family: monospace; }
|
|
25
|
+
.row { display: flex; gap: 0.5rem; align-items: center; margin: 0.3rem 0; }
|
|
26
|
+
.live-value { background: #0a1a2e; border: 1px solid #1a3a5c; border-radius: 6px; padding: 1rem; font-family: monospace; font-size: 0.9rem; min-height: 3rem; white-space: pre-wrap; }
|
|
27
|
+
small { color: #666; }
|
|
28
|
+
#url { width: 300px; }
|
|
29
|
+
</style>
|
|
30
|
+
</head>
|
|
31
|
+
<body>
|
|
32
|
+
|
|
33
|
+
<h1>BodDB Remote Demo</h1>
|
|
34
|
+
<div class="row">
|
|
35
|
+
<input id="url" value="wss://db-main.bod.ee" placeholder="wss://...">
|
|
36
|
+
<button onclick="doConnect()">Connect</button>
|
|
37
|
+
<button onclick="doDisconnect()">Disconnect</button>
|
|
38
|
+
</div>
|
|
39
|
+
<div id="status" class="status disconnected">Disconnected</div>
|
|
40
|
+
|
|
41
|
+
<h2>CRUD</h2>
|
|
42
|
+
<div class="controls">
|
|
43
|
+
<button onclick="doSet()">Set demo/hello</button>
|
|
44
|
+
<button onclick="doGet()">Get demo/hello</button>
|
|
45
|
+
<button onclick="doDelete()">Delete demo/hello</button>
|
|
46
|
+
<button onclick="doPush()">Push to demo/logs</button>
|
|
47
|
+
<button onclick="doGetLogs()">Get demo/logs</button>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<h2>Live Subscription <small>(demo/counter)</small></h2>
|
|
51
|
+
<div class="controls">
|
|
52
|
+
<button onclick="doSubscribe()">Subscribe</button>
|
|
53
|
+
<button onclick="doUnsubscribe()">Unsubscribe</button>
|
|
54
|
+
<button onclick="doIncrement()">Increment</button>
|
|
55
|
+
</div>
|
|
56
|
+
<div id="live" class="live-value">—</div>
|
|
57
|
+
|
|
58
|
+
<h2>Event Log</h2>
|
|
59
|
+
<div id="log" class="log"></div>
|
|
60
|
+
|
|
61
|
+
<script>
|
|
62
|
+
let ws = null;
|
|
63
|
+
let msgId = 0;
|
|
64
|
+
let pending = {};
|
|
65
|
+
let subActive = false;
|
|
66
|
+
|
|
67
|
+
function log(op, data, isErr) {
|
|
68
|
+
const el = document.getElementById('log');
|
|
69
|
+
const time = new Date().toLocaleTimeString();
|
|
70
|
+
const cls = isErr ? 'err' : 'data';
|
|
71
|
+
el.innerHTML += `<div><span class="time">${time}</span> <span class="op">[${op}]</span> <span class="${cls}">${typeof data === 'string' ? data : JSON.stringify(data)}</span></div>`;
|
|
72
|
+
el.scrollTop = el.scrollHeight;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function setStatus(s) {
|
|
76
|
+
const el = document.getElementById('status');
|
|
77
|
+
el.textContent = s.charAt(0).toUpperCase() + s.slice(1);
|
|
78
|
+
el.className = 'status ' + s;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function send(op, params) {
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const id = String(++msgId);
|
|
84
|
+
pending[id] = { resolve, reject };
|
|
85
|
+
ws.send(JSON.stringify({ id, op, ...params }));
|
|
86
|
+
setTimeout(() => { if (pending[id]) { delete pending[id]; reject(new Error('timeout')); } }, 10000);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function doConnect() {
|
|
91
|
+
if (ws && ws.readyState === WebSocket.OPEN) return;
|
|
92
|
+
const url = document.getElementById('url').value;
|
|
93
|
+
setStatus('connecting');
|
|
94
|
+
ws = new WebSocket(url);
|
|
95
|
+
ws.onopen = () => { setStatus('connected'); log('connect', 'Connected to ' + url); };
|
|
96
|
+
ws.onclose = () => { setStatus('disconnected'); log('close', 'Disconnected'); };
|
|
97
|
+
ws.onerror = (e) => { log('error', 'WebSocket error', true); };
|
|
98
|
+
ws.onmessage = (e) => {
|
|
99
|
+
const msg = JSON.parse(e.data);
|
|
100
|
+
// subscription event
|
|
101
|
+
if (msg.type === 'value') {
|
|
102
|
+
document.getElementById('live').textContent = JSON.stringify(msg.data, null, 2);
|
|
103
|
+
log('sub:value', { path: msg.path, data: msg.data });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// request response
|
|
107
|
+
if (msg.id && pending[msg.id]) {
|
|
108
|
+
const p = pending[msg.id];
|
|
109
|
+
delete pending[msg.id];
|
|
110
|
+
if (msg.ok) p.resolve(msg.data);
|
|
111
|
+
else p.reject(new Error(msg.error));
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function doDisconnect() { if (ws) { ws.close(); ws = null; subActive = false; } }
|
|
117
|
+
|
|
118
|
+
async function timed(op, fn) {
|
|
119
|
+
const t0 = performance.now();
|
|
120
|
+
try {
|
|
121
|
+
const result = await fn();
|
|
122
|
+
const ms = (performance.now() - t0).toFixed(1);
|
|
123
|
+
return { result, ms };
|
|
124
|
+
} catch (e) {
|
|
125
|
+
const ms = (performance.now() - t0).toFixed(1);
|
|
126
|
+
log(op, `${e.message} (${ms}ms)`, true);
|
|
127
|
+
throw e;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function doSet() {
|
|
132
|
+
try {
|
|
133
|
+
const { ms } = await timed('set', () => send('set', { path: 'demo/hello', value: { message: 'Hello from browser!', ts: Date.now() } }));
|
|
134
|
+
log('set', `demo/hello written (${ms}ms)`);
|
|
135
|
+
} catch (e) {}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function doGet() {
|
|
139
|
+
try {
|
|
140
|
+
const { result, ms } = await timed('get', () => send('get', { path: 'demo/hello' }));
|
|
141
|
+
log('get', { data: result, ms: ms + 'ms' });
|
|
142
|
+
} catch (e) {}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function doDelete() {
|
|
146
|
+
try {
|
|
147
|
+
const { ms } = await timed('delete', () => send('delete', { path: 'demo/hello' }));
|
|
148
|
+
log('delete', `demo/hello deleted (${ms}ms)`);
|
|
149
|
+
} catch (e) {}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function doPush() {
|
|
153
|
+
try {
|
|
154
|
+
const { result, ms } = await timed('push', () => send('push', { path: 'demo/logs', value: { msg: 'event-' + Date.now(), ts: Date.now() } }));
|
|
155
|
+
log('push', `pushed key: ${result} (${ms}ms)`);
|
|
156
|
+
} catch (e) {}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function doGetLogs() {
|
|
160
|
+
try {
|
|
161
|
+
const { result, ms } = await timed('get', () => send('get', { path: 'demo/logs' }));
|
|
162
|
+
log('get', { data: result, ms: ms + 'ms' });
|
|
163
|
+
} catch (e) {}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function doSubscribe() {
|
|
167
|
+
if (subActive) return;
|
|
168
|
+
try {
|
|
169
|
+
const { ms } = await timed('sub', () => send('sub', { path: 'demo/counter', event: 'value' }));
|
|
170
|
+
subActive = true;
|
|
171
|
+
log('sub', `subscribed to demo/counter (${ms}ms)`);
|
|
172
|
+
} catch (e) {}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function doUnsubscribe() {
|
|
176
|
+
if (!subActive) return;
|
|
177
|
+
try {
|
|
178
|
+
const { ms } = await timed('unsub', () => send('unsub', { path: 'demo/counter', event: 'value' }));
|
|
179
|
+
subActive = false;
|
|
180
|
+
document.getElementById('live').textContent = '—';
|
|
181
|
+
log('unsub', `unsubscribed from demo/counter (${ms}ms)`);
|
|
182
|
+
} catch (e) {}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function doIncrement() {
|
|
186
|
+
try {
|
|
187
|
+
const t0 = performance.now();
|
|
188
|
+
const cur = await send('get', { path: 'demo/counter' });
|
|
189
|
+
await send('set', { path: 'demo/counter', value: (typeof cur === 'number' ? cur : 0) + 1 });
|
|
190
|
+
const ms = (performance.now() - t0).toFixed(1);
|
|
191
|
+
log('set', `demo/counter incremented (${ms}ms total)`);
|
|
192
|
+
} catch (e) { log('set', e.message, true); }
|
|
193
|
+
}
|
|
194
|
+
</script>
|
|
195
|
+
</body>
|
|
196
|
+
</html>
|
package/deploy/deploy.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Usage: bun run deploy/deploy.ts <instance> [bootstrap|deploy|logs|ssh|provision]
|
|
2
|
+
// Deep-merges deploy/base.yaml + deploy/<instance>.yaml, writes .tmp/vmdrop.yaml, runs vmdrop
|
|
3
|
+
|
|
4
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
5
|
+
import { parse, stringify } from 'yaml';
|
|
6
|
+
|
|
7
|
+
const [instance, cmd = 'deploy', ...rest] = process.argv.slice(2);
|
|
8
|
+
if (!instance) { console.error('Usage: bun deploy/deploy.ts <instance> [cmd]'); process.exit(1); }
|
|
9
|
+
|
|
10
|
+
const base = parse(readFileSync('deploy/base.yaml', 'utf8'));
|
|
11
|
+
const override = parse(readFileSync(`deploy/${instance}.yaml`, 'utf8'));
|
|
12
|
+
const merged = deepMerge(base, override);
|
|
13
|
+
|
|
14
|
+
mkdirSync('.tmp', { recursive: true });
|
|
15
|
+
writeFileSync('.tmp/vmdrop.yaml', stringify(merged));
|
|
16
|
+
|
|
17
|
+
const proc = Bun.spawn(['bunx', 'vmdrop', cmd, '--config', '.tmp/vmdrop.yaml', ...rest], {
|
|
18
|
+
stdio: ['inherit', 'inherit', 'inherit'],
|
|
19
|
+
});
|
|
20
|
+
await proc.exited;
|
|
21
|
+
process.exit(proc.exitCode ?? 0);
|
|
22
|
+
|
|
23
|
+
function deepMerge(a: any, b: any): any {
|
|
24
|
+
if (!b) return a;
|
|
25
|
+
const result = { ...a };
|
|
26
|
+
for (const key of Object.keys(b)) {
|
|
27
|
+
result[key] = (typeof a[key] === 'object' && typeof b[key] === 'object' && !Array.isArray(b[key]))
|
|
28
|
+
? deepMerge(a[key], b[key])
|
|
29
|
+
: b[key];
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
path: './data/bod.db',
|
|
3
|
+
port: 4401,
|
|
4
|
+
sweepInterval: 60000,
|
|
5
|
+
rules: { '': { read: true, write: true } },
|
|
6
|
+
indexes: {},
|
|
7
|
+
fts: {},
|
|
8
|
+
mq: { visibilityTimeout: 30, maxDeliveries: 5 },
|
|
9
|
+
transport: {
|
|
10
|
+
staticRoutes: {
|
|
11
|
+
'/admin': './admin/ui.html',
|
|
12
|
+
'/': './admin/ui.html',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
path: './data/bod.db',
|
|
3
|
+
port: 4400,
|
|
4
|
+
sweepInterval: 60000,
|
|
5
|
+
rules: { '': { read: true, write: true } },
|
|
6
|
+
indexes: {},
|
|
7
|
+
fts: {},
|
|
8
|
+
mq: { visibilityTimeout: 30, maxDeliveries: 5 },
|
|
9
|
+
transport: {
|
|
10
|
+
staticRoutes: {
|
|
11
|
+
'/admin': './admin/ui.html',
|
|
12
|
+
'/': './admin/ui.html',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { BodDB, BodDBOptions } from './src/server/BodDB.ts';
|
|
2
|
+
export type { TransactionProxy } from './src/server/BodDB.ts';
|
|
3
|
+
export { StorageEngine, StorageEngineOptions, generatePushId } from './src/server/StorageEngine.ts';
|
|
4
|
+
export { SubscriptionEngine } from './src/server/SubscriptionEngine.ts';
|
|
5
|
+
export { QueryEngine } from './src/server/QueryEngine.ts';
|
|
6
|
+
export { RulesEngine, RulesEngineOptions } from './src/server/RulesEngine.ts';
|
|
7
|
+
export type { RuleContext, PathRule } from './src/server/RulesEngine.ts';
|
|
8
|
+
export { Transport, TransportOptions } from './src/server/Transport.ts';
|
|
9
|
+
export { FTSEngine, FTSEngineOptions } from './src/server/FTSEngine.ts';
|
|
10
|
+
export { VectorEngine, VectorEngineOptions } from './src/server/VectorEngine.ts';
|
|
11
|
+
export { StreamEngine, StreamEngineOptions } from './src/server/StreamEngine.ts';
|
|
12
|
+
export { MQEngine, MQEngineOptions } from './src/server/MQEngine.ts';
|
|
13
|
+
export type { MQMessage } from './src/server/MQEngine.ts';
|
|
14
|
+
export type { StreamEvent, CompactOptions, CompactResult, StreamSnapshot } from './src/server/StreamEngine.ts';
|
|
15
|
+
export { FileAdapter, FileAdapterOptions } from './src/server/FileAdapter.ts';
|
|
16
|
+
export { compileRule } from './src/server/ExpressionRules.ts';
|
|
17
|
+
export { increment, serverTimestamp, arrayUnion, arrayRemove, ref } from './src/shared/transforms.ts';
|
|
18
|
+
export * from './src/shared/protocol.ts';
|
|
19
|
+
export * from './src/shared/errors.ts';
|
|
20
|
+
export { normalizePath, validatePath, ancestors, parentPath, pathKey, prefixEnd, flatten, reconstruct } from './src/shared/pathUtils.ts';
|
package/mcp.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { BodClient } from './src/client/BodClient.ts';
|
|
3
|
+
import { MCPAdapter } from './src/server/MCPAdapter.ts';
|
|
4
|
+
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
const flags: Record<string, string> = {};
|
|
7
|
+
|
|
8
|
+
for (let i = 0; i < args.length; i++) {
|
|
9
|
+
const arg = args[i];
|
|
10
|
+
if (arg === '--help' || arg === '-h') { printHelp(); process.exit(0); }
|
|
11
|
+
if (arg.startsWith('--')) {
|
|
12
|
+
const [key, val] = arg.slice(2).split('=');
|
|
13
|
+
const booleanFlags = ['stdio', 'http'];
|
|
14
|
+
if (val != null) flags[key] = val;
|
|
15
|
+
else if (booleanFlags.includes(key)) flags[key] = 'true';
|
|
16
|
+
else flags[key] = args[++i] ?? '';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const url = flags.url ?? `ws://localhost:${flags.port ?? '4400'}`;
|
|
21
|
+
const mode = flags.http ? 'http' : 'stdio';
|
|
22
|
+
|
|
23
|
+
const client = new BodClient({ url });
|
|
24
|
+
try {
|
|
25
|
+
await client.connect();
|
|
26
|
+
} catch (e: any) {
|
|
27
|
+
console.error(`Failed to connect to ${url} — is BodDB running? (${e.message ?? e})`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
console.error(`bod-db MCP connected to ${url}`);
|
|
31
|
+
|
|
32
|
+
const mcp = new MCPAdapter(client);
|
|
33
|
+
|
|
34
|
+
if (mode === 'stdio') {
|
|
35
|
+
mcp.serveStdio();
|
|
36
|
+
} else {
|
|
37
|
+
const httpPort = parseInt(flags['http-port'] ?? flags.port ?? '4401', 10);
|
|
38
|
+
const handler = mcp.httpHandler();
|
|
39
|
+
Bun.serve({ port: httpPort, fetch: handler });
|
|
40
|
+
console.error(`bod-db MCP HTTP on http://localhost:${httpPort}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
process.on('SIGINT', () => process.exit(0));
|
|
44
|
+
process.on('SIGTERM', () => process.exit(0));
|
|
45
|
+
|
|
46
|
+
function printHelp() {
|
|
47
|
+
console.log(`
|
|
48
|
+
bod-db-mcp — MCP client for a running BodDB server
|
|
49
|
+
|
|
50
|
+
Usage:
|
|
51
|
+
bod-db-mcp [options]
|
|
52
|
+
|
|
53
|
+
Transport (pick one):
|
|
54
|
+
--stdio Line-delimited JSON-RPC on stdin/stdout (default)
|
|
55
|
+
--http HTTP POST JSON-RPC server
|
|
56
|
+
|
|
57
|
+
Options:
|
|
58
|
+
--url <ws://...> BodDB server URL (default: ws://localhost:4400)
|
|
59
|
+
--port <n> Shorthand for --url ws://localhost:<n>
|
|
60
|
+
--http-port <n> HTTP listen port when using --http (default: 4401)
|
|
61
|
+
-h, --help Show this help
|
|
62
|
+
|
|
63
|
+
Examples:
|
|
64
|
+
bod-db-mcp --stdio
|
|
65
|
+
bod-db-mcp --stdio --port 4400
|
|
66
|
+
bod-db-mcp --http --http-port 4401
|
|
67
|
+
|
|
68
|
+
Claude Code .mcp.json:
|
|
69
|
+
{
|
|
70
|
+
"mcpServers": {
|
|
71
|
+
"bod-db": {
|
|
72
|
+
"command": "bun",
|
|
73
|
+
"args": ["run", "${import.meta.dir}/mcp.ts", "--stdio"]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
`);
|
|
78
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bod.ee/db",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"module": "index.ts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"bod-db": "cli.ts",
|
|
8
|
+
"bod-db-mcp": "mcp.ts"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@types/bun": "latest",
|
|
12
|
+
"yaml": "^2.8.2"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"admin": "bun --watch run admin/server.ts",
|
|
16
|
+
"serve": "bun run cli.ts",
|
|
17
|
+
"start": "bun run cli.ts config.ts",
|
|
18
|
+
"publish-lib": "bun publish --access public",
|
|
19
|
+
"mcp": "bun run mcp.ts --stdio",
|
|
20
|
+
"deploy": "bun run deploy/deploy.ts boddb deploy",
|
|
21
|
+
"deploy-logs-db": "bun run deploy/deploy.ts boddb-logs ",
|
|
22
|
+
"deploy:bootstrap": "bun run deploy/deploy.ts boddb bootstrap",
|
|
23
|
+
"deploy:logs": "bun run deploy/deploy.ts boddb logs",
|
|
24
|
+
"deploy:ssh": "bun run deploy/deploy.ts boddb ssh"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"typescript": "^5"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/react.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useValue, useChildren, useQuery, useMutation } from './src/react/hooks.ts';
|