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.
Files changed (84) hide show
  1. package/LICENSE +72 -0
  2. package/README.md +322 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/cli.d.ts +7 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +253 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/node-index.d.ts +52 -0
  9. package/dist/node-index.d.ts.map +1 -0
  10. package/dist/node-index.js +359 -0
  11. package/dist/node-index.js.map +1 -0
  12. package/dist/node-wrapper.d.ts +44 -0
  13. package/dist/node-wrapper.d.ts.map +1 -0
  14. package/dist/node-wrapper.js +294 -0
  15. package/dist/node-wrapper.js.map +1 -0
  16. package/dist/types/index.d.ts +28 -0
  17. package/dist/types/index.d.ts.map +1 -0
  18. package/dist/types/index.js +3 -0
  19. package/dist/types/index.js.map +1 -0
  20. package/dist/types/node-types.d.ts +59 -0
  21. package/dist/types/node-types.d.ts.map +1 -0
  22. package/dist/types/node-types.js +6 -0
  23. package/dist/types/node-types.js.map +1 -0
  24. package/dist/vscode/extension.d.ts +81 -0
  25. package/dist/vscode/extension.d.ts.map +1 -0
  26. package/dist/vscode/extension.js +309 -0
  27. package/dist/vscode/extension.js.map +1 -0
  28. package/examples/basic-usage.d.ts +2 -0
  29. package/examples/basic-usage.d.ts.map +1 -0
  30. package/examples/basic-usage.js +26 -0
  31. package/examples/basic-usage.js.map +1 -0
  32. package/examples/basic-usage.ts +29 -0
  33. package/examples/vscode-extension-example/README.md +95 -0
  34. package/examples/vscode-extension-example/package.json +49 -0
  35. package/examples/vscode-extension-example/src/extension.ts +163 -0
  36. package/examples/vscode-extension-example/tsconfig.json +12 -0
  37. package/examples/vscode-extension-integration.d.ts +24 -0
  38. package/examples/vscode-extension-integration.d.ts.map +1 -0
  39. package/examples/vscode-extension-integration.js +285 -0
  40. package/examples/vscode-extension-integration.js.map +1 -0
  41. package/examples/vscode-extension-integration.ts +41 -0
  42. package/package.json +115 -0
  43. package/scripts/compiled-crud-verify.ts +28 -0
  44. package/scripts/dogfood.ts +258 -0
  45. package/scripts/postinstall.js +155 -0
  46. package/scripts/run-tests.ts +175 -0
  47. package/scripts/setup-libclang.ps1 +209 -0
  48. package/src/benchmarks/memory-benchmarks.ts +316 -0
  49. package/src/benchmarks/run-benchmarks.ts +293 -0
  50. package/src/cli.ts +231 -0
  51. package/src/config.ts +49 -0
  52. package/src/core/crdt.ts +104 -0
  53. package/src/core/database.ts +494 -0
  54. package/src/healthcheck.ts +156 -0
  55. package/src/http/api-server.ts +334 -0
  56. package/src/index.ts +28 -0
  57. package/src/logic/rules.ts +44 -0
  58. package/src/main.rs +3 -0
  59. package/src/main.ts +190 -0
  60. package/src/network/websocket-server.ts +115 -0
  61. package/src/node-index.ts +385 -0
  62. package/src/node-wrapper.ts +320 -0
  63. package/src/sqlite-compat.ts +586 -0
  64. package/src/sqlite3-compat.ts +55 -0
  65. package/src/storage/kv-storage.ts +71 -0
  66. package/src/tests/core.test.ts +281 -0
  67. package/src/tests/fixtures/performance-data.json +71 -0
  68. package/src/tests/fixtures/test-data.json +124 -0
  69. package/src/tests/integration/api-server.test.ts +232 -0
  70. package/src/tests/integration/mesh-network.test.ts +297 -0
  71. package/src/tests/logic.test.ts +30 -0
  72. package/src/tests/performance/load.test.ts +288 -0
  73. package/src/tests/security/input-validation.test.ts +282 -0
  74. package/src/tests/unit/core.test.ts +216 -0
  75. package/src/tests/unit/subscriptions.test.ts +135 -0
  76. package/src/tests/unit/vector-search.test.ts +173 -0
  77. package/src/tests/vscode_extension_test.ts +253 -0
  78. package/src/types/index.ts +32 -0
  79. package/src/types/node-types.ts +66 -0
  80. package/src/util/debug.ts +14 -0
  81. package/src/vector/index.ts +59 -0
  82. package/src/vscode/extension.ts +364 -0
  83. package/web/README.md +27 -0
  84. package/web/svelte/package.json +31 -0
