@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/dist/index.js
CHANGED
|
@@ -1,8 +1,793 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
// src/LioranManager.ts
|
|
2
|
+
import path4 from "path";
|
|
3
|
+
import fs4 from "fs";
|
|
4
|
+
import process2 from "process";
|
|
5
|
+
|
|
6
|
+
// src/core/database.ts
|
|
7
|
+
import path from "path";
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
|
|
10
|
+
// src/core/collection.ts
|
|
11
|
+
import { ClassicLevel } from "classic-level";
|
|
12
|
+
|
|
13
|
+
// src/core/query.ts
|
|
14
|
+
function getByPath(obj, path5) {
|
|
15
|
+
return path5.split(".").reduce((o, p) => o ? o[p] : void 0, obj);
|
|
16
|
+
}
|
|
17
|
+
function matchDocument(doc, query) {
|
|
18
|
+
for (const key of Object.keys(query)) {
|
|
19
|
+
const cond = query[key];
|
|
20
|
+
const val = getByPath(doc, key);
|
|
21
|
+
if (cond && typeof cond === "object" && !Array.isArray(cond)) {
|
|
22
|
+
for (const op of Object.keys(cond)) {
|
|
23
|
+
const v = cond[op];
|
|
24
|
+
if (op === "$gt" && !(val > v)) return false;
|
|
25
|
+
if (op === "$gte" && !(val >= v)) return false;
|
|
26
|
+
if (op === "$lt" && !(val < v)) return false;
|
|
27
|
+
if (op === "$lte" && !(val <= v)) return false;
|
|
28
|
+
if (op === "$ne" && val === v) return false;
|
|
29
|
+
if (op === "$eq" && val !== v) return false;
|
|
30
|
+
if (op === "$in" && (!Array.isArray(v) || !v.includes(val)))
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
if (val !== cond) return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
function applyUpdate(oldDoc, update) {
|
|
40
|
+
const doc = structuredClone(oldDoc);
|
|
41
|
+
if (update.$set) {
|
|
42
|
+
for (const k in update.$set) {
|
|
43
|
+
const parts = k.split(".");
|
|
44
|
+
let cur = doc;
|
|
45
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
46
|
+
cur[parts[i]] ??= {};
|
|
47
|
+
cur = cur[parts[i]];
|
|
48
|
+
}
|
|
49
|
+
cur[parts.at(-1)] = update.$set[k];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (update.$inc) {
|
|
53
|
+
for (const k in update.$inc) {
|
|
54
|
+
const val = getByPath(doc, k) ?? 0;
|
|
55
|
+
const parts = k.split(".");
|
|
56
|
+
let cur = doc;
|
|
57
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
58
|
+
cur[parts[i]] ??= {};
|
|
59
|
+
cur = cur[parts[i]];
|
|
60
|
+
}
|
|
61
|
+
cur[parts.at(-1)] = val + update.$inc[k];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const hasOp = Object.keys(update).some((k) => k.startsWith("$"));
|
|
65
|
+
if (!hasOp) {
|
|
66
|
+
return { ...doc, ...update };
|
|
67
|
+
}
|
|
68
|
+
return doc;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/core/collection.ts
|
|
72
|
+
import { v4 as uuid } from "uuid";
|
|
73
|
+
|
|
74
|
+
// src/utils/encryption.ts
|
|
75
|
+
import crypto2 from "crypto";
|
|
76
|
+
|
|
77
|
+
// src/utils/secureKey.ts
|
|
78
|
+
import crypto from "crypto";
|
|
79
|
+
import os from "os";
|
|
80
|
+
function getMasterKey() {
|
|
81
|
+
const fingerprint = [
|
|
82
|
+
os.hostname(),
|
|
83
|
+
os.platform(),
|
|
84
|
+
os.arch(),
|
|
85
|
+
os.cpus()?.[0]?.model ?? "unknown",
|
|
86
|
+
os.cpus()?.length ?? 0,
|
|
87
|
+
os.totalmem()
|
|
88
|
+
].join("|");
|
|
89
|
+
return crypto.createHash("sha256").update(fingerprint).digest();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/utils/encryption.ts
|
|
93
|
+
var algorithm = "aes-256-gcm";
|
|
94
|
+
var ACTIVE_KEY = getMasterKey();
|
|
95
|
+
function setEncryptionKey(key) {
|
|
96
|
+
if (!key) return;
|
|
97
|
+
if (typeof key === "string") {
|
|
98
|
+
ACTIVE_KEY = crypto2.createHash("sha256").update(key).digest();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (Buffer.isBuffer(key)) {
|
|
102
|
+
if (key.length !== 32) {
|
|
103
|
+
throw new Error("Encryption key must be 32 bytes");
|
|
104
|
+
}
|
|
105
|
+
ACTIVE_KEY = key;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
throw new Error("Invalid encryption key format");
|
|
109
|
+
}
|
|
110
|
+
function encryptData(obj) {
|
|
111
|
+
const iv = crypto2.randomBytes(16);
|
|
112
|
+
const json = JSON.stringify(obj);
|
|
113
|
+
if (json.length > 5e6) {
|
|
114
|
+
throw new Error("Document too large (>5MB)");
|
|
115
|
+
}
|
|
116
|
+
const data = Buffer.from(json, "utf8");
|
|
117
|
+
const cipher = crypto2.createCipheriv(algorithm, ACTIVE_KEY, iv);
|
|
118
|
+
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
|
119
|
+
const tag = cipher.getAuthTag();
|
|
120
|
+
return Buffer.concat([iv, tag, encrypted]).toString("base64");
|
|
121
|
+
}
|
|
122
|
+
function decryptData(enc) {
|
|
123
|
+
const buf = Buffer.from(enc, "base64");
|
|
124
|
+
const iv = buf.subarray(0, 16);
|
|
125
|
+
const tag = buf.subarray(16, 32);
|
|
126
|
+
const encrypted = buf.subarray(32);
|
|
127
|
+
const decipher = crypto2.createDecipheriv(algorithm, ACTIVE_KEY, iv);
|
|
128
|
+
decipher.setAuthTag(tag);
|
|
129
|
+
const decrypted = Buffer.concat([
|
|
130
|
+
decipher.update(encrypted),
|
|
131
|
+
decipher.final()
|
|
132
|
+
]);
|
|
133
|
+
return JSON.parse(decrypted.toString("utf8"));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/utils/schema.ts
|
|
137
|
+
function validateSchema(schema, data) {
|
|
138
|
+
const result = schema.safeParse(data);
|
|
139
|
+
if (!result.success) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
"Schema validation failed:\n" + JSON.stringify(result.error.format(), null, 2)
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return result.data;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/core/collection.ts
|
|
148
|
+
var Collection = class {
|
|
149
|
+
dir;
|
|
150
|
+
db;
|
|
151
|
+
queue = Promise.resolve();
|
|
152
|
+
schema;
|
|
153
|
+
constructor(dir, schema) {
|
|
154
|
+
this.dir = dir;
|
|
155
|
+
this.db = new ClassicLevel(dir, { valueEncoding: "utf8" });
|
|
156
|
+
this.schema = schema;
|
|
157
|
+
}
|
|
158
|
+
setSchema(schema) {
|
|
159
|
+
this.schema = schema;
|
|
160
|
+
}
|
|
161
|
+
validate(doc) {
|
|
162
|
+
return this.schema ? validateSchema(this.schema, doc) : doc;
|
|
163
|
+
}
|
|
164
|
+
_enqueue(task) {
|
|
165
|
+
this.queue = this.queue.then(task).catch(console.error);
|
|
166
|
+
return this.queue;
|
|
167
|
+
}
|
|
168
|
+
async close() {
|
|
169
|
+
try {
|
|
170
|
+
await this.db.close();
|
|
171
|
+
} catch {
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async _exec(op, args) {
|
|
175
|
+
switch (op) {
|
|
176
|
+
case "insertOne":
|
|
177
|
+
return this._insertOne(args[0]);
|
|
178
|
+
case "insertMany":
|
|
179
|
+
return this._insertMany(args[0]);
|
|
180
|
+
case "find":
|
|
181
|
+
return this._find(args[0]);
|
|
182
|
+
case "findOne":
|
|
183
|
+
return this._findOne(args[0]);
|
|
184
|
+
case "updateOne":
|
|
185
|
+
return this._updateOne(args[0], args[1], args[2]);
|
|
186
|
+
case "updateMany":
|
|
187
|
+
return this._updateMany(args[0], args[1]);
|
|
188
|
+
case "deleteOne":
|
|
189
|
+
return this._deleteOne(args[0]);
|
|
190
|
+
case "deleteMany":
|
|
191
|
+
return this._deleteMany(args[0]);
|
|
192
|
+
case "countDocuments":
|
|
193
|
+
return this._countDocuments(args[0]);
|
|
194
|
+
default:
|
|
195
|
+
throw new Error(`Unknown operation: ${op}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
insertOne(doc) {
|
|
199
|
+
return this._enqueue(() => this._exec("insertOne", [doc]));
|
|
200
|
+
}
|
|
201
|
+
insertMany(docs = []) {
|
|
202
|
+
return this._enqueue(() => this._exec("insertMany", [docs]));
|
|
203
|
+
}
|
|
204
|
+
find(query = {}) {
|
|
205
|
+
return this._enqueue(() => this._exec("find", [query]));
|
|
206
|
+
}
|
|
207
|
+
findOne(query = {}) {
|
|
208
|
+
return this._enqueue(() => this._exec("findOne", [query]));
|
|
209
|
+
}
|
|
210
|
+
updateOne(filter, update, options = {}) {
|
|
211
|
+
return this._enqueue(
|
|
212
|
+
() => this._exec("updateOne", [filter, update, options])
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
updateMany(filter, update) {
|
|
216
|
+
return this._enqueue(
|
|
217
|
+
() => this._exec("updateMany", [filter, update])
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
deleteOne(filter) {
|
|
221
|
+
return this._enqueue(
|
|
222
|
+
() => this._exec("deleteOne", [filter])
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
deleteMany(filter) {
|
|
226
|
+
return this._enqueue(
|
|
227
|
+
() => this._exec("deleteMany", [filter])
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
countDocuments(filter = {}) {
|
|
231
|
+
return this._enqueue(
|
|
232
|
+
() => this._exec("countDocuments", [filter])
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
/* ---------------- Storage ---------------- */
|
|
236
|
+
async _insertOne(doc) {
|
|
237
|
+
const _id = doc._id ?? uuid();
|
|
238
|
+
const final = this.validate({ _id, ...doc });
|
|
239
|
+
await this.db.put(String(_id), encryptData(final));
|
|
240
|
+
return final;
|
|
241
|
+
}
|
|
242
|
+
async _insertMany(docs) {
|
|
243
|
+
const batch = [];
|
|
244
|
+
const out = [];
|
|
245
|
+
for (const d of docs) {
|
|
246
|
+
const _id = d._id ?? uuid();
|
|
247
|
+
const final = this.validate({ _id, ...d });
|
|
248
|
+
batch.push({ type: "put", key: String(_id), value: encryptData(final) });
|
|
249
|
+
out.push(final);
|
|
250
|
+
}
|
|
251
|
+
await this.db.batch(batch);
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
async _findOne(query) {
|
|
255
|
+
if (query?._id) {
|
|
256
|
+
try {
|
|
257
|
+
const enc = await this.db.get(String(query._id));
|
|
258
|
+
return enc ? decryptData(enc) : null;
|
|
259
|
+
} catch {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
for await (const [, enc] of this.db.iterator()) {
|
|
264
|
+
const v = decryptData(enc);
|
|
265
|
+
if (matchDocument(v, query)) return v;
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
async _updateOne(filter, update, options) {
|
|
270
|
+
for await (const [key, enc] of this.db.iterator()) {
|
|
271
|
+
const value = decryptData(enc);
|
|
272
|
+
if (matchDocument(value, filter)) {
|
|
273
|
+
const updated = this.validate(applyUpdate(value, update));
|
|
274
|
+
updated._id = value._id;
|
|
275
|
+
await this.db.put(key, encryptData(updated));
|
|
276
|
+
return updated;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (options?.upsert) {
|
|
280
|
+
const doc = this.validate({ _id: uuid(), ...applyUpdate({}, update) });
|
|
281
|
+
await this.db.put(String(doc._id), encryptData(doc));
|
|
282
|
+
return doc;
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
async _updateMany(filter, update) {
|
|
287
|
+
const out = [];
|
|
288
|
+
for await (const [key, enc] of this.db.iterator()) {
|
|
289
|
+
const value = decryptData(enc);
|
|
290
|
+
if (matchDocument(value, filter)) {
|
|
291
|
+
const updated = this.validate(applyUpdate(value, update));
|
|
292
|
+
updated._id = value._id;
|
|
293
|
+
await this.db.put(key, encryptData(updated));
|
|
294
|
+
out.push(updated);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return out;
|
|
298
|
+
}
|
|
299
|
+
async _find(query) {
|
|
300
|
+
const out = [];
|
|
301
|
+
for await (const [, enc] of this.db.iterator()) {
|
|
302
|
+
const v = decryptData(enc);
|
|
303
|
+
if (matchDocument(v, query)) out.push(v);
|
|
304
|
+
}
|
|
305
|
+
return out;
|
|
306
|
+
}
|
|
307
|
+
async _deleteOne(filter) {
|
|
308
|
+
for await (const [key, enc] of this.db.iterator()) {
|
|
309
|
+
if (matchDocument(decryptData(enc), filter)) {
|
|
310
|
+
await this.db.del(key);
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
async _deleteMany(filter) {
|
|
317
|
+
let count = 0;
|
|
318
|
+
for await (const [key, enc] of this.db.iterator()) {
|
|
319
|
+
if (matchDocument(decryptData(enc), filter)) {
|
|
320
|
+
await this.db.del(key);
|
|
321
|
+
count++;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return count;
|
|
325
|
+
}
|
|
326
|
+
async _countDocuments(filter) {
|
|
327
|
+
let c = 0;
|
|
328
|
+
for await (const [, enc] of this.db.iterator()) {
|
|
329
|
+
if (matchDocument(decryptData(enc), filter)) c++;
|
|
330
|
+
}
|
|
331
|
+
return c;
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// src/core/database.ts
|
|
336
|
+
var DBTransactionContext = class {
|
|
337
|
+
constructor(db, txId) {
|
|
338
|
+
this.db = db;
|
|
339
|
+
this.txId = txId;
|
|
340
|
+
}
|
|
341
|
+
ops = [];
|
|
342
|
+
collection(name) {
|
|
343
|
+
return new Proxy({}, {
|
|
344
|
+
get: (_, prop) => {
|
|
345
|
+
return (...args) => {
|
|
346
|
+
this.ops.push({
|
|
347
|
+
tx: this.txId,
|
|
348
|
+
col: name,
|
|
349
|
+
op: prop,
|
|
350
|
+
args
|
|
351
|
+
});
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
async commit() {
|
|
357
|
+
await this.db.writeWAL(this.ops);
|
|
358
|
+
await this.db.writeWAL([{ tx: this.txId, commit: true }]);
|
|
359
|
+
await this.db.applyTransaction(this.ops);
|
|
360
|
+
await this.db.writeWAL([{ tx: this.txId, applied: true }]);
|
|
361
|
+
await this.db.clearWAL();
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
var LioranDB = class _LioranDB {
|
|
365
|
+
basePath;
|
|
366
|
+
dbName;
|
|
367
|
+
manager;
|
|
368
|
+
collections;
|
|
369
|
+
walPath;
|
|
370
|
+
static TX_SEQ = 0;
|
|
371
|
+
constructor(basePath, dbName, manager) {
|
|
372
|
+
this.basePath = basePath;
|
|
373
|
+
this.dbName = dbName;
|
|
374
|
+
this.manager = manager;
|
|
375
|
+
this.collections = /* @__PURE__ */ new Map();
|
|
376
|
+
this.walPath = path.join(basePath, "__tx_wal.log");
|
|
377
|
+
fs.mkdirSync(basePath, { recursive: true });
|
|
378
|
+
this.recoverFromWAL().catch(console.error);
|
|
379
|
+
}
|
|
380
|
+
async writeWAL(entries) {
|
|
381
|
+
const fd = await fs.promises.open(this.walPath, "a");
|
|
382
|
+
for (const e of entries) {
|
|
383
|
+
await fd.write(JSON.stringify(e) + "\n");
|
|
384
|
+
}
|
|
385
|
+
await fd.sync();
|
|
386
|
+
await fd.close();
|
|
387
|
+
}
|
|
388
|
+
async clearWAL() {
|
|
389
|
+
try {
|
|
390
|
+
await fs.promises.unlink(this.walPath);
|
|
391
|
+
} catch {
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
async recoverFromWAL() {
|
|
395
|
+
if (!fs.existsSync(this.walPath)) return;
|
|
396
|
+
const raw = await fs.promises.readFile(this.walPath, "utf8");
|
|
397
|
+
const committed = /* @__PURE__ */ new Set();
|
|
398
|
+
const applied = /* @__PURE__ */ new Set();
|
|
399
|
+
const ops = /* @__PURE__ */ new Map();
|
|
400
|
+
for (const line of raw.split("\n")) {
|
|
401
|
+
if (!line.trim()) continue;
|
|
402
|
+
try {
|
|
403
|
+
const entry = JSON.parse(line);
|
|
404
|
+
if ("commit" in entry) committed.add(entry.tx);
|
|
405
|
+
else if ("applied" in entry) applied.add(entry.tx);
|
|
406
|
+
else {
|
|
407
|
+
if (!ops.has(entry.tx)) ops.set(entry.tx, []);
|
|
408
|
+
ops.get(entry.tx).push(entry);
|
|
409
|
+
}
|
|
410
|
+
} catch {
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
for (const tx of committed) {
|
|
415
|
+
if (applied.has(tx)) continue;
|
|
416
|
+
const txOps = ops.get(tx);
|
|
417
|
+
if (txOps) await this.applyTransaction(txOps);
|
|
418
|
+
}
|
|
419
|
+
await this.clearWAL();
|
|
420
|
+
}
|
|
421
|
+
async applyTransaction(ops) {
|
|
422
|
+
for (const { col, op, args } of ops) {
|
|
423
|
+
const collection = this.collection(col);
|
|
424
|
+
await collection._exec(op, args);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
collection(name, schema) {
|
|
428
|
+
if (this.collections.has(name)) {
|
|
429
|
+
const col2 = this.collections.get(name);
|
|
430
|
+
if (schema) col2.setSchema(schema);
|
|
431
|
+
return col2;
|
|
432
|
+
}
|
|
433
|
+
const colPath = path.join(this.basePath, name);
|
|
434
|
+
fs.mkdirSync(colPath, { recursive: true });
|
|
435
|
+
const col = new Collection(colPath, schema);
|
|
436
|
+
this.collections.set(name, col);
|
|
437
|
+
return col;
|
|
438
|
+
}
|
|
439
|
+
async transaction(fn) {
|
|
440
|
+
const txId = ++_LioranDB.TX_SEQ;
|
|
441
|
+
const tx = new DBTransactionContext(this, txId);
|
|
442
|
+
const result = await fn(tx);
|
|
443
|
+
await tx.commit();
|
|
444
|
+
return result;
|
|
445
|
+
}
|
|
446
|
+
async close() {
|
|
447
|
+
for (const col of this.collections.values()) {
|
|
448
|
+
try {
|
|
449
|
+
await col.close();
|
|
450
|
+
} catch {
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
this.collections.clear();
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
// src/utils/rootpath.ts
|
|
458
|
+
import os2 from "os";
|
|
459
|
+
import path2 from "path";
|
|
460
|
+
import fs2 from "fs";
|
|
461
|
+
function getDefaultRootPath() {
|
|
462
|
+
let dbPath = process.env.LIORANDB_PATH;
|
|
463
|
+
if (!dbPath) {
|
|
464
|
+
const homeDir = os2.homedir();
|
|
465
|
+
dbPath = path2.join(homeDir, "LioranDB", "db");
|
|
466
|
+
if (!fs2.existsSync(dbPath)) {
|
|
467
|
+
fs2.mkdirSync(dbPath, { recursive: true });
|
|
468
|
+
}
|
|
469
|
+
process.env.LIORANDB_PATH = dbPath;
|
|
470
|
+
}
|
|
471
|
+
return dbPath;
|
|
472
|
+
}
|
|
473
|
+
function getBaseDBFolder() {
|
|
474
|
+
return getDefaultRootPath();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// src/ipc/client.ts
|
|
478
|
+
import net from "net";
|
|
479
|
+
|
|
480
|
+
// src/ipc/socketPath.ts
|
|
481
|
+
import os3 from "os";
|
|
482
|
+
import path3 from "path";
|
|
483
|
+
function getIPCSocketPath(rootPath) {
|
|
484
|
+
if (os3.platform() === "win32") {
|
|
485
|
+
return `\\\\.\\pipe\\liorandb_${rootPath.replace(/[:\\\/]/g, "_")}`;
|
|
486
|
+
}
|
|
487
|
+
return path3.join(rootPath, ".lioran.sock");
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// src/ipc/client.ts
|
|
491
|
+
function delay(ms) {
|
|
492
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
493
|
+
}
|
|
494
|
+
async function connectWithRetry(path5) {
|
|
495
|
+
let attempt = 0;
|
|
496
|
+
while (true) {
|
|
497
|
+
try {
|
|
498
|
+
return await new Promise((resolve, reject) => {
|
|
499
|
+
const socket = net.connect(path5, () => resolve(socket));
|
|
500
|
+
socket.once("error", reject);
|
|
501
|
+
});
|
|
502
|
+
} catch (err) {
|
|
503
|
+
if (err.code === "ENOENT" || err.code === "ECONNREFUSED") {
|
|
504
|
+
if (attempt++ > 80) {
|
|
505
|
+
throw new Error("IPC server not reachable");
|
|
506
|
+
}
|
|
507
|
+
await delay(50);
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
throw err;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
var IPCClient = class {
|
|
515
|
+
socket;
|
|
516
|
+
buffer = "";
|
|
517
|
+
seq = 0;
|
|
518
|
+
pending = /* @__PURE__ */ new Map();
|
|
519
|
+
ready;
|
|
520
|
+
constructor(rootPath) {
|
|
521
|
+
const socketPath = getIPCSocketPath(rootPath);
|
|
522
|
+
this.ready = this.init(socketPath);
|
|
523
|
+
}
|
|
524
|
+
async init(socketPath) {
|
|
525
|
+
this.socket = await connectWithRetry(socketPath);
|
|
526
|
+
this.socket.on("data", (data) => {
|
|
527
|
+
this.buffer += data.toString();
|
|
528
|
+
while (this.buffer.includes("\n")) {
|
|
529
|
+
const idx = this.buffer.indexOf("\n");
|
|
530
|
+
const raw = this.buffer.slice(0, idx);
|
|
531
|
+
this.buffer = this.buffer.slice(idx + 1);
|
|
532
|
+
const msg = JSON.parse(raw);
|
|
533
|
+
const cb = this.pending.get(msg.id);
|
|
534
|
+
if (cb) {
|
|
535
|
+
this.pending.delete(msg.id);
|
|
536
|
+
cb(msg);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
this.socket.on("error", (err) => {
|
|
541
|
+
console.error("IPC socket error:", err);
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
async exec(action, args) {
|
|
545
|
+
await this.ready;
|
|
546
|
+
return new Promise((resolve, reject) => {
|
|
547
|
+
const id = ++this.seq;
|
|
548
|
+
this.pending.set(id, (msg) => {
|
|
549
|
+
msg.ok ? resolve(msg.result) : reject(new Error(msg.error));
|
|
550
|
+
});
|
|
551
|
+
this.socket.write(JSON.stringify({ id, action, args }) + "\n");
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
close() {
|
|
555
|
+
try {
|
|
556
|
+
this.socket.end();
|
|
557
|
+
} catch {
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
// src/ipc/queue.ts
|
|
563
|
+
var DBQueue = class {
|
|
564
|
+
client;
|
|
565
|
+
constructor(rootPath = getDefaultRootPath()) {
|
|
566
|
+
this.client = new IPCClient(rootPath);
|
|
567
|
+
}
|
|
568
|
+
exec(action, args) {
|
|
569
|
+
return this.client.exec(action, args);
|
|
570
|
+
}
|
|
571
|
+
async shutdown() {
|
|
572
|
+
try {
|
|
573
|
+
await this.exec("shutdown", {});
|
|
574
|
+
} catch {
|
|
575
|
+
}
|
|
576
|
+
this.client.close();
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
var dbQueue = new DBQueue();
|
|
580
|
+
|
|
581
|
+
// src/ipc/server.ts
|
|
582
|
+
import net2 from "net";
|
|
583
|
+
import fs3 from "fs";
|
|
584
|
+
var IPCServer = class {
|
|
585
|
+
server;
|
|
586
|
+
manager;
|
|
587
|
+
socketPath;
|
|
588
|
+
constructor(manager, rootPath) {
|
|
589
|
+
this.manager = manager;
|
|
590
|
+
this.socketPath = getIPCSocketPath(rootPath);
|
|
591
|
+
}
|
|
592
|
+
start() {
|
|
593
|
+
if (!this.socketPath.startsWith("\\\\.\\")) {
|
|
594
|
+
if (fs3.existsSync(this.socketPath)) fs3.unlinkSync(this.socketPath);
|
|
595
|
+
}
|
|
596
|
+
this.server = net2.createServer((socket) => {
|
|
597
|
+
let buffer = "";
|
|
598
|
+
socket.on("data", async (data) => {
|
|
599
|
+
buffer += data.toString();
|
|
600
|
+
while (buffer.includes("\n")) {
|
|
601
|
+
const idx = buffer.indexOf("\n");
|
|
602
|
+
const raw = buffer.slice(0, idx);
|
|
603
|
+
buffer = buffer.slice(idx + 1);
|
|
604
|
+
const msg = JSON.parse(raw);
|
|
605
|
+
this.handleMessage(socket, msg).catch(console.error);
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
this.server.listen(this.socketPath, () => {
|
|
610
|
+
console.log("[IPC] Server listening:", this.socketPath);
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
async handleMessage(socket, msg) {
|
|
614
|
+
const { id, action, args } = msg;
|
|
615
|
+
try {
|
|
616
|
+
let result;
|
|
617
|
+
switch (action) {
|
|
618
|
+
case "db":
|
|
619
|
+
await this.manager.db(args.db);
|
|
620
|
+
result = true;
|
|
621
|
+
break;
|
|
622
|
+
case "op": {
|
|
623
|
+
const { db, col, method, params } = args;
|
|
624
|
+
const collection = (await this.manager.db(db)).collection(col);
|
|
625
|
+
result = await collection[method](...params);
|
|
626
|
+
break;
|
|
627
|
+
}
|
|
628
|
+
case "shutdown":
|
|
629
|
+
await this.manager.closeAll();
|
|
630
|
+
result = true;
|
|
631
|
+
break;
|
|
632
|
+
default:
|
|
633
|
+
throw new Error("Unknown IPC action");
|
|
634
|
+
}
|
|
635
|
+
socket.write(JSON.stringify({ id, ok: true, result }) + "\n");
|
|
636
|
+
} catch (err) {
|
|
637
|
+
socket.write(JSON.stringify({ id, ok: false, error: err.message }) + "\n");
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
async close() {
|
|
641
|
+
if (this.server) this.server.close();
|
|
642
|
+
if (!this.socketPath.startsWith("\\\\.\\")) {
|
|
643
|
+
try {
|
|
644
|
+
fs3.unlinkSync(this.socketPath);
|
|
645
|
+
} catch {
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
// src/LioranManager.ts
|
|
652
|
+
var LioranManager = class {
|
|
653
|
+
rootPath;
|
|
654
|
+
openDBs;
|
|
655
|
+
closed = false;
|
|
656
|
+
mode;
|
|
657
|
+
lockFd;
|
|
658
|
+
ipcServer;
|
|
659
|
+
constructor(options = {}) {
|
|
660
|
+
const { rootPath, encryptionKey } = options;
|
|
661
|
+
this.rootPath = rootPath || getDefaultRootPath();
|
|
662
|
+
if (!fs4.existsSync(this.rootPath)) {
|
|
663
|
+
fs4.mkdirSync(this.rootPath, { recursive: true });
|
|
664
|
+
}
|
|
665
|
+
if (encryptionKey) {
|
|
666
|
+
setEncryptionKey(encryptionKey);
|
|
667
|
+
}
|
|
668
|
+
this.openDBs = /* @__PURE__ */ new Map();
|
|
669
|
+
this.mode = this.tryAcquireLock() ? "primary" /* PRIMARY */ : "client" /* CLIENT */;
|
|
670
|
+
if (this.mode === "primary" /* PRIMARY */) {
|
|
671
|
+
this.ipcServer = new IPCServer(this, this.rootPath);
|
|
672
|
+
this.ipcServer.start();
|
|
673
|
+
this._registerShutdownHooks();
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
isProcessAlive(pid) {
|
|
677
|
+
try {
|
|
678
|
+
process2.kill(pid, 0);
|
|
679
|
+
return true;
|
|
680
|
+
} catch {
|
|
681
|
+
return false;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
tryAcquireLock() {
|
|
685
|
+
const lockPath = path4.join(this.rootPath, ".lioran.lock");
|
|
686
|
+
try {
|
|
687
|
+
this.lockFd = fs4.openSync(lockPath, "wx");
|
|
688
|
+
fs4.writeSync(this.lockFd, String(process2.pid));
|
|
689
|
+
return true;
|
|
690
|
+
} catch {
|
|
691
|
+
try {
|
|
692
|
+
const pid = Number(fs4.readFileSync(lockPath, "utf8"));
|
|
693
|
+
if (!this.isProcessAlive(pid)) {
|
|
694
|
+
fs4.unlinkSync(lockPath);
|
|
695
|
+
this.lockFd = fs4.openSync(lockPath, "wx");
|
|
696
|
+
fs4.writeSync(this.lockFd, String(process2.pid));
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
} catch {
|
|
700
|
+
}
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
async db(name) {
|
|
705
|
+
if (this.mode === "client" /* CLIENT */) {
|
|
706
|
+
await dbQueue.exec("db", { db: name });
|
|
707
|
+
return new IPCDatabase(name);
|
|
708
|
+
}
|
|
709
|
+
return this.openDatabase(name);
|
|
710
|
+
}
|
|
711
|
+
async openDatabase(name) {
|
|
712
|
+
this._assertOpen();
|
|
713
|
+
if (this.openDBs.has(name)) {
|
|
714
|
+
return this.openDBs.get(name);
|
|
715
|
+
}
|
|
716
|
+
const dbPath = path4.join(this.rootPath, name);
|
|
717
|
+
await fs4.promises.mkdir(dbPath, { recursive: true });
|
|
718
|
+
const db = new LioranDB(dbPath, name, this);
|
|
719
|
+
this.openDBs.set(name, db);
|
|
720
|
+
return db;
|
|
721
|
+
}
|
|
722
|
+
async closeAll() {
|
|
723
|
+
if (this.closed) return;
|
|
724
|
+
this.closed = true;
|
|
725
|
+
if (this.mode === "client" /* CLIENT */) {
|
|
726
|
+
await dbQueue.shutdown();
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
for (const db of this.openDBs.values()) {
|
|
730
|
+
try {
|
|
731
|
+
await db.close();
|
|
732
|
+
} catch {
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
this.openDBs.clear();
|
|
736
|
+
try {
|
|
737
|
+
if (this.lockFd) fs4.closeSync(this.lockFd);
|
|
738
|
+
fs4.unlinkSync(path4.join(this.rootPath, ".lioran.lock"));
|
|
739
|
+
} catch {
|
|
740
|
+
}
|
|
741
|
+
await this.ipcServer?.close();
|
|
742
|
+
}
|
|
743
|
+
async close() {
|
|
744
|
+
return this.closeAll();
|
|
745
|
+
}
|
|
746
|
+
_registerShutdownHooks() {
|
|
747
|
+
const shutdown = async () => {
|
|
748
|
+
await this.closeAll();
|
|
749
|
+
};
|
|
750
|
+
process2.on("SIGINT", shutdown);
|
|
751
|
+
process2.on("SIGTERM", shutdown);
|
|
752
|
+
process2.on("exit", shutdown);
|
|
753
|
+
}
|
|
754
|
+
_assertOpen() {
|
|
755
|
+
if (this.closed) {
|
|
756
|
+
throw new Error("LioranManager is closed");
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
var IPCDatabase = class {
|
|
761
|
+
constructor(name) {
|
|
762
|
+
this.name = name;
|
|
763
|
+
}
|
|
764
|
+
collection(name) {
|
|
765
|
+
return new IPCCollection(this.name, name);
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
var IPCCollection = class {
|
|
769
|
+
constructor(db, col) {
|
|
770
|
+
this.db = db;
|
|
771
|
+
this.col = col;
|
|
772
|
+
}
|
|
773
|
+
call(method, params) {
|
|
774
|
+
return dbQueue.exec("op", {
|
|
775
|
+
db: this.db,
|
|
776
|
+
col: this.col,
|
|
777
|
+
method,
|
|
778
|
+
params
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
insertOne = (doc) => this.call("insertOne", [doc]);
|
|
782
|
+
insertMany = (docs) => this.call("insertMany", [docs]);
|
|
783
|
+
find = (query) => this.call("find", [query]);
|
|
784
|
+
findOne = (query) => this.call("findOne", [query]);
|
|
785
|
+
updateOne = (filter, update, options) => this.call("updateOne", [filter, update, options]);
|
|
786
|
+
updateMany = (filter, update) => this.call("updateMany", [filter, update]);
|
|
787
|
+
deleteOne = (filter) => this.call("deleteOne", [filter]);
|
|
788
|
+
deleteMany = (filter) => this.call("deleteMany", [filter]);
|
|
789
|
+
countDocuments = (filter) => this.call("countDocuments", [filter]);
|
|
790
|
+
};
|
|
6
791
|
export {
|
|
7
792
|
LioranDB,
|
|
8
793
|
LioranManager,
|