@liorandb/core 1.0.11 → 1.0.13
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-ST6KMJQJ.js +759 -0
- package/dist/index.d.ts +46 -49
- package/dist/index.js +790 -5
- package/dist/worker/dbWorker.js +11 -13
- package/package.json +2 -2
- package/src/LioranManager.ts +62 -114
- package/src/core/collection.ts +74 -244
- package/src/core/database.ts +103 -86
- package/src/core/transaction.ts +34 -0
- package/src/ipc/client.ts +85 -0
- package/src/ipc/queue.ts +11 -126
- package/src/ipc/server.ts +84 -0
- package/src/ipc/socketPath.ts +10 -0
- package/src/worker/dbWorker.ts +0 -45
package/src/core/collection.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
1
|
import { ClassicLevel } from "classic-level";
|
|
4
2
|
import { matchDocument, applyUpdate } from "./query.js";
|
|
5
3
|
import { v4 as uuid } from "uuid";
|
|
@@ -14,19 +12,13 @@ export interface UpdateOptions {
|
|
|
14
12
|
export class Collection<T = any> {
|
|
15
13
|
dir: string;
|
|
16
14
|
db: ClassicLevel<string, string>;
|
|
17
|
-
private queue: Promise<any
|
|
18
|
-
private walPath: string;
|
|
15
|
+
private queue: Promise<any> = Promise.resolve();
|
|
19
16
|
private schema?: ZodSchema<T>;
|
|
20
|
-
private walCounter = 0;
|
|
21
17
|
|
|
22
18
|
constructor(dir: string, schema?: ZodSchema<T>) {
|
|
23
19
|
this.dir = dir;
|
|
24
|
-
this.db = new ClassicLevel(dir);
|
|
25
|
-
this.queue = Promise.resolve();
|
|
26
|
-
this.walPath = path.join(dir, "__wal.log");
|
|
20
|
+
this.db = new ClassicLevel(dir, { valueEncoding: "utf8" });
|
|
27
21
|
this.schema = schema;
|
|
28
|
-
|
|
29
|
-
this.recoverFromWAL().catch(console.error);
|
|
30
22
|
}
|
|
31
23
|
|
|
32
24
|
setSchema(schema: ZodSchema<T>) {
|
|
@@ -37,349 +29,187 @@ export class Collection<T = any> {
|
|
|
37
29
|
return this.schema ? validateSchema(this.schema, doc) : doc;
|
|
38
30
|
}
|
|
39
31
|
|
|
40
|
-
/* ---------------- WAL ---------------- */
|
|
41
|
-
|
|
42
|
-
private async writeWAL(entry: any) {
|
|
43
|
-
await fs.promises.appendFile(
|
|
44
|
-
this.walPath,
|
|
45
|
-
JSON.stringify(entry) + "\n"
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
private async clearWAL() {
|
|
50
|
-
if (fs.existsSync(this.walPath)) {
|
|
51
|
-
await fs.promises.unlink(this.walPath);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
private async maybeCheckpoint() {
|
|
56
|
-
if (++this.walCounter >= 100) {
|
|
57
|
-
this.walCounter = 0;
|
|
58
|
-
await this.clearWAL();
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
private async recoverFromWAL() {
|
|
63
|
-
if (!fs.existsSync(this.walPath)) return;
|
|
64
|
-
|
|
65
|
-
const lines = (await fs.promises.readFile(this.walPath, "utf8"))
|
|
66
|
-
.split("\n")
|
|
67
|
-
.filter(Boolean);
|
|
68
|
-
|
|
69
|
-
for (const line of lines) {
|
|
70
|
-
try {
|
|
71
|
-
const { op, args } = JSON.parse(line);
|
|
72
|
-
await this._exec(op, args, false);
|
|
73
|
-
} catch (err) {
|
|
74
|
-
console.error("WAL recovery failed:", err);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
await this.clearWAL();
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/* ---------------- Queue ---------------- */
|
|
82
|
-
|
|
83
32
|
private _enqueue<R>(task: () => Promise<R>): Promise<R> {
|
|
84
33
|
this.queue = this.queue.then(task).catch(console.error);
|
|
85
34
|
return this.queue;
|
|
86
35
|
}
|
|
87
36
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (log) await this.writeWAL({ op, args });
|
|
92
|
-
|
|
93
|
-
let result: any;
|
|
37
|
+
async close(): Promise<void> {
|
|
38
|
+
try { await this.db.close(); } catch {}
|
|
39
|
+
}
|
|
94
40
|
|
|
41
|
+
async _exec(op: string, args: any[]) {
|
|
95
42
|
switch (op) {
|
|
96
|
-
case "insertOne":
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
case "
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
case "
|
|
105
|
-
|
|
106
|
-
break;
|
|
107
|
-
|
|
108
|
-
case "findOne":
|
|
109
|
-
result = await this._findOne(args[0]);
|
|
110
|
-
break;
|
|
111
|
-
|
|
112
|
-
case "updateOne":
|
|
113
|
-
result = await this._updateOne(args[0], args[1], args[2]);
|
|
114
|
-
break;
|
|
115
|
-
|
|
116
|
-
case "updateMany":
|
|
117
|
-
result = await this._updateMany(args[0], args[1]);
|
|
118
|
-
break;
|
|
119
|
-
|
|
120
|
-
case "deleteOne":
|
|
121
|
-
result = await this._deleteOne(args[0]);
|
|
122
|
-
break;
|
|
123
|
-
|
|
124
|
-
case "deleteMany":
|
|
125
|
-
result = await this._deleteMany(args[0]);
|
|
126
|
-
break;
|
|
127
|
-
|
|
128
|
-
case "countDocuments":
|
|
129
|
-
result = await this._countDocuments(args[0]);
|
|
130
|
-
break;
|
|
131
|
-
|
|
132
|
-
default:
|
|
133
|
-
throw new Error(`Unknown operation: ${op}`);
|
|
43
|
+
case "insertOne": return this._insertOne(args[0]);
|
|
44
|
+
case "insertMany": return this._insertMany(args[0]);
|
|
45
|
+
case "find": return this._find(args[0]);
|
|
46
|
+
case "findOne": return this._findOne(args[0]);
|
|
47
|
+
case "updateOne": return this._updateOne(args[0], args[1], args[2]);
|
|
48
|
+
case "updateMany": return this._updateMany(args[0], args[1]);
|
|
49
|
+
case "deleteOne": return this._deleteOne(args[0]);
|
|
50
|
+
case "deleteMany": return this._deleteMany(args[0]);
|
|
51
|
+
case "countDocuments": return this._countDocuments(args[0]);
|
|
52
|
+
default: throw new Error(`Unknown operation: ${op}`);
|
|
134
53
|
}
|
|
135
|
-
|
|
136
|
-
if (log) await this.maybeCheckpoint();
|
|
137
|
-
return result;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/* ---------------- Public API ---------------- */
|
|
141
|
-
|
|
142
|
-
async close(): Promise<void> {
|
|
143
|
-
try {
|
|
144
|
-
await this.db.close();
|
|
145
|
-
} catch {}
|
|
146
54
|
}
|
|
147
55
|
|
|
148
|
-
insertOne(doc: T & { _id?: string })
|
|
56
|
+
insertOne(doc: T & { _id?: string }) {
|
|
149
57
|
return this._enqueue(() => this._exec("insertOne", [doc]));
|
|
150
58
|
}
|
|
151
59
|
|
|
152
|
-
insertMany(docs: (T & { _id?: string })[] = [])
|
|
60
|
+
insertMany(docs: (T & { _id?: string })[] = []) {
|
|
153
61
|
return this._enqueue(() => this._exec("insertMany", [docs]));
|
|
154
62
|
}
|
|
155
63
|
|
|
156
|
-
find(query: any = {})
|
|
64
|
+
find(query: any = {}) {
|
|
157
65
|
return this._enqueue(() => this._exec("find", [query]));
|
|
158
66
|
}
|
|
159
67
|
|
|
160
|
-
findOne(query: any = {})
|
|
68
|
+
findOne(query: any = {}) {
|
|
161
69
|
return this._enqueue(() => this._exec("findOne", [query]));
|
|
162
70
|
}
|
|
163
71
|
|
|
164
|
-
updateOne(
|
|
165
|
-
filter: any = {},
|
|
166
|
-
update: any = {},
|
|
167
|
-
options: UpdateOptions = { upsert: false }
|
|
168
|
-
): Promise<T | null> {
|
|
72
|
+
updateOne(filter: any, update: any, options: UpdateOptions = {}) {
|
|
169
73
|
return this._enqueue(() =>
|
|
170
74
|
this._exec("updateOne", [filter, update, options])
|
|
171
75
|
);
|
|
172
76
|
}
|
|
173
77
|
|
|
174
|
-
updateMany(filter: any
|
|
78
|
+
updateMany(filter: any, update: any) {
|
|
175
79
|
return this._enqueue(() =>
|
|
176
80
|
this._exec("updateMany", [filter, update])
|
|
177
81
|
);
|
|
178
82
|
}
|
|
179
83
|
|
|
180
|
-
deleteOne(filter: any
|
|
181
|
-
return this._enqueue(() =>
|
|
84
|
+
deleteOne(filter: any) {
|
|
85
|
+
return this._enqueue(() =>
|
|
86
|
+
this._exec("deleteOne", [filter])
|
|
87
|
+
);
|
|
182
88
|
}
|
|
183
89
|
|
|
184
|
-
deleteMany(filter: any
|
|
185
|
-
return this._enqueue(() =>
|
|
90
|
+
deleteMany(filter: any) {
|
|
91
|
+
return this._enqueue(() =>
|
|
92
|
+
this._exec("deleteMany", [filter])
|
|
93
|
+
);
|
|
186
94
|
}
|
|
187
95
|
|
|
188
|
-
countDocuments(filter: any = {})
|
|
96
|
+
countDocuments(filter: any = {}) {
|
|
189
97
|
return this._enqueue(() =>
|
|
190
98
|
this._exec("countDocuments", [filter])
|
|
191
99
|
);
|
|
192
100
|
}
|
|
193
101
|
|
|
194
|
-
/* ----------------
|
|
102
|
+
/* ---------------- Storage ---------------- */
|
|
195
103
|
|
|
196
|
-
private async _insertOne(doc:
|
|
104
|
+
private async _insertOne(doc: any) {
|
|
197
105
|
const _id = doc._id ?? uuid();
|
|
198
106
|
const final = this.validate({ _id, ...doc });
|
|
199
|
-
|
|
200
107
|
await this.db.put(String(_id), encryptData(final));
|
|
201
108
|
return final;
|
|
202
109
|
}
|
|
203
110
|
|
|
204
|
-
private async _insertMany(
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const ops: Array<{ type: "put"; key: string; value: string }> = [];
|
|
208
|
-
const out: T[] = [];
|
|
111
|
+
private async _insertMany(docs: any[]) {
|
|
112
|
+
const batch: Array<{ type: "put"; key: string; value: string }> = [];
|
|
113
|
+
const out = [];
|
|
209
114
|
|
|
210
115
|
for (const d of docs) {
|
|
211
116
|
const _id = d._id ?? uuid();
|
|
212
117
|
const final = this.validate({ _id, ...d });
|
|
213
|
-
|
|
214
|
-
ops.push({
|
|
215
|
-
type: "put",
|
|
216
|
-
key: String(_id),
|
|
217
|
-
value: encryptData(final)
|
|
218
|
-
});
|
|
219
|
-
|
|
118
|
+
batch.push({ type: "put", key: String(_id), value: encryptData(final) });
|
|
220
119
|
out.push(final);
|
|
221
120
|
}
|
|
222
121
|
|
|
223
|
-
await this.db.batch(
|
|
122
|
+
await this.db.batch(batch);
|
|
224
123
|
return out;
|
|
225
124
|
}
|
|
226
125
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
private async _findOne(query: any): Promise<T | null> {
|
|
126
|
+
private async _findOne(query: any) {
|
|
230
127
|
if (query?._id) {
|
|
231
128
|
try {
|
|
232
129
|
const enc = await this.db.get(String(query._id));
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
} catch {
|
|
236
|
-
return null;
|
|
237
|
-
}
|
|
130
|
+
return enc ? decryptData(enc) : null;
|
|
131
|
+
} catch { return null; }
|
|
238
132
|
}
|
|
239
133
|
|
|
240
134
|
for await (const [, enc] of this.db.iterator()) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (matchDocument(value, query)) return value;
|
|
135
|
+
const v = decryptData(enc);
|
|
136
|
+
if (matchDocument(v, query)) return v;
|
|
244
137
|
}
|
|
245
138
|
|
|
246
139
|
return null;
|
|
247
140
|
}
|
|
248
141
|
|
|
249
|
-
private async _updateOne(
|
|
250
|
-
filter: any,
|
|
251
|
-
update: any,
|
|
252
|
-
options: UpdateOptions
|
|
253
|
-
): Promise<T | null> {
|
|
254
|
-
if (filter?._id) {
|
|
255
|
-
try {
|
|
256
|
-
const enc = await this.db.get(String(filter._id));
|
|
257
|
-
if (!enc) throw new Error("Not found");
|
|
258
|
-
const value = decryptData(enc);
|
|
259
|
-
|
|
260
|
-
const updated = applyUpdate(value, update);
|
|
261
|
-
updated._id = value._id;
|
|
262
|
-
|
|
263
|
-
const validated = this.validate(updated);
|
|
264
|
-
await this.db.put(String(updated._id), encryptData(validated));
|
|
265
|
-
|
|
266
|
-
return validated;
|
|
267
|
-
} catch {
|
|
268
|
-
if (!options?.upsert) return null;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
142
|
+
private async _updateOne(filter: any, update: any, options: UpdateOptions) {
|
|
272
143
|
for await (const [key, enc] of this.db.iterator()) {
|
|
273
|
-
if (!enc) continue;
|
|
274
144
|
const value = decryptData(enc);
|
|
275
|
-
|
|
276
145
|
if (matchDocument(value, filter)) {
|
|
277
|
-
const updated = applyUpdate(value, update);
|
|
146
|
+
const updated = this.validate(applyUpdate(value, update)) as any;
|
|
278
147
|
updated._id = value._id;
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
await this.db.put(key, encryptData(validated));
|
|
282
|
-
|
|
283
|
-
return validated;
|
|
148
|
+
await this.db.put(key, encryptData(updated));
|
|
149
|
+
return updated;
|
|
284
150
|
}
|
|
285
151
|
}
|
|
286
152
|
|
|
287
153
|
if (options?.upsert) {
|
|
288
|
-
const doc = applyUpdate(
|
|
289
|
-
doc._id
|
|
290
|
-
|
|
291
|
-
const validated = this.validate(doc);
|
|
292
|
-
await this.db.put(String(doc._id), encryptData(validated));
|
|
293
|
-
|
|
294
|
-
return validated;
|
|
154
|
+
const doc = this.validate({ _id: uuid(), ...applyUpdate({}, update) }) as any;
|
|
155
|
+
await this.db.put(String(doc._id), encryptData(doc));
|
|
156
|
+
return doc;
|
|
295
157
|
}
|
|
296
158
|
|
|
297
159
|
return null;
|
|
298
160
|
}
|
|
299
161
|
|
|
300
|
-
private async
|
|
301
|
-
|
|
302
|
-
try {
|
|
303
|
-
await this.db.del(String(filter._id));
|
|
304
|
-
return true;
|
|
305
|
-
} catch {
|
|
306
|
-
return false;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
162
|
+
private async _updateMany(filter: any, update: any) {
|
|
163
|
+
const out = [];
|
|
309
164
|
|
|
310
165
|
for await (const [key, enc] of this.db.iterator()) {
|
|
311
|
-
if (!enc) continue;
|
|
312
166
|
const value = decryptData(enc);
|
|
313
|
-
|
|
314
167
|
if (matchDocument(value, filter)) {
|
|
315
|
-
|
|
316
|
-
|
|
168
|
+
const updated = this.validate(applyUpdate(value, update)) as any;
|
|
169
|
+
updated._id = value._id;
|
|
170
|
+
await this.db.put(key, encryptData(updated));
|
|
171
|
+
out.push(updated);
|
|
317
172
|
}
|
|
318
173
|
}
|
|
319
174
|
|
|
320
|
-
return
|
|
175
|
+
return out;
|
|
321
176
|
}
|
|
322
177
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
for await (const [key, enc] of this.db.iterator()) {
|
|
329
|
-
if (!enc) continue;
|
|
330
|
-
const value = decryptData(enc);
|
|
331
|
-
|
|
332
|
-
if (matchDocument(value, filter)) {
|
|
333
|
-
const doc = applyUpdate(value, update);
|
|
334
|
-
doc._id = value._id;
|
|
335
|
-
|
|
336
|
-
const validated = this.validate(doc);
|
|
337
|
-
await this.db.put(key, encryptData(validated));
|
|
338
|
-
|
|
339
|
-
updated.push(validated);
|
|
340
|
-
}
|
|
178
|
+
private async _find(query: any) {
|
|
179
|
+
const out = [];
|
|
180
|
+
for await (const [, enc] of this.db.iterator()) {
|
|
181
|
+
const v = decryptData(enc);
|
|
182
|
+
if (matchDocument(v, query)) out.push(v);
|
|
341
183
|
}
|
|
342
|
-
|
|
343
|
-
return updated;
|
|
184
|
+
return out;
|
|
344
185
|
}
|
|
345
186
|
|
|
346
|
-
private async
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
if (matchDocument(value, query)) out.push(value);
|
|
187
|
+
private async _deleteOne(filter: any) {
|
|
188
|
+
for await (const [key, enc] of this.db.iterator()) {
|
|
189
|
+
if (matchDocument(decryptData(enc), filter)) {
|
|
190
|
+
await this.db.del(key);
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
353
193
|
}
|
|
354
|
-
|
|
355
|
-
return out;
|
|
194
|
+
return false;
|
|
356
195
|
}
|
|
357
196
|
|
|
358
|
-
private async _deleteMany(filter: any)
|
|
197
|
+
private async _deleteMany(filter: any) {
|
|
359
198
|
let count = 0;
|
|
360
|
-
|
|
361
199
|
for await (const [key, enc] of this.db.iterator()) {
|
|
362
|
-
if (
|
|
363
|
-
const value = decryptData(enc);
|
|
364
|
-
|
|
365
|
-
if (matchDocument(value, filter)) {
|
|
200
|
+
if (matchDocument(decryptData(enc), filter)) {
|
|
366
201
|
await this.db.del(key);
|
|
367
202
|
count++;
|
|
368
203
|
}
|
|
369
204
|
}
|
|
370
|
-
|
|
371
205
|
return count;
|
|
372
206
|
}
|
|
373
207
|
|
|
374
|
-
private async _countDocuments(filter: any)
|
|
208
|
+
private async _countDocuments(filter: any) {
|
|
375
209
|
let c = 0;
|
|
376
|
-
|
|
377
210
|
for await (const [, enc] of this.db.iterator()) {
|
|
378
|
-
if (
|
|
379
|
-
const value = decryptData(enc);
|
|
380
|
-
if (matchDocument(value, filter)) c++;
|
|
211
|
+
if (matchDocument(decryptData(enc), filter)) c++;
|
|
381
212
|
}
|
|
382
|
-
|
|
383
213
|
return c;
|
|
384
214
|
}
|
|
385
215
|
}
|
package/src/core/database.ts
CHANGED
|
@@ -4,133 +4,150 @@ import { Collection } from "./collection.js";
|
|
|
4
4
|
import type { LioranManager } from "../LioranManager.js";
|
|
5
5
|
import type { ZodSchema } from "zod";
|
|
6
6
|
|
|
7
|
+
type TXOp = { tx: number; col: string; op: string; args: any[] };
|
|
8
|
+
type TXCommit = { tx: number; commit: true };
|
|
9
|
+
type TXApplied = { tx: number; applied: true };
|
|
10
|
+
type WALEntry = TXOp | TXCommit | TXApplied;
|
|
11
|
+
|
|
12
|
+
class DBTransactionContext {
|
|
13
|
+
private ops: TXOp[] = [];
|
|
14
|
+
|
|
15
|
+
constructor(
|
|
16
|
+
private db: LioranDB,
|
|
17
|
+
public readonly txId: number
|
|
18
|
+
) {}
|
|
19
|
+
|
|
20
|
+
collection(name: string) {
|
|
21
|
+
return new Proxy({}, {
|
|
22
|
+
get: (_, prop: string) => {
|
|
23
|
+
return (...args: any[]) => {
|
|
24
|
+
this.ops.push({
|
|
25
|
+
tx: this.txId,
|
|
26
|
+
col: name,
|
|
27
|
+
op: prop,
|
|
28
|
+
args
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async commit() {
|
|
36
|
+
await this.db.writeWAL(this.ops);
|
|
37
|
+
await this.db.writeWAL([{ tx: this.txId, commit: true }]);
|
|
38
|
+
await this.db.applyTransaction(this.ops);
|
|
39
|
+
await this.db.writeWAL([{ tx: this.txId, applied: true }]);
|
|
40
|
+
await this.db.clearWAL();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
7
44
|
export class LioranDB {
|
|
8
45
|
basePath: string;
|
|
9
46
|
dbName: string;
|
|
10
47
|
manager: LioranManager;
|
|
11
48
|
collections: Map<string, Collection>;
|
|
49
|
+
private walPath: string;
|
|
50
|
+
|
|
51
|
+
private static TX_SEQ = 0;
|
|
12
52
|
|
|
13
53
|
constructor(basePath: string, dbName: string, manager: LioranManager) {
|
|
14
54
|
this.basePath = basePath;
|
|
15
55
|
this.dbName = dbName;
|
|
16
56
|
this.manager = manager;
|
|
17
57
|
this.collections = new Map();
|
|
58
|
+
this.walPath = path.join(basePath, "__tx_wal.log");
|
|
18
59
|
|
|
19
|
-
|
|
20
|
-
fs.mkdirSync(basePath, { recursive: true });
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/* -------------------------------- COLLECTION -------------------------------- */
|
|
60
|
+
fs.mkdirSync(basePath, { recursive: true });
|
|
25
61
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const col = this.collections.get(name)!;
|
|
29
|
-
if (schema) col.setSchema(schema);
|
|
30
|
-
return col as Collection<T>;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const colPath = path.join(this.basePath, name);
|
|
62
|
+
this.recoverFromWAL().catch(console.error);
|
|
63
|
+
}
|
|
34
64
|
|
|
35
|
-
|
|
36
|
-
|
|
65
|
+
async writeWAL(entries: WALEntry[]) {
|
|
66
|
+
const fd = await fs.promises.open(this.walPath, "a");
|
|
67
|
+
for (const e of entries) {
|
|
68
|
+
await fd.write(JSON.stringify(e) + "\n");
|
|
37
69
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
this.collections.set(name, col);
|
|
41
|
-
|
|
42
|
-
return col;
|
|
70
|
+
await fd.sync();
|
|
71
|
+
await fd.close();
|
|
43
72
|
}
|
|
44
73
|
|
|
45
|
-
async
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
): Promise<Collection<T>> {
|
|
49
|
-
const colPath = path.join(this.basePath, name);
|
|
50
|
-
|
|
51
|
-
if (fs.existsSync(colPath)) {
|
|
52
|
-
throw new Error("Collection already exists");
|
|
53
|
-
}
|
|
74
|
+
async clearWAL() {
|
|
75
|
+
try { await fs.promises.unlink(this.walPath); } catch {}
|
|
76
|
+
}
|
|
54
77
|
|
|
55
|
-
|
|
78
|
+
private async recoverFromWAL() {
|
|
79
|
+
if (!fs.existsSync(this.walPath)) return;
|
|
56
80
|
|
|
57
|
-
const
|
|
58
|
-
this.collections.set(name, col);
|
|
81
|
+
const raw = await fs.promises.readFile(this.walPath, "utf8");
|
|
59
82
|
|
|
60
|
-
|
|
61
|
-
|
|
83
|
+
const committed = new Set<number>();
|
|
84
|
+
const applied = new Set<number>();
|
|
85
|
+
const ops = new Map<number, TXOp[]>();
|
|
62
86
|
|
|
63
|
-
|
|
64
|
-
|
|
87
|
+
for (const line of raw.split("\n")) {
|
|
88
|
+
if (!line.trim()) continue;
|
|
65
89
|
|
|
66
|
-
|
|
67
|
-
|
|
90
|
+
try {
|
|
91
|
+
const entry: WALEntry = JSON.parse(line);
|
|
92
|
+
|
|
93
|
+
if ("commit" in entry) committed.add(entry.tx);
|
|
94
|
+
else if ("applied" in entry) applied.add(entry.tx);
|
|
95
|
+
else {
|
|
96
|
+
if (!ops.has(entry.tx)) ops.set(entry.tx, []);
|
|
97
|
+
ops.get(entry.tx)!.push(entry);
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// Ignore corrupted WAL tail
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
68
103
|
}
|
|
69
104
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
105
|
+
for (const tx of committed) {
|
|
106
|
+
if (applied.has(tx)) continue;
|
|
107
|
+
|
|
108
|
+
const txOps = ops.get(tx);
|
|
109
|
+
if (txOps) await this.applyTransaction(txOps);
|
|
73
110
|
}
|
|
74
111
|
|
|
75
|
-
await
|
|
76
|
-
return true;
|
|
112
|
+
await this.clearWAL();
|
|
77
113
|
}
|
|
78
114
|
|
|
79
|
-
async
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (!fs.existsSync(oldPath)) {
|
|
84
|
-
throw new Error("Collection does not exist");
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (fs.existsSync(newPath)) {
|
|
88
|
-
throw new Error("New collection name already exists");
|
|
115
|
+
async applyTransaction(ops: TXOp[]) {
|
|
116
|
+
for (const { col, op, args } of ops) {
|
|
117
|
+
const collection = this.collection(col);
|
|
118
|
+
await (collection as any)._exec(op, args);
|
|
89
119
|
}
|
|
120
|
+
}
|
|
90
121
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
this.collections.
|
|
122
|
+
collection<T = any>(name: string, schema?: ZodSchema<T>): Collection<T> {
|
|
123
|
+
if (this.collections.has(name)) {
|
|
124
|
+
const col = this.collections.get(name)!;
|
|
125
|
+
if (schema) col.setSchema(schema);
|
|
126
|
+
return col as Collection<T>;
|
|
94
127
|
}
|
|
95
128
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const col = new Collection(newPath);
|
|
99
|
-
this.collections.set(newName, col);
|
|
129
|
+
const colPath = path.join(this.basePath, name);
|
|
130
|
+
fs.mkdirSync(colPath, { recursive: true });
|
|
100
131
|
|
|
101
|
-
|
|
132
|
+
const col = new Collection<T>(colPath, schema);
|
|
133
|
+
this.collections.set(name, col);
|
|
134
|
+
return col;
|
|
102
135
|
}
|
|
103
136
|
|
|
104
|
-
async
|
|
105
|
-
|
|
106
|
-
|
|
137
|
+
async transaction<T>(fn: (tx: DBTransactionContext) => Promise<T>): Promise<T> {
|
|
138
|
+
const txId = ++LioranDB.TX_SEQ;
|
|
139
|
+
const tx = new DBTransactionContext(this, txId);
|
|
107
140
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
withFileTypes: true
|
|
111
|
-
});
|
|
141
|
+
const result = await fn(tx);
|
|
142
|
+
await tx.commit();
|
|
112
143
|
|
|
113
|
-
return
|
|
144
|
+
return result;
|
|
114
145
|
}
|
|
115
146
|
|
|
116
|
-
/* -------------------------------- LIFECYCLE -------------------------------- */
|
|
117
|
-
|
|
118
147
|
async close(): Promise<void> {
|
|
119
148
|
for (const col of this.collections.values()) {
|
|
120
|
-
try {
|
|
121
|
-
await col.close();
|
|
122
|
-
} catch {}
|
|
149
|
+
try { await col.close(); } catch {}
|
|
123
150
|
}
|
|
124
151
|
this.collections.clear();
|
|
125
152
|
}
|
|
126
|
-
|
|
127
|
-
/* -------------------------------- DEBUG -------------------------------- */
|
|
128
|
-
|
|
129
|
-
getStats() {
|
|
130
|
-
return {
|
|
131
|
-
dbName: this.dbName,
|
|
132
|
-
basePath: this.basePath,
|
|
133
|
-
collections: [...this.collections.keys()]
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
}
|
|
153
|
+
}
|