deepbase-json 3.4.9 → 3.4.11
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/package.json +3 -2
- package/src/JsonDriver.js +55 -14
- package/src/index.d.ts +2 -0
- package/test/test.js +427 -0
- package/test/worker.js +33 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deepbase-json",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.11",
|
|
4
4
|
"description": "⚡ DeepBase JSON - filesystem driver",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.cjs",
|
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
+
"proper-lockfile": "^4.1.2",
|
|
17
18
|
"steno": "^0.4.4"
|
|
18
19
|
},
|
|
19
20
|
"peerDependencies": {
|
|
20
|
-
"deepbase": "^3.4.
|
|
21
|
+
"deepbase": "^3.4.11"
|
|
21
22
|
},
|
|
22
23
|
"scripts": {
|
|
23
24
|
"test": "mocha test/test.js"
|
package/src/JsonDriver.js
CHANGED
|
@@ -2,24 +2,29 @@ import { DeepBaseDriver } from 'deepbase';
|
|
|
2
2
|
import steno from 'steno';
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import * as pathModule from 'path';
|
|
5
|
+
import lockfile from 'proper-lockfile';
|
|
5
6
|
|
|
6
7
|
export class JsonDriver extends DeepBaseDriver {
|
|
7
8
|
static _instances = {};
|
|
8
9
|
|
|
9
|
-
constructor({name, path, stringify, parse, ...opts} = {}) {
|
|
10
|
+
constructor({name, path, stringify, parse, multiProcess, ...opts} = {}) {
|
|
10
11
|
super(opts);
|
|
11
12
|
|
|
12
13
|
this.name = name || "default";
|
|
13
14
|
this.path = path || new URL('../../../db', import.meta.url).pathname;
|
|
14
15
|
this.stringify = stringify || ((obj) => JSON.stringify(obj, null, 4));
|
|
15
16
|
this.parse = parse || JSON.parse;
|
|
17
|
+
this.multiProcess = multiProcess || false;
|
|
16
18
|
|
|
17
19
|
this.path = pathModule.resolve(this.path);
|
|
18
20
|
this.fileName = pathModule.join(this.path, `${this.name}.json`);
|
|
19
21
|
|
|
20
22
|
// Singleton pattern per file
|
|
21
23
|
if (JsonDriver._instances[this.fileName]) {
|
|
22
|
-
|
|
24
|
+
const existing = JsonDriver._instances[this.fileName];
|
|
25
|
+
// Upgrade to multiProcess if requested
|
|
26
|
+
if (multiProcess) existing.multiProcess = true;
|
|
27
|
+
return existing;
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
this.obj = {};
|
|
@@ -38,6 +43,9 @@ export class JsonDriver extends DeepBaseDriver {
|
|
|
38
43
|
if (fs.existsSync(this.fileName)) {
|
|
39
44
|
const fileContent = fs.readFileSync(this.fileName, "utf8");
|
|
40
45
|
this.obj = fileContent ? this.parse(fileContent) : {};
|
|
46
|
+
} else {
|
|
47
|
+
// Create the file so proper-lockfile can lock it in multiProcess mode
|
|
48
|
+
fs.writeFileSync(this.fileName, this.stringify(this.obj));
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
this._connected = true;
|
|
@@ -48,7 +56,10 @@ export class JsonDriver extends DeepBaseDriver {
|
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
async get(...args) {
|
|
51
|
-
//
|
|
59
|
+
// In multiProcess mode, re-read from disk for fresh data
|
|
60
|
+
if (this.multiProcess) {
|
|
61
|
+
this._refreshFromDisk();
|
|
62
|
+
}
|
|
52
63
|
const value = this._getRecursive(this.obj, args.slice());
|
|
53
64
|
return typeof value === 'object' && value !== null
|
|
54
65
|
? this.parse(this.stringify(value))
|
|
@@ -136,6 +147,14 @@ export class JsonDriver extends DeepBaseDriver {
|
|
|
136
147
|
return keys;
|
|
137
148
|
}
|
|
138
149
|
|
|
150
|
+
// Re-read file from disk into this.obj
|
|
151
|
+
_refreshFromDisk() {
|
|
152
|
+
if (fs.existsSync(this.fileName)) {
|
|
153
|
+
const fileContent = fs.readFileSync(this.fileName, "utf8");
|
|
154
|
+
this.obj = fileContent ? this.parse(fileContent) : {};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
139
158
|
// Queue operations to prevent race conditions
|
|
140
159
|
async _queueOperation(operation) {
|
|
141
160
|
const previousOperation = this._operationQueue;
|
|
@@ -148,17 +167,34 @@ export class JsonDriver extends DeepBaseDriver {
|
|
|
148
167
|
try {
|
|
149
168
|
// Wait for previous operation to complete
|
|
150
169
|
await previousOperation;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
170
|
+
|
|
171
|
+
if (this.multiProcess) {
|
|
172
|
+
// Lock the file, re-read, operate, then unlock
|
|
173
|
+
const release = await lockfile.lock(this.fileName, {
|
|
174
|
+
retries: { retries: 10, minTimeout: 50, maxTimeout: 500 },
|
|
175
|
+
stale: 10000
|
|
176
|
+
});
|
|
177
|
+
try {
|
|
178
|
+
this._refreshFromDisk();
|
|
179
|
+
const result = await operation();
|
|
180
|
+
return result;
|
|
181
|
+
} finally {
|
|
182
|
+
await release();
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
const result = await operation();
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
155
188
|
} catch (error) {
|
|
156
|
-
resolver();
|
|
157
189
|
throw error;
|
|
190
|
+
} finally {
|
|
191
|
+
resolver();
|
|
158
192
|
}
|
|
159
193
|
}
|
|
160
194
|
|
|
161
195
|
_setRecursive(obj, keys, value) {
|
|
196
|
+
if (keys.length === 0) return;
|
|
197
|
+
|
|
162
198
|
if (keys.length === 1) {
|
|
163
199
|
obj[keys[0]] = value;
|
|
164
200
|
return;
|
|
@@ -185,13 +221,18 @@ export class JsonDriver extends DeepBaseDriver {
|
|
|
185
221
|
}
|
|
186
222
|
|
|
187
223
|
async _saveToFile() {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
224
|
+
const serializedData = this.stringify(this.obj);
|
|
225
|
+
if (this.multiProcess) {
|
|
226
|
+
// Sync write ensures data is on disk before lock release
|
|
227
|
+
fs.writeFileSync(this.fileName, serializedData);
|
|
228
|
+
} else {
|
|
229
|
+
return new Promise((resolve, reject) => {
|
|
230
|
+
steno.writeFile(this.fileName, serializedData, err => {
|
|
231
|
+
if (err) reject(err);
|
|
232
|
+
else resolve();
|
|
233
|
+
});
|
|
193
234
|
});
|
|
194
|
-
}
|
|
235
|
+
}
|
|
195
236
|
}
|
|
196
237
|
}
|
|
197
238
|
|
package/src/index.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export interface JsonDriverOptions extends DeepBaseDriverOptions {
|
|
|
5
5
|
path?: string;
|
|
6
6
|
stringify?: (obj: any) => string;
|
|
7
7
|
parse?: (str: string) => any;
|
|
8
|
+
/** Enable cross-process file locking for safe multi-process access */
|
|
9
|
+
multiProcess?: boolean;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
export class JsonDriver extends DeepBaseDriver {
|
package/test/test.js
CHANGED
|
@@ -2,8 +2,10 @@ import assert from 'assert';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
|
+
import { fork } from 'child_process';
|
|
5
6
|
import { DeepBase } from '../../core/src/index.js';
|
|
6
7
|
import { JsonDriver } from '../src/JsonDriver.js';
|
|
8
|
+
import { SqliteDriver } from '../../driver-sqlite/src/SqliteDriver.js';
|
|
7
9
|
|
|
8
10
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
11
|
const testDataPath = path.join(__dirname, 'test-data');
|
|
@@ -275,6 +277,98 @@ describe('JsonDriver', function() {
|
|
|
275
277
|
});
|
|
276
278
|
});
|
|
277
279
|
|
|
280
|
+
describe('Queue / Stack (add + pop + shift)', function() {
|
|
281
|
+
it('should work as FIFO queue with add + shift', async function() {
|
|
282
|
+
// Enqueue items
|
|
283
|
+
await db.add('queue', 'first');
|
|
284
|
+
await db.add('queue', 'second');
|
|
285
|
+
await db.add('queue', 'third');
|
|
286
|
+
|
|
287
|
+
// Dequeue in insertion order (FIFO)
|
|
288
|
+
assert.strictEqual(await db.shift('queue'), 'first');
|
|
289
|
+
assert.strictEqual(await db.shift('queue'), 'second');
|
|
290
|
+
assert.strictEqual(await db.shift('queue'), 'third');
|
|
291
|
+
assert.strictEqual(await db.shift('queue'), undefined);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should work as LIFO stack with add + pop', async function() {
|
|
295
|
+
// Push items
|
|
296
|
+
await db.add('stack', 'first');
|
|
297
|
+
await db.add('stack', 'second');
|
|
298
|
+
await db.add('stack', 'third');
|
|
299
|
+
|
|
300
|
+
// Pop in reverse order (LIFO)
|
|
301
|
+
assert.strictEqual(await db.pop('stack'), 'third');
|
|
302
|
+
assert.strictEqual(await db.pop('stack'), 'second');
|
|
303
|
+
assert.strictEqual(await db.pop('stack'), 'first');
|
|
304
|
+
assert.strictEqual(await db.pop('stack'), undefined);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should interleave add and shift correctly', async function() {
|
|
308
|
+
await db.add('q', 'a');
|
|
309
|
+
await db.add('q', 'b');
|
|
310
|
+
assert.strictEqual(await db.shift('q'), 'a');
|
|
311
|
+
|
|
312
|
+
await db.add('q', 'c');
|
|
313
|
+
assert.strictEqual(await db.shift('q'), 'b');
|
|
314
|
+
assert.strictEqual(await db.shift('q'), 'c');
|
|
315
|
+
assert.strictEqual(await db.shift('q'), undefined);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('should interleave add and pop correctly', async function() {
|
|
319
|
+
await db.add('s', 'a');
|
|
320
|
+
await db.add('s', 'b');
|
|
321
|
+
assert.strictEqual(await db.pop('s'), 'b');
|
|
322
|
+
|
|
323
|
+
await db.add('s', 'c');
|
|
324
|
+
assert.strictEqual(await db.pop('s'), 'c');
|
|
325
|
+
assert.strictEqual(await db.pop('s'), 'a');
|
|
326
|
+
assert.strictEqual(await db.pop('s'), undefined);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('should track length correctly through add/pop/shift', async function() {
|
|
330
|
+
await db.add('items', 1);
|
|
331
|
+
await db.add('items', 2);
|
|
332
|
+
await db.add('items', 3);
|
|
333
|
+
assert.strictEqual(await db.len('items'), 3);
|
|
334
|
+
|
|
335
|
+
await db.pop('items');
|
|
336
|
+
assert.strictEqual(await db.len('items'), 2);
|
|
337
|
+
|
|
338
|
+
await db.shift('items');
|
|
339
|
+
assert.strictEqual(await db.len('items'), 1);
|
|
340
|
+
|
|
341
|
+
await db.add('items', 4);
|
|
342
|
+
assert.strictEqual(await db.len('items'), 2);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('should preserve values order with add + values()', async function() {
|
|
346
|
+
await db.add('list', 'x');
|
|
347
|
+
await db.add('list', 'y');
|
|
348
|
+
await db.add('list', 'z');
|
|
349
|
+
|
|
350
|
+
const vals = await db.values('list');
|
|
351
|
+
assert.deepStrictEqual(vals, ['x', 'y', 'z']);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('should handle objects in queue', async function() {
|
|
355
|
+
await db.add('tasks', { task: 'build', priority: 1 });
|
|
356
|
+
await db.add('tasks', { task: 'test', priority: 2 });
|
|
357
|
+
await db.add('tasks', { task: 'deploy', priority: 3 });
|
|
358
|
+
|
|
359
|
+
const first = await db.shift('tasks');
|
|
360
|
+
assert.deepStrictEqual(first, { task: 'build', priority: 1 });
|
|
361
|
+
|
|
362
|
+
const last = await db.pop('tasks');
|
|
363
|
+
assert.deepStrictEqual(last, { task: 'deploy', priority: 3 });
|
|
364
|
+
|
|
365
|
+
assert.strictEqual(await db.len('tasks'), 1);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// Limitation: pop/shift are designed for object-based collections (via add()),
|
|
369
|
+
// not native JS arrays. delete arr[i] leaves a hole instead of shrinking the array.
|
|
370
|
+
});
|
|
371
|
+
|
|
278
372
|
describe('Singleton Pattern', function() {
|
|
279
373
|
it('should return same instance for same file', function() {
|
|
280
374
|
const driver1 = new JsonDriver({ name: 'singleton', path: testDataPath });
|
|
@@ -387,5 +481,338 @@ describe('JsonDriver', function() {
|
|
|
387
481
|
assert.strictEqual(result, 500, 'All decrements should be applied atomically');
|
|
388
482
|
});
|
|
389
483
|
});
|
|
484
|
+
|
|
485
|
+
describe('Multi-Process Safety (multiProcess: true)', function() {
|
|
486
|
+
const workerPath = path.join(__dirname, 'worker.js');
|
|
487
|
+
const multiProcessPath = path.join(testDataPath, 'multi-process');
|
|
488
|
+
|
|
489
|
+
function spawnWorker(args) {
|
|
490
|
+
return new Promise((resolve, reject) => {
|
|
491
|
+
const child = fork(workerPath, [JSON.stringify(args)], {
|
|
492
|
+
stdio: 'pipe'
|
|
493
|
+
});
|
|
494
|
+
let stderr = '';
|
|
495
|
+
child.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
496
|
+
child.on('exit', (code) => {
|
|
497
|
+
if (code === 0) resolve();
|
|
498
|
+
else reject(new Error(`Worker exited with code ${code}: ${stderr}`));
|
|
499
|
+
});
|
|
500
|
+
child.on('error', reject);
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
beforeEach(function() {
|
|
505
|
+
// Clean singleton cache so multiProcess instances are fresh
|
|
506
|
+
JsonDriver._instances = {};
|
|
507
|
+
if (fs.existsSync(multiProcessPath)) {
|
|
508
|
+
fs.rmSync(multiProcessPath, { recursive: true, force: true });
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
afterEach(function() {
|
|
513
|
+
JsonDriver._instances = {};
|
|
514
|
+
if (fs.existsSync(multiProcessPath)) {
|
|
515
|
+
fs.rmSync(multiProcessPath, { recursive: true, force: true });
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it('should handle concurrent increments from multiple processes', async function() {
|
|
520
|
+
this.timeout(30000);
|
|
521
|
+
|
|
522
|
+
const dbName = 'mp-inc-test';
|
|
523
|
+
const numProcesses = 4;
|
|
524
|
+
const iterationsPerProcess = 25;
|
|
525
|
+
|
|
526
|
+
// Initialize the file with counter = 0
|
|
527
|
+
const driver = new JsonDriver({ name: dbName, path: multiProcessPath, multiProcess: true });
|
|
528
|
+
await driver.connect();
|
|
529
|
+
await driver.set('counter', 0);
|
|
530
|
+
await driver.disconnect();
|
|
531
|
+
JsonDriver._instances = {};
|
|
532
|
+
|
|
533
|
+
// Spawn N processes that each increment counter M times
|
|
534
|
+
const workers = Array.from({ length: numProcesses }, () =>
|
|
535
|
+
spawnWorker({ name: dbName, path: multiProcessPath, task: 'increment', iterations: iterationsPerProcess })
|
|
536
|
+
);
|
|
537
|
+
await Promise.all(workers);
|
|
538
|
+
|
|
539
|
+
// Read the final value
|
|
540
|
+
const verifyDriver = new JsonDriver({ name: dbName, path: multiProcessPath, multiProcess: true });
|
|
541
|
+
await verifyDriver.connect();
|
|
542
|
+
const result = await verifyDriver.get('counter');
|
|
543
|
+
await verifyDriver.disconnect();
|
|
544
|
+
JsonDriver._instances = {};
|
|
545
|
+
|
|
546
|
+
assert.strictEqual(result, numProcesses * iterationsPerProcess,
|
|
547
|
+
`Expected counter to be ${numProcesses * iterationsPerProcess}, got ${result}`);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('should handle concurrent inc() from multiple processes', async function() {
|
|
551
|
+
this.timeout(30000);
|
|
552
|
+
|
|
553
|
+
const dbName = 'mp-atomic-inc-test';
|
|
554
|
+
const numProcesses = 4;
|
|
555
|
+
const iterationsPerProcess = 25;
|
|
556
|
+
|
|
557
|
+
// Initialize
|
|
558
|
+
const driver = new JsonDriver({ name: dbName, path: multiProcessPath, multiProcess: true });
|
|
559
|
+
await driver.connect();
|
|
560
|
+
await driver.set('counter', 0);
|
|
561
|
+
await driver.disconnect();
|
|
562
|
+
JsonDriver._instances = {};
|
|
563
|
+
|
|
564
|
+
// Spawn processes using inc()
|
|
565
|
+
const workers = Array.from({ length: numProcesses }, () =>
|
|
566
|
+
spawnWorker({ name: dbName, path: multiProcessPath, task: 'inc', iterations: iterationsPerProcess })
|
|
567
|
+
);
|
|
568
|
+
await Promise.all(workers);
|
|
569
|
+
|
|
570
|
+
// Verify
|
|
571
|
+
const verifyDriver = new JsonDriver({ name: dbName, path: multiProcessPath, multiProcess: true });
|
|
572
|
+
await verifyDriver.connect();
|
|
573
|
+
const result = await verifyDriver.get('counter');
|
|
574
|
+
await verifyDriver.disconnect();
|
|
575
|
+
JsonDriver._instances = {};
|
|
576
|
+
|
|
577
|
+
assert.strictEqual(result, numProcesses * iterationsPerProcess,
|
|
578
|
+
`Expected counter to be ${numProcesses * iterationsPerProcess}, got ${result}`);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it('should preserve writes from different processes on different keys', async function() {
|
|
582
|
+
this.timeout(30000);
|
|
583
|
+
|
|
584
|
+
const dbName = 'mp-keys-test';
|
|
585
|
+
const numProcesses = 3;
|
|
586
|
+
const iterationsPerProcess = 10;
|
|
587
|
+
|
|
588
|
+
// Initialize
|
|
589
|
+
const driver = new JsonDriver({ name: dbName, path: multiProcessPath, multiProcess: true });
|
|
590
|
+
await driver.connect();
|
|
591
|
+
await driver.set('entries', {});
|
|
592
|
+
await driver.disconnect();
|
|
593
|
+
JsonDriver._instances = {};
|
|
594
|
+
|
|
595
|
+
// Spawn processes that each write unique keys
|
|
596
|
+
const workers = Array.from({ length: numProcesses }, () =>
|
|
597
|
+
spawnWorker({ name: dbName, path: multiProcessPath, task: 'set-unique', iterations: iterationsPerProcess })
|
|
598
|
+
);
|
|
599
|
+
await Promise.all(workers);
|
|
600
|
+
|
|
601
|
+
// Verify all keys are present
|
|
602
|
+
const verifyDriver = new JsonDriver({ name: dbName, path: multiProcessPath, multiProcess: true });
|
|
603
|
+
await verifyDriver.connect();
|
|
604
|
+
const entries = await verifyDriver.get('entries');
|
|
605
|
+
await verifyDriver.disconnect();
|
|
606
|
+
JsonDriver._instances = {};
|
|
607
|
+
|
|
608
|
+
const totalKeys = Object.keys(entries).length;
|
|
609
|
+
const expected = numProcesses * iterationsPerProcess;
|
|
610
|
+
assert.strictEqual(totalKeys, expected,
|
|
611
|
+
`Expected ${expected} entries, got ${totalKeys}`);
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
it('should work correctly in single-process multiProcess mode', async function() {
|
|
615
|
+
this.timeout(5000);
|
|
616
|
+
|
|
617
|
+
const mpDb = new DeepBase(new JsonDriver({
|
|
618
|
+
name: 'mp-single-test',
|
|
619
|
+
path: multiProcessPath,
|
|
620
|
+
multiProcess: true
|
|
621
|
+
}));
|
|
622
|
+
await mpDb.connect();
|
|
623
|
+
|
|
624
|
+
// Basic operations should still work
|
|
625
|
+
await mpDb.set('key', 'value');
|
|
626
|
+
assert.strictEqual(await mpDb.get('key'), 'value');
|
|
627
|
+
|
|
628
|
+
await mpDb.set('counter', 0);
|
|
629
|
+
const promises = Array.from({ length: 50 }, () => mpDb.inc('counter', 1));
|
|
630
|
+
await Promise.all(promises);
|
|
631
|
+
assert.strictEqual(await mpDb.get('counter'), 50);
|
|
632
|
+
|
|
633
|
+
await mpDb.set('user', 'name', 'Alice');
|
|
634
|
+
await mpDb.del('user', 'name');
|
|
635
|
+
const user = await mpDb.get('user');
|
|
636
|
+
assert.deepStrictEqual(user, {});
|
|
637
|
+
|
|
638
|
+
await mpDb.disconnect();
|
|
639
|
+
JsonDriver._instances = {};
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
describe('Multi-Driver: JsonDriver + SqliteDriver (add + pop + shift)', function() {
|
|
645
|
+
const multiDataPath = path.join(__dirname, 'test-data-multi');
|
|
646
|
+
let db, jsonDriver, sqliteDriver;
|
|
647
|
+
let testCounter = 0;
|
|
648
|
+
|
|
649
|
+
before(function() {
|
|
650
|
+
if (fs.existsSync(multiDataPath)) {
|
|
651
|
+
fs.rmSync(multiDataPath, { recursive: true, force: true });
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
beforeEach(async function() {
|
|
656
|
+
testCounter++;
|
|
657
|
+
JsonDriver._instances = {};
|
|
658
|
+
SqliteDriver._instances = {};
|
|
659
|
+
|
|
660
|
+
jsonDriver = new JsonDriver({ name: `multi-${testCounter}`, path: multiDataPath });
|
|
661
|
+
sqliteDriver = new SqliteDriver({ name: `multi-${testCounter}`, path: multiDataPath });
|
|
662
|
+
|
|
663
|
+
db = new DeepBase([jsonDriver, sqliteDriver]);
|
|
664
|
+
await db.connect();
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
afterEach(async function() {
|
|
668
|
+
await db.disconnect();
|
|
669
|
+
JsonDriver._instances = {};
|
|
670
|
+
SqliteDriver._instances = {};
|
|
671
|
+
if (fs.existsSync(multiDataPath)) {
|
|
672
|
+
fs.rmSync(multiDataPath, { recursive: true, force: true });
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
it('should write to both drivers on set', async function() {
|
|
677
|
+
await db.set('key', 'value');
|
|
678
|
+
|
|
679
|
+
const fromJson = await jsonDriver.get('key');
|
|
680
|
+
const fromSqlite = await sqliteDriver.get('key');
|
|
681
|
+
|
|
682
|
+
assert.strictEqual(fromJson, 'value');
|
|
683
|
+
assert.strictEqual(fromSqlite, 'value');
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it('should keep both drivers in sync after add + pop (FIFO via shift)', async function() {
|
|
687
|
+
// Add items
|
|
688
|
+
await db.add('queue', 'first');
|
|
689
|
+
await db.add('queue', 'second');
|
|
690
|
+
await db.add('queue', 'third');
|
|
691
|
+
|
|
692
|
+
// Check both drivers have same number of items
|
|
693
|
+
const jsonBefore = await jsonDriver.get('queue');
|
|
694
|
+
const sqliteBefore = await sqliteDriver.get('queue');
|
|
695
|
+
assert.strictEqual(Object.keys(jsonBefore).length, 3, 'JSON should have 3 items');
|
|
696
|
+
assert.strictEqual(Object.keys(sqliteBefore).length, 3, 'SQLite should have 3 items');
|
|
697
|
+
|
|
698
|
+
// Check that both drivers have the same keys (same IDs)
|
|
699
|
+
const jsonKeys = Object.keys(jsonBefore).sort();
|
|
700
|
+
const sqliteKeys = Object.keys(sqliteBefore).sort();
|
|
701
|
+
assert.deepStrictEqual(jsonKeys, sqliteKeys, 'Both drivers should have the same keys');
|
|
702
|
+
|
|
703
|
+
// Shift (FIFO dequeue)
|
|
704
|
+
const shifted = await db.shift('queue');
|
|
705
|
+
assert.strictEqual(shifted, 'first');
|
|
706
|
+
|
|
707
|
+
// Both drivers should have 2 items remaining
|
|
708
|
+
const jsonAfter = await jsonDriver.get('queue');
|
|
709
|
+
const sqliteAfter = await sqliteDriver.get('queue');
|
|
710
|
+
assert.strictEqual(Object.keys(jsonAfter).length, 2, 'JSON should have 2 items after shift');
|
|
711
|
+
assert.strictEqual(Object.keys(sqliteAfter).length, 2, 'SQLite should have 2 items after shift');
|
|
712
|
+
|
|
713
|
+
// Remaining values should match
|
|
714
|
+
assert.deepStrictEqual(Object.values(jsonAfter), ['second', 'third']);
|
|
715
|
+
assert.deepStrictEqual(Object.values(sqliteAfter), ['second', 'third']);
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it('should keep both drivers in sync after add + pop (LIFO)', async function() {
|
|
719
|
+
await db.add('stack', 'a');
|
|
720
|
+
await db.add('stack', 'b');
|
|
721
|
+
await db.add('stack', 'c');
|
|
722
|
+
|
|
723
|
+
// Pop (LIFO)
|
|
724
|
+
const popped = await db.pop('stack');
|
|
725
|
+
assert.strictEqual(popped, 'c');
|
|
726
|
+
|
|
727
|
+
// Both should have 2 items
|
|
728
|
+
const jsonAfter = await jsonDriver.get('stack');
|
|
729
|
+
const sqliteAfter = await sqliteDriver.get('stack');
|
|
730
|
+
assert.strictEqual(Object.keys(jsonAfter).length, 2, 'JSON should have 2 items after pop');
|
|
731
|
+
assert.strictEqual(Object.keys(sqliteAfter).length, 2, 'SQLite should have 2 items after pop');
|
|
732
|
+
|
|
733
|
+
// Same keys and values in both
|
|
734
|
+
assert.deepStrictEqual(Object.keys(jsonAfter).sort(), Object.keys(sqliteAfter).sort());
|
|
735
|
+
assert.deepStrictEqual(Object.values(jsonAfter), ['a', 'b']);
|
|
736
|
+
assert.deepStrictEqual(Object.values(sqliteAfter), ['a', 'b']);
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
it('should drain queue completely from both drivers', async function() {
|
|
740
|
+
await db.add('q', 1);
|
|
741
|
+
await db.add('q', 2);
|
|
742
|
+
await db.add('q', 3);
|
|
743
|
+
|
|
744
|
+
assert.strictEqual(await db.shift('q'), 1);
|
|
745
|
+
assert.strictEqual(await db.shift('q'), 2);
|
|
746
|
+
assert.strictEqual(await db.shift('q'), 3);
|
|
747
|
+
assert.strictEqual(await db.shift('q'), undefined);
|
|
748
|
+
|
|
749
|
+
// Both drivers should be empty (JSON returns {}, SQLite returns null when all keys deleted)
|
|
750
|
+
const jsonData = await jsonDriver.get('q');
|
|
751
|
+
const sqliteData = await sqliteDriver.get('q');
|
|
752
|
+
const jsonLen = jsonData ? Object.keys(jsonData).length : 0;
|
|
753
|
+
const sqliteLen = sqliteData ? Object.keys(sqliteData).length : 0;
|
|
754
|
+
assert.strictEqual(jsonLen, 0, 'JSON queue should be empty');
|
|
755
|
+
assert.strictEqual(sqliteLen, 0, 'SQLite queue should be empty');
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
it('should handle interleaved add + pop across both drivers', async function() {
|
|
759
|
+
await db.add('items', 'x');
|
|
760
|
+
await db.add('items', 'y');
|
|
761
|
+
assert.strictEqual(await db.pop('items'), 'y');
|
|
762
|
+
|
|
763
|
+
await db.add('items', 'z');
|
|
764
|
+
assert.strictEqual(await db.pop('items'), 'z');
|
|
765
|
+
assert.strictEqual(await db.pop('items'), 'x');
|
|
766
|
+
assert.strictEqual(await db.pop('items'), undefined);
|
|
767
|
+
|
|
768
|
+
// Both empty (JSON returns {}, SQLite returns null when all keys deleted)
|
|
769
|
+
const jsonData = await jsonDriver.get('items');
|
|
770
|
+
const sqliteData = await sqliteDriver.get('items');
|
|
771
|
+
const jsonLen = jsonData ? Object.keys(jsonData).length : 0;
|
|
772
|
+
const sqliteLen = sqliteData ? Object.keys(sqliteData).length : 0;
|
|
773
|
+
assert.strictEqual(jsonLen, 0, 'JSON items should be empty');
|
|
774
|
+
assert.strictEqual(sqliteLen, 0, 'SQLite items should be empty');
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
it('should maintain consistent len() across both drivers', async function() {
|
|
778
|
+
await db.add('list', 'a');
|
|
779
|
+
await db.add('list', 'b');
|
|
780
|
+
await db.add('list', 'c');
|
|
781
|
+
|
|
782
|
+
assert.strictEqual(await db.len('list'), 3);
|
|
783
|
+
|
|
784
|
+
// Check each driver directly
|
|
785
|
+
const jsonList = await jsonDriver.get('list');
|
|
786
|
+
const sqliteList = await sqliteDriver.get('list');
|
|
787
|
+
assert.strictEqual(Object.keys(jsonList).length, 3);
|
|
788
|
+
assert.strictEqual(Object.keys(sqliteList).length, 3);
|
|
789
|
+
|
|
790
|
+
await db.pop('list');
|
|
791
|
+
|
|
792
|
+
const jsonAfter = await jsonDriver.get('list');
|
|
793
|
+
const sqliteAfter = await sqliteDriver.get('list');
|
|
794
|
+
assert.strictEqual(Object.keys(jsonAfter).length, 2);
|
|
795
|
+
assert.strictEqual(Object.keys(sqliteAfter).length, 2);
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
it('should handle objects in multi-driver queue', async function() {
|
|
799
|
+
await db.add('tasks', { name: 'build', prio: 1 });
|
|
800
|
+
await db.add('tasks', { name: 'test', prio: 2 });
|
|
801
|
+
await db.add('tasks', { name: 'deploy', prio: 3 });
|
|
802
|
+
|
|
803
|
+
const first = await db.shift('tasks');
|
|
804
|
+
assert.deepStrictEqual(first, { name: 'build', prio: 1 });
|
|
805
|
+
|
|
806
|
+
const last = await db.pop('tasks');
|
|
807
|
+
assert.deepStrictEqual(last, { name: 'deploy', prio: 3 });
|
|
808
|
+
|
|
809
|
+
// Both drivers should have 1 item left
|
|
810
|
+
const jsonTasks = await jsonDriver.get('tasks');
|
|
811
|
+
const sqliteTasks = await sqliteDriver.get('tasks');
|
|
812
|
+
assert.strictEqual(Object.keys(jsonTasks).length, 1);
|
|
813
|
+
assert.strictEqual(Object.keys(sqliteTasks).length, 1);
|
|
814
|
+
assert.deepStrictEqual(Object.values(jsonTasks)[0], { name: 'test', prio: 2 });
|
|
815
|
+
assert.deepStrictEqual(Object.values(sqliteTasks)[0], { name: 'test', prio: 2 });
|
|
816
|
+
});
|
|
390
817
|
});
|
|
391
818
|
|
package/test/worker.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker script for multi-process tests.
|
|
3
|
+
* Receives commands via IPC and executes them against a JsonDriver instance.
|
|
4
|
+
*/
|
|
5
|
+
import { JsonDriver } from '../src/JsonDriver.js';
|
|
6
|
+
|
|
7
|
+
const { name, path, task, iterations } = JSON.parse(process.argv[2]);
|
|
8
|
+
|
|
9
|
+
const driver = new JsonDriver({ name, path, multiProcess: true });
|
|
10
|
+
await driver.connect();
|
|
11
|
+
|
|
12
|
+
if (task === 'increment') {
|
|
13
|
+
// Perform N sequential increments using upd (atomic read-modify-write)
|
|
14
|
+
for (let i = 0; i < iterations; i++) {
|
|
15
|
+
await driver.upd('counter', (current) => (current || 0) + 1);
|
|
16
|
+
}
|
|
17
|
+
} else if (task === 'set-unique') {
|
|
18
|
+
// Set unique keys per process
|
|
19
|
+
const pid = process.pid;
|
|
20
|
+
for (let i = 0; i < iterations; i++) {
|
|
21
|
+
await driver.set('entries', `p${pid}_${i}`, { pid, index: i });
|
|
22
|
+
}
|
|
23
|
+
} else if (task === 'inc') {
|
|
24
|
+
// Use the driver's inc method
|
|
25
|
+
for (let i = 0; i < iterations; i++) {
|
|
26
|
+
await driver.inc('counter', 1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
await driver.disconnect();
|
|
31
|
+
|
|
32
|
+
// Signal success
|
|
33
|
+
process.exit(0);
|