@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/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; // 16 MB
19
+ const MAX_WAL_SIZE = 16 * 1024 * 1024; // 16MB
20
20
  const WAL_DIR = "__wal";
21
21
 
22
22
  /* =========================
23
- CRC32 IMPLEMENTATION
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 = 0xFFFFFFFF;
39
+ let crc = 0xffffffff;
41
40
  for (let i = 0; i < input.length; i++) {
42
- const byte = input.charCodeAt(i);
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 ^ 0xFFFFFFFF) >>> 0;
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
- fs.mkdirSync(this.walDir, { recursive: true });
61
- this.currentGen = this.detectLastGeneration();
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) max = Math.max(max, Number(m[1]));
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
- await this.fd!.write(JSON.stringify(stored) + "\n");
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 = fs
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
- const data = fs.readFileSync(filePath, "utf8");
150
- const lines = data.split("\n");
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()) continue;
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
- console.error("WAL parse error, stopping replay");
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
- console.error(
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) => this.call("insertOne", [doc]);
39
- insertMany = (docs: any[]) => this.call("insertMany", [docs]);
40
- find = (query?: any) => this.call("find", [query]);
41
- findOne = (query?: any) => this.call("findOne", [query]);
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
- deleteOne = (filter: any) => this.call("deleteOne", [filter]);
47
- deleteMany = (filter: any) => this.call("deleteMany", [filter]);
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) => this.callIndex("createIndex", [def]);
54
- dropIndex = (field: string) => this.callIndex("dropIndex", [field]);
55
- listIndexes = () => this.callIndex("listIndexes", []);
56
- rebuildIndexes = () => this.callIndex("rebuildIndexes", []);
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 = () => this.callCompact();
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 };
@@ -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 { IPCClient } from "./client.js";
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 client: IPCClient;
21
+ private manager: LioranManager;
22
+ private pool: IPCWorkerPool;
23
+ private destroyed = false;
25
24
 
26
- constructor(rootPath = getDefaultRootPath()) {
27
- this.client = new IPCClient(rootPath);
28
- }
25
+ constructor(private rootPath = getDefaultRootPath()) {
26
+ // Single shared DB instance
27
+ this.manager = new LioranManager({ rootPath });
29
28
 
30
- exec(action: IPCAction, args: any) {
31
- return this.client.exec(action, args);
29
+ // Worker threads (for future compute-heavy tasks)
30
+ this.pool = new IPCWorkerPool();
31
+ this.pool.start();
32
32
  }
33
33
 
34
- /* ----------------------------- COMPACTION API ----------------------------- */
34
+ /* -------------------------------- EXEC -------------------------------- */
35
35
 
36
- compactCollection(db: string, col: string) {
37
- return this.exec("compact:collection", { db, col });
38
- }
36
+ async exec(action: IPCAction, args: any) {
37
+ if (this.destroyed) {
38
+ throw new Error("DBQueue already shutdown");
39
+ }
39
40
 
40
- compactDB(db: string) {
41
- return this.exec("compact:db", { db });
42
- }
41
+ switch (action) {
42
+ /* ---------------- DB ---------------- */
43
43
 
44
- compactAll() {
45
- return this.exec("compact:all", {});
46
- }
44
+ case "db":
45
+ await this.manager.db(args.db);
46
+ return true;
47
47
 
48
- /* ----------------------------- SNAPSHOT API ----------------------------- */
48
+ /* ---------------- CRUD OPS ---------------- */
49
49
 
50
- snapshot(path: string) {
51
- return this.exec("snapshot", { path });
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
- restore(path: string) {
55
- return this.exec("restore", { path });
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
- try {
62
- await this.exec("shutdown", {});
63
- } catch {}
64
- this.client.close();
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