ueberdb2 2.1.1 → 2.2.3

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/index.js CHANGED
@@ -18,24 +18,12 @@
18
18
  */
19
19
 
20
20
  const cacheAndBufferLayer = require('./lib/CacheAndBufferLayer');
21
+ const logging = require('./lib/logging');
21
22
  const util = require('util');
22
23
 
23
- // Returns a logger derived from the given logger (which may be null) that has debug() and
24
- // isDebugEnabled() methods.
25
- const normalizeLogger = (logger) => {
26
- const logLevelsUsed = ['debug', 'error'];
27
- logger = Object.create(logger || {});
28
- for (const level of logLevelsUsed) {
29
- const enabledFnName = `is${level.charAt(0).toUpperCase() + level.slice(1)}Enabled`;
30
- if (typeof logger[level] !== 'function') {
31
- logger[level] = () => {};
32
- logger[enabledFnName] = () => false;
33
- } else if (typeof logger[enabledFnName] !== 'function') {
34
- logger[enabledFnName] = () => true;
35
- }
36
- }
37
- return logger;
38
- };
24
+ const cbDb = {};
25
+ const fns = ['close', 'findKeys', 'flush', 'get', 'getSub', 'init', 'remove', 'set', 'setSub'];
26
+ for (const fn of fns) cbDb[fn] = util.callbackify(cacheAndBufferLayer.Database.prototype[fn]);
39
27
 
40
28
  const makeDoneCallback = (callback, deprecated) => (err) => {
41
29
  if (callback) callback(err);
@@ -62,8 +50,9 @@ exports.Database = class {
62
50
  this.dbModule = require(`./databases/${type}_db`);
63
51
  this.dbSettings = dbSettings;
64
52
  this.wrapperSettings = wrapperSettings;
65
- this.logger = normalizeLogger(logger);
53
+ this.logger = logging.normalizeLogger(logger);
66
54
  const db = new this.dbModule.Database(this.dbSettings);
55
+ db.logger = this.logger;
67
56
  this.db = new cacheAndBufferLayer.Database(db, this.wrapperSettings, this.logger);
68
57
 
69
58
  // Expose the cache wrapper's metrics to the user. See lib/CacheAndBufferLayer.js for details.
@@ -73,12 +62,12 @@ exports.Database = class {
73
62
  this.metrics = this.db.metrics;
74
63
  }
75
64
 
76
- init(callback) {
77
- if (callback) {
78
- util.callbackify(this.db.init.bind(this.db))(callback);
79
- } else {
80
- return this.db.init();
81
- }
65
+ /**
66
+ * @param callback - Deprecated. Node-style callback. If null, a Promise is returned.
67
+ */
68
+ init(callback = null) {
69
+ if (callback != null) return cbDb.init.call(this.db, callback);
70
+ return this.db.init();
82
71
  }
83
72
 
84
73
  /**
@@ -87,104 +76,95 @@ exports.Database = class {
87
76
 
88
77
  /**
89
78
  * Deprecated synonym of flush().
79
+ *
80
+ * @param callback - Deprecated. Node-style callback. If null, a Promise is returned.
90
81
  */
91
- doShutdown(callback) {
92
- this.flush(callback);
82
+ doShutdown(callback = null) {
83
+ return this.flush(callback);
93
84
  }
94
85
 
95
86
  /**
96
87
  * Writes any unsaved changes to the underlying database.
88
+ *
89
+ * @param callback - Deprecated. Node-style callback. If null, a Promise is returned.
97
90
  */
98
- flush(callback) {
99
- util.callbackify(this.db.flush.bind(this.db))(callback);
91
+ flush(callback = null) {
92
+ if (callback != null) return cbDb.flush.call(this.db, callback);
93
+ return this.db.flush();
100
94
  }
101
95
 
102
- get(key, callback) {
103
- util.callbackify(this.db.get.bind(this.db))(key, (err, val) => callback(err, clone(val)));
96
+ /**
97
+ * @param callback - Deprecated. Node-style callback. If null, a Promise is returned.
98
+ */
99
+ get(key, callback = null) {
100
+ if (callback != null) return cbDb.get.call(this.db, key, callback);
101
+ return this.db.get(key);
104
102
  }
105
103
 
106
- findKeys(key, notKey, callback) {
107
- util.callbackify(this.db.findKeys.bind(this.db))(key, notKey, (e, v) => callback(e, clone(v)));
104
+ /**
105
+ * @param callback - Deprecated. Node-style callback. If null, a Promise is returned.
106
+ */
107
+ findKeys(key, notKey, callback = null) {
108
+ if (callback != null) return cbDb.findKeys.call(this.db, key, notKey, callback);
109
+ return this.db.findKeys(key, notKey);
108
110
  }
109
111
 
110
112
  /**
111
113
  * Removes an entry from the database if present.
112
114
  *
113
- * @param cb Called when the write has been committed to the underlying database driver.
114
- * @param deprecated Deprecated callback that is called just after cb.
115
+ * @param cb Deprecated. Node-style callback. Called when the write has been committed to the
116
+ * underlying database driver. If null, a Promise is returned.
117
+ * @param deprecated Deprecated callback that is called just after cb. Ignored if cb is null.
115
118
  */
116
- remove(key, cb, deprecated = null) {
117
- util.callbackify(this.db.remove.bind(this.db))(key, makeDoneCallback(cb, deprecated));
119
+ remove(key, cb = null, deprecated = null) {
120
+ if (cb != null) return cbDb.remove.call(this.db, key, makeDoneCallback(cb, deprecated));
121
+ return this.db.remove(key);
118
122
  }
119
123
 
120
124
  /**
121
125
  * Adds or changes the value of an entry.
122
126
  *
123
- * @param cb Called when the write has been committed to the underlying database driver.
124
- * @param deprecated Deprecated callback that is called just after cb.
127
+ * @param cb Deprecated. Node-style callback. Called when the write has been committed to the
128
+ * underlying database driver. If null, a Promise is returned.
129
+ * @param deprecated Deprecated callback that is called just after cb. Ignored if cb is null.
125
130
  */
126
- set(key, value, cb, deprecated = null) {
127
- util.callbackify(this.db.set.bind(this.db))(
128
- key, clone(value), makeDoneCallback(cb, deprecated));
131
+ set(key, value, cb = null, deprecated = null) {
132
+ if (cb != null) return cbDb.set.call(this.db, key, value, makeDoneCallback(cb, deprecated));
133
+ return this.db.set(key, value);
129
134
  }
130
135
 
131
- getSub(key, sub, callback) {
132
- util.callbackify(this.db.getSub.bind(this.db))(
133
- key, sub, (err, val) => callback(err, clone(val)));
136
+ /**
137
+ * @param callback - Deprecated. Node-style callback. If null, a Promise is returned.
138
+ */
139
+ getSub(key, sub, callback = null) {
140
+ if (callback != null) return cbDb.getSub.call(this.db, key, sub, callback);
141
+ return this.db.getSub(key, sub);
134
142
  }
135
143
 
136
144
  /**
137
145
  * Adds or changes a subvalue of an entry.
138
146
  *
139
- * @param cb Called when the write has been committed to the underlying database driver.
140
- * @param deprecated Deprecated callback that is called just after cb.
147
+ * @param cb Deprecated. Node-style callback. Called when the write has been committed to the
148
+ * underlying database driver. If null, a Promise is returned.
149
+ * @param deprecated Deprecated callback that is called just after cb. Ignored if cb is null.
141
150
  */
142
- setSub(key, sub, value, cb, deprecated = null) {
143
- util.callbackify(this.db.setSub.bind(this.db))(
144
- key, sub, clone(value), makeDoneCallback(cb, deprecated));
151
+ setSub(key, sub, value, cb = null, deprecated = null) {
152
+ if (cb != null) {
153
+ return cbDb.setSub.call(this.db, key, sub, value, makeDoneCallback(cb, deprecated));
154
+ }
155
+ return this.db.setSub(key, sub, value);
145
156
  }
146
157
 
147
158
  /**
148
159
  * Flushes unwritten changes then closes the connection to the underlying database. After this
149
160
  * returns, any future call to a method on this object may result in an error.
161
+ *
162
+ * @param callback - Deprecated. Node-style callback. If null, a Promise is returned.
150
163
  */
151
- close(callback) {
152
- util.callbackify(this.db.close.bind(this.db))(callback);
153
- }
154
- };
155
-
156
- const clone = (obj, key = '') => {
157
- // Handle the 3 simple types, and null or undefined
158
- if (null == obj || 'object' !== typeof obj) return obj;
159
-
160
- if (typeof obj.toJSON === 'function') return clone(obj.toJSON(key));
161
-
162
- // Handle Date
163
- if (obj instanceof Date) {
164
- const copy = new Date();
165
- copy.setTime(obj.getTime());
166
- return copy;
164
+ close(callback = null) {
165
+ if (callback != null) return cbDb.close.call(this.db, callback);
166
+ return this.db.close();
167
167
  }
168
-
169
- // Handle Array
170
- if (obj instanceof Array) {
171
- const copy = [];
172
- for (let i = 0, len = obj.length; i < len; ++i) {
173
- copy[i] = clone(obj[i], String(i));
174
- }
175
- return copy;
176
- }
177
-
178
- // Handle Object
179
- if (obj instanceof Object) {
180
- const copy = {};
181
- for (const attr in obj) {
182
- if (Object.prototype.hasOwnProperty.call(obj, attr)) copy[attr] = clone(obj[attr], attr);
183
- }
184
- return copy;
185
- }
186
-
187
- throw new Error("Unable to copy obj! Its type isn't supported.");
188
168
  };
189
169
 
190
170
  /**
@@ -1,5 +1,9 @@
1
1
  'use strict';
2
2
 
3
+ const logging = require('./logging');
4
+
5
+ const nullLogger = logging.normalizeLogger(null);
6
+
3
7
  module.exports = class AbstractDatabase {
4
8
  constructor() {
5
9
  if (new.target === module.exports) {
@@ -8,6 +12,7 @@ module.exports = class AbstractDatabase {
8
12
  for (const fn of ['init', 'close', 'get', 'findKeys', 'remove', 'set']) {
9
13
  if (typeof this[fn] !== 'function') throw new TypeError(`method ${fn} not defined`);
10
14
  }
15
+ this.logger = nullLogger;
11
16
  }
12
17
 
13
18
  /**
@@ -165,6 +165,11 @@ exports.Database = class {
165
165
  // underlying database and we are awaiting commit.
166
166
  this.buffer = new LRU(this.settings.cache, (k, v) => !v.dirty && !v.writingInProgress);
167
167
 
168
+ // Either null if flushing is currently allowed, or a Promise that will resolve when it is OK to
169
+ // start flushing. The Promise has a `count` property that tracks the number of operations that
170
+ // are currently preventing flush() from running.
171
+ this._flushPaused = null;
172
+
168
173
  // Maps database key to a Promise that is resolved when the record is unlocked.
169
174
  this._locks = new Map();
170
175
 
@@ -245,6 +250,33 @@ exports.Database = class {
245
250
  this._locks.delete(key);
246
251
  }
247
252
 
253
+ // Block flush() until _resumeFlush() is called. This is needed so that a call to flush() after a
254
+ // write (set(), setSub(), or remove() call) in the same ECMAScript macro- or microtask will see
255
+ // the enqueued write and flush it.
256
+ //
257
+ // An alternative would be to change flush() to schedule its actions in a future microtask after
258
+ // the write has been queued in the buffer, but:
259
+ //
260
+ // * That would be fragile: Every use of await moves the subsequent processing to a new
261
+ // microtask, so flush() would need to do a number of `await Promise.resolve();` calls equal
262
+ // to the number of awaits before a write is actually buffered.
263
+ //
264
+ // * It won't work for setSub() because it must wait for a read to complete before it buffers
265
+ // the write.
266
+ _pauseFlush() {
267
+ if (this._flushPaused == null) {
268
+ this._flushPaused = new SelfContainedPromise();
269
+ this._flushPaused.count = 0;
270
+ }
271
+ ++this._flushPaused.count;
272
+ }
273
+
274
+ _resumeFlush() {
275
+ if (--this._flushPaused.count > 0) return;
276
+ this._flushPaused.done();
277
+ this._flushPaused = null;
278
+ }
279
+
248
280
  /**
249
281
  * wraps the init function of the original DB
250
282
  */
@@ -266,12 +298,14 @@ exports.Database = class {
266
298
  * Gets the value trough the wrapper.
267
299
  */
268
300
  async get(key) {
301
+ let v;
269
302
  await this._lock(key);
270
303
  try {
271
- return await this._getLocked(key);
304
+ v = await this._getLocked(key);
272
305
  } finally {
273
306
  this._unlock(key);
274
307
  }
308
+ return clone(v);
275
309
  }
276
310
 
277
311
  async _getLocked(key) {
@@ -334,12 +368,13 @@ exports.Database = class {
334
368
  * returns the key entries via callback.
335
369
  */
336
370
  async findKeys(key, notKey) {
337
- const bufferKey = `${key}-${notKey}`;
371
+ await this.flush();
338
372
  const keyValues = await this.wrappedDB.findKeys(key, notKey);
339
373
  if (this.logger.isDebugEnabled()) {
340
- this.logger.debug(`GET - ${bufferKey} - ${JSON.stringify(keyValues)} - from database `);
374
+ this.logger.debug(
375
+ `GET - ${key}-${notKey} - ${JSON.stringify(keyValues)} - from database `);
341
376
  }
342
- return keyValues;
377
+ return clone(keyValues);
343
378
  }
344
379
 
345
380
  /**
@@ -354,9 +389,19 @@ exports.Database = class {
354
389
  * Sets the value trough the wrapper
355
390
  */
356
391
  async set(key, value) {
357
- await this._lock(key);
358
- const p = this._setLocked(key, value);
359
- this._unlock(key);
392
+ value = clone(value);
393
+ let p;
394
+ this._pauseFlush();
395
+ try {
396
+ await this._lock(key);
397
+ try {
398
+ p = this._setLocked(key, value);
399
+ } finally {
400
+ this._unlock(key);
401
+ }
402
+ } finally {
403
+ this._resumeFlush();
404
+ }
360
405
  await p;
361
406
  }
362
407
 
@@ -406,47 +451,53 @@ exports.Database = class {
406
451
  * Sets a subvalue
407
452
  */
408
453
  async setSub(key, sub, value) {
454
+ value = clone(value);
409
455
  if (this.logger.isDebugEnabled()) {
410
456
  this.logger.debug(`SETSUB - ${key}${JSON.stringify(sub)} - ${JSON.stringify(value)}`);
411
457
  }
412
458
  let p;
413
- await this._lock(key);
459
+ this._pauseFlush();
414
460
  try {
415
- let base;
461
+ await this._lock(key);
416
462
  try {
417
- const fullValue = await this._getLocked(key);
418
- base = {fullValue};
419
- // Emulate a pointer to the property that should be set to `value`.
420
- const ptr = {obj: base, prop: 'fullValue'};
421
- for (let i = 0; i < sub.length; i++) {
422
- if (sub[i] === '__proto__') {
423
- throw new Error('Modifying object prototype is not supported for security reasons');
424
- }
425
- let o = ptr.obj[ptr.prop];
426
- if (o == null) ptr.obj[ptr.prop] = o = {};
427
- // If o is a primitive (string, number, etc.), then setting `o.foo` has no effect because
428
- // ECMAScript automatically wraps primitives in a temporary wrapper object.
429
- if (typeof o !== 'object') {
430
- throw new TypeError(
431
- `Cannot set property ${JSON.stringify(sub[i])} on non-object ` +
463
+ let base;
464
+ try {
465
+ const fullValue = await this._getLocked(key);
466
+ base = {fullValue};
467
+ // Emulate a pointer to the property that should be set to `value`.
468
+ const ptr = {obj: base, prop: 'fullValue'};
469
+ for (let i = 0; i < sub.length; i++) {
470
+ if (sub[i] === '__proto__') {
471
+ throw new Error('Modifying object prototype is not supported for security reasons');
472
+ }
473
+ let o = ptr.obj[ptr.prop];
474
+ if (o == null) ptr.obj[ptr.prop] = o = {};
475
+ // If o is a primitive (string, number, etc.), then setting `o.foo` has no effect
476
+ // because ECMAScript automatically wraps primitives in a temporary wrapper object.
477
+ if (typeof o !== 'object') {
478
+ throw new TypeError(
479
+ `Cannot set property ${JSON.stringify(sub[i])} on non-object ` +
432
480
  `${JSON.stringify(o)} (key: ${JSON.stringify(key)} ` +
433
481
  `value in db: ${JSON.stringify(fullValue)} ` +
434
482
  `sub: ${JSON.stringify(sub.slice(0, i + 1))})`);
483
+ }
484
+ ptr.obj = ptr.obj[ptr.prop];
485
+ ptr.prop = sub[i];
435
486
  }
436
- ptr.obj = ptr.obj[ptr.prop];
437
- ptr.prop = sub[i];
487
+ ptr.obj[ptr.prop] = value;
488
+ } catch (err) {
489
+ // this._setLocked() will not be called but it should still count as a write failure.
490
+ ++this.metrics.writes;
491
+ ++this.metrics.writesFailed;
492
+ ++this.metrics.writesFinished;
493
+ throw err;
438
494
  }
439
- ptr.obj[ptr.prop] = value;
440
- } catch (err) {
441
- // this._setLocked() will not be called but it should still count as a write failure.
442
- ++this.metrics.writes;
443
- ++this.metrics.writesFailed;
444
- ++this.metrics.writesFinished;
445
- throw err;
495
+ p = this._setLocked(key, base.fullValue);
496
+ } finally {
497
+ this._unlock(key);
446
498
  }
447
- p = this._setLocked(key, base.fullValue);
448
499
  } finally {
449
- this._unlock(key);
500
+ this._resumeFlush();
450
501
  }
451
502
  await p;
452
503
  }
@@ -457,13 +508,14 @@ exports.Database = class {
457
508
  * "bla"]
458
509
  */
459
510
  async getSub(key, sub) {
511
+ let subvalue;
460
512
  await this._lock(key);
461
513
  try {
462
514
  // get the full value
463
515
  const value = await this._getLocked(key);
464
516
 
465
517
  // everything is correct, navigate to the subvalue and return it
466
- let subvalue = value;
518
+ subvalue = value;
467
519
 
468
520
  for (let i = 0; i < sub.length; i++) {
469
521
  // test if the subvalue exist
@@ -479,10 +531,10 @@ exports.Database = class {
479
531
  if (this.logger.isDebugEnabled()) {
480
532
  this.logger.debug(`GETSUB - ${key}${JSON.stringify(sub)} - ${JSON.stringify(subvalue)}`);
481
533
  }
482
- return subvalue;
483
534
  } finally {
484
535
  this._unlock(key);
485
536
  }
537
+ return clone(subvalue);
486
538
  }
487
539
 
488
540
  /**
@@ -492,6 +544,7 @@ exports.Database = class {
492
544
  if (this._flushDone == null) {
493
545
  this._flushDone = (async () => {
494
546
  while (true) {
547
+ while (this._flushPaused != null) await this._flushPaused;
495
548
  const dirtyEntries = [];
496
549
  for (const entry of this.buffer) {
497
550
  if (entry[1].dirty && !entry[1].writingInProgress) {
@@ -576,10 +629,12 @@ exports.Database = class {
576
629
  }
577
630
  };
578
631
 
579
- const clone = (obj) => {
632
+ const clone = (obj, key = '') => {
580
633
  // Handle the 3 simple types, and null or undefined
581
634
  if (null == obj || 'object' !== typeof obj) return obj;
582
635
 
636
+ if (typeof obj.toJSON === 'function') return clone(obj.toJSON(key));
637
+
583
638
  // Handle Date
584
639
  if (obj instanceof Date) {
585
640
  const copy = new Date();
@@ -591,7 +646,7 @@ const clone = (obj) => {
591
646
  if (obj instanceof Array) {
592
647
  const copy = [];
593
648
  for (let i = 0, len = obj.length; i < len; ++i) {
594
- copy[i] = clone(obj[i]);
649
+ copy[i] = clone(obj[i], String(i));
595
650
  }
596
651
  return copy;
597
652
  }
@@ -600,7 +655,7 @@ const clone = (obj) => {
600
655
  if (obj instanceof Object) {
601
656
  const copy = {};
602
657
  for (const attr in obj) {
603
- if (Object.prototype.hasOwnProperty.call(obj, attr)) copy[attr] = clone(obj[attr]);
658
+ if (Object.prototype.hasOwnProperty.call(obj, attr)) copy[attr] = clone(obj[attr], attr);
604
659
  }
605
660
  return copy;
606
661
  }
package/lib/logging.js ADDED
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ const {Console} = require('console');
4
+ const {stdout, stderr} = require('process');
5
+
6
+ class ConsoleLogger extends Console {
7
+ constructor(opts = {}) { super({stdout, stderr, inspectOptions: {depth: Infinity}, ...opts}); }
8
+ isDebugEnabled() { return false; }
9
+ isInfoEnabled() { return true; }
10
+ isWarnEnabled() { return true; }
11
+ isErrorEnabled() { return true; }
12
+ }
13
+
14
+ exports.ConsoleLogger = ConsoleLogger;
15
+
16
+ exports.normalizeLogger = (logger) => {
17
+ const logLevelsUsed = ['debug', 'info', 'warn', 'error'];
18
+ logger = Object.create(logger || {});
19
+ for (const level of logLevelsUsed) {
20
+ const enabledFnName = `is${level.charAt(0).toUpperCase() + level.slice(1)}Enabled`;
21
+ if (typeof logger[level] !== 'function') {
22
+ logger[level] = () => {};
23
+ logger[enabledFnName] = () => false;
24
+ } else if (typeof logger[enabledFnName] !== 'function') {
25
+ logger[enabledFnName] = () => true;
26
+ }
27
+ }
28
+ return logger;
29
+ };
package/package.json CHANGED
@@ -57,7 +57,7 @@
57
57
  "url": "https://github.com/ether/ueberDB.git"
58
58
  },
59
59
  "main": "./index",
60
- "version": "2.1.1",
60
+ "version": "2.2.3",
61
61
  "bugs": {
62
62
  "url": "https://github.com/ether/ueberDB/issues"
63
63
  },
@@ -51,13 +51,14 @@ exports.databases = {
51
51
  removeMax: 0.3,
52
52
  },
53
53
  },
54
- /* Disabled due to "Document update conflict" error in findKeys()
55
54
  couch: {
56
55
  host: '127.0.0.1',
57
56
  port: 5984,
58
57
  database: 'ueberdb',
59
58
  user: 'ueberdb',
60
59
  password: 'ueberdb',
60
+ speeds: {
61
+ findKeysMax: 30,
62
+ },
61
63
  },
62
- */
63
64
  };