@liorandb/core 1.0.17 → 1.0.19

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.js CHANGED
@@ -1,20 +1,18 @@
1
1
  // src/LioranManager.ts
2
- import path7 from "path";
3
- import fs7 from "fs";
2
+ import path9 from "path";
3
+ import fs9 from "fs";
4
4
  import process2 from "process";
5
5
 
6
6
  // src/core/database.ts
7
- import path4 from "path";
8
- import fs4 from "fs";
9
- import { execFile } from "child_process";
10
- import { promisify } from "util";
7
+ import path6 from "path";
8
+ import fs6 from "fs";
11
9
 
12
10
  // src/core/collection.ts
13
11
  import { ClassicLevel as ClassicLevel3 } from "classic-level";
14
12
 
15
13
  // src/core/query.ts
16
- function getByPath(obj, path8) {
17
- return path8.split(".").reduce((o, p) => o ? o[p] : void 0, obj);
14
+ function getByPath(obj, path10) {
15
+ return path10.split(".").reduce((o, p) => o ? o[p] : void 0, obj);
18
16
  }
19
17
  function matchDocument(doc, query) {
20
18
  for (const key of Object.keys(query)) {
@@ -295,12 +293,13 @@ var Index = class {
295
293
 
296
294
  // src/core/compaction.ts
297
295
  var TMP_SUFFIX = "__compact_tmp";
298
- var OLD_SUFFIX = "__old";
296
+ var OLD_SUFFIX = "__compact_old";
297
+ var INDEX_DIR = "__indexes";
299
298
  async function compactCollectionEngine(col) {
300
- await crashRecovery(col.dir);
301
299
  const baseDir = col.dir;
302
300
  const tmpDir = baseDir + TMP_SUFFIX;
303
301
  const oldDir = baseDir + OLD_SUFFIX;
302
+ await crashRecovery(baseDir);
304
303
  safeRemove(tmpDir);
305
304
  safeRemove(oldDir);
306
305
  await snapshotRebuild(col, tmpDir);
@@ -309,9 +308,13 @@ async function compactCollectionEngine(col) {
309
308
  }
310
309
  async function snapshotRebuild(col, tmpDir) {
311
310
  fs2.mkdirSync(tmpDir, { recursive: true });
312
- const tmpDB = new ClassicLevel2(tmpDir, { valueEncoding: "utf8" });
311
+ const tmpDB = new ClassicLevel2(tmpDir, {
312
+ valueEncoding: "utf8"
313
+ });
313
314
  for await (const [key, val] of col.db.iterator()) {
314
- await tmpDB.put(key, val);
315
+ if (val !== void 0) {
316
+ await tmpDB.put(key, val);
317
+ }
315
318
  }
316
319
  await tmpDB.close();
317
320
  await col.db.close();
@@ -323,38 +326,44 @@ function atomicSwap(base, tmp, old) {
323
326
  async function crashRecovery(baseDir) {
324
327
  const tmp = baseDir + TMP_SUFFIX;
325
328
  const old = baseDir + OLD_SUFFIX;
326
- if (fs2.existsSync(tmp) && fs2.existsSync(old)) {
329
+ const baseExists = fs2.existsSync(baseDir);
330
+ const tmpExists = fs2.existsSync(tmp);
331
+ const oldExists = fs2.existsSync(old);
332
+ if (tmpExists && oldExists) {
327
333
  safeRemove(baseDir);
328
334
  fs2.renameSync(tmp, baseDir);
329
335
  safeRemove(old);
336
+ return;
330
337
  }
331
- if (fs2.existsSync(old) && !fs2.existsSync(baseDir)) {
338
+ if (!baseExists && oldExists) {
332
339
  fs2.renameSync(old, baseDir);
340
+ return;
333
341
  }
334
- if (fs2.existsSync(tmp) && !fs2.existsSync(old)) {
342
+ if (tmpExists && !oldExists) {
335
343
  safeRemove(tmp);
336
344
  }
337
345
  }
338
346
  async function rebuildIndexes(col) {
339
- const indexRoot = path2.join(col.dir, "__indexes");
340
- safeRemove(indexRoot);
341
- fs2.mkdirSync(indexRoot, { recursive: true });
347
+ const indexRoot = path2.join(col.dir, INDEX_DIR);
342
348
  for (const idx of col["indexes"].values()) {
343
349
  try {
344
350
  await idx.close();
345
351
  } catch {
346
352
  }
347
353
  }
354
+ safeRemove(indexRoot);
355
+ fs2.mkdirSync(indexRoot, { recursive: true });
348
356
  const newIndexes = /* @__PURE__ */ new Map();
349
357
  for (const idx of col["indexes"].values()) {
350
- const fresh = new Index(col.dir, idx.field, {
358
+ const rebuilt = new Index(col.dir, idx.field, {
351
359
  unique: idx.unique
352
360
  });
353
361
  for await (const [, enc] of col.db.iterator()) {
362
+ if (!enc) continue;
354
363
  const doc = decryptData(enc);
355
- await fresh.insert(doc);
364
+ await rebuilt.insert(doc);
356
365
  }
357
- newIndexes.set(idx.field, fresh);
366
+ newIndexes.set(idx.field, rebuilt);
358
367
  }
359
368
  col["indexes"] = newIndexes;
360
369
  }
@@ -370,26 +379,43 @@ var Collection = class {
370
379
  db;
371
380
  queue = Promise.resolve();
372
381
  schema;
382
+ schemaVersion = 1;
383
+ migrations = [];
373
384
  indexes = /* @__PURE__ */ new Map();
374
- constructor(dir, schema) {
385
+ constructor(dir, schema, schemaVersion = 1) {
375
386
  this.dir = dir;
376
387
  this.db = new ClassicLevel3(dir, { valueEncoding: "utf8" });
377
388
  this.schema = schema;
389
+ this.schemaVersion = schemaVersion;
378
390
  }
379
- /* ---------------------- INDEX MANAGEMENT ---------------------- */
380
- registerIndex(index) {
381
- this.indexes.set(index.field, index);
382
- }
383
- getIndex(field) {
384
- return this.indexes.get(field);
385
- }
386
- /* -------------------------- CORE -------------------------- */
387
- setSchema(schema) {
391
+ /* ===================== SCHEMA ===================== */
392
+ setSchema(schema, version) {
388
393
  this.schema = schema;
394
+ this.schemaVersion = version;
395
+ }
396
+ addMigration(migration) {
397
+ this.migrations.push(migration);
398
+ this.migrations.sort((a, b) => a.from - b.from);
389
399
  }
390
400
  validate(doc) {
391
401
  return this.schema ? validateSchema(this.schema, doc) : doc;
392
402
  }
403
+ migrateIfNeeded(doc) {
404
+ let currentVersion = doc.__v ?? 1;
405
+ if (currentVersion === this.schemaVersion) {
406
+ return doc;
407
+ }
408
+ let working = doc;
409
+ for (const migration of this.migrations) {
410
+ if (migration.from === currentVersion) {
411
+ working = migration.migrate(working);
412
+ currentVersion = migration.to;
413
+ }
414
+ }
415
+ working.__v = this.schemaVersion;
416
+ return this.validate(working);
417
+ }
418
+ /* ===================== QUEUE ===================== */
393
419
  _enqueue(task) {
394
420
  this.queue = this.queue.then(task).catch(console.error);
395
421
  return this.queue;
@@ -406,7 +432,19 @@ var Collection = class {
406
432
  } catch {
407
433
  }
408
434
  }
409
- /* -------------------- COMPACTION ENGINE -------------------- */
435
+ /* ===================== INDEX MANAGEMENT ===================== */
436
+ registerIndex(index) {
437
+ this.indexes.set(index.field, index);
438
+ }
439
+ getIndex(field) {
440
+ return this.indexes.get(field);
441
+ }
442
+ async _updateIndexes(oldDoc, newDoc) {
443
+ for (const index of this.indexes.values()) {
444
+ await index.update(oldDoc, newDoc);
445
+ }
446
+ }
447
+ /* ===================== COMPACTION ===================== */
410
448
  async compact() {
411
449
  return this._enqueue(async () => {
412
450
  try {
@@ -418,6 +456,7 @@ var Collection = class {
418
456
  await rebuildIndexes(this);
419
457
  });
420
458
  }
459
+ /* ===================== INTERNAL EXEC ===================== */
421
460
  async _exec(op, args) {
422
461
  switch (op) {
423
462
  case "insertOne":
@@ -442,16 +481,14 @@ var Collection = class {
442
481
  throw new Error(`Unknown operation: ${op}`);
443
482
  }
444
483
  }
445
- /* ------------------ INDEX HOOK ------------------ */
446
- async _updateIndexes(oldDoc, newDoc) {
447
- for (const index of this.indexes.values()) {
448
- await index.update(oldDoc, newDoc);
449
- }
450
- }
451
- /* ---------------- Storage ---------------- */
484
+ /* ===================== STORAGE ===================== */
452
485
  async _insertOne(doc) {
453
486
  const _id = doc._id ?? uuid();
454
- const final = this.validate({ _id, ...doc });
487
+ const final = this.validate({
488
+ _id,
489
+ ...doc,
490
+ __v: this.schemaVersion
491
+ });
455
492
  await this.db.put(String(_id), encryptData(final));
456
493
  await this._updateIndexes(null, final);
457
494
  return final;
@@ -461,7 +498,11 @@ var Collection = class {
461
498
  const out = [];
462
499
  for (const d of docs) {
463
500
  const _id = d._id ?? uuid();
464
- const final = this.validate({ _id, ...d });
501
+ const final = this.validate({
502
+ _id,
503
+ ...d,
504
+ __v: this.schemaVersion
505
+ });
465
506
  batch.push({
466
507
  type: "put",
467
508
  key: String(_id),
@@ -475,7 +516,7 @@ var Collection = class {
475
516
  }
476
517
  return out;
477
518
  }
478
- /* ---------------- QUERY ENGINE (INDEXED) ---------------- */
519
+ /* ===================== QUERY ===================== */
479
520
  async _getCandidateIds(query) {
480
521
  const indexedFields = new Set(this.indexes.keys());
481
522
  return runIndexedQuery(
@@ -497,15 +538,26 @@ var Collection = class {
497
538
  }
498
539
  );
499
540
  }
541
+ async _readAndMigrate(id) {
542
+ const enc = await this.db.get(id);
543
+ if (!enc) return null;
544
+ const raw = decryptData(enc);
545
+ const migrated = this.migrateIfNeeded(raw);
546
+ if (raw.__v !== this.schemaVersion) {
547
+ await this.db.put(id, encryptData(migrated));
548
+ await this._updateIndexes(raw, migrated);
549
+ }
550
+ return migrated;
551
+ }
500
552
  async _find(query) {
501
553
  const ids = await this._getCandidateIds(query);
502
554
  const out = [];
503
555
  for (const id of ids) {
504
556
  try {
505
- const enc = await this.db.get(id);
506
- if (!enc) continue;
507
- const doc = decryptData(enc);
508
- if (matchDocument(doc, query)) out.push(doc);
557
+ const doc = await this._readAndMigrate(id);
558
+ if (doc && matchDocument(doc, query)) {
559
+ out.push(doc);
560
+ }
509
561
  } catch {
510
562
  }
511
563
  }
@@ -514,8 +566,7 @@ var Collection = class {
514
566
  async _findOne(query) {
515
567
  if (query?._id) {
516
568
  try {
517
- const enc = await this.db.get(String(query._id));
518
- return enc ? decryptData(enc) : null;
569
+ return await this._readAndMigrate(String(query._id));
519
570
  } catch {
520
571
  return null;
521
572
  }
@@ -523,10 +574,10 @@ var Collection = class {
523
574
  const ids = await this._getCandidateIds(query);
524
575
  for (const id of ids) {
525
576
  try {
526
- const enc = await this.db.get(id);
527
- if (!enc) continue;
528
- const doc = decryptData(enc);
529
- if (matchDocument(doc, query)) return doc;
577
+ const doc = await this._readAndMigrate(id);
578
+ if (doc && matchDocument(doc, query)) {
579
+ return doc;
580
+ }
530
581
  } catch {
531
582
  }
532
583
  }
@@ -537,40 +588,34 @@ var Collection = class {
537
588
  let count = 0;
538
589
  for (const id of ids) {
539
590
  try {
540
- const enc = await this.db.get(id);
541
- if (!enc) continue;
542
- if (matchDocument(decryptData(enc), filter)) count++;
591
+ const doc = await this._readAndMigrate(id);
592
+ if (doc && matchDocument(doc, filter)) {
593
+ count++;
594
+ }
543
595
  } catch {
544
596
  }
545
597
  }
546
598
  return count;
547
599
  }
548
- /* ---------------- UPDATE ---------------- */
600
+ /* ===================== UPDATE ===================== */
549
601
  async _updateOne(filter, update, options) {
550
602
  const ids = await this._getCandidateIds(filter);
551
603
  for (const id of ids) {
552
- try {
553
- const enc = await this.db.get(id);
554
- if (!enc) continue;
555
- const value = decryptData(enc);
556
- if (matchDocument(value, filter)) {
557
- const updated = this.validate(applyUpdate(value, update));
558
- updated._id = value._id;
559
- await this.db.put(id, encryptData(updated));
560
- await this._updateIndexes(value, updated);
561
- return updated;
562
- }
563
- } catch {
604
+ const existing = await this._readAndMigrate(id);
605
+ if (!existing) continue;
606
+ if (matchDocument(existing, filter)) {
607
+ const updated = this.validate({
608
+ ...applyUpdate(existing, update),
609
+ _id: existing._id,
610
+ __v: this.schemaVersion
611
+ });
612
+ await this.db.put(id, encryptData(updated));
613
+ await this._updateIndexes(existing, updated);
614
+ return updated;
564
615
  }
565
616
  }
566
617
  if (options?.upsert) {
567
- const doc = this.validate({
568
- _id: uuid(),
569
- ...applyUpdate({}, update)
570
- });
571
- await this.db.put(String(doc._id), encryptData(doc));
572
- await this._updateIndexes(null, doc);
573
- return doc;
618
+ return this._insertOne(applyUpdate({}, update));
574
619
  }
575
620
  return null;
576
621
  }
@@ -578,36 +623,31 @@ var Collection = class {
578
623
  const ids = await this._getCandidateIds(filter);
579
624
  const out = [];
580
625
  for (const id of ids) {
581
- try {
582
- const enc = await this.db.get(id);
583
- if (!enc) continue;
584
- const value = decryptData(enc);
585
- if (matchDocument(value, filter)) {
586
- const updated = this.validate(applyUpdate(value, update));
587
- updated._id = value._id;
588
- await this.db.put(id, encryptData(updated));
589
- await this._updateIndexes(value, updated);
590
- out.push(updated);
591
- }
592
- } catch {
626
+ const existing = await this._readAndMigrate(id);
627
+ if (!existing) continue;
628
+ if (matchDocument(existing, filter)) {
629
+ const updated = this.validate({
630
+ ...applyUpdate(existing, update),
631
+ _id: existing._id,
632
+ __v: this.schemaVersion
633
+ });
634
+ await this.db.put(id, encryptData(updated));
635
+ await this._updateIndexes(existing, updated);
636
+ out.push(updated);
593
637
  }
594
638
  }
595
639
  return out;
596
640
  }
597
- /* ---------------- DELETE ---------------- */
641
+ /* ===================== DELETE ===================== */
598
642
  async _deleteOne(filter) {
599
643
  const ids = await this._getCandidateIds(filter);
600
644
  for (const id of ids) {
601
- try {
602
- const enc = await this.db.get(id);
603
- if (!enc) continue;
604
- const value = decryptData(enc);
605
- if (matchDocument(value, filter)) {
606
- await this.db.del(id);
607
- await this._updateIndexes(value, null);
608
- return true;
609
- }
610
- } catch {
645
+ const existing = await this._readAndMigrate(id);
646
+ if (!existing) continue;
647
+ if (matchDocument(existing, filter)) {
648
+ await this.db.del(id);
649
+ await this._updateIndexes(existing, null);
650
+ return true;
611
651
  }
612
652
  }
613
653
  return false;
@@ -616,21 +656,17 @@ var Collection = class {
616
656
  const ids = await this._getCandidateIds(filter);
617
657
  let count = 0;
618
658
  for (const id of ids) {
619
- try {
620
- const enc = await this.db.get(id);
621
- if (!enc) continue;
622
- const value = decryptData(enc);
623
- if (matchDocument(value, filter)) {
624
- await this.db.del(id);
625
- await this._updateIndexes(value, null);
626
- count++;
627
- }
628
- } catch {
659
+ const existing = await this._readAndMigrate(id);
660
+ if (!existing) continue;
661
+ if (matchDocument(existing, filter)) {
662
+ await this.db.del(id);
663
+ await this._updateIndexes(existing, null);
664
+ count++;
629
665
  }
630
666
  }
631
667
  return count;
632
668
  }
633
- /* ---------------- PUBLIC API (Mongo-style) ---------------- */
669
+ /* ===================== PUBLIC API ===================== */
634
670
  insertOne(doc) {
635
671
  return this._enqueue(() => this._exec("insertOne", [doc]));
636
672
  }
@@ -793,10 +829,230 @@ var MigrationEngine = class {
793
829
  }
794
830
  };
795
831
 
832
+ // src/core/wal.ts
833
+ import fs4 from "fs";
834
+ import path4 from "path";
835
+ var MAX_WAL_SIZE = 16 * 1024 * 1024;
836
+ var WAL_DIR = "__wal";
837
+ var CRC32_TABLE = (() => {
838
+ const table = new Uint32Array(256);
839
+ for (let i = 0; i < 256; i++) {
840
+ let c = i;
841
+ for (let k = 0; k < 8; k++) {
842
+ c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
843
+ }
844
+ table[i] = c >>> 0;
845
+ }
846
+ return table;
847
+ })();
848
+ function crc32(input) {
849
+ let crc = 4294967295;
850
+ for (let i = 0; i < input.length; i++) {
851
+ const byte = input.charCodeAt(i);
852
+ crc = CRC32_TABLE[(crc ^ byte) & 255] ^ crc >>> 8;
853
+ }
854
+ return (crc ^ 4294967295) >>> 0;
855
+ }
856
+ var WALManager = class {
857
+ walDir;
858
+ currentGen = 1;
859
+ lsn = 0;
860
+ fd = null;
861
+ constructor(baseDir) {
862
+ this.walDir = path4.join(baseDir, WAL_DIR);
863
+ fs4.mkdirSync(this.walDir, { recursive: true });
864
+ this.currentGen = this.detectLastGeneration();
865
+ }
866
+ /* -------------------------
867
+ INTERNAL HELPERS
868
+ ------------------------- */
869
+ walPath(gen = this.currentGen) {
870
+ return path4.join(
871
+ this.walDir,
872
+ `wal-${String(gen).padStart(6, "0")}.log`
873
+ );
874
+ }
875
+ detectLastGeneration() {
876
+ if (!fs4.existsSync(this.walDir)) return 1;
877
+ const files = fs4.readdirSync(this.walDir);
878
+ let max = 0;
879
+ for (const f of files) {
880
+ const m = f.match(/^wal-(\d+)\.log$/);
881
+ if (m) max = Math.max(max, Number(m[1]));
882
+ }
883
+ return max || 1;
884
+ }
885
+ async open() {
886
+ if (!this.fd) {
887
+ this.fd = await fs4.promises.open(this.walPath(), "a");
888
+ }
889
+ }
890
+ async rotate() {
891
+ if (this.fd) {
892
+ await this.fd.close();
893
+ this.fd = null;
894
+ }
895
+ this.currentGen++;
896
+ }
897
+ /* -------------------------
898
+ APPEND
899
+ ------------------------- */
900
+ async append(record) {
901
+ await this.open();
902
+ const full = {
903
+ ...record,
904
+ lsn: ++this.lsn
905
+ };
906
+ const body = JSON.stringify(full);
907
+ const stored = {
908
+ ...full,
909
+ crc: crc32(body)
910
+ };
911
+ await this.fd.write(JSON.stringify(stored) + "\n");
912
+ await this.fd.sync();
913
+ const stat = await this.fd.stat();
914
+ if (stat.size >= MAX_WAL_SIZE) {
915
+ await this.rotate();
916
+ }
917
+ return full.lsn;
918
+ }
919
+ /* -------------------------
920
+ REPLAY
921
+ ------------------------- */
922
+ async replay(fromLSN, apply) {
923
+ if (!fs4.existsSync(this.walDir)) return;
924
+ const files = fs4.readdirSync(this.walDir).filter((f) => f.startsWith("wal-")).sort();
925
+ for (const file of files) {
926
+ const filePath = path4.join(this.walDir, file);
927
+ const data = fs4.readFileSync(filePath, "utf8");
928
+ const lines = data.split("\n");
929
+ for (let i = 0; i < lines.length; i++) {
930
+ const line = lines[i];
931
+ if (!line.trim()) continue;
932
+ let parsed;
933
+ try {
934
+ parsed = JSON.parse(line);
935
+ } catch {
936
+ console.error("WAL parse error, stopping replay");
937
+ return;
938
+ }
939
+ const { crc, ...record } = parsed;
940
+ const expected = crc32(JSON.stringify(record));
941
+ if (expected !== crc) {
942
+ console.error(
943
+ "WAL checksum mismatch, stopping replay",
944
+ { file, line: i + 1 }
945
+ );
946
+ return;
947
+ }
948
+ if (record.lsn <= fromLSN) continue;
949
+ this.lsn = Math.max(this.lsn, record.lsn);
950
+ await apply(record);
951
+ }
952
+ }
953
+ }
954
+ /* -------------------------
955
+ CLEANUP
956
+ ------------------------- */
957
+ async cleanup(beforeGen) {
958
+ if (!fs4.existsSync(this.walDir)) return;
959
+ const files = fs4.readdirSync(this.walDir);
960
+ for (const f of files) {
961
+ const m = f.match(/^wal-(\d+)\.log$/);
962
+ if (!m) continue;
963
+ const gen = Number(m[1]);
964
+ if (gen < beforeGen) {
965
+ fs4.unlinkSync(path4.join(this.walDir, f));
966
+ }
967
+ }
968
+ }
969
+ /* -------------------------
970
+ GETTERS
971
+ ------------------------- */
972
+ getCurrentLSN() {
973
+ return this.lsn;
974
+ }
975
+ getCurrentGen() {
976
+ return this.currentGen;
977
+ }
978
+ };
979
+
980
+ // src/core/checkpoint.ts
981
+ import fs5 from "fs";
982
+ import path5 from "path";
983
+ var CHECKPOINT_FILE = "__checkpoint.json";
984
+ var TMP_SUFFIX2 = ".tmp";
985
+ var FORMAT_VERSION = 1;
986
+ var CheckpointManager = class {
987
+ filePath;
988
+ data;
989
+ constructor(baseDir) {
990
+ this.filePath = path5.join(baseDir, CHECKPOINT_FILE);
991
+ this.data = {
992
+ lsn: 0,
993
+ walGen: 1,
994
+ time: 0,
995
+ version: FORMAT_VERSION
996
+ };
997
+ this.load();
998
+ }
999
+ /* -------------------------
1000
+ LOAD (Crash-safe)
1001
+ ------------------------- */
1002
+ load() {
1003
+ if (!fs5.existsSync(this.filePath)) {
1004
+ return;
1005
+ }
1006
+ try {
1007
+ const raw = fs5.readFileSync(this.filePath, "utf8");
1008
+ const parsed = JSON.parse(raw);
1009
+ if (typeof parsed.lsn === "number" && typeof parsed.walGen === "number") {
1010
+ this.data = parsed;
1011
+ }
1012
+ } catch {
1013
+ console.error("Checkpoint corrupted, starting from zero");
1014
+ this.data = {
1015
+ lsn: 0,
1016
+ walGen: 1,
1017
+ time: 0,
1018
+ version: FORMAT_VERSION
1019
+ };
1020
+ }
1021
+ }
1022
+ /* -------------------------
1023
+ SAVE (Atomic Write)
1024
+ ------------------------- */
1025
+ save(lsn, walGen) {
1026
+ const newData = {
1027
+ lsn,
1028
+ walGen,
1029
+ time: Date.now(),
1030
+ version: FORMAT_VERSION
1031
+ };
1032
+ const tmpPath = this.filePath + TMP_SUFFIX2;
1033
+ try {
1034
+ fs5.writeFileSync(
1035
+ tmpPath,
1036
+ JSON.stringify(newData, null, 2),
1037
+ { encoding: "utf8" }
1038
+ );
1039
+ fs5.renameSync(tmpPath, this.filePath);
1040
+ this.data = newData;
1041
+ } catch (err) {
1042
+ console.error("Failed to write checkpoint:", err);
1043
+ }
1044
+ }
1045
+ /* -------------------------
1046
+ GET CURRENT
1047
+ ------------------------- */
1048
+ get() {
1049
+ return this.data;
1050
+ }
1051
+ };
1052
+
796
1053
  // src/core/database.ts
797
- var exec = promisify(execFile);
798
1054
  var META_FILE = "__db_meta.json";
799
- var META_VERSION = 1;
1055
+ var META_VERSION = 2;
800
1056
  var DEFAULT_SCHEMA_VERSION = "v1";
801
1057
  var DBTransactionContext = class {
802
1058
  constructor(db, txId) {
@@ -819,11 +1075,26 @@ var DBTransactionContext = class {
819
1075
  });
820
1076
  }
821
1077
  async commit() {
822
- await this.db.writeWAL(this.ops);
823
- await this.db.writeWAL([{ tx: this.txId, commit: true }]);
1078
+ for (const op of this.ops) {
1079
+ const recordOp = {
1080
+ tx: this.txId,
1081
+ type: "op",
1082
+ payload: op
1083
+ };
1084
+ await this.db.wal.append(recordOp);
1085
+ }
1086
+ const commitRecord = {
1087
+ tx: this.txId,
1088
+ type: "commit"
1089
+ };
1090
+ await this.db.wal.append(commitRecord);
824
1091
  await this.db.applyTransaction(this.ops);
825
- await this.db.writeWAL([{ tx: this.txId, applied: true }]);
826
- await this.db.clearWAL();
1092
+ const appliedRecord = {
1093
+ tx: this.txId,
1094
+ type: "applied"
1095
+ };
1096
+ await this.db.wal.append(appliedRecord);
1097
+ await this.db.postCommitMaintenance();
827
1098
  }
828
1099
  };
829
1100
  var LioranDB = class _LioranDB {
@@ -831,26 +1102,56 @@ var LioranDB = class _LioranDB {
831
1102
  dbName;
832
1103
  manager;
833
1104
  collections;
834
- walPath;
835
1105
  metaPath;
836
1106
  meta;
837
1107
  migrator;
838
1108
  static TX_SEQ = 0;
1109
+ wal;
1110
+ checkpoint;
839
1111
  constructor(basePath, dbName, manager) {
840
1112
  this.basePath = basePath;
841
1113
  this.dbName = dbName;
842
1114
  this.manager = manager;
843
1115
  this.collections = /* @__PURE__ */ new Map();
844
- this.walPath = path4.join(basePath, "__tx_wal.log");
845
- this.metaPath = path4.join(basePath, META_FILE);
846
- fs4.mkdirSync(basePath, { recursive: true });
1116
+ this.metaPath = path6.join(basePath, META_FILE);
1117
+ fs6.mkdirSync(basePath, { recursive: true });
847
1118
  this.loadMeta();
1119
+ this.wal = new WALManager(basePath);
1120
+ this.checkpoint = new CheckpointManager(basePath);
848
1121
  this.migrator = new MigrationEngine(this);
849
- this.recoverFromWAL().catch(console.error);
1122
+ this.initialize().catch(console.error);
1123
+ }
1124
+ /* ------------------------- INIT & RECOVERY ------------------------- */
1125
+ async initialize() {
1126
+ await this.recoverFromWAL();
1127
+ }
1128
+ async recoverFromWAL() {
1129
+ const checkpointData = this.checkpoint.get();
1130
+ const fromLSN = checkpointData.lsn;
1131
+ const committed = /* @__PURE__ */ new Set();
1132
+ const applied = /* @__PURE__ */ new Set();
1133
+ const ops = /* @__PURE__ */ new Map();
1134
+ await this.wal.replay(fromLSN, async (record) => {
1135
+ if (record.type === "commit") {
1136
+ committed.add(record.tx);
1137
+ } else if (record.type === "applied") {
1138
+ applied.add(record.tx);
1139
+ } else if (record.type === "op") {
1140
+ if (!ops.has(record.tx)) ops.set(record.tx, []);
1141
+ ops.get(record.tx).push(record.payload);
1142
+ }
1143
+ });
1144
+ for (const tx of committed) {
1145
+ if (applied.has(tx)) continue;
1146
+ const txOps = ops.get(tx);
1147
+ if (txOps) {
1148
+ await this.applyTransaction(txOps);
1149
+ }
1150
+ }
850
1151
  }
851
1152
  /* ------------------------- META ------------------------- */
852
1153
  loadMeta() {
853
- if (!fs4.existsSync(this.metaPath)) {
1154
+ if (!fs6.existsSync(this.metaPath)) {
854
1155
  this.meta = {
855
1156
  version: META_VERSION,
856
1157
  indexes: {},
@@ -859,14 +1160,14 @@ var LioranDB = class _LioranDB {
859
1160
  this.saveMeta();
860
1161
  return;
861
1162
  }
862
- this.meta = JSON.parse(fs4.readFileSync(this.metaPath, "utf8"));
1163
+ this.meta = JSON.parse(fs6.readFileSync(this.metaPath, "utf8"));
863
1164
  if (!this.meta.schemaVersion) {
864
1165
  this.meta.schemaVersion = DEFAULT_SCHEMA_VERSION;
865
1166
  this.saveMeta();
866
1167
  }
867
1168
  }
868
1169
  saveMeta() {
869
- fs4.writeFileSync(this.metaPath, JSON.stringify(this.meta, null, 2));
1170
+ fs6.writeFileSync(this.metaPath, JSON.stringify(this.meta, null, 2));
870
1171
  }
871
1172
  getSchemaVersion() {
872
1173
  return this.meta.schemaVersion;
@@ -875,7 +1176,7 @@ var LioranDB = class _LioranDB {
875
1176
  this.meta.schemaVersion = v;
876
1177
  this.saveMeta();
877
1178
  }
878
- /* ------------------------- MIGRATION API ------------------------- */
1179
+ /* ------------------------- DB MIGRATIONS ------------------------- */
879
1180
  migrate(from, to, fn) {
880
1181
  this.migrator.register(from, to, async (db) => {
881
1182
  await fn(db);
@@ -885,44 +1186,7 @@ var LioranDB = class _LioranDB {
885
1186
  async applyMigrations(targetVersion) {
886
1187
  await this.migrator.upgradeToLatest();
887
1188
  }
888
- /* ------------------------- WAL ------------------------- */
889
- async writeWAL(entries) {
890
- const fd = await fs4.promises.open(this.walPath, "a");
891
- for (const e of entries) {
892
- await fd.write(JSON.stringify(e) + "\n");
893
- }
894
- await fd.sync();
895
- await fd.close();
896
- }
897
- async clearWAL() {
898
- try {
899
- await fs4.promises.unlink(this.walPath);
900
- } catch {
901
- }
902
- }
903
- async recoverFromWAL() {
904
- if (!fs4.existsSync(this.walPath)) return;
905
- const raw = await fs4.promises.readFile(this.walPath, "utf8");
906
- const committed = /* @__PURE__ */ new Set();
907
- const applied = /* @__PURE__ */ new Set();
908
- const ops = /* @__PURE__ */ new Map();
909
- for (const line of raw.split("\n")) {
910
- if (!line.trim()) continue;
911
- const entry = JSON.parse(line);
912
- if ("commit" in entry) committed.add(entry.tx);
913
- else if ("applied" in entry) applied.add(entry.tx);
914
- else {
915
- if (!ops.has(entry.tx)) ops.set(entry.tx, []);
916
- ops.get(entry.tx).push(entry);
917
- }
918
- }
919
- for (const tx of committed) {
920
- if (applied.has(tx)) continue;
921
- const txOps = ops.get(tx);
922
- if (txOps) await this.applyTransaction(txOps);
923
- }
924
- await this.clearWAL();
925
- }
1189
+ /* ------------------------- TX APPLY ------------------------- */
926
1190
  async applyTransaction(ops) {
927
1191
  for (const { col, op, args } of ops) {
928
1192
  const collection = this.collection(col);
@@ -930,15 +1194,21 @@ var LioranDB = class _LioranDB {
930
1194
  }
931
1195
  }
932
1196
  /* ------------------------- COLLECTION ------------------------- */
933
- collection(name, schema) {
1197
+ collection(name, schema, schemaVersion) {
934
1198
  if (this.collections.has(name)) {
935
1199
  const col2 = this.collections.get(name);
936
- if (schema) col2.setSchema(schema);
1200
+ if (schema && schemaVersion !== void 0) {
1201
+ col2.setSchema(schema, schemaVersion);
1202
+ }
937
1203
  return col2;
938
1204
  }
939
- const colPath = path4.join(this.basePath, name);
940
- fs4.mkdirSync(colPath, { recursive: true });
941
- const col = new Collection(colPath, schema);
1205
+ const colPath = path6.join(this.basePath, name);
1206
+ fs6.mkdirSync(colPath, { recursive: true });
1207
+ const col = new Collection(
1208
+ colPath,
1209
+ schema,
1210
+ schemaVersion ?? 1
1211
+ );
942
1212
  const metas = this.meta.indexes[name] ?? [];
943
1213
  for (const m of metas) {
944
1214
  col.registerIndex(new Index(colPath, m.field, m.options));
@@ -955,11 +1225,11 @@ var LioranDB = class _LioranDB {
955
1225
  for await (const [key, enc] of col.db.iterator()) {
956
1226
  if (!enc) continue;
957
1227
  try {
958
- const doc = decryptData2(enc);
1228
+ const doc = decryptData(enc);
959
1229
  await index.insert(doc);
960
1230
  } catch (err) {
961
- const errorMessage = err instanceof Error ? err.message : String(err);
962
- console.warn(`Could not decrypt document ${key} during index build: ${errorMessage}`);
1231
+ const msg = err instanceof Error ? err.message : String(err);
1232
+ console.warn(`Index build skipped doc ${key}: ${msg}`);
963
1233
  }
964
1234
  }
965
1235
  col.registerIndex(index);
@@ -971,12 +1241,10 @@ var LioranDB = class _LioranDB {
971
1241
  }
972
1242
  /* ------------------------- COMPACTION ------------------------- */
973
1243
  async compactCollection(name) {
974
- await this.clearWAL();
975
1244
  const col = this.collection(name);
976
1245
  await col.compact();
977
1246
  }
978
1247
  async compactAll() {
979
- await this.clearWAL();
980
1248
  for (const name of this.collections.keys()) {
981
1249
  await this.compactCollection(name);
982
1250
  }
@@ -989,6 +1257,9 @@ var LioranDB = class _LioranDB {
989
1257
  await tx.commit();
990
1258
  return result;
991
1259
  }
1260
+ /* ------------------------- POST COMMIT ------------------------- */
1261
+ async postCommitMaintenance() {
1262
+ }
992
1263
  /* ------------------------- SHUTDOWN ------------------------- */
993
1264
  async close() {
994
1265
  for (const col of this.collections.values()) {
@@ -1000,21 +1271,18 @@ var LioranDB = class _LioranDB {
1000
1271
  this.collections.clear();
1001
1272
  }
1002
1273
  };
1003
- function decryptData2(enc) {
1004
- throw new Error("Function not implemented.");
1005
- }
1006
1274
 
1007
1275
  // src/utils/rootpath.ts
1008
1276
  import os2 from "os";
1009
- import path5 from "path";
1010
- import fs5 from "fs";
1277
+ import path7 from "path";
1278
+ import fs7 from "fs";
1011
1279
  function getDefaultRootPath() {
1012
1280
  let dbPath = process.env.LIORANDB_PATH;
1013
1281
  if (!dbPath) {
1014
1282
  const homeDir = os2.homedir();
1015
- dbPath = path5.join(homeDir, "LioranDB", "db");
1016
- if (!fs5.existsSync(dbPath)) {
1017
- fs5.mkdirSync(dbPath, { recursive: true });
1283
+ dbPath = path7.join(homeDir, "LioranDB", "db");
1284
+ if (!fs7.existsSync(dbPath)) {
1285
+ fs7.mkdirSync(dbPath, { recursive: true });
1018
1286
  }
1019
1287
  process.env.LIORANDB_PATH = dbPath;
1020
1288
  }
@@ -1029,24 +1297,24 @@ import net from "net";
1029
1297
 
1030
1298
  // src/ipc/socketPath.ts
1031
1299
  import os3 from "os";
1032
- import path6 from "path";
1300
+ import path8 from "path";
1033
1301
  function getIPCSocketPath(rootPath) {
1034
1302
  if (os3.platform() === "win32") {
1035
1303
  return `\\\\.\\pipe\\liorandb_${rootPath.replace(/[:\\\/]/g, "_")}`;
1036
1304
  }
1037
- return path6.join(rootPath, ".lioran.sock");
1305
+ return path8.join(rootPath, ".lioran.sock");
1038
1306
  }
1039
1307
 
1040
1308
  // src/ipc/client.ts
1041
1309
  function delay(ms) {
1042
1310
  return new Promise((r) => setTimeout(r, ms));
1043
1311
  }
1044
- async function connectWithRetry(path8) {
1312
+ async function connectWithRetry(path10) {
1045
1313
  let attempt = 0;
1046
1314
  while (true) {
1047
1315
  try {
1048
1316
  return await new Promise((resolve, reject) => {
1049
- const socket = net.connect(path8, () => resolve(socket));
1317
+ const socket = net.connect(path10, () => resolve(socket));
1050
1318
  socket.once("error", reject);
1051
1319
  });
1052
1320
  } catch (err) {
@@ -1129,11 +1397,11 @@ var DBQueue = class {
1129
1397
  return this.exec("compact:all", {});
1130
1398
  }
1131
1399
  /* ----------------------------- SNAPSHOT API ----------------------------- */
1132
- snapshot(path8) {
1133
- return this.exec("snapshot", { path: path8 });
1400
+ snapshot(path10) {
1401
+ return this.exec("snapshot", { path: path10 });
1134
1402
  }
1135
- restore(path8) {
1136
- return this.exec("restore", { path: path8 });
1403
+ restore(path10) {
1404
+ return this.exec("restore", { path: path10 });
1137
1405
  }
1138
1406
  /* ------------------------------ SHUTDOWN ------------------------------ */
1139
1407
  async shutdown() {
@@ -1148,7 +1416,7 @@ var dbQueue = new DBQueue();
1148
1416
 
1149
1417
  // src/ipc/server.ts
1150
1418
  import net2 from "net";
1151
- import fs6 from "fs";
1419
+ import fs8 from "fs";
1152
1420
  var IPCServer = class {
1153
1421
  server;
1154
1422
  manager;
@@ -1159,7 +1427,7 @@ var IPCServer = class {
1159
1427
  }
1160
1428
  start() {
1161
1429
  if (!this.socketPath.startsWith("\\\\.\\")) {
1162
- if (fs6.existsSync(this.socketPath)) fs6.unlinkSync(this.socketPath);
1430
+ if (fs8.existsSync(this.socketPath)) fs8.unlinkSync(this.socketPath);
1163
1431
  }
1164
1432
  this.server = net2.createServer((socket) => {
1165
1433
  let buffer = "";
@@ -1261,7 +1529,7 @@ var IPCServer = class {
1261
1529
  if (this.server) this.server.close();
1262
1530
  if (!this.socketPath.startsWith("\\\\.\\")) {
1263
1531
  try {
1264
- fs6.unlinkSync(this.socketPath);
1532
+ fs8.unlinkSync(this.socketPath);
1265
1533
  } catch {
1266
1534
  }
1267
1535
  }
@@ -1279,8 +1547,8 @@ var LioranManager = class {
1279
1547
  constructor(options = {}) {
1280
1548
  const { rootPath, encryptionKey } = options;
1281
1549
  this.rootPath = rootPath || getDefaultRootPath();
1282
- if (!fs7.existsSync(this.rootPath)) {
1283
- fs7.mkdirSync(this.rootPath, { recursive: true });
1550
+ if (!fs9.existsSync(this.rootPath)) {
1551
+ fs9.mkdirSync(this.rootPath, { recursive: true });
1284
1552
  }
1285
1553
  if (encryptionKey) {
1286
1554
  setEncryptionKey(encryptionKey);
@@ -1303,18 +1571,18 @@ var LioranManager = class {
1303
1571
  }
1304
1572
  }
1305
1573
  tryAcquireLock() {
1306
- const lockPath = path7.join(this.rootPath, ".lioran.lock");
1574
+ const lockPath = path9.join(this.rootPath, ".lioran.lock");
1307
1575
  try {
1308
- this.lockFd = fs7.openSync(lockPath, "wx");
1309
- fs7.writeSync(this.lockFd, String(process2.pid));
1576
+ this.lockFd = fs9.openSync(lockPath, "wx");
1577
+ fs9.writeSync(this.lockFd, String(process2.pid));
1310
1578
  return true;
1311
1579
  } catch {
1312
1580
  try {
1313
- const pid = Number(fs7.readFileSync(lockPath, "utf8"));
1581
+ const pid = Number(fs9.readFileSync(lockPath, "utf8"));
1314
1582
  if (!this.isProcessAlive(pid)) {
1315
- fs7.unlinkSync(lockPath);
1316
- this.lockFd = fs7.openSync(lockPath, "wx");
1317
- fs7.writeSync(this.lockFd, String(process2.pid));
1583
+ fs9.unlinkSync(lockPath);
1584
+ this.lockFd = fs9.openSync(lockPath, "wx");
1585
+ fs9.writeSync(this.lockFd, String(process2.pid));
1318
1586
  return true;
1319
1587
  }
1320
1588
  } catch {
@@ -1335,8 +1603,8 @@ var LioranManager = class {
1335
1603
  if (this.openDBs.has(name)) {
1336
1604
  return this.openDBs.get(name);
1337
1605
  }
1338
- const dbPath = path7.join(this.rootPath, name);
1339
- await fs7.promises.mkdir(dbPath, { recursive: true });
1606
+ const dbPath = path9.join(this.rootPath, name);
1607
+ await fs9.promises.mkdir(dbPath, { recursive: true });
1340
1608
  const db = new LioranDB(dbPath, name, this);
1341
1609
  this.openDBs.set(name, db);
1342
1610
  return db;
@@ -1357,7 +1625,7 @@ var LioranManager = class {
1357
1625
  }
1358
1626
  }
1359
1627
  }
1360
- fs7.mkdirSync(path7.dirname(snapshotPath), { recursive: true });
1628
+ fs9.mkdirSync(path9.dirname(snapshotPath), { recursive: true });
1361
1629
  const tar = await import("tar");
1362
1630
  await tar.c({
1363
1631
  gzip: true,
@@ -1375,8 +1643,8 @@ var LioranManager = class {
1375
1643
  return dbQueue.exec("restore", { path: snapshotPath });
1376
1644
  }
1377
1645
  await this.closeAll();
1378
- fs7.rmSync(this.rootPath, { recursive: true, force: true });
1379
- fs7.mkdirSync(this.rootPath, { recursive: true });
1646
+ fs9.rmSync(this.rootPath, { recursive: true, force: true });
1647
+ fs9.mkdirSync(this.rootPath, { recursive: true });
1380
1648
  const tar = await import("tar");
1381
1649
  await tar.x({
1382
1650
  file: snapshotPath,
@@ -1401,8 +1669,8 @@ var LioranManager = class {
1401
1669
  }
1402
1670
  this.openDBs.clear();
1403
1671
  try {
1404
- if (this.lockFd) fs7.closeSync(this.lockFd);
1405
- fs7.unlinkSync(path7.join(this.rootPath, ".lioran.lock"));
1672
+ if (this.lockFd) fs9.closeSync(this.lockFd);
1673
+ fs9.unlinkSync(path9.join(this.rootPath, ".lioran.lock"));
1406
1674
  } catch {
1407
1675
  }
1408
1676
  await this.ipcServer?.close();