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,494 @@
|
|
|
1
|
+
import { KvStorage } from "../storage/kv-storage.ts";
|
|
2
|
+
import type { MeshMessage, NodeRecord } from "../types/index.ts";
|
|
3
|
+
import { mergeNodes } from "./crdt.ts";
|
|
4
|
+
import { connectToPeer, type MeshServer, startMeshServer } from "../network/websocket-server.ts";
|
|
5
|
+
import { debugLog } from "../util/debug.ts";
|
|
6
|
+
import { RuleEngine, type Rule, type RuleContext } from "../logic/rules.ts";
|
|
7
|
+
import { BruteForceVectorIndex } from "../vector/index.ts";
|
|
8
|
+
|
|
9
|
+
const FUNCTION_PLACEHOLDER = "[sanitized function]";
|
|
10
|
+
|
|
11
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
12
|
+
if (value === null || typeof value !== "object") return false;
|
|
13
|
+
const proto = Object.getPrototypeOf(value);
|
|
14
|
+
return proto === Object.prototype || proto === null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function sanitizeValue(value: unknown, seen: WeakSet<object>): unknown {
|
|
18
|
+
if (typeof value === "function") return FUNCTION_PLACEHOLDER;
|
|
19
|
+
if (value === null || typeof value !== "object") return value;
|
|
20
|
+
if (seen.has(value as object)) return "[circular]";
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
seen.add(value);
|
|
23
|
+
return value.map((item) => sanitizeValue(item, seen));
|
|
24
|
+
}
|
|
25
|
+
if (!isPlainObject(value)) return value;
|
|
26
|
+
seen.add(value as object);
|
|
27
|
+
const clean: Record<string, unknown> = Object.create(null);
|
|
28
|
+
for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
|
|
29
|
+
if (key === "__proto__" || key === "constructor") continue;
|
|
30
|
+
clean[key] = sanitizeValue(entry, seen);
|
|
31
|
+
}
|
|
32
|
+
return clean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function sanitizeRecord(data: Record<string, unknown>): Record<string, unknown> {
|
|
36
|
+
const result = sanitizeValue(data, new WeakSet()) as Record<string, unknown> | string;
|
|
37
|
+
if (typeof result === "string" || result === undefined) return Object.create(null);
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function sanitizeForOutput(data: Record<string, unknown>): Record<string, unknown> {
|
|
42
|
+
const clean = sanitizeRecord(data);
|
|
43
|
+
if (typeof clean["toString"] !== "string") {
|
|
44
|
+
clean["toString"] = Object.prototype.toString.call(clean);
|
|
45
|
+
}
|
|
46
|
+
return clean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ServeOptions {
|
|
50
|
+
port?: number;
|
|
51
|
+
}
|
|
52
|
+
export interface DatabaseOptions {
|
|
53
|
+
kvPath?: string;
|
|
54
|
+
peerId?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class GunDB {
|
|
58
|
+
private readonly storage: KvStorage;
|
|
59
|
+
private readonly listeners: Map<string, Set<(node: NodeRecord | null) => void>> = new Map();
|
|
60
|
+
private readonly anyListeners: Set<(event: { id: string; node: NodeRecord | null }) => void> =
|
|
61
|
+
new Set();
|
|
62
|
+
private readonly peerId: string;
|
|
63
|
+
private meshServer: MeshServer | null = null;
|
|
64
|
+
private readonly peerSockets: Set<WebSocket> = new Set();
|
|
65
|
+
private closed = false;
|
|
66
|
+
private readyState = false;
|
|
67
|
+
private readonly rules = new RuleEngine();
|
|
68
|
+
private readonly vectorIndex = new BruteForceVectorIndex();
|
|
69
|
+
|
|
70
|
+
constructor(options?: DatabaseOptions) {
|
|
71
|
+
this.storage = new KvStorage();
|
|
72
|
+
this.peerId = options?.peerId ?? crypto.randomUUID();
|
|
73
|
+
// Open storage synchronously-ish
|
|
74
|
+
// Caller should await ready()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async ready(kvPath?: string): Promise<void> {
|
|
78
|
+
await this.storage.open(kvPath);
|
|
79
|
+
// Rebuild in-memory vector index from storage
|
|
80
|
+
for await (const node of this.storage.listNodes()) {
|
|
81
|
+
if (node.vector && node.vector.length > 0) {
|
|
82
|
+
this.vectorIndex.upsert(node.id, node.vector);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
this.closed = false;
|
|
86
|
+
this.readyState = true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Basic CRUD
|
|
90
|
+
async put(id: string, data: Record<string, unknown>): Promise<void> {
|
|
91
|
+
this.ensureReady();
|
|
92
|
+
await this.applyPut(id, data, false);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private async applyPut(
|
|
96
|
+
id: string,
|
|
97
|
+
data: Record<string, unknown>,
|
|
98
|
+
suppressRules: boolean,
|
|
99
|
+
): Promise<void> {
|
|
100
|
+
if (this.closed) return;
|
|
101
|
+
debugLog("put()", { id, keys: Object.keys(data ?? {}) });
|
|
102
|
+
const existing = await this.storage.getNode(id);
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
const existingClock = existing?.vectorClock ?? {};
|
|
105
|
+
const newClock = {
|
|
106
|
+
...existingClock,
|
|
107
|
+
[this.peerId]: (existingClock[this.peerId] ?? 0) + 1,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const sanitizedData = sanitizeRecord(data ?? {});
|
|
111
|
+
let vector: number[] | undefined = undefined;
|
|
112
|
+
const record = sanitizedData as Record<string, unknown>;
|
|
113
|
+
const maybeVector = record.vector as unknown;
|
|
114
|
+
if (
|
|
115
|
+
Array.isArray(maybeVector) &&
|
|
116
|
+
maybeVector.every((v) => typeof v === "number" && Number.isFinite(v))
|
|
117
|
+
) {
|
|
118
|
+
vector = maybeVector as number[];
|
|
119
|
+
} else if (typeof record.text === "string") {
|
|
120
|
+
vector = embedTextToVector(record.text);
|
|
121
|
+
} else if (typeof record.content === "string") {
|
|
122
|
+
vector = embedTextToVector(record.content);
|
|
123
|
+
} else vector = existing?.vector ?? undefined;
|
|
124
|
+
|
|
125
|
+
const newState: Record<string, number> = { ...(existing?.state ?? {}) };
|
|
126
|
+
for (const key of Object.keys(record ?? {})) newState[key] = now;
|
|
127
|
+
|
|
128
|
+
const updated: NodeRecord = {
|
|
129
|
+
id,
|
|
130
|
+
data: record,
|
|
131
|
+
vector,
|
|
132
|
+
type:
|
|
133
|
+
typeof record.type === "string" ? (record.type as string) : (existing?.type ?? undefined),
|
|
134
|
+
timestamp: now,
|
|
135
|
+
state: newState,
|
|
136
|
+
vectorClock: newClock,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const merged = mergeNodes(existing, updated);
|
|
140
|
+
await this.storage.setNode(merged);
|
|
141
|
+
debugLog("put() merged", { id, timestamp: merged.timestamp });
|
|
142
|
+
this.emit(id, merged);
|
|
143
|
+
if (merged.vector && merged.vector.length > 0) this.vectorIndex.upsert(id, merged.vector);
|
|
144
|
+
else this.vectorIndex.remove(id);
|
|
145
|
+
if (!suppressRules) {
|
|
146
|
+
await this.evaluateRules(merged);
|
|
147
|
+
}
|
|
148
|
+
this.broadcast({ type: "put", originId: this.peerId, node: merged });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async get<T = Record<string, unknown>>(id: string): Promise<(T & { id: string }) | null> {
|
|
152
|
+
this.ensureReady();
|
|
153
|
+
const node = await this.storage.getNode(id);
|
|
154
|
+
if (!node) return null;
|
|
155
|
+
const sanitized = sanitizeForOutput((node.data ?? {}) as Record<string, unknown>);
|
|
156
|
+
return { id: node.id, ...(sanitized as T) };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async delete(id: string): Promise<void> {
|
|
160
|
+
this.ensureReady();
|
|
161
|
+
if (this.closed) return;
|
|
162
|
+
debugLog("delete()", { id });
|
|
163
|
+
await this.storage.deleteNode(id);
|
|
164
|
+
this.emit(id, null);
|
|
165
|
+
this.vectorIndex.remove(id);
|
|
166
|
+
this.broadcast({ type: "delete", originId: this.peerId, id });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Subscriptions
|
|
170
|
+
on(id: string, callback: (node: NodeRecord | null) => void): () => void {
|
|
171
|
+
this.ensureReady();
|
|
172
|
+
const set = this.listeners.get(id) ?? new Set();
|
|
173
|
+
set.add(callback);
|
|
174
|
+
this.listeners.set(id, set);
|
|
175
|
+
return () => this.off(id, callback);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
off(id: string, callback?: (node: NodeRecord | null) => void): void {
|
|
179
|
+
const set = this.listeners.get(id);
|
|
180
|
+
if (!set) return;
|
|
181
|
+
if (callback) set.delete(callback);
|
|
182
|
+
else set.clear();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private emit(id: string, node: NodeRecord | null): void {
|
|
186
|
+
const set = this.listeners.get(id);
|
|
187
|
+
if (!set) return;
|
|
188
|
+
for (const cb of set) {
|
|
189
|
+
queueMicrotask(() => cb(node));
|
|
190
|
+
}
|
|
191
|
+
for (const cb of this.anyListeners) {
|
|
192
|
+
queueMicrotask(() => cb({ id, node }));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Vector search
|
|
197
|
+
async vectorSearch(
|
|
198
|
+
query: string | number[],
|
|
199
|
+
limit: number,
|
|
200
|
+
): Promise<Array<NodeRecord & { similarity?: number }>> {
|
|
201
|
+
this.ensureReady();
|
|
202
|
+
const queryVector = Array.isArray(query) ? query : embedTextToVector(query);
|
|
203
|
+
const results = this.vectorIndex.search(queryVector, limit);
|
|
204
|
+
if (results.length > 0) {
|
|
205
|
+
const nodes: Array<NodeRecord & { similarity?: number }> = [];
|
|
206
|
+
for (const r of results) {
|
|
207
|
+
const n = await this.storage.getNode(r.id);
|
|
208
|
+
if (n) nodes.push({ ...n, similarity: r.score });
|
|
209
|
+
}
|
|
210
|
+
return nodes;
|
|
211
|
+
}
|
|
212
|
+
// Fallback: scan storage when index is empty
|
|
213
|
+
const scored: Array<{ score: number; node: NodeRecord }> = [];
|
|
214
|
+
for await (const node of this.storage.listNodes()) {
|
|
215
|
+
if (!node.vector || node.vector.length === 0) continue;
|
|
216
|
+
const score = cosineSimilarity(queryVector, node.vector);
|
|
217
|
+
if (Number.isFinite(score)) scored.push({ score, node });
|
|
218
|
+
}
|
|
219
|
+
scored.sort((a, b) => b.score - a.score);
|
|
220
|
+
return scored.slice(0, limit).map((s) => ({ ...s.node, similarity: s.score }));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Type system convenience
|
|
224
|
+
async instancesOf(typeName: string): Promise<NodeRecord[]> {
|
|
225
|
+
this.ensureReady();
|
|
226
|
+
const results: NodeRecord[] = [];
|
|
227
|
+
for await (const node of this.storage.listNodes()) {
|
|
228
|
+
if (node.type === typeName) results.push(node);
|
|
229
|
+
}
|
|
230
|
+
return results;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async getNodeHistory(id: string): Promise<NodeRecord[]> {
|
|
234
|
+
this.ensureReady();
|
|
235
|
+
return await this.storage.getNodeHistory(id);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async restoreNodeVersion(id: string, timestamp: number): Promise<void> {
|
|
239
|
+
this.ensureReady();
|
|
240
|
+
const history = await this.getNodeHistory(id);
|
|
241
|
+
const version = history.find((v) => v.timestamp === timestamp);
|
|
242
|
+
if (!version) throw new Error(`Version not found for node ${id} at timestamp ${timestamp}`);
|
|
243
|
+
|
|
244
|
+
// Restore by putting the historical version
|
|
245
|
+
await this.put(id, version.data);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async setType(id: string, typeName: string): Promise<void> {
|
|
249
|
+
this.ensureReady();
|
|
250
|
+
const existing = await this.storage.getNode(id);
|
|
251
|
+
const data: Record<string, unknown> = existing ? existing.data : {};
|
|
252
|
+
data.type = typeName;
|
|
253
|
+
await this.put(id, data);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Any-change subscription (internal use for API streaming)
|
|
257
|
+
onAny(callback: (event: { id: string; node: NodeRecord | null }) => void): () => void {
|
|
258
|
+
this.ensureReady();
|
|
259
|
+
this.anyListeners.add(callback);
|
|
260
|
+
return () => this.offAny(callback);
|
|
261
|
+
}
|
|
262
|
+
offAny(callback: (event: { id: string; node: NodeRecord | null }) => void): void {
|
|
263
|
+
this.anyListeners.delete(callback);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async *list(): AsyncIterable<NodeRecord> {
|
|
267
|
+
this.ensureReady();
|
|
268
|
+
for await (const node of this.storage.listNodes()) {
|
|
269
|
+
yield node;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async getAll(): Promise<NodeRecord[]> {
|
|
274
|
+
this.ensureReady();
|
|
275
|
+
const out: NodeRecord[] = [];
|
|
276
|
+
for await (const node of this.storage.listNodes()) out.push(node);
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Mesh networking
|
|
281
|
+
serve(options?: ServeOptions): void {
|
|
282
|
+
this.ensureReady();
|
|
283
|
+
const port = options?.port ?? 8080;
|
|
284
|
+
if (!this.meshServer) {
|
|
285
|
+
debugLog("serve() starting", { port });
|
|
286
|
+
this.meshServer = startMeshServer({
|
|
287
|
+
port,
|
|
288
|
+
onMessage: ({ msg, source, send, broadcast }) => {
|
|
289
|
+
this.handleInboundMessage(msg as MeshMessage, {
|
|
290
|
+
send,
|
|
291
|
+
broadcast,
|
|
292
|
+
source,
|
|
293
|
+
});
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
connect(url: string): void {
|
|
300
|
+
this.ensureReady();
|
|
301
|
+
const socket = connectToPeer(url, {
|
|
302
|
+
onOpen: (s) => {
|
|
303
|
+
// Request a snapshot
|
|
304
|
+
try {
|
|
305
|
+
s.send(JSON.stringify({ type: "sync_request", originId: this.peerId }));
|
|
306
|
+
} catch {
|
|
307
|
+
/* ignore */
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
onMessage: (msg) =>
|
|
311
|
+
this.handleInboundMessage(msg as MeshMessage, {
|
|
312
|
+
send: (obj) => {
|
|
313
|
+
try {
|
|
314
|
+
socket.send(JSON.stringify(obj));
|
|
315
|
+
} catch {
|
|
316
|
+
/* ignore */
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
broadcast: (_obj) => {
|
|
320
|
+
/* do not rebroadcast from clients */
|
|
321
|
+
},
|
|
322
|
+
source: socket,
|
|
323
|
+
}),
|
|
324
|
+
});
|
|
325
|
+
this.peerSockets.add(socket);
|
|
326
|
+
socket.onclose = () => this.peerSockets.delete(socket);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async close(): Promise<void> {
|
|
330
|
+
this.closed = true;
|
|
331
|
+
this.readyState = false;
|
|
332
|
+
for (const s of this.peerSockets) {
|
|
333
|
+
try {
|
|
334
|
+
s.onmessage = null;
|
|
335
|
+
s.close();
|
|
336
|
+
} catch {
|
|
337
|
+
/* ignore */
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
this.peerSockets.clear();
|
|
341
|
+
if (this.meshServer) {
|
|
342
|
+
try {
|
|
343
|
+
this.meshServer.close();
|
|
344
|
+
} catch {
|
|
345
|
+
/* ignore */
|
|
346
|
+
}
|
|
347
|
+
this.meshServer = null;
|
|
348
|
+
}
|
|
349
|
+
await this.storage.close();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private ensureReady(): void {
|
|
353
|
+
if (!this.readyState || this.closed) {
|
|
354
|
+
throw new Error("Database not ready");
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private async handleInboundMessage(
|
|
359
|
+
msg: MeshMessage,
|
|
360
|
+
ctx: {
|
|
361
|
+
send: (obj: unknown) => void;
|
|
362
|
+
broadcast: (obj: unknown, exclude?: WebSocket) => void;
|
|
363
|
+
source: WebSocket;
|
|
364
|
+
},
|
|
365
|
+
): Promise<void> {
|
|
366
|
+
if (this.closed) return;
|
|
367
|
+
if (!msg || typeof msg !== "object") return;
|
|
368
|
+
const originId = (msg as Partial<{ originId: string }>).originId;
|
|
369
|
+
debugLog("inbound", { type: (msg as { type: string }).type, originId });
|
|
370
|
+
if (originId === this.peerId) return; // ignore our own
|
|
371
|
+
|
|
372
|
+
switch (msg.type) {
|
|
373
|
+
case "put": {
|
|
374
|
+
const compatPayload = msg as Partial<{
|
|
375
|
+
id: string;
|
|
376
|
+
data: Record<string, unknown>;
|
|
377
|
+
}>;
|
|
378
|
+
if (!("node" in msg) && compatPayload.id && compatPayload.data) {
|
|
379
|
+
debugLog("apply put (compat)", { id: compatPayload.id });
|
|
380
|
+
await this.put(compatPayload.id, compatPayload.data);
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
const { node } = msg;
|
|
384
|
+
debugLog("apply put", { id: node.id });
|
|
385
|
+
const existing = await this.storage.getNode(node.id);
|
|
386
|
+
const merged = mergeNodes(existing, node);
|
|
387
|
+
await this.storage.setNode(merged);
|
|
388
|
+
this.emit(node.id, merged);
|
|
389
|
+
if (merged.vector && merged.vector.length > 0)
|
|
390
|
+
this.vectorIndex.upsert(node.id, merged.vector);
|
|
391
|
+
else this.vectorIndex.remove(node.id);
|
|
392
|
+
await this.evaluateRules(merged);
|
|
393
|
+
try {
|
|
394
|
+
ctx.broadcast(msg, ctx.source);
|
|
395
|
+
} catch {
|
|
396
|
+
/* ignore */
|
|
397
|
+
}
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
case "delete": {
|
|
401
|
+
debugLog("apply delete", { id: msg.id });
|
|
402
|
+
await this.storage.deleteNode(msg.id);
|
|
403
|
+
this.emit(msg.id, null);
|
|
404
|
+
try {
|
|
405
|
+
ctx.broadcast(msg, ctx.source);
|
|
406
|
+
} catch {
|
|
407
|
+
/* ignore */
|
|
408
|
+
}
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
case "sync_request": {
|
|
412
|
+
debugLog("sync_request sending snapshot");
|
|
413
|
+
// send snapshot to requester
|
|
414
|
+
for await (const node of this.storage.listNodes()) {
|
|
415
|
+
ctx.send({ type: "put", originId: this.peerId, node });
|
|
416
|
+
}
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private broadcast(obj: unknown): void {
|
|
423
|
+
if (this.meshServer) {
|
|
424
|
+
try {
|
|
425
|
+
this.meshServer.broadcast(obj);
|
|
426
|
+
} catch {
|
|
427
|
+
/* ignore */
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// Also forward to directly connected peers (client mode)
|
|
431
|
+
for (const s of this.peerSockets) {
|
|
432
|
+
try {
|
|
433
|
+
s.send(JSON.stringify(obj));
|
|
434
|
+
} catch {
|
|
435
|
+
/* ignore */
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// --- rules ---
|
|
441
|
+
addRule(rule: Rule): void {
|
|
442
|
+
this.rules.addRule(rule);
|
|
443
|
+
}
|
|
444
|
+
removeRule(name: string): void {
|
|
445
|
+
this.rules.removeRule(name);
|
|
446
|
+
}
|
|
447
|
+
private async evaluateRules(node: NodeRecord): Promise<void> {
|
|
448
|
+
const ctx: RuleContext = {
|
|
449
|
+
db: {
|
|
450
|
+
put: (id, data) => this.applyPut(id, data, true),
|
|
451
|
+
get: (id) => this.get(id),
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
await this.rules.evaluateNode(node, ctx);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// --- utilities ---
|
|
459
|
+
function embedTextToVector(text: string, dims = 64): number[] {
|
|
460
|
+
const vec = new Float32Array(dims);
|
|
461
|
+
let h = 2166136261 >>> 0; // FNV-1a baseline
|
|
462
|
+
for (let i = 0; i < text.length; i++) {
|
|
463
|
+
h ^= text.charCodeAt(i);
|
|
464
|
+
h = Math.imul(h, 16777619);
|
|
465
|
+
const idx = h % dims;
|
|
466
|
+
vec[idx] += 1;
|
|
467
|
+
}
|
|
468
|
+
// L2 normalize
|
|
469
|
+
let norm = 0;
|
|
470
|
+
for (let i = 0; i < dims; i++) norm += vec[i] * vec[i];
|
|
471
|
+
norm = Math.sqrt(norm) || 1;
|
|
472
|
+
for (let i = 0; i < dims; i++) vec[i] /= norm;
|
|
473
|
+
return Array.from(vec);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function cosineSimilarity(a: number[], b: number[]): number {
|
|
477
|
+
if (a.length !== b.length) {
|
|
478
|
+
const dims = Math.min(a.length, b.length);
|
|
479
|
+
a = a.slice(0, dims);
|
|
480
|
+
b = b.slice(0, dims);
|
|
481
|
+
}
|
|
482
|
+
let dot = 0,
|
|
483
|
+
na = 0,
|
|
484
|
+
nb = 0;
|
|
485
|
+
for (let i = 0; i < a.length; i++) {
|
|
486
|
+
const av = a[i] ?? 0;
|
|
487
|
+
const bv = b[i] ?? 0;
|
|
488
|
+
dot += av * bv;
|
|
489
|
+
na += av * av;
|
|
490
|
+
nb += bv * bv;
|
|
491
|
+
}
|
|
492
|
+
const denom = Math.sqrt(na) * Math.sqrt(nb) || 1;
|
|
493
|
+
return dot / denom;
|
|
494
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env -S deno run -A
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Health check script for Docker containers
|
|
5
|
+
* Verifies that PluresDB is running and responding correctly
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const API_PORT = Deno.env.get("PLURESDB_PORT") || "34567";
|
|
9
|
+
const WEB_PORT = Deno.env.get("PLURESDB_WEB_PORT") || "34568";
|
|
10
|
+
const HOST = Deno.env.get("PLURESDB_HOST") || "localhost";
|
|
11
|
+
|
|
12
|
+
interface HealthStatus {
|
|
13
|
+
status: "healthy" | "unhealthy";
|
|
14
|
+
checks: {
|
|
15
|
+
api: boolean;
|
|
16
|
+
web: boolean;
|
|
17
|
+
database: boolean;
|
|
18
|
+
};
|
|
19
|
+
timestamp: string;
|
|
20
|
+
uptime: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function checkApiHealth(): Promise<boolean> {
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch(`http://${HOST}:${API_PORT}/api/health`, {
|
|
26
|
+
method: "GET",
|
|
27
|
+
headers: {
|
|
28
|
+
Accept: "application/json",
|
|
29
|
+
"User-Agent": "pluresdb-healthcheck/1.0.0",
|
|
30
|
+
},
|
|
31
|
+
signal: AbortSignal.timeout(5000), // 5 second timeout
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
console.error(`API health check failed: ${response.status} ${response.statusText}`);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const data = await response.json();
|
|
40
|
+
return data.status === "healthy" || data.status === "ok";
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error(`API health check error: ${error.message}`);
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function checkWebHealth(): Promise<boolean> {
|
|
48
|
+
try {
|
|
49
|
+
const response = await fetch(`http://${HOST}:${WEB_PORT}/`, {
|
|
50
|
+
method: "GET",
|
|
51
|
+
headers: {
|
|
52
|
+
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
53
|
+
"User-Agent": "pluresdb-healthcheck/1.0.0",
|
|
54
|
+
},
|
|
55
|
+
signal: AbortSignal.timeout(5000), // 5 second timeout
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
console.error(`Web health check failed: ${response.status} ${response.statusText}`);
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const contentType = response.headers.get("content-type");
|
|
64
|
+
return contentType?.includes("text/html") || contentType?.includes("application/json");
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error(`Web health check error: ${error.message}`);
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function checkDatabaseHealth(): Promise<boolean> {
|
|
72
|
+
try {
|
|
73
|
+
// Check if data directory exists and is writable
|
|
74
|
+
const dataDir = Deno.env.get("PLURESDB_DATA_DIR") || "./data";
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
await Deno.stat(dataDir);
|
|
78
|
+
} catch {
|
|
79
|
+
// Data directory doesn't exist, try to create it
|
|
80
|
+
await Deno.mkdir(dataDir, { recursive: true });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Test write permissions
|
|
84
|
+
const testFile = `${dataDir}/.healthcheck-test`;
|
|
85
|
+
await Deno.writeTextFile(testFile, "healthcheck");
|
|
86
|
+
await Deno.remove(testFile);
|
|
87
|
+
|
|
88
|
+
return true;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error(`Database health check error: ${error.message}`);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function main(): Promise<void> {
|
|
96
|
+
const startTime = Date.now();
|
|
97
|
+
|
|
98
|
+
console.log("Starting PluresDB health check...");
|
|
99
|
+
console.log(`API: http://${HOST}:${API_PORT}/api/health`);
|
|
100
|
+
console.log(`Web: http://${HOST}:${WEB_PORT}/`);
|
|
101
|
+
|
|
102
|
+
// Run health checks in parallel
|
|
103
|
+
const [apiHealthy, webHealthy, dbHealthy] = await Promise.all([
|
|
104
|
+
checkApiHealth(),
|
|
105
|
+
checkWebHealth(),
|
|
106
|
+
checkDatabaseHealth(),
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
const uptime = Date.now() - startTime;
|
|
110
|
+
const allHealthy = apiHealthy && webHealthy && dbHealthy;
|
|
111
|
+
|
|
112
|
+
const healthStatus: HealthStatus = {
|
|
113
|
+
status: allHealthy ? "healthy" : "unhealthy",
|
|
114
|
+
checks: {
|
|
115
|
+
api: apiHealthy,
|
|
116
|
+
web: webHealthy,
|
|
117
|
+
database: dbHealthy,
|
|
118
|
+
},
|
|
119
|
+
timestamp: new Date().toISOString(),
|
|
120
|
+
uptime,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
console.log("Health check results:");
|
|
124
|
+
console.log(` API Server: ${apiHealthy ? "✓" : "✗"}`);
|
|
125
|
+
console.log(` Web UI: ${webHealthy ? "✓" : "✗"}`);
|
|
126
|
+
console.log(` Database: ${dbHealthy ? "✓" : "✗"}`);
|
|
127
|
+
console.log(` Overall: ${allHealthy ? "✓ Healthy" : "✗ Unhealthy"}`);
|
|
128
|
+
console.log(` Response time: ${uptime}ms`);
|
|
129
|
+
|
|
130
|
+
if (allHealthy) {
|
|
131
|
+
console.log("Health check passed");
|
|
132
|
+
Deno.exit(0);
|
|
133
|
+
} else {
|
|
134
|
+
console.error("Health check failed");
|
|
135
|
+
Deno.exit(1);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Handle graceful shutdown
|
|
140
|
+
Deno.addSignalListener("SIGTERM", () => {
|
|
141
|
+
console.log("Health check interrupted by SIGTERM");
|
|
142
|
+
Deno.exit(0);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
Deno.addSignalListener("SIGINT", () => {
|
|
146
|
+
console.log("Health check interrupted by SIGINT");
|
|
147
|
+
Deno.exit(0);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Run the health check
|
|
151
|
+
if (import.meta.main) {
|
|
152
|
+
main().catch((error) => {
|
|
153
|
+
console.error("Health check failed with error:", error);
|
|
154
|
+
Deno.exit(1);
|
|
155
|
+
});
|
|
156
|
+
}
|