pluresdb 1.0.1
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/LICENSE +72 -0
- package/README.md +322 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +253 -0
- package/dist/cli.js.map +1 -0
- package/dist/node-index.d.ts +52 -0
- package/dist/node-index.d.ts.map +1 -0
- package/dist/node-index.js +359 -0
- package/dist/node-index.js.map +1 -0
- package/dist/node-wrapper.d.ts +44 -0
- package/dist/node-wrapper.d.ts.map +1 -0
- package/dist/node-wrapper.js +294 -0
- package/dist/node-wrapper.js.map +1 -0
- package/dist/types/index.d.ts +28 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/node-types.d.ts +59 -0
- package/dist/types/node-types.d.ts.map +1 -0
- package/dist/types/node-types.js +6 -0
- package/dist/types/node-types.js.map +1 -0
- package/dist/vscode/extension.d.ts +81 -0
- package/dist/vscode/extension.d.ts.map +1 -0
- package/dist/vscode/extension.js +309 -0
- package/dist/vscode/extension.js.map +1 -0
- package/examples/basic-usage.d.ts +2 -0
- package/examples/basic-usage.d.ts.map +1 -0
- package/examples/basic-usage.js +26 -0
- package/examples/basic-usage.js.map +1 -0
- package/examples/basic-usage.ts +29 -0
- package/examples/vscode-extension-example/README.md +95 -0
- package/examples/vscode-extension-example/package.json +49 -0
- package/examples/vscode-extension-example/src/extension.ts +163 -0
- package/examples/vscode-extension-example/tsconfig.json +12 -0
- package/examples/vscode-extension-integration.d.ts +24 -0
- package/examples/vscode-extension-integration.d.ts.map +1 -0
- package/examples/vscode-extension-integration.js +285 -0
- package/examples/vscode-extension-integration.js.map +1 -0
- package/examples/vscode-extension-integration.ts +41 -0
- package/package.json +115 -0
- package/scripts/compiled-crud-verify.ts +28 -0
- package/scripts/dogfood.ts +258 -0
- package/scripts/postinstall.js +155 -0
- package/scripts/run-tests.ts +175 -0
- package/scripts/setup-libclang.ps1 +209 -0
- package/src/benchmarks/memory-benchmarks.ts +316 -0
- package/src/benchmarks/run-benchmarks.ts +293 -0
- package/src/cli.ts +231 -0
- package/src/config.ts +49 -0
- package/src/core/crdt.ts +104 -0
- package/src/core/database.ts +494 -0
- package/src/healthcheck.ts +156 -0
- package/src/http/api-server.ts +334 -0
- package/src/index.ts +28 -0
- package/src/logic/rules.ts +44 -0
- package/src/main.rs +3 -0
- package/src/main.ts +190 -0
- package/src/network/websocket-server.ts +115 -0
- package/src/node-index.ts +385 -0
- package/src/node-wrapper.ts +320 -0
- package/src/sqlite-compat.ts +586 -0
- package/src/sqlite3-compat.ts +55 -0
- package/src/storage/kv-storage.ts +71 -0
- package/src/tests/core.test.ts +281 -0
- package/src/tests/fixtures/performance-data.json +71 -0
- package/src/tests/fixtures/test-data.json +124 -0
- package/src/tests/integration/api-server.test.ts +232 -0
- package/src/tests/integration/mesh-network.test.ts +297 -0
- package/src/tests/logic.test.ts +30 -0
- package/src/tests/performance/load.test.ts +288 -0
- package/src/tests/security/input-validation.test.ts +282 -0
- package/src/tests/unit/core.test.ts +216 -0
- package/src/tests/unit/subscriptions.test.ts +135 -0
- package/src/tests/unit/vector-search.test.ts +173 -0
- package/src/tests/vscode_extension_test.ts +253 -0
- package/src/types/index.ts +32 -0
- package/src/types/node-types.ts +66 -0
- package/src/util/debug.ts +14 -0
- package/src/vector/index.ts +59 -0
- package/src/vscode/extension.ts +364 -0
- package/web/README.md +27 -0
- package/web/svelte/package.json +31 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { GunDB } from "../core/database.ts";
|
|
2
|
+
import { loadConfig, saveConfig } from "../config.ts";
|
|
3
|
+
|
|
4
|
+
export interface ApiServerHandle {
|
|
5
|
+
url: string;
|
|
6
|
+
close: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const STATIC_ROOT = new URL("../../web/svelte/dist/", import.meta.url);
|
|
10
|
+
|
|
11
|
+
function corsHeaders(extra?: Record<string, string>): Headers {
|
|
12
|
+
const headers = new Headers(extra);
|
|
13
|
+
headers.set("Access-Control-Allow-Origin", "*");
|
|
14
|
+
headers.set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
|
|
15
|
+
headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
16
|
+
headers.set("Vary", "Origin");
|
|
17
|
+
return headers;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function startApiServer(opts: { port: number; db: GunDB }): ApiServerHandle {
|
|
21
|
+
const { port, db } = opts;
|
|
22
|
+
|
|
23
|
+
const handler = async (req: Request): Promise<Response> => {
|
|
24
|
+
try {
|
|
25
|
+
const url = new URL(req.url);
|
|
26
|
+
const path = url.pathname;
|
|
27
|
+
|
|
28
|
+
if (req.method === "OPTIONS") {
|
|
29
|
+
return new Response(null, { status: 200, headers: corsHeaders() });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (path === "/api/events") {
|
|
33
|
+
const stream = new ReadableStream({
|
|
34
|
+
start(controller) {
|
|
35
|
+
const enc = new TextEncoder();
|
|
36
|
+
const send = (evt: { id: string; node: unknown | null }) => {
|
|
37
|
+
const line = `data: ${JSON.stringify(evt)}\n\n`;
|
|
38
|
+
controller.enqueue(enc.encode(line));
|
|
39
|
+
};
|
|
40
|
+
const cb = (e: { id: string; node: unknown | null }) => send(e);
|
|
41
|
+
db.onAny(cb as any);
|
|
42
|
+
(async () => {
|
|
43
|
+
for await (const n of db.list()) send({ id: n.id, node: { id: n.id, data: n.data } });
|
|
44
|
+
})();
|
|
45
|
+
return () => db.offAny(cb as any);
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
return new Response(stream, {
|
|
49
|
+
headers: corsHeaders({ "content-type": "text/event-stream" }),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (path.startsWith("/api/nodes/")) {
|
|
54
|
+
const id = decodeURIComponent(path.slice("/api/nodes/".length));
|
|
55
|
+
if (req.method === "GET") {
|
|
56
|
+
const val = await db.get<Record<string, unknown>>(id);
|
|
57
|
+
if (!val) return json({ error: "not found" }, 404);
|
|
58
|
+
return json(val);
|
|
59
|
+
}
|
|
60
|
+
if (req.method === "PUT") {
|
|
61
|
+
const body = await req.json().catch(() => null);
|
|
62
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
63
|
+
return json({ error: "invalid json" }, 400);
|
|
64
|
+
}
|
|
65
|
+
await db.put(id, body as Record<string, unknown>);
|
|
66
|
+
return json({ ok: true });
|
|
67
|
+
}
|
|
68
|
+
if (req.method === "DELETE") {
|
|
69
|
+
await db.delete(id);
|
|
70
|
+
return json({ ok: true });
|
|
71
|
+
}
|
|
72
|
+
return json({ error: "method" }, 405);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (path.startsWith("/api/")) {
|
|
76
|
+
switch (path) {
|
|
77
|
+
case "/api/config": {
|
|
78
|
+
if (req.method === "GET") {
|
|
79
|
+
const cfg = await loadConfig();
|
|
80
|
+
return json(cfg);
|
|
81
|
+
}
|
|
82
|
+
if (req.method === "POST") {
|
|
83
|
+
const body = (await req.json().catch(() => null)) as Record<string, unknown> | null;
|
|
84
|
+
if (!body) return json({ error: "missing body" }, 400);
|
|
85
|
+
const current = await loadConfig();
|
|
86
|
+
const next = { ...current, ...body } as Record<string, unknown>;
|
|
87
|
+
await saveConfig(next);
|
|
88
|
+
return json({ ok: true });
|
|
89
|
+
}
|
|
90
|
+
return json({ error: "method" }, 405);
|
|
91
|
+
}
|
|
92
|
+
case "/api/get": {
|
|
93
|
+
const id = url.searchParams.get("id");
|
|
94
|
+
if (!id) return json({ error: "missing id" }, 400);
|
|
95
|
+
const val = await db.get<Record<string, unknown>>(id);
|
|
96
|
+
return json(val);
|
|
97
|
+
}
|
|
98
|
+
case "/api/put": {
|
|
99
|
+
if (req.method !== "POST") return json({ error: "method" }, 405);
|
|
100
|
+
const body = (await req.json().catch(() => null)) as
|
|
101
|
+
| { id?: string; data?: Record<string, unknown> }
|
|
102
|
+
| null;
|
|
103
|
+
if (!body?.id || !body?.data) return json({ error: "missing body {id,data}" }, 400);
|
|
104
|
+
await db.put(body.id, body.data);
|
|
105
|
+
return json({ ok: true });
|
|
106
|
+
}
|
|
107
|
+
case "/api/delete": {
|
|
108
|
+
const id = url.searchParams.get("id");
|
|
109
|
+
if (!id) return json({ error: "missing id" }, 400);
|
|
110
|
+
await db.delete(id);
|
|
111
|
+
return json({ ok: true });
|
|
112
|
+
}
|
|
113
|
+
case "/api/search": {
|
|
114
|
+
if (req.method === "POST") {
|
|
115
|
+
const payload = (await req.json().catch(() => null)) as
|
|
116
|
+
| { query?: string | number[]; limit?: number }
|
|
117
|
+
| null;
|
|
118
|
+
if (!payload || (!payload.query && payload.query !== "")) {
|
|
119
|
+
return json({ error: "missing query" }, 400);
|
|
120
|
+
}
|
|
121
|
+
const limit = Number.isFinite(payload.limit) ? payload.limit! : 5;
|
|
122
|
+
const nodes = await db.vectorSearch(payload.query ?? "", limit);
|
|
123
|
+
return json(nodes.map((n) => ({ id: n.id, data: n.data })));
|
|
124
|
+
}
|
|
125
|
+
const q = url.searchParams.get("q") ?? "";
|
|
126
|
+
const k = Number(url.searchParams.get("k") ?? "5");
|
|
127
|
+
const nodes = await db.vectorSearch(q, Number.isFinite(k) ? k : 5);
|
|
128
|
+
return json(nodes.map((n) => ({ id: n.id, data: n.data })));
|
|
129
|
+
}
|
|
130
|
+
case "/api/list": {
|
|
131
|
+
const out: Array<{ id: string; data: Record<string, unknown> }> = [];
|
|
132
|
+
for await (const n of db.list()) {
|
|
133
|
+
out.push({ id: n.id, data: n.data as Record<string, unknown> });
|
|
134
|
+
}
|
|
135
|
+
return json(out);
|
|
136
|
+
}
|
|
137
|
+
case "/api/instances": {
|
|
138
|
+
const typeName = url.searchParams.get("type");
|
|
139
|
+
if (!typeName) return json({ error: "missing type" }, 400);
|
|
140
|
+
const nodes = await db.instancesOf(typeName);
|
|
141
|
+
return json(nodes.map((n) => ({ id: n.id, data: n.data })));
|
|
142
|
+
}
|
|
143
|
+
case "/api/history": {
|
|
144
|
+
const id = url.searchParams.get("id");
|
|
145
|
+
if (!id) return json({ error: "missing id" }, 400);
|
|
146
|
+
const history = await db.getNodeHistory(id);
|
|
147
|
+
return json(
|
|
148
|
+
history.map((h) => ({
|
|
149
|
+
id: h.id,
|
|
150
|
+
data: h.data,
|
|
151
|
+
timestamp: h.timestamp,
|
|
152
|
+
vectorClock: h.vectorClock,
|
|
153
|
+
state: h.state,
|
|
154
|
+
})),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
case "/api/restore": {
|
|
158
|
+
const id = url.searchParams.get("id");
|
|
159
|
+
const timestamp = url.searchParams.get("timestamp");
|
|
160
|
+
if (!id || !timestamp) return json({ error: "missing id or timestamp" }, 400);
|
|
161
|
+
await db.restoreNodeVersion(id, parseInt(timestamp));
|
|
162
|
+
return json({ success: true });
|
|
163
|
+
}
|
|
164
|
+
default:
|
|
165
|
+
return json({ error: "not found" }, 404);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (req.method === "GET") {
|
|
170
|
+
const mapPath = path === "/" ? "/index.html" : path;
|
|
171
|
+
const fileUrl = new URL(mapPath.startsWith("/") ? `.${mapPath}` : mapPath, STATIC_ROOT);
|
|
172
|
+
try {
|
|
173
|
+
const data = await Deno.readFile(fileUrl);
|
|
174
|
+
const contentType = mapPath.endsWith(".html")
|
|
175
|
+
? "text/html; charset=utf-8"
|
|
176
|
+
: mapPath.endsWith(".js")
|
|
177
|
+
? "application/javascript"
|
|
178
|
+
: mapPath.endsWith(".css")
|
|
179
|
+
? "text/css"
|
|
180
|
+
: mapPath.endsWith(".json")
|
|
181
|
+
? "application/json"
|
|
182
|
+
: mapPath.endsWith(".svg")
|
|
183
|
+
? "image/svg+xml"
|
|
184
|
+
: mapPath.endsWith(".png")
|
|
185
|
+
? "image/png"
|
|
186
|
+
: "application/octet-stream";
|
|
187
|
+
return new Response(data, { headers: corsHeaders({ "content-type": contentType }) });
|
|
188
|
+
} catch {
|
|
189
|
+
if (path === "/" || path === "/index.html") {
|
|
190
|
+
return new Response(INDEX_HTML, {
|
|
191
|
+
headers: corsHeaders({ "content-type": "text/html; charset=utf-8" }),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return new Response("Not Found", { status: 404, headers: corsHeaders() });
|
|
198
|
+
} catch (e) {
|
|
199
|
+
const msg =
|
|
200
|
+
e && typeof e === "object" && "message" in e ? String((e as any).message) : String(e);
|
|
201
|
+
return json({ error: msg }, 500);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const server = Deno.serve({ port, onListen: () => {} }, handler);
|
|
206
|
+
return {
|
|
207
|
+
url: `http://localhost:${port}`,
|
|
208
|
+
close: () => {
|
|
209
|
+
try {
|
|
210
|
+
server.shutdown();
|
|
211
|
+
} catch {
|
|
212
|
+
/* ignore */
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function json(data: unknown, status = 200): Response {
|
|
219
|
+
return new Response(JSON.stringify(data), {
|
|
220
|
+
status,
|
|
221
|
+
headers: corsHeaders({ "content-type": "application/json" }),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const INDEX_HTML = `<!doctype html>
|
|
226
|
+
<html>
|
|
227
|
+
<head>
|
|
228
|
+
<meta charset="utf-8" />
|
|
229
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
230
|
+
<title>PluresDB</title>
|
|
231
|
+
<style>
|
|
232
|
+
body { font-family: system-ui, sans-serif; margin: 24px; }
|
|
233
|
+
h1 { margin-top: 0; }
|
|
234
|
+
section { border: 1px solid #ddd; padding: 12px; margin-bottom: 16px; border-radius: 8px; }
|
|
235
|
+
input, textarea, button, select { font: inherit; }
|
|
236
|
+
code { background:#f5f5f5; padding:2px 4px; border-radius:4px; }
|
|
237
|
+
pre { background:#f9f9f9; padding:8px; border-radius:6px; overflow:auto; }
|
|
238
|
+
label { display:block; margin:6px 0 4px; }
|
|
239
|
+
</style>
|
|
240
|
+
</head>
|
|
241
|
+
<body>
|
|
242
|
+
<h1>PluresDB</h1>
|
|
243
|
+
<p>Reactive UI. Data updates automatically.</p>
|
|
244
|
+
<div style="display:flex; gap:16px; align-items:flex-start;">
|
|
245
|
+
<section style="flex:1; min-width:300px;">
|
|
246
|
+
<h3>Nodes</h3>
|
|
247
|
+
<input id="filter" placeholder="filter by id..." oninput="render()" />
|
|
248
|
+
<ul id="list" style="list-style:none; padding-left:0; max-height:60vh; overflow:auto;"></ul>
|
|
249
|
+
<button onclick="createNode()">Create node</button>
|
|
250
|
+
</section>
|
|
251
|
+
<section style="flex:2;">
|
|
252
|
+
<h3>Details</h3>
|
|
253
|
+
<div id="detail-empty">Select a node on the left</div>
|
|
254
|
+
<div id="detail" style="display:none;">
|
|
255
|
+
<label>Id</label>
|
|
256
|
+
<input id="d-id" disabled />
|
|
257
|
+
<label>JSON data</label>
|
|
258
|
+
<textarea id="d-json" rows="12" oninput="debouncedSave()"></textarea>
|
|
259
|
+
<div style="margin-top:8px; display:flex; gap:8px;">
|
|
260
|
+
<button onclick="delSelected()">Delete</button>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
<h3>Vector search</h3>
|
|
264
|
+
<input id="q" placeholder="search text" oninput="debouncedSearch()" />
|
|
265
|
+
<pre id="search-out"></pre>
|
|
266
|
+
</section>
|
|
267
|
+
</div>
|
|
268
|
+
<script>
|
|
269
|
+
const state = { items: new Map(), selected: null, timer: null, stimer: null };
|
|
270
|
+
const ev = new EventSource('/api/events');
|
|
271
|
+
ev.onmessage = (e) => {
|
|
272
|
+
const evt = JSON.parse(e.data);
|
|
273
|
+
if(evt.node){ state.items.set(evt.id, evt.node.data); } else { state.items.delete(evt.id); }
|
|
274
|
+
if(state.selected === evt.id && evt.node) {
|
|
275
|
+
// Avoid clobber when user is actively editing: basic heuristic
|
|
276
|
+
const el = document.getElementById('d-json');
|
|
277
|
+
if(document.activeElement !== el) el.value = JSON.stringify(evt.node.data,null,2);
|
|
278
|
+
}
|
|
279
|
+
render();
|
|
280
|
+
};
|
|
281
|
+
function render(){
|
|
282
|
+
const list = document.getElementById('list');
|
|
283
|
+
const f = (document.getElementById('filter').value||'').toLowerCase();
|
|
284
|
+
list.innerHTML = '';
|
|
285
|
+
const ids = Array.from(state.items.keys()).filter(id=>id.toLowerCase().includes(f)).sort();
|
|
286
|
+
for(const id of ids){
|
|
287
|
+
const li = document.createElement('li');
|
|
288
|
+
li.textContent = id;
|
|
289
|
+
li.style.cursor='pointer';
|
|
290
|
+
li.style.padding='4px 6px';
|
|
291
|
+
if(id===state.selected) { li.style.background='#eef'; }
|
|
292
|
+
li.onclick=()=>select(id);
|
|
293
|
+
list.appendChild(li);
|
|
294
|
+
}
|
|
295
|
+
const hasSel = !!state.selected;
|
|
296
|
+
document.getElementById('detail-empty').style.display = hasSel? 'none':'block';
|
|
297
|
+
document.getElementById('detail').style.display = hasSel? 'block':'none';
|
|
298
|
+
}
|
|
299
|
+
async function select(id){
|
|
300
|
+
state.selected = id;
|
|
301
|
+
document.getElementById('d-id').value = id;
|
|
302
|
+
const data = state.items.get(id) ?? {};
|
|
303
|
+
document.getElementById('d-json').value = JSON.stringify(data,null,2);
|
|
304
|
+
render();
|
|
305
|
+
}
|
|
306
|
+
function debouncedSave(){
|
|
307
|
+
clearTimeout(state.timer);
|
|
308
|
+
state.timer = setTimeout(saveNow, 350);
|
|
309
|
+
}
|
|
310
|
+
async function saveNow(){
|
|
311
|
+
const id = state.selected; if(!id) return;
|
|
312
|
+
let data; try{ data = JSON.parse(document.getElementById('d-json').value); }catch{ return }
|
|
313
|
+
await fetch('/api/put', { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify({id, data}) });
|
|
314
|
+
}
|
|
315
|
+
async function delSelected(){
|
|
316
|
+
const id = state.selected; if(!id) return;
|
|
317
|
+
await fetch('/api/delete?id='+encodeURIComponent(id));
|
|
318
|
+
state.selected = null; render();
|
|
319
|
+
}
|
|
320
|
+
async function createNode(){
|
|
321
|
+
const id = prompt('New node id:'); if(!id) return;
|
|
322
|
+
await fetch('/api/put', { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify({id, data:{}}) });
|
|
323
|
+
select(id);
|
|
324
|
+
}
|
|
325
|
+
function debouncedSearch(){ clearTimeout(state.stimer); state.stimer = setTimeout(searchNow,300); }
|
|
326
|
+
async function searchNow(){
|
|
327
|
+
const q = document.getElementById('q').value || '';
|
|
328
|
+
const res = await fetch('/api/search?q='+encodeURIComponent(q)+'&k=5');
|
|
329
|
+
const j = await res.json();
|
|
330
|
+
document.getElementById('search-out').textContent = JSON.stringify(j,null,2);
|
|
331
|
+
}
|
|
332
|
+
</script>
|
|
333
|
+
</body>
|
|
334
|
+
</html>`;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deno-first entry point for PluresDB.
|
|
3
|
+
*
|
|
4
|
+
* This module intentionally re-exports only browser/Deno compatible code.
|
|
5
|
+
* Node.js consumers should use the compiled entry at `pluresdb/node` instead.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { GunDB } from "./core/database.ts";
|
|
9
|
+
export type { DatabaseOptions, ServeOptions } from "./core/database.ts";
|
|
10
|
+
|
|
11
|
+
export { mergeNodes } from "./core/crdt.ts";
|
|
12
|
+
export type { NodeRecord, MeshMessage, VectorClock } from "./types/index.ts";
|
|
13
|
+
|
|
14
|
+
export { startApiServer } from "./http/api-server.ts";
|
|
15
|
+
export type { ApiServerHandle } from "./http/api-server.ts";
|
|
16
|
+
|
|
17
|
+
export { loadConfig, saveConfig } from "./config.ts";
|
|
18
|
+
|
|
19
|
+
export { startMeshServer, connectToPeer } from "./network/websocket-server.ts";
|
|
20
|
+
export type { MeshServer } from "./network/websocket-server.ts";
|
|
21
|
+
|
|
22
|
+
export { RuleEngine } from "./logic/rules.ts";
|
|
23
|
+
export type { Rule, RuleContext } from "./logic/rules.ts";
|
|
24
|
+
|
|
25
|
+
export { BruteForceVectorIndex } from "./vector/index.ts";
|
|
26
|
+
export type { VectorIndex, VectorIndexResult } from "./vector/index.ts";
|
|
27
|
+
|
|
28
|
+
export { debugLog } from "./util/debug.ts";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { NodeRecord } from "../types/index.ts";
|
|
2
|
+
|
|
3
|
+
// Minimal DB interface to avoid circular imports
|
|
4
|
+
export interface DatabaseLike {
|
|
5
|
+
put(id: string, data: Record<string, unknown>): Promise<void>;
|
|
6
|
+
get<T = Record<string, unknown>>(id: string): Promise<(T & { id: string }) | null>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface RuleContext {
|
|
10
|
+
db: DatabaseLike;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type RulePredicate = (node: NodeRecord) => boolean | Promise<boolean>;
|
|
14
|
+
export type RuleAction = (ctx: RuleContext, node: NodeRecord) => Promise<void>;
|
|
15
|
+
|
|
16
|
+
export interface Rule {
|
|
17
|
+
name: string;
|
|
18
|
+
whenType?: string;
|
|
19
|
+
predicate?: RulePredicate;
|
|
20
|
+
action: RuleAction;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class RuleEngine {
|
|
24
|
+
private readonly rules: Map<string, Rule> = new Map();
|
|
25
|
+
|
|
26
|
+
addRule(rule: Rule): void {
|
|
27
|
+
this.rules.set(rule.name, rule);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
removeRule(name: string): void {
|
|
31
|
+
this.rules.delete(name);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async evaluateNode(node: NodeRecord, ctx: RuleContext): Promise<void> {
|
|
35
|
+
for (const rule of this.rules.values()) {
|
|
36
|
+
if (rule.whenType && node.type !== rule.whenType) continue;
|
|
37
|
+
if (rule.predicate) {
|
|
38
|
+
const ok = await rule.predicate(node);
|
|
39
|
+
if (!ok) continue;
|
|
40
|
+
}
|
|
41
|
+
await rule.action(ctx, node);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/main.rs
ADDED
package/src/main.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { GunDB } from "./core/database.ts";
|
|
2
|
+
import { debugLog } from "./util/debug.ts";
|
|
3
|
+
import { startApiServer } from "./http/api-server.ts";
|
|
4
|
+
import { loadConfig, saveConfig } from "./config.ts";
|
|
5
|
+
|
|
6
|
+
function printUsage() {
|
|
7
|
+
console.log(
|
|
8
|
+
[
|
|
9
|
+
"Usage:",
|
|
10
|
+
" deno run -A src/main.ts serve [--port <port>] [--kv <path>] [ws://peer ...]",
|
|
11
|
+
" deno run -A src/main.ts put <id> <json> [--kv <path>]",
|
|
12
|
+
" deno run -A src/main.ts get <id> [--kv <path>]",
|
|
13
|
+
" deno run -A src/main.ts delete <id> [--kv <path>]",
|
|
14
|
+
" deno run -A src/main.ts vsearch <query> <k> [--kv <path>]",
|
|
15
|
+
" deno run -A src/main.ts type <id> <TypeName> [--kv <path>]",
|
|
16
|
+
" deno run -A src/main.ts instances <TypeName> [--kv <path>]",
|
|
17
|
+
" deno run -A src/main.ts list [--kv <path>]",
|
|
18
|
+
"",
|
|
19
|
+
].join("\n"),
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (import.meta.main) {
|
|
24
|
+
const [cmd, ...rest] = Deno.args;
|
|
25
|
+
switch (cmd) {
|
|
26
|
+
case "serve": {
|
|
27
|
+
const cfg = await loadConfig();
|
|
28
|
+
let port = cfg.port ?? 8080;
|
|
29
|
+
let kvPath: string | undefined = cfg.kvPath;
|
|
30
|
+
const pi = rest.indexOf("--port");
|
|
31
|
+
if (pi >= 0 && rest[pi + 1]) {
|
|
32
|
+
const n = Number(rest[pi + 1]);
|
|
33
|
+
if (Number.isFinite(n)) port = n;
|
|
34
|
+
}
|
|
35
|
+
const ki = rest.indexOf("--kv");
|
|
36
|
+
if (ki >= 0 && rest[ki + 1]) kvPath = rest[ki + 1];
|
|
37
|
+
const peers = (cfg.peers ?? []).concat(
|
|
38
|
+
rest.filter((v) => v.startsWith("ws://") || v.startsWith("wss://")),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const db = new GunDB();
|
|
42
|
+
await db.ready(kvPath);
|
|
43
|
+
db.serve({ port });
|
|
44
|
+
for (const p of peers) db.connect(p);
|
|
45
|
+
|
|
46
|
+
const api = startApiServer({ port: port + (cfg.apiPortOffset ?? 1), db });
|
|
47
|
+
console.log(`PluresDB node serving on ws://localhost:${port}`);
|
|
48
|
+
console.log(`HTTP API/UI on ${api.url}`);
|
|
49
|
+
if (peers.length) console.log("Connected to peers:", peers.join(", "));
|
|
50
|
+
|
|
51
|
+
// Keep process alive
|
|
52
|
+
await new Promise(() => {});
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
case "put": {
|
|
56
|
+
const [id, json, ...flags] = rest;
|
|
57
|
+
if (!id || !json) {
|
|
58
|
+
printUsage();
|
|
59
|
+
Deno.exit(1);
|
|
60
|
+
}
|
|
61
|
+
let kvPath: string | undefined;
|
|
62
|
+
const ki = flags.indexOf("--kv");
|
|
63
|
+
if (ki >= 0 && flags[ki + 1]) kvPath = flags[ki + 1];
|
|
64
|
+
const db = new GunDB();
|
|
65
|
+
await db.ready(kvPath);
|
|
66
|
+
const obj = JSON.parse(json);
|
|
67
|
+
await db.put(id, obj);
|
|
68
|
+
console.log("ok");
|
|
69
|
+
await db.close();
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
case "get": {
|
|
73
|
+
const [id, ...flags] = rest;
|
|
74
|
+
if (!id) {
|
|
75
|
+
printUsage();
|
|
76
|
+
Deno.exit(1);
|
|
77
|
+
}
|
|
78
|
+
let kvPath: string | undefined;
|
|
79
|
+
const ki = flags.indexOf("--kv");
|
|
80
|
+
if (ki >= 0 && flags[ki + 1]) kvPath = flags[ki + 1];
|
|
81
|
+
const db = new GunDB();
|
|
82
|
+
await db.ready(kvPath);
|
|
83
|
+
const val = await db.get<Record<string, unknown>>(id);
|
|
84
|
+
console.log(JSON.stringify(val));
|
|
85
|
+
await db.close();
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
case "delete": {
|
|
89
|
+
const [id, ...flags] = rest;
|
|
90
|
+
if (!id) {
|
|
91
|
+
printUsage();
|
|
92
|
+
Deno.exit(1);
|
|
93
|
+
}
|
|
94
|
+
let kvPath: string | undefined;
|
|
95
|
+
const ki = flags.indexOf("--kv");
|
|
96
|
+
if (ki >= 0 && flags[ki + 1]) kvPath = flags[ki + 1];
|
|
97
|
+
const db = new GunDB();
|
|
98
|
+
await db.ready(kvPath);
|
|
99
|
+
await db.delete(id);
|
|
100
|
+
console.log("ok");
|
|
101
|
+
await db.close();
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
case "vsearch": {
|
|
105
|
+
const [query, kRaw, ...flags] = rest;
|
|
106
|
+
if (!query || !kRaw) {
|
|
107
|
+
printUsage();
|
|
108
|
+
Deno.exit(1);
|
|
109
|
+
}
|
|
110
|
+
const k = Number(kRaw);
|
|
111
|
+
if (!Number.isFinite(k)) {
|
|
112
|
+
printUsage();
|
|
113
|
+
Deno.exit(1);
|
|
114
|
+
}
|
|
115
|
+
let kvPath: string | undefined;
|
|
116
|
+
const ki = flags.indexOf("--kv");
|
|
117
|
+
if (ki >= 0 && flags[ki + 1]) kvPath = flags[ki + 1];
|
|
118
|
+
const db = new GunDB();
|
|
119
|
+
await db.ready(kvPath);
|
|
120
|
+
const results = await db.vectorSearch(query, k);
|
|
121
|
+
console.log(JSON.stringify(results.map((n) => ({ id: n.id, data: n.data }))));
|
|
122
|
+
await db.close();
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
case "type": {
|
|
126
|
+
const [id, typeName, ...flags] = rest;
|
|
127
|
+
if (!id || !typeName) {
|
|
128
|
+
printUsage();
|
|
129
|
+
Deno.exit(1);
|
|
130
|
+
}
|
|
131
|
+
let kvPath: string | undefined;
|
|
132
|
+
const ki = flags.indexOf("--kv");
|
|
133
|
+
if (ki >= 0 && flags[ki + 1]) kvPath = flags[ki + 1];
|
|
134
|
+
const db = new GunDB();
|
|
135
|
+
await db.ready(kvPath);
|
|
136
|
+
await db.setType(id, typeName);
|
|
137
|
+
console.log("ok");
|
|
138
|
+
await db.close();
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
case "instances": {
|
|
142
|
+
const [typeName, ...flags] = rest;
|
|
143
|
+
if (!typeName) {
|
|
144
|
+
printUsage();
|
|
145
|
+
Deno.exit(1);
|
|
146
|
+
}
|
|
147
|
+
let kvPath: string | undefined;
|
|
148
|
+
const ki = flags.indexOf("--kv");
|
|
149
|
+
if (ki >= 0 && flags[ki + 1]) kvPath = flags[ki + 1];
|
|
150
|
+
const db = new GunDB();
|
|
151
|
+
await db.ready(kvPath);
|
|
152
|
+
const rows = await db.instancesOf(typeName);
|
|
153
|
+
console.log(JSON.stringify(rows.map((n) => ({ id: n.id, data: n.data }))));
|
|
154
|
+
await db.close();
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
case "list": {
|
|
158
|
+
const flags = rest;
|
|
159
|
+
let kvPath: string | undefined;
|
|
160
|
+
const ki = flags.indexOf("--kv");
|
|
161
|
+
if (ki >= 0 && flags[ki + 1]) kvPath = flags[ki + 1];
|
|
162
|
+
const db = new GunDB();
|
|
163
|
+
await db.ready(kvPath);
|
|
164
|
+
const nodes = await db.getAll();
|
|
165
|
+
const out = nodes.map((node) => ({ id: node.id, data: node.data as Record<string, unknown> }));
|
|
166
|
+
console.log(JSON.stringify(out));
|
|
167
|
+
await db.close();
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
case "config": {
|
|
171
|
+
const cfg = await loadConfig();
|
|
172
|
+
console.log(JSON.stringify(cfg, null, 2));
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
case "config:set": {
|
|
176
|
+
const [key, value] = rest;
|
|
177
|
+
if (!key || value === undefined) {
|
|
178
|
+
printUsage();
|
|
179
|
+
Deno.exit(1);
|
|
180
|
+
}
|
|
181
|
+
const cfg = await loadConfig();
|
|
182
|
+
(cfg as any)[key] = /^[0-9]+$/.test(value) ? Number(value) : value;
|
|
183
|
+
await saveConfig(cfg);
|
|
184
|
+
console.log("ok");
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
default:
|
|
188
|
+
printUsage();
|
|
189
|
+
}
|
|
190
|
+
}
|