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