deepbase-json 3.8.1 → 3.8.4

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.8.1",
3
+ "version": "3.8.4",
4
4
  "description": "⚡ DeepBase JSON - filesystem driver",
5
5
  "type": "module",
6
6
  "main": "src/index.cjs",
@@ -18,7 +18,7 @@
18
18
  "steno": "^0.4.4"
19
19
  },
20
20
  "peerDependencies": {
21
- "deepbase": "^3.8.1"
21
+ "deepbase": "^3.8.4"
22
22
  },
23
23
  "devDependencies": {
24
24
  "mocha": "^10.8.2"
package/src/JsonDriver.js CHANGED
@@ -7,7 +7,16 @@ import lockfile from 'proper-lockfile';
7
7
  export class JsonDriver extends DeepBaseDriver {
8
8
  static _instances = {};
9
9
 
10
- constructor({name, path, stringify, parse, multiProcess, ...opts} = {}) {
10
+ constructor({
11
+ name,
12
+ path,
13
+ stringify,
14
+ parse,
15
+ multiProcess,
16
+ encodeForMemory,
17
+ decodeFromMemory,
18
+ ...opts
19
+ } = {}) {
11
20
  super(opts);
12
21
 
13
22
  this.name = name || "default";
@@ -15,6 +24,8 @@ export class JsonDriver extends DeepBaseDriver {
15
24
  this.stringify = stringify || ((obj) => JSON.stringify(obj, null, 4));
16
25
  this.parse = parse || JSON.parse;
17
26
  this.multiProcess = multiProcess || false;
27
+ this.encodeForMemory = encodeForMemory || (value => value);
28
+ this.decodeFromMemory = decodeFromMemory || (value => value);
18
29
 
19
30
  this.path = pathModule.resolve(this.path);
20
31
  this.fileName = pathModule.join(this.path, `${this.name}.json`);
@@ -31,11 +42,15 @@ export class JsonDriver extends DeepBaseDriver {
31
42
  // Queue for serializing concurrent operations
32
43
  this._operationQueue = Promise.resolve();
33
44
  this._queueLock = false;
45
+ this._disposing = false;
46
+ this._disposed = false;
47
+ this._disposePromise = null;
34
48
 
35
49
  JsonDriver._instances[this.fileName] = this;
36
50
  }
37
51
 
38
52
  _connectSync() {
53
+ this._assertUsable();
39
54
  if (this._connected) return;
40
55
 
41
56
  if (!fs.existsSync(this.path)) {
@@ -44,10 +59,11 @@ export class JsonDriver extends DeepBaseDriver {
44
59
 
45
60
  if (fs.existsSync(this.fileName)) {
46
61
  const fileContent = fs.readFileSync(this.fileName, "utf8");
47
- this.obj = fileContent ? this.parse(fileContent) : {};
62
+ const parsed = fileContent ? this.parse(fileContent) : {};
63
+ this.obj = this._encodeForMemory(parsed, []);
48
64
  } else {
49
65
  // Create the file so proper-lockfile can lock it in multiProcess mode
50
- fs.writeFileSync(this.fileName, this.stringify(this.obj));
66
+ fs.writeFileSync(this.fileName, this._serializeMemory());
51
67
  }
52
68
 
53
69
  this._connected = true;
@@ -58,19 +74,25 @@ export class JsonDriver extends DeepBaseDriver {
58
74
  }
59
75
 
60
76
  async disconnect() {
77
+ if (this._disposed) {
78
+ return;
79
+ }
80
+ if (this._disposePromise) {
81
+ return this._disposePromise;
82
+ }
83
+ await this._operationQueue;
61
84
  await this._saveToFile();
62
85
  this._connected = false;
63
86
  }
64
87
 
65
88
  async get(...args) {
89
+ this._assertUsable();
66
90
  // In multiProcess mode, re-read from disk for fresh data
67
91
  if (this.multiProcess) {
68
92
  this._refreshFromDisk();
69
93
  }
70
94
  const value = this._getRecursive(this.obj, args.slice());
71
- return typeof value === 'object' && value !== null
72
- ? this.parse(this.stringify(value))
73
- : value;
95
+ return this._decodeFromMemory(value, args);
74
96
  }
75
97
 
76
98
  getSync(...args) {
@@ -79,21 +101,20 @@ export class JsonDriver extends DeepBaseDriver {
79
101
  }
80
102
  this._connectSync();
81
103
  const value = this._getRecursive(this.obj, args.slice());
82
- return typeof value === 'object' && value !== null
83
- ? this.parse(this.stringify(value))
84
- : value;
104
+ return this._decodeFromMemory(value, args);
85
105
  }
86
106
 
87
107
  async set(...args) {
88
108
  return this._queueOperation(async () => {
89
109
  if (args.length < 2) {
90
- this.obj = args[0];
110
+ const value = this._encodeForMemory(args[0], []);
111
+ this.obj = value;
91
112
  await this._saveToFile();
92
113
  return [];
93
114
  }
94
115
 
95
116
  const keys = args.slice(0, -1);
96
- const value = args[args.length - 1];
117
+ const value = this._encodeForMemory(args[args.length - 1], keys);
97
118
 
98
119
  // Make a copy to avoid modifying the original array
99
120
  this._setRecursive(this.obj, keys.slice(), value);
@@ -143,7 +164,8 @@ export class JsonDriver extends DeepBaseDriver {
143
164
  return this._queueOperation(async () => {
144
165
  const func = args.pop();
145
166
  const currentValue = this._getRecursive(this.obj, args.slice());
146
- const newValue = func(currentValue);
167
+ const decodedValue = this._decodeFromMemory(currentValue, args);
168
+ const newValue = func(decodedValue);
147
169
  await this._setInternal(...args, newValue);
148
170
  return args;
149
171
  });
@@ -191,13 +213,14 @@ export class JsonDriver extends DeepBaseDriver {
191
213
  // Internal set without queuing (for use within queued operations)
192
214
  async _setInternal(...args) {
193
215
  if (args.length < 2) {
194
- this.obj = args[0];
216
+ const value = this._encodeForMemory(args[0], []);
217
+ this.obj = value;
195
218
  await this._saveToFile();
196
219
  return [];
197
220
  }
198
221
 
199
222
  const keys = args.slice(0, -1);
200
- const value = args[args.length - 1];
223
+ const value = this._encodeForMemory(args[args.length - 1], keys);
201
224
 
202
225
  this._setRecursive(this.obj, keys.slice(), value);
203
226
  await this._saveToFile();
@@ -208,12 +231,14 @@ export class JsonDriver extends DeepBaseDriver {
208
231
  _refreshFromDisk() {
209
232
  if (fs.existsSync(this.fileName)) {
210
233
  const fileContent = fs.readFileSync(this.fileName, "utf8");
211
- this.obj = fileContent ? this.parse(fileContent) : {};
234
+ const parsed = fileContent ? this.parse(fileContent) : {};
235
+ this.obj = this._encodeForMemory(parsed, []);
212
236
  }
213
237
  }
214
238
 
215
239
  // Queue operations to prevent race conditions
216
240
  async _queueOperation(operation) {
241
+ this._assertUsable();
217
242
  const previousOperation = this._operationQueue;
218
243
 
219
244
  let resolver;
@@ -248,6 +273,62 @@ export class JsonDriver extends DeepBaseDriver {
248
273
  resolver();
249
274
  }
250
275
  }
276
+
277
+ async dispose({ clearMemory = true, releaseInstance = true } = {}) {
278
+ if (this._disposePromise) {
279
+ return this._disposePromise;
280
+ }
281
+
282
+ this._disposing = true;
283
+ this._disposePromise = (async () => {
284
+ try {
285
+ await this._operationQueue;
286
+ if (this._connected) {
287
+ await this._saveToFile();
288
+ }
289
+ this._connected = false;
290
+
291
+ if (clearMemory) {
292
+ this.obj = {};
293
+ }
294
+ if (releaseInstance && JsonDriver._instances[this.fileName] === this) {
295
+ delete JsonDriver._instances[this.fileName];
296
+ }
297
+
298
+ this._disposed = true;
299
+ } catch (error) {
300
+ this._disposing = false;
301
+ this._disposePromise = null;
302
+ throw error;
303
+ }
304
+ })();
305
+
306
+ return this._disposePromise;
307
+ }
308
+
309
+ _assertUsable() {
310
+ if (this._disposing || this._disposed) {
311
+ throw new Error('JsonDriver has been disposed');
312
+ }
313
+ }
314
+
315
+ _clone(value) {
316
+ return typeof value === 'object' && value !== null
317
+ ? this.parse(this.stringify(value))
318
+ : value;
319
+ }
320
+
321
+ _encodeForMemory(value, path) {
322
+ return this.encodeForMemory(this._clone(value), path.map(String));
323
+ }
324
+
325
+ _decodeFromMemory(value, path) {
326
+ return this.decodeFromMemory(this._clone(value), path.map(String));
327
+ }
328
+
329
+ _serializeMemory() {
330
+ return this.stringify(this._decodeFromMemory(this.obj, []));
331
+ }
251
332
 
252
333
  _setRecursive(obj, keys, value) {
253
334
  if (keys.length === 0) return;
@@ -278,7 +359,7 @@ export class JsonDriver extends DeepBaseDriver {
278
359
  }
279
360
 
280
361
  async _saveToFile() {
281
- const serializedData = this.stringify(this.obj);
362
+ const serializedData = this._serializeMemory();
282
363
  if (this.multiProcess) {
283
364
  // Sync write ensures data is on disk before lock release
284
365
  fs.writeFileSync(this.fileName, serializedData);
package/src/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
- import { DeepBaseDriver, DeepBaseDriverOptions } from 'deepbase';
1
+ import { DeepBaseDriver, DeepBaseDriverOptions, DisposeOptions } from 'deepbase';
2
+
3
+ export type MemoryTransform = (value: unknown, path: string[]) => unknown;
2
4
 
3
5
  export interface JsonDriverOptions extends DeepBaseDriverOptions {
4
6
  name?: string;
@@ -7,6 +9,10 @@ export interface JsonDriverOptions extends DeepBaseDriverOptions {
7
9
  parse?: (str: string) => any;
8
10
  /** Enable cross-process file locking for safe multi-process access */
9
11
  multiProcess?: boolean;
12
+ /** Transform a defensive copy before it enters the in-memory cache */
13
+ encodeForMemory?: MemoryTransform;
14
+ /** Transform a defensive copy when it leaves the in-memory cache */
15
+ decodeFromMemory?: MemoryTransform;
10
16
  }
11
17
 
12
18
  export class JsonDriver extends DeepBaseDriver {
@@ -17,7 +23,11 @@ export class JsonDriver extends DeepBaseDriver {
17
23
  fileName: string;
18
24
  stringify: (obj: any) => string;
19
25
  parse: (str: string) => any;
20
- obj: Record<string, any>;
26
+ encodeForMemory: MemoryTransform;
27
+ decodeFromMemory: MemoryTransform;
28
+ obj: unknown;
29
+
30
+ dispose(options?: DisposeOptions): Promise<void>;
21
31
  }
22
32
 
23
33
  export default JsonDriver;
package/test/test.js CHANGED
@@ -315,6 +315,215 @@ describe('JsonDriver', function() {
315
315
  });
316
316
  });
317
317
 
318
+ describe('Memory transforms', function() {
319
+ function transformSecrets(value, path, transform) {
320
+ if (path.at(-1) === 'privateKey' || path.at(-1) === 'mnemonic') {
321
+ return transform(value);
322
+ }
323
+ if (Array.isArray(value)) {
324
+ return value.map((item, index) =>
325
+ transformSecrets(item, [...path, String(index)], transform)
326
+ );
327
+ }
328
+ if (value !== null && typeof value === 'object') {
329
+ return Object.fromEntries(
330
+ Object.entries(value).map(([key, item]) => [
331
+ key,
332
+ transformSecrets(item, [...path, key], transform)
333
+ ])
334
+ );
335
+ }
336
+ return value;
337
+ }
338
+
339
+ function createMemoryTransforms() {
340
+ return {
341
+ encodeForMemory(value, path) {
342
+ return transformSecrets(value, path, secret => ({ sealed: secret }));
343
+ },
344
+ decodeFromMemory(value, path) {
345
+ return transformSecrets(value, path, sealed => {
346
+ if (!sealed || typeof sealed !== 'object' || !('sealed' in sealed)) {
347
+ throw new Error('Authentication failed');
348
+ }
349
+ return sealed.sealed;
350
+ });
351
+ }
352
+ };
353
+ }
354
+
355
+ it('encodes only secrets while preserving queryable structure', async function() {
356
+ const driver = new JsonDriver({
357
+ name: 'memory-transform',
358
+ path: testDataPath,
359
+ ...createMemoryTransforms()
360
+ });
361
+ const secureDb = new DeepBase(driver);
362
+ await secureDb.connect();
363
+
364
+ const account = {
365
+ address: '0x123',
366
+ privateKey: 'secret',
367
+ wallet: { mnemonic: 'words' }
368
+ };
369
+ await secureDb.set('account', account);
370
+ account.privateKey = 'caller-mutation';
371
+
372
+ assert.strictEqual(driver.obj.account.address, '0x123');
373
+ assert.deepStrictEqual(driver.obj.account.privateKey, { sealed: 'secret' });
374
+ assert.deepStrictEqual(driver.obj.account.wallet.mnemonic, { sealed: 'words' });
375
+ assert.strictEqual(await secureDb.get('account', 'address'), '0x123');
376
+
377
+ const decoded = await secureDb.get('account');
378
+ assert.deepStrictEqual(decoded, {
379
+ address: '0x123',
380
+ privateKey: 'secret',
381
+ wallet: { mnemonic: 'words' }
382
+ });
383
+ decoded.address = 'returned-mutation';
384
+ assert.strictEqual(driver.obj.account.address, '0x123');
385
+ await secureDb.dispose();
386
+ });
387
+
388
+ it('decodes values, entries and upd callbacks without exposing cache references', async function() {
389
+ const driver = new JsonDriver({
390
+ name: 'memory-collections',
391
+ path: testDataPath,
392
+ ...createMemoryTransforms()
393
+ });
394
+ const secureDb = new DeepBase(driver);
395
+ await secureDb.connect();
396
+ await secureDb.set('accounts', 'main', {
397
+ address: '0x123',
398
+ privateKey: 'old-secret'
399
+ });
400
+
401
+ assert.deepStrictEqual(await secureDb.values('accounts'), [{
402
+ address: '0x123',
403
+ privateKey: 'old-secret'
404
+ }]);
405
+ assert.deepStrictEqual(await secureDb.entries('accounts'), [[
406
+ 'main',
407
+ { address: '0x123', privateKey: 'old-secret' }
408
+ ]]);
409
+
410
+ const addedPath = await driver.add('accounts', {
411
+ address: '0x456',
412
+ privateKey: 'added-secret'
413
+ });
414
+ assert.deepStrictEqual(
415
+ driver.obj.accounts[addedPath.at(-1)].privateKey,
416
+ { sealed: 'added-secret' }
417
+ );
418
+
419
+ await secureDb.upd('accounts', 'main', account => {
420
+ assert.strictEqual(account.privateKey, 'old-secret');
421
+ account.privateKey = 'new-secret';
422
+ return account;
423
+ });
424
+ assert.deepStrictEqual(driver.obj.accounts.main.privateKey, { sealed: 'new-secret' });
425
+ await secureDb.dispose();
426
+ });
427
+
428
+ it('keeps the existing decoded disk format and encodes parsed data on load', async function() {
429
+ const options = {
430
+ name: 'memory-persistence',
431
+ path: testDataPath,
432
+ ...createMemoryTransforms()
433
+ };
434
+ const firstDriver = new JsonDriver(options);
435
+ const firstDb = new DeepBase(firstDriver);
436
+ await firstDb.connect();
437
+ await firstDb.set('account', { address: '0x123', privateKey: 'secret' });
438
+ await firstDb.dispose();
439
+
440
+ const filePath = path.join(testDataPath, 'memory-persistence.json');
441
+ assert.deepStrictEqual(JSON.parse(fs.readFileSync(filePath, 'utf8')), {
442
+ account: { address: '0x123', privateKey: 'secret' }
443
+ });
444
+
445
+ const secondDriver = new JsonDriver(options);
446
+ const secondDb = new DeepBase(secondDriver);
447
+ await secondDb.connect();
448
+ assert.deepStrictEqual(secondDriver.obj.account.privateKey, { sealed: 'secret' });
449
+ assert.strictEqual(await secondDb.get('account', 'privateKey'), 'secret');
450
+ await secondDb.dispose();
451
+ });
452
+
453
+ it('does not modify cache when encoding fails', async function() {
454
+ const driver = new JsonDriver({
455
+ name: 'memory-encode-failure',
456
+ path: testDataPath,
457
+ encodeForMemory(value, path) {
458
+ if (path.at(-1) === 'privateKey') {
459
+ throw new Error('Encoding failed');
460
+ }
461
+ return value;
462
+ }
463
+ });
464
+ const secureDb = new DeepBase(driver);
465
+ await secureDb.connect();
466
+ await secureDb.set('account', 'address', '0x123');
467
+
468
+ await assert.rejects(
469
+ secureDb.set('account', 'privateKey', 'secret'),
470
+ /Encoding failed/
471
+ );
472
+ assert.deepStrictEqual(driver.obj, { account: { address: '0x123' } });
473
+ await secureDb.dispose();
474
+ });
475
+
476
+ it('throws authentication errors instead of returning cached ciphertext', async function() {
477
+ const driver = new JsonDriver({
478
+ name: 'memory-auth-failure',
479
+ path: testDataPath,
480
+ ...createMemoryTransforms()
481
+ });
482
+ const secureDb = new DeepBase(driver);
483
+ await secureDb.connect();
484
+ await secureDb.set('account', 'privateKey', 'secret');
485
+ driver.obj.account.privateKey = { invalid: 'ciphertext' };
486
+
487
+ await assert.rejects(
488
+ secureDb.get('account', 'privateKey'),
489
+ /Authentication failed/
490
+ );
491
+ driver.obj.account.privateKey = { sealed: 'secret' };
492
+ await secureDb.dispose();
493
+ });
494
+ });
495
+
496
+ describe('Dispose', function() {
497
+ it('waits for pending writes, clears memory and releases the singleton', async function() {
498
+ const driver = db.getDriver(0);
499
+ let releaseWrite;
500
+ let writeStarted;
501
+ const writeGate = new Promise(resolve => { releaseWrite = resolve; });
502
+ const started = new Promise(resolve => { writeStarted = resolve; });
503
+ const saveToFile = driver._saveToFile.bind(driver);
504
+ driver._saveToFile = async () => {
505
+ writeStarted();
506
+ await writeGate;
507
+ return saveToFile();
508
+ };
509
+
510
+ const pendingWrite = db.set('account', 'address', '0x123');
511
+ await started;
512
+ const disposal = db.dispose({ clearMemory: true, releaseInstance: true });
513
+
514
+ assert.strictEqual(JsonDriver._instances[driver.fileName], driver);
515
+ releaseWrite();
516
+ await Promise.all([pendingWrite, disposal]);
517
+
518
+ assert.deepStrictEqual(driver.obj, {});
519
+ assert.strictEqual(JsonDriver._instances[driver.fileName], undefined);
520
+ assert.notStrictEqual(
521
+ new JsonDriver({ name: driver.name, path: driver.path }),
522
+ driver
523
+ );
524
+ });
525
+ });
526
+
318
527
  describe('Queue / Stack (add + pop + shift)', function() {
319
528
  it('should work as FIFO queue with add + shift', async function() {
320
529
  // Enqueue items