@liorandb/core 1.0.15 → 1.0.16
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 +8 -6
- package/dist/index.js +119 -26
- package/package.json +4 -1
- package/src/LioranManager.ts +67 -8
- package/src/core/database.ts +62 -33
- package/src/ipc/index.ts +20 -0
- package/src/ipc/queue.ts +17 -1
- package/src/ipc/server.ts +21 -3
- package/src/utils/tar.ts +89 -0
package/dist/index.d.ts
CHANGED
|
@@ -102,13 +102,7 @@ declare class LioranDB {
|
|
|
102
102
|
applyTransaction(ops: TXOp[]): Promise<void>;
|
|
103
103
|
collection<T = any>(name: string, schema?: ZodSchema<T>): Collection<T>;
|
|
104
104
|
createIndex(collection: string, field: string, options?: IndexOptions): Promise<void>;
|
|
105
|
-
/**
|
|
106
|
-
* Compact single collection safely (WAL + TX safe)
|
|
107
|
-
*/
|
|
108
105
|
compactCollection(name: string): Promise<void>;
|
|
109
|
-
/**
|
|
110
|
-
* Compact entire database safely
|
|
111
|
-
*/
|
|
112
106
|
compactAll(): Promise<void>;
|
|
113
107
|
transaction<T>(fn: (tx: DBTransactionContext) => Promise<T>): Promise<T>;
|
|
114
108
|
close(): Promise<void>;
|
|
@@ -130,6 +124,14 @@ declare class LioranManager {
|
|
|
130
124
|
private tryAcquireLock;
|
|
131
125
|
db(name: string): Promise<LioranDB>;
|
|
132
126
|
openDatabase(name: string): Promise<LioranDB>;
|
|
127
|
+
/**
|
|
128
|
+
* Create TAR snapshot of full DB directory
|
|
129
|
+
*/
|
|
130
|
+
snapshot(snapshotPath: string): Promise<unknown>;
|
|
131
|
+
/**
|
|
132
|
+
* Restore TAR snapshot safely
|
|
133
|
+
*/
|
|
134
|
+
restore(snapshotPath: string): Promise<unknown>;
|
|
133
135
|
closeAll(): Promise<void>;
|
|
134
136
|
close(): Promise<void>;
|
|
135
137
|
private _registerShutdownHooks;
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,8 @@ import process2 from "process";
|
|
|
6
6
|
// src/core/database.ts
|
|
7
7
|
import path3 from "path";
|
|
8
8
|
import fs3 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";
|
|
@@ -669,6 +671,7 @@ var Collection = class {
|
|
|
669
671
|
};
|
|
670
672
|
|
|
671
673
|
// src/core/database.ts
|
|
674
|
+
var exec = promisify(execFile);
|
|
672
675
|
var META_FILE = "__db_meta.json";
|
|
673
676
|
var META_VERSION = 1;
|
|
674
677
|
var DBTransactionContext = class {
|
|
@@ -726,11 +729,7 @@ var LioranDB = class _LioranDB {
|
|
|
726
729
|
this.saveMeta();
|
|
727
730
|
return;
|
|
728
731
|
}
|
|
729
|
-
|
|
730
|
-
this.meta = JSON.parse(fs3.readFileSync(this.metaPath, "utf8"));
|
|
731
|
-
} catch {
|
|
732
|
-
throw new Error("Database metadata corrupted");
|
|
733
|
-
}
|
|
732
|
+
this.meta = JSON.parse(fs3.readFileSync(this.metaPath, "utf8"));
|
|
734
733
|
}
|
|
735
734
|
saveMeta() {
|
|
736
735
|
fs3.writeFileSync(this.metaPath, JSON.stringify(this.meta, null, 2));
|
|
@@ -758,16 +757,12 @@ var LioranDB = class _LioranDB {
|
|
|
758
757
|
const ops = /* @__PURE__ */ new Map();
|
|
759
758
|
for (const line of raw.split("\n")) {
|
|
760
759
|
if (!line.trim()) continue;
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
ops.get(entry.tx).push(entry);
|
|
768
|
-
}
|
|
769
|
-
} catch {
|
|
770
|
-
break;
|
|
760
|
+
const entry = JSON.parse(line);
|
|
761
|
+
if ("commit" in entry) committed.add(entry.tx);
|
|
762
|
+
else if ("applied" in entry) applied.add(entry.tx);
|
|
763
|
+
else {
|
|
764
|
+
if (!ops.has(entry.tx)) ops.set(entry.tx, []);
|
|
765
|
+
ops.get(entry.tx).push(entry);
|
|
771
766
|
}
|
|
772
767
|
}
|
|
773
768
|
for (const tx of committed) {
|
|
@@ -817,18 +812,47 @@ var LioranDB = class _LioranDB {
|
|
|
817
812
|
this.meta.indexes[collection].push({ field, options });
|
|
818
813
|
this.saveMeta();
|
|
819
814
|
}
|
|
820
|
-
/*
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
815
|
+
/* ------------------------- SNAPSHOT ENGINE ------------------------- */
|
|
816
|
+
// async snapshot(snapshotPath: string) {
|
|
817
|
+
// await this.clearWAL();
|
|
818
|
+
// for (const col of this.collections.values()) {
|
|
819
|
+
// await col.close();
|
|
820
|
+
// }
|
|
821
|
+
// fs.mkdirSync(path.dirname(snapshotPath), { recursive: true });
|
|
822
|
+
// await exec("tar", [
|
|
823
|
+
// "-czf",
|
|
824
|
+
// snapshotPath,
|
|
825
|
+
// "-C",
|
|
826
|
+
// this.basePath,
|
|
827
|
+
// "."
|
|
828
|
+
// ]);
|
|
829
|
+
// }
|
|
830
|
+
// async restore(snapshotPath: string) {
|
|
831
|
+
// if (!fs.existsSync(snapshotPath)) {
|
|
832
|
+
// throw new Error("Snapshot file does not exist");
|
|
833
|
+
// }
|
|
834
|
+
// await this.close();
|
|
835
|
+
// const tmp = this.basePath + ".restore";
|
|
836
|
+
// await fs.promises.rm(tmp, { recursive: true, force: true });
|
|
837
|
+
// await fs.promises.mkdir(tmp, { recursive: true });
|
|
838
|
+
// await exec("tar", [
|
|
839
|
+
// "-xzf",
|
|
840
|
+
// snapshotPath,
|
|
841
|
+
// "-C",
|
|
842
|
+
// tmp
|
|
843
|
+
// ]);
|
|
844
|
+
// await fs.promises.rm(this.basePath, { recursive: true, force: true });
|
|
845
|
+
// await fs.promises.rename(tmp, this.basePath);
|
|
846
|
+
// this.collections.clear();
|
|
847
|
+
// this.loadMeta();
|
|
848
|
+
// await this.recoverFromWAL();
|
|
849
|
+
// }
|
|
850
|
+
/* ------------------------- COMPACTION ------------------------- */
|
|
824
851
|
async compactCollection(name) {
|
|
825
|
-
const col = this.collection(name);
|
|
826
852
|
await this.clearWAL();
|
|
853
|
+
const col = this.collection(name);
|
|
827
854
|
await col.compact();
|
|
828
855
|
}
|
|
829
|
-
/**
|
|
830
|
-
* Compact entire database safely
|
|
831
|
-
*/
|
|
832
856
|
async compactAll() {
|
|
833
857
|
await this.clearWAL();
|
|
834
858
|
for (const name of this.collections.keys()) {
|
|
@@ -979,6 +1003,13 @@ var DBQueue = class {
|
|
|
979
1003
|
compactAll() {
|
|
980
1004
|
return this.exec("compact:all", {});
|
|
981
1005
|
}
|
|
1006
|
+
/* ----------------------------- SNAPSHOT API ----------------------------- */
|
|
1007
|
+
snapshot(path7) {
|
|
1008
|
+
return this.exec("snapshot", { path: path7 });
|
|
1009
|
+
}
|
|
1010
|
+
restore(path7) {
|
|
1011
|
+
return this.exec("restore", { path: path7 });
|
|
1012
|
+
}
|
|
982
1013
|
/* ------------------------------ SHUTDOWN ------------------------------ */
|
|
983
1014
|
async shutdown() {
|
|
984
1015
|
try {
|
|
@@ -1016,7 +1047,7 @@ var IPCServer = class {
|
|
|
1016
1047
|
try {
|
|
1017
1048
|
const msg = JSON.parse(raw);
|
|
1018
1049
|
await this.handleMessage(socket, msg);
|
|
1019
|
-
} catch
|
|
1050
|
+
} catch {
|
|
1020
1051
|
socket.write(JSON.stringify({
|
|
1021
1052
|
id: null,
|
|
1022
1053
|
ok: false,
|
|
@@ -1059,12 +1090,27 @@ var IPCServer = class {
|
|
|
1059
1090
|
case "compact:db": {
|
|
1060
1091
|
const { db } = args;
|
|
1061
1092
|
const database = await this.manager.db(db);
|
|
1062
|
-
await database.
|
|
1093
|
+
await database.compactAll();
|
|
1063
1094
|
result = true;
|
|
1064
1095
|
break;
|
|
1065
1096
|
}
|
|
1066
1097
|
case "compact:all": {
|
|
1067
|
-
|
|
1098
|
+
for (const db of this.manager.openDBs.values()) {
|
|
1099
|
+
await db.compactAll();
|
|
1100
|
+
}
|
|
1101
|
+
result = true;
|
|
1102
|
+
break;
|
|
1103
|
+
}
|
|
1104
|
+
/* ---------------- SNAPSHOT ---------------- */
|
|
1105
|
+
case "snapshot": {
|
|
1106
|
+
const { path: snapshotPath } = args;
|
|
1107
|
+
await this.manager.snapshot(snapshotPath);
|
|
1108
|
+
result = true;
|
|
1109
|
+
break;
|
|
1110
|
+
}
|
|
1111
|
+
case "restore": {
|
|
1112
|
+
const { path: snapshotPath } = args;
|
|
1113
|
+
await this.manager.restore(snapshotPath);
|
|
1068
1114
|
result = true;
|
|
1069
1115
|
break;
|
|
1070
1116
|
}
|
|
@@ -1122,6 +1168,7 @@ var LioranManager = class {
|
|
|
1122
1168
|
this._registerShutdownHooks();
|
|
1123
1169
|
}
|
|
1124
1170
|
}
|
|
1171
|
+
/* ---------------- LOCK MANAGEMENT ---------------- */
|
|
1125
1172
|
isProcessAlive(pid) {
|
|
1126
1173
|
try {
|
|
1127
1174
|
process2.kill(pid, 0);
|
|
@@ -1150,6 +1197,7 @@ var LioranManager = class {
|
|
|
1150
1197
|
return false;
|
|
1151
1198
|
}
|
|
1152
1199
|
}
|
|
1200
|
+
/* ---------------- DB OPEN ---------------- */
|
|
1153
1201
|
async db(name) {
|
|
1154
1202
|
if (this.mode === "client" /* CLIENT */) {
|
|
1155
1203
|
await dbQueue.exec("db", { db: name });
|
|
@@ -1168,6 +1216,51 @@ var LioranManager = class {
|
|
|
1168
1216
|
this.openDBs.set(name, db);
|
|
1169
1217
|
return db;
|
|
1170
1218
|
}
|
|
1219
|
+
/* ---------------- SNAPSHOT ORCHESTRATION ---------------- */
|
|
1220
|
+
/**
|
|
1221
|
+
* Create TAR snapshot of full DB directory
|
|
1222
|
+
*/
|
|
1223
|
+
async snapshot(snapshotPath) {
|
|
1224
|
+
if (this.mode === "client" /* CLIENT */) {
|
|
1225
|
+
return dbQueue.exec("snapshot", { path: snapshotPath });
|
|
1226
|
+
}
|
|
1227
|
+
for (const db of this.openDBs.values()) {
|
|
1228
|
+
for (const col of db.collections.values()) {
|
|
1229
|
+
try {
|
|
1230
|
+
await col.db.close();
|
|
1231
|
+
} catch {
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
fs6.mkdirSync(path6.dirname(snapshotPath), { recursive: true });
|
|
1236
|
+
const tar = await import("tar");
|
|
1237
|
+
await tar.c({
|
|
1238
|
+
gzip: true,
|
|
1239
|
+
file: snapshotPath,
|
|
1240
|
+
cwd: this.rootPath,
|
|
1241
|
+
portable: true
|
|
1242
|
+
}, ["./"]);
|
|
1243
|
+
return true;
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Restore TAR snapshot safely
|
|
1247
|
+
*/
|
|
1248
|
+
async restore(snapshotPath) {
|
|
1249
|
+
if (this.mode === "client" /* CLIENT */) {
|
|
1250
|
+
return dbQueue.exec("restore", { path: snapshotPath });
|
|
1251
|
+
}
|
|
1252
|
+
await this.closeAll();
|
|
1253
|
+
fs6.rmSync(this.rootPath, { recursive: true, force: true });
|
|
1254
|
+
fs6.mkdirSync(this.rootPath, { recursive: true });
|
|
1255
|
+
const tar = await import("tar");
|
|
1256
|
+
await tar.x({
|
|
1257
|
+
file: snapshotPath,
|
|
1258
|
+
cwd: this.rootPath
|
|
1259
|
+
});
|
|
1260
|
+
console.log("Restore completed. Restart required.");
|
|
1261
|
+
process2.exit(0);
|
|
1262
|
+
}
|
|
1263
|
+
/* ---------------- SHUTDOWN ---------------- */
|
|
1171
1264
|
async closeAll() {
|
|
1172
1265
|
if (this.closed) return;
|
|
1173
1266
|
this.closed = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liorandb/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
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
|
},
|
package/src/LioranManager.ts
CHANGED
|
@@ -51,6 +51,8 @@ export class LioranManager {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/* ---------------- LOCK MANAGEMENT ---------------- */
|
|
55
|
+
|
|
54
56
|
private isProcessAlive(pid: number): boolean {
|
|
55
57
|
try {
|
|
56
58
|
process.kill(pid, 0);
|
|
@@ -68,7 +70,6 @@ export class LioranManager {
|
|
|
68
70
|
fs.writeSync(this.lockFd, String(process.pid));
|
|
69
71
|
return true;
|
|
70
72
|
} catch {
|
|
71
|
-
// Possible stale lock → validate PID
|
|
72
73
|
try {
|
|
73
74
|
const pid = Number(fs.readFileSync(lockPath, "utf8"));
|
|
74
75
|
if (!this.isProcessAlive(pid)) {
|
|
@@ -77,18 +78,18 @@ export class LioranManager {
|
|
|
77
78
|
fs.writeSync(this.lockFd, String(process.pid));
|
|
78
79
|
return true;
|
|
79
80
|
}
|
|
80
|
-
} catch {}
|
|
81
|
-
|
|
81
|
+
} catch { }
|
|
82
82
|
return false;
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/* ---------------- DB OPEN ---------------- */
|
|
87
|
+
|
|
86
88
|
async db(name: string): Promise<LioranDB> {
|
|
87
89
|
if (this.mode === ProcessMode.CLIENT) {
|
|
88
90
|
await dbQueue.exec("db", { db: name });
|
|
89
91
|
return new IPCDatabase(name) as any;
|
|
90
92
|
}
|
|
91
|
-
|
|
92
93
|
return this.openDatabase(name);
|
|
93
94
|
}
|
|
94
95
|
|
|
@@ -107,6 +108,64 @@ export class LioranManager {
|
|
|
107
108
|
return db;
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
/* ---------------- SNAPSHOT ORCHESTRATION ---------------- */
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Create TAR snapshot of full DB directory
|
|
115
|
+
*/
|
|
116
|
+
async snapshot(snapshotPath: string) {
|
|
117
|
+
if (this.mode === ProcessMode.CLIENT) {
|
|
118
|
+
return dbQueue.exec("snapshot", { path: snapshotPath });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Flush all DBs safely
|
|
122
|
+
for (const db of this.openDBs.values()) {
|
|
123
|
+
for (const col of db.collections.values()) {
|
|
124
|
+
try { await col.db.close(); } catch { }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Ensure backup directory exists
|
|
129
|
+
fs.mkdirSync(path.dirname(snapshotPath), { recursive: true });
|
|
130
|
+
|
|
131
|
+
const tar = await import("tar");
|
|
132
|
+
|
|
133
|
+
await tar.c({
|
|
134
|
+
gzip: true,
|
|
135
|
+
file: snapshotPath,
|
|
136
|
+
cwd: this.rootPath,
|
|
137
|
+
portable: true
|
|
138
|
+
}, ["./"]);
|
|
139
|
+
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Restore TAR snapshot safely
|
|
145
|
+
*/
|
|
146
|
+
async restore(snapshotPath: string) {
|
|
147
|
+
if (this.mode === ProcessMode.CLIENT) {
|
|
148
|
+
return dbQueue.exec("restore", { path: snapshotPath });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
await this.closeAll();
|
|
152
|
+
|
|
153
|
+
fs.rmSync(this.rootPath, { recursive: true, force: true });
|
|
154
|
+
fs.mkdirSync(this.rootPath, { recursive: true });
|
|
155
|
+
|
|
156
|
+
const tar = await import("tar");
|
|
157
|
+
|
|
158
|
+
await tar.x({
|
|
159
|
+
file: snapshotPath,
|
|
160
|
+
cwd: this.rootPath
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
console.log("Restore completed. Restart required.");
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* ---------------- SHUTDOWN ---------------- */
|
|
168
|
+
|
|
110
169
|
async closeAll(): Promise<void> {
|
|
111
170
|
if (this.closed) return;
|
|
112
171
|
this.closed = true;
|
|
@@ -117,7 +176,7 @@ export class LioranManager {
|
|
|
117
176
|
}
|
|
118
177
|
|
|
119
178
|
for (const db of this.openDBs.values()) {
|
|
120
|
-
try { await db.close(); } catch {}
|
|
179
|
+
try { await db.close(); } catch { }
|
|
121
180
|
}
|
|
122
181
|
|
|
123
182
|
this.openDBs.clear();
|
|
@@ -125,7 +184,7 @@ export class LioranManager {
|
|
|
125
184
|
try {
|
|
126
185
|
if (this.lockFd) fs.closeSync(this.lockFd);
|
|
127
186
|
fs.unlinkSync(path.join(this.rootPath, ".lioran.lock"));
|
|
128
|
-
} catch {}
|
|
187
|
+
} catch { }
|
|
129
188
|
|
|
130
189
|
await this.ipcServer?.close();
|
|
131
190
|
}
|
|
@@ -154,7 +213,7 @@ export class LioranManager {
|
|
|
154
213
|
/* ---------------- IPC PROXY DB ---------------- */
|
|
155
214
|
|
|
156
215
|
class IPCDatabase {
|
|
157
|
-
constructor(private name: string) {}
|
|
216
|
+
constructor(private name: string) { }
|
|
158
217
|
|
|
159
218
|
collection(name: string) {
|
|
160
219
|
return new IPCCollection(this.name, name);
|
|
@@ -165,7 +224,7 @@ class IPCCollection {
|
|
|
165
224
|
constructor(
|
|
166
225
|
private db: string,
|
|
167
226
|
private col: string
|
|
168
|
-
) {}
|
|
227
|
+
) { }
|
|
169
228
|
|
|
170
229
|
private call(method: string, params: any[]) {
|
|
171
230
|
return dbQueue.exec("op", {
|
package/src/core/database.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs";
|
|
3
|
+
import { execFile } from "child_process";
|
|
4
|
+
import { promisify } from "util";
|
|
3
5
|
import { Collection } from "./collection.js";
|
|
4
6
|
import { Index, IndexOptions } from "./index.js";
|
|
5
7
|
import type { LioranManager } from "../LioranManager.js";
|
|
6
8
|
import type { ZodSchema } from "zod";
|
|
7
9
|
|
|
10
|
+
const exec = promisify(execFile);
|
|
11
|
+
|
|
8
12
|
/* ----------------------------- TYPES ----------------------------- */
|
|
9
13
|
|
|
10
14
|
type TXOp = { tx: number; col: string; op: string; args: any[] };
|
|
@@ -95,11 +99,7 @@ export class LioranDB {
|
|
|
95
99
|
return;
|
|
96
100
|
}
|
|
97
101
|
|
|
98
|
-
|
|
99
|
-
this.meta = JSON.parse(fs.readFileSync(this.metaPath, "utf8"));
|
|
100
|
-
} catch {
|
|
101
|
-
throw new Error("Database metadata corrupted");
|
|
102
|
-
}
|
|
102
|
+
this.meta = JSON.parse(fs.readFileSync(this.metaPath, "utf8"));
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
private saveMeta() {
|
|
@@ -133,17 +133,13 @@ export class LioranDB {
|
|
|
133
133
|
for (const line of raw.split("\n")) {
|
|
134
134
|
if (!line.trim()) continue;
|
|
135
135
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
ops.get(entry.tx)!.push(entry);
|
|
144
|
-
}
|
|
145
|
-
} catch {
|
|
146
|
-
break;
|
|
136
|
+
const entry: WALEntry = JSON.parse(line);
|
|
137
|
+
|
|
138
|
+
if ("commit" in entry) committed.add(entry.tx);
|
|
139
|
+
else if ("applied" in entry) applied.add(entry.tx);
|
|
140
|
+
else {
|
|
141
|
+
if (!ops.has(entry.tx)) ops.set(entry.tx, []);
|
|
142
|
+
ops.get(entry.tx)!.push(entry);
|
|
147
143
|
}
|
|
148
144
|
}
|
|
149
145
|
|
|
@@ -177,7 +173,6 @@ export class LioranDB {
|
|
|
177
173
|
|
|
178
174
|
const col = new Collection<T>(colPath, schema);
|
|
179
175
|
|
|
180
|
-
// Auto-load indexes
|
|
181
176
|
const metas = this.meta.indexes[name] ?? [];
|
|
182
177
|
for (const m of metas) {
|
|
183
178
|
col.registerIndex(new Index(colPath, m.field, m.options));
|
|
@@ -216,27 +211,63 @@ export class LioranDB {
|
|
|
216
211
|
this.saveMeta();
|
|
217
212
|
}
|
|
218
213
|
|
|
219
|
-
/*
|
|
214
|
+
/* ------------------------- SNAPSHOT ENGINE ------------------------- */
|
|
220
215
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
*/
|
|
224
|
-
async compactCollection(name: string) {
|
|
225
|
-
const col = this.collection(name);
|
|
216
|
+
// async snapshot(snapshotPath: string) {
|
|
217
|
+
// await this.clearWAL();
|
|
226
218
|
|
|
227
|
-
|
|
228
|
-
|
|
219
|
+
// for (const col of this.collections.values()) {
|
|
220
|
+
// await col.close();
|
|
221
|
+
// }
|
|
222
|
+
|
|
223
|
+
// fs.mkdirSync(path.dirname(snapshotPath), { recursive: true });
|
|
224
|
+
|
|
225
|
+
// await exec("tar", [
|
|
226
|
+
// "-czf",
|
|
227
|
+
// snapshotPath,
|
|
228
|
+
// "-C",
|
|
229
|
+
// this.basePath,
|
|
230
|
+
// "."
|
|
231
|
+
// ]);
|
|
232
|
+
// }
|
|
233
|
+
|
|
234
|
+
// async restore(snapshotPath: string) {
|
|
235
|
+
// if (!fs.existsSync(snapshotPath)) {
|
|
236
|
+
// throw new Error("Snapshot file does not exist");
|
|
237
|
+
// }
|
|
238
|
+
|
|
239
|
+
// await this.close();
|
|
240
|
+
|
|
241
|
+
// const tmp = this.basePath + ".restore";
|
|
242
|
+
|
|
243
|
+
// await fs.promises.rm(tmp, { recursive: true, force: true });
|
|
244
|
+
// await fs.promises.mkdir(tmp, { recursive: true });
|
|
229
245
|
|
|
230
|
-
|
|
246
|
+
// await exec("tar", [
|
|
247
|
+
// "-xzf",
|
|
248
|
+
// snapshotPath,
|
|
249
|
+
// "-C",
|
|
250
|
+
// tmp
|
|
251
|
+
// ]);
|
|
252
|
+
|
|
253
|
+
// await fs.promises.rm(this.basePath, { recursive: true, force: true });
|
|
254
|
+
// await fs.promises.rename(tmp, this.basePath);
|
|
255
|
+
|
|
256
|
+
// this.collections.clear();
|
|
257
|
+
// this.loadMeta();
|
|
258
|
+
// await this.recoverFromWAL();
|
|
259
|
+
// }
|
|
260
|
+
|
|
261
|
+
/* ------------------------- COMPACTION ------------------------- */
|
|
262
|
+
|
|
263
|
+
async compactCollection(name: string) {
|
|
264
|
+
await this.clearWAL();
|
|
265
|
+
const col = this.collection(name);
|
|
231
266
|
await col.compact();
|
|
232
267
|
}
|
|
233
268
|
|
|
234
|
-
/**
|
|
235
|
-
* Compact entire database safely
|
|
236
|
-
*/
|
|
237
269
|
async compactAll() {
|
|
238
270
|
await this.clearWAL();
|
|
239
|
-
|
|
240
271
|
for (const name of this.collections.keys()) {
|
|
241
272
|
await this.compactCollection(name);
|
|
242
273
|
}
|
|
@@ -247,10 +278,8 @@ export class LioranDB {
|
|
|
247
278
|
async transaction<T>(fn: (tx: DBTransactionContext) => Promise<T>): Promise<T> {
|
|
248
279
|
const txId = ++LioranDB.TX_SEQ;
|
|
249
280
|
const tx = new DBTransactionContext(this, txId);
|
|
250
|
-
|
|
251
281
|
const result = await fn(tx);
|
|
252
282
|
await tx.commit();
|
|
253
|
-
|
|
254
283
|
return result;
|
|
255
284
|
}
|
|
256
285
|
|
|
@@ -262,4 +291,4 @@ export class LioranDB {
|
|
|
262
291
|
}
|
|
263
292
|
this.collections.clear();
|
|
264
293
|
}
|
|
265
|
-
}
|
|
294
|
+
}
|
package/src/ipc/index.ts
CHANGED
|
@@ -72,6 +72,14 @@ class DBProxy {
|
|
|
72
72
|
compact() {
|
|
73
73
|
return dbQueue.exec("compact:db", { db: this.dbName });
|
|
74
74
|
}
|
|
75
|
+
|
|
76
|
+
snapshot(path: string) {
|
|
77
|
+
return dbQueue.exec("snapshot", { path });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
restore(path: string) {
|
|
81
|
+
return dbQueue.exec("restore", { path });
|
|
82
|
+
}
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
/* -------------------------------- MANAGER PROXY -------------------------------- */
|
|
@@ -85,6 +93,18 @@ class LioranManagerIPC {
|
|
|
85
93
|
compactAll() {
|
|
86
94
|
return dbQueue.exec("compact:all", {});
|
|
87
95
|
}
|
|
96
|
+
|
|
97
|
+
snapshot(path: string) {
|
|
98
|
+
return dbQueue.exec("snapshot", { path });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
restore(path: string) {
|
|
102
|
+
return dbQueue.exec("restore", { path });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
shutdown() {
|
|
106
|
+
return dbQueue.shutdown();
|
|
107
|
+
}
|
|
88
108
|
}
|
|
89
109
|
|
|
90
110
|
export const manager = new LioranManagerIPC();
|
package/src/ipc/queue.ts
CHANGED
|
@@ -10,7 +10,13 @@ export type IPCAction =
|
|
|
10
10
|
| "compact:collection"
|
|
11
11
|
| "compact:db"
|
|
12
12
|
| "compact:all"
|
|
13
|
-
| "shutdown"
|
|
13
|
+
| "shutdown"
|
|
14
|
+
| "open"
|
|
15
|
+
| "close"
|
|
16
|
+
| "minimize"
|
|
17
|
+
| "maximize"
|
|
18
|
+
| "restore"
|
|
19
|
+
| "snapshot";
|
|
14
20
|
|
|
15
21
|
/* -------------------------------- DB QUEUE -------------------------------- */
|
|
16
22
|
|
|
@@ -39,6 +45,16 @@ export class DBQueue {
|
|
|
39
45
|
return this.exec("compact:all", {});
|
|
40
46
|
}
|
|
41
47
|
|
|
48
|
+
/* ----------------------------- SNAPSHOT API ----------------------------- */
|
|
49
|
+
|
|
50
|
+
snapshot(path: string) {
|
|
51
|
+
return this.exec("snapshot", { path });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
restore(path: string) {
|
|
55
|
+
return this.exec("restore", { path });
|
|
56
|
+
}
|
|
57
|
+
|
|
42
58
|
/* ------------------------------ SHUTDOWN ------------------------------ */
|
|
43
59
|
|
|
44
60
|
async shutdown() {
|
package/src/ipc/server.ts
CHANGED
|
@@ -32,7 +32,7 @@ export class IPCServer {
|
|
|
32
32
|
try {
|
|
33
33
|
const msg = JSON.parse(raw);
|
|
34
34
|
await this.handleMessage(socket, msg);
|
|
35
|
-
} catch
|
|
35
|
+
} catch {
|
|
36
36
|
socket.write(JSON.stringify({
|
|
37
37
|
id: null,
|
|
38
38
|
ok: false,
|
|
@@ -85,13 +85,31 @@ export class IPCServer {
|
|
|
85
85
|
case "compact:db": {
|
|
86
86
|
const { db } = args;
|
|
87
87
|
const database = await this.manager.db(db);
|
|
88
|
-
await (database as any).
|
|
88
|
+
await (database as any).compactAll();
|
|
89
89
|
result = true;
|
|
90
90
|
break;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
case "compact:all": {
|
|
94
|
-
|
|
94
|
+
for (const db of this.manager.openDBs.values()) {
|
|
95
|
+
await db.compactAll();
|
|
96
|
+
}
|
|
97
|
+
result = true;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* ---------------- SNAPSHOT ---------------- */
|
|
102
|
+
|
|
103
|
+
case "snapshot": {
|
|
104
|
+
const { path: snapshotPath } = args;
|
|
105
|
+
await this.manager.snapshot(snapshotPath);
|
|
106
|
+
result = true;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
case "restore": {
|
|
111
|
+
const { path: snapshotPath } = args;
|
|
112
|
+
await this.manager.restore(snapshotPath);
|
|
95
113
|
result = true;
|
|
96
114
|
break;
|
|
97
115
|
}
|
package/src/utils/tar.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { pipeline } from "stream/promises";
|
|
4
|
+
import zlib from "zlib";
|
|
5
|
+
import tar from "tar-stream";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create a TAR.GZ archive from a directory
|
|
9
|
+
*/
|
|
10
|
+
export async function createTarGz(
|
|
11
|
+
sourceDir: string,
|
|
12
|
+
outFile: string
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
await fs.promises.mkdir(path.dirname(outFile), { recursive: true });
|
|
15
|
+
|
|
16
|
+
const pack = tar.pack();
|
|
17
|
+
const gzip = zlib.createGzip();
|
|
18
|
+
const out = fs.createWriteStream(outFile + ".tmp");
|
|
19
|
+
|
|
20
|
+
const writeStream = pipeline(pack, gzip, out);
|
|
21
|
+
|
|
22
|
+
async function walk(dir: string) {
|
|
23
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
24
|
+
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
const fullPath = path.join(dir, entry.name);
|
|
27
|
+
const relPath = path.relative(sourceDir, fullPath);
|
|
28
|
+
|
|
29
|
+
if (entry.isDirectory()) {
|
|
30
|
+
await walk(fullPath);
|
|
31
|
+
} else if (entry.isFile()) {
|
|
32
|
+
const stat = await fs.promises.stat(fullPath);
|
|
33
|
+
const stream = fs.createReadStream(fullPath);
|
|
34
|
+
|
|
35
|
+
await new Promise<void>((resolve, reject) => {
|
|
36
|
+
const header = {
|
|
37
|
+
name: relPath,
|
|
38
|
+
size: stat.size,
|
|
39
|
+
mode: stat.mode,
|
|
40
|
+
mtime: stat.mtime
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const entryStream = pack.entry(header, err => {
|
|
44
|
+
if (err) reject(err);
|
|
45
|
+
else resolve();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
stream.pipe(entryStream);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await walk(sourceDir);
|
|
55
|
+
|
|
56
|
+
pack.finalize();
|
|
57
|
+
await writeStream;
|
|
58
|
+
|
|
59
|
+
await fs.promises.rename(outFile + ".tmp", outFile);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Extract TAR.GZ archive into directory
|
|
64
|
+
*/
|
|
65
|
+
export async function extractTarGz(
|
|
66
|
+
archiveFile: string,
|
|
67
|
+
targetDir: string
|
|
68
|
+
): Promise<void> {
|
|
69
|
+
await fs.promises.mkdir(targetDir, { recursive: true });
|
|
70
|
+
|
|
71
|
+
const extract = tar.extract();
|
|
72
|
+
const gunzip = zlib.createGunzip();
|
|
73
|
+
const input = fs.createReadStream(archiveFile);
|
|
74
|
+
|
|
75
|
+
extract.on("entry", async (header, stream, next) => {
|
|
76
|
+
const outPath = path.join(targetDir, header.name);
|
|
77
|
+
|
|
78
|
+
await fs.promises.mkdir(path.dirname(outPath), { recursive: true });
|
|
79
|
+
|
|
80
|
+
const out = fs.createWriteStream(outPath, {
|
|
81
|
+
mode: header.mode
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await pipeline(stream, out);
|
|
85
|
+
next();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await pipeline(input, gunzip, extract);
|
|
89
|
+
}
|