@liorandb/core 1.0.15 → 1.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -92,23 +92,22 @@ declare class LioranDB {
92
92
  private walPath;
93
93
  private metaPath;
94
94
  private meta;
95
+ private migrator;
95
96
  private static TX_SEQ;
96
97
  constructor(basePath: string, dbName: string, manager: LioranManager);
97
98
  private loadMeta;
98
99
  private saveMeta;
100
+ getSchemaVersion(): string;
101
+ setSchemaVersion(v: string): void;
102
+ migrate(from: string, to: string, fn: (db: LioranDB) => Promise<void>): void;
103
+ applyMigrations(targetVersion: string): Promise<void>;
99
104
  writeWAL(entries: WALEntry[]): Promise<void>;
100
105
  clearWAL(): Promise<void>;
101
106
  private recoverFromWAL;
102
107
  applyTransaction(ops: TXOp[]): Promise<void>;
103
108
  collection<T = any>(name: string, schema?: ZodSchema<T>): Collection<T>;
104
109
  createIndex(collection: string, field: string, options?: IndexOptions): Promise<void>;
105
- /**
106
- * Compact single collection safely (WAL + TX safe)
107
- */
108
110
  compactCollection(name: string): Promise<void>;
109
- /**
110
- * Compact entire database safely
111
- */
112
111
  compactAll(): Promise<void>;
113
112
  transaction<T>(fn: (tx: DBTransactionContext) => Promise<T>): Promise<T>;
114
113
  close(): Promise<void>;
@@ -130,6 +129,14 @@ declare class LioranManager {
130
129
  private tryAcquireLock;
131
130
  db(name: string): Promise<LioranDB>;
132
131
  openDatabase(name: string): Promise<LioranDB>;
132
+ /**
133
+ * Create TAR snapshot of full DB directory
134
+ */
135
+ snapshot(snapshotPath: string): Promise<unknown>;
136
+ /**
137
+ * Restore TAR snapshot safely
138
+ */
139
+ restore(snapshotPath: string): Promise<unknown>;
133
140
  closeAll(): Promise<void>;
134
141
  close(): Promise<void>;
135
142
  private _registerShutdownHooks;
package/dist/index.js CHANGED
@@ -1,18 +1,20 @@
1
1
  // src/LioranManager.ts
2
- import path6 from "path";
3
- import fs6 from "fs";
2
+ import path7 from "path";
3
+ import fs7 from "fs";
4
4
  import process2 from "process";
5
5
 
6
6
  // src/core/database.ts
7
- import path3 from "path";
8
- import fs3 from "fs";
7
+ import path4 from "path";
8
+ import fs4 from "fs";
9
+ import { execFile } from "child_process";
10
+ import { promisify } from "util";
9
11
 
10
12
  // src/core/collection.ts
11
13
  import { ClassicLevel as ClassicLevel3 } from "classic-level";
12
14
 
13
15
  // src/core/query.ts
14
- function getByPath(obj, path7) {
15
- return path7.split(".").reduce((o, p) => o ? o[p] : void 0, obj);
16
+ function getByPath(obj, path8) {
17
+ return path8.split(".").reduce((o, p) => o ? o[p] : void 0, obj);
16
18
  }
17
19
  function matchDocument(doc, query) {
18
20
  for (const key of Object.keys(query)) {
@@ -668,9 +670,134 @@ var Collection = class {
668
670
  }
669
671
  };
670
672
 
673
+ // src/core/migration.ts
674
+ import fs3 from "fs";
675
+ import path3 from "path";
676
+ import crypto3 from "crypto";
677
+ var LOCK_FILE = "__migration.lock";
678
+ var HISTORY_FILE = "__migration_history.json";
679
+ var MigrationEngine = class {
680
+ constructor(db) {
681
+ this.db = db;
682
+ }
683
+ migrations = /* @__PURE__ */ new Map();
684
+ /* ------------------------------------------------------------ */
685
+ /* Public API */
686
+ /* ------------------------------------------------------------ */
687
+ register(from, to, fn) {
688
+ const key = `${from}\u2192${to}`;
689
+ if (this.migrations.has(key)) {
690
+ throw new Error(`Duplicate migration: ${key}`);
691
+ }
692
+ this.migrations.set(key, fn);
693
+ }
694
+ async migrate(from, to, fn) {
695
+ this.register(from, to, fn);
696
+ await this.execute();
697
+ }
698
+ async upgradeToLatest() {
699
+ await this.execute();
700
+ }
701
+ /* ------------------------------------------------------------ */
702
+ /* Core Execution Logic */
703
+ /* ------------------------------------------------------------ */
704
+ async execute() {
705
+ let current = this.db.getSchemaVersion();
706
+ while (true) {
707
+ const next = this.findNext(current);
708
+ if (!next) break;
709
+ const fn = this.migrations.get(`${current}\u2192${next}`);
710
+ await this.runMigration(current, next, fn);
711
+ current = next;
712
+ }
713
+ }
714
+ findNext(current) {
715
+ for (const key of this.migrations.keys()) {
716
+ const [from, to] = key.split("\u2192");
717
+ if (from === current) return to;
718
+ }
719
+ return null;
720
+ }
721
+ /* ------------------------------------------------------------ */
722
+ /* Atomic Migration Execution */
723
+ /* ------------------------------------------------------------ */
724
+ async runMigration(from, to, fn) {
725
+ const current = this.db.getSchemaVersion();
726
+ if (current !== from) {
727
+ throw new Error(
728
+ `Schema mismatch: DB=${current}, expected=${from}`
729
+ );
730
+ }
731
+ const lockPath = path3.join(this.db.basePath, LOCK_FILE);
732
+ if (fs3.existsSync(lockPath)) {
733
+ throw new Error(
734
+ "Previous migration interrupted. Resolve manually before continuing."
735
+ );
736
+ }
737
+ this.acquireLock(lockPath);
738
+ try {
739
+ await this.db.transaction(async () => {
740
+ await fn(this.db);
741
+ this.writeHistory(from, to, fn);
742
+ this.db.setSchemaVersion(to);
743
+ });
744
+ } finally {
745
+ this.releaseLock(lockPath);
746
+ }
747
+ }
748
+ /* ------------------------------------------------------------ */
749
+ /* Locking */
750
+ /* ------------------------------------------------------------ */
751
+ acquireLock(file) {
752
+ const token = crypto3.randomBytes(16).toString("hex");
753
+ fs3.writeFileSync(
754
+ file,
755
+ JSON.stringify({
756
+ pid: process.pid,
757
+ token,
758
+ time: Date.now()
759
+ })
760
+ );
761
+ }
762
+ releaseLock(file) {
763
+ if (fs3.existsSync(file)) fs3.unlinkSync(file);
764
+ }
765
+ /* ------------------------------------------------------------ */
766
+ /* Migration History */
767
+ /* ------------------------------------------------------------ */
768
+ historyPath() {
769
+ return path3.join(this.db.basePath, HISTORY_FILE);
770
+ }
771
+ readHistory() {
772
+ if (!fs3.existsSync(this.historyPath())) return [];
773
+ return JSON.parse(fs3.readFileSync(this.historyPath(), "utf8"));
774
+ }
775
+ writeHistory(from, to, fn) {
776
+ const history = this.readHistory();
777
+ history.push({
778
+ from,
779
+ to,
780
+ checksum: this.hash(fn.toString()),
781
+ appliedAt: Date.now()
782
+ });
783
+ fs3.writeFileSync(this.historyPath(), JSON.stringify(history, null, 2));
784
+ }
785
+ hash(data) {
786
+ return crypto3.createHash("sha256").update(data).digest("hex");
787
+ }
788
+ /* ------------------------------------------------------------ */
789
+ /* Diagnostics */
790
+ /* ------------------------------------------------------------ */
791
+ getHistory() {
792
+ return this.readHistory();
793
+ }
794
+ };
795
+
671
796
  // src/core/database.ts
797
+ var exec = promisify(execFile);
672
798
  var META_FILE = "__db_meta.json";
673
799
  var META_VERSION = 1;
800
+ var DEFAULT_SCHEMA_VERSION = "v1";
674
801
  var DBTransactionContext = class {
675
802
  constructor(db, txId) {
676
803
  this.db = db;
@@ -707,37 +834,60 @@ var LioranDB = class _LioranDB {
707
834
  walPath;
708
835
  metaPath;
709
836
  meta;
837
+ migrator;
710
838
  static TX_SEQ = 0;
711
839
  constructor(basePath, dbName, manager) {
712
840
  this.basePath = basePath;
713
841
  this.dbName = dbName;
714
842
  this.manager = manager;
715
843
  this.collections = /* @__PURE__ */ new Map();
716
- this.walPath = path3.join(basePath, "__tx_wal.log");
717
- this.metaPath = path3.join(basePath, META_FILE);
718
- fs3.mkdirSync(basePath, { recursive: true });
844
+ this.walPath = path4.join(basePath, "__tx_wal.log");
845
+ this.metaPath = path4.join(basePath, META_FILE);
846
+ fs4.mkdirSync(basePath, { recursive: true });
719
847
  this.loadMeta();
848
+ this.migrator = new MigrationEngine(this);
720
849
  this.recoverFromWAL().catch(console.error);
721
850
  }
722
851
  /* ------------------------- META ------------------------- */
723
852
  loadMeta() {
724
- if (!fs3.existsSync(this.metaPath)) {
725
- this.meta = { version: META_VERSION, indexes: {} };
853
+ if (!fs4.existsSync(this.metaPath)) {
854
+ this.meta = {
855
+ version: META_VERSION,
856
+ indexes: {},
857
+ schemaVersion: DEFAULT_SCHEMA_VERSION
858
+ };
726
859
  this.saveMeta();
727
860
  return;
728
861
  }
729
- try {
730
- this.meta = JSON.parse(fs3.readFileSync(this.metaPath, "utf8"));
731
- } catch {
732
- throw new Error("Database metadata corrupted");
862
+ this.meta = JSON.parse(fs4.readFileSync(this.metaPath, "utf8"));
863
+ if (!this.meta.schemaVersion) {
864
+ this.meta.schemaVersion = DEFAULT_SCHEMA_VERSION;
865
+ this.saveMeta();
733
866
  }
734
867
  }
735
868
  saveMeta() {
736
- fs3.writeFileSync(this.metaPath, JSON.stringify(this.meta, null, 2));
869
+ fs4.writeFileSync(this.metaPath, JSON.stringify(this.meta, null, 2));
870
+ }
871
+ getSchemaVersion() {
872
+ return this.meta.schemaVersion;
873
+ }
874
+ setSchemaVersion(v) {
875
+ this.meta.schemaVersion = v;
876
+ this.saveMeta();
877
+ }
878
+ /* ------------------------- MIGRATION API ------------------------- */
879
+ migrate(from, to, fn) {
880
+ this.migrator.register(from, to, async (db) => {
881
+ await fn(db);
882
+ db.setSchemaVersion(to);
883
+ });
884
+ }
885
+ async applyMigrations(targetVersion) {
886
+ await this.migrator.upgradeToLatest();
737
887
  }
738
888
  /* ------------------------- WAL ------------------------- */
739
889
  async writeWAL(entries) {
740
- const fd = await fs3.promises.open(this.walPath, "a");
890
+ const fd = await fs4.promises.open(this.walPath, "a");
741
891
  for (const e of entries) {
742
892
  await fd.write(JSON.stringify(e) + "\n");
743
893
  }
@@ -746,28 +896,24 @@ var LioranDB = class _LioranDB {
746
896
  }
747
897
  async clearWAL() {
748
898
  try {
749
- await fs3.promises.unlink(this.walPath);
899
+ await fs4.promises.unlink(this.walPath);
750
900
  } catch {
751
901
  }
752
902
  }
753
903
  async recoverFromWAL() {
754
- if (!fs3.existsSync(this.walPath)) return;
755
- const raw = await fs3.promises.readFile(this.walPath, "utf8");
904
+ if (!fs4.existsSync(this.walPath)) return;
905
+ const raw = await fs4.promises.readFile(this.walPath, "utf8");
756
906
  const committed = /* @__PURE__ */ new Set();
757
907
  const applied = /* @__PURE__ */ new Set();
758
908
  const ops = /* @__PURE__ */ new Map();
759
909
  for (const line of raw.split("\n")) {
760
910
  if (!line.trim()) continue;
761
- try {
762
- const entry = JSON.parse(line);
763
- if ("commit" in entry) committed.add(entry.tx);
764
- else if ("applied" in entry) applied.add(entry.tx);
765
- else {
766
- if (!ops.has(entry.tx)) ops.set(entry.tx, []);
767
- ops.get(entry.tx).push(entry);
768
- }
769
- } catch {
770
- break;
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);
771
917
  }
772
918
  }
773
919
  for (const tx of committed) {
@@ -790,8 +936,8 @@ var LioranDB = class _LioranDB {
790
936
  if (schema) col2.setSchema(schema);
791
937
  return col2;
792
938
  }
793
- const colPath = path3.join(this.basePath, name);
794
- fs3.mkdirSync(colPath, { recursive: true });
939
+ const colPath = path4.join(this.basePath, name);
940
+ fs4.mkdirSync(colPath, { recursive: true });
795
941
  const col = new Collection(colPath, schema);
796
942
  const metas = this.meta.indexes[name] ?? [];
797
943
  for (const m of metas) {
@@ -806,9 +952,15 @@ var LioranDB = class _LioranDB {
806
952
  const existing = this.meta.indexes[collection]?.find((i) => i.field === field);
807
953
  if (existing) return;
808
954
  const index = new Index(col.dir, field, options);
809
- for await (const [, enc] of col.db.iterator()) {
810
- const doc = JSON.parse(Buffer.from(enc, "base64").subarray(32).toString("utf8"));
811
- await index.insert(doc);
955
+ for await (const [key, enc] of col.db.iterator()) {
956
+ if (!enc) continue;
957
+ try {
958
+ const doc = decryptData2(enc);
959
+ await index.insert(doc);
960
+ } catch (err) {
961
+ const errorMessage = err instanceof Error ? err.message : String(err);
962
+ console.warn(`Could not decrypt document ${key} during index build: ${errorMessage}`);
963
+ }
812
964
  }
813
965
  col.registerIndex(index);
814
966
  if (!this.meta.indexes[collection]) {
@@ -817,18 +969,12 @@ var LioranDB = class _LioranDB {
817
969
  this.meta.indexes[collection].push({ field, options });
818
970
  this.saveMeta();
819
971
  }
820
- /* ---------------------- COMPACTION ORCHESTRATOR ---------------------- */
821
- /**
822
- * Compact single collection safely (WAL + TX safe)
823
- */
972
+ /* ------------------------- COMPACTION ------------------------- */
824
973
  async compactCollection(name) {
825
- const col = this.collection(name);
826
974
  await this.clearWAL();
975
+ const col = this.collection(name);
827
976
  await col.compact();
828
977
  }
829
- /**
830
- * Compact entire database safely
831
- */
832
978
  async compactAll() {
833
979
  await this.clearWAL();
834
980
  for (const name of this.collections.keys()) {
@@ -854,18 +1000,21 @@ var LioranDB = class _LioranDB {
854
1000
  this.collections.clear();
855
1001
  }
856
1002
  };
1003
+ function decryptData2(enc) {
1004
+ throw new Error("Function not implemented.");
1005
+ }
857
1006
 
858
1007
  // src/utils/rootpath.ts
859
1008
  import os2 from "os";
860
- import path4 from "path";
861
- import fs4 from "fs";
1009
+ import path5 from "path";
1010
+ import fs5 from "fs";
862
1011
  function getDefaultRootPath() {
863
1012
  let dbPath = process.env.LIORANDB_PATH;
864
1013
  if (!dbPath) {
865
1014
  const homeDir = os2.homedir();
866
- dbPath = path4.join(homeDir, "LioranDB", "db");
867
- if (!fs4.existsSync(dbPath)) {
868
- fs4.mkdirSync(dbPath, { recursive: true });
1015
+ dbPath = path5.join(homeDir, "LioranDB", "db");
1016
+ if (!fs5.existsSync(dbPath)) {
1017
+ fs5.mkdirSync(dbPath, { recursive: true });
869
1018
  }
870
1019
  process.env.LIORANDB_PATH = dbPath;
871
1020
  }
@@ -880,24 +1029,24 @@ import net from "net";
880
1029
 
881
1030
  // src/ipc/socketPath.ts
882
1031
  import os3 from "os";
883
- import path5 from "path";
1032
+ import path6 from "path";
884
1033
  function getIPCSocketPath(rootPath) {
885
1034
  if (os3.platform() === "win32") {
886
1035
  return `\\\\.\\pipe\\liorandb_${rootPath.replace(/[:\\\/]/g, "_")}`;
887
1036
  }
888
- return path5.join(rootPath, ".lioran.sock");
1037
+ return path6.join(rootPath, ".lioran.sock");
889
1038
  }
890
1039
 
891
1040
  // src/ipc/client.ts
892
1041
  function delay(ms) {
893
1042
  return new Promise((r) => setTimeout(r, ms));
894
1043
  }
895
- async function connectWithRetry(path7) {
1044
+ async function connectWithRetry(path8) {
896
1045
  let attempt = 0;
897
1046
  while (true) {
898
1047
  try {
899
1048
  return await new Promise((resolve, reject) => {
900
- const socket = net.connect(path7, () => resolve(socket));
1049
+ const socket = net.connect(path8, () => resolve(socket));
901
1050
  socket.once("error", reject);
902
1051
  });
903
1052
  } catch (err) {
@@ -979,6 +1128,13 @@ var DBQueue = class {
979
1128
  compactAll() {
980
1129
  return this.exec("compact:all", {});
981
1130
  }
1131
+ /* ----------------------------- SNAPSHOT API ----------------------------- */
1132
+ snapshot(path8) {
1133
+ return this.exec("snapshot", { path: path8 });
1134
+ }
1135
+ restore(path8) {
1136
+ return this.exec("restore", { path: path8 });
1137
+ }
982
1138
  /* ------------------------------ SHUTDOWN ------------------------------ */
983
1139
  async shutdown() {
984
1140
  try {
@@ -992,7 +1148,7 @@ var dbQueue = new DBQueue();
992
1148
 
993
1149
  // src/ipc/server.ts
994
1150
  import net2 from "net";
995
- import fs5 from "fs";
1151
+ import fs6 from "fs";
996
1152
  var IPCServer = class {
997
1153
  server;
998
1154
  manager;
@@ -1003,7 +1159,7 @@ var IPCServer = class {
1003
1159
  }
1004
1160
  start() {
1005
1161
  if (!this.socketPath.startsWith("\\\\.\\")) {
1006
- if (fs5.existsSync(this.socketPath)) fs5.unlinkSync(this.socketPath);
1162
+ if (fs6.existsSync(this.socketPath)) fs6.unlinkSync(this.socketPath);
1007
1163
  }
1008
1164
  this.server = net2.createServer((socket) => {
1009
1165
  let buffer = "";
@@ -1016,7 +1172,7 @@ var IPCServer = class {
1016
1172
  try {
1017
1173
  const msg = JSON.parse(raw);
1018
1174
  await this.handleMessage(socket, msg);
1019
- } catch (err) {
1175
+ } catch {
1020
1176
  socket.write(JSON.stringify({
1021
1177
  id: null,
1022
1178
  ok: false,
@@ -1059,12 +1215,27 @@ var IPCServer = class {
1059
1215
  case "compact:db": {
1060
1216
  const { db } = args;
1061
1217
  const database = await this.manager.db(db);
1062
- await database.compact();
1218
+ await database.compactAll();
1063
1219
  result = true;
1064
1220
  break;
1065
1221
  }
1066
1222
  case "compact:all": {
1067
- await this.manager.compactAll();
1223
+ for (const db of this.manager.openDBs.values()) {
1224
+ await db.compactAll();
1225
+ }
1226
+ result = true;
1227
+ break;
1228
+ }
1229
+ /* ---------------- SNAPSHOT ---------------- */
1230
+ case "snapshot": {
1231
+ const { path: snapshotPath } = args;
1232
+ await this.manager.snapshot(snapshotPath);
1233
+ result = true;
1234
+ break;
1235
+ }
1236
+ case "restore": {
1237
+ const { path: snapshotPath } = args;
1238
+ await this.manager.restore(snapshotPath);
1068
1239
  result = true;
1069
1240
  break;
1070
1241
  }
@@ -1090,7 +1261,7 @@ var IPCServer = class {
1090
1261
  if (this.server) this.server.close();
1091
1262
  if (!this.socketPath.startsWith("\\\\.\\")) {
1092
1263
  try {
1093
- fs5.unlinkSync(this.socketPath);
1264
+ fs6.unlinkSync(this.socketPath);
1094
1265
  } catch {
1095
1266
  }
1096
1267
  }
@@ -1108,8 +1279,8 @@ var LioranManager = class {
1108
1279
  constructor(options = {}) {
1109
1280
  const { rootPath, encryptionKey } = options;
1110
1281
  this.rootPath = rootPath || getDefaultRootPath();
1111
- if (!fs6.existsSync(this.rootPath)) {
1112
- fs6.mkdirSync(this.rootPath, { recursive: true });
1282
+ if (!fs7.existsSync(this.rootPath)) {
1283
+ fs7.mkdirSync(this.rootPath, { recursive: true });
1113
1284
  }
1114
1285
  if (encryptionKey) {
1115
1286
  setEncryptionKey(encryptionKey);
@@ -1122,6 +1293,7 @@ var LioranManager = class {
1122
1293
  this._registerShutdownHooks();
1123
1294
  }
1124
1295
  }
1296
+ /* ---------------- LOCK MANAGEMENT ---------------- */
1125
1297
  isProcessAlive(pid) {
1126
1298
  try {
1127
1299
  process2.kill(pid, 0);
@@ -1131,18 +1303,18 @@ var LioranManager = class {
1131
1303
  }
1132
1304
  }
1133
1305
  tryAcquireLock() {
1134
- const lockPath = path6.join(this.rootPath, ".lioran.lock");
1306
+ const lockPath = path7.join(this.rootPath, ".lioran.lock");
1135
1307
  try {
1136
- this.lockFd = fs6.openSync(lockPath, "wx");
1137
- fs6.writeSync(this.lockFd, String(process2.pid));
1308
+ this.lockFd = fs7.openSync(lockPath, "wx");
1309
+ fs7.writeSync(this.lockFd, String(process2.pid));
1138
1310
  return true;
1139
1311
  } catch {
1140
1312
  try {
1141
- const pid = Number(fs6.readFileSync(lockPath, "utf8"));
1313
+ const pid = Number(fs7.readFileSync(lockPath, "utf8"));
1142
1314
  if (!this.isProcessAlive(pid)) {
1143
- fs6.unlinkSync(lockPath);
1144
- this.lockFd = fs6.openSync(lockPath, "wx");
1145
- fs6.writeSync(this.lockFd, String(process2.pid));
1315
+ fs7.unlinkSync(lockPath);
1316
+ this.lockFd = fs7.openSync(lockPath, "wx");
1317
+ fs7.writeSync(this.lockFd, String(process2.pid));
1146
1318
  return true;
1147
1319
  }
1148
1320
  } catch {
@@ -1150,6 +1322,7 @@ var LioranManager = class {
1150
1322
  return false;
1151
1323
  }
1152
1324
  }
1325
+ /* ---------------- DB OPEN ---------------- */
1153
1326
  async db(name) {
1154
1327
  if (this.mode === "client" /* CLIENT */) {
1155
1328
  await dbQueue.exec("db", { db: name });
@@ -1162,12 +1335,57 @@ var LioranManager = class {
1162
1335
  if (this.openDBs.has(name)) {
1163
1336
  return this.openDBs.get(name);
1164
1337
  }
1165
- const dbPath = path6.join(this.rootPath, name);
1166
- await fs6.promises.mkdir(dbPath, { recursive: true });
1338
+ const dbPath = path7.join(this.rootPath, name);
1339
+ await fs7.promises.mkdir(dbPath, { recursive: true });
1167
1340
  const db = new LioranDB(dbPath, name, this);
1168
1341
  this.openDBs.set(name, db);
1169
1342
  return db;
1170
1343
  }
1344
+ /* ---------------- SNAPSHOT ORCHESTRATION ---------------- */
1345
+ /**
1346
+ * Create TAR snapshot of full DB directory
1347
+ */
1348
+ async snapshot(snapshotPath) {
1349
+ if (this.mode === "client" /* CLIENT */) {
1350
+ return dbQueue.exec("snapshot", { path: snapshotPath });
1351
+ }
1352
+ for (const db of this.openDBs.values()) {
1353
+ for (const col of db.collections.values()) {
1354
+ try {
1355
+ await col.db.close();
1356
+ } catch {
1357
+ }
1358
+ }
1359
+ }
1360
+ fs7.mkdirSync(path7.dirname(snapshotPath), { recursive: true });
1361
+ const tar = await import("tar");
1362
+ await tar.c({
1363
+ gzip: true,
1364
+ file: snapshotPath,
1365
+ cwd: this.rootPath,
1366
+ portable: true
1367
+ }, ["./"]);
1368
+ return true;
1369
+ }
1370
+ /**
1371
+ * Restore TAR snapshot safely
1372
+ */
1373
+ async restore(snapshotPath) {
1374
+ if (this.mode === "client" /* CLIENT */) {
1375
+ return dbQueue.exec("restore", { path: snapshotPath });
1376
+ }
1377
+ await this.closeAll();
1378
+ fs7.rmSync(this.rootPath, { recursive: true, force: true });
1379
+ fs7.mkdirSync(this.rootPath, { recursive: true });
1380
+ const tar = await import("tar");
1381
+ await tar.x({
1382
+ file: snapshotPath,
1383
+ cwd: this.rootPath
1384
+ });
1385
+ console.log("Restore completed. Restart required.");
1386
+ process2.exit(0);
1387
+ }
1388
+ /* ---------------- SHUTDOWN ---------------- */
1171
1389
  async closeAll() {
1172
1390
  if (this.closed) return;
1173
1391
  this.closed = true;
@@ -1183,8 +1401,8 @@ var LioranManager = class {
1183
1401
  }
1184
1402
  this.openDBs.clear();
1185
1403
  try {
1186
- if (this.lockFd) fs6.closeSync(this.lockFd);
1187
- fs6.unlinkSync(path6.join(this.rootPath, ".lioran.lock"));
1404
+ if (this.lockFd) fs7.closeSync(this.lockFd);
1405
+ fs7.unlinkSync(path7.join(this.rootPath, ".lioran.lock"));
1188
1406
  } catch {
1189
1407
  }
1190
1408
  await this.ipcServer?.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liorandb/core",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "description": "LioranDB Core Module – Lightweight, local-first, peer-to-peer database management for Node.js.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -28,7 +28,10 @@
28
28
  },
29
29
  "homepage": "https://github.com/LioranGroupOfficial/Liorandb#readme",
30
30
  "dependencies": {
31
+ "@types/tar-stream": "^3.1.4",
31
32
  "classic-level": "^3.0.0",
33
+ "tar": "^7.5.9",
34
+ "tar-stream": "^3.1.7",
32
35
  "uuid": "^13.0.0",
33
36
  "zod": "^4.3.6"
34
37
  },