pluresdb 1.0.1 → 1.3.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/README.md +100 -5
- package/dist/.tsbuildinfo +1 -1
- package/dist/better-sqlite3-shared.d.ts +12 -0
- package/dist/better-sqlite3-shared.d.ts.map +1 -0
- package/dist/better-sqlite3-shared.js +143 -0
- package/dist/better-sqlite3-shared.js.map +1 -0
- package/dist/better-sqlite3.d.ts +4 -0
- package/dist/better-sqlite3.d.ts.map +1 -0
- package/dist/better-sqlite3.js +8 -0
- package/dist/better-sqlite3.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +21 -16
- package/dist/cli.js.map +1 -1
- package/dist/node-index.d.ts +98 -2
- package/dist/node-index.d.ts.map +1 -1
- package/dist/node-index.js +312 -6
- package/dist/node-index.js.map +1 -1
- package/dist/node-wrapper.d.ts.map +1 -1
- package/dist/node-wrapper.js +5 -3
- package/dist/node-wrapper.js.map +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/node-types.d.ts +12 -0
- package/dist/types/node-types.d.ts.map +1 -1
- package/dist/types/node-types.js.map +1 -1
- package/dist/vscode/extension.d.ts.map +1 -1
- package/dist/vscode/extension.js +4 -4
- package/dist/vscode/extension.js.map +1 -1
- package/examples/basic-usage.d.ts +1 -1
- package/examples/vscode-extension-example/src/extension.ts +15 -6
- package/examples/vscode-extension-integration.d.ts +24 -17
- package/examples/vscode-extension-integration.js +140 -106
- package/examples/vscode-extension-integration.ts +1 -1
- package/{src → legacy}/benchmarks/memory-benchmarks.ts +85 -51
- package/{src → legacy}/benchmarks/run-benchmarks.ts +32 -10
- package/legacy/better-sqlite3-shared.ts +157 -0
- package/legacy/better-sqlite3.ts +4 -0
- package/{src → legacy}/cli.ts +14 -4
- package/{src → legacy}/config.ts +2 -1
- package/{src → legacy}/core/crdt.ts +4 -1
- package/{src → legacy}/core/database.ts +57 -22
- package/{src → legacy}/healthcheck.ts +11 -5
- package/{src → legacy}/http/api-server.ts +125 -21
- package/{src → legacy}/index.ts +2 -2
- package/{src → legacy}/logic/rules.ts +3 -1
- package/{src → legacy}/main.ts +11 -4
- package/legacy/node-index.ts +823 -0
- package/{src → legacy}/node-wrapper.ts +18 -9
- package/{src → legacy}/sqlite-compat.ts +63 -16
- package/{src → legacy}/sqlite3-compat.ts +2 -2
- package/{src → legacy}/storage/kv-storage.ts +3 -1
- package/{src → legacy}/tests/core.test.ts +37 -13
- package/{src → legacy}/tests/fixtures/test-data.json +6 -1
- package/{src → legacy}/tests/integration/api-server.test.ts +110 -8
- package/{src → legacy}/tests/integration/mesh-network.test.ts +8 -2
- package/{src → legacy}/tests/logic.test.ts +6 -2
- package/{src → legacy}/tests/performance/load.test.ts +4 -2
- package/{src → legacy}/tests/security/input-validation.test.ts +5 -1
- package/{src → legacy}/tests/unit/core.test.ts +13 -3
- package/{src → legacy}/tests/unit/subscriptions.test.ts +1 -1
- package/{src → legacy}/tests/vscode_extension_test.ts +39 -11
- package/{src → legacy}/types/node-types.ts +14 -0
- package/{src → legacy}/vscode/extension.ts +37 -14
- package/package.json +19 -9
- package/scripts/compiled-crud-verify.ts +3 -1
- package/scripts/dogfood.ts +55 -16
- package/scripts/postinstall.js +4 -3
- package/scripts/release-check.js +190 -0
- package/scripts/run-tests.ts +5 -2
- package/scripts/update-changelog.js +214 -0
- package/web/svelte/package.json +5 -5
- package/src/node-index.ts +0 -385
- /package/{src → legacy}/main.rs +0 -0
- /package/{src → legacy}/network/websocket-server.ts +0 -0
- /package/{src → legacy}/tests/fixtures/performance-data.json +0 -0
- /package/{src → legacy}/tests/unit/vector-search.test.ts +0 -0
- /package/{src → legacy}/types/index.ts +0 -0
- /package/{src → legacy}/util/debug.ts +0 -0
- /package/{src → legacy}/vector/index.ts +0 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { QueryResult } from "./types/node-types";
|
|
2
|
+
|
|
3
|
+
export function isPlainObject(
|
|
4
|
+
value: unknown,
|
|
5
|
+
): value is Record<string, unknown> {
|
|
6
|
+
if (value === null || typeof value !== "object") {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
const proto = Object.getPrototypeOf(value);
|
|
10
|
+
return proto === Object.prototype || proto === null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function normalizeParameterInput(args: unknown[]): unknown[] {
|
|
14
|
+
if (args.length === 0) {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
if (args.length === 1) {
|
|
18
|
+
const first = args[0];
|
|
19
|
+
if (Array.isArray(first)) {
|
|
20
|
+
return first;
|
|
21
|
+
}
|
|
22
|
+
if (isPlainObject(first)) {
|
|
23
|
+
return [first];
|
|
24
|
+
}
|
|
25
|
+
return [first];
|
|
26
|
+
}
|
|
27
|
+
return args;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function expandDotNotation(
|
|
31
|
+
row: Record<string, unknown>,
|
|
32
|
+
): Record<string, unknown> {
|
|
33
|
+
const result: Record<string, unknown> = {};
|
|
34
|
+
for (const [key, value] of Object.entries(row)) {
|
|
35
|
+
const parts = key.split(".");
|
|
36
|
+
let cursor: Record<string, unknown> = result;
|
|
37
|
+
for (let index = 0; index < parts.length; index++) {
|
|
38
|
+
const part = parts[index];
|
|
39
|
+
if (index === parts.length - 1) {
|
|
40
|
+
cursor[part] = value;
|
|
41
|
+
} else {
|
|
42
|
+
const next = cursor[part];
|
|
43
|
+
if (!isPlainObject(next)) {
|
|
44
|
+
cursor[part] = {};
|
|
45
|
+
}
|
|
46
|
+
cursor = cursor[part] as Record<string, unknown>;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function shapeRow(
|
|
54
|
+
row: unknown,
|
|
55
|
+
columns: string[] | undefined,
|
|
56
|
+
mode: { raw: boolean; pluck: boolean; expand: boolean },
|
|
57
|
+
): unknown {
|
|
58
|
+
if (mode.raw) {
|
|
59
|
+
return row;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let normalized: unknown;
|
|
63
|
+
|
|
64
|
+
if (Array.isArray(row)) {
|
|
65
|
+
if (columns && columns.length > 0) {
|
|
66
|
+
const mapped: Record<string, unknown> = {};
|
|
67
|
+
columns.forEach((column, index) => {
|
|
68
|
+
mapped[column] = row[index];
|
|
69
|
+
});
|
|
70
|
+
normalized = mapped;
|
|
71
|
+
} else {
|
|
72
|
+
normalized = [...row];
|
|
73
|
+
}
|
|
74
|
+
} else if (isPlainObject(row)) {
|
|
75
|
+
normalized = { ...(row as Record<string, unknown>) };
|
|
76
|
+
} else {
|
|
77
|
+
normalized = row;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (mode.pluck) {
|
|
81
|
+
if (Array.isArray(row)) {
|
|
82
|
+
return row[0];
|
|
83
|
+
}
|
|
84
|
+
if (isPlainObject(row)) {
|
|
85
|
+
const keys = Object.keys(row as Record<string, unknown>);
|
|
86
|
+
return keys.length > 0
|
|
87
|
+
? (row as Record<string, unknown>)[keys[0]]
|
|
88
|
+
: undefined;
|
|
89
|
+
}
|
|
90
|
+
if (columns && columns.length > 0 && isPlainObject(normalized)) {
|
|
91
|
+
return (normalized as Record<string, unknown>)[columns[0]];
|
|
92
|
+
}
|
|
93
|
+
return normalized;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (mode.expand && isPlainObject(normalized)) {
|
|
97
|
+
return expandDotNotation(normalized as Record<string, unknown>);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return normalized;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function normalizeQueryResult(raw: unknown): QueryResult {
|
|
104
|
+
if (
|
|
105
|
+
raw && typeof raw === "object" && "rows" in (raw as Record<string, unknown>)
|
|
106
|
+
) {
|
|
107
|
+
const result = raw as Partial<QueryResult> & Record<string, unknown>;
|
|
108
|
+
const columnsValue = Array.isArray(result.columns) ? result.columns : [];
|
|
109
|
+
const rowsValue = Array.isArray(result.rows) ? result.rows : [];
|
|
110
|
+
const changesValue = typeof result.changes === "number"
|
|
111
|
+
? result.changes
|
|
112
|
+
: 0;
|
|
113
|
+
const lastInsertRowIdValue = typeof result.lastInsertRowId === "number"
|
|
114
|
+
? result.lastInsertRowId
|
|
115
|
+
: typeof (result as Record<string, unknown>).lastInsertRowid === "number"
|
|
116
|
+
? Number((result as Record<string, unknown>).lastInsertRowid)
|
|
117
|
+
: 0;
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
rows: rowsValue,
|
|
121
|
+
columns: columnsValue,
|
|
122
|
+
changes: changesValue,
|
|
123
|
+
lastInsertRowId: lastInsertRowIdValue,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (Array.isArray(raw)) {
|
|
128
|
+
return {
|
|
129
|
+
rows: raw,
|
|
130
|
+
columns: [],
|
|
131
|
+
changes: 0,
|
|
132
|
+
lastInsertRowId: 0,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (raw === undefined || raw === null) {
|
|
137
|
+
return { rows: [], columns: [], changes: 0, lastInsertRowId: 0 };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
rows: [raw],
|
|
142
|
+
columns: [],
|
|
143
|
+
changes: 0,
|
|
144
|
+
lastInsertRowId: 0,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function splitSqlStatements(sql: string): string[] {
|
|
149
|
+
return sql
|
|
150
|
+
.split(/;\s*(?=(?:[^"']|"[^"]*"|'[^']*')*$)/)
|
|
151
|
+
.map((statement) => statement.trim())
|
|
152
|
+
.filter((statement) => statement.length > 0);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function sanitizeDataDirName(name: string): string {
|
|
156
|
+
return name.replace(/[\s\\/:]+/g, "_").replace(/_+/g, "_");
|
|
157
|
+
}
|
package/{src → legacy}/cli.ts
RENAMED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { PluresNode } from "./node-wrapper";
|
|
9
9
|
import * as path from "path";
|
|
10
10
|
import * as fs from "fs";
|
|
11
|
+
import process from "node:process";
|
|
11
12
|
|
|
12
13
|
// Parse command line arguments
|
|
13
14
|
const args = process.argv.slice(2);
|
|
@@ -43,7 +44,9 @@ Examples:
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
if (command === "--version") {
|
|
46
|
-
const packageJson = JSON.parse(
|
|
47
|
+
const packageJson = JSON.parse(
|
|
48
|
+
fs.readFileSync(path.join(__dirname, "../package.json"), "utf8"),
|
|
49
|
+
);
|
|
47
50
|
console.log(packageJson.version);
|
|
48
51
|
process.exit(0);
|
|
49
52
|
}
|
|
@@ -103,7 +106,8 @@ async function main() {
|
|
|
103
106
|
const config = {
|
|
104
107
|
port: options.port ? parseInt(options.port) : 34567,
|
|
105
108
|
host: options.host || "localhost",
|
|
106
|
-
dataDir: options["data-dir"] ||
|
|
109
|
+
dataDir: options["data-dir"] ||
|
|
110
|
+
path.join(require("os").homedir(), ".pluresdb"),
|
|
107
111
|
webPort: options["web-port"] ? parseInt(options["web-port"]) : 34568,
|
|
108
112
|
logLevel: options["log-level"] || "info",
|
|
109
113
|
};
|
|
@@ -219,13 +223,19 @@ async function main() {
|
|
|
219
223
|
}
|
|
220
224
|
}
|
|
221
225
|
} catch (error) {
|
|
222
|
-
console.error(
|
|
226
|
+
console.error(
|
|
227
|
+
"Error:",
|
|
228
|
+
error instanceof Error ? error.message : String(error),
|
|
229
|
+
);
|
|
223
230
|
process.exit(1);
|
|
224
231
|
}
|
|
225
232
|
}
|
|
226
233
|
|
|
227
234
|
// Run the main function
|
|
228
235
|
main().catch((error) => {
|
|
229
|
-
console.error(
|
|
236
|
+
console.error(
|
|
237
|
+
"Fatal error:",
|
|
238
|
+
error instanceof Error ? error.message : String(error),
|
|
239
|
+
);
|
|
230
240
|
process.exit(1);
|
|
231
241
|
});
|
package/{src → legacy}/config.ts
RENAMED
|
@@ -27,7 +27,8 @@ export function getConfigPath(): string {
|
|
|
27
27
|
try {
|
|
28
28
|
const os = Deno.build.os;
|
|
29
29
|
if (os === "windows") {
|
|
30
|
-
const appData = Deno.env.get("APPDATA") || Deno.env.get("LOCALAPPDATA") ||
|
|
30
|
+
const appData = Deno.env.get("APPDATA") || Deno.env.get("LOCALAPPDATA") ||
|
|
31
|
+
".";
|
|
31
32
|
return `${appData}\\${appName}\\config.json`;
|
|
32
33
|
}
|
|
33
34
|
const home = Deno.env.get("HOME") || ".";
|
|
@@ -51,7 +51,10 @@ function deepMergeWithDeletes(
|
|
|
51
51
|
return { data: out, state: outState };
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
export function mergeNodes(
|
|
54
|
+
export function mergeNodes(
|
|
55
|
+
local: NodeRecord | null,
|
|
56
|
+
incoming: NodeRecord,
|
|
57
|
+
): NodeRecord {
|
|
55
58
|
if (!local) return incoming;
|
|
56
59
|
if (local.id !== incoming.id) {
|
|
57
60
|
throw new Error("mergeNodes called with mismatched ids");
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { KvStorage } from "../storage/kv-storage.ts";
|
|
2
2
|
import type { MeshMessage, NodeRecord } from "../types/index.ts";
|
|
3
3
|
import { mergeNodes } from "./crdt.ts";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
connectToPeer,
|
|
6
|
+
type MeshServer,
|
|
7
|
+
startMeshServer,
|
|
8
|
+
} from "../network/websocket-server.ts";
|
|
5
9
|
import { debugLog } from "../util/debug.ts";
|
|
6
|
-
import {
|
|
10
|
+
import { type Rule, type RuleContext, RuleEngine } from "../logic/rules.ts";
|
|
7
11
|
import { BruteForceVectorIndex } from "../vector/index.ts";
|
|
8
12
|
|
|
9
13
|
const FUNCTION_PLACEHOLDER = "[sanitized function]";
|
|
@@ -32,13 +36,21 @@ function sanitizeValue(value: unknown, seen: WeakSet<object>): unknown {
|
|
|
32
36
|
return clean;
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
function sanitizeRecord(
|
|
36
|
-
|
|
37
|
-
|
|
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
|
+
}
|
|
38
48
|
return result;
|
|
39
49
|
}
|
|
40
50
|
|
|
41
|
-
function sanitizeForOutput(
|
|
51
|
+
function sanitizeForOutput(
|
|
52
|
+
data: Record<string, unknown>,
|
|
53
|
+
): Record<string, unknown> {
|
|
42
54
|
const clean = sanitizeRecord(data);
|
|
43
55
|
if (typeof clean["toString"] !== "string") {
|
|
44
56
|
clean["toString"] = Object.prototype.toString.call(clean);
|
|
@@ -56,9 +68,13 @@ export interface DatabaseOptions {
|
|
|
56
68
|
|
|
57
69
|
export class GunDB {
|
|
58
70
|
private readonly storage: KvStorage;
|
|
59
|
-
private readonly listeners: Map<
|
|
60
|
-
|
|
61
|
-
|
|
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();
|
|
62
78
|
private readonly peerId: string;
|
|
63
79
|
private meshServer: MeshServer | null = null;
|
|
64
80
|
private readonly peerSockets: Set<WebSocket> = new Set();
|
|
@@ -129,8 +145,9 @@ export class GunDB {
|
|
|
129
145
|
id,
|
|
130
146
|
data: record,
|
|
131
147
|
vector,
|
|
132
|
-
type:
|
|
133
|
-
|
|
148
|
+
type: typeof record.type === "string"
|
|
149
|
+
? (record.type as string)
|
|
150
|
+
: (existing?.type ?? undefined),
|
|
134
151
|
timestamp: now,
|
|
135
152
|
state: newState,
|
|
136
153
|
vectorClock: newClock,
|
|
@@ -140,19 +157,24 @@ export class GunDB {
|
|
|
140
157
|
await this.storage.setNode(merged);
|
|
141
158
|
debugLog("put() merged", { id, timestamp: merged.timestamp });
|
|
142
159
|
this.emit(id, merged);
|
|
143
|
-
if (merged.vector && merged.vector.length > 0)
|
|
144
|
-
|
|
160
|
+
if (merged.vector && merged.vector.length > 0) {
|
|
161
|
+
this.vectorIndex.upsert(id, merged.vector);
|
|
162
|
+
} else this.vectorIndex.remove(id);
|
|
145
163
|
if (!suppressRules) {
|
|
146
164
|
await this.evaluateRules(merged);
|
|
147
165
|
}
|
|
148
166
|
this.broadcast({ type: "put", originId: this.peerId, node: merged });
|
|
149
167
|
}
|
|
150
168
|
|
|
151
|
-
async get<T = Record<string, unknown>>(
|
|
169
|
+
async get<T = Record<string, unknown>>(
|
|
170
|
+
id: string,
|
|
171
|
+
): Promise<(T & { id: string }) | null> {
|
|
152
172
|
this.ensureReady();
|
|
153
173
|
const node = await this.storage.getNode(id);
|
|
154
174
|
if (!node) return null;
|
|
155
|
-
const sanitized = sanitizeForOutput(
|
|
175
|
+
const sanitized = sanitizeForOutput(
|
|
176
|
+
(node.data ?? {}) as Record<string, unknown>,
|
|
177
|
+
);
|
|
156
178
|
return { id: node.id, ...(sanitized as T) };
|
|
157
179
|
}
|
|
158
180
|
|
|
@@ -217,7 +239,10 @@ export class GunDB {
|
|
|
217
239
|
if (Number.isFinite(score)) scored.push({ score, node });
|
|
218
240
|
}
|
|
219
241
|
scored.sort((a, b) => b.score - a.score);
|
|
220
|
-
return scored.slice(0, limit).map((s) => ({
|
|
242
|
+
return scored.slice(0, limit).map((s) => ({
|
|
243
|
+
...s.node,
|
|
244
|
+
similarity: s.score,
|
|
245
|
+
}));
|
|
221
246
|
}
|
|
222
247
|
|
|
223
248
|
// Type system convenience
|
|
@@ -239,7 +264,11 @@ export class GunDB {
|
|
|
239
264
|
this.ensureReady();
|
|
240
265
|
const history = await this.getNodeHistory(id);
|
|
241
266
|
const version = history.find((v) => v.timestamp === timestamp);
|
|
242
|
-
if (!version)
|
|
267
|
+
if (!version) {
|
|
268
|
+
throw new Error(
|
|
269
|
+
`Version not found for node ${id} at timestamp ${timestamp}`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
243
272
|
|
|
244
273
|
// Restore by putting the historical version
|
|
245
274
|
await this.put(id, version.data);
|
|
@@ -254,12 +283,16 @@ export class GunDB {
|
|
|
254
283
|
}
|
|
255
284
|
|
|
256
285
|
// Any-change subscription (internal use for API streaming)
|
|
257
|
-
onAny(
|
|
286
|
+
onAny(
|
|
287
|
+
callback: (event: { id: string; node: NodeRecord | null }) => void,
|
|
288
|
+
): () => void {
|
|
258
289
|
this.ensureReady();
|
|
259
290
|
this.anyListeners.add(callback);
|
|
260
291
|
return () => this.offAny(callback);
|
|
261
292
|
}
|
|
262
|
-
offAny(
|
|
293
|
+
offAny(
|
|
294
|
+
callback: (event: { id: string; node: NodeRecord | null }) => void,
|
|
295
|
+
): void {
|
|
263
296
|
this.anyListeners.delete(callback);
|
|
264
297
|
}
|
|
265
298
|
|
|
@@ -302,7 +335,9 @@ export class GunDB {
|
|
|
302
335
|
onOpen: (s) => {
|
|
303
336
|
// Request a snapshot
|
|
304
337
|
try {
|
|
305
|
-
s.send(
|
|
338
|
+
s.send(
|
|
339
|
+
JSON.stringify({ type: "sync_request", originId: this.peerId }),
|
|
340
|
+
);
|
|
306
341
|
} catch {
|
|
307
342
|
/* ignore */
|
|
308
343
|
}
|
|
@@ -386,9 +421,9 @@ export class GunDB {
|
|
|
386
421
|
const merged = mergeNodes(existing, node);
|
|
387
422
|
await this.storage.setNode(merged);
|
|
388
423
|
this.emit(node.id, merged);
|
|
389
|
-
if (merged.vector && merged.vector.length > 0)
|
|
424
|
+
if (merged.vector && merged.vector.length > 0) {
|
|
390
425
|
this.vectorIndex.upsert(node.id, merged.vector);
|
|
391
|
-
else this.vectorIndex.remove(node.id);
|
|
426
|
+
} else this.vectorIndex.remove(node.id);
|
|
392
427
|
await this.evaluateRules(merged);
|
|
393
428
|
try {
|
|
394
429
|
ctx.broadcast(msg, ctx.source);
|
|
@@ -32,7 +32,9 @@ async function checkApiHealth(): Promise<boolean> {
|
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
if (!response.ok) {
|
|
35
|
-
console.error(
|
|
35
|
+
console.error(
|
|
36
|
+
`API health check failed: ${response.status} ${response.statusText}`,
|
|
37
|
+
);
|
|
36
38
|
return false;
|
|
37
39
|
}
|
|
38
40
|
|
|
@@ -49,19 +51,23 @@ async function checkWebHealth(): Promise<boolean> {
|
|
|
49
51
|
const response = await fetch(`http://${HOST}:${WEB_PORT}/`, {
|
|
50
52
|
method: "GET",
|
|
51
53
|
headers: {
|
|
52
|
-
Accept:
|
|
54
|
+
Accept:
|
|
55
|
+
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
53
56
|
"User-Agent": "pluresdb-healthcheck/1.0.0",
|
|
54
57
|
},
|
|
55
58
|
signal: AbortSignal.timeout(5000), // 5 second timeout
|
|
56
59
|
});
|
|
57
60
|
|
|
58
61
|
if (!response.ok) {
|
|
59
|
-
console.error(
|
|
62
|
+
console.error(
|
|
63
|
+
`Web health check failed: ${response.status} ${response.statusText}`,
|
|
64
|
+
);
|
|
60
65
|
return false;
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
const contentType = response.headers.get("content-type");
|
|
64
|
-
return contentType?.includes("text/html") ||
|
|
69
|
+
return contentType?.includes("text/html") ||
|
|
70
|
+
contentType?.includes("application/json");
|
|
65
71
|
} catch (error) {
|
|
66
72
|
console.error(`Web health check error: ${error.message}`);
|
|
67
73
|
return false;
|
|
@@ -109,7 +115,7 @@ async function main(): Promise<void> {
|
|
|
109
115
|
const uptime = Date.now() - startTime;
|
|
110
116
|
const allHealthy = apiHealthy && webHealthy && dbHealthy;
|
|
111
117
|
|
|
112
|
-
const
|
|
118
|
+
const _healthStatus: HealthStatus = {
|
|
113
119
|
status: allHealthy ? "healthy" : "unhealthy",
|
|
114
120
|
checks: {
|
|
115
121
|
api: apiHealthy,
|
|
@@ -17,7 +17,9 @@ function corsHeaders(extra?: Record<string, string>): Headers {
|
|
|
17
17
|
return headers;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export function startApiServer(
|
|
20
|
+
export function startApiServer(
|
|
21
|
+
opts: { port: number; db: GunDB },
|
|
22
|
+
): ApiServerHandle {
|
|
21
23
|
const { port, db } = opts;
|
|
22
24
|
|
|
23
25
|
const handler = async (req: Request): Promise<Response> => {
|
|
@@ -40,7 +42,9 @@ export function startApiServer(opts: { port: number; db: GunDB }): ApiServerHand
|
|
|
40
42
|
const cb = (e: { id: string; node: unknown | null }) => send(e);
|
|
41
43
|
db.onAny(cb as any);
|
|
42
44
|
(async () => {
|
|
43
|
-
for await (const n of db.list())
|
|
45
|
+
for await (const n of db.list()) {
|
|
46
|
+
send({ id: n.id, node: { id: n.id, data: n.data } });
|
|
47
|
+
}
|
|
44
48
|
})();
|
|
45
49
|
return () => db.offAny(cb as any);
|
|
46
50
|
},
|
|
@@ -80,7 +84,9 @@ export function startApiServer(opts: { port: number; db: GunDB }): ApiServerHand
|
|
|
80
84
|
return json(cfg);
|
|
81
85
|
}
|
|
82
86
|
if (req.method === "POST") {
|
|
83
|
-
const body = (await req.json().catch(() => null)) as
|
|
87
|
+
const body = (await req.json().catch(() => null)) as
|
|
88
|
+
| Record<string, unknown>
|
|
89
|
+
| null;
|
|
84
90
|
if (!body) return json({ error: "missing body" }, 400);
|
|
85
91
|
const current = await loadConfig();
|
|
86
92
|
const next = { ...current, ...body } as Record<string, unknown>;
|
|
@@ -100,7 +106,9 @@ export function startApiServer(opts: { port: number; db: GunDB }): ApiServerHand
|
|
|
100
106
|
const body = (await req.json().catch(() => null)) as
|
|
101
107
|
| { id?: string; data?: Record<string, unknown> }
|
|
102
108
|
| null;
|
|
103
|
-
if (!body?.id || !body?.data)
|
|
109
|
+
if (!body?.id || !body?.data) {
|
|
110
|
+
return json({ error: "missing body {id,data}" }, 400);
|
|
111
|
+
}
|
|
104
112
|
await db.put(body.id, body.data);
|
|
105
113
|
return json({ ok: true });
|
|
106
114
|
}
|
|
@@ -128,7 +136,8 @@ export function startApiServer(opts: { port: number; db: GunDB }): ApiServerHand
|
|
|
128
136
|
return json(nodes.map((n) => ({ id: n.id, data: n.data })));
|
|
129
137
|
}
|
|
130
138
|
case "/api/list": {
|
|
131
|
-
const out: Array<{ id: string; data: Record<string, unknown> }> =
|
|
139
|
+
const out: Array<{ id: string; data: Record<string, unknown> }> =
|
|
140
|
+
[];
|
|
132
141
|
for await (const n of db.list()) {
|
|
133
142
|
out.push({ id: n.id, data: n.data as Record<string, unknown> });
|
|
134
143
|
}
|
|
@@ -157,10 +166,97 @@ export function startApiServer(opts: { port: number; db: GunDB }): ApiServerHand
|
|
|
157
166
|
case "/api/restore": {
|
|
158
167
|
const id = url.searchParams.get("id");
|
|
159
168
|
const timestamp = url.searchParams.get("timestamp");
|
|
160
|
-
if (!id || !timestamp)
|
|
169
|
+
if (!id || !timestamp) {
|
|
170
|
+
return json({ error: "missing id or timestamp" }, 400);
|
|
171
|
+
}
|
|
161
172
|
await db.restoreNodeVersion(id, parseInt(timestamp));
|
|
162
173
|
return json({ success: true });
|
|
163
174
|
}
|
|
175
|
+
case "/api/identity": {
|
|
176
|
+
if (req.method !== "POST") return json({ error: "method" }, 405);
|
|
177
|
+
const body = (await req.json().catch(() => null)) as
|
|
178
|
+
| { name?: string; email?: string }
|
|
179
|
+
| null;
|
|
180
|
+
if (!body?.name || !body?.email) {
|
|
181
|
+
return json({ error: "missing name or email" }, 400);
|
|
182
|
+
}
|
|
183
|
+
// Generate a simple identity (stub implementation)
|
|
184
|
+
const id = `peer_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
185
|
+
const publicKey = `pk_${Math.random().toString(36).substring(2)}`;
|
|
186
|
+
return json({ id, publicKey, name: body.name, email: body.email });
|
|
187
|
+
}
|
|
188
|
+
case "/api/peers/search": {
|
|
189
|
+
const q = url.searchParams.get("q") ?? "";
|
|
190
|
+
// Stub implementation - return empty array for now
|
|
191
|
+
// In a real implementation, this would search for peers in the network
|
|
192
|
+
return json([]);
|
|
193
|
+
}
|
|
194
|
+
case "/api/share": {
|
|
195
|
+
if (req.method !== "POST") return json({ error: "method" }, 405);
|
|
196
|
+
const body = (await req.json().catch(() => null)) as
|
|
197
|
+
| {
|
|
198
|
+
nodeId?: string;
|
|
199
|
+
targetPeerId?: string;
|
|
200
|
+
accessLevel?: string;
|
|
201
|
+
}
|
|
202
|
+
| null;
|
|
203
|
+
if (!body?.nodeId || !body?.targetPeerId) {
|
|
204
|
+
return json(
|
|
205
|
+
{ error: "missing nodeId or targetPeerId" },
|
|
206
|
+
400,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
// Stub implementation - generate a shared node ID
|
|
210
|
+
const sharedNodeId = `shared_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
211
|
+
return json({
|
|
212
|
+
sharedNodeId,
|
|
213
|
+
nodeId: body.nodeId,
|
|
214
|
+
targetPeerId: body.targetPeerId,
|
|
215
|
+
accessLevel: body.accessLevel || "read-only",
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
case "/api/share/accept": {
|
|
219
|
+
if (req.method !== "POST") return json({ error: "method" }, 405);
|
|
220
|
+
const body = (await req.json().catch(() => null)) as
|
|
221
|
+
| { sharedNodeId?: string }
|
|
222
|
+
| null;
|
|
223
|
+
if (!body?.sharedNodeId) {
|
|
224
|
+
return json({ error: "missing sharedNodeId" }, 400);
|
|
225
|
+
}
|
|
226
|
+
// Stub implementation - accept shared node
|
|
227
|
+
return json({ success: true, sharedNodeId: body.sharedNodeId });
|
|
228
|
+
}
|
|
229
|
+
case "/api/devices": {
|
|
230
|
+
if (req.method === "POST") {
|
|
231
|
+
const body = (await req.json().catch(() => null)) as
|
|
232
|
+
| { name?: string; type?: string }
|
|
233
|
+
| null;
|
|
234
|
+
if (!body?.name || !body?.type) {
|
|
235
|
+
return json({ error: "missing name or type" }, 400);
|
|
236
|
+
}
|
|
237
|
+
// Stub implementation - generate device ID
|
|
238
|
+
const id = `device_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
239
|
+
return json({
|
|
240
|
+
id,
|
|
241
|
+
name: body.name,
|
|
242
|
+
type: body.type,
|
|
243
|
+
status: "online",
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
// GET devices list
|
|
247
|
+
return json([]);
|
|
248
|
+
}
|
|
249
|
+
case "/api/devices/sync": {
|
|
250
|
+
if (req.method !== "POST") return json({ error: "method" }, 405);
|
|
251
|
+
const body = (await req.json().catch(() => null)) as
|
|
252
|
+
| { deviceId?: string }
|
|
253
|
+
| null;
|
|
254
|
+
if (!body?.deviceId) {
|
|
255
|
+
return json({ error: "missing deviceId" }, 400);
|
|
256
|
+
}
|
|
257
|
+
// Stub implementation - sync with device
|
|
258
|
+
return json({ success: true, deviceId: body.deviceId });
|
|
259
|
+
}
|
|
164
260
|
default:
|
|
165
261
|
return json({ error: "not found" }, 404);
|
|
166
262
|
}
|
|
@@ -168,27 +264,34 @@ export function startApiServer(opts: { port: number; db: GunDB }): ApiServerHand
|
|
|
168
264
|
|
|
169
265
|
if (req.method === "GET") {
|
|
170
266
|
const mapPath = path === "/" ? "/index.html" : path;
|
|
171
|
-
const fileUrl = new URL(
|
|
267
|
+
const fileUrl = new URL(
|
|
268
|
+
mapPath.startsWith("/") ? `.${mapPath}` : mapPath,
|
|
269
|
+
STATIC_ROOT,
|
|
270
|
+
);
|
|
172
271
|
try {
|
|
173
272
|
const data = await Deno.readFile(fileUrl);
|
|
174
273
|
const contentType = mapPath.endsWith(".html")
|
|
175
274
|
? "text/html; charset=utf-8"
|
|
176
275
|
: mapPath.endsWith(".js")
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return new Response(data, {
|
|
276
|
+
? "application/javascript"
|
|
277
|
+
: mapPath.endsWith(".css")
|
|
278
|
+
? "text/css"
|
|
279
|
+
: mapPath.endsWith(".json")
|
|
280
|
+
? "application/json"
|
|
281
|
+
: mapPath.endsWith(".svg")
|
|
282
|
+
? "image/svg+xml"
|
|
283
|
+
: mapPath.endsWith(".png")
|
|
284
|
+
? "image/png"
|
|
285
|
+
: "application/octet-stream";
|
|
286
|
+
return new Response(data, {
|
|
287
|
+
headers: corsHeaders({ "content-type": contentType }),
|
|
288
|
+
});
|
|
188
289
|
} catch {
|
|
189
290
|
if (path === "/" || path === "/index.html") {
|
|
190
291
|
return new Response(INDEX_HTML, {
|
|
191
|
-
headers: corsHeaders({
|
|
292
|
+
headers: corsHeaders({
|
|
293
|
+
"content-type": "text/html; charset=utf-8",
|
|
294
|
+
}),
|
|
192
295
|
});
|
|
193
296
|
}
|
|
194
297
|
}
|
|
@@ -196,8 +299,9 @@ export function startApiServer(opts: { port: number; db: GunDB }): ApiServerHand
|
|
|
196
299
|
|
|
197
300
|
return new Response("Not Found", { status: 404, headers: corsHeaders() });
|
|
198
301
|
} catch (e) {
|
|
199
|
-
const msg =
|
|
200
|
-
|
|
302
|
+
const msg = e && typeof e === "object" && "message" in e
|
|
303
|
+
? String((e as any).message)
|
|
304
|
+
: String(e);
|
|
201
305
|
return json({ error: msg }, 500);
|
|
202
306
|
}
|
|
203
307
|
};
|
package/{src → legacy}/index.ts
RENAMED
|
@@ -9,14 +9,14 @@ export { GunDB } from "./core/database.ts";
|
|
|
9
9
|
export type { DatabaseOptions, ServeOptions } from "./core/database.ts";
|
|
10
10
|
|
|
11
11
|
export { mergeNodes } from "./core/crdt.ts";
|
|
12
|
-
export type {
|
|
12
|
+
export type { MeshMessage, NodeRecord, VectorClock } from "./types/index.ts";
|
|
13
13
|
|
|
14
14
|
export { startApiServer } from "./http/api-server.ts";
|
|
15
15
|
export type { ApiServerHandle } from "./http/api-server.ts";
|
|
16
16
|
|
|
17
17
|
export { loadConfig, saveConfig } from "./config.ts";
|
|
18
18
|
|
|
19
|
-
export {
|
|
19
|
+
export { connectToPeer, startMeshServer } from "./network/websocket-server.ts";
|
|
20
20
|
export type { MeshServer } from "./network/websocket-server.ts";
|
|
21
21
|
|
|
22
22
|
export { RuleEngine } from "./logic/rules.ts";
|