keuss 2.0.7 → 2.2.0

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.
@@ -7,10 +7,10 @@ const mongo = require ('mongodb');
7
7
  const Queue = require ('../Queue');
8
8
  const QFactory_MongoDB_defaults = require ('../QFactory-MongoDB-defaults');
9
9
 
10
- var debug = require('debug')('keuss:Queue:StreamMongo');
10
+ const debug = require('debug')('keuss:Queue:StreamMongo');
11
11
 
12
- class StreamMongoQueue extends Queue {
13
12
 
13
+ class StreamMongoQueue extends Queue {
14
14
  //////////////////////////////////////////////
15
15
  constructor (name, factory, opts, orig_opts) {
16
16
  super (name, factory, opts, orig_opts);
@@ -48,7 +48,7 @@ class StreamMongoQueue extends Queue {
48
48
 
49
49
  /////////////////////////////////////////
50
50
  // add element to queue
51
- insert (entry, callback) {
51
+ insert (entry, cb) {
52
52
  const mtr = entry.mature;
53
53
  const tr = entry.tries;
54
54
 
@@ -58,17 +58,18 @@ class StreamMongoQueue extends Queue {
58
58
 
59
59
  entry.t = new Date();
60
60
 
61
- this._col.insertOne (entry, {}, (err, result) => {
62
- if (err) return callback (err);
63
- callback (null, result.insertedId);
61
+ this._col.insertOne (entry, {})
62
+ .then (res => {
63
+ cb (null, res.insertedId);
64
64
  this._groups_vector.forEach (i => this._stats.incr (`stream.${i}.put`));
65
- });
65
+ })
66
+ .catch (cb);
66
67
  }
67
68
 
68
69
 
69
70
  /////////////////////////////////////////
70
71
  // get element from queue
71
- get (callback) {
72
+ get (cb) {
72
73
  const gid = this._gid;
73
74
  const q = {};
74
75
 
@@ -82,30 +83,33 @@ class StreamMongoQueue extends Queue {
82
83
  updt.$set[`mature.${gid}`] = Queue.nowPlusSecs (100 * this._opts.ttl);
83
84
 
84
85
  const opts = {
85
- sort: {}
86
+ sort: {},
87
+ includeResultMetadata: true
86
88
  };
89
+
87
90
  opts.sort[`mature.${gid}`] = 1;
88
91
 
89
92
  debug ('get() with q %O, upd %O, opts %o', q, updt, opts);
90
93
 
91
- this._col.findOneAndUpdate (q, updt, opts, (err, result) => {
92
- if (err) return callback (err);
93
- const v = result && result.value;
94
- if (!v) return callback ();
94
+ this._col.findOneAndUpdate (q, updt, opts)
95
+ .then (res => {
96
+ const v = res && res.value;
97
+ if (!v) return cb ();
95
98
  if (v.payload._bsontype == 'Binary') v.payload = v.payload.buffer;
96
99
  v.mature = v.mature[gid];
97
100
  v.tries = v.tries[gid];
98
101
  delete v.processed;
99
102
  delete v.t;
100
- callback (null, v);
103
+ cb (null, v);
101
104
  this._stats.incr (`stream.${gid}.get`);
102
- });
105
+ })
106
+ .catch (cb);
103
107
  }
104
108
 
105
109
 
106
110
  //////////////////////////////////
107
111
  // reserve element: call cb (err, pl) where pl has an id
108
- reserve (callback) {
112
+ reserve (cb) {
109
113
  const gid = this._gid;
110
114
  const delay = this._opts.reserve_delay || 120;
111
115
 
@@ -124,41 +128,44 @@ class StreamMongoQueue extends Queue {
124
128
 
125
129
  const opts = {
126
130
  sort: {},
127
- returnDocument: 'before'
131
+ returnDocument: 'before',
132
+ includeResultMetadata: true
128
133
  };
134
+
129
135
  opts.sort[`mature.${gid}`] = 1;
130
136
 
131
137
  debug ('reserve() with q %O, upd %O, opts %o', q, updt, opts);
132
138
 
133
- this._col.findOneAndUpdate (q, updt, opts, (err, result) => {
134
- if (err) return callback (err);
135
- const v = result && result.value;
136
- if (!v) return callback ();
139
+ this._col.findOneAndUpdate (q, updt, opts)
140
+ .then (res => {
141
+ const v = res && res.value;
142
+ if (!v) return cb ();
137
143
  if (v.payload._bsontype == 'Binary') v.payload = v.payload.buffer;
138
144
  v.mature = v.mature[gid];
139
145
  v.tries = v.tries[gid];
140
146
  delete v.processed;
141
147
  delete v.t;
142
- callback (null, v);
148
+ cb (null, v);
143
149
  this._stats.incr (`stream.${gid}.reserve`);
144
- });
150
+ })
151
+ .catch (cb);
145
152
  }
146
153
 
147
154
 
148
155
  //////////////////////////////////
149
156
  // commit previous reserve, by p.id
150
- commit (id, callback) {
157
+ commit (id, cb) {
151
158
  const gid = this._gid;
152
159
  let q;
153
160
 
154
161
  try {
155
162
  q = {
156
- _id: (_.isString(id) ? new mongo.ObjectID (id) : id),
163
+ _id: (_.isString(id) ? new mongo.ObjectId (id) : id),
157
164
  };
158
165
  q[`reserved.${gid}`] = {$exists: true};
159
166
  }
160
167
  catch (e) {
161
- return callback ('id [' + id + '] can not be used as rollback id: ' + e);
168
+ return cb ('id [' + id + '] can not be used as rollback id: ' + e);
162
169
  }
163
170
 
164
171
  const updt = {
@@ -173,33 +180,34 @@ class StreamMongoQueue extends Queue {
173
180
 
174
181
  debug ('commit() with q %O, upd %O, opts %o', q, updt, opts);
175
182
 
176
- this._col.updateOne (q, updt, opts, (err, result) => {
177
- if (err) return callback (err);
178
- callback (null, result && (result.modifiedCount == 1));
183
+ this._col.updateOne (q, updt, opts)
184
+ .then (res => {
185
+ cb (null, res && (res.modifiedCount == 1));
179
186
  this._stats.incr (`stream.${gid}.commit`);
180
- });
187
+ })
188
+ .catch (cb);
181
189
  }
182
190
 
183
191
 
184
192
  //////////////////////////////////
185
193
  // rollback previous reserve, by p.id
186
- rollback (id, next_t, callback) {
194
+ rollback (id, next_t, cb) {
187
195
  const gid = this._gid;
188
196
  let q;
189
197
 
190
198
  if (_.isFunction (next_t)) {
191
- callback = next_t;
199
+ cb = next_t;
192
200
  next_t = null;
193
201
  }
194
202
 
195
203
  try {
196
204
  q = {
197
- _id: (_.isString(id) ? new mongo.ObjectID (id) : id),
205
+ _id: (_.isString(id) ? new mongo.ObjectId (id) : id),
198
206
  };
199
207
  q[`reserved.${gid}`] = {$exists: true};
200
208
  }
201
209
  catch (e) {
202
- return callback ('id [' + id + '] can not be used as rollback id: ' + e);
210
+ return cb ('id [' + id + '] can not be used as rollback id: ' + e);
203
211
  }
204
212
 
205
213
  const updt = {
@@ -213,30 +221,33 @@ class StreamMongoQueue extends Queue {
213
221
 
214
222
  debug ('rollback() with q %O, upd %O, opts %o', q, updt, opts);
215
223
 
216
- this._col.updateOne (q, updt, opts, (err, result) => {
217
- if (err) return callback (err);
218
- callback (null, result && (result.modifiedCount == 1));
224
+ this._col.updateOne (q, updt, opts)
225
+ .then (res => {
226
+ cb (null, res && (res.modifiedCount == 1));
219
227
  this._stats.incr (`stream.${gid}.rollback`);
220
- });
228
+ })
229
+ .catch (cb);
221
230
  }
222
231
 
223
232
 
224
233
  //////////////////////////////////
225
234
  // queue size including non-mature elements
226
- totalSize (callback, gid) {
235
+ totalSize (cb, gid) {
227
236
  const gr = gid || this._gid;
228
237
 
229
238
  const q = {};
230
239
  q[`processed.${gr}`] = false;
231
240
 
232
241
  const opts = {};
233
- this._col.countDocuments (q, opts, callback);
242
+ this._col.countDocuments (q, opts)
243
+ .then (res => cb (null, res))
244
+ .catch (cb);
234
245
  }
235
246
 
236
247
 
237
248
  //////////////////////////////////
238
249
  // queue size NOT including non-mature elements
239
- size (callback, gid) {
250
+ size (cb, gid) {
240
251
  const gr = gid || this._gid;
241
252
 
242
253
  const q = {};
@@ -244,13 +255,15 @@ class StreamMongoQueue extends Queue {
244
255
  q[`mature.${gr}`] = {$lte: Queue.now()};
245
256
 
246
257
  const opts = {};
247
- this._col.countDocuments (q, opts, callback);
258
+ this._col.countDocuments (q, opts)
259
+ .then (res => cb (null, res))
260
+ .catch (cb);
248
261
  }
249
262
 
250
263
 
251
264
  //////////////////////////////////
252
265
  // queue size of non-mature elements only
253
- schedSize (callback, gid) {
266
+ schedSize (cb, gid) {
254
267
  const gr = gid || this._gid;
255
268
 
256
269
  const q = {};
@@ -259,13 +272,15 @@ class StreamMongoQueue extends Queue {
259
272
  q[`mature.${gr}`] = {$gt: Queue.now()};
260
273
 
261
274
  const opts = {};
262
- this._col.countDocuments (q, opts, callback);
275
+ this._col.countDocuments (q, opts)
276
+ .then (res => cb (null, res))
277
+ .catch (cb);
263
278
  }
264
279
 
265
280
 
266
281
  //////////////////////////////////
267
282
  // queue size of reserved elements only
268
- resvSize (callback, gid) {
283
+ resvSize (cb, gid) {
269
284
  const gr = gid || this._gid;
270
285
 
271
286
  const q = {};
@@ -274,13 +289,15 @@ class StreamMongoQueue extends Queue {
274
289
  q[`mature.${gr}`] = {$gt: Queue.now()};
275
290
 
276
291
  const opts = {};
277
- this._col.countDocuments (q, opts, callback);
292
+ this._col.countDocuments (q, opts)
293
+ .then (res => cb (null, res))
294
+ .catch (cb);
278
295
  }
279
296
 
280
297
 
281
298
  /////////////////////////////////////////
282
299
  // get element from queue
283
- next_t (callback, gid) {
300
+ next_t (cb, gid) {
284
301
  const gr = gid || this._gid;
285
302
 
286
303
  const q = {};
@@ -294,11 +311,12 @@ class StreamMongoQueue extends Queue {
294
311
  .limit(1)
295
312
  .sort (sort)
296
313
  .project ({mature:1})
297
- .next ((err, result) => {
298
- if (err) return callback (err);
299
- debug ('next_t with git %s: got %o', gr, result);
300
- callback (null, result && result.mature && result.mature[gr]);
301
- });
314
+ .next ()
315
+ .then (res => {
316
+ debug ('next_t with git %s: got %o', gr, res);
317
+ cb (null, res && res.mature && res.mature[gr]);
318
+ })
319
+ .catch (cb);
302
320
  }
303
321
 
304
322
 
@@ -398,10 +416,14 @@ class StreamMongoQueue extends Queue {
398
416
  this._groups_vector.forEach (i => {
399
417
  const idx = {};
400
418
  idx[`mature.${i}`] = 1;
401
- tasks.push (cb => this._col.createIndex (idx, cb));
419
+ tasks.push (cb => this._col.createIndex (idx).then (res => cb(null, res)).catch (cb));
402
420
  });
403
- tasks.push (cb => this._col.createIndex ({t: 1}, {expireAfterSeconds: this._opts.ttl}, cb));
404
- async.series (tasks, err => cb (err, this));
421
+
422
+ tasks.push (cb => this._col.createIndex ({t: 1}, {expireAfterSeconds: this._opts.ttl}).then (res => cb(null, res)).catch (cb));
423
+
424
+ async.series (tasks)
425
+ .then (() => cb (null, this))
426
+ .catch (cb);
405
427
  }
406
428
  }
407
429
 
@@ -447,7 +469,8 @@ class Factory extends QFactory_MongoDB_defaults {
447
469
  pipeline: false,
448
470
  tape: true,
449
471
  remove: false,
450
- stream: true
472
+ stream: true,
473
+ id: false,
451
474
  };
452
475
  }
453
476
  }
@@ -456,16 +479,12 @@ function creator (opts, cb) {
456
479
  const _opts = opts || {};
457
480
  const m_url = _opts.url || 'mongodb://localhost:27017/keuss';
458
481
 
459
- MongoClient.connect (m_url, { useNewUrlParser: true }, (err, cl) => {
460
- if (err) return cb (err);
482
+ MongoClient.connect (m_url)
483
+ .then (cl => {
461
484
  const F = new Factory (_opts, cl);
462
485
  F.async_init (err => cb (null, F));
463
- });
486
+ })
487
+ .catch (cb);
464
488
  }
465
489
 
466
490
  module.exports = creator;
467
-
468
-
469
-
470
-
471
-
@@ -0,0 +1,313 @@
1
+ const EventEmitter = require('events')
2
+ const noop = function () {}
3
+
4
+ class Channel extends EventEmitter {
5
+ /**
6
+ * Channel constructor.
7
+ *
8
+ * @param {Connection} connection
9
+ * @param {String} [name] optional channel/collection name, default is 'mubsub'
10
+ * @param {Object} [options] optional options
11
+ * - `size` max size of the collection in bytes, default is 5mb
12
+ * - `max` max amount of documents in the collection
13
+ * - `retryInterval` time in ms to wait if no docs found, default is 200ms
14
+ * - `recreate` recreate the tailable cursor on error, default is true
15
+ * @api public
16
+ */
17
+ constructor (connection, name, options) {
18
+ super()
19
+ options || (options = {})
20
+ this.options = {
21
+ recreate: typeof options.recreate === 'boolean' ? options.recreate : true,
22
+ retryInterval:
23
+ typeof options.retryInterval === 'number' ? options.retryInterval : 200
24
+ }
25
+
26
+ this.collectionOpts = {
27
+ capped: true,
28
+ size: options.size || 1024 * 1024 * 5,
29
+ max: options.max
30
+ }
31
+
32
+ this.connection = connection
33
+ this.closed = false
34
+ this.listening = null
35
+ this.name = name || 'mubsub'
36
+
37
+ this.create().listen()
38
+ this.setMaxListeners(0)
39
+ }
40
+
41
+ /**
42
+ * Close the channel.
43
+ *
44
+ * @return {Channel} this
45
+ * @api public
46
+ */
47
+ close () {
48
+ this.closed = true
49
+
50
+ return this
51
+ }
52
+
53
+ /**
54
+ * Publish an event.
55
+ *
56
+ * @param {String} event
57
+ * @param {Object} [message]
58
+ * @param {Function} [callback]
59
+ * @return {Channel} this
60
+ * @api public
61
+ */
62
+ publish (event, message, callback) {
63
+ callback || (callback = noop)
64
+
65
+ this.ready(function (collection) {
66
+ collection
67
+ .insertOne({ event, message })
68
+ .then((result) => {
69
+ callback(null, { _id: result.insertedId })
70
+ })
71
+ .catch((err) => {
72
+ callback(err)
73
+ })
74
+ })
75
+
76
+ return this
77
+ }
78
+
79
+ /**
80
+ * Subscribe an event.
81
+ *
82
+ * @param {String} [event] if no event passed - all events are subscribed.
83
+ * @param {Function} callback
84
+ * @return {Object} unsubscribe function
85
+ * @api public
86
+ */
87
+ subscribe (event, callback) {
88
+ const self = this
89
+
90
+ if (typeof event === 'function') {
91
+ callback = event
92
+ event = 'message'
93
+ }
94
+
95
+ this.on(event, callback)
96
+
97
+ return {
98
+ unsubscribe: function () {
99
+ self.removeListener(event, callback)
100
+ }
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Create a channel collection.
106
+ *
107
+ * @return {Channel} this
108
+ * @api private
109
+ */
110
+ create () {
111
+ const self = this
112
+
113
+ function create () {
114
+ self.connection.db
115
+ .createCollection(self.name, self.collectionOpts)
116
+ .then((collection) => {
117
+ self.collection = collection
118
+ self.emit('collection', self.collection)
119
+ })
120
+ .catch((err) => {
121
+ const collectionExistsError =
122
+ err &&
123
+ (err.codeName === 'NamespaceExists' ||
124
+ err.message === 'collection already exists' ||
125
+ err.message.toLowerCase().includes('collection already exists') ||
126
+ err.message.match(/^a collection.+already exists$/) ||
127
+ err.message.match(/^Collection.+already exists\.$/))
128
+ if (collectionExistsError) {
129
+ const collection = self.connection.db.collection(self.name)
130
+ collection.isCapped().then((capped) => {
131
+ if (!capped) {
132
+ self.emit(
133
+ 'error',
134
+ new Error(`${err.message}, but it's NOT capped.`)
135
+ )
136
+ } else {
137
+ self.collection = collection
138
+ self.emit('collection', self.collection)
139
+ }
140
+ })
141
+ } else if (err) {
142
+ self.emit('error', err)
143
+ }
144
+ })
145
+ }
146
+
147
+ this.connection.db ? create() : this.connection.once('connect', create)
148
+
149
+ return this
150
+ }
151
+
152
+ /**
153
+ * Create a listener which will emit events for subscribers.
154
+ * It will listen to any document with event property.
155
+ *
156
+ * @param {Object} [latest] latest document to start listening from
157
+ * @return {Channel} this
158
+ * @api private
159
+ */
160
+ listen (latest) {
161
+ const self = this
162
+
163
+ this.latest(
164
+ latest,
165
+ this.handle(true, function (latest, collection) {
166
+ const cursor = collection.find(
167
+ { _id: { $gt: latest._id } },
168
+ {
169
+ tailable: true,
170
+ awaitData: true,
171
+ timeout: false,
172
+ maxAwaitTimeMS: self.options.retryInterval
173
+ }
174
+ ).hint({ $natural: 1 })
175
+ const next = self.handle(function (doc) {
176
+ // There is no document only if the cursor is closed by accident.
177
+ // F.e. if collection was dropped or connection died.
178
+ if (!doc) {
179
+ return setTimeout(function () {
180
+ self.emit('error', new Error('Mubsub: broken cursor.'))
181
+ if (self.options.recreate) {
182
+ self.create().listen(latest)
183
+ }
184
+ }, 1000)
185
+ }
186
+ latest = doc
187
+ if (doc.event) {
188
+ self.emit(doc.event, doc.message)
189
+ self.emit('message', doc.message)
190
+ }
191
+
192
+ self.emit('document', doc)
193
+ process.nextTick(more)
194
+ })
195
+
196
+ const more = function () {
197
+ cursor
198
+ .next()
199
+ .then((doc) => next(undefined, doc))
200
+ .catch((err) => next(err))
201
+ }
202
+
203
+ more()
204
+ self.listening = collection
205
+ self.emit('ready', collection)
206
+ })
207
+ )
208
+
209
+ return this
210
+ }
211
+
212
+ /**
213
+ * Get the latest document from the collection. Insert a dummy object in case
214
+ * the collection is empty, because otherwise we don't get a tailable cursor
215
+ * and need to poll in a loop.
216
+ *
217
+ * @param {Object} [latest] latest known document
218
+ * @param {Function} callback
219
+ * @return {Channel} this
220
+ * @api private
221
+ */
222
+ latest (latest, callback) {
223
+ function onCollection (collection) {
224
+ const cursor = collection
225
+ .find(latest ? { _id: latest._id } : {}, { timeout: false })
226
+ .hint({ $natural: -1 })
227
+ .limit(1)
228
+ cursor
229
+ .next()
230
+ .then((doc) => {
231
+ cursor.close() // Is this required?
232
+ if (doc) { // further check if there are larger _id than this doc.
233
+ collection.findOne({ _id: { $gt: doc._id } }, { sort: { _id: -1 } }).then((record) => {
234
+ if (record) {
235
+ return callback(undefined, record, collection)
236
+ }
237
+ return callback(undefined, doc, collection)
238
+ }).catch(err => {
239
+ console.warn(`failed to find the largest _id: ${err.message}, ignore it.`)
240
+ return callback(undefined, doc, collection)
241
+ })
242
+ } else {
243
+ collection
244
+ .insertOne({ dummy: true })
245
+ .then((result) => {
246
+ callback(null, { _id: result.insertedId }, collection)
247
+ })
248
+ .catch((err) => {
249
+ callback(err, undefined, collection)
250
+ })
251
+ }
252
+ })
253
+ .catch((err) => {
254
+ callback(err, undefined, collection)
255
+ })
256
+ }
257
+
258
+ this.collection
259
+ ? onCollection(this.collection)
260
+ : this.once('collection', onCollection)
261
+
262
+ return this
263
+ }
264
+
265
+ /**
266
+ * Return a function which will handle errors and consider channel and connection
267
+ * state.
268
+ *
269
+ * @param {Boolean} [exit] if error happens and exit is true, callback will not be called
270
+ * @param {Function} callback
271
+ * @return {Function}
272
+ * @api private
273
+ */
274
+ handle (exit, callback) {
275
+ const self = this
276
+
277
+ if (typeof exit === 'function') {
278
+ callback = exit
279
+ exit = null
280
+ }
281
+
282
+ return function () {
283
+ if (self.closed || self.connection.destroyed) {
284
+ return
285
+ }
286
+
287
+ const args = [].slice.call(arguments)
288
+ const err = args.shift()
289
+ if (err) self.emit('error', err)
290
+ if (err && exit) return
291
+ callback.apply(self, args)
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Call back if collection is ready for publishing.
297
+ *
298
+ * @param {Function} callback
299
+ * @return {Channel} this
300
+ * @api private
301
+ */
302
+ ready (callback) {
303
+ if (this.listening) {
304
+ callback(this.listening)
305
+ } else {
306
+ this.once('ready', callback)
307
+ }
308
+
309
+ return this
310
+ }
311
+ }
312
+
313
+ module.exports = Channel