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.
Files changed (79) hide show
  1. package/README.md +100 -5
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/better-sqlite3-shared.d.ts +12 -0
  4. package/dist/better-sqlite3-shared.d.ts.map +1 -0
  5. package/dist/better-sqlite3-shared.js +143 -0
  6. package/dist/better-sqlite3-shared.js.map +1 -0
  7. package/dist/better-sqlite3.d.ts +4 -0
  8. package/dist/better-sqlite3.d.ts.map +1 -0
  9. package/dist/better-sqlite3.js +8 -0
  10. package/dist/better-sqlite3.js.map +1 -0
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +21 -16
  13. package/dist/cli.js.map +1 -1
  14. package/dist/node-index.d.ts +98 -2
  15. package/dist/node-index.d.ts.map +1 -1
  16. package/dist/node-index.js +312 -6
  17. package/dist/node-index.js.map +1 -1
  18. package/dist/node-wrapper.d.ts.map +1 -1
  19. package/dist/node-wrapper.js +5 -3
  20. package/dist/node-wrapper.js.map +1 -1
  21. package/dist/types/index.d.ts.map +1 -1
  22. package/dist/types/index.js.map +1 -1
  23. package/dist/types/node-types.d.ts +12 -0
  24. package/dist/types/node-types.d.ts.map +1 -1
  25. package/dist/types/node-types.js.map +1 -1
  26. package/dist/vscode/extension.d.ts.map +1 -1
  27. package/dist/vscode/extension.js +4 -4
  28. package/dist/vscode/extension.js.map +1 -1
  29. package/examples/basic-usage.d.ts +1 -1
  30. package/examples/vscode-extension-example/src/extension.ts +15 -6
  31. package/examples/vscode-extension-integration.d.ts +24 -17
  32. package/examples/vscode-extension-integration.js +140 -106
  33. package/examples/vscode-extension-integration.ts +1 -1
  34. package/{src → legacy}/benchmarks/memory-benchmarks.ts +85 -51
  35. package/{src → legacy}/benchmarks/run-benchmarks.ts +32 -10
  36. package/legacy/better-sqlite3-shared.ts +157 -0
  37. package/legacy/better-sqlite3.ts +4 -0
  38. package/{src → legacy}/cli.ts +14 -4
  39. package/{src → legacy}/config.ts +2 -1
  40. package/{src → legacy}/core/crdt.ts +4 -1
  41. package/{src → legacy}/core/database.ts +57 -22
  42. package/{src → legacy}/healthcheck.ts +11 -5
  43. package/{src → legacy}/http/api-server.ts +125 -21
  44. package/{src → legacy}/index.ts +2 -2
  45. package/{src → legacy}/logic/rules.ts +3 -1
  46. package/{src → legacy}/main.ts +11 -4
  47. package/legacy/node-index.ts +823 -0
  48. package/{src → legacy}/node-wrapper.ts +18 -9
  49. package/{src → legacy}/sqlite-compat.ts +63 -16
  50. package/{src → legacy}/sqlite3-compat.ts +2 -2
  51. package/{src → legacy}/storage/kv-storage.ts +3 -1
  52. package/{src → legacy}/tests/core.test.ts +37 -13
  53. package/{src → legacy}/tests/fixtures/test-data.json +6 -1
  54. package/{src → legacy}/tests/integration/api-server.test.ts +110 -8
  55. package/{src → legacy}/tests/integration/mesh-network.test.ts +8 -2
  56. package/{src → legacy}/tests/logic.test.ts +6 -2
  57. package/{src → legacy}/tests/performance/load.test.ts +4 -2
  58. package/{src → legacy}/tests/security/input-validation.test.ts +5 -1
  59. package/{src → legacy}/tests/unit/core.test.ts +13 -3
  60. package/{src → legacy}/tests/unit/subscriptions.test.ts +1 -1
  61. package/{src → legacy}/tests/vscode_extension_test.ts +39 -11
  62. package/{src → legacy}/types/node-types.ts +14 -0
  63. package/{src → legacy}/vscode/extension.ts +37 -14
  64. package/package.json +19 -9
  65. package/scripts/compiled-crud-verify.ts +3 -1
  66. package/scripts/dogfood.ts +55 -16
  67. package/scripts/postinstall.js +4 -3
  68. package/scripts/release-check.js +190 -0
  69. package/scripts/run-tests.ts +5 -2
  70. package/scripts/update-changelog.js +214 -0
  71. package/web/svelte/package.json +5 -5
  72. package/src/node-index.ts +0 -385
  73. /package/{src → legacy}/main.rs +0 -0
  74. /package/{src → legacy}/network/websocket-server.ts +0 -0
  75. /package/{src → legacy}/tests/fixtures/performance-data.json +0 -0
  76. /package/{src → legacy}/tests/unit/vector-search.test.ts +0 -0
  77. /package/{src → legacy}/types/index.ts +0 -0
  78. /package/{src → legacy}/util/debug.ts +0 -0
  79. /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
+ }
@@ -0,0 +1,4 @@
1
+ import { BetterSQLite3Database, BetterSQLite3Statement } from "./node-index";
2
+
3
+ export default BetterSQLite3Database;
4
+ export { BetterSQLite3Database, BetterSQLite3Statement };
@@ -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(fs.readFileSync(path.join(__dirname, "../package.json"), "utf8"));
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"] || path.join(require("os").homedir(), ".pluresdb"),
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("Error:", error instanceof Error ? error.message : String(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("Fatal error:", error instanceof Error ? error.message : String(error));
236
+ console.error(
237
+ "Fatal error:",
238
+ error instanceof Error ? error.message : String(error),
239
+ );
230
240
  process.exit(1);
231
241
  });
@@ -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(local: NodeRecord | null, incoming: NodeRecord): NodeRecord {
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 { connectToPeer, type MeshServer, startMeshServer } from "../network/websocket-server.ts";
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 { RuleEngine, type Rule, type RuleContext } from "../logic/rules.ts";
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(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);
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(data: Record<string, unknown>): Record<string, unknown> {
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<string, Set<(node: NodeRecord | null) => void>> = new Map();
60
- private readonly anyListeners: Set<(event: { id: string; node: NodeRecord | null }) => void> =
61
- new Set();
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
- typeof record.type === "string" ? (record.type as string) : (existing?.type ?? undefined),
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) this.vectorIndex.upsert(id, merged.vector);
144
- else this.vectorIndex.remove(id);
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>>(id: string): Promise<(T & { id: string }) | null> {
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((node.data ?? {}) as Record<string, unknown>);
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) => ({ ...s.node, similarity: s.score }));
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) throw new Error(`Version not found for node ${id} at timestamp ${timestamp}`);
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(callback: (event: { id: string; node: NodeRecord | null }) => void): () => void {
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(callback: (event: { id: string; node: NodeRecord | null }) => void): void {
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(JSON.stringify({ type: "sync_request", originId: this.peerId }));
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(`API health check failed: ${response.status} ${response.statusText}`);
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: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
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(`Web health check failed: ${response.status} ${response.statusText}`);
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") || contentType?.includes("application/json");
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 healthStatus: HealthStatus = {
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(opts: { port: number; db: GunDB }): ApiServerHandle {
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()) send({ id: n.id, node: { id: n.id, data: n.data } });
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 Record<string, unknown> | null;
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) return json({ error: "missing body {id,data}" }, 400);
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) return json({ error: "missing id or timestamp" }, 400);
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(mapPath.startsWith("/") ? `.${mapPath}` : mapPath, STATIC_ROOT);
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
- ? "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 }) });
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({ "content-type": "text/html; charset=utf-8" }),
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
- e && typeof e === "object" && "message" in e ? String((e as any).message) : String(e);
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
  };
@@ -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 { NodeRecord, MeshMessage, VectorClock } from "./types/index.ts";
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 { startMeshServer, connectToPeer } from "./network/websocket-server.ts";
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";