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,823 @@
1
+ /**
2
+ * Node.js Entry Point for PluresDB
3
+ * This provides a clean API for VSCode extensions and other Node.js applications
4
+ */
5
+
6
+ import { EventEmitter } from "node:events";
7
+ import { ChildProcess, spawn } from "node:child_process";
8
+ import * as path from "node:path";
9
+ import * as fs from "node:fs";
10
+ import * as os from "node:os";
11
+ import process from "node:process";
12
+ import {
13
+ MessageChannel,
14
+ receiveMessageOnPort,
15
+ Worker,
16
+ } from "node:worker_threads";
17
+ import {
18
+ BetterSQLite3Options,
19
+ BetterSQLite3RunResult,
20
+ Device,
21
+ Peer,
22
+ PluresDBConfig,
23
+ PluresDBOptions,
24
+ QueryResult,
25
+ SharedNode,
26
+ } from "./types/node-types";
27
+ import {
28
+ isPlainObject,
29
+ normalizeParameterInput,
30
+ normalizeQueryResult,
31
+ sanitizeDataDirName,
32
+ shapeRow,
33
+ splitSqlStatements,
34
+ } from "./better-sqlite3-shared";
35
+
36
+ const packageRoot = typeof __dirname !== "undefined"
37
+ ? path.resolve(__dirname, "..")
38
+ : process.cwd();
39
+
40
+ export class PluresNode extends EventEmitter {
41
+ private process: ChildProcess | null = null;
42
+ private config: PluresDBConfig;
43
+ private denoPath: string;
44
+ private isRunning = false;
45
+ private apiUrl: string = "";
46
+
47
+ constructor(options: PluresDBOptions = {}) {
48
+ super();
49
+
50
+ this.config = {
51
+ port: 34567,
52
+ host: "localhost",
53
+ dataDir: path.join(os.homedir(), ".pluresdb"),
54
+ webPort: 34568,
55
+ logLevel: "info",
56
+ ...options.config,
57
+ };
58
+
59
+ this.denoPath = options.denoPath || this.findDenoPath();
60
+
61
+ if (options.autoStart !== false) {
62
+ this.start();
63
+ }
64
+ }
65
+
66
+ private findDenoPath(): string {
67
+ // Try to find Deno in common locations
68
+ const possiblePaths = [
69
+ "deno", // In PATH
70
+ path.join(os.homedir(), ".deno", "bin", "deno"),
71
+ path.join(os.homedir(), ".local", "bin", "deno"),
72
+ "/usr/local/bin/deno",
73
+ "/opt/homebrew/bin/deno",
74
+ "C:\\Users\\" + os.userInfo().username + "\\.deno\\bin\\deno.exe",
75
+ "C:\\Program Files\\deno\\deno.exe",
76
+ ];
77
+
78
+ for (const denoPath of possiblePaths) {
79
+ try {
80
+ if (fs.existsSync(denoPath) || this.isCommandAvailable(denoPath)) {
81
+ return denoPath;
82
+ }
83
+ } catch {
84
+ // Continue to next path
85
+ }
86
+ }
87
+
88
+ throw new Error(
89
+ "Deno not found. Please install Deno from https://deno.land/",
90
+ );
91
+ }
92
+
93
+ private isCommandAvailable(command: string): boolean {
94
+ try {
95
+ require("child_process").execSync(`"${command}" --version`, {
96
+ stdio: "ignore",
97
+ });
98
+ return true;
99
+ } catch {
100
+ return false;
101
+ }
102
+ }
103
+
104
+ async start(): Promise<void> {
105
+ if (this.isRunning) {
106
+ return;
107
+ }
108
+
109
+ return new Promise((resolve, reject) => {
110
+ try {
111
+ // Ensure data directory exists
112
+ if (!fs.existsSync(this.config.dataDir!)) {
113
+ fs.mkdirSync(this.config.dataDir!, { recursive: true });
114
+ }
115
+
116
+ const kvPath = path.join(this.config.dataDir!, "pluresdb.kv");
117
+
118
+ // Find the main.ts file
119
+ const mainTsPath = path.join(packageRoot, "src", "main.ts");
120
+ if (!fs.existsSync(mainTsPath)) {
121
+ throw new Error(
122
+ "PluresDB main.ts not found. Please ensure the package is properly installed.",
123
+ );
124
+ }
125
+
126
+ // Start the Deno process
127
+ const args = [
128
+ "run",
129
+ "-A",
130
+ "--unstable-kv",
131
+ "--no-lock",
132
+ mainTsPath,
133
+ "serve",
134
+ "--port",
135
+ this.config.port!.toString(),
136
+ "--host",
137
+ this.config.host!,
138
+ "--kv",
139
+ kvPath,
140
+ ];
141
+
142
+ this.process = spawn(this.denoPath, args, {
143
+ stdio: ["pipe", "pipe", "pipe"],
144
+ cwd: packageRoot,
145
+ });
146
+
147
+ this.apiUrl = `http://${this.config.host}:${this.config.port}`;
148
+
149
+ // Handle process events
150
+ this.process.on("error", (error) => {
151
+ this.emit("error", error);
152
+ reject(error);
153
+ });
154
+
155
+ this.process.on("exit", (code) => {
156
+ this.isRunning = false;
157
+ this.emit("exit", code);
158
+ });
159
+
160
+ // Wait for server to start
161
+ this.waitForServer()
162
+ .then(() => {
163
+ this.isRunning = true;
164
+ this.emit("started");
165
+ resolve();
166
+ })
167
+ .catch(reject);
168
+
169
+ // Handle stdout/stderr
170
+ this.process.stdout?.on("data", (data) => {
171
+ const output = data.toString();
172
+ this.emit("stdout", output);
173
+ });
174
+
175
+ this.process.stderr?.on("data", (data) => {
176
+ const output = data.toString();
177
+ this.emit("stderr", output);
178
+ });
179
+ } catch (error) {
180
+ reject(error);
181
+ }
182
+ });
183
+ }
184
+
185
+ private async waitForServer(timeout = 20000): Promise<void> {
186
+ const startTime = Date.now();
187
+
188
+ while (Date.now() - startTime < timeout) {
189
+ try {
190
+ const response = await fetch(`${this.apiUrl}/api/config`);
191
+ if (response.ok) {
192
+ return;
193
+ }
194
+ } catch {
195
+ // Server not ready yet
196
+ }
197
+
198
+ await new Promise((resolve) => setTimeout(resolve, 100));
199
+ }
200
+
201
+ throw new Error("Server failed to start within timeout");
202
+ }
203
+
204
+ async stop(): Promise<void> {
205
+ if (!this.isRunning || !this.process) {
206
+ return;
207
+ }
208
+
209
+ return new Promise((resolve) => {
210
+ this.process!.kill("SIGTERM");
211
+
212
+ this.process!.on("exit", () => {
213
+ this.isRunning = false;
214
+ this.emit("stopped");
215
+ resolve();
216
+ });
217
+
218
+ // Force kill after 5 seconds
219
+ setTimeout(() => {
220
+ if (this.process && this.isRunning) {
221
+ this.process.kill("SIGKILL");
222
+ }
223
+ resolve();
224
+ }, 5000);
225
+ });
226
+ }
227
+
228
+ getApiUrl(): string {
229
+ return this.apiUrl;
230
+ }
231
+
232
+ getWebUrl(): string {
233
+ return `http://${this.config.host}:${this.config.webPort}`;
234
+ }
235
+
236
+ isServerRunning(): boolean {
237
+ return this.isRunning;
238
+ }
239
+
240
+ // SQLite-compatible API methods
241
+ async query(sql: string, params: any[] = []): Promise<any> {
242
+ const response = await fetch(`${this.apiUrl}/api/query`, {
243
+ method: "POST",
244
+ headers: { "Content-Type": "application/json" },
245
+ body: JSON.stringify({ sql, params }),
246
+ });
247
+
248
+ if (!response.ok) {
249
+ throw new Error(`Query failed: ${response.statusText}`);
250
+ }
251
+
252
+ return response.json();
253
+ }
254
+
255
+ async put(key: string, value: any): Promise<void> {
256
+ const response = await fetch(`${this.apiUrl}/api/data`, {
257
+ method: "PUT",
258
+ headers: { "Content-Type": "application/json" },
259
+ body: JSON.stringify({ key, value }),
260
+ });
261
+
262
+ if (!response.ok) {
263
+ throw new Error(`Put failed: ${response.statusText}`);
264
+ }
265
+ }
266
+
267
+ async get(key: string): Promise<any> {
268
+ const response = await fetch(
269
+ `${this.apiUrl}/api/data/${encodeURIComponent(key)}`,
270
+ );
271
+
272
+ if (!response.ok) {
273
+ if (response.status === 404) {
274
+ return null;
275
+ }
276
+ throw new Error(`Get failed: ${response.statusText}`);
277
+ }
278
+
279
+ return response.json();
280
+ }
281
+
282
+ async delete(key: string): Promise<void> {
283
+ const response = await fetch(
284
+ `${this.apiUrl}/api/data/${encodeURIComponent(key)}`,
285
+ {
286
+ method: "DELETE",
287
+ },
288
+ );
289
+
290
+ if (!response.ok) {
291
+ throw new Error(`Delete failed: ${response.statusText}`);
292
+ }
293
+ }
294
+
295
+ async vectorSearch(query: string, limit = 10): Promise<any[]> {
296
+ const response = await fetch(`${this.apiUrl}/api/vsearch`, {
297
+ method: "POST",
298
+ headers: { "Content-Type": "application/json" },
299
+ body: JSON.stringify({ query, limit }),
300
+ });
301
+
302
+ if (!response.ok) {
303
+ throw new Error(`Vector search failed: ${response.statusText}`);
304
+ }
305
+
306
+ return response.json() as Promise<any[]>;
307
+ }
308
+
309
+ async list(prefix?: string): Promise<string[]> {
310
+ const url = prefix
311
+ ? `${this.apiUrl}/api/list?prefix=${encodeURIComponent(prefix)}`
312
+ : `${this.apiUrl}/api/list`;
313
+ const response = await fetch(url);
314
+
315
+ if (!response.ok) {
316
+ throw new Error(`List failed: ${response.statusText}`);
317
+ }
318
+
319
+ return response.json() as Promise<string[]>;
320
+ }
321
+
322
+ async getConfig(): Promise<any> {
323
+ const response = await fetch(`${this.apiUrl}/api/config`);
324
+
325
+ if (!response.ok) {
326
+ throw new Error(`Get config failed: ${response.statusText}`);
327
+ }
328
+
329
+ return response.json();
330
+ }
331
+
332
+ async setConfig(config: any): Promise<void> {
333
+ const response = await fetch(`${this.apiUrl}/api/config`, {
334
+ method: "POST",
335
+ headers: { "Content-Type": "application/json" },
336
+ body: JSON.stringify(config),
337
+ });
338
+
339
+ if (!response.ok) {
340
+ throw new Error(`Set config failed: ${response.statusText}`);
341
+ }
342
+ }
343
+
344
+ // P2P API methods
345
+ async createIdentity(options: {
346
+ name: string;
347
+ email: string;
348
+ }): Promise<{ id: string; publicKey: string }> {
349
+ const response = await fetch(`${this.apiUrl}/api/identity`, {
350
+ method: "POST",
351
+ headers: { "Content-Type": "application/json" },
352
+ body: JSON.stringify(options),
353
+ });
354
+
355
+ if (!response.ok) {
356
+ throw new Error(`Create identity failed: ${response.statusText}`);
357
+ }
358
+
359
+ return response.json() as Promise<{ id: string; publicKey: string }>;
360
+ }
361
+
362
+ async searchPeers(query: string): Promise<Peer[]> {
363
+ const response = await fetch(
364
+ `${this.apiUrl}/api/peers/search?q=${encodeURIComponent(query)}`,
365
+ );
366
+
367
+ if (!response.ok) {
368
+ throw new Error(`Search peers failed: ${response.statusText}`);
369
+ }
370
+
371
+ return response.json() as Promise<Peer[]>;
372
+ }
373
+
374
+ async shareNode(
375
+ nodeId: string,
376
+ targetPeerId: string,
377
+ options?: { accessLevel?: "read-only" | "read-write" | "admin" },
378
+ ): Promise<{ sharedNodeId: string }> {
379
+ const response = await fetch(`${this.apiUrl}/api/share`, {
380
+ method: "POST",
381
+ headers: { "Content-Type": "application/json" },
382
+ body: JSON.stringify({
383
+ nodeId,
384
+ targetPeerId,
385
+ accessLevel: options?.accessLevel || "read-only",
386
+ }),
387
+ });
388
+
389
+ if (!response.ok) {
390
+ throw new Error(`Share node failed: ${response.statusText}`);
391
+ }
392
+
393
+ return response.json() as Promise<{ sharedNodeId: string }>;
394
+ }
395
+
396
+ async acceptSharedNode(sharedNodeId: string): Promise<{ success: boolean }> {
397
+ const response = await fetch(`${this.apiUrl}/api/share/accept`, {
398
+ method: "POST",
399
+ headers: { "Content-Type": "application/json" },
400
+ body: JSON.stringify({ sharedNodeId }),
401
+ });
402
+
403
+ if (!response.ok) {
404
+ throw new Error(`Accept shared node failed: ${response.statusText}`);
405
+ }
406
+
407
+ return response.json() as Promise<{ success: boolean }>;
408
+ }
409
+
410
+ async addDevice(device: {
411
+ name: string;
412
+ type: "laptop" | "phone" | "server" | "desktop";
413
+ }): Promise<{ id: string }> {
414
+ const response = await fetch(`${this.apiUrl}/api/devices`, {
415
+ method: "POST",
416
+ headers: { "Content-Type": "application/json" },
417
+ body: JSON.stringify(device),
418
+ });
419
+
420
+ if (!response.ok) {
421
+ throw new Error(`Add device failed: ${response.statusText}`);
422
+ }
423
+
424
+ return response.json() as Promise<{ id: string }>;
425
+ }
426
+
427
+ async syncWithDevice(deviceId: string): Promise<{ success: boolean }> {
428
+ const response = await fetch(`${this.apiUrl}/api/devices/sync`, {
429
+ method: "POST",
430
+ headers: { "Content-Type": "application/json" },
431
+ body: JSON.stringify({ deviceId }),
432
+ });
433
+
434
+ if (!response.ok) {
435
+ throw new Error(`Sync with device failed: ${response.statusText}`);
436
+ }
437
+
438
+ return response.json() as Promise<{ success: boolean }>;
439
+ }
440
+ }
441
+
442
+ // SQLite-compatible API for easy migration
443
+ export class SQLiteCompatibleAPI {
444
+ private plures: PluresNode;
445
+
446
+ constructor(options?: PluresDBOptions) {
447
+ this.plures = new PluresNode(options);
448
+ }
449
+
450
+ async start() {
451
+ await this.plures.start();
452
+ }
453
+
454
+ async stop() {
455
+ await this.plures.stop();
456
+ }
457
+
458
+ // SQLite-compatible methods
459
+ async run(sql: string, params: any[] = []) {
460
+ return this.plures.query(sql, params);
461
+ }
462
+
463
+ async get(sql: string, params: any[] = []) {
464
+ const result = await this.plures.query(sql, params);
465
+ return result.rows?.[0] || null;
466
+ }
467
+
468
+ async all(sql: string, params: any[] = []) {
469
+ const result = await this.plures.query(sql, params);
470
+ return result.rows || [];
471
+ }
472
+
473
+ async exec(sql: string) {
474
+ return this.plures.query(sql);
475
+ }
476
+
477
+ // Additional PluresDB specific methods
478
+ async put(key: string, value: any) {
479
+ return this.plures.put(key, value);
480
+ }
481
+
482
+ async getValue(key: string) {
483
+ return this.plures.get(key);
484
+ }
485
+
486
+ async delete(key: string) {
487
+ return this.plures.delete(key);
488
+ }
489
+
490
+ async vectorSearch(query: string, limit = 10) {
491
+ return this.plures.vectorSearch(query, limit);
492
+ }
493
+
494
+ async list(prefix?: string) {
495
+ return this.plures.list(prefix);
496
+ }
497
+
498
+ getApiUrl() {
499
+ return this.plures.getApiUrl();
500
+ }
501
+
502
+ getWebUrl() {
503
+ return this.plures.getWebUrl();
504
+ }
505
+
506
+ isRunning() {
507
+ return this.plures.isServerRunning();
508
+ }
509
+
510
+ // P2P API methods
511
+ async createIdentity(options: { name: string; email: string }) {
512
+ return this.plures.createIdentity(options);
513
+ }
514
+
515
+ async searchPeers(query: string) {
516
+ return this.plures.searchPeers(query);
517
+ }
518
+
519
+ async shareNode(
520
+ nodeId: string,
521
+ targetPeerId: string,
522
+ options?: { accessLevel?: "read-only" | "read-write" | "admin" },
523
+ ) {
524
+ return this.plures.shareNode(nodeId, targetPeerId, options);
525
+ }
526
+
527
+ async acceptSharedNode(sharedNodeId: string) {
528
+ return this.plures.acceptSharedNode(sharedNodeId);
529
+ }
530
+
531
+ async addDevice(device: {
532
+ name: string;
533
+ type: "laptop" | "phone" | "server" | "desktop";
534
+ }) {
535
+ return this.plures.addDevice(device);
536
+ }
537
+
538
+ async syncWithDevice(deviceId: string) {
539
+ return this.plures.syncWithDevice(deviceId);
540
+ }
541
+ }
542
+
543
+ export class BetterSQLite3Statement {
544
+ private boundParams: unknown[] | undefined;
545
+ private rawMode = false;
546
+ private pluckMode = false;
547
+ private expandMode = false;
548
+ readonly reader: boolean;
549
+
550
+ constructor(
551
+ private readonly database: BetterSQLite3Database,
552
+ private readonly sql: string,
553
+ ) {
554
+ this.reader = /^\s*select/i.test(sql);
555
+ }
556
+
557
+ get databaseInstance(): BetterSQLite3Database {
558
+ return this.database;
559
+ }
560
+
561
+ bind(...params: unknown[]): this {
562
+ this.boundParams = normalizeParameterInput(params);
563
+ return this;
564
+ }
565
+
566
+ raw(toggle = true): this {
567
+ this.rawMode = toggle;
568
+ if (toggle) {
569
+ this.pluckMode = false;
570
+ }
571
+ return this;
572
+ }
573
+
574
+ pluck(toggle = true): this {
575
+ this.pluckMode = toggle;
576
+ if (toggle) {
577
+ this.expandMode = false;
578
+ this.rawMode = false;
579
+ }
580
+ return this;
581
+ }
582
+
583
+ expand(toggle = true): this {
584
+ this.expandMode = toggle;
585
+ if (toggle) {
586
+ this.pluckMode = false;
587
+ }
588
+ return this;
589
+ }
590
+
591
+ safeIntegers(): this {
592
+ // No-op for compatibility with better-sqlite3 API
593
+ return this;
594
+ }
595
+
596
+ async run(...params: unknown[]): Promise<BetterSQLite3RunResult> {
597
+ const result = await this.database.executeStatement(
598
+ this.sql,
599
+ this.resolveParams(params),
600
+ );
601
+ return {
602
+ changes: typeof result.changes === "number" ? result.changes : 0,
603
+ lastInsertRowid: typeof result.lastInsertRowId === "number"
604
+ ? result.lastInsertRowId
605
+ : null,
606
+ columns: result.columns,
607
+ };
608
+ }
609
+
610
+ async get(...params: unknown[]): Promise<unknown> {
611
+ const rows = await this.fetchRows(params);
612
+ return rows.length > 0 ? rows[0] : undefined;
613
+ }
614
+
615
+ async all(...params: unknown[]): Promise<unknown[]> {
616
+ return await this.fetchRows(params);
617
+ }
618
+
619
+ async *iterate(...params: unknown[]): AsyncIterableIterator<unknown> {
620
+ const rows = await this.fetchRows(params);
621
+ for (const row of rows) {
622
+ yield row;
623
+ }
624
+ }
625
+
626
+ async columns(): Promise<string[]> {
627
+ const result = await this.database.executeStatement(
628
+ this.sql,
629
+ this.boundParams ?? [],
630
+ );
631
+ return result.columns ?? [];
632
+ }
633
+
634
+ private resolveParams(params: unknown[]): unknown[] {
635
+ const normalized = normalizeParameterInput(params);
636
+ if (normalized.length > 0) {
637
+ return normalized;
638
+ }
639
+ return this.boundParams ? [...this.boundParams] : [];
640
+ }
641
+
642
+ private async fetchRows(params: unknown[]): Promise<unknown[]> {
643
+ const result = await this.database.executeStatement(
644
+ this.sql,
645
+ this.resolveParams(params),
646
+ );
647
+ return this.transformRows(result);
648
+ }
649
+
650
+ private transformRows(result: QueryResult): unknown[] {
651
+ return result.rows.map((row) =>
652
+ shapeRow(row, result.columns, {
653
+ raw: this.rawMode,
654
+ pluck: this.pluckMode,
655
+ expand: this.expandMode,
656
+ })
657
+ );
658
+ }
659
+ }
660
+
661
+ export class BetterSQLite3Database {
662
+ private readonly options: BetterSQLite3Options;
663
+ private readonly plures: PluresNode;
664
+ private readonly filename: string;
665
+ private readonly verbose?: (...args: unknown[]) => void;
666
+ private openPromise: Promise<void> | null = null;
667
+ private opened = false;
668
+
669
+ constructor(
670
+ filenameOrOptions?: string | BetterSQLite3Options,
671
+ maybeOptions?: BetterSQLite3Options,
672
+ ) {
673
+ const { filename, options } = this.resolveOptions(
674
+ filenameOrOptions,
675
+ maybeOptions,
676
+ );
677
+ this.filename = filename;
678
+ this.options = options;
679
+ this.verbose = options.verbose;
680
+
681
+ const config: PluresDBConfig = { ...options.config };
682
+ if (!config.dataDir) {
683
+ const baseDir = options.memory
684
+ ? path.join(os.tmpdir(), "pluresdb", "better-sqlite3-memory")
685
+ : path.join(os.homedir(), ".pluresdb", "better-sqlite3");
686
+ const safeName = sanitizeDataDirName(
687
+ filename === ":memory:" ? "memory" : filename,
688
+ );
689
+ config.dataDir = path.join(baseDir, safeName);
690
+ }
691
+
692
+ this.plures = new PluresNode({
693
+ config,
694
+ denoPath: options.denoPath,
695
+ autoStart: false,
696
+ });
697
+
698
+ if (options.autoStart !== false) {
699
+ void this.open();
700
+ }
701
+ }
702
+
703
+ get name(): string {
704
+ return this.filename;
705
+ }
706
+
707
+ get isOpen(): boolean {
708
+ return this.opened;
709
+ }
710
+
711
+ async open(): Promise<this> {
712
+ await this.ensureOpen();
713
+ return this;
714
+ }
715
+
716
+ async close(): Promise<void> {
717
+ if (!this.opened && !this.openPromise) {
718
+ return;
719
+ }
720
+ await this.plures.stop();
721
+ this.opened = false;
722
+ this.openPromise = null;
723
+ }
724
+
725
+ prepare(sql: string): BetterSQLite3Statement {
726
+ if (!this.opened) {
727
+ throw new Error(
728
+ "Database is not open. Call await db.open() before preparing statements.",
729
+ );
730
+ }
731
+ return new BetterSQLite3Statement(this, sql);
732
+ }
733
+
734
+ async exec(sql: string): Promise<this> {
735
+ await this.ensureOpen();
736
+ for (const statement of splitSqlStatements(sql)) {
737
+ await this.executeStatement(statement, []);
738
+ }
739
+ return this;
740
+ }
741
+
742
+ transaction<TArgs extends unknown[], TResult>(
743
+ fn: (...args: TArgs) => Promise<TResult> | TResult,
744
+ ): (...args: TArgs) => Promise<TResult> {
745
+ return async (...args: TArgs): Promise<TResult> => {
746
+ await this.ensureOpen();
747
+ await this.executeStatement("BEGIN", []);
748
+ try {
749
+ const result = await fn(...args);
750
+ await this.executeStatement("COMMIT", []);
751
+ return result;
752
+ } catch (error) {
753
+ await this.executeStatement("ROLLBACK", []).catch(() => undefined);
754
+ throw error;
755
+ }
756
+ };
757
+ }
758
+
759
+ async pragma(statement: string): Promise<unknown[]> {
760
+ const sql = /^\s*pragma/i.test(statement)
761
+ ? statement
762
+ : `PRAGMA ${statement}`;
763
+ const result = await this.executeStatement(sql, []);
764
+ return result.rows;
765
+ }
766
+
767
+ defaultSafeIntegers(): this {
768
+ return this;
769
+ }
770
+
771
+ unsafeMode(): this {
772
+ return this;
773
+ }
774
+
775
+ async executeStatement(sql: string, params: unknown[]): Promise<QueryResult> {
776
+ await this.ensureOpen();
777
+ const normalizedParams = normalizeParameterInput(params);
778
+ const raw = await this.plures.query(sql, normalizedParams);
779
+ return normalizeQueryResult(raw);
780
+ }
781
+
782
+ private async ensureOpen(): Promise<void> {
783
+ if (this.opened) {
784
+ return;
785
+ }
786
+ if (!this.openPromise) {
787
+ this.openPromise = (async () => {
788
+ await this.plures.start();
789
+ this.opened = true;
790
+ if (this.verbose) {
791
+ this.verbose(
792
+ `PluresDB ready for better-sqlite3 compatibility (${this.filename})`,
793
+ );
794
+ }
795
+ })();
796
+ }
797
+ await this.openPromise;
798
+ }
799
+
800
+ private resolveOptions(
801
+ filenameOrOptions?: string | BetterSQLite3Options,
802
+ maybeOptions?: BetterSQLite3Options,
803
+ ): { filename: string; options: BetterSQLite3Options } {
804
+ if (typeof filenameOrOptions === "string") {
805
+ return {
806
+ filename: filenameOrOptions,
807
+ options: { ...(maybeOptions ?? {}), filename: filenameOrOptions },
808
+ };
809
+ }
810
+
811
+ const options = filenameOrOptions ?? {};
812
+ const filename = options.filename ?? ":memory:";
813
+ return { filename, options: { ...options, filename } };
814
+ }
815
+ }
816
+
817
+ // Export the main class and types
818
+ export { PluresNode as default };
819
+ export * from "./types/node-types";
820
+ export {
821
+ createPluresExtension,
822
+ PluresVSCodeExtension,
823
+ } from "./vscode/extension";