@liorandb/core 1.0.19 → 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.
- package/dist/chunk-2FSI7HX7.js +1689 -0
- package/dist/index.d.ts +24 -11
- package/dist/index.js +5 -1725
- package/dist/queue-YILKSUEI.js +179 -0
- package/package.json +1 -1
- package/src/LioranManager.ts +99 -44
- package/src/core/checkpoint.ts +86 -34
- package/src/core/collection.ts +39 -8
- package/src/core/compaction.ts +53 -38
- package/src/core/database.ts +83 -38
- package/src/core/wal.ts +113 -29
- package/src/ipc/index.ts +41 -19
- package/src/ipc/pool.ts +136 -0
- package/src/ipc/queue.ts +85 -31
- package/src/ipc/worker.ts +72 -0
- package/src/ipc/client.ts +0 -85
- package/src/ipc/server.ts +0 -147
- package/src/ipc/socketPath.ts +0 -10
package/src/core/wal.ts
CHANGED
|
@@ -16,12 +16,11 @@ type StoredRecord = WALRecord & { crc: number };
|
|
|
16
16
|
CONSTANTS
|
|
17
17
|
========================= */
|
|
18
18
|
|
|
19
|
-
const MAX_WAL_SIZE = 16 * 1024 * 1024; //
|
|
19
|
+
const MAX_WAL_SIZE = 16 * 1024 * 1024; // 16MB
|
|
20
20
|
const WAL_DIR = "__wal";
|
|
21
21
|
|
|
22
22
|
/* =========================
|
|
23
|
-
CRC32
|
|
24
|
-
(no dependencies)
|
|
23
|
+
CRC32 (no deps)
|
|
25
24
|
========================= */
|
|
26
25
|
|
|
27
26
|
const CRC32_TABLE = (() => {
|
|
@@ -37,12 +36,11 @@ const CRC32_TABLE = (() => {
|
|
|
37
36
|
})();
|
|
38
37
|
|
|
39
38
|
function crc32(input: string): number {
|
|
40
|
-
let crc =
|
|
39
|
+
let crc = 0xffffffff;
|
|
41
40
|
for (let i = 0; i < input.length; i++) {
|
|
42
|
-
|
|
43
|
-
crc = CRC32_TABLE[(crc ^ byte) & 0xFF] ^ (crc >>> 8);
|
|
41
|
+
crc = CRC32_TABLE[(crc ^ input.charCodeAt(i)) & 0xff] ^ (crc >>> 8);
|
|
44
42
|
}
|
|
45
|
-
return (crc ^
|
|
43
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
46
44
|
}
|
|
47
45
|
|
|
48
46
|
/* =========================
|
|
@@ -54,11 +52,20 @@ export class WALManager {
|
|
|
54
52
|
private currentGen = 1;
|
|
55
53
|
private lsn = 0;
|
|
56
54
|
private fd: fs.promises.FileHandle | null = null;
|
|
55
|
+
private readonlyMode: boolean;
|
|
57
56
|
|
|
58
|
-
constructor(baseDir: string) {
|
|
57
|
+
constructor(baseDir: string, options?: { readonly?: boolean }) {
|
|
59
58
|
this.walDir = path.join(baseDir, WAL_DIR);
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
this.readonlyMode = options?.readonly ?? false;
|
|
60
|
+
|
|
61
|
+
if (!this.readonlyMode) {
|
|
62
|
+
fs.mkdirSync(this.walDir, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (fs.existsSync(this.walDir)) {
|
|
66
|
+
this.currentGen = this.detectLastGeneration();
|
|
67
|
+
this.recoverLSNFromExistingLogs();
|
|
68
|
+
}
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
/* -------------------------
|
|
@@ -80,20 +87,69 @@ export class WALManager {
|
|
|
80
87
|
|
|
81
88
|
for (const f of files) {
|
|
82
89
|
const m = f.match(/^wal-(\d+)\.log$/);
|
|
83
|
-
if (m)
|
|
90
|
+
if (m) {
|
|
91
|
+
const gen = Number(m[1]);
|
|
92
|
+
if (!Number.isNaN(gen)) {
|
|
93
|
+
max = Math.max(max, gen);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
84
96
|
}
|
|
85
97
|
|
|
86
98
|
return max || 1;
|
|
87
99
|
}
|
|
88
100
|
|
|
101
|
+
private recoverLSNFromExistingLogs() {
|
|
102
|
+
const files = this.getSortedWalFiles();
|
|
103
|
+
|
|
104
|
+
for (const file of files) {
|
|
105
|
+
const filePath = path.join(this.walDir, file);
|
|
106
|
+
const lines = fs.readFileSync(filePath, "utf8").split("\n");
|
|
107
|
+
|
|
108
|
+
for (const line of lines) {
|
|
109
|
+
if (!line.trim()) continue;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const parsed: StoredRecord = JSON.parse(line);
|
|
113
|
+
const { crc, ...record } = parsed;
|
|
114
|
+
|
|
115
|
+
if (crc32(JSON.stringify(record)) !== crc) break;
|
|
116
|
+
|
|
117
|
+
this.lsn = Math.max(this.lsn, record.lsn);
|
|
118
|
+
} catch {
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private getSortedWalFiles(): string[] {
|
|
126
|
+
if (!fs.existsSync(this.walDir)) return [];
|
|
127
|
+
|
|
128
|
+
return fs
|
|
129
|
+
.readdirSync(this.walDir)
|
|
130
|
+
.filter(f => /^wal-\d+\.log$/.test(f))
|
|
131
|
+
.sort((a, b) => {
|
|
132
|
+
const ga = Number(a.match(/^wal-(\d+)\.log$/)![1]);
|
|
133
|
+
const gb = Number(b.match(/^wal-(\d+)\.log$/)![1]);
|
|
134
|
+
return ga - gb;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
89
138
|
private async open() {
|
|
139
|
+
if (this.readonlyMode) {
|
|
140
|
+
throw new Error("WAL is in readonly replica mode");
|
|
141
|
+
}
|
|
142
|
+
|
|
90
143
|
if (!this.fd) {
|
|
91
144
|
this.fd = await fs.promises.open(this.walPath(), "a");
|
|
92
145
|
}
|
|
93
146
|
}
|
|
94
147
|
|
|
95
148
|
private async rotate() {
|
|
149
|
+
if (this.readonlyMode) return;
|
|
150
|
+
|
|
96
151
|
if (this.fd) {
|
|
152
|
+
await this.fd.sync();
|
|
97
153
|
await this.fd.close();
|
|
98
154
|
this.fd = null;
|
|
99
155
|
}
|
|
@@ -101,10 +157,14 @@ export class WALManager {
|
|
|
101
157
|
}
|
|
102
158
|
|
|
103
159
|
/* -------------------------
|
|
104
|
-
APPEND
|
|
160
|
+
APPEND (Primary only)
|
|
105
161
|
------------------------- */
|
|
106
162
|
|
|
107
163
|
async append(record: Omit<WALRecord, "lsn">): Promise<number> {
|
|
164
|
+
if (this.readonlyMode) {
|
|
165
|
+
throw new Error("Cannot append WAL in readonly replica mode");
|
|
166
|
+
}
|
|
167
|
+
|
|
108
168
|
await this.open();
|
|
109
169
|
|
|
110
170
|
const full: WALRecord = {
|
|
@@ -113,12 +173,15 @@ export class WALManager {
|
|
|
113
173
|
};
|
|
114
174
|
|
|
115
175
|
const body = JSON.stringify(full);
|
|
176
|
+
|
|
116
177
|
const stored: StoredRecord = {
|
|
117
178
|
...full,
|
|
118
179
|
crc: crc32(body)
|
|
119
180
|
};
|
|
120
181
|
|
|
121
|
-
|
|
182
|
+
const line = JSON.stringify(stored) + "\n";
|
|
183
|
+
|
|
184
|
+
await this.fd!.write(line);
|
|
122
185
|
await this.fd!.sync();
|
|
123
186
|
|
|
124
187
|
const stat = await this.fd!.stat();
|
|
@@ -130,7 +193,7 @@ export class WALManager {
|
|
|
130
193
|
}
|
|
131
194
|
|
|
132
195
|
/* -------------------------
|
|
133
|
-
REPLAY
|
|
196
|
+
REPLAY (Replica allowed)
|
|
134
197
|
------------------------- */
|
|
135
198
|
|
|
136
199
|
async replay(
|
|
@@ -139,55 +202,72 @@ export class WALManager {
|
|
|
139
202
|
): Promise<void> {
|
|
140
203
|
if (!fs.existsSync(this.walDir)) return;
|
|
141
204
|
|
|
142
|
-
const files =
|
|
143
|
-
.readdirSync(this.walDir)
|
|
144
|
-
.filter(f => f.startsWith("wal-"))
|
|
145
|
-
.sort();
|
|
205
|
+
const files = this.getSortedWalFiles();
|
|
146
206
|
|
|
147
207
|
for (const file of files) {
|
|
148
208
|
const filePath = path.join(this.walDir, file);
|
|
149
|
-
|
|
150
|
-
const
|
|
209
|
+
|
|
210
|
+
const fd = fs.openSync(
|
|
211
|
+
filePath,
|
|
212
|
+
this.readonlyMode ? "r" : "r+"
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
216
|
+
const lines = content.split("\n");
|
|
217
|
+
|
|
218
|
+
let validOffset = 0;
|
|
151
219
|
|
|
152
220
|
for (let i = 0; i < lines.length; i++) {
|
|
153
221
|
const line = lines[i];
|
|
154
|
-
if (!line.trim())
|
|
222
|
+
if (!line.trim()) {
|
|
223
|
+
validOffset += line.length + 1;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
155
226
|
|
|
156
227
|
let parsed: StoredRecord;
|
|
228
|
+
|
|
157
229
|
try {
|
|
158
230
|
parsed = JSON.parse(line);
|
|
159
231
|
} catch {
|
|
160
|
-
|
|
161
|
-
return;
|
|
232
|
+
break;
|
|
162
233
|
}
|
|
163
234
|
|
|
164
235
|
const { crc, ...record } = parsed;
|
|
165
236
|
const expected = crc32(JSON.stringify(record));
|
|
166
237
|
|
|
167
238
|
if (expected !== crc) {
|
|
168
|
-
|
|
169
|
-
"WAL checksum mismatch, stopping replay",
|
|
170
|
-
{ file, line: i + 1 }
|
|
171
|
-
);
|
|
172
|
-
return;
|
|
239
|
+
break;
|
|
173
240
|
}
|
|
174
241
|
|
|
242
|
+
validOffset += line.length + 1;
|
|
243
|
+
|
|
175
244
|
if (record.lsn <= fromLSN) continue;
|
|
176
245
|
|
|
177
246
|
this.lsn = Math.max(this.lsn, record.lsn);
|
|
178
247
|
await apply(record);
|
|
179
248
|
}
|
|
249
|
+
|
|
250
|
+
if (!this.readonlyMode) {
|
|
251
|
+
const stat = fs.fstatSync(fd);
|
|
252
|
+
if (validOffset < stat.size) {
|
|
253
|
+
fs.ftruncateSync(fd, validOffset);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
fs.closeSync(fd);
|
|
180
258
|
}
|
|
181
259
|
}
|
|
182
260
|
|
|
183
261
|
/* -------------------------
|
|
184
|
-
CLEANUP
|
|
262
|
+
CLEANUP (Primary only)
|
|
185
263
|
------------------------- */
|
|
186
264
|
|
|
187
265
|
async cleanup(beforeGen: number) {
|
|
266
|
+
if (this.readonlyMode) return;
|
|
188
267
|
if (!fs.existsSync(this.walDir)) return;
|
|
189
268
|
|
|
190
269
|
const files = fs.readdirSync(this.walDir);
|
|
270
|
+
|
|
191
271
|
for (const f of files) {
|
|
192
272
|
const m = f.match(/^wal-(\d+)\.log$/);
|
|
193
273
|
if (!m) continue;
|
|
@@ -210,4 +290,8 @@ export class WALManager {
|
|
|
210
290
|
getCurrentGen() {
|
|
211
291
|
return this.currentGen;
|
|
212
292
|
}
|
|
293
|
+
|
|
294
|
+
isReadonly() {
|
|
295
|
+
return this.readonlyMode;
|
|
296
|
+
}
|
|
213
297
|
}
|
package/src/ipc/index.ts
CHANGED
|
@@ -8,6 +8,8 @@ class CollectionProxy {
|
|
|
8
8
|
private collectionName: string
|
|
9
9
|
) {}
|
|
10
10
|
|
|
11
|
+
/* ------------------------------ INTERNAL CALLERS ------------------------------ */
|
|
12
|
+
|
|
11
13
|
private call(method: string, params: any[]): Promise<any> {
|
|
12
14
|
return dbQueue.exec("op", {
|
|
13
15
|
db: this.dbName,
|
|
@@ -26,38 +28,56 @@ class CollectionProxy {
|
|
|
26
28
|
});
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
private callCompact(): Promise<any> {
|
|
30
|
-
return dbQueue.exec("compact:collection", {
|
|
31
|
-
db: this.dbName,
|
|
32
|
-
col: this.collectionName
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
31
|
/* ------------------------------ CRUD ------------------------------ */
|
|
37
32
|
|
|
38
|
-
insertOne = (doc: any) =>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
insertOne = (doc: any) =>
|
|
34
|
+
this.call("insertOne", [doc]);
|
|
35
|
+
|
|
36
|
+
insertMany = (docs: any[]) =>
|
|
37
|
+
this.call("insertMany", [docs]);
|
|
38
|
+
|
|
39
|
+
find = (query?: any) =>
|
|
40
|
+
this.call("find", [query]);
|
|
41
|
+
|
|
42
|
+
findOne = (query?: any) =>
|
|
43
|
+
this.call("findOne", [query]);
|
|
44
|
+
|
|
42
45
|
updateOne = (filter: any, update: any, options?: any) =>
|
|
43
46
|
this.call("updateOne", [filter, update, options]);
|
|
47
|
+
|
|
44
48
|
updateMany = (filter: any, update: any) =>
|
|
45
49
|
this.call("updateMany", [filter, update]);
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
|
|
51
|
+
deleteOne = (filter: any) =>
|
|
52
|
+
this.call("deleteOne", [filter]);
|
|
53
|
+
|
|
54
|
+
deleteMany = (filter: any) =>
|
|
55
|
+
this.call("deleteMany", [filter]);
|
|
56
|
+
|
|
48
57
|
countDocuments = (filter?: any) =>
|
|
49
58
|
this.call("countDocuments", [filter]);
|
|
50
59
|
|
|
51
60
|
/* ------------------------------ INDEX ----------------------------- */
|
|
52
61
|
|
|
53
|
-
createIndex = (def: any) =>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
createIndex = (def: any) =>
|
|
63
|
+
this.callIndex("createIndex", [def]);
|
|
64
|
+
|
|
65
|
+
dropIndex = (field: string) =>
|
|
66
|
+
this.callIndex("dropIndex", [field]);
|
|
67
|
+
|
|
68
|
+
listIndexes = () =>
|
|
69
|
+
this.callIndex("listIndexes", []);
|
|
70
|
+
|
|
71
|
+
rebuildIndexes = () =>
|
|
72
|
+
this.callIndex("rebuildIndexes", []);
|
|
57
73
|
|
|
58
74
|
/* --------------------------- COMPACTION --------------------------- */
|
|
59
75
|
|
|
60
|
-
compact = () =>
|
|
76
|
+
compact = () =>
|
|
77
|
+
dbQueue.exec("compact:collection", {
|
|
78
|
+
db: this.dbName,
|
|
79
|
+
col: this.collectionName
|
|
80
|
+
});
|
|
61
81
|
}
|
|
62
82
|
|
|
63
83
|
/* -------------------------------- DATABASE PROXY -------------------------------- */
|
|
@@ -103,9 +123,11 @@ class LioranManagerIPC {
|
|
|
103
123
|
}
|
|
104
124
|
|
|
105
125
|
shutdown() {
|
|
106
|
-
return dbQueue.shutdown
|
|
126
|
+
return dbQueue.exec("shutdown", {});
|
|
107
127
|
}
|
|
108
128
|
}
|
|
109
129
|
|
|
130
|
+
/* -------------------------------- EXPORTS -------------------------------- */
|
|
131
|
+
|
|
110
132
|
export const manager = new LioranManagerIPC();
|
|
111
133
|
export type { CollectionProxy, DBProxy };
|
package/src/ipc/pool.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { Worker } from "worker_threads";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Worker Thread Pool
|
|
8
|
+
*
|
|
9
|
+
* - Spawns multiple worker threads (based on CPU cores)
|
|
10
|
+
* - Auto-restarts crashed workers
|
|
11
|
+
* - Supports graceful shutdown
|
|
12
|
+
* - Round-robin task scheduling
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export class IPCWorkerPool {
|
|
16
|
+
private workers: Worker[] = [];
|
|
17
|
+
private workerCount: number;
|
|
18
|
+
private shuttingDown = false;
|
|
19
|
+
private rrIndex = 0;
|
|
20
|
+
|
|
21
|
+
constructor() {
|
|
22
|
+
// Minimum 2 workers, scale with CPU cores
|
|
23
|
+
this.workerCount = Math.max(2, os.cpus().length);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* -------------------------------------------------- */
|
|
27
|
+
/* START POOL */
|
|
28
|
+
/* -------------------------------------------------- */
|
|
29
|
+
|
|
30
|
+
start() {
|
|
31
|
+
for (let i = 0; i < this.workerCount; i++) {
|
|
32
|
+
this.spawnWorker();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log(
|
|
36
|
+
`[WorkerPool] Started ${this.workerCount} worker threads`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* -------------------------------------------------- */
|
|
41
|
+
/* SPAWN WORKER */
|
|
42
|
+
/* -------------------------------------------------- */
|
|
43
|
+
|
|
44
|
+
private spawnWorker() {
|
|
45
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
46
|
+
const __dirname = path.dirname(__filename);
|
|
47
|
+
|
|
48
|
+
// Worker compiled output must exist in dist
|
|
49
|
+
const workerFile = path.join(__dirname, "worker.js");
|
|
50
|
+
|
|
51
|
+
const worker = new Worker(workerFile);
|
|
52
|
+
|
|
53
|
+
worker.on("exit", code => {
|
|
54
|
+
if (this.shuttingDown) return;
|
|
55
|
+
|
|
56
|
+
console.error(
|
|
57
|
+
`[WorkerPool] Worker exited (code=${code}). Restarting...`
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Remove dead worker
|
|
61
|
+
this.workers = this.workers.filter(w => w !== worker);
|
|
62
|
+
|
|
63
|
+
// Restart after short delay
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
this.spawnWorker();
|
|
66
|
+
}, 500);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
worker.on("error", err => {
|
|
70
|
+
console.error("[WorkerPool] Worker error:", err);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
this.workers.push(worker);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* -------------------------------------------------- */
|
|
77
|
+
/* EXECUTE TASK */
|
|
78
|
+
/* -------------------------------------------------- */
|
|
79
|
+
|
|
80
|
+
exec(task: any): Promise<any> {
|
|
81
|
+
if (this.workers.length === 0) {
|
|
82
|
+
throw new Error("No workers available");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const worker = this.workers[this.rrIndex];
|
|
86
|
+
this.rrIndex = (this.rrIndex + 1) % this.workers.length;
|
|
87
|
+
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
const id = Date.now() + Math.random();
|
|
90
|
+
|
|
91
|
+
const messageHandler = (msg: any) => {
|
|
92
|
+
if (msg.id !== id) return;
|
|
93
|
+
|
|
94
|
+
worker.off("message", messageHandler);
|
|
95
|
+
|
|
96
|
+
if (msg.ok) resolve(msg.result);
|
|
97
|
+
else reject(new Error(msg.error));
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
worker.on("message", messageHandler);
|
|
101
|
+
|
|
102
|
+
worker.postMessage({
|
|
103
|
+
id,
|
|
104
|
+
task
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* -------------------------------------------------- */
|
|
110
|
+
/* SHUTDOWN */
|
|
111
|
+
/* -------------------------------------------------- */
|
|
112
|
+
|
|
113
|
+
async shutdown() {
|
|
114
|
+
this.shuttingDown = true;
|
|
115
|
+
|
|
116
|
+
console.log("[WorkerPool] Shutting down worker threads...");
|
|
117
|
+
|
|
118
|
+
for (const worker of this.workers) {
|
|
119
|
+
try {
|
|
120
|
+
await worker.terminate();
|
|
121
|
+
} catch (err) {
|
|
122
|
+
console.error("[WorkerPool] Worker terminate error:", err);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.workers = [];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* -------------------------------------------------- */
|
|
130
|
+
/* INFO */
|
|
131
|
+
/* -------------------------------------------------- */
|
|
132
|
+
|
|
133
|
+
get size(): number {
|
|
134
|
+
return this.workerCount;
|
|
135
|
+
}
|
|
136
|
+
}
|
package/src/ipc/queue.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { LioranManager } from "../LioranManager.js";
|
|
2
2
|
import { getDefaultRootPath } from "../utils/rootpath.js";
|
|
3
|
+
import { IPCWorkerPool } from "./pool.js";
|
|
3
4
|
|
|
4
5
|
/* -------------------------------- ACTION TYPES -------------------------------- */
|
|
5
6
|
|
|
@@ -11,57 +12,110 @@ export type IPCAction =
|
|
|
11
12
|
| "compact:db"
|
|
12
13
|
| "compact:all"
|
|
13
14
|
| "shutdown"
|
|
14
|
-
| "open"
|
|
15
|
-
| "close"
|
|
16
|
-
| "minimize"
|
|
17
|
-
| "maximize"
|
|
18
15
|
| "restore"
|
|
19
16
|
| "snapshot";
|
|
20
17
|
|
|
21
18
|
/* -------------------------------- DB QUEUE -------------------------------- */
|
|
22
19
|
|
|
23
20
|
export class DBQueue {
|
|
24
|
-
private
|
|
21
|
+
private manager: LioranManager;
|
|
22
|
+
private pool: IPCWorkerPool;
|
|
23
|
+
private destroyed = false;
|
|
25
24
|
|
|
26
|
-
constructor(rootPath = getDefaultRootPath()) {
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
constructor(private rootPath = getDefaultRootPath()) {
|
|
26
|
+
// Single shared DB instance
|
|
27
|
+
this.manager = new LioranManager({ rootPath });
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
// Worker threads (for future compute-heavy tasks)
|
|
30
|
+
this.pool = new IPCWorkerPool();
|
|
31
|
+
this.pool.start();
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
/*
|
|
34
|
+
/* -------------------------------- EXEC -------------------------------- */
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
async exec(action: IPCAction, args: any) {
|
|
37
|
+
if (this.destroyed) {
|
|
38
|
+
throw new Error("DBQueue already shutdown");
|
|
39
|
+
}
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
41
|
+
switch (action) {
|
|
42
|
+
/* ---------------- DB ---------------- */
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
case "db":
|
|
45
|
+
await this.manager.db(args.db);
|
|
46
|
+
return true;
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
/* ---------------- CRUD OPS ---------------- */
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
case "op": {
|
|
51
|
+
const { db, col, method, params } = args;
|
|
52
|
+
const collection = (await this.manager.db(db)).collection(col);
|
|
53
|
+
return await (collection as any)[method](...params);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* ---------------- INDEX OPS ---------------- */
|
|
57
|
+
|
|
58
|
+
case "index": {
|
|
59
|
+
const { db, col, method, params } = args;
|
|
60
|
+
const collection = (await this.manager.db(db)).collection(col);
|
|
61
|
+
return await (collection as any)[method](...params);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* ---------------- COMPACTION ---------------- */
|
|
65
|
+
|
|
66
|
+
case "compact:collection": {
|
|
67
|
+
const { db, col } = args;
|
|
68
|
+
const collection = (await this.manager.db(db)).collection(col);
|
|
69
|
+
await collection.compact();
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
53
72
|
|
|
54
|
-
|
|
55
|
-
|
|
73
|
+
case "compact:db": {
|
|
74
|
+
const { db } = args;
|
|
75
|
+
const database = await this.manager.db(db);
|
|
76
|
+
await database.compactAll();
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
case "compact:all": {
|
|
81
|
+
for (const db of this.manager.openDBs.values()) {
|
|
82
|
+
await db.compactAll();
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* ---------------- SNAPSHOT ---------------- */
|
|
88
|
+
|
|
89
|
+
case "snapshot":
|
|
90
|
+
await this.manager.snapshot(args.path);
|
|
91
|
+
return true;
|
|
92
|
+
|
|
93
|
+
case "restore":
|
|
94
|
+
await this.manager.restore(args.path);
|
|
95
|
+
return true;
|
|
96
|
+
|
|
97
|
+
/* ---------------- CONTROL ---------------- */
|
|
98
|
+
|
|
99
|
+
case "shutdown":
|
|
100
|
+
await this.shutdown();
|
|
101
|
+
return true;
|
|
102
|
+
|
|
103
|
+
default:
|
|
104
|
+
throw new Error(`Unknown action: ${action}`);
|
|
105
|
+
}
|
|
56
106
|
}
|
|
57
107
|
|
|
58
108
|
/* ------------------------------ SHUTDOWN ------------------------------ */
|
|
59
109
|
|
|
60
110
|
async shutdown() {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
111
|
+
if (this.destroyed) return;
|
|
112
|
+
this.destroyed = true;
|
|
113
|
+
|
|
114
|
+
// Close DBs
|
|
115
|
+
await this.manager.closeAll();
|
|
116
|
+
|
|
117
|
+
// Shutdown worker threads
|
|
118
|
+
await this.pool.shutdown();
|
|
65
119
|
}
|
|
66
120
|
}
|
|
67
121
|
|