@@ -0,0 +1,293 @@
1
+ #!/usr/bin/env -S deno run -A
2
+
3
+ import { GunDB } from "../core/database.ts";
4
+
5
+ interface BenchmarkResult {
6
+ name: string;
7
+ operations: number;
8
+ totalTime: number;
9
+ averageTime: number;
10
+ operationsPerSecond: number;
11
+ memoryUsage?: number;
12
+ }
13
+
14
+ class BenchmarkRunner {
15
+ private results: BenchmarkResult[] = [];
16
+
17
+ async runBenchmark(
18
+ name: string,
19
+ operations: number,
20
+ operation: () => Promise<void>,
21
+ ): Promise<BenchmarkResult> {
22
+ console.log(`Running benchmark: ${name} (${operations} operations)`);
23
+
24
+ const startTime = performance.now();
25
+ const startMemory = (performance as any).memory?.usedJSHeapSize || 0;
26
+
27
+ for (let i = 0; i < operations; i++) {
28
+ await operation();
29
+ }
30
+
31
+ const endTime = performance.now();
32
+ const endMemory = (performance as any).memory?.usedJSHeapSize || 0;
33
+
34
+ const totalTime = endTime - startTime;
35
+ const averageTime = totalTime / operations;
36
+ const operationsPerSecond = (operations / totalTime) * 1000;
37
+ const memoryUsage = endMemory - startMemory;
38
+
39
+ const result: BenchmarkResult = {
40
+ name,
41
+ operations,
42
+ totalTime,
43
+ averageTime,
44
+ operationsPerSecond,
45
+ memoryUsage,
46
+ };
47
+
48
+ this.results.push(result);
49
+ console.log(` ✓ ${operationsPerSecond.toFixed(2)} ops/sec (${averageTime.toFixed(2)}ms avg)`);
50
+
51
+ return result;
52
+ }
53
+
54
+ printSummary() {
55
+ console.log("\n" + "=".repeat(80));
56
+ console.log("BENCHMARK SUMMARY");
57
+ console.log("=".repeat(80));
58
+
59
+ this.results.forEach((result) => {
60
+ console.log(`${result.name}:`);
61
+ console.log(` Operations: ${result.operations.toLocaleString()}`);
62
+ console.log(` Total Time: ${result.totalTime.toFixed(2)}ms`);
63
+ console.log(` Average Time: ${result.averageTime.toFixed(2)}ms`);
64
+ console.log(` Operations/sec: ${result.operationsPerSecond.toFixed(2)}`);
65
+ if (result.memoryUsage) {
66
+ console.log(` Memory Usage: ${(result.memoryUsage / 1024 / 1024).toFixed(2)}MB`);
67
+ }
68
+ console.log();
69
+ });
70
+ }
71
+ }
72
+
73
+ async function runCoreBenchmarks() {
74
+ const runner = new BenchmarkRunner();
75
+ const db = new GunDB();
76
+
77
+ try {
78
+ const kvPath = await Deno.makeTempFile({
79
+ prefix: "kv_",
80
+ suffix: ".sqlite",
81
+ });
82
+ await db.ready(kvPath);
83
+
84
+ console.log("Starting Core Database Benchmarks...\n");
85
+
86
+ // Benchmark 1: Basic CRUD Operations
87
+ await runner.runBenchmark("Basic CRUD Operations", 1000, async () => {
88
+ const id = `crud:${Math.random()}`;
89
+ await db.put(id, { value: Math.random(), timestamp: Date.now() });
90
+ await db.get(id);
91
+ await db.delete(id);
92
+ });
93
+
94
+ // Benchmark 2: Bulk Insert
95
+ await runner.runBenchmark("Bulk Insert", 5000, async () => {
96
+ const id = `bulk:${Math.random()}`;
97
+ await db.put(id, {
98
+ data: "x".repeat(100),
99
+ timestamp: Date.now(),
100
+ random: Math.random(),
101
+ });
102
+ });
103
+
104
+ // Benchmark 3: Bulk Read
105
+ // First populate with data
106
+ for (let i = 0; i < 1000; i++) {
107
+ await db.put(`read:${i}`, { value: i, data: `Data ${i}` });
108
+ }
109
+
110
+ let readCount = 0;
111
+ await runner.runBenchmark("Bulk Read", 1000, async () => {
112
+ await db.get(`read:${readCount % 1000}`);
113
+ readCount++;
114
+ });
115
+
116
+ // Benchmark 4: Vector Search
117
+ // First populate with documents
118
+ for (let i = 0; i < 100; i++) {
119
+ await db.put(`doc:${i}`, {
120
+ text: `Document ${i} about machine learning and artificial intelligence`,
121
+ content: `This is document number ${i} containing information about AI and ML algorithms`,
122
+ });
123
+ }
124
+
125
+ const searchQueries = [
126
+ "machine learning",
127
+ "artificial intelligence",
128
+ "neural networks",
129
+ "deep learning",
130
+ "data science",
131
+ ];
132
+
133
+ let queryCount = 0;
134
+ await runner.runBenchmark("Vector Search", 100, async () => {
135
+ const query = searchQueries[queryCount % searchQueries.length];
136
+ await db.vectorSearch(query, 10);
137
+ queryCount++;
138
+ });
139
+
140
+ // Benchmark 5: Subscription Performance
141
+ const subscriptions: Array<() => void> = [];
142
+ for (let i = 0; i < 100; i++) {
143
+ subscriptions.push(db.on(`sub:${i}`, () => {}));
144
+ }
145
+
146
+ let updateCount = 0;
147
+ await runner.runBenchmark("Subscription Updates", 500, async () => {
148
+ await db.put(`sub:${updateCount % 100}`, {
149
+ update: updateCount,
150
+ timestamp: Date.now(),
151
+ });
152
+ updateCount++;
153
+ });
154
+
155
+ // Clean up subscriptions
156
+ subscriptions.forEach((unsubscribe) => unsubscribe());
157
+
158
+ // Benchmark 6: Type System Operations
159
+ await runner.runBenchmark("Type System Operations", 1000, async () => {
160
+ const id = `type:${Math.random()}`;
161
+ await db.put(id, { name: `Item ${Math.random()}` });
162
+ await db.setType(id, "TestItem");
163
+ await db.instancesOf("TestItem");
164
+ });
165
+
166
+ runner.printSummary();
167
+ } finally {
168
+ await db.close();
169
+ }
170
+ }
171
+
172
+ async function runNetworkBenchmarks() {
173
+ const runner = new BenchmarkRunner();
174
+
175
+ console.log("Starting Network Benchmarks...\n");
176
+
177
+ // Benchmark 1: WebSocket Connection Performance
178
+ const db = new GunDB();
179
+ try {
180
+ const kvPath = await Deno.makeTempFile({
181
+ prefix: "kv_",
182
+ suffix: ".sqlite",
183
+ });
184
+ await db.ready(kvPath);
185
+
186
+ const port = 18000 + Math.floor(Math.random() * 10000);
187
+ await db.serve({ port });
188
+
189
+ const serverUrl = `ws://localhost:${port}`;
190
+
191
+ await runner.runBenchmark("WebSocket Connections", 10, async () => {
192
+ const clientDb = new GunDB();
193
+ const clientKv = await Deno.makeTempFile({
194
+ prefix: "kv_client_",
195
+ suffix: ".sqlite",
196
+ });
197
+ await clientDb.ready(clientKv);
198
+
199
+ const connectionPromise = new Promise<void>((resolve, reject) => {
200
+ const ws = new WebSocket(serverUrl);
201
+ ws.onopen = () => {
202
+ ws.close();
203
+ resolve();
204
+ };
205
+ ws.onerror = reject;
206
+ setTimeout(() => reject(new Error("Connection timeout")), 5000);
207
+ });
208
+
209
+ await connectionPromise;
210
+ await clientDb.close();
211
+ });
212
+ } finally {
213
+ await db.close();
214
+ }
215
+
216
+ runner.printSummary();
217
+ }
218
+
219
+ async function runMemoryBenchmarks() {
220
+ const runner = new BenchmarkRunner();
221
+ const db = new GunDB();
222
+
223
+ try {
224
+ const kvPath = await Deno.makeTempFile({
225
+ prefix: "kv_",
226
+ suffix: ".sqlite",
227
+ });
228
+ await db.ready(kvPath);
229
+
230
+ console.log("Starting Memory Benchmarks...\n");
231
+
232
+ // Benchmark 1: Memory Usage with Large Datasets
233
+ const initialMemory = (performance as any).memory?.usedJSHeapSize || 0;
234
+
235
+ await runner.runBenchmark("Large Dataset Memory Usage", 1000, async () => {
236
+ const id = `memory:${Math.random()}`;
237
+ await db.put(id, {
238
+ data: "x".repeat(1024), // 1KB per record
239
+ timestamp: Date.now(),
240
+ random: Math.random(),
241
+ });
242
+ });
243
+
244
+ const finalMemory = (performance as any).memory?.usedJSHeapSize || 0;
245
+ const memoryIncrease = finalMemory - initialMemory;
246
+
247
+ console.log(`Memory Usage: ${(memoryIncrease / 1024 / 1024).toFixed(2)}MB for 1000 records`);
248
+ console.log(`Memory per record: ${(memoryIncrease / 1000).toFixed(2)} bytes`);
249
+
250
+ // Benchmark 2: Subscription Memory Usage
251
+ const subscriptionMemory = (performance as any).memory?.usedJSHeapSize || 0;
252
+
253
+ const subscriptions: Array<() => void> = [];
254
+ await runner.runBenchmark("Subscription Memory Usage", 1000, async () => {
255
+ const id = `sub:${Math.random()}`;
256
+ const unsubscribe = db.on(id, () => {});
257
+ subscriptions.push(unsubscribe);
258
+ });
259
+
260
+ const afterSubscriptionMemory = (performance as any).memory?.usedJSHeapSize || 0;
261
+ const subscriptionMemoryIncrease = afterSubscriptionMemory - subscriptionMemory;
262
+
263
+ console.log(
264
+ `Subscription Memory: ${(subscriptionMemoryIncrease / 1024).toFixed(2)}KB for 1000 subscriptions`,
265
+ );
266
+ console.log(`Memory per subscription: ${(subscriptionMemoryIncrease / 1000).toFixed(2)} bytes`);
267
+
268
+ // Clean up subscriptions
269
+ subscriptions.forEach((unsubscribe) => unsubscribe());
270
+ } finally {
271
+ await db.close();
272
+ }
273
+ }
274
+
275
+ async function main() {
276
+ console.log("PluresDB Benchmark Suite");
277
+ console.log("========================\n");
278
+
279
+ try {
280
+ await runCoreBenchmarks();
281
+ await runNetworkBenchmarks();
282
+ await runMemoryBenchmarks();
283
+
284
+ console.log("All benchmarks completed successfully!");
285
+ } catch (error) {
286
+ console.error("Benchmark failed:", error);
287
+ Deno.exit(1);
288
+ }
289
+ }
290
+
291
+ if (import.meta.main) {
292
+ await main();
293
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * CLI wrapper for PluresDB in Node.js environment
5
+ * This allows VSCode extensions to use pluresdb as a regular npm package
6
+ */
7
+
8
+ import { PluresNode } from "./node-wrapper";
9
+ import * as path from "path";
10
+ import * as fs from "fs";
11
+
12
+ // Parse command line arguments
13
+ const args = process.argv.slice(2);
14
+ const command = args[0];
15
+
16
+ if (!command) {
17
+ console.log(`
18
+ PluresDB - P2P Graph Database with SQLite Compatibility
19
+
20
+ Usage: pluresdb <command> [options]
21
+
22
+ Commands:
23
+ serve Start the PluresDB server
24
+ put <key> <value> Store a key-value pair
25
+ get <key> Retrieve a value by key
26
+ delete <key> Delete a key-value pair
27
+ query <sql> Execute SQL query
28
+ vsearch <query> Perform vector search
29
+ list [prefix] List all keys (optionally with prefix)
30
+ config Show configuration
31
+ config set <key> <value> Set configuration value
32
+ --help Show this help message
33
+ --version Show version
34
+
35
+ Examples:
36
+ pluresdb serve --port 8080
37
+ pluresdb put "user:123" '{"name": "John"}'
38
+ pluresdb get "user:123"
39
+ pluresdb query "SELECT * FROM users"
40
+ pluresdb vsearch "machine learning"
41
+ `);
42
+ process.exit(0);
43
+ }
44
+
45
+ if (command === "--version") {
46
+ const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, "../package.json"), "utf8"));
47
+ console.log(packageJson.version);
48
+ process.exit(0);
49
+ }
50
+
51
+ if (command === "--help") {
52
+ console.log(`
53
+ PluresDB - P2P Graph Database with SQLite Compatibility
54
+
55
+ Usage: pluresdb <command> [options]
56
+
57
+ Commands:
58
+ serve Start the PluresDB server
59
+ put <key> <value> Store a key-value pair
60
+ get <key> Retrieve a value by key
61
+ delete <key> Delete a key-value pair
62
+ query <sql> Execute SQL query
63
+ vsearch <query> Perform vector search
64
+ list [prefix] List all keys (optionally with prefix)
65
+ config Show configuration
66
+ config set <key> <value> Set configuration value
67
+ --help Show this help message
68
+ --version Show version
69
+
70
+ Examples:
71
+ pluresdb serve --port 8080
72
+ pluresdb put "user:123" '{"name": "John"}'
73
+ pluresdb get "user:123"
74
+ pluresdb query "SELECT * FROM users"
75
+ pluresdb vsearch "machine learning"
76
+ `);
77
+ process.exit(0);
78
+ }
79
+
80
+ // Parse options
81
+ const options: any = {};
82
+ let i = 1;
83
+ while (i < args.length) {
84
+ const arg = args[i];
85
+ if (arg.startsWith("--")) {
86
+ const key = arg.substring(2);
87
+ const value = args[i + 1];
88
+ if (value && !value.startsWith("--")) {
89
+ options[key] = value;
90
+ i += 2;
91
+ } else {
92
+ options[key] = true;
93
+ i += 1;
94
+ }
95
+ } else {
96
+ i += 1;
97
+ }
98
+ }
99
+
100
+ async function main() {
101
+ try {
102
+ if (command === "serve") {
103
+ const config = {
104
+ port: options.port ? parseInt(options.port) : 34567,
105
+ host: options.host || "localhost",
106
+ dataDir: options["data-dir"] || path.join(require("os").homedir(), ".pluresdb"),
107
+ webPort: options["web-port"] ? parseInt(options["web-port"]) : 34568,
108
+ logLevel: options["log-level"] || "info",
109
+ };
110
+
111
+ const plures = new PluresNode({ config, autoStart: true });
112
+
113
+ console.log(`🚀 PluresDB server starting...`);
114
+ console.log(`📊 API: http://${config.host}:${config.port}`);
115
+ console.log(`🌐 Web UI: http://${config.host}:${config.webPort}`);
116
+ console.log(`📁 Data: ${config.dataDir}`);
117
+ console.log(`\nPress Ctrl+C to stop the server`);
118
+
119
+ // Handle graceful shutdown
120
+ process.on("SIGINT", async () => {
121
+ console.log("\n🛑 Shutting down PluresDB...");
122
+ await plures.stop();
123
+ process.exit(0);
124
+ });
125
+
126
+ // Keep the process alive
127
+ await new Promise(() => {});
128
+ } else {
129
+ // For other commands, we need to start the server first
130
+ const plures = new PluresNode({ autoStart: true });
131
+
132
+ try {
133
+ switch (command) {
134
+ case "put":
135
+ if (args.length < 3) {
136
+ console.error("Error: put command requires key and value");
137
+ process.exit(1);
138
+ }
139
+ const key = args[1];
140
+ const value = JSON.parse(args[2]);
141
+ await plures.put(key, value);
142
+ console.log(`✅ Stored: ${key}`);
143
+ break;
144
+
145
+ case "get":
146
+ if (args.length < 2) {
147
+ console.error("Error: get command requires key");
148
+ process.exit(1);
149
+ }
150
+ const getKey = args[1];
151
+ const result = await plures.get(getKey);
152
+ if (result === null) {
153
+ console.log("Key not found");
154
+ } else {
155
+ console.log(JSON.stringify(result, null, 2));
156
+ }
157
+ break;
158
+
159
+ case "delete":
160
+ if (args.length < 2) {
161
+ console.error("Error: delete command requires key");
162
+ process.exit(1);
163
+ }
164
+ const deleteKey = args[1];
165
+ await plures.delete(deleteKey);
166
+ console.log(`✅ Deleted: ${deleteKey}`);
167
+ break;
168
+
169
+ case "query":
170
+ if (args.length < 2) {
171
+ console.error("Error: query command requires SQL");
172
+ process.exit(1);
173
+ }
174
+ const sql = args[1];
175
+ const queryResult = await plures.query(sql);
176
+ console.log(JSON.stringify(queryResult, null, 2));
177
+ break;
178
+
179
+ case "vsearch":
180
+ if (args.length < 2) {
181
+ console.error("Error: vsearch command requires query");
182
+ process.exit(1);
183
+ }
184
+ const searchQuery = args[1];
185
+ const limit = options.limit ? parseInt(options.limit) : 10;
186
+ const searchResult = await plures.vectorSearch(searchQuery, limit);
187
+ console.log(JSON.stringify(searchResult, null, 2));
188
+ break;
189
+
190
+ case "list":
191
+ const prefix = args[1];
192
+ const listResult = await plures.list(prefix);
193
+ console.log(JSON.stringify(listResult, null, 2));
194
+ break;
195
+
196
+ case "config":
197
+ if (args[1] === "set") {
198
+ if (args.length < 4) {
199
+ console.error("Error: config set requires key and value");
200
+ process.exit(1);
201
+ }
202
+ const configKey = args[2];
203
+ const configValue = args[3];
204
+ await plures.setConfig({ [configKey]: configValue });
205
+ console.log(`✅ Set config: ${configKey} = ${configValue}`);
206
+ } else {
207
+ const config = await plures.getConfig();
208
+ console.log(JSON.stringify(config, null, 2));
209
+ }
210
+ break;
211
+
212
+ default:
213
+ console.error(`Unknown command: ${command}`);
214
+ console.log('Run "pluresdb --help" for usage information');
215
+ process.exit(1);
216
+ }
217
+ } finally {
218
+ await plures.stop();
219
+ }
220
+ }
221
+ } catch (error) {
222
+ console.error("Error:", error instanceof Error ? error.message : String(error));
223
+ process.exit(1);
224
+ }
225
+ }
226
+
227
+ // Run the main function
228
+ main().catch((error) => {
229
+ console.error("Fatal error:", error instanceof Error ? error.message : String(error));
230
+ process.exit(1);
231
+ });
package/src/config.ts ADDED
@@ -0,0 +1,49 @@
1
+ export interface AppConfig {
2
+ kvPath?: string;
3
+ port?: number;
4
+ peers?: string[];
5
+ apiPortOffset?: number; // default 1
6
+ }
7
+
8
+ export async function loadConfig(): Promise<AppConfig> {
9
+ const path = getConfigPath();
10
+ try {
11
+ const text = await Deno.readTextFile(path);
12
+ const cfg = JSON.parse(text) as AppConfig;
13
+ return cfg;
14
+ } catch {
15
+ return {};
16
+ }
17
+ }
18
+
19
+ export async function saveConfig(cfg: AppConfig): Promise<void> {
20
+ const path = getConfigPath();
21
+ await ensureDirForFile(path);
22
+ await Deno.writeTextFile(path, JSON.stringify(cfg, null, 2));
23
+ }
24
+
25
+ export function getConfigPath(): string {
26
+ const appName = "PluresDB";
27
+ try {
28
+ const os = Deno.build.os;
29
+ if (os === "windows") {
30
+ const appData = Deno.env.get("APPDATA") || Deno.env.get("LOCALAPPDATA") || ".";
31
+ return `${appData}\\${appName}\\config.json`;
32
+ }
33
+ const home = Deno.env.get("HOME") || ".";
34
+ return `${home}/.${appName.toLowerCase()}/config.json`;
35
+ } catch {
36
+ return `./config.json`;
37
+ }
38
+ }
39
+
40
+ async function ensureDirForFile(filePath: string): Promise<void> {
41
+ const sep = filePath.includes("\\") ? "\\" : "/";
42
+ const dir = filePath.split(sep).slice(0, -1).join(sep);
43
+ if (!dir) return;
44
+ try {
45
+ await Deno.mkdir(dir, { recursive: true });
46
+ } catch {
47
+ /* ignore */
48
+ }
49
+ }
@@ -0,0 +1,104 @@
1
+ import type { NodeRecord, VectorClock } from "../types/index.ts";
2
+
3
+ export function mergeVectorClocks(a: VectorClock, b: VectorClock): VectorClock {
4
+ const merged: VectorClock = {};
5
+ const keys = new Set([...Object.keys(a ?? {}), ...Object.keys(b ?? {})]);
6
+ for (const key of keys) {
7
+ merged[key] = Math.max(a?.[key] ?? 0, b?.[key] ?? 0);
8
+ }
9
+ return merged;
10
+ }
11
+
12
+ function isPlainObject(val: unknown): val is Record<string, unknown> {
13
+ return typeof val === "object" && val !== null && !Array.isArray(val);
14
+ }
15
+
16
+ function deepMergeWithDeletes(
17
+ base: Record<string, unknown>,
18
+ incoming: Record<string, unknown>,
19
+ baseState: Record<string, number> | undefined,
20
+ incomingState: Record<string, number> | undefined,
21
+ now: number,
22
+ ): { data: Record<string, unknown>; state: Record<string, number> } {
23
+ const out: Record<string, unknown> = { ...base };
24
+ const outState: Record<string, number> = { ...(baseState ?? {}) };
25
+ for (const [key, incVal] of Object.entries(incoming)) {
26
+ const baseVal = out[key];
27
+ const incTs = (incomingState ?? {})[key] ?? now; // default to now if missing
28
+ const baseTs = (baseState ?? {})[key] ?? 0;
29
+ if (incTs < baseTs) continue; // base wins
30
+
31
+ if (incVal === null) {
32
+ delete out[key];
33
+ outState[key] = incTs;
34
+ continue;
35
+ }
36
+ if (isPlainObject(baseVal) && isPlainObject(incVal)) {
37
+ const merged = deepMergeWithDeletes(
38
+ baseVal as Record<string, unknown>,
39
+ incVal as Record<string, unknown>,
40
+ baseState,
41
+ incomingState,
42
+ now,
43
+ );
44
+ out[key] = merged.data;
45
+ outState[key] = incTs;
46
+ } else {
47
+ out[key] = incVal as unknown;
48
+ outState[key] = incTs;
49
+ }
50
+ }
51
+ return { data: out, state: outState };
52
+ }
53
+
54
+ export function mergeNodes(local: NodeRecord | null, incoming: NodeRecord): NodeRecord {
55
+ if (!local) return incoming;
56
+ if (local.id !== incoming.id) {
57
+ throw new Error("mergeNodes called with mismatched ids");
58
+ }
59
+
60
+ if (incoming.timestamp > local.timestamp) {
61
+ const merged = deepMergeWithDeletes(
62
+ local.data,
63
+ incoming.data,
64
+ local.state,
65
+ incoming.state,
66
+ incoming.timestamp,
67
+ );
68
+ return {
69
+ id: local.id,
70
+ data: merged.data,
71
+ vector: incoming.vector ?? local.vector,
72
+ type: incoming.type ?? local.type,
73
+ timestamp: incoming.timestamp,
74
+ state: merged.state,
75
+ vectorClock: mergeVectorClocks(local.vectorClock, incoming.vectorClock),
76
+ };
77
+ }
78
+ if (incoming.timestamp < local.timestamp) {
79
+ return {
80
+ ...local,
81
+ vectorClock: mergeVectorClocks(local.vectorClock, incoming.vectorClock),
82
+ };
83
+ }
84
+
85
+ // Equal timestamps: deterministic field-wise merge with per-field state
86
+ const merged = deepMergeWithDeletes(
87
+ local.data,
88
+ incoming.data,
89
+ local.state,
90
+ incoming.state,
91
+ incoming.timestamp,
92
+ );
93
+ const mergedVector = incoming.vector ?? local.vector;
94
+ const mergedType = incoming.type ?? local.type;
95
+ return {
96
+ id: local.id,
97
+ data: merged.data,
98
+ vector: mergedVector,
99
+ type: mergedType,
100
+ timestamp: incoming.timestamp, // ties keep timestamp
101
+ state: merged.state,
102
+ vectorClock: mergeVectorClocks(local.vectorClock, incoming.vectorClock),
103
+ };
104
+ }