@liorandb/core 1.1.0 → 1.1.1

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.
@@ -0,0 +1,1689 @@
1
+ // src/core/database.ts
2
+ import path6 from "path";
3
+ import fs6 from "fs";
4
+
5
+ // src/core/collection.ts
6
+ import { ClassicLevel as ClassicLevel3 } from "classic-level";
7
+
8
+ // src/core/query.ts
9
+ function getByPath(obj, path9) {
10
+ return path9.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))) return false;
26
+ }
27
+ } else {
28
+ if (val !== cond) return false;
29
+ }
30
+ }
31
+ return true;
32
+ }
33
+ function applyUpdate(oldDoc, update) {
34
+ const doc = structuredClone(oldDoc);
35
+ if (update.$set) {
36
+ for (const k in update.$set) {
37
+ const parts = k.split(".");
38
+ let cur = doc;
39
+ for (let i = 0; i < parts.length - 1; i++) {
40
+ cur[parts[i]] ??= {};
41
+ cur = cur[parts[i]];
42
+ }
43
+ cur[parts.at(-1)] = update.$set[k];
44
+ }
45
+ }
46
+ if (update.$inc) {
47
+ for (const k in update.$inc) {
48
+ const val = getByPath(doc, k) ?? 0;
49
+ const parts = k.split(".");
50
+ let cur = doc;
51
+ for (let i = 0; i < parts.length - 1; i++) {
52
+ cur[parts[i]] ??= {};
53
+ cur = cur[parts[i]];
54
+ }
55
+ cur[parts.at(-1)] = val + update.$inc[k];
56
+ }
57
+ }
58
+ const hasOp = Object.keys(update).some((k) => k.startsWith("$"));
59
+ if (!hasOp) {
60
+ return { ...doc, ...update };
61
+ }
62
+ return doc;
63
+ }
64
+ function selectIndex(query, indexes) {
65
+ for (const key of Object.keys(query)) {
66
+ if (!indexes.has(key)) continue;
67
+ const cond = query[key];
68
+ if (cond && typeof cond === "object" && !Array.isArray(cond)) {
69
+ return { field: key, cond };
70
+ }
71
+ return { field: key, cond: { $eq: cond } };
72
+ }
73
+ return null;
74
+ }
75
+ async function runIndexedQuery(query, indexProvider, allDocIds) {
76
+ const indexes = indexProvider.indexes;
77
+ if (!indexes?.size) {
78
+ return new Set(await allDocIds());
79
+ }
80
+ const sel = selectIndex(query, indexes);
81
+ if (!sel) {
82
+ return new Set(await allDocIds());
83
+ }
84
+ const { field, cond } = sel;
85
+ if ("$eq" in cond) {
86
+ return await indexProvider.findByIndex(field, cond.$eq) ?? new Set(await allDocIds());
87
+ }
88
+ if ("$in" in cond) {
89
+ const out = /* @__PURE__ */ new Set();
90
+ for (const v of cond.$in) {
91
+ const r = await indexProvider.findByIndex(field, v);
92
+ if (r) for (const id of r) out.add(id);
93
+ }
94
+ return out;
95
+ }
96
+ if (indexProvider.rangeByIndex && ("$gt" in cond || "$gte" in cond || "$lt" in cond || "$lte" in cond)) {
97
+ return await indexProvider.rangeByIndex(field, cond) ?? new Set(await allDocIds());
98
+ }
99
+ return new Set(await allDocIds());
100
+ }
101
+
102
+ // src/core/collection.ts
103
+ import { v4 as uuid } from "uuid";
104
+
105
+ // src/utils/encryption.ts
106
+ import crypto2 from "crypto";
107
+
108
+ // src/utils/secureKey.ts
109
+ import crypto from "crypto";
110
+ import os from "os";
111
+ function getMasterKey() {
112
+ const fingerprint = [
113
+ os.hostname(),
114
+ os.platform(),
115
+ os.arch(),
116
+ os.cpus()?.[0]?.model ?? "unknown",
117
+ os.cpus()?.length ?? 0,
118
+ os.totalmem()
119
+ ].join("|");
120
+ return crypto.createHash("sha256").update(fingerprint).digest();
121
+ }
122
+
123
+ // src/utils/encryption.ts
124
+ var algorithm = "aes-256-gcm";
125
+ var ACTIVE_KEY = getMasterKey();
126
+ function setEncryptionKey(key) {
127
+ if (!key) return;
128
+ if (typeof key === "string") {
129
+ ACTIVE_KEY = crypto2.createHash("sha256").update(key).digest();
130
+ return;
131
+ }
132
+ if (Buffer.isBuffer(key)) {
133
+ if (key.length !== 32) {
134
+ throw new Error("Encryption key must be 32 bytes");
135
+ }
136
+ ACTIVE_KEY = key;
137
+ return;
138
+ }
139
+ throw new Error("Invalid encryption key format");
140
+ }
141
+ function encryptData(obj) {
142
+ const iv = crypto2.randomBytes(16);
143
+ const json = JSON.stringify(obj);
144
+ if (json.length > 5e6) {
145
+ throw new Error("Document too large (>5MB)");
146
+ }
147
+ const data = Buffer.from(json, "utf8");
148
+ const cipher = crypto2.createCipheriv(algorithm, ACTIVE_KEY, iv);
149
+ const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
150
+ const tag = cipher.getAuthTag();
151
+ return Buffer.concat([iv, tag, encrypted]).toString("base64");
152
+ }
153
+ function decryptData(enc) {
154
+ const buf = Buffer.from(enc, "base64");
155
+ const iv = buf.subarray(0, 16);
156
+ const tag = buf.subarray(16, 32);
157
+ const encrypted = buf.subarray(32);
158
+ const decipher = crypto2.createDecipheriv(algorithm, ACTIVE_KEY, iv);
159
+ decipher.setAuthTag(tag);
160
+ const decrypted = Buffer.concat([
161
+ decipher.update(encrypted),
162
+ decipher.final()
163
+ ]);
164
+ return JSON.parse(decrypted.toString("utf8"));
165
+ }
166
+
167
+ // src/utils/schema.ts
168
+ function validateSchema(schema, data) {
169
+ const result = schema.safeParse(data);
170
+ if (!result.success) {
171
+ throw new Error(
172
+ "Schema validation failed:\n" + JSON.stringify(result.error.format(), null, 2)
173
+ );
174
+ }
175
+ return result.data;
176
+ }
177
+
178
+ // src/core/compaction.ts
179
+ import fs2 from "fs";
180
+ import path2 from "path";
181
+ import { ClassicLevel as ClassicLevel2 } from "classic-level";
182
+
183
+ // src/core/index.ts
184
+ import path from "path";
185
+ import fs from "fs";
186
+ import { ClassicLevel } from "classic-level";
187
+ var Index = class {
188
+ field;
189
+ unique;
190
+ dir;
191
+ db;
192
+ constructor(baseDir, field, options = {}) {
193
+ this.field = field;
194
+ this.unique = !!options.unique;
195
+ this.dir = path.join(baseDir, "__indexes", field + ".idx");
196
+ fs.mkdirSync(this.dir, { recursive: true });
197
+ this.db = new ClassicLevel(this.dir, { valueEncoding: "utf8" });
198
+ }
199
+ /* ------------------------- INTERNAL ------------------------- */
200
+ normalizeKey(value) {
201
+ if (value === null || value === void 0) return "__null__";
202
+ if (typeof value === "object") {
203
+ return JSON.stringify(value);
204
+ }
205
+ return String(value);
206
+ }
207
+ async getRaw(key) {
208
+ try {
209
+ const v = await this.db.get(key);
210
+ if (v === void 0) return null;
211
+ return JSON.parse(v);
212
+ } catch {
213
+ return null;
214
+ }
215
+ }
216
+ async setRaw(key, value) {
217
+ await this.db.put(key, JSON.stringify(value));
218
+ }
219
+ async delRaw(key) {
220
+ try {
221
+ await this.db.del(key);
222
+ } catch {
223
+ }
224
+ }
225
+ /* --------------------------- API --------------------------- */
226
+ async insert(doc) {
227
+ const val = doc[this.field];
228
+ if (val === void 0) return;
229
+ const key = this.normalizeKey(val);
230
+ if (this.unique) {
231
+ const existing = await this.getRaw(key);
232
+ if (existing) {
233
+ throw new Error(
234
+ `Unique index violation on "${this.field}" = ${val}`
235
+ );
236
+ }
237
+ await this.setRaw(key, doc._id);
238
+ return;
239
+ }
240
+ const arr = await this.getRaw(key);
241
+ if (!arr) {
242
+ await this.setRaw(key, [doc._id]);
243
+ } else {
244
+ if (!arr.includes(doc._id)) {
245
+ arr.push(doc._id);
246
+ await this.setRaw(key, arr);
247
+ }
248
+ }
249
+ }
250
+ async delete(doc) {
251
+ const val = doc[this.field];
252
+ if (val === void 0) return;
253
+ const key = this.normalizeKey(val);
254
+ if (this.unique) {
255
+ await this.delRaw(key);
256
+ return;
257
+ }
258
+ const arr = await this.getRaw(key);
259
+ if (!arr) return;
260
+ const next = arr.filter((id) => id !== doc._id);
261
+ if (next.length === 0) {
262
+ await this.delRaw(key);
263
+ } else {
264
+ await this.setRaw(key, next);
265
+ }
266
+ }
267
+ async update(oldDoc, newDoc) {
268
+ const oldVal = oldDoc?.[this.field];
269
+ const newVal = newDoc?.[this.field];
270
+ if (oldVal === newVal) return;
271
+ if (oldDoc) await this.delete(oldDoc);
272
+ if (newDoc) await this.insert(newDoc);
273
+ }
274
+ async find(value) {
275
+ const key = this.normalizeKey(value);
276
+ const raw = await this.getRaw(key);
277
+ if (!raw) return [];
278
+ if (this.unique) return [raw];
279
+ return raw;
280
+ }
281
+ async close() {
282
+ try {
283
+ await this.db.close();
284
+ } catch {
285
+ }
286
+ }
287
+ };
288
+
289
+ // src/core/compaction.ts
290
+ var TMP_SUFFIX = "__compact_tmp";
291
+ var OLD_SUFFIX = "__compact_old";
292
+ var INDEX_DIR = "__indexes";
293
+ async function compactCollectionEngine(col) {
294
+ const baseDir = col.dir;
295
+ const tmpDir = baseDir + TMP_SUFFIX;
296
+ const oldDir = baseDir + OLD_SUFFIX;
297
+ await crashRecovery(baseDir);
298
+ safeRemove(tmpDir);
299
+ safeRemove(oldDir);
300
+ await snapshotRebuild(col, tmpDir);
301
+ await atomicSwap(baseDir, tmpDir, oldDir);
302
+ safeRemove(oldDir);
303
+ await reopenCollectionDB(col);
304
+ await rebuildIndexes(col);
305
+ }
306
+ async function snapshotRebuild(col, tmpDir) {
307
+ fs2.mkdirSync(tmpDir, { recursive: true });
308
+ const tmpDB = new ClassicLevel2(tmpDir, {
309
+ valueEncoding: "utf8"
310
+ });
311
+ for await (const [key, val] of col.db.iterator()) {
312
+ if (val !== void 0) {
313
+ await tmpDB.put(key, val);
314
+ }
315
+ }
316
+ await tmpDB.close();
317
+ await col.db.close();
318
+ }
319
+ async function atomicSwap(base, tmp, old) {
320
+ fs2.renameSync(base, old);
321
+ try {
322
+ fs2.renameSync(tmp, base);
323
+ } catch (err) {
324
+ if (fs2.existsSync(old)) {
325
+ fs2.renameSync(old, base);
326
+ }
327
+ throw err;
328
+ }
329
+ }
330
+ async function crashRecovery(baseDir) {
331
+ const tmp = baseDir + TMP_SUFFIX;
332
+ const old = baseDir + OLD_SUFFIX;
333
+ const baseExists = fs2.existsSync(baseDir);
334
+ const tmpExists = fs2.existsSync(tmp);
335
+ const oldExists = fs2.existsSync(old);
336
+ if (tmpExists && oldExists) {
337
+ safeRemove(baseDir);
338
+ fs2.renameSync(tmp, baseDir);
339
+ safeRemove(old);
340
+ return;
341
+ }
342
+ if (!baseExists && oldExists) {
343
+ fs2.renameSync(old, baseDir);
344
+ return;
345
+ }
346
+ if (tmpExists && !oldExists) {
347
+ safeRemove(tmp);
348
+ }
349
+ }
350
+ async function reopenCollectionDB(col) {
351
+ col.db = new ClassicLevel2(col.dir, {
352
+ valueEncoding: "utf8"
353
+ });
354
+ }
355
+ async function rebuildIndexes(col) {
356
+ const indexRoot = path2.join(col.dir, INDEX_DIR);
357
+ const oldIndexes = new Map(col["indexes"]);
358
+ for (const idx of oldIndexes.values()) {
359
+ try {
360
+ await idx.close();
361
+ } catch {
362
+ }
363
+ }
364
+ safeRemove(indexRoot);
365
+ fs2.mkdirSync(indexRoot, { recursive: true });
366
+ const rebuiltIndexes = /* @__PURE__ */ new Map();
367
+ for (const idx of oldIndexes.values()) {
368
+ const rebuilt = new Index(col.dir, idx.field, {
369
+ unique: idx.unique
370
+ });
371
+ for await (const [, enc] of col.db.iterator()) {
372
+ if (!enc) continue;
373
+ try {
374
+ const doc = decryptData(enc);
375
+ await rebuilt.insert(doc);
376
+ } catch {
377
+ }
378
+ }
379
+ rebuiltIndexes.set(idx.field, rebuilt);
380
+ }
381
+ col["indexes"] = rebuiltIndexes;
382
+ }
383
+ function safeRemove(p) {
384
+ if (fs2.existsSync(p)) {
385
+ fs2.rmSync(p, { recursive: true, force: true });
386
+ }
387
+ }
388
+
389
+ // src/core/collection.ts
390
+ var Collection = class {
391
+ dir;
392
+ db;
393
+ queue = Promise.resolve();
394
+ schema;
395
+ schemaVersion = 1;
396
+ migrations = [];
397
+ indexes = /* @__PURE__ */ new Map();
398
+ readonlyMode;
399
+ constructor(dir, schema, schemaVersion = 1, options) {
400
+ this.dir = dir;
401
+ this.schema = schema;
402
+ this.schemaVersion = schemaVersion;
403
+ this.readonlyMode = options?.readonly ?? false;
404
+ this.db = new ClassicLevel3(dir, {
405
+ valueEncoding: "utf8",
406
+ readOnly: this.readonlyMode
407
+ });
408
+ }
409
+ /* ===================== INTERNAL ===================== */
410
+ assertWritable() {
411
+ if (this.readonlyMode) {
412
+ throw new Error("Collection is in readonly replica mode");
413
+ }
414
+ }
415
+ /* ===================== SCHEMA ===================== */
416
+ setSchema(schema, version) {
417
+ this.schema = schema;
418
+ this.schemaVersion = version;
419
+ }
420
+ addMigration(migration) {
421
+ this.migrations.push(migration);
422
+ this.migrations.sort((a, b) => a.from - b.from);
423
+ }
424
+ validate(doc) {
425
+ return this.schema ? validateSchema(this.schema, doc) : doc;
426
+ }
427
+ migrateIfNeeded(doc) {
428
+ let currentVersion = doc.__v ?? 1;
429
+ if (currentVersion === this.schemaVersion) {
430
+ return doc;
431
+ }
432
+ let working = doc;
433
+ for (const migration of this.migrations) {
434
+ if (migration.from === currentVersion) {
435
+ working = migration.migrate(working);
436
+ currentVersion = migration.to;
437
+ }
438
+ }
439
+ working.__v = this.schemaVersion;
440
+ return this.validate(working);
441
+ }
442
+ /* ===================== QUEUE ===================== */
443
+ _enqueue(task) {
444
+ this.queue = this.queue.then(task).catch(console.error);
445
+ return this.queue;
446
+ }
447
+ async close() {
448
+ for (const idx of this.indexes.values()) {
449
+ try {
450
+ await idx.close();
451
+ } catch {
452
+ }
453
+ }
454
+ try {
455
+ await this.db.close();
456
+ } catch {
457
+ }
458
+ }
459
+ /* ===================== INDEX MANAGEMENT ===================== */
460
+ registerIndex(index) {
461
+ this.indexes.set(index.field, index);
462
+ }
463
+ getIndex(field) {
464
+ return this.indexes.get(field);
465
+ }
466
+ async _updateIndexes(oldDoc, newDoc) {
467
+ if (this.readonlyMode) return;
468
+ for (const index of this.indexes.values()) {
469
+ await index.update(oldDoc, newDoc);
470
+ }
471
+ }
472
+ /* ===================== COMPACTION ===================== */
473
+ async compact() {
474
+ this.assertWritable();
475
+ return this._enqueue(async () => {
476
+ try {
477
+ await this.db.close();
478
+ } catch {
479
+ }
480
+ await compactCollectionEngine(this);
481
+ this.db = new ClassicLevel3(this.dir, { valueEncoding: "utf8" });
482
+ await rebuildIndexes(this);
483
+ });
484
+ }
485
+ /* ===================== INTERNAL EXEC ===================== */
486
+ async _exec(op, args) {
487
+ switch (op) {
488
+ case "insertOne":
489
+ return this._insertOne(args[0]);
490
+ case "insertMany":
491
+ return this._insertMany(args[0]);
492
+ case "find":
493
+ return this._find(args[0]);
494
+ case "findOne":
495
+ return this._findOne(args[0]);
496
+ case "updateOne":
497
+ return this._updateOne(args[0], args[1], args[2]);
498
+ case "updateMany":
499
+ return this._updateMany(args[0], args[1]);
500
+ case "deleteOne":
501
+ return this._deleteOne(args[0]);
502
+ case "deleteMany":
503
+ return this._deleteMany(args[0]);
504
+ case "countDocuments":
505
+ return this._countDocuments(args[0]);
506
+ default:
507
+ throw new Error(`Unknown operation: ${op}`);
508
+ }
509
+ }
510
+ /* ===================== STORAGE ===================== */
511
+ async _insertOne(doc) {
512
+ this.assertWritable();
513
+ const _id = doc._id ?? uuid();
514
+ const final = this.validate({
515
+ _id,
516
+ ...doc,
517
+ __v: this.schemaVersion
518
+ });
519
+ await this.db.put(String(_id), encryptData(final));
520
+ await this._updateIndexes(null, final);
521
+ return final;
522
+ }
523
+ async _insertMany(docs) {
524
+ this.assertWritable();
525
+ const batch = [];
526
+ const out = [];
527
+ for (const d of docs) {
528
+ const _id = d._id ?? uuid();
529
+ const final = this.validate({
530
+ _id,
531
+ ...d,
532
+ __v: this.schemaVersion
533
+ });
534
+ batch.push({
535
+ type: "put",
536
+ key: String(_id),
537
+ value: encryptData(final)
538
+ });
539
+ out.push(final);
540
+ }
541
+ await this.db.batch(batch);
542
+ for (const doc of out) {
543
+ await this._updateIndexes(null, doc);
544
+ }
545
+ return out;
546
+ }
547
+ /* ===================== QUERY ===================== */
548
+ async _getCandidateIds(query) {
549
+ const indexedFields = new Set(this.indexes.keys());
550
+ return runIndexedQuery(
551
+ query,
552
+ {
553
+ indexes: indexedFields,
554
+ findByIndex: async (field, value) => {
555
+ const idx = this.indexes.get(field);
556
+ if (!idx) return null;
557
+ return new Set(await idx.find(value));
558
+ }
559
+ },
560
+ async () => {
561
+ const ids = [];
562
+ for await (const [key] of this.db.iterator()) {
563
+ ids.push(key);
564
+ }
565
+ return ids;
566
+ }
567
+ );
568
+ }
569
+ async _readAndMigrate(id) {
570
+ const enc = await this.db.get(id).catch(() => null);
571
+ if (!enc) return null;
572
+ const raw = decryptData(enc);
573
+ const migrated = this.migrateIfNeeded(raw);
574
+ if (!this.readonlyMode && raw.__v !== this.schemaVersion) {
575
+ await this.db.put(id, encryptData(migrated));
576
+ await this._updateIndexes(raw, migrated);
577
+ }
578
+ return migrated;
579
+ }
580
+ async _find(query) {
581
+ const ids = await this._getCandidateIds(query);
582
+ const out = [];
583
+ for (const id of ids) {
584
+ try {
585
+ const doc = await this._readAndMigrate(id);
586
+ if (doc && matchDocument(doc, query)) {
587
+ out.push(doc);
588
+ }
589
+ } catch {
590
+ }
591
+ }
592
+ return out;
593
+ }
594
+ async _findOne(query) {
595
+ if (query?._id) {
596
+ return this._readAndMigrate(String(query._id));
597
+ }
598
+ const ids = await this._getCandidateIds(query);
599
+ for (const id of ids) {
600
+ try {
601
+ const doc = await this._readAndMigrate(id);
602
+ if (doc && matchDocument(doc, query)) {
603
+ return doc;
604
+ }
605
+ } catch {
606
+ }
607
+ }
608
+ return null;
609
+ }
610
+ async _countDocuments(filter) {
611
+ const ids = await this._getCandidateIds(filter);
612
+ let count = 0;
613
+ for (const id of ids) {
614
+ try {
615
+ const doc = await this._readAndMigrate(id);
616
+ if (doc && matchDocument(doc, filter)) {
617
+ count++;
618
+ }
619
+ } catch {
620
+ }
621
+ }
622
+ return count;
623
+ }
624
+ /* ===================== UPDATE ===================== */
625
+ async _updateOne(filter, update, options) {
626
+ this.assertWritable();
627
+ const ids = await this._getCandidateIds(filter);
628
+ for (const id of ids) {
629
+ const existing = await this._readAndMigrate(id);
630
+ if (!existing) continue;
631
+ if (matchDocument(existing, filter)) {
632
+ const updated = this.validate({
633
+ ...applyUpdate(existing, update),
634
+ _id: existing._id,
635
+ __v: this.schemaVersion
636
+ });
637
+ await this.db.put(id, encryptData(updated));
638
+ await this._updateIndexes(existing, updated);
639
+ return updated;
640
+ }
641
+ }
642
+ if (options?.upsert) {
643
+ return this._insertOne(applyUpdate({}, update));
644
+ }
645
+ return null;
646
+ }
647
+ async _updateMany(filter, update) {
648
+ this.assertWritable();
649
+ const ids = await this._getCandidateIds(filter);
650
+ const out = [];
651
+ for (const id of ids) {
652
+ const existing = await this._readAndMigrate(id);
653
+ if (!existing) continue;
654
+ if (matchDocument(existing, filter)) {
655
+ const updated = this.validate({
656
+ ...applyUpdate(existing, update),
657
+ _id: existing._id,
658
+ __v: this.schemaVersion
659
+ });
660
+ await this.db.put(id, encryptData(updated));
661
+ await this._updateIndexes(existing, updated);
662
+ out.push(updated);
663
+ }
664
+ }
665
+ return out;
666
+ }
667
+ /* ===================== DELETE ===================== */
668
+ async _deleteOne(filter) {
669
+ this.assertWritable();
670
+ const ids = await this._getCandidateIds(filter);
671
+ for (const id of ids) {
672
+ const existing = await this._readAndMigrate(id);
673
+ if (!existing) continue;
674
+ if (matchDocument(existing, filter)) {
675
+ await this.db.del(id);
676
+ await this._updateIndexes(existing, null);
677
+ return true;
678
+ }
679
+ }
680
+ return false;
681
+ }
682
+ async _deleteMany(filter) {
683
+ this.assertWritable();
684
+ const ids = await this._getCandidateIds(filter);
685
+ let count = 0;
686
+ for (const id of ids) {
687
+ const existing = await this._readAndMigrate(id);
688
+ if (!existing) continue;
689
+ if (matchDocument(existing, filter)) {
690
+ await this.db.del(id);
691
+ await this._updateIndexes(existing, null);
692
+ count++;
693
+ }
694
+ }
695
+ return count;
696
+ }
697
+ /* ===================== PUBLIC API ===================== */
698
+ insertOne(doc) {
699
+ return this._enqueue(() => this._exec("insertOne", [doc]));
700
+ }
701
+ insertMany(docs) {
702
+ return this._enqueue(() => this._exec("insertMany", [docs]));
703
+ }
704
+ find(query = {}) {
705
+ return this._enqueue(() => this._exec("find", [query]));
706
+ }
707
+ findOne(query = {}) {
708
+ return this._enqueue(() => this._exec("findOne", [query]));
709
+ }
710
+ updateOne(filter, update, options) {
711
+ return this._enqueue(
712
+ () => this._exec("updateOne", [filter, update, options])
713
+ );
714
+ }
715
+ updateMany(filter, update) {
716
+ return this._enqueue(
717
+ () => this._exec("updateMany", [filter, update])
718
+ );
719
+ }
720
+ deleteOne(filter) {
721
+ return this._enqueue(
722
+ () => this._exec("deleteOne", [filter])
723
+ );
724
+ }
725
+ deleteMany(filter) {
726
+ return this._enqueue(
727
+ () => this._exec("deleteMany", [filter])
728
+ );
729
+ }
730
+ countDocuments(filter = {}) {
731
+ return this._enqueue(
732
+ () => this._exec("countDocuments", [filter])
733
+ );
734
+ }
735
+ };
736
+
737
+ // src/core/migration.ts
738
+ import fs3 from "fs";
739
+ import path3 from "path";
740
+ import crypto3 from "crypto";
741
+ var LOCK_FILE = "__migration.lock";
742
+ var HISTORY_FILE = "__migration_history.json";
743
+ var MigrationEngine = class {
744
+ constructor(db) {
745
+ this.db = db;
746
+ }
747
+ migrations = /* @__PURE__ */ new Map();
748
+ /* ------------------------------------------------------------ */
749
+ /* Public API */
750
+ /* ------------------------------------------------------------ */
751
+ register(from, to, fn) {
752
+ const key = `${from}\u2192${to}`;
753
+ if (this.migrations.has(key)) {
754
+ throw new Error(`Duplicate migration: ${key}`);
755
+ }
756
+ this.migrations.set(key, fn);
757
+ }
758
+ async migrate(from, to, fn) {
759
+ this.register(from, to, fn);
760
+ await this.execute();
761
+ }
762
+ async upgradeToLatest() {
763
+ await this.execute();
764
+ }
765
+ /* ------------------------------------------------------------ */
766
+ /* Core Execution Logic */
767
+ /* ------------------------------------------------------------ */
768
+ async execute() {
769
+ let current = this.db.getSchemaVersion();
770
+ while (true) {
771
+ const next = this.findNext(current);
772
+ if (!next) break;
773
+ const fn = this.migrations.get(`${current}\u2192${next}`);
774
+ await this.runMigration(current, next, fn);
775
+ current = next;
776
+ }
777
+ }
778
+ findNext(current) {
779
+ for (const key of this.migrations.keys()) {
780
+ const [from, to] = key.split("\u2192");
781
+ if (from === current) return to;
782
+ }
783
+ return null;
784
+ }
785
+ /* ------------------------------------------------------------ */
786
+ /* Atomic Migration Execution */
787
+ /* ------------------------------------------------------------ */
788
+ async runMigration(from, to, fn) {
789
+ const current = this.db.getSchemaVersion();
790
+ if (current !== from) {
791
+ throw new Error(
792
+ `Schema mismatch: DB=${current}, expected=${from}`
793
+ );
794
+ }
795
+ const lockPath = path3.join(this.db.basePath, LOCK_FILE);
796
+ if (fs3.existsSync(lockPath)) {
797
+ throw new Error(
798
+ "Previous migration interrupted. Resolve manually before continuing."
799
+ );
800
+ }
801
+ this.acquireLock(lockPath);
802
+ try {
803
+ await this.db.transaction(async () => {
804
+ await fn(this.db);
805
+ this.writeHistory(from, to, fn);
806
+ this.db.setSchemaVersion(to);
807
+ });
808
+ } finally {
809
+ this.releaseLock(lockPath);
810
+ }
811
+ }
812
+ /* ------------------------------------------------------------ */
813
+ /* Locking */
814
+ /* ------------------------------------------------------------ */
815
+ acquireLock(file) {
816
+ const token = crypto3.randomBytes(16).toString("hex");
817
+ fs3.writeFileSync(
818
+ file,
819
+ JSON.stringify({
820
+ pid: process.pid,
821
+ token,
822
+ time: Date.now()
823
+ })
824
+ );
825
+ }
826
+ releaseLock(file) {
827
+ if (fs3.existsSync(file)) fs3.unlinkSync(file);
828
+ }
829
+ /* ------------------------------------------------------------ */
830
+ /* Migration History */
831
+ /* ------------------------------------------------------------ */
832
+ historyPath() {
833
+ return path3.join(this.db.basePath, HISTORY_FILE);
834
+ }
835
+ readHistory() {
836
+ if (!fs3.existsSync(this.historyPath())) return [];
837
+ return JSON.parse(fs3.readFileSync(this.historyPath(), "utf8"));
838
+ }
839
+ writeHistory(from, to, fn) {
840
+ const history = this.readHistory();
841
+ history.push({
842
+ from,
843
+ to,
844
+ checksum: this.hash(fn.toString()),
845
+ appliedAt: Date.now()
846
+ });
847
+ fs3.writeFileSync(this.historyPath(), JSON.stringify(history, null, 2));
848
+ }
849
+ hash(data) {
850
+ return crypto3.createHash("sha256").update(data).digest("hex");
851
+ }
852
+ /* ------------------------------------------------------------ */
853
+ /* Diagnostics */
854
+ /* ------------------------------------------------------------ */
855
+ getHistory() {
856
+ return this.readHistory();
857
+ }
858
+ };
859
+
860
+ // src/core/wal.ts
861
+ import fs4 from "fs";
862
+ import path4 from "path";
863
+ var MAX_WAL_SIZE = 16 * 1024 * 1024;
864
+ var WAL_DIR = "__wal";
865
+ var CRC32_TABLE = (() => {
866
+ const table = new Uint32Array(256);
867
+ for (let i = 0; i < 256; i++) {
868
+ let c = i;
869
+ for (let k = 0; k < 8; k++) {
870
+ c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
871
+ }
872
+ table[i] = c >>> 0;
873
+ }
874
+ return table;
875
+ })();
876
+ function crc32(input) {
877
+ let crc = 4294967295;
878
+ for (let i = 0; i < input.length; i++) {
879
+ crc = CRC32_TABLE[(crc ^ input.charCodeAt(i)) & 255] ^ crc >>> 8;
880
+ }
881
+ return (crc ^ 4294967295) >>> 0;
882
+ }
883
+ var WALManager = class {
884
+ walDir;
885
+ currentGen = 1;
886
+ lsn = 0;
887
+ fd = null;
888
+ readonlyMode;
889
+ constructor(baseDir, options) {
890
+ this.walDir = path4.join(baseDir, WAL_DIR);
891
+ this.readonlyMode = options?.readonly ?? false;
892
+ if (!this.readonlyMode) {
893
+ fs4.mkdirSync(this.walDir, { recursive: true });
894
+ }
895
+ if (fs4.existsSync(this.walDir)) {
896
+ this.currentGen = this.detectLastGeneration();
897
+ this.recoverLSNFromExistingLogs();
898
+ }
899
+ }
900
+ /* -------------------------
901
+ INTERNAL HELPERS
902
+ ------------------------- */
903
+ walPath(gen = this.currentGen) {
904
+ return path4.join(
905
+ this.walDir,
906
+ `wal-${String(gen).padStart(6, "0")}.log`
907
+ );
908
+ }
909
+ detectLastGeneration() {
910
+ if (!fs4.existsSync(this.walDir)) return 1;
911
+ const files = fs4.readdirSync(this.walDir);
912
+ let max = 0;
913
+ for (const f of files) {
914
+ const m = f.match(/^wal-(\d+)\.log$/);
915
+ if (m) {
916
+ const gen = Number(m[1]);
917
+ if (!Number.isNaN(gen)) {
918
+ max = Math.max(max, gen);
919
+ }
920
+ }
921
+ }
922
+ return max || 1;
923
+ }
924
+ recoverLSNFromExistingLogs() {
925
+ const files = this.getSortedWalFiles();
926
+ for (const file of files) {
927
+ const filePath = path4.join(this.walDir, file);
928
+ const lines = fs4.readFileSync(filePath, "utf8").split("\n");
929
+ for (const line of lines) {
930
+ if (!line.trim()) continue;
931
+ try {
932
+ const parsed = JSON.parse(line);
933
+ const { crc, ...record } = parsed;
934
+ if (crc32(JSON.stringify(record)) !== crc) break;
935
+ this.lsn = Math.max(this.lsn, record.lsn);
936
+ } catch {
937
+ break;
938
+ }
939
+ }
940
+ }
941
+ }
942
+ getSortedWalFiles() {
943
+ if (!fs4.existsSync(this.walDir)) return [];
944
+ return fs4.readdirSync(this.walDir).filter((f) => /^wal-\d+\.log$/.test(f)).sort((a, b) => {
945
+ const ga = Number(a.match(/^wal-(\d+)\.log$/)[1]);
946
+ const gb = Number(b.match(/^wal-(\d+)\.log$/)[1]);
947
+ return ga - gb;
948
+ });
949
+ }
950
+ async open() {
951
+ if (this.readonlyMode) {
952
+ throw new Error("WAL is in readonly replica mode");
953
+ }
954
+ if (!this.fd) {
955
+ this.fd = await fs4.promises.open(this.walPath(), "a");
956
+ }
957
+ }
958
+ async rotate() {
959
+ if (this.readonlyMode) return;
960
+ if (this.fd) {
961
+ await this.fd.sync();
962
+ await this.fd.close();
963
+ this.fd = null;
964
+ }
965
+ this.currentGen++;
966
+ }
967
+ /* -------------------------
968
+ APPEND (Primary only)
969
+ ------------------------- */
970
+ async append(record) {
971
+ if (this.readonlyMode) {
972
+ throw new Error("Cannot append WAL in readonly replica mode");
973
+ }
974
+ await this.open();
975
+ const full = {
976
+ ...record,
977
+ lsn: ++this.lsn
978
+ };
979
+ const body = JSON.stringify(full);
980
+ const stored = {
981
+ ...full,
982
+ crc: crc32(body)
983
+ };
984
+ const line = JSON.stringify(stored) + "\n";
985
+ await this.fd.write(line);
986
+ await this.fd.sync();
987
+ const stat = await this.fd.stat();
988
+ if (stat.size >= MAX_WAL_SIZE) {
989
+ await this.rotate();
990
+ }
991
+ return full.lsn;
992
+ }
993
+ /* -------------------------
994
+ REPLAY (Replica allowed)
995
+ ------------------------- */
996
+ async replay(fromLSN, apply) {
997
+ if (!fs4.existsSync(this.walDir)) return;
998
+ const files = this.getSortedWalFiles();
999
+ for (const file of files) {
1000
+ const filePath = path4.join(this.walDir, file);
1001
+ const fd = fs4.openSync(
1002
+ filePath,
1003
+ this.readonlyMode ? "r" : "r+"
1004
+ );
1005
+ const content = fs4.readFileSync(filePath, "utf8");
1006
+ const lines = content.split("\n");
1007
+ let validOffset = 0;
1008
+ for (let i = 0; i < lines.length; i++) {
1009
+ const line = lines[i];
1010
+ if (!line.trim()) {
1011
+ validOffset += line.length + 1;
1012
+ continue;
1013
+ }
1014
+ let parsed;
1015
+ try {
1016
+ parsed = JSON.parse(line);
1017
+ } catch {
1018
+ break;
1019
+ }
1020
+ const { crc, ...record } = parsed;
1021
+ const expected = crc32(JSON.stringify(record));
1022
+ if (expected !== crc) {
1023
+ break;
1024
+ }
1025
+ validOffset += line.length + 1;
1026
+ if (record.lsn <= fromLSN) continue;
1027
+ this.lsn = Math.max(this.lsn, record.lsn);
1028
+ await apply(record);
1029
+ }
1030
+ if (!this.readonlyMode) {
1031
+ const stat = fs4.fstatSync(fd);
1032
+ if (validOffset < stat.size) {
1033
+ fs4.ftruncateSync(fd, validOffset);
1034
+ }
1035
+ }
1036
+ fs4.closeSync(fd);
1037
+ }
1038
+ }
1039
+ /* -------------------------
1040
+ CLEANUP (Primary only)
1041
+ ------------------------- */
1042
+ async cleanup(beforeGen) {
1043
+ if (this.readonlyMode) return;
1044
+ if (!fs4.existsSync(this.walDir)) return;
1045
+ const files = fs4.readdirSync(this.walDir);
1046
+ for (const f of files) {
1047
+ const m = f.match(/^wal-(\d+)\.log$/);
1048
+ if (!m) continue;
1049
+ const gen = Number(m[1]);
1050
+ if (gen < beforeGen) {
1051
+ fs4.unlinkSync(path4.join(this.walDir, f));
1052
+ }
1053
+ }
1054
+ }
1055
+ /* -------------------------
1056
+ GETTERS
1057
+ ------------------------- */
1058
+ getCurrentLSN() {
1059
+ return this.lsn;
1060
+ }
1061
+ getCurrentGen() {
1062
+ return this.currentGen;
1063
+ }
1064
+ isReadonly() {
1065
+ return this.readonlyMode;
1066
+ }
1067
+ };
1068
+
1069
+ // src/core/checkpoint.ts
1070
+ import fs5 from "fs";
1071
+ import path5 from "path";
1072
+ var CHECKPOINT_A = "__checkpoint_A.json";
1073
+ var CHECKPOINT_B = "__checkpoint_B.json";
1074
+ var FORMAT_VERSION = 1;
1075
+ var CRC32_TABLE2 = (() => {
1076
+ const table = new Uint32Array(256);
1077
+ for (let i = 0; i < 256; i++) {
1078
+ let c = i;
1079
+ for (let k = 0; k < 8; k++) {
1080
+ c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
1081
+ }
1082
+ table[i] = c >>> 0;
1083
+ }
1084
+ return table;
1085
+ })();
1086
+ function crc322(input) {
1087
+ let crc = 4294967295;
1088
+ for (let i = 0; i < input.length; i++) {
1089
+ crc = CRC32_TABLE2[(crc ^ input.charCodeAt(i)) & 255] ^ crc >>> 8;
1090
+ }
1091
+ return (crc ^ 4294967295) >>> 0;
1092
+ }
1093
+ var CheckpointManager = class {
1094
+ baseDir;
1095
+ data;
1096
+ constructor(baseDir) {
1097
+ this.baseDir = baseDir;
1098
+ this.data = {
1099
+ lsn: 0,
1100
+ walGen: 1,
1101
+ time: 0,
1102
+ version: FORMAT_VERSION
1103
+ };
1104
+ this.load();
1105
+ }
1106
+ /* -------------------------
1107
+ LOAD (CRC + FALLBACK)
1108
+ ------------------------- */
1109
+ load() {
1110
+ const a = this.readCheckpoint(CHECKPOINT_A);
1111
+ const b = this.readCheckpoint(CHECKPOINT_B);
1112
+ if (a && b) {
1113
+ this.data = a.data.lsn >= b.data.lsn ? a.data : b.data;
1114
+ return;
1115
+ }
1116
+ if (a) {
1117
+ this.data = a.data;
1118
+ return;
1119
+ }
1120
+ if (b) {
1121
+ this.data = b.data;
1122
+ return;
1123
+ }
1124
+ console.warn("No valid checkpoint found, starting from zero");
1125
+ }
1126
+ readCheckpoint(file) {
1127
+ const filePath = path5.join(this.baseDir, file);
1128
+ if (!fs5.existsSync(filePath)) return null;
1129
+ try {
1130
+ const raw = fs5.readFileSync(filePath, "utf8");
1131
+ const parsed = JSON.parse(raw);
1132
+ if (!parsed?.data || typeof parsed.crc !== "number") {
1133
+ return null;
1134
+ }
1135
+ const expected = crc322(JSON.stringify(parsed.data));
1136
+ if (expected !== parsed.crc) {
1137
+ console.error(`Checkpoint CRC mismatch: ${file}`);
1138
+ return null;
1139
+ }
1140
+ return parsed;
1141
+ } catch {
1142
+ return null;
1143
+ }
1144
+ }
1145
+ /* -------------------------
1146
+ SAVE (DUAL WRITE)
1147
+ ------------------------- */
1148
+ save(lsn, walGen) {
1149
+ const data = {
1150
+ lsn,
1151
+ walGen,
1152
+ time: Date.now(),
1153
+ version: FORMAT_VERSION
1154
+ };
1155
+ const stored = {
1156
+ data,
1157
+ crc: crc322(JSON.stringify(data))
1158
+ };
1159
+ const target = lsn % 2 === 0 ? CHECKPOINT_A : CHECKPOINT_B;
1160
+ try {
1161
+ fs5.writeFileSync(
1162
+ path5.join(this.baseDir, target),
1163
+ JSON.stringify(stored, null, 2),
1164
+ "utf8"
1165
+ );
1166
+ this.data = data;
1167
+ } catch (err) {
1168
+ console.error("Failed to write checkpoint:", err);
1169
+ }
1170
+ }
1171
+ /* -------------------------
1172
+ GET CURRENT
1173
+ ------------------------- */
1174
+ get() {
1175
+ return this.data;
1176
+ }
1177
+ };
1178
+
1179
+ // src/core/database.ts
1180
+ var META_FILE = "__db_meta.json";
1181
+ var META_VERSION = 2;
1182
+ var DEFAULT_SCHEMA_VERSION = "v1";
1183
+ var DBTransactionContext = class {
1184
+ constructor(db, txId) {
1185
+ this.db = db;
1186
+ this.txId = txId;
1187
+ }
1188
+ ops = [];
1189
+ collection(name) {
1190
+ return new Proxy(
1191
+ {},
1192
+ {
1193
+ get: (_, prop) => {
1194
+ return (...args) => {
1195
+ this.ops.push({
1196
+ tx: this.txId,
1197
+ col: name,
1198
+ op: prop,
1199
+ args
1200
+ });
1201
+ };
1202
+ }
1203
+ }
1204
+ );
1205
+ }
1206
+ async commit() {
1207
+ if (this.db.isReadonly()) {
1208
+ throw new Error("Cannot commit transaction in readonly mode");
1209
+ }
1210
+ for (const op of this.ops) {
1211
+ await this.db.wal.append({
1212
+ tx: this.txId,
1213
+ type: "op",
1214
+ payload: op
1215
+ });
1216
+ }
1217
+ const commitLSN = await this.db.wal.append({
1218
+ tx: this.txId,
1219
+ type: "commit"
1220
+ });
1221
+ await this.db.applyTransaction(this.ops);
1222
+ const appliedLSN = await this.db.wal.append({
1223
+ tx: this.txId,
1224
+ type: "applied"
1225
+ });
1226
+ this.db.advanceCheckpoint(appliedLSN);
1227
+ await this.db.postCommitMaintenance();
1228
+ }
1229
+ };
1230
+ var LioranDB = class _LioranDB {
1231
+ basePath;
1232
+ dbName;
1233
+ manager;
1234
+ collections;
1235
+ metaPath;
1236
+ meta;
1237
+ migrator;
1238
+ static TX_SEQ = 0;
1239
+ wal;
1240
+ checkpoint;
1241
+ readonlyMode;
1242
+ constructor(basePath, dbName, manager) {
1243
+ this.basePath = basePath;
1244
+ this.dbName = dbName;
1245
+ this.manager = manager;
1246
+ this.collections = /* @__PURE__ */ new Map();
1247
+ this.readonlyMode = manager?.isReadonly?.() ?? false;
1248
+ this.metaPath = path6.join(basePath, META_FILE);
1249
+ fs6.mkdirSync(basePath, { recursive: true });
1250
+ this.loadMeta();
1251
+ if (!this.readonlyMode) {
1252
+ this.wal = new WALManager(basePath);
1253
+ this.checkpoint = new CheckpointManager(basePath);
1254
+ }
1255
+ this.migrator = new MigrationEngine(this);
1256
+ this.initialize().catch(console.error);
1257
+ }
1258
+ /* ------------------------- MODE ------------------------- */
1259
+ isReadonly() {
1260
+ return this.readonlyMode;
1261
+ }
1262
+ assertWritable() {
1263
+ if (this.readonlyMode) {
1264
+ throw new Error("Database is in readonly replica mode");
1265
+ }
1266
+ }
1267
+ /* ------------------------- INIT & RECOVERY ------------------------- */
1268
+ async initialize() {
1269
+ if (!this.readonlyMode) {
1270
+ await this.recoverFromWAL();
1271
+ }
1272
+ }
1273
+ async recoverFromWAL() {
1274
+ const checkpointData = this.checkpoint.get();
1275
+ const fromLSN = checkpointData.lsn;
1276
+ const committed = /* @__PURE__ */ new Set();
1277
+ const applied = /* @__PURE__ */ new Set();
1278
+ const ops = /* @__PURE__ */ new Map();
1279
+ await this.wal.replay(fromLSN, async (record) => {
1280
+ if (record.type === "commit") {
1281
+ committed.add(record.tx);
1282
+ } else if (record.type === "applied") {
1283
+ applied.add(record.tx);
1284
+ } else if (record.type === "op") {
1285
+ if (!ops.has(record.tx)) ops.set(record.tx, []);
1286
+ ops.get(record.tx).push(record.payload);
1287
+ }
1288
+ });
1289
+ let highestAppliedLSN = fromLSN;
1290
+ for (const tx of committed) {
1291
+ if (applied.has(tx)) continue;
1292
+ const txOps = ops.get(tx);
1293
+ if (txOps) {
1294
+ await this.applyTransaction(txOps);
1295
+ highestAppliedLSN = this.wal.getCurrentLSN();
1296
+ }
1297
+ }
1298
+ this.advanceCheckpoint(highestAppliedLSN);
1299
+ }
1300
+ /* ------------------------- CHECKPOINT ADVANCE ------------------------- */
1301
+ advanceCheckpoint(lsn) {
1302
+ if (this.readonlyMode) return;
1303
+ const current = this.checkpoint.get();
1304
+ if (lsn > current.lsn) {
1305
+ this.checkpoint.save(lsn, this.wal.getCurrentGen());
1306
+ this.wal.cleanup(this.wal.getCurrentGen() - 1).catch(() => {
1307
+ });
1308
+ }
1309
+ }
1310
+ /* ------------------------- META ------------------------- */
1311
+ loadMeta() {
1312
+ if (!fs6.existsSync(this.metaPath)) {
1313
+ this.meta = {
1314
+ version: META_VERSION,
1315
+ indexes: {},
1316
+ schemaVersion: DEFAULT_SCHEMA_VERSION
1317
+ };
1318
+ this.saveMeta();
1319
+ return;
1320
+ }
1321
+ this.meta = JSON.parse(fs6.readFileSync(this.metaPath, "utf8"));
1322
+ if (!this.meta.schemaVersion) {
1323
+ this.meta.schemaVersion = DEFAULT_SCHEMA_VERSION;
1324
+ this.saveMeta();
1325
+ }
1326
+ }
1327
+ saveMeta() {
1328
+ if (this.readonlyMode) return;
1329
+ fs6.writeFileSync(this.metaPath, JSON.stringify(this.meta, null, 2));
1330
+ }
1331
+ getSchemaVersion() {
1332
+ return this.meta.schemaVersion;
1333
+ }
1334
+ setSchemaVersion(v) {
1335
+ this.assertWritable();
1336
+ this.meta.schemaVersion = v;
1337
+ this.saveMeta();
1338
+ }
1339
+ /* ------------------------- DB MIGRATIONS ------------------------- */
1340
+ migrate(from, to, fn) {
1341
+ this.assertWritable();
1342
+ this.migrator.register(from, to, async (db) => {
1343
+ await fn(db);
1344
+ db.setSchemaVersion(to);
1345
+ });
1346
+ }
1347
+ async applyMigrations(targetVersion) {
1348
+ this.assertWritable();
1349
+ await this.migrator.upgradeToLatest();
1350
+ }
1351
+ /* ------------------------- TX APPLY ------------------------- */
1352
+ async applyTransaction(ops) {
1353
+ for (const { col, op, args } of ops) {
1354
+ const collection = this.collection(col);
1355
+ await collection._exec(op, args);
1356
+ }
1357
+ }
1358
+ /* ------------------------- COLLECTION ------------------------- */
1359
+ collection(name, schema, schemaVersion) {
1360
+ if (this.collections.has(name)) {
1361
+ const col2 = this.collections.get(name);
1362
+ if (schema && schemaVersion !== void 0) {
1363
+ col2.setSchema(schema, schemaVersion);
1364
+ }
1365
+ return col2;
1366
+ }
1367
+ const colPath = path6.join(this.basePath, name);
1368
+ fs6.mkdirSync(colPath, { recursive: true });
1369
+ const col = new Collection(
1370
+ colPath,
1371
+ schema,
1372
+ schemaVersion ?? 1,
1373
+ { readonly: this.readonlyMode }
1374
+ );
1375
+ const metas = this.meta.indexes[name] ?? [];
1376
+ for (const m of metas) {
1377
+ col.registerIndex(new Index(colPath, m.field, m.options));
1378
+ }
1379
+ this.collections.set(name, col);
1380
+ return col;
1381
+ }
1382
+ /* ------------------------- INDEX API ------------------------- */
1383
+ async createIndex(collection, field, options = {}) {
1384
+ this.assertWritable();
1385
+ const col = this.collection(collection);
1386
+ const existing = this.meta.indexes[collection]?.find((i) => i.field === field);
1387
+ if (existing) return;
1388
+ const index = new Index(col.dir, field, options);
1389
+ for await (const [key, enc] of col.db.iterator()) {
1390
+ if (!enc) continue;
1391
+ try {
1392
+ const doc = decryptData(enc);
1393
+ await index.insert(doc);
1394
+ } catch {
1395
+ }
1396
+ }
1397
+ col.registerIndex(index);
1398
+ if (!this.meta.indexes[collection]) {
1399
+ this.meta.indexes[collection] = [];
1400
+ }
1401
+ this.meta.indexes[collection].push({ field, options });
1402
+ this.saveMeta();
1403
+ }
1404
+ /* ------------------------- COMPACTION ------------------------- */
1405
+ async compactCollection(name) {
1406
+ this.assertWritable();
1407
+ const col = this.collection(name);
1408
+ await col.compact();
1409
+ }
1410
+ async compactAll() {
1411
+ this.assertWritable();
1412
+ for (const name of this.collections.keys()) {
1413
+ await this.compactCollection(name);
1414
+ }
1415
+ }
1416
+ /* ------------------------- TX API ------------------------- */
1417
+ async transaction(fn) {
1418
+ this.assertWritable();
1419
+ const txId = ++_LioranDB.TX_SEQ;
1420
+ const tx = new DBTransactionContext(this, txId);
1421
+ const result = await fn(tx);
1422
+ await tx.commit();
1423
+ return result;
1424
+ }
1425
+ /* ------------------------- POST COMMIT ------------------------- */
1426
+ async postCommitMaintenance() {
1427
+ }
1428
+ /* ------------------------- SHUTDOWN ------------------------- */
1429
+ async close() {
1430
+ for (const col of this.collections.values()) {
1431
+ try {
1432
+ await col.close();
1433
+ } catch {
1434
+ }
1435
+ }
1436
+ this.collections.clear();
1437
+ }
1438
+ };
1439
+
1440
+ // src/utils/rootpath.ts
1441
+ import os2 from "os";
1442
+ import path7 from "path";
1443
+ import fs7 from "fs";
1444
+ function getDefaultRootPath() {
1445
+ let dbPath = process.env.LIORANDB_PATH;
1446
+ if (!dbPath) {
1447
+ const homeDir = os2.homedir();
1448
+ dbPath = path7.join(homeDir, "LioranDB", "db");
1449
+ if (!fs7.existsSync(dbPath)) {
1450
+ fs7.mkdirSync(dbPath, { recursive: true });
1451
+ }
1452
+ process.env.LIORANDB_PATH = dbPath;
1453
+ }
1454
+ return dbPath;
1455
+ }
1456
+ function getBaseDBFolder() {
1457
+ return getDefaultRootPath();
1458
+ }
1459
+
1460
+ // src/LioranManager.ts
1461
+ import path8 from "path";
1462
+ import fs8 from "fs";
1463
+ import process2 from "process";
1464
+ var LioranManager = class {
1465
+ rootPath;
1466
+ openDBs;
1467
+ closed = false;
1468
+ mode;
1469
+ lockFd;
1470
+ constructor(options = {}) {
1471
+ const { rootPath, encryptionKey, ipc } = options;
1472
+ this.rootPath = rootPath || getDefaultRootPath();
1473
+ if (!fs8.existsSync(this.rootPath)) {
1474
+ fs8.mkdirSync(this.rootPath, { recursive: true });
1475
+ }
1476
+ if (encryptionKey) {
1477
+ setEncryptionKey(encryptionKey);
1478
+ }
1479
+ this.openDBs = /* @__PURE__ */ new Map();
1480
+ if (ipc === "readonly") {
1481
+ this.mode = "readonly" /* READONLY */;
1482
+ } else if (ipc === "client") {
1483
+ this.mode = "client" /* CLIENT */;
1484
+ } else if (ipc === "primary") {
1485
+ this.mode = "primary" /* PRIMARY */;
1486
+ this.tryAcquireLock();
1487
+ } else {
1488
+ this.mode = this.tryAcquireLock() ? "primary" /* PRIMARY */ : "client" /* CLIENT */;
1489
+ }
1490
+ if (this.mode === "primary" /* PRIMARY */) {
1491
+ this._registerShutdownHooks();
1492
+ }
1493
+ }
1494
+ /* ---------------- MODE HELPERS ---------------- */
1495
+ isPrimary() {
1496
+ return this.mode === "primary" /* PRIMARY */;
1497
+ }
1498
+ isClient() {
1499
+ return this.mode === "client" /* CLIENT */;
1500
+ }
1501
+ isReadOnly() {
1502
+ return this.mode === "readonly" /* READONLY */;
1503
+ }
1504
+ /* ---------------- QUEUE HELPER ---------------- */
1505
+ async getQueue() {
1506
+ const { dbQueue } = await import("./queue-YILKSUEI.js");
1507
+ return dbQueue;
1508
+ }
1509
+ /* ---------------- LOCK MANAGEMENT ---------------- */
1510
+ isProcessAlive(pid) {
1511
+ try {
1512
+ process2.kill(pid, 0);
1513
+ return true;
1514
+ } catch {
1515
+ return false;
1516
+ }
1517
+ }
1518
+ tryAcquireLock() {
1519
+ const lockPath = path8.join(this.rootPath, ".lioran.lock");
1520
+ try {
1521
+ this.lockFd = fs8.openSync(lockPath, "wx");
1522
+ fs8.writeSync(this.lockFd, String(process2.pid));
1523
+ return true;
1524
+ } catch {
1525
+ try {
1526
+ const pid = Number(fs8.readFileSync(lockPath, "utf8"));
1527
+ if (!this.isProcessAlive(pid)) {
1528
+ fs8.unlinkSync(lockPath);
1529
+ this.lockFd = fs8.openSync(lockPath, "wx");
1530
+ fs8.writeSync(this.lockFd, String(process2.pid));
1531
+ return true;
1532
+ }
1533
+ } catch {
1534
+ }
1535
+ return false;
1536
+ }
1537
+ }
1538
+ /* ---------------- DB OPEN ---------------- */
1539
+ async db(name) {
1540
+ if (this.mode === "client" /* CLIENT */) {
1541
+ const queue = await this.getQueue();
1542
+ await queue.exec("db", { db: name });
1543
+ return new IPCDatabase(name);
1544
+ }
1545
+ return this.openDatabase(name);
1546
+ }
1547
+ async openDatabase(name) {
1548
+ this._assertOpen();
1549
+ if (this.openDBs.has(name)) {
1550
+ return this.openDBs.get(name);
1551
+ }
1552
+ const dbPath = path8.join(this.rootPath, name);
1553
+ await fs8.promises.mkdir(dbPath, { recursive: true });
1554
+ const db = new LioranDB(dbPath, name, this);
1555
+ this.openDBs.set(name, db);
1556
+ return db;
1557
+ }
1558
+ /* ---------------- SNAPSHOT ---------------- */
1559
+ async snapshot(snapshotPath) {
1560
+ if (this.mode === "client" /* CLIENT */) {
1561
+ const queue = await this.getQueue();
1562
+ return queue.exec("snapshot", { path: snapshotPath });
1563
+ }
1564
+ if (this.mode === "readonly" /* READONLY */) {
1565
+ throw new Error("Snapshot not allowed in readonly mode");
1566
+ }
1567
+ for (const db of this.openDBs.values()) {
1568
+ try {
1569
+ await db.close();
1570
+ } catch {
1571
+ }
1572
+ }
1573
+ fs8.mkdirSync(path8.dirname(snapshotPath), { recursive: true });
1574
+ const tar = await import("tar");
1575
+ await tar.c(
1576
+ {
1577
+ gzip: true,
1578
+ file: snapshotPath,
1579
+ cwd: this.rootPath,
1580
+ portable: true
1581
+ },
1582
+ ["./"]
1583
+ );
1584
+ return true;
1585
+ }
1586
+ /* ---------------- RESTORE ---------------- */
1587
+ async restore(snapshotPath) {
1588
+ if (this.mode === "client" /* CLIENT */) {
1589
+ const queue = await this.getQueue();
1590
+ return queue.exec("restore", { path: snapshotPath });
1591
+ }
1592
+ if (this.mode === "readonly" /* READONLY */) {
1593
+ throw new Error("Restore not allowed in readonly mode");
1594
+ }
1595
+ await this.closeAll();
1596
+ fs8.rmSync(this.rootPath, { recursive: true, force: true });
1597
+ fs8.mkdirSync(this.rootPath, { recursive: true });
1598
+ const tar = await import("tar");
1599
+ await tar.x({
1600
+ file: snapshotPath,
1601
+ cwd: this.rootPath
1602
+ });
1603
+ console.log("Restore completed. Restart required.");
1604
+ process2.exit(0);
1605
+ }
1606
+ /* ---------------- SHUTDOWN ---------------- */
1607
+ async closeAll() {
1608
+ if (this.closed) return;
1609
+ this.closed = true;
1610
+ if (this.mode === "client" /* CLIENT */) {
1611
+ const queue = await this.getQueue();
1612
+ await queue.shutdown();
1613
+ return;
1614
+ }
1615
+ for (const db of this.openDBs.values()) {
1616
+ try {
1617
+ await db.close();
1618
+ } catch {
1619
+ }
1620
+ }
1621
+ this.openDBs.clear();
1622
+ if (this.mode === "primary" /* PRIMARY */) {
1623
+ try {
1624
+ if (this.lockFd) fs8.closeSync(this.lockFd);
1625
+ fs8.unlinkSync(path8.join(this.rootPath, ".lioran.lock"));
1626
+ } catch {
1627
+ }
1628
+ }
1629
+ }
1630
+ async close() {
1631
+ return this.closeAll();
1632
+ }
1633
+ _registerShutdownHooks() {
1634
+ const shutdown = async () => {
1635
+ await this.closeAll();
1636
+ };
1637
+ process2.on("SIGINT", shutdown);
1638
+ process2.on("SIGTERM", shutdown);
1639
+ process2.on("exit", shutdown);
1640
+ }
1641
+ _assertOpen() {
1642
+ if (this.closed) {
1643
+ throw new Error("LioranManager is closed");
1644
+ }
1645
+ }
1646
+ };
1647
+ var IPCDatabase = class {
1648
+ constructor(name) {
1649
+ this.name = name;
1650
+ }
1651
+ collection(name) {
1652
+ return new IPCCollection(this.name, name);
1653
+ }
1654
+ };
1655
+ var IPCCollection = class {
1656
+ constructor(db, col) {
1657
+ this.db = db;
1658
+ this.col = col;
1659
+ }
1660
+ async getQueue() {
1661
+ const { dbQueue } = await import("./queue-YILKSUEI.js");
1662
+ return dbQueue;
1663
+ }
1664
+ async call(method, params) {
1665
+ const queue = await this.getQueue();
1666
+ return queue.exec("op", {
1667
+ db: this.db,
1668
+ col: this.col,
1669
+ method,
1670
+ params
1671
+ });
1672
+ }
1673
+ insertOne = (doc) => this.call("insertOne", [doc]);
1674
+ insertMany = (docs) => this.call("insertMany", [docs]);
1675
+ find = (query) => this.call("find", [query]);
1676
+ findOne = (query) => this.call("findOne", [query]);
1677
+ updateOne = (filter, update, options) => this.call("updateOne", [filter, update, options]);
1678
+ updateMany = (filter, update) => this.call("updateMany", [filter, update]);
1679
+ deleteOne = (filter) => this.call("deleteOne", [filter]);
1680
+ deleteMany = (filter) => this.call("deleteMany", [filter]);
1681
+ countDocuments = (filter) => this.call("countDocuments", [filter]);
1682
+ };
1683
+
1684
+ export {
1685
+ LioranDB,
1686
+ getDefaultRootPath,
1687
+ getBaseDBFolder,
1688
+ LioranManager
1689
+ };