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 +3 -3
- package/src/JsonDriver.js +85 -30
- package/test/test.js +97 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deepbase-json",
|
|
3
|
-
"version": "3.1.
|
|
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.
|
|
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
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
103
|
-
return this.
|
|
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
|
|