@liorandb/core 1.0.11 → 1.0.13

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.
@@ -1,37 +1,35 @@
1
1
  import {
2
2
  LioranManager
3
- } from "../chunk-RIHXIHVF.js";
3
+ } from "../chunk-ST6KMJQJ.js";
4
4
 
5
5
  // src/worker/dbWorker.ts
6
- var manager = new LioranManager();
6
+ var manager = new LioranManager({ ipc: false });
7
7
  process.on("message", async (msg) => {
8
8
  const { id, action, args } = msg;
9
9
  try {
10
10
  let result;
11
11
  switch (action) {
12
- case "shutdown": {
12
+ case "shutdown":
13
13
  await manager.closeAll();
14
14
  result = true;
15
15
  break;
16
- }
17
- case "db": {
18
- const db = await manager.db(args.db);
16
+ case "db":
17
+ await manager.db(args.db);
19
18
  result = true;
20
19
  break;
21
- }
22
- case "collection": {
23
- const db = await manager.db(args.db);
24
- result = db.collection(args.collection);
25
- break;
26
- }
27
20
  case "op": {
28
21
  const { db, col, method, params } = args;
29
22
  const collection = (await manager.db(db)).collection(col);
30
23
  result = await collection[method](...params);
31
24
  break;
32
25
  }
26
+ case "tx": {
27
+ const db = await manager.db(args.db);
28
+ result = await db.transaction(args.fn);
29
+ break;
30
+ }
33
31
  default:
34
- throw new Error("Unknown action");
32
+ throw new Error("Unknown IPC action");
35
33
  }
36
34
  process.send?.({ id, ok: true, result });
37
35
  } catch (err) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@liorandb/core",
3
- "version": "1.0.11",
4
- "description": "**LioranDB Core Module** – Lightweight, local-first, peer-to-peer database management for Node.js.",
3
+ "version": "1.0.13",
4
+ "description": "LioranDB Core Module – Lightweight, local-first, peer-to-peer database management for Node.js.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
@@ -1,27 +1,34 @@
1
1
  import path from "path";
2
2
  import fs from "fs";
3
+ import process from "process";
3
4
  import { LioranDB } from "./core/database.js";
4
5
  import { setEncryptionKey } from "./utils/encryption.js";
5
6
  import { getDefaultRootPath } from "./utils/rootpath.js";
6
7
  import { dbQueue } from "./ipc/queue.js";
8
+ import { IPCServer } from "./ipc/server.js";
9
+
10
+ enum ProcessMode {
11
+ PRIMARY = "primary",
12
+ CLIENT = "client"
13
+ }
7
14
 
8
15
  export interface LioranManagerOptions {
9
16
  rootPath?: string;
10
17
  encryptionKey?: string | Buffer;
11
- ipc?: boolean;
12
18
  }
13
19
 
14
20
  export class LioranManager {
15
21
  rootPath: string;
16
22
  openDBs: Map<string, LioranDB>;
17
23
  private closed = false;
18
- private ipc: boolean;
24
+ private mode: ProcessMode;
25
+ private lockFd?: number;
26
+ private ipcServer?: IPCServer;
19
27
 
20
28
  constructor(options: LioranManagerOptions = {}) {
21
- const { rootPath, encryptionKey, ipc } = options;
29
+ const { rootPath, encryptionKey } = options;
22
30
 
23
31
  this.rootPath = rootPath || getDefaultRootPath();
24
- this.ipc = ipc ?? process.env.LIORANDB_IPC === "1";
25
32
 
26
33
  if (!fs.existsSync(this.rootPath)) {
27
34
  fs.mkdirSync(this.rootPath, { recursive: true });
@@ -33,33 +40,56 @@ export class LioranManager {
33
40
 
34
41
  this.openDBs = new Map();
35
42
 
36
- if (!this.ipc) {
43
+ this.mode = this.tryAcquireLock()
44
+ ? ProcessMode.PRIMARY
45
+ : ProcessMode.CLIENT;
46
+
47
+ if (this.mode === ProcessMode.PRIMARY) {
48
+ this.ipcServer = new IPCServer(this, this.rootPath);
49
+ this.ipcServer.start();
37
50
  this._registerShutdownHooks();
38
51
  }
39
52
  }
40
53
 
41
- /* -------------------------------- CORE -------------------------------- */
42
-
43
- async db(name: string): Promise<LioranDB> {
44
- if (this.ipc) {
45
- await dbQueue.exec("db", { db: name });
46
- return new IPCDatabase(name) as any;
54
+ private isProcessAlive(pid: number): boolean {
55
+ try {
56
+ process.kill(pid, 0);
57
+ return true;
58
+ } catch {
59
+ return false;
47
60
  }
48
-
49
- return this.openDatabase(name);
50
61
  }
51
62
 
52
- async createDatabase(name: string): Promise<LioranDB> {
53
- this._assertOpen();
63
+ private tryAcquireLock(): boolean {
64
+ const lockPath = path.join(this.rootPath, ".lioran.lock");
54
65
 
55
- const dbPath = path.join(this.rootPath, name);
66
+ try {
67
+ this.lockFd = fs.openSync(lockPath, "wx");
68
+ fs.writeSync(this.lockFd, String(process.pid));
69
+ return true;
70
+ } catch {
71
+ // Possible stale lock → validate PID
72
+ try {
73
+ const pid = Number(fs.readFileSync(lockPath, "utf8"));
74
+ if (!this.isProcessAlive(pid)) {
75
+ fs.unlinkSync(lockPath);
76
+ this.lockFd = fs.openSync(lockPath, "wx");
77
+ fs.writeSync(this.lockFd, String(process.pid));
78
+ return true;
79
+ }
80
+ } catch {}
56
81
 
57
- if (fs.existsSync(dbPath)) {
58
- throw new Error(`Database "${name}" already exists`);
82
+ return false;
59
83
  }
84
+ }
60
85
 
61
- await fs.promises.mkdir(dbPath, { recursive: true });
62
- return this.db(name);
86
+ async db(name: string): Promise<LioranDB> {
87
+ if (this.mode === ProcessMode.CLIENT) {
88
+ await dbQueue.exec("db", { db: name });
89
+ return new IPCDatabase(name) as any;
90
+ }
91
+
92
+ return this.openDatabase(name);
63
93
  }
64
94
 
65
95
  async openDatabase(name: string): Promise<LioranDB> {
@@ -70,54 +100,36 @@ export class LioranManager {
70
100
  }
71
101
 
72
102
  const dbPath = path.join(this.rootPath, name);
73
-
74
- if (!fs.existsSync(dbPath)) {
75
- await fs.promises.mkdir(dbPath, { recursive: true });
76
- }
103
+ await fs.promises.mkdir(dbPath, { recursive: true });
77
104
 
78
105
  const db = new LioranDB(dbPath, name, this);
79
106
  this.openDBs.set(name, db);
80
107
  return db;
81
108
  }
82
109
 
83
- /* -------------------------------- LIFECYCLE -------------------------------- */
84
-
85
- async closeDatabase(name: string): Promise<void> {
86
- if (this.ipc) return;
87
-
88
- if (!this.openDBs.has(name)) return;
89
-
90
- const db = this.openDBs.get(name)!;
91
- await db.close();
92
- this.openDBs.delete(name);
93
- }
94
-
95
- /**
96
- * Gracefully shuts down everything.
97
- * - Closes all databases
98
- * - Terminates IPC worker if running
99
- */
100
110
  async closeAll(): Promise<void> {
101
111
  if (this.closed) return;
102
112
  this.closed = true;
103
113
 
104
- if (this.ipc) {
114
+ if (this.mode === ProcessMode.CLIENT) {
105
115
  await dbQueue.shutdown();
106
116
  return;
107
117
  }
108
118
 
109
119
  for (const db of this.openDBs.values()) {
110
- try {
111
- await db.close();
112
- } catch {}
120
+ try { await db.close(); } catch {}
113
121
  }
114
122
 
115
123
  this.openDBs.clear();
124
+
125
+ try {
126
+ if (this.lockFd) fs.closeSync(this.lockFd);
127
+ fs.unlinkSync(path.join(this.rootPath, ".lioran.lock"));
128
+ } catch {}
129
+
130
+ await this.ipcServer?.close();
116
131
  }
117
132
 
118
- /**
119
- * Alias for closeAll() (clean public API)
120
- */
121
133
  async close(): Promise<void> {
122
134
  return this.closeAll();
123
135
  }
@@ -137,73 +149,9 @@ export class LioranManager {
137
149
  throw new Error("LioranManager is closed");
138
150
  }
139
151
  }
140
-
141
- /* -------------------------------- MANAGEMENT -------------------------------- */
142
-
143
- async renameDatabase(oldName: string, newName: string): Promise<boolean> {
144
- if (this.ipc) {
145
- return (await dbQueue.exec("renameDatabase", { oldName, newName })) as boolean;
146
- }
147
-
148
- const oldPath = path.join(this.rootPath, oldName);
149
- const newPath = path.join(this.rootPath, newName);
150
-
151
- if (!fs.existsSync(oldPath)) {
152
- throw new Error(`Database "${oldName}" not found`);
153
- }
154
-
155
- if (fs.existsSync(newPath)) {
156
- throw new Error(`Database "${newName}" already exists`);
157
- }
158
-
159
- await this.closeDatabase(oldName);
160
- await fs.promises.rename(oldPath, newPath);
161
- return true;
162
- }
163
-
164
- async deleteDatabase(name: string): Promise<boolean> {
165
- return this.dropDatabase(name);
166
- }
167
-
168
- async dropDatabase(name: string): Promise<boolean> {
169
- if (this.ipc) {
170
- return (await dbQueue.exec("dropDatabase", { name })) as boolean;
171
- }
172
-
173
- const dbPath = path.join(this.rootPath, name);
174
-
175
- if (!fs.existsSync(dbPath)) return false;
176
-
177
- await this.closeDatabase(name);
178
- await fs.promises.rm(dbPath, { recursive: true, force: true });
179
- return true;
180
- }
181
-
182
- async listDatabases(): Promise<string[]> {
183
- if (this.ipc) {
184
- return (await dbQueue.exec("listDatabases", {})) as string[];
185
- }
186
-
187
- const items = await fs.promises.readdir(this.rootPath, {
188
- withFileTypes: true
189
- });
190
-
191
- return items.filter(i => i.isDirectory()).map(i => i.name);
192
- }
193
-
194
- /* -------------------------------- DEBUG -------------------------------- */
195
-
196
- getStats() {
197
- return {
198
- rootPath: this.rootPath,
199
- openDatabases: this.ipc ? ["<ipc>"] : [...this.openDBs.keys()],
200
- ipc: this.ipc,
201
- closed: this.closed
202
- };
203
- }
204
152
  }
205
153
 
206
- /* -------------------------------- IPC PROXY DB -------------------------------- */
154
+ /* ---------------- IPC PROXY DB ---------------- */
207
155
 
208
156
  class IPCDatabase {
209
157
  constructor(private name: string) {}
@@ -240,4 +188,4 @@ class IPCCollection {
240
188
  deleteMany = (filter: any) => this.call("deleteMany", [filter]);
241
189
  countDocuments = (filter?: any) =>
242
190
  this.call("countDocuments", [filter]);
243
- }
191
+ }