@liorandb/core 1.0.12 → 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.
- package/dist/index.d.ts +10 -23
- package/dist/index.js +790 -5
- package/package.json +1 -1
- package/src/LioranManager.ts +62 -114
- package/src/core/database.ts +22 -12
- package/src/ipc/client.ts +85 -0
- package/src/ipc/queue.ts +11 -126
- package/src/ipc/server.ts +84 -0
- package/src/ipc/socketPath.ts +10 -0
- package/src/worker/dbWorker.ts +0 -43
package/package.json
CHANGED
package/src/LioranManager.ts
CHANGED
|
@@ -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
|
|
24
|
+
private mode: ProcessMode;
|
|
25
|
+
private lockFd?: number;
|
|
26
|
+
private ipcServer?: IPCServer;
|
|
19
27
|
|
|
20
28
|
constructor(options: LioranManagerOptions = {}) {
|
|
21
|
-
const { rootPath, encryptionKey
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return
|
|
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
|
-
|
|
53
|
-
this.
|
|
63
|
+
private tryAcquireLock(): boolean {
|
|
64
|
+
const lockPath = path.join(this.rootPath, ".lioran.lock");
|
|
54
65
|
|
|
55
|
-
|
|
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
|
-
|
|
58
|
-
throw new Error(`Database "${name}" already exists`);
|
|
82
|
+
return false;
|
|
59
83
|
}
|
|
84
|
+
}
|
|
60
85
|
|
|
61
|
-
|
|
62
|
-
|
|
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.
|
|
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
|
-
/*
|
|
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
|
+
}
|
package/src/core/database.ts
CHANGED
|
@@ -6,7 +6,8 @@ import type { ZodSchema } from "zod";
|
|
|
6
6
|
|
|
7
7
|
type TXOp = { tx: number; col: string; op: string; args: any[] };
|
|
8
8
|
type TXCommit = { tx: number; commit: true };
|
|
9
|
-
type
|
|
9
|
+
type TXApplied = { tx: number; applied: true };
|
|
10
|
+
type WALEntry = TXOp | TXCommit | TXApplied;
|
|
10
11
|
|
|
11
12
|
class DBTransactionContext {
|
|
12
13
|
private ops: TXOp[] = [];
|
|
@@ -35,6 +36,7 @@ class DBTransactionContext {
|
|
|
35
36
|
await this.db.writeWAL(this.ops);
|
|
36
37
|
await this.db.writeWAL([{ tx: this.txId, commit: true }]);
|
|
37
38
|
await this.db.applyTransaction(this.ops);
|
|
39
|
+
await this.db.writeWAL([{ tx: this.txId, applied: true }]);
|
|
38
40
|
await this.db.clearWAL();
|
|
39
41
|
}
|
|
40
42
|
}
|
|
@@ -76,25 +78,33 @@ export class LioranDB {
|
|
|
76
78
|
private async recoverFromWAL() {
|
|
77
79
|
if (!fs.existsSync(this.walPath)) return;
|
|
78
80
|
|
|
79
|
-
const
|
|
80
|
-
.split("\n")
|
|
81
|
-
.filter(Boolean);
|
|
81
|
+
const raw = await fs.promises.readFile(this.walPath, "utf8");
|
|
82
82
|
|
|
83
83
|
const committed = new Set<number>();
|
|
84
|
+
const applied = new Set<number>();
|
|
84
85
|
const ops = new Map<number, TXOp[]>();
|
|
85
86
|
|
|
86
|
-
for (const line of
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (
|
|
93
|
-
|
|
87
|
+
for (const line of raw.split("\n")) {
|
|
88
|
+
if (!line.trim()) continue;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const entry: WALEntry = JSON.parse(line);
|
|
92
|
+
|
|
93
|
+
if ("commit" in entry) committed.add(entry.tx);
|
|
94
|
+
else if ("applied" in entry) applied.add(entry.tx);
|
|
95
|
+
else {
|
|
96
|
+
if (!ops.has(entry.tx)) ops.set(entry.tx, []);
|
|
97
|
+
ops.get(entry.tx)!.push(entry);
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// Ignore corrupted WAL tail
|
|
101
|
+
break;
|
|
94
102
|
}
|
|
95
103
|
}
|
|
96
104
|
|
|
97
105
|
for (const tx of committed) {
|
|
106
|
+
if (applied.has(tx)) continue;
|
|
107
|
+
|
|
98
108
|
const txOps = ops.get(tx);
|
|
99
109
|
if (txOps) await this.applyTransaction(txOps);
|
|
100
110
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import net from "net";
|
|
2
|
+
import { getIPCSocketPath } from "./socketPath.js";
|
|
3
|
+
|
|
4
|
+
function delay(ms: number) {
|
|
5
|
+
return new Promise(r => setTimeout(r, ms));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async function connectWithRetry(path: string): Promise<net.Socket> {
|
|
9
|
+
let attempt = 0;
|
|
10
|
+
|
|
11
|
+
while (true) {
|
|
12
|
+
try {
|
|
13
|
+
return await new Promise((resolve, reject) => {
|
|
14
|
+
const socket = net.connect(path, () => resolve(socket));
|
|
15
|
+
socket.once("error", reject);
|
|
16
|
+
});
|
|
17
|
+
} catch (err: any) {
|
|
18
|
+
if (err.code === "ENOENT" || err.code === "ECONNREFUSED") {
|
|
19
|
+
if (attempt++ > 80) {
|
|
20
|
+
throw new Error("IPC server not reachable");
|
|
21
|
+
}
|
|
22
|
+
await delay(50);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class IPCClient {
|
|
31
|
+
private socket!: net.Socket;
|
|
32
|
+
private buffer = "";
|
|
33
|
+
private seq = 0;
|
|
34
|
+
private pending = new Map<number, (v: any) => void>();
|
|
35
|
+
private ready: Promise<void>;
|
|
36
|
+
|
|
37
|
+
constructor(rootPath: string) {
|
|
38
|
+
const socketPath = getIPCSocketPath(rootPath);
|
|
39
|
+
this.ready = this.init(socketPath);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private async init(socketPath: string) {
|
|
43
|
+
this.socket = await connectWithRetry(socketPath);
|
|
44
|
+
|
|
45
|
+
this.socket.on("data", data => {
|
|
46
|
+
this.buffer += data.toString();
|
|
47
|
+
|
|
48
|
+
while (this.buffer.includes("\n")) {
|
|
49
|
+
const idx = this.buffer.indexOf("\n");
|
|
50
|
+
const raw = this.buffer.slice(0, idx);
|
|
51
|
+
this.buffer = this.buffer.slice(idx + 1);
|
|
52
|
+
|
|
53
|
+
const msg = JSON.parse(raw);
|
|
54
|
+
const cb = this.pending.get(msg.id);
|
|
55
|
+
|
|
56
|
+
if (cb) {
|
|
57
|
+
this.pending.delete(msg.id);
|
|
58
|
+
cb(msg);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
this.socket.on("error", err => {
|
|
64
|
+
console.error("IPC socket error:", err);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async exec(action: string, args: any) {
|
|
69
|
+
await this.ready; // 🔥 HARD BARRIER — guarantees socket exists
|
|
70
|
+
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const id = ++this.seq;
|
|
73
|
+
|
|
74
|
+
this.pending.set(id, msg => {
|
|
75
|
+
msg.ok ? resolve(msg.result) : reject(new Error(msg.error));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
this.socket.write(JSON.stringify({ id, action, args }) + "\n");
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
close() {
|
|
83
|
+
try { this.socket.end(); } catch {}
|
|
84
|
+
}
|
|
85
|
+
}
|
package/src/ipc/queue.ts
CHANGED
|
@@ -1,137 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import { fileURLToPath } from "url";
|
|
4
|
-
|
|
5
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
-
const __dirname = path.dirname(__filename);
|
|
7
|
-
|
|
8
|
-
function resolveWorkerPath() {
|
|
9
|
-
return path.resolve(__dirname, "../dist/worker/dbWorker.js");
|
|
10
|
-
}
|
|
1
|
+
import { IPCClient } from "./client.js";
|
|
2
|
+
import { getDefaultRootPath } from "../utils/rootpath.js";
|
|
11
3
|
|
|
12
4
|
export class DBQueue {
|
|
13
|
-
private
|
|
14
|
-
private seq = 0;
|
|
15
|
-
private pending = new Map<number, (r: any) => void>();
|
|
16
|
-
private isShutdown = false;
|
|
17
|
-
private restarting = false;
|
|
18
|
-
private workerAlive = false;
|
|
19
|
-
|
|
20
|
-
constructor() {
|
|
21
|
-
this.spawnWorker();
|
|
5
|
+
private client: IPCClient;
|
|
22
6
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
process.once("SIGTERM", () => this.shutdown());
|
|
7
|
+
constructor(rootPath = getDefaultRootPath()) {
|
|
8
|
+
this.client = new IPCClient(rootPath);
|
|
26
9
|
}
|
|
27
10
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
private spawnWorker() {
|
|
31
|
-
const workerPath = resolveWorkerPath();
|
|
32
|
-
|
|
33
|
-
this.worker = fork(workerPath, [], {
|
|
34
|
-
stdio: ["inherit", "inherit", "inherit", "ipc"],
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
this.workerAlive = true;
|
|
38
|
-
|
|
39
|
-
this.worker.on("message", (msg: any) => {
|
|
40
|
-
const cb = this.pending.get(msg.id);
|
|
41
|
-
if (cb) {
|
|
42
|
-
this.pending.delete(msg.id);
|
|
43
|
-
cb(msg);
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
this.worker.once("exit", (code, signal) => {
|
|
48
|
-
this.workerAlive = false;
|
|
49
|
-
|
|
50
|
-
if (this.isShutdown) return;
|
|
51
|
-
|
|
52
|
-
console.error("DB Worker crashed, restarting...", { code, signal });
|
|
53
|
-
|
|
54
|
-
this.restartWorker();
|
|
55
|
-
});
|
|
11
|
+
exec(action: string, args: any) {
|
|
12
|
+
return this.client.exec(action, args);
|
|
56
13
|
}
|
|
57
14
|
|
|
58
|
-
private restartWorker() {
|
|
59
|
-
if (this.restarting || this.isShutdown) return;
|
|
60
|
-
|
|
61
|
-
this.restarting = true;
|
|
62
|
-
|
|
63
|
-
setTimeout(() => {
|
|
64
|
-
if (this.isShutdown) return;
|
|
65
|
-
|
|
66
|
-
for (const [, cb] of this.pending) {
|
|
67
|
-
cb({ ok: false, error: "IPC worker crashed" });
|
|
68
|
-
}
|
|
69
|
-
this.pending.clear();
|
|
70
|
-
this.seq = 0;
|
|
71
|
-
|
|
72
|
-
this.spawnWorker();
|
|
73
|
-
this.restarting = false;
|
|
74
|
-
}, 500);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/* ---------------- IPC Exec ---------------- */
|
|
78
|
-
|
|
79
|
-
exec(action: string, args: any, timeout = 15000) {
|
|
80
|
-
if (this.isShutdown) {
|
|
81
|
-
return Promise.reject(new Error("DBQueue is shutdown"));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (!this.workerAlive) {
|
|
85
|
-
return Promise.reject(new Error("IPC worker not running"));
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return new Promise((resolve, reject) => {
|
|
89
|
-
const id = ++this.seq;
|
|
90
|
-
|
|
91
|
-
const timer = setTimeout(() => {
|
|
92
|
-
this.pending.delete(id);
|
|
93
|
-
reject(new Error(`IPC timeout: ${action}`));
|
|
94
|
-
}, timeout);
|
|
95
|
-
|
|
96
|
-
this.pending.set(id, (msg) => {
|
|
97
|
-
clearTimeout(timer);
|
|
98
|
-
this.pending.delete(id);
|
|
99
|
-
msg.ok ? resolve(msg.result) : reject(new Error(msg.error));
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
try {
|
|
103
|
-
this.worker.send({ id, action, args });
|
|
104
|
-
} catch (err) {
|
|
105
|
-
clearTimeout(timer);
|
|
106
|
-
this.pending.delete(id);
|
|
107
|
-
reject(err);
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/* ---------------- Clean Shutdown ---------------- */
|
|
113
|
-
|
|
114
15
|
async shutdown() {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
try {
|
|
120
|
-
this.worker.send({ action: "shutdown" });
|
|
121
|
-
} catch {}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
await new Promise(resolve => {
|
|
125
|
-
if (!this.workerAlive) return resolve(null);
|
|
126
|
-
this.worker.once("exit", resolve);
|
|
127
|
-
setTimeout(resolve, 250);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
for (const [, cb] of this.pending) {
|
|
131
|
-
cb({ ok: false, error: "IPC shutdown" });
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
this.pending.clear();
|
|
16
|
+
try {
|
|
17
|
+
await this.exec("shutdown", {});
|
|
18
|
+
} catch {}
|
|
19
|
+
this.client.close();
|
|
135
20
|
}
|
|
136
21
|
}
|
|
137
22
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import net from "net";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { LioranManager } from "../LioranManager.js";
|
|
4
|
+
import { getIPCSocketPath } from "./socketPath.js";
|
|
5
|
+
|
|
6
|
+
export class IPCServer {
|
|
7
|
+
private server!: net.Server;
|
|
8
|
+
private manager: LioranManager;
|
|
9
|
+
private socketPath: string;
|
|
10
|
+
|
|
11
|
+
constructor(manager: LioranManager, rootPath: string) {
|
|
12
|
+
this.manager = manager;
|
|
13
|
+
this.socketPath = getIPCSocketPath(rootPath);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
start() {
|
|
17
|
+
if (!this.socketPath.startsWith("\\\\.\\")) {
|
|
18
|
+
if (fs.existsSync(this.socketPath)) fs.unlinkSync(this.socketPath);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
this.server = net.createServer(socket => {
|
|
22
|
+
let buffer = "";
|
|
23
|
+
|
|
24
|
+
socket.on("data", async data => {
|
|
25
|
+
buffer += data.toString();
|
|
26
|
+
|
|
27
|
+
while (buffer.includes("\n")) {
|
|
28
|
+
const idx = buffer.indexOf("\n");
|
|
29
|
+
const raw = buffer.slice(0, idx);
|
|
30
|
+
buffer = buffer.slice(idx + 1);
|
|
31
|
+
|
|
32
|
+
const msg = JSON.parse(raw);
|
|
33
|
+
this.handleMessage(socket, msg).catch(console.error);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
this.server.listen(this.socketPath, () => {
|
|
39
|
+
console.log("[IPC] Server listening:", this.socketPath);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private async handleMessage(socket: net.Socket, msg: any) {
|
|
44
|
+
const { id, action, args } = msg;
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
let result;
|
|
48
|
+
|
|
49
|
+
switch (action) {
|
|
50
|
+
case "db":
|
|
51
|
+
await this.manager.db(args.db);
|
|
52
|
+
result = true;
|
|
53
|
+
break;
|
|
54
|
+
|
|
55
|
+
case "op": {
|
|
56
|
+
const { db, col, method, params } = args;
|
|
57
|
+
const collection = (await this.manager.db(db)).collection(col);
|
|
58
|
+
result = await (collection as any)[method](...params);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
case "shutdown":
|
|
63
|
+
await this.manager.closeAll();
|
|
64
|
+
result = true;
|
|
65
|
+
break;
|
|
66
|
+
|
|
67
|
+
default:
|
|
68
|
+
throw new Error("Unknown IPC action");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
socket.write(JSON.stringify({ id, ok: true, result }) + "\n");
|
|
72
|
+
} catch (err: any) {
|
|
73
|
+
socket.write(JSON.stringify({ id, ok: false, error: err.message }) + "\n");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async close() {
|
|
78
|
+
if (this.server) this.server.close();
|
|
79
|
+
|
|
80
|
+
if (!this.socketPath.startsWith("\\\\.\\")) {
|
|
81
|
+
try { fs.unlinkSync(this.socketPath); } catch {}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
export function getIPCSocketPath(rootPath: string) {
|
|
5
|
+
if (os.platform() === "win32") {
|
|
6
|
+
return `\\\\.\\pipe\\liorandb_${rootPath.replace(/[:\\\/]/g, "_")}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return path.join(rootPath, ".lioran.sock");
|
|
10
|
+
}
|
package/src/worker/dbWorker.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { LioranManager } from "../LioranManager.js";
|
|
2
|
-
|
|
3
|
-
const manager = new LioranManager({ ipc: false });
|
|
4
|
-
|
|
5
|
-
process.on("message", async (msg: any) => {
|
|
6
|
-
const { id, action, args } = msg;
|
|
7
|
-
|
|
8
|
-
try {
|
|
9
|
-
let result;
|
|
10
|
-
|
|
11
|
-
switch (action) {
|
|
12
|
-
case "shutdown":
|
|
13
|
-
await manager.closeAll();
|
|
14
|
-
result = true;
|
|
15
|
-
break;
|
|
16
|
-
|
|
17
|
-
case "db":
|
|
18
|
-
await manager.db(args.db);
|
|
19
|
-
result = true;
|
|
20
|
-
break;
|
|
21
|
-
|
|
22
|
-
case "op": {
|
|
23
|
-
const { db, col, method, params } = args;
|
|
24
|
-
const collection = (await manager.db(db)).collection(col);
|
|
25
|
-
result = await (collection as any)[method](...params);
|
|
26
|
-
break;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
case "tx": {
|
|
30
|
-
const db = await manager.db(args.db);
|
|
31
|
-
result = await db.transaction(args.fn);
|
|
32
|
-
break;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
default:
|
|
36
|
-
throw new Error("Unknown IPC action");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
process.send?.({ id, ok: true, result });
|
|
40
|
-
} catch (err: any) {
|
|
41
|
-
process.send?.({ id, ok: false, error: err.message });
|
|
42
|
-
}
|
|
43
|
-
});
|