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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepbase-json",
3
- "version": "3.4.9",
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.9"
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
- return JsonDriver._instances[this.fileName];
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
- // Reads don't modify state, so they can run without queuing
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
- // Execute current operation
152
- const result = await operation();
153
- resolver();
154
- return result;
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
- return new Promise((resolve, reject) => {
189
- const serializedData = this.stringify(this.obj);
190
- steno.writeFile(this.fileName, serializedData, err => {
191
- if (err) reject(err);
192
- else resolve();
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);