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