deepbase-json 3.1.2 → 3.1.6

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.1.2",
3
+ "version": "3.1.6",
4
4
  "description": "⚡ DeepBase JSON - filesystem driver",
5
5
  "type": "module",
6
6
  "main": "src/index.cjs",
@@ -15,7 +15,7 @@
15
15
  "steno": "^0.4.4"
16
16
  },
17
17
  "peerDependencies": {
18
- "deepbase": "^3.1.2"
18
+ "deepbase": "^3.1.6"
19
19
  },
20
20
  "scripts": {
21
21
  "test": "mocha test/test.js"
@@ -42,5 +42,5 @@
42
42
  "bugs": {
43
43
  "url": "https://github.com/clasen/DeepBase/issues"
44
44
  },
45
- "homepage": "https://github.com/clasen/DeepBase#readme"
45
+ "homepage": "https://github.com/clasen/DeepBase/tree/main/packages/driver-json"
46
46
  }
package/src/JsonDriver.js CHANGED
@@ -23,6 +23,10 @@ export class JsonDriver extends DeepBaseDriver {
23
23
  }
24
24
 
25
25
  this.obj = {};
26
+ // Queue for serializing concurrent operations
27
+ this._operationQueue = Promise.resolve();
28
+ this._queueLock = false;
29
+
26
30
  JsonDriver._instances[this.fileName] = this;
27
31
  }
28
32
 
@@ -44,6 +48,7 @@ export class JsonDriver extends DeepBaseDriver {
44
48
  }
45
49
 
