elasticio-sailor-nodejs 2.7.7 → 3.0.0-dev1

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/lib/amqp.js DELETED
@@ -1,624 +0,0 @@
1
- const log = require('./logging.js');
2
- const amqplib = require('amqplib');
3
- const { IllegalOperationError } = require('amqplib/lib/error');
4
- const Encryptor = require('./encryptor.js');
5
- const _ = require('lodash');
6
- const eventToPromise = require('event-to-promise');
7
- const uuid = require('uuid');
8
- const os = require('os');
9
- const messagesDB = require('./messagesDB.js');
10
- const assert = require('assert');
11
-
12
- const HEADER_ROUTING_KEY = 'x-eio-routing-key';
13
- const HEADER_ERROR_RESPONSE = 'x-eio-error-response';
14
-
15
- class Amqp {
16
- constructor(settings) {
17
- this.settings = settings;
18
- this._encryptor = new Encryptor(this.settings.MESSAGE_CRYPTO_PASSWORD, this.settings.MESSAGE_CRYPTO_IV);
19
- this.closed = true;
20
- this.consume = undefined;
21
- }
22
-
23
- async connect() {
24
- await Promise.all([this._ensurePublishChannel(), this._ensureConsumerChannel()]);
25
- this.closed = false;
26
- }
27
-
28
- async disconnect() {
29
- await this.stopConsume();
30
- this.closed = true;
31
- log.trace('Close AMQP connections');
32
- if (this._readConnection) {
33
- this._readConnection.removeAllListeners('close');
34
- }
35
- if (this._writeConnection) {
36
- this._writeConnection.removeAllListeners('close');
37
- }
38
- if (this.consumerChannel) {
39
- this.consumerChannel.removeAllListeners('close');
40
- }
41
- if (this.publishChannel) {
42
- this.publishChannel.removeAllListeners('close');
43
- }
44
- try {
45
- await this.consumerChannel.close();
46
- } catch (alreadyClosed) {
47
- log.debug('Subscribe channel is closed already');
48
- }
49
- try {
50
- await this.publishChannel.close();
51
- } catch (alreadyClosed) {
52
- log.debug('Publish channel is closed already');
53
- }
54
- try {
55
- await this._readConnection.close();
56
- } catch (alreadyClosed) {
57
- log.debug('AMQP read connection is closed already');
58
- }
59
- try {
60
- await this._writeConnection.close();
61
- } catch (alreadyClosed) {
62
- log.debug('AMQP write connection is closed already');
63
- }
64
- log.debug('Successfully closed AMQP connections');
65
- }
66
-
67
- async _ensureReadConnection() {
68
- if (this._readConnection) {
69
- return this._readConnection;
70
- }
71
- if (this._creatingReadConnection) {
72
- do {
73
- await new Promise(resolve => setImmediate(resolve));
74
- } while (!this._readConnection);
75
- return this._readConnection;
76
- }
77
- log.debug('Creating new read connection');
78
- this._creatingReadConnection = true;
79
- this._readConnection = await this._createConnection('read');
80
- this._creatingReadConnection = false;
81
- log.debug('Read connection created');
82
- this._readConnection.on('error', err => log.error({ err }, 'AMQP read Connection error'));
83
- this._readConnection.once('close', err => {
84
- log.error({ err }, 'Unexpected connection close.');
85
- delete this._readConnection;
86
- });
87
- return this._readConnection;
88
- }
89
-
90
- async _ensureWriteConnection() {
91
- if (this._writeConnection) {
92
- return this._writeConnection;
93
- }
94
- if (this._creatingWriteConnection) {
95
- do {
96
- await new Promise(resolve => setImmediate(resolve));
97
- } while (!this._writeConnection);
98
- return this._writeConnection;
99
- }
100
- log.debug('Creating new write connection');
101
- this._creatingWriteConnection = true;
102
- this._writeConnection = await this._createConnection('write');
103
- this._creatingWriteConnection = false;
104
- log.debug('Write connection created');
105
- this._writeConnection.on('error', err => log.error({ err }, 'AMQP write Connection error'));
106
- this._writeConnection.once('close', err => {
107
- log.error({ err }, 'Unexpected connection close.');
108
- delete this._writeConnection;
109
- });
110
- return this._writeConnection;
111
- }
112
-
113
- async _ensureConsumerChannel() {
114
- if (this.consumerChannel) {
115
- return this.consumerChannel;
116
- }
117
- if (this._creatingConsumerChannel) {
118
- do {
119
- await new Promise(resolve => setImmediate(resolve));
120
- } while (!this.consumerChannel);
121
- return this.consumerChannel;
122
- }
123
- log.debug({ prefetch: this.settings.RABBITMQ_PREFETCH_SAILOR }, 'Creating new consume channel');
124
- this._creatingConsumerChannel = true;
125
- const amqp = await this._ensureReadConnection();
126
- this.consumerChannel = await amqp.createChannel();
127
- log.debug('Consume channel created');
128
- this._creatingConsumerChannel = false;
129
- this.consumerChannel.prefetch(this.settings.RABBITMQ_PREFETCH_SAILOR);
130
- this.consumerChannel.on('error', err => log.error({ err }, 'Consumer channel error'));
131
- this.consumerChannel.once('close', () => {
132
- delete this.consumerChannel;
133
- if (this.consume) {
134
- log.warn('Channel unexpectedly closed, but we were listening. Reconnecting and re-listening queue');
135
- this.consume.consumerTag = undefined;
136
- // when RabbitMQ closes connection, amqplib will first emit 'close' for channel and then for connection
137
- // we use setImmediate to wait for connection 'close' event which will unset connection property
138
- // otherwise listenQueue will try to create channel on closing connection and fail hard
139
- setImmediate(() => this.listenQueue(this.consume.queue, this.consume.messageHandler));
140
- }
141
- });
142
- return this.consumerChannel;
143
- }
144
-
145
- async _ensurePublishChannel() {
146
- if (this.publishChannel) {
147
- return this.publishChannel;
148
- }
149
- if (this._creatingPublishChannel) {
150
- do {
151
- await new Promise(resolve => setImmediate(resolve));
152
- } while (!this.publishChannel);
153
- return this.publishChannel;
154
- }
155
- log.debug('Creating new publish connection and channel');
156
- this._creatingPublishChannel = true;
157
- const amqp = await this._ensureWriteConnection();
158
- this.publishChannel = await amqp.createConfirmChannel();
159
- log.debug('Publish connection and channel created');
160
- this._creatingPublishChannel = false;
161
- this.publishChannel.on('error', err => log.error({ err }, 'Publish channel error'));
162
- this.publishChannel.once('close', () => {
163
- delete this.publishChannel;
164
- });
165
- return this.publishChannel;
166
- }
167
-
168
- async _createConnection(name) {
169
- const uri = this.settings.AMQP_URI;
170
- const allowedAttempts = parseInt(this.settings.AMQP_RECONNECT_ATTEMPTS);
171
- let lastErr;
172
- let attempts = 0;
173
- while (attempts <= allowedAttempts) {
174
- if (attempts > 0) {
175
- await new Promise(resolve => setTimeout(resolve, parseInt(this.settings.AMQP_RECONNECT_TIMEOUT)));
176
- log.debug(
177
- {
178
- reconnectAttempt: attempts,
179
- AMQP_RECONNECT_ATTEMPTS: this.settings.AMQP_RECONNECT_ATTEMPTS
180
- },
181
- 'AMQP Reconnecting'
182
- );
183
- }
184
- try {
185
- return await amqplib.connect(uri,
186
- { clientProperties: { connection_name: `${os.hostname()}-${name}` } });
187
- } catch (err) {
188
- lastErr = err;
189
- log.error(err, 'AMQP Connection error');
190
- attempts++;
191
- }
192
- }
193
- throw lastErr;
194
- }
195
-
196
- async stopConsume() {
197
- if (!this.consume) {
198
- return;
199
- }
200
- const consume = this.consume;
201
- this.consume = undefined;
202
- await this.consumerChannel.cancel(consume.consumerTag);
203
- log.debug({ queue: consume.queue }, 'Stopped listening for messages');
204
- }
205
-
206
- async listenQueue(queue, messageHandler) {
207
- await this._ensureConsumerChannel();
208
-
209
- const { consumerTag } = await this.consumerChannel.consume(queue, async (amqpMessage) => {
210
- if (!amqpMessage) {
211
- log.warn('Consumer cancelled by rabbitmq');
212
- return;
213
- }
214
- let message;
215
- try {
216
- message = this._decodeMessage(amqpMessage);
217
- } catch (err) {
218
- log.error({ err, deliveryTag: amqpMessage.fields.deliveryTag },
219
- 'Error occurred while parsing message payload');
220
- this.rejectOriginal(amqpMessage);
221
- return;
222
- }
223
- try {
224
- await messageHandler(message, amqpMessage);
225
- } catch (err) {
226
- log.error({ err, deliveryTag: amqpMessage.fields.deliveryTag }, 'Failed to process message, reject');
227
- this.rejectOriginal(amqpMessage);
228
- }
229
- });
230
- log.debug({ queue }, 'Started listening for messages');
231
- this.consume = { queue, messageHandler, consumerTag };
232
- }
233
-
234
- _decodeMessage(amqpMessage) {
235
- log.trace('Message received');
236
- let message;
237
- if (this.settings.INPUT_FORMAT === 'error') {
238
- message = this._decodeErrorMessage(amqpMessage);
239
- } else {
240
- message = this._decodeDefaultMessage(amqpMessage);
241
- }
242
- message.headers = message.headers || {};
243
- if (amqpMessage.properties.headers.reply_to) {
244
- message.headers.reply_to = amqpMessage.properties.headers.reply_to;
245
- }
246
- return message;
247
- }
248
-
249
- _decodeDefaultMessage(amqpMessage) {
250
- const protocolVersion = Number(amqpMessage.properties.headers.protocolVersion || 1);
251
- return this._encryptor.decryptMessageContent(
252
- amqpMessage.content,
253
- protocolVersion < 2 ? 'base64' : undefined
254
- );
255
- }
256
-
257
- _decodeErrorMessage(amqpMessage) {
258
- const errorBody = JSON.parse(amqpMessage.content.toString());
259
- // NOTICE both error and errorInput are transferred as base64 encoded.
260
- // this does not depend on protocolVersion header of message (see _decodeDefault message)
261
- // this should be fixed in future, but it's OK at this moment
262
- if (errorBody.error) {
263
- errorBody.error = this._encryptor.decryptMessageContent(Buffer.from(errorBody.error), 'base64');
264
- }
265
- if (errorBody.errorInput) {
266
- errorBody.errorInput = this._encryptor.decryptMessageContent(errorBody.errorInput, 'base64');
267
- }
268
- return {
269
- body: errorBody,
270
- headers: amqpMessage.properties.headers
271
- };
272
- }
273
-
274
- decryptMessage(callback, message) {
275
- log.trace('Message received');
276
-
277
- if (message === null) {
278
- log.warn('NULL message received');
279
- return;
280
- }
281
-
282
- const protocolVersion = Number(message.properties.headers.protocolVersion || 1);
283
- let decryptedContent;
284
- try {
285
- decryptedContent = this._encryptor.decryptMessageContent(
286
- message.content,
287
- protocolVersion < 2 ? 'base64' : undefined
288
- );
289
- } catch (err) {
290
- log.error(err,
291
- 'Error occurred while parsing message #%j payload',
292
- message.fields.deliveryTag
293
- );
294
- // I didn't find a quick way to use reject that waits for new message when
295
- // connection is re-established, so simply try to reject original message
296
- // This will be fixed when we implement to Sailor Proxy
297
- return this.rejectOriginal(message);
298
- }
299
- decryptedContent.headers = decryptedContent.headers || {};
300
- if (message.properties.headers.reply_to) {
301
- decryptedContent.headers.reply_to = message.properties.headers.reply_to;
302
- }
303
-
304
- try {
305
- // pass to callback both decrypted content & original message
306
- callback(decryptedContent, message);
307
- } catch (err) {
308
- log.error(err, 'Failed to process message #%j, reject', message.fields.deliveryTag);
309
- // I didn't find a quick way to use reject that waits for new message when
310
- // connection is re-established, so simply try to reject original message
311
- // This will be fixed when we implement to Sailor Proxy
312
- return this.rejectOriginal(message);
313
- }
314
- }
315
-
316
- async getMostRecentMessage(messageId) {
317
- let message = messagesDB.getMessageById(messageId);
318
- assert(message, `Message with ID ${messageId} not found in messagesDB`);
319
- // If we stopped listening fo the new messages or the message was received
320
- // from the same channel we can return it immediately
321
- if (!this.consume || message.fields.consumerTag === this.consume.consumerTag) {
322
- return message;
323
- }
324
- log.debug({ messageId }, 'Waiting for message from new channel');
325
- message = await new Promise((resolve) => {
326
- const onMessageUpdated = (updatedMessageId, updatedMessage) => {
327
- if (updatedMessageId === messageId
328
- && updatedMessage.fields.consumerTag === this.consume.consumerTag) {
329
- messagesDB.off('message-updated', onMessageUpdated);
330
- resolve(updatedMessage);
331
- }
332
- };
333
- messagesDB.on('message-updated', onMessageUpdated);
334
- });
335
- log.debug({ messageId }, 'Message received from new channel');
336
- return message;
337
- }
338
-
339
- async ack(messageId) {
340
- const message = await this.getMostRecentMessage(messageId);
341
- log.debug(message.fields, 'Message ack');
342
- this.consumerChannel.ack(message);
343
- messagesDB.deleteMessage(messageId);
344
- }
345
-
346
- async reject(messageId) {
347
- const message = await this.getMostRecentMessage(messageId);
348
- log.debug(message.fields, 'Message reject');
349
- this.consumerChannel.reject(message, false);
350
- messagesDB.deleteMessage(messageId);
351
- }
352
-
353
- rejectOriginal(message) {
354
- log.debug('Message #%j reject', message.fields.deliveryTag);
355
- return this.consumerChannel.reject(message, false);
356
- }
357
-
358
- async sendToExchange(exchangeName, routingKey, payload, options, throttle) {
359
- if (throttle) {
360
- log.debug('Throttling outgoing message');
361
- await throttle();
362
- }
363
- const buffer = Buffer.from(payload);
364
-
365
- return this.publishMessage(exchangeName, routingKey, buffer, options, 0);
366
- }
367
-
368
- async publishMessage(exchangeName, routingKey, payloadBuffer, options, iteration) {
369
- const settings = this.settings;
370
- if (iteration) {
371
- options.headers.retry = iteration;
372
- }
373
- // AMQP_PERSISTENT_MESSAGES is false by default if not specified by env var
374
- options.persistent = this.settings.AMQP_PERSISTENT_MESSAGES;
375
-
376
- log.debug('Current memory usage: %s Mb', process.memoryUsage().heapUsed / 1048576);
377
- log.trace('Pushing to exchange=%s, routingKey=%s, messageSize=%d, options=%j, iteration=%d',
378
- exchangeName, routingKey, payloadBuffer.length, options, iteration);
379
- try {
380
- const result = await this._promisifiedPublish(exchangeName, routingKey, payloadBuffer, options);
381
- if (!result) {
382
- log.warn('Buffer full when publishing a message to '
383
- + 'exchange=%s with routingKey=%s', exchangeName, routingKey);
384
- }
385
- return result;
386
- } catch (error) {
387
- if (error instanceof IllegalOperationError) {
388
- log.error(error, `Failed on publishing ${options.headers.messageId} message to MQ`);
389
- throw new Error(`Failed on publishing ${options.headers.messageId} message to MQ: ` + error);
390
- }
391
- log.error(error, 'Failed on publishing message to queue');
392
- const delay = this._getDelay(
393
- settings.AMQP_PUBLISH_RETRY_DELAY,
394
- settings.AMQP_PUBLISH_MAX_RETRY_DELAY,
395
- iteration
396
- );
397
- await this._sleep(delay);
398
- iteration += 1;
399
- if (iteration < settings.AMQP_PUBLISH_RETRY_ATTEMPTS) {
400
- return this.publishMessage(exchangeName, routingKey, payloadBuffer, options, iteration);
401
- } else {
402
- throw new Error(`Failed on publishing ${options.headers.messageId} message to MQ: ` + error);
403
- }
404
- }
405
- }
406
-
407
- _getDelay(defaultDelay, maxDelay, iteration) {
408
- log.debug({ defaultDelay }, 'Current delay');
409
- log.debug({ maxDelay }, 'Current delay');
410
- const delay = Math.min(defaultDelay * Math.pow(2, iteration), maxDelay);
411
- log.debug({ delay }, 'Calculated delay');
412
- return delay;
413
- }
414
-
415
- async _sleep(time) {
416
- await new Promise(resolve => setTimeout(resolve, time));
417
- }
418
-
419
- async _promisifiedPublish(exchangeName, routingKey, payloadBuffer, options) {
420
- await this._ensurePublishChannel();
421
- try {
422
- let result;
423
- const publishChannel = this.publishChannel;
424
- const publishPromise = new Promise((resolve, reject) => {
425
- result = publishChannel.publish(exchangeName, routingKey, payloadBuffer, options, (err, ok) => {
426
- err ? reject(err) : resolve(ok);
427
- });
428
- });
429
- await Promise.all([
430
- (async () => {
431
- if (this.settings.PROCESS_AMQP_DRAIN && !result) {
432
- log.debug('Amqp buffer is full: waiting for drain event..');
433
- await eventToPromise(this.publishChannel, 'drain');
434
- log.debug('Amqp buffer drained!');
435
- result = true;
436
- }
437
- })(),
438
- publishPromise
439
- ]);
440
- return result;
441
- } catch (error) {
442
- throw error;
443
- }
444
- }
445
-
446
- encryptMessageContent(body, protocolVersion = 1) {
447
- return this._encryptor.encryptMessageContent(
448
- body,
449
- protocolVersion < 2
450
- ? 'base64'
451
- : undefined
452
- );
453
- }
454
-
455
- async prepareMessageAndSendToExchange(data, properties, routingKey, throttle) {
456
- const settings = this.settings;
457
-
458
- data.headers = filterMessageHeaders(data.headers);
459
- const protocolVersion = Number(properties.headers.protocolVersion || 1);
460
- const encryptedData = this.encryptMessageContent(data, protocolVersion);
461
-
462
- if (encryptedData.length > settings.OUTGOING_MESSAGE_SIZE_LIMIT) {
463
- const error = new Error(`Outgoing message size ${encryptedData.length}`
464
- + ` exceeds limit of ${settings.OUTGOING_MESSAGE_SIZE_LIMIT}.`);
465
- log.error(error);
466
- throw error;
467
- }
468
-
469
- return this.sendToExchange(settings.PUBLISH_MESSAGES_TO, routingKey, encryptedData, properties, throttle);
470
- }
471
-
472
- async sendData(data, headers, throttle) {
473
- const properties = this._createPropsFromHeaders(headers);
474
- const settings = this.settings;
475
- const routingKey = getRoutingKeyFromHeaders(data.headers) || settings.DATA_ROUTING_KEY;
476
- properties.headers.protocolVersion = settings.PROTOCOL_VERSION;
477
- return this.prepareMessageAndSendToExchange(data, properties, routingKey, throttle);
478
- }
479
-
480
- async sendHttpReply(data, headers) {
481
- const properties = this._createPropsFromHeaders(headers);
482
- const routingKey = headers.reply_to;
483
- properties.headers.protocolVersion = 1;
484
-
485
- if (!routingKey) {
486
- throw new Error(`Component emitted 'httpReply' event but 'reply_to' was not found in AMQP headers`);
487
- }
488
- return this.prepareMessageAndSendToExchange(data, properties, routingKey);
489
- }
490
-
491
- async sendError(err, headers, originalMessage, throttle) {
492
- // NOTICE both error and errorInput are transferred as base64 encoded.
493
- // this does not depend on protocolVersion header of message (see _decodeDefaultMessage or sendData methods)
494
- // this should be fixed in future, but it's OK at this moment
495
- const properties = this._createPropsFromHeaders(headers);
496
- const settings = this.settings;
497
-
498
- const encryptedError = this._encryptor.encryptMessageContent({
499
- name: err.name,
500
- message: err.message,
501
- stack: err.stack
502
- }, 'base64').toString();
503
-
504
- const payload = {
505
- error: encryptedError
506
- };
507
- if (originalMessage && originalMessage.content) {
508
- const protocolVersion = Number(originalMessage.properties.headers.protocolVersion || 1);
509
- if (protocolVersion >= 2) {
510
- payload.errorInput = this._encryptor.encryptMessageContent(
511
- this._encryptor.decryptMessageContent(originalMessage.content),
512
- 'base64'
513
- ).toString();
514
- } else {
515
- payload.errorInput = originalMessage.content.toString();
516
- }
517
- }
518
- const errorPayload = JSON.stringify(payload);
519
-
520
- let result = this.sendToExchange(
521
- settings.PUBLISH_MESSAGES_TO,
522
- settings.ERROR_ROUTING_KEY,
523
- errorPayload, properties,
524
- throttle
525
- );
526
-
527
- if (!settings.NO_ERROR_REPLIES && headers.reply_to) {
528
- log.debug('Sending error to %s', headers.reply_to);
529
- const replyToOptions = _.cloneDeep(properties);
530
- replyToOptions.headers[HEADER_ERROR_RESPONSE] = true;
531
- result = this.sendToExchange(settings.PUBLISH_MESSAGES_TO,
532
- headers.reply_to, encryptedError, replyToOptions);
533
- }
534
-
535
- return result;
536
- }
537
-
538
- async sendRebound(reboundError, originalMessage, outgoingHeaders) {
539
- const { settings } = this;
540
- let { properties: { headers } } = originalMessage;
541
- headers = {
542
- ...headers,
543
- end: new Date().getTime(),
544
- reboundReason: reboundError.message
545
- };
546
- log.trace('Rebound message');
547
- let reboundIteration = 1;
548
-
549
- if (headers.reboundIteration && typeof headers.reboundIteration === 'number') {
550
- reboundIteration = headers.reboundIteration + 1;
551
- }
552
-
553
- if (reboundIteration > settings.REBOUND_LIMIT) {
554
- return this.sendError(
555
- new Error('Rebound limit exceeded'),
556
- outgoingHeaders,
557
- originalMessage
558
- );
559
- } else {
560
- const properties = {
561
- ...originalMessage.properties,
562
- // retry in 15 sec, 30 sec, 1 min, 2 min, 4 min, 8 min, etc.
563
- expiration: Math.pow(2, reboundIteration - 1) * settings.REBOUND_INITIAL_EXPIRATION,
564
- headers: {
565
- ...headers,
566
- reboundIteration
567
- }
568
- };
569
-
570
- return this.sendToExchange(
571
- settings.PUBLISH_MESSAGES_TO,
572
- settings.REBOUND_ROUTING_KEY,
573
- originalMessage.content,
574
- properties
575
- );
576
- }
577
- }
578
-
579
- async sendSnapshot(data, headers, throttle) {
580
- const settings = this.settings;
581
- const exchange = settings.PUBLISH_MESSAGES_TO;
582
- const routingKey = settings.SNAPSHOT_ROUTING_KEY;
583
- const payload = JSON.stringify(data);
584
- const properties = this._createPropsFromHeaders(headers);
585
- return this.sendToExchange(exchange, routingKey, payload, properties, throttle);
586
- }
587
-
588
- _createPropsFromHeaders(headers) {
589
- return {
590
- contentType: 'application/json',
591
- contentEncoding: 'utf8',
592
- mandatory: true,
593
- headers: {
594
- ...headers,
595
- messageId: headers.messageId || uuid.v4()
596
- }
597
- };
598
- }
599
- }
600
-
601
- function getRoutingKeyFromHeaders(headers) {
602
- if (!headers) {
603
- return null;
604
- }
605
-
606
- function headerNamesToLowerCase(result, value, key) {
607
- result[key.toLowerCase()] = value;
608
- }
609
-
610
- const lowerCaseHeaders = _.transform(headers, headerNamesToLowerCase, {});
611
-
612
- return lowerCaseHeaders[HEADER_ROUTING_KEY];
613
- }
614
-
615
- function filterMessageHeaders(headers = {}) {
616
- return _.transform(headers, (result, value, key) => {
617
- if ([HEADER_ROUTING_KEY].includes(key.toLowerCase())) {
618
- return;
619
- }
620
- result[key] = value;
621
- }, {});
622
- }
623
-
624
- exports.Amqp = Amqp;
package/lib/messagesDB.js DELETED
@@ -1,37 +0,0 @@
1
- // Simple map to store messages by their IDs
2
- // This is useful when connection is re-established and we need to get the same
3
- // message again, but now from the new connection
4
- const EventEmitter = require('events');
5
-
6
- const messagesDB = (() => {
7
- const messagesById = new Map();
8
- const emitter = new EventEmitter();
9
-
10
- return {
11
- getMessageById: function getMessageById(id) {
12
- return messagesById.get(id);
13
- },
14
- addMessage: function addMessage(id, message) {
15
- const existingMessage = messagesById.get(id);
16
- messagesById.set(id, message);
17
- if (existingMessage) {
18
- emitter.emit('message-updated', id, message);
19
- }
20
- },
21
- deleteMessage: function deleteMessage(id) {
22
- messagesById.delete(id);
23
- },
24
- on: function on(event, listener) {
25
- emitter.on(event, listener);
26
- },
27
- off: function off(event, listener) {
28
- emitter.off(event, listener);
29
- },
30
- __reset__: function reset() {
31
- messagesById.clear();
32
- emitter.removeAllListeners();
33
- }
34
- };
35
- })();
36
-
37
- module.exports = messagesDB;