@liorandb/core 1.0.8 → 1.0.10

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.
@@ -3,20 +3,25 @@ import fs from "fs";
3
3
  import { LioranDB } from "./core/database.js";
4
4
  import { setEncryptionKey } from "./utils/encryption.js";
5
5
  import { getDefaultRootPath } from "./utils/rootpath.js";
6
+ import { dbQueue } from "./ipc/queue.js";
6
7
 
7
8
  export interface LioranManagerOptions {
8
9
  rootPath?: string;
9
10
  encryptionKey?: string | Buffer;
11
+ ipc?: boolean;
10
12
  }
11
13
 
12
14
  export class LioranManager {
13
15
  rootPath: string;
14
16
  openDBs: Map<string, LioranDB>;
17
+ private closed = false;
18
+ private ipc: boolean;
15
19
 
16
20
  constructor(options: LioranManagerOptions = {}) {
17
- const { rootPath, encryptionKey } = options;
21
+ const { rootPath, encryptionKey, ipc } = options;
18
22
 
19
23
  this.rootPath = rootPath || getDefaultRootPath();
24
+ this.ipc = ipc ?? process.env.LIORANDB_IPC === "1";
20
25
 
21
26
  if (!fs.existsSync(this.rootPath)) {
22
27
  fs.mkdirSync(this.rootPath, { recursive: true });
@@ -27,13 +32,26 @@ export class LioranManager {
27
32
  }
28
33
 
29
34
  this.openDBs = new Map();
35
+
36
+ if (!this.ipc) {
37
+ this._registerShutdownHooks();
38
+ }
30
39
  }
31
40
 
41
+ /* -------------------------------- CORE -------------------------------- */
42
+
32
43
  async db(name: string): Promise<LioranDB> {
44
+ if (this.ipc) {
45
+ await dbQueue.exec("db", { db: name });
46
+ return new IPCDatabase(name) as any;
47
+ }
48
+
33
49
  return this.openDatabase(name);
34
50
  }
35
51
 
36
52
  async createDatabase(name: string): Promise<LioranDB> {
53
+ this._assertOpen();
54
+
37
55
  const dbPath = path.join(this.rootPath, name);
38
56
 
39
57
  if (fs.existsSync(dbPath)) {
@@ -41,44 +59,99 @@ export class LioranManager {
41
59
  }
42
60
 
43
61
  await fs.promises.mkdir(dbPath, { recursive: true });
44
- return this.openDatabase(name);
62
+ return this.db(name);
45
63
  }
46
64
 
47
65
  async openDatabase(name: string): Promise<LioranDB> {
66
+ this._assertOpen();
67
+
68
+ if (this.openDBs.has(name)) {
69
+ return this.openDBs.get(name)!;
70
+ }
71
+
48
72
  const dbPath = path.join(this.rootPath, name);
49
73
 
50
74
  if (!fs.existsSync(dbPath)) {
51
75
  await fs.promises.mkdir(dbPath, { recursive: true });
52
76
  }
53
77
 
54
- if (this.openDBs.has(name)) {
55
- return this.openDBs.get(name)!;
56
- }
57
-
58
78
  const db = new LioranDB(dbPath, name, this);
59
79
  this.openDBs.set(name, db);
60
80
  return db;
61
81
  }
62
82
 
83
+ /* -------------------------------- LIFECYCLE -------------------------------- */
84
+
63
85
  async closeDatabase(name: string): Promise<void> {
86
+ if (this.ipc) return;
87
+
64
88
  if (!this.openDBs.has(name)) return;
65
89
 
66
90
  const db = this.openDBs.get(name)!;
91
+ await db.close();
92
+ this.openDBs.delete(name);
93
+ }
67
94
 
68
- for (const [, col] of db.collections.entries()) {
69
- await col.close();
95
+ /**
96
+ * Gracefully shuts down everything.
97
+ * - Closes all databases
98
+ * - Terminates IPC worker if running
99
+ */
100
+ async closeAll(): Promise<void> {
101
+ if (this.closed) return;
102
+ this.closed = true;
103
+
104
+ if (this.ipc) {
105
+ await dbQueue.shutdown();
106
+ return;
70
107
  }
71
108
 
72
- this.openDBs.delete(name);
109
+ for (const db of this.openDBs.values()) {
110
+ try {
111
+ await db.close();
112
+ } catch {}
113
+ }
114
+
115
+ this.openDBs.clear();
116
+ }
117
+
118
+ /**
119
+ * Alias for closeAll() (clean public API)
120
+ */
121
+ async close(): Promise<void> {
122
+ return this.closeAll();
123
+ }
124
+
125
+ private _registerShutdownHooks() {
126
+ const shutdown = async () => {
127
+ await this.closeAll();
128
+ };
129
+
130
+ process.on("SIGINT", shutdown);
131
+ process.on("SIGTERM", shutdown);
132
+ process.on("exit", shutdown);
73
133
  }
74
134
 
135
+ private _assertOpen() {
136
+ if (this.closed) {
137
+ throw new Error("LioranManager is closed");
138
+ }
139
+ }
140
+
141
+ /* -------------------------------- MANAGEMENT -------------------------------- */
142
+
75
143
  async renameDatabase(oldName: string, newName: string): Promise<boolean> {
144
+ if (this.ipc) {
145
+ return (await dbQueue.exec("renameDatabase", { oldName, newName })) as boolean;
146
+ }
147
+
76
148
  const oldPath = path.join(this.rootPath, oldName);
77
149
  const newPath = path.join(this.rootPath, newName);
78
150
 
79
151
  if (!fs.existsSync(oldPath)) {
80
152
  throw new Error(`Database "${oldName}" not found`);
81
153
  }
154
+
82
155
  if (fs.existsSync(newPath)) {
83
156
  throw new Error(`Database "${newName}" already exists`);
84
157
  }
@@ -93,6 +166,10 @@ export class LioranManager {
93
166
  }
94
167
 
95
168
  async dropDatabase(name: string): Promise<boolean> {
169
+ if (this.ipc) {
170
+ return (await dbQueue.exec("dropDatabase", { name })) as boolean;
171
+ }
172
+
96
173
  const dbPath = path.join(this.rootPath, name);
97
174
 
98
175
  if (!fs.existsSync(dbPath)) return false;
@@ -103,10 +180,64 @@ export class LioranManager {
103
180
  }
104
181
 
105
182
  async listDatabases(): Promise<string[]> {
183
+ if (this.ipc) {
184
+ return (await dbQueue.exec("listDatabases", {})) as string[];
185
+ }
186
+
106
187
  const items = await fs.promises.readdir(this.rootPath, {
107
188
  withFileTypes: true
108
189
  });
109
190
 
110
191
  return items.filter(i => i.isDirectory()).map(i => i.name);
111
192
  }
193
+
194
+ /* -------------------------------- DEBUG -------------------------------- */
195
+
196
+ getStats() {
197
+ return {
198
+ rootPath: this.rootPath,
199
+ openDatabases: this.ipc ? ["<ipc>"] : [...this.openDBs.keys()],
200
+ ipc: this.ipc,
201
+ closed: this.closed
202
+ };
203
+ }
204
+ }
205
+
206
+ /* -------------------------------- IPC PROXY DB -------------------------------- */
207
+
208
+ class IPCDatabase {
209
+ constructor(private name: string) {}
210
+
211
+ collection(name: string) {
212
+ return new IPCCollection(this.name, name);
213
+ }
214
+ }
215
+
216
+ class IPCCollection {
217
+ constructor(
218
+ private db: string,
219
+ private col: string
220
+ ) {}
221
+
222
+ private call(method: string, params: any[]) {
223
+ return dbQueue.exec("op", {
224
+ db: this.db,
225
+ col: this.col,
226
+ method,
227
+ params
228
+ });
229
+ }
230
+
231
+ insertOne = (doc: any) => this.call("insertOne", [doc]);
232
+ insertMany = (docs: any[]) => this.call("insertMany", [docs]);
233
+ find = (query?: any) => this.call("find", [query]);
234
+ findOne = (query?: any) => this.call("findOne", [query]);
235
+ updateOne = (filter: any, update: any, options?: any) =>
236
+ this.call("updateOne", [filter, update, options]);
237
+ updateMany = (filter: any, update: any) =>
238
+ this.call("updateMany", [filter, update]);
239
+ deleteOne = (filter: any) => this.call("deleteOne", [filter]);
240
+ deleteMany = (filter: any) => this.call("deleteMany", [filter]);
241
+ countDocuments = (filter?: any) =>
242
+ this.call("countDocuments", [filter]);
112
243
  }
@@ -1,7 +1,11 @@
1
+ import fs from "fs";
2
+ import path from "path";
1
3
  import { ClassicLevel } from "classic-level";
2
4
  import { matchDocument, applyUpdate } from "./query.js";
3
5
  import { v4 as uuid } from "uuid";
4
6
  import { encryptData, decryptData } from "../utils/encryption.js";
7
+ import type { ZodSchema } from "zod";
8
+ import { validateSchema } from "../utils/schema.js";
5
9
 
6
10
  export interface UpdateOptions {
7
11
  upsert?: boolean;
@@ -11,153 +15,315 @@ export class Collection<T = any> {
11
15
  dir: string;
12
16
  db: ClassicLevel<string, string>;
13
17
  private queue: Promise<any>;
18
+ private walPath: string;
19
+ private schema?: ZodSchema<T>;
14
20
 
15
- constructor(dir: string) {
21
+ constructor(dir: string, schema?: ZodSchema<T>) {
16
22
  this.dir = dir;
17
23
  this.db = new ClassicLevel(dir);
18
24
  this.queue = Promise.resolve();
25
+ this.walPath = path.join(dir, "__wal.log");
26
+ this.schema = schema;
27
+
28
+ this.recoverFromWAL().catch(console.error);
29
+ }
30
+
31
+ setSchema(schema: ZodSchema<T>) {
32
+ this.schema = schema;
33
+ }
34
+
35
+ private validate(doc: any): T {
36
+ return this.schema ? validateSchema(this.schema, doc) : doc;
37
+ }
38
+
39
+ /* ---------------- WAL ---------------- */
40
+
41
+ private async writeWAL(entry: any) {
42
+ await fs.promises.appendFile(
43
+ this.walPath,
44
+ JSON.stringify(entry) + "\n"
45
+ );
46
+ }
47
+
48
+ private async clearWAL() {
49
+ if (fs.existsSync(this.walPath)) {
50
+ await fs.promises.unlink(this.walPath);
51
+ }
19
52
  }
20
53
 
54
+ private async recoverFromWAL() {
55
+ if (!fs.existsSync(this.walPath)) return;
56
+
57
+ const lines = (await fs.promises.readFile(this.walPath, "utf8"))
58
+ .split("\n")
59
+ .filter(Boolean);
60
+
61
+ for (const line of lines) {
62
+ try {
63
+ const { op, args } = JSON.parse(line);
64
+ await this._exec(op, args, false);
65
+ } catch (err) {
66
+ console.error("WAL recovery failed:", err);
67
+ }
68
+ }
69
+
70
+ await this.clearWAL();
71
+ }
72
+
73
+ /* ---------------- Queue ---------------- */
74
+
21
75
  private _enqueue<R>(task: () => Promise<R>): Promise<R> {
22
76
  this.queue = this.queue.then(task).catch(console.error);
23
77
  return this.queue;
24
78
  }
25
79
 
80
+ /* ---------------- Core Executor ---------------- */
81
+
82
+ private async _exec(op: string, args: any[], log = true) {
83
+ if (log) await this.writeWAL({ op, args });
84
+
85
+ let result: any;
86
+
87
+ switch (op) {
88
+ case "insertOne":
89
+ result = await this._insertOne(args[0]);
90
+ break;
91
+
92
+ case "insertMany":
93
+ result = await this._insertMany(args[0]);
94
+ break;
95
+
96
+ case "find":
97
+ result = await this._find(args[0]);
98
+ break;
99
+
100
+ case "findOne":
101
+ result = await this._findOne(args[0]);
102
+ break;
103
+
104
+ case "updateOne":
105
+ result = await this._updateOne(args[0], args[1], args[2]);
106
+ break;
107
+
108
+ case "updateMany":
109
+ result = await this._updateMany(args[0], args[1]);
110
+ break;
111
+
112
+ case "deleteOne":
113
+ result = await this._deleteOne(args[0]);
114
+ break;
115
+
116
+ case "deleteMany":
117
+ result = await this._deleteMany(args[0]);
118
+ break;
119
+
120
+ case "countDocuments":
121
+ result = await this._countDocuments(args[0]);
122
+ break;
123
+
124
+ default:
125
+ throw new Error(`Unknown operation: ${op}`);
126
+ }
127
+
128
+ if (log) await this.clearWAL();
129
+ return result;
130
+ }
131
+
132
+ /* ---------------- Public API ---------------- */
133
+
26
134
  async close(): Promise<void> {
27
135
  try {
28
136
  await this.db.close();
29
137
  } catch {}
30
138
  }
31
139
 
32
- async insertOne(doc: T & { _id?: string }): Promise<T> {
33
- return this._enqueue(async () => {
34
- const _id = doc._id ?? uuid();
35
- const final = { _id, ...doc } as T;
36
- await this.db.put(String(_id), encryptData(final));
37
- return final;
38
- });
39
- }
40
-
41
- async insertMany(docs: (T & { _id?: string })[] = []): Promise<T[]> {
42
- return this._enqueue(async () => {
43
- const ops: Array<{ type: "put"; key: string; value: string }> = [];
44
- const out: T[] = [];
45
-
46
- for (const d of docs) {
47
- const _id = d._id ?? uuid();
48
- const final = { _id, ...d } as T;
49
- ops.push({
50
- type: "put",
51
- key: String(_id),
52
- value: encryptData(final)
53
- });
54
- out.push(final);
55
- }
140
+ insertOne(doc: T & { _id?: string }): Promise<T> {
141
+ return this._enqueue(() => this._exec("insertOne", [doc]));
142
+ }
56
143
 
57
- await this.db.batch(ops);
58
- return out;
59
- });
144
+ insertMany(docs: (T & { _id?: string })[] = []): Promise<T[]> {
145
+ return this._enqueue(() => this._exec("insertMany", [docs]));
60
146
  }
61
147
 
62
- async find(query: any = {}): Promise<T[]> {
63
- return this._enqueue(async () => {
64
- const out: T[] = [];
65
- for await (const [, enc] of this.db.iterator()) {
66
- const value = decryptData(enc);
67
- if (matchDocument(value, query)) out.push(value);
68
- }
69
- return out;
70
- });
148
+ find(query: any = {}): Promise<T[]> {
149
+ return this._enqueue(() => this._exec("find", [query]));
71
150
  }
72
151
 
73
- async findOne(query: any = {}): Promise<T | null> {
74
- return this._enqueue(async () => {
75
- for await (const [, enc] of this.db.iterator()) {
76
- const value = decryptData(enc);
77
- if (matchDocument(value, query)) return value;
78
- }
79
- return null;
80
- });
152
+ findOne(query: any = {}): Promise<T | null> {
153
+ return this._enqueue(() => this._exec("findOne", [query]));
81
154
  }
82
155
 
83
- async updateOne(
156
+ updateOne(
84
157
  filter: any = {},
85
158
  update: any = {},
86
159
  options: UpdateOptions = { upsert: false }
87
160
  ): Promise<T | null> {
88
- return this._enqueue(async () => {
89
- for await (const [key, enc] of this.db.iterator()) {
90
- const value = decryptData(enc);
91
- if (matchDocument(value, filter)) {
92
- const updated = applyUpdate(value, update);
93
- updated._id = value._id;
94
- await this.db.put(key, encryptData(updated));
95
- return updated;
96
- }
97
- }
161
+ return this._enqueue(() =>
162
+ this._exec("updateOne", [filter, update, options])
163
+ );
164
+ }
98
165
 
99
- if (options.upsert) {
100
- const doc = applyUpdate(filter, update);
101
- doc._id ??= uuid();
102
- await this.db.put(String(doc._id), encryptData(doc));
103
- return doc;
104
- }
166
+ updateMany(filter: any = {}, update: any = {}): Promise<T[]> {
167
+ return this._enqueue(() =>
168
+ this._exec("updateMany", [filter, update])
169
+ );
170
+ }
171
+
172
+ deleteOne(filter: any = {}): Promise<boolean> {
173
+ return this._enqueue(() => this._exec("deleteOne", [filter]));
174
+ }
175
+
176
+ deleteMany(filter: any = {}): Promise<number> {
177
+ return this._enqueue(() => this._exec("deleteMany", [filter]));
178
+ }
105
179
 
106
- return null;
107
- });
108
- }
109
-
110
- async updateMany(filter: any = {}, update: any = {}): Promise<T[]> {
111
- return this._enqueue(async () => {
112
- const updated: T[] = [];
113
- for await (const [key, enc] of this.db.iterator()) {
114
- const value = decryptData(enc);
115
- if (matchDocument(value, filter)) {
116
- const doc = applyUpdate(value, update);
117
- doc._id = value._id;
118
- await this.db.put(key, encryptData(doc));
119
- updated.push(doc);
120
- }
180
+ countDocuments(filter: any = {}): Promise<number> {
181
+ return this._enqueue(() =>
182
+ this._exec("countDocuments", [filter])
183
+ );
184
+ }
185
+
186
+ /* ---------------- Internal Ops ---------------- */
187
+
188
+ private async _insertOne(doc: T & { _id?: string }): Promise<T> {
189
+ const _id = doc._id ?? uuid();
190
+ const final = this.validate({ _id, ...doc });
191
+
192
+ await this.db.put(String(_id), encryptData(final));
193
+ return final;
194
+ }
195
+
196
+ private async _insertMany(
197
+ docs: (T & { _id?: string })[]
198
+ ): Promise<T[]> {
199
+ const ops: Array<{ type: "put"; key: string; value: string }> = [];
200
+ const out: T[] = [];
201
+
202
+ for (const d of docs) {
203
+ const _id = d._id ?? uuid();
204
+ const final = this.validate({ _id, ...d });
205
+
206
+ ops.push({
207
+ type: "put",
208
+ key: String(_id),
209
+ value: encryptData(final)
210
+ });
211
+
212
+ out.push(final);
213
+ }
214
+
215
+ await this.db.batch(ops);
216
+ return out;
217
+ }
218
+
219
+ private async _updateOne(
220
+ filter: any,
221
+ update: any,
222
+ options: UpdateOptions
223
+ ): Promise<T | null> {
224
+ for await (const [key, enc] of this.db.iterator()) {
225
+ const value = decryptData(enc);
226
+
227
+ if (matchDocument(value, filter)) {
228
+ const updated = applyUpdate(value, update);
229
+ updated._id = value._id;
230
+
231
+ const validated = this.validate(updated);
232
+ await this.db.put(key, encryptData(validated));
233
+
234
+ return validated;
121
235
  }
122
- return updated;
123
- });
124
- }
125
-
126
- async deleteOne(filter: any = {}): Promise<boolean> {
127
- return this._enqueue(async () => {
128
- for await (const [key, enc] of this.db.iterator()) {
129
- const value = decryptData(enc);
130
- if (matchDocument(value, filter)) {
131
- await this.db.del(key);
132
- return true;
133
- }
236
+ }
237
+
238
+ if (options?.upsert) {
239
+ const doc = applyUpdate(filter, update);
240
+ doc._id ??= uuid();
241
+
242
+ const validated = this.validate(doc);
243
+ await this.db.put(String(doc._id), encryptData(validated));
244
+
245
+ return validated;
246
+ }
247
+
248
+ return null;
249
+ }
250
+
251
+ private async _updateMany(filter: any, update: any): Promise<T[]> {
252
+ const updated: T[] = [];
253
+
254
+ for await (const [key, enc] of this.db.iterator()) {
255
+ const value = decryptData(enc);
256
+
257
+ if (matchDocument(value, filter)) {
258
+ const doc = applyUpdate(value, update);
259
+ doc._id = value._id;
260
+
261
+ const validated = this.validate(doc);
262
+ await this.db.put(key, encryptData(validated));
263
+
264
+ updated.push(validated);
134
265
  }
135
- return false;
136
- });
137
- }
138
-
139
- async deleteMany(filter: any = {}): Promise<number> {
140
- return this._enqueue(async () => {
141
- let count = 0;
142
- for await (const [key, enc] of this.db.iterator()) {
143
- const value = decryptData(enc);
144
- if (matchDocument(value, filter)) {
145
- await this.db.del(key);
146
- count++;
147
- }
266
+ }
267
+
268
+ return updated;
269
+ }
270
+
271
+ private async _find(query: any): Promise<T[]> {
272
+ const out: T[] = [];
273
+
274
+ for await (const [, enc] of this.db.iterator()) {
275
+ const value = decryptData(enc);
276
+ if (matchDocument(value, query)) out.push(value);
277
+ }
278
+
279
+ return out;
280
+ }
281
+
282
+ private async _findOne(query: any): Promise<T | null> {
283
+ for await (const [, enc] of this.db.iterator()) {
284
+ const value = decryptData(enc);
285
+ if (matchDocument(value, query)) return value;
286
+ }
287
+
288
+ return null;
289
+ }
290
+
291
+ private async _deleteOne(filter: any): Promise<boolean> {
292
+ for await (const [key, enc] of this.db.iterator()) {
293
+ const value = decryptData(enc);
294
+
295
+ if (matchDocument(value, filter)) {
296
+ await this.db.del(key);
297
+ return true;
148
298
  }
149
- return count;
150
- });
299
+ }
300
+
301
+ return false;
151
302
  }
152
303
 
153
- async countDocuments(filter: any = {}): Promise<number> {
154
- return this._enqueue(async () => {
155
- let c = 0;
156
- for await (const [, enc] of this.db.iterator()) {
157
- const value = decryptData(enc);
158
- if (matchDocument(value, filter)) c++;
304
+ private async _deleteMany(filter: any): Promise<number> {
305
+ let count = 0;
306
+
307
+ for await (const [key, enc] of this.db.iterator()) {
308
+ const value = decryptData(enc);
309
+
310
+ if (matchDocument(value, filter)) {
311
+ await this.db.del(key);
312
+ count++;
159
313
  }
160
- return c;
161
- });
314
+ }
315
+
316
+ return count;
317
+ }
318
+
319
+ private async _countDocuments(filter: any): Promise<number> {
320
+ let c = 0;
321
+
322
+ for await (const [, enc] of this.db.iterator()) {
323
+ const value = decryptData(enc);
324
+ if (matchDocument(value, filter)) c++;
325
+ }
326
+
327
+ return c;
162
328
  }
163
329
  }