46
50
  async get(...args) {
51
+ // Reads don't modify state, so they can run without queuing
47
52
  const value = this._getRecursive(this.obj, args.slice());
48
53
  return typeof value === 'object' && value !== null
49
54
  ? this.parse(this.stringify(value))
@@ -51,34 +56,38 @@ export class JsonDriver extends DeepBaseDriver {
51
56
  }
52
57
 
53
58
  async set(...args) {
54
- if (args.length < 2) {
55
- this.obj = args[0];
59
+ return this._queueOperation(async () => {
60
+ if (args.length < 2) {
61
+ this.obj = args[0];
62
+ await this._saveToFile();
63
+ return [];
64
+ }
65
+
66
+ const keys = args.slice(0, -1);
67
+ const value = args[args.length - 1];
68
+
69
+ // Make a copy to avoid modifying the original array
70
+ this._setRecursive(this.obj, keys.slice(), value);
56
71
  await this._saveToFile();
57
- return [];
58
- }
59
-
60
- const keys = args.slice(0, -1);
61
- const value = args[args.length - 1];
62
-
63
- // Make a copy to avoid modifying the original array
64
- this._setRecursive(this.obj, keys.slice(), value);
65
- await this._saveToFile();
66
- return keys;
72
+ return keys;
73
+ });
67
74
  }
68
75
 
69
76
  async del(...keys) {
70
- if (keys.length === 0) {
71
- this.obj = {};
72
- return this._saveToFile();
73
- }
74
-
75
- const key = keys.pop();
76
- const parentObj = this._getRecursive(this.obj, keys.slice());
77
-
78
- if (parentObj && parentObj.hasOwnProperty(key)) {
79
- delete parentObj[key];
80
- return this._saveToFile();
81
- }
77
+ return this._queueOperation(async () => {
78
+ if (keys.length === 0) {
79
+ this.obj = {};
80
+ return this._saveToFile();
81
+ }
82
+
83
+ const key = keys.pop();
84
+ const parentObj = this._getRecursive(this.obj, keys.slice());
85
+
86
+ if (parentObj && parentObj.hasOwnProperty(key)) {
87
+ delete parentObj[key];
88
+ return this._saveToFile();
89
+ }
90
+ });
82
91
  }
83
92
 
84
93
  async inc(...args) {
@@ -92,15 +101,61 @@ export class JsonDriver extends DeepBaseDriver {
92
101
  }
93
102
 
94
103
  async add(...keys) {
95
- const value = keys.pop();
96
- const id = this.nanoid();
97
- await this.set(...[...keys, id], value);
98
- return [...keys, id];
104
+ return this._queueOperation(async () => {
105
+ const value = keys.pop();
106
+ const id = this.nanoid();
107
+ await this._setInternal(...[...keys, id], value);
108
+ return [...keys, id];
109
+ });
99
110
  }
100
111
 
101
112
  async upd(...args) {
102
- const func = args.pop();
103
- return this.set(...args, func(await this.get(...args)));
113
+ // Queue the entire get+update+set operation to make it atomic
114
+ return this._queueOperation(async () => {
115
+ const func = args.pop();
116
+ const currentValue = this._getRecursive(this.obj, args.slice());
117
+ const newValue = func(currentValue);
118
+ await this._setInternal(...args, newValue);
119
+ return args;
120
+ });
121
+ }
122
+
123
+ // Internal set without queuing (for use within queued operations)
124
+ async _setInternal(...args) {
125
+ if (args.length < 2) {
126
+ this.obj = args[0];
127
+ await this._saveToFile();
128
+ return [];
129
+ }
130
+
131
+ const keys = args.slice(0, -1);
132
+ const value = args[args.length - 1];
133
+
134
+ this._setRecursive(this.obj, keys.slice(), value);
135
+ await this._saveToFile();
136
+ return keys;
137
+ }
138
+
139
+ // Queue operations to prevent race conditions
140
+ async _queueOperation(operation) {
141
+ const previousOperation = this._operationQueue;
142
+
143
+ let resolver;
144
+ this._operationQueue = new Promise(resolve => {
145
+ resolver = resolve;
146
+ });
147
+
148
+ try {
149
+ // Wait for previous operation to complete
150
+ await previousOperation;
151
+ // Execute current operation
152
+ const result = await operation();
153
+ resolver();
154
+ return result;
155
+ } catch (error) {
156
+ resolver();
157
+ throw error;
158
+ }
104
159
  }
105
160
 
106
161
  _setRecursive(obj, keys, value) {
package/test/test.js CHANGED
@@ -290,5 +290,102 @@ describe('JsonDriver', function() {
290
290
  assert.notStrictEqual(driver1, driver2);
291
291
  });
292
292
  });
293
+
294
+ describe('Race Conditions', function() {
295
+ it('should handle 100 concurrent increments correctly', async function() {
296
+ this.timeout(5000);
297
+
298
+ await db.set('counter', 0);
299
+
300
+ // Run 100 concurrent increments
301
+ const promises = Array.from({ length: 100 }, () => db.inc('counter', 1));
302
+ await Promise.all(promises);
303
+
304
+ const result = await db.get('counter');
305
+ assert.strictEqual(result, 100, 'All increments should be applied atomically');
306
+ });
307
+
308
+ it('should handle concurrent read-modify-write operations', async function() {
309
+ this.timeout(5000);
310
+
311
+ await db.set('data', { counter: 0, items: [] });
312
+
313
+ // Run 50 concurrent updates that read, modify, and write
314
+ const promises = Array.from({ length: 50 }, (_, i) =>
315
+ db.upd('data', (current) => ({
316
+ counter: current.counter + 1,
317
+ items: [...current.items, `item-${current.counter}`]
318
+ }))
319
+ );
320
+
321
+ await Promise.all(promises);
322
+
323
+ const finalData = await db.get('data');
324
+ assert.strictEqual(finalData.counter, 50, 'Counter should be exactly 50');
325
+ assert.strictEqual(finalData.items.length, 50, 'Should have exactly 50 items');
326
+
327
+ // Check that all items have unique values
328
+ const uniqueItems = new Set(finalData.items);
329
+ assert.strictEqual(uniqueItems.size, 50, 'All items should be unique');
330
+ });
331
+
332
+ it('should handle concurrent sets on different keys', async function() {
333
+ this.timeout(5000);
334
+
335
+ // Run 50 concurrent set operations on different keys
336
+ const promises = Array.from({ length: 50 }, (_, i) =>
337
+ db.set('users', `user${i}`, { name: `User ${i}`, value: i })
338
+ );
339
+
340
+ await Promise.all(promises);
341
+
342
+ // Verify all keys exist and have correct values
343
+ const users = await db.get('users');
344
+ assert.strictEqual(Object.keys(users).length, 50, 'Should have 50 users');
345
+
346
+ for (let i = 0; i < 50; i++) {
347
+ assert.deepStrictEqual(
348
+ users[`user${i}`],
349
+ { name: `User ${i}`, value: i },
350
+ `User ${i} should have correct data`
351
+ );
352
+ }
353
+ });
354
+
355
+ it('should handle concurrent add operations with unique IDs', async function() {
356
+ this.timeout(5000);
357
+
358
+ await db.set('items', {});
359
+
360
+ // Run 50 concurrent add operations
361
+ const promises = Array.from({ length: 50 }, (_, i) =>
362
+ db.add('items', { value: i })
363
+ );
364
+
365
+ const results = await Promise.all(promises);
366
+
367
+ // Verify all items were added with unique IDs
368
+ const items = await db.get('items');
369
+ assert.strictEqual(Object.keys(items).length, 50, 'Should have 50 items');
370
+
371
+ // Check that all IDs are unique
372
+ const ids = results.map(r => r[r.length - 1]);
373
+ const uniqueIds = new Set(ids);
374
+ assert.strictEqual(uniqueIds.size, 50, 'All IDs should be unique');
375
+ });
376
+
377
+ it('should handle concurrent decrements correctly', async function() {
378
+ this.timeout(5000);
379
+
380
+ await db.set('inventory', 1000);
381
+
382
+ // Run 100 concurrent decrements
383
+ const promises = Array.from({ length: 100 }, () => db.dec('inventory', 5));
384
+ await Promise.all(promises);
385
+
386
+ const result = await db.get('inventory');
387
+ assert.strictEqual(result, 500, 'All decrements should be applied atomically');
388
+ });
389
+ });
293
390
  });
294
391