ueberdb2 2.1.0 → 2.2.2

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,44 +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
- let o = ptr.obj[ptr.prop];
423
- if (o == null) ptr.obj[ptr.prop] = o = {};
424
- // If o is a primitive (string, number, etc.), then setting `o.foo` has no effect because
425
- // ECMAScript automatically wraps primitives in a temporary wrapper object.
426
- if (typeof o !== 'object') {
427
- throw new TypeError(
428
- `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 ` +
429
480
  `${JSON.stringify(o)} (key: ${JSON.stringify(key)} ` +
430
481
  `value in db: ${JSON.stringify(fullValue)} ` +
431
482
  `sub: ${JSON.stringify(sub.slice(0, i + 1))})`);
483
+ }
484
+ ptr.obj = ptr.obj[ptr.prop];
485
+ ptr.prop = sub[i];
432
486
  }
433
- ptr.obj = ptr.obj[ptr.prop];
434
- 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;
435
494
  }
436
- ptr.obj[ptr.prop] = value;
437
- } catch (err) {
438
- // this._setLocked() will not be called but it should still count as a write failure.
439
- ++this.metrics.writes;
440
- ++this.metrics.writesFailed;
441
- ++this.metrics.writesFinished;
442
- throw err;
495
+ p = this._setLocked(key, base.fullValue);
496
+ } finally {
497
+ this._unlock(key);
443
498
  }
444
- p = this._setLocked(key, base.fullValue);
445
499
  } finally {
446
- this._unlock(key);
500
+ this._resumeFlush();
447
501
  }
448
502
  await p;
449
503
  }
@@ -454,13 +508,14 @@ exports.Database = class {
454
508
  * "bla"]
455
509
  */
456
510
  async getSub(key, sub) {
511
+ let subvalue;
457
512
  await this._lock(key);
458
513
  try {
459
514
  // get the full value
460
515
  const value = await this._getLocked(key);
461
516
 
462
517
  // everything is correct, navigate to the subvalue and return it
463
- let subvalue = value;
518
+ subvalue = value;
464
519
 
465
520
  for (let i = 0; i < sub.length; i++) {
466
521
  // test if the subvalue exist
@@ -476,10 +531,10 @@ exports.Database = class {
476
531
  if (this.logger.isDebugEnabled()) {
477
532
  this.logger.debug(`GETSUB - ${key}${JSON.stringify(sub)} - ${JSON.stringify(subvalue)}`);
478
533
  }
479
- return subvalue;
480
534
  } finally {
481
535
  this._unlock(key);
482
536
  }
537
+ return clone(subvalue);
483
538
  }
484
539
 
485
540
  /**
@@ -489,6 +544,7 @@ exports.Database = class {
489
544
  if (this._flushDone == null) {
490
545
  this._flushDone = (async () => {
491
546
  while (true) {
547
+ while (this._flushPaused != null) await this._flushPaused;
492
548
  const dirtyEntries = [];
493
549
  for (const entry of this.buffer) {
494
550
  if (entry[1].dirty && !entry[1].writingInProgress) {
@@ -573,10 +629,12 @@ exports.Database = class {
573
629
  }
574
630
  };
575
631
 
576
- const clone = (obj) => {
632
+ const clone = (obj, key = '') => {
577
633
  // Handle the 3 simple types, and null or undefined
578
634
  if (null == obj || 'object' !== typeof obj) return obj;
579
635
 
636
+ if (typeof obj.toJSON === 'function') return clone(obj.toJSON(key));
637
+
580
638
  // Handle Date
581
639
  if (obj instanceof Date) {
582
640
  const copy = new Date();
@@ -588,7 +646,7 @@ const clone = (obj) => {
588
646
  if (obj instanceof Array) {
589
647
  const copy = [];
590
648
  for (let i = 0, len = obj.length; i < len; ++i) {
591
- copy[i] = clone(obj[i]);
649
+ copy[i] = clone(obj[i], String(i));
592
650
  }
593
651
  return copy;
594
652
  }
@@ -597,7 +655,7 @@ const clone = (obj) => {
597
655
  if (obj instanceof Object) {
598
656
  const copy = {};
599
657
  for (const attr in obj) {
600
- 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);
601
659
  }
602
660
  return copy;
603
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
@@ -21,24 +21,24 @@
21
21
  }
22
22
  ],
23
23
  "dependencies": {
24
- "async": "^3.2.2",
24
+ "async": "^3.2.3",
25
25
  "cassandra-driver": "^4.6.3",
26
26
  "dirty": "^1.1.3",
27
- "elasticsearch": "^16.7.2",
27
+ "elasticsearch": "^16.7.3",
28
28
  "mongodb": "^3.7.3",
29
29
  "mssql": "^8.1.0",
30
30
  "mysql": "2.18.1",
31
- "nano": "^9.0.5",
32
- "pg": "^8.7.1",
31
+ "nano": "^10.0.0",
32
+ "pg": "^8.7.3",
33
33
  "redis": "^3.1.2",
34
34
  "rethinkdb": "^2.4.2",
35
- "simple-git": "^3.6.0"
35
+ "simple-git": "^3.7.1"
36
36
  },
37
37
  "optionalDependencies": {
38
- "sqlite3": "github:mapbox/node-sqlite3#593c9d498be2510d286349134537e3bf89401c4a"
38
+ "sqlite3": "^5.0.6"
39
39
  },
40
40
  "devDependencies": {
41
- "cli-table": "^0.3.8",
41
+ "cli-table": "^0.3.11",
42
42
  "eslint": "^7.32.0",
43
43
  "eslint-config-etherpad": "^2.0.3",
44
44
  "eslint-plugin-cypress": "^2.12.1",
@@ -57,7 +57,7 @@
57
57
  "url": "https://github.com/ether/ueberDB.git"
58
58
  },
59
59
  "main": "./index",
60
- "version": "2.1.0",
60
+ "version": "2.2.2",
61
61
  "bugs": {
62
62
  "url": "https://github.com/ether/ueberDB/issues"
63
63
  },
@@ -117,6 +117,6 @@
117
117
  "root": true
118
118
  },
119
119
  "engines": {
120
- "node": "^10.17.0 || >=11.14.0"
120
+ "node": ">=14.15.0"
121
121
  }
122
122
  }
@@ -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
  };