@sera4/essentia 1.0.19 → 1.0.21

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
@@ -7,6 +7,7 @@ module.exports = {
7
7
  lastCommit : require("./last_commit"),
8
8
  halDecorator : require("./hal"),
9
9
  formatter : require("./formatter"),
10
- healthCheck: require("./health")
10
+ healthCheck : require("./health"),
11
+ queue : require("./queue")
11
12
  }
12
13
  //console.log(module.exports.formatter.formatError({ status: 200, error: ["error1"] }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sera4/essentia",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "description": "A library of utilities for Teleporte Web Services",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -25,6 +25,7 @@
25
25
  "sinon": "^5.0.1"
26
26
  },
27
27
  "dependencies": {
28
+ "amqplib": "^0.5.5",
28
29
  "async": "^2.6.1",
29
30
  "fast-safe-stringify": "^2.0.6",
30
31
  "git-rev": "^0.2.1",
package/package.tar.gz CHANGED
Binary file
package/queue/index.js ADDED
@@ -0,0 +1,196 @@
1
+ "use strict";
2
+ const amqp = require("amqplib/callback_api");
3
+ const logger = require("./queue-logger");
4
+ const { Publisher } = require("./publisher");
5
+ const { Subscriber } = require("./subscriber");
6
+
7
+ /**
8
+ * An interface to AMQP service
9
+ */
10
+ class S4Queue {
11
+ constructor() {
12
+ this.connection = null;
13
+ this.retry = 10;
14
+ this.pubs = {};
15
+ this.subs = {};
16
+ }
17
+
18
+ /**
19
+ * Opens a connection to AMQP server.
20
+ * @param {Object} options Connection parameters. At the very least options.connectionUrl must be present.
21
+ */
22
+ async openConnection(options) {
23
+ logger.setEnabled(options.logEnabled);
24
+
25
+ if (this.connection) {
26
+ logger.warn("Already initialized");
27
+ return;
28
+ }
29
+
30
+ const retry = parseInt(options.retryInterval);
31
+ this.retry = retry || 10;
32
+
33
+ logger.debug("Attempting connection...");
34
+
35
+ // The docs say there is amqp.connect returning
36
+ // a promise, but this version doesn't seem to like
37
+ // when a callback isn't provided, therefore run
38
+ // in a new Promise
39
+ return new Promise((resolve, reject) => {
40
+ amqp.connect(options.connectionUrl, (err, conn) => {
41
+
42
+ if (err) {
43
+ return reject(err);
44
+ }
45
+
46
+ this.connection = conn;
47
+ if (!this.connection) {
48
+ return reject(new Error("Failed to obtain connection"));
49
+ }
50
+
51
+ this.connection.on("error", (err) => {
52
+ logger.error("connection error", err);
53
+ // Do not attempt to reconnect as the
54
+ // 'close' event will also be emitted
55
+ // if this error closes the connection
56
+ });
57
+
58
+ this.connection.on("close", (err) => {
59
+ logger.error("connection closed");
60
+ this.connection = null;
61
+ if (err) {
62
+ logger.debug("connection closed due to error:", err);
63
+ logger.debug("Reconnecting in", this.retry, "seconds");
64
+ setTimeout(() => this.openConnection(options), this.retry * 1000);
65
+ } else {
66
+ // If there was no error, it is us who closed
67
+ // this connection, thus a reconnection will not
68
+ // be attempted.
69
+ logger.debug("Will not re-attempt");
70
+ }
71
+ });
72
+
73
+ logger.debug("Connected");
74
+ resolve();
75
+ });
76
+ });
77
+ }
78
+
79
+ /**
80
+ * Closes the connection to AMQP server
81
+ * and clears the registered publishers
82
+ * and subscribers
83
+ */
84
+ async closeConnection() {
85
+ return new Promise((resolve, reject) => {
86
+ if (!this.connection) {
87
+ return resolve();
88
+ }
89
+ this.connection.close((err) => {
90
+ if (err) {
91
+ reject(err);
92
+ } else {
93
+ this.subs = {};
94
+ this.pubs = {};
95
+ this.connection = null;
96
+ resolve();
97
+ }
98
+ });
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Returns whether we have an active connection
104
+ * or not.
105
+ */
106
+ isConnected() {
107
+ return this.connection != null && typeof(this.connection) != 'undefined';
108
+ }
109
+
110
+ /**
111
+ * Registers a new direct publisher on the given exchange.
112
+ * If exchange doesn't exist, one will be created.
113
+ * @param {String} exchange the name of the exchange on AMQP service.
114
+ */
115
+ async registerDirectPublisher(exchange) {
116
+ const name = exchange;
117
+ if (Object.keys(this.pubs).includes(name)) {
118
+ throw new Error(`Publisher ${name} already registered`);
119
+ }
120
+
121
+ const p = new Publisher(exchange, 'direct', true, name);
122
+ await p.init(this.connection);
123
+ this.pubs[name] = p;
124
+ }
125
+
126
+ /**
127
+ * Registers a new direct subscriber on a given exchange and queue.
128
+ * If exchange and queue don't exists, they'll be created
129
+ * and bound through the provided routing key.
130
+ * @param {String} exchange the name of the exchange.
131
+ * @param {String} queue the name of the queue.
132
+ * @param {String} key the name of the routing key.
133
+ */
134
+ async registerDirectSubscriber(exchange, queue, key) {
135
+ const name = `${exchange}-${queue}-${key}`;
136
+ if (Object.keys(this.subs).includes(name)) {
137
+ throw new Error(`Subscriber ${name} already registered`);
138
+ }
139
+
140
+ const s = new Subscriber(exchange, queue, 'direct', true, key, name);
141
+ await s.init(this.connection);
142
+ this.subs[name] = s;
143
+ }
144
+
145
+ /**
146
+ * Publishes a message on a given exchange with routing key.
147
+ * @param {String} exchange the name of the exchange.
148
+ * @param {String} key the name of the routing key.
149
+ * @param {Object} msg a JSON object as the message.
150
+ */
151
+ publishDirectMessage(exchange, key, msg) {
152
+ const p = this.pubs[exchange];
153
+
154
+ if (!key) {
155
+ throw(new Error(`Direct message require a key`));
156
+ }
157
+
158
+ if (!p) {
159
+ throw(new Error(`No PUB for ${exchange} found`));
160
+ }
161
+
162
+ if (p.type !== 'direct') {
163
+ throw(new Error(`PUB ${exchange} is not direct`));
164
+ }
165
+
166
+ let content = msg;
167
+ if (content.constructor !== ({}).constructor) {
168
+ // if not in JSON format, let's make it so
169
+ content = {
170
+ msg: msg
171
+ }
172
+ }
173
+
174
+ p.publish({key, content});
175
+
176
+ }
177
+
178
+ /**
179
+ * Adds a listener for a specific key.
180
+ * If a subscriber that was regitered was found
181
+ * with that key, the listener is added, otherwise
182
+ * it's ignored
183
+ * @param {*} key AMQP routing key
184
+ * @param {*} listener callback
185
+ */
186
+ addMessageListener(key, listener) {
187
+ Object.keys(this.subs).forEach(k => {
188
+ const s = this.subs[k];
189
+ if (s.key === key) {
190
+ s.registerCallback(listener);
191
+ }
192
+ });
193
+ }
194
+ }
195
+
196
+ module.exports = new S4Queue();
@@ -0,0 +1,77 @@
1
+ const logger = require("./queue-logger");
2
+
3
+ /**
4
+ * A publisher for AMQP service
5
+ */
6
+ class Publisher {
7
+
8
+ /**
9
+ *
10
+ * @param {String} exchange AMQP exchange name.
11
+ * @param {String} type direct|fanout| etc (supported types by RabbitMQ).
12
+ * @param {boolean} durable true if the messages should persist service restarts.
13
+ * @param {String} name a name to identify this publisher in the logs.
14
+ */
15
+ constructor(exchange, type, durable, name) {
16
+ this.exchange = exchange;
17
+ this.type = type;
18
+ this.durable = durable;
19
+ this.name = name;
20
+ this.channel = null;
21
+ }
22
+
23
+ /**
24
+ * Initializes this publisher with a connection
25
+ * and asserts the exchange exists
26
+ * @param {Object} conn amqp connection object
27
+ */
28
+ async init (conn) {
29
+ logger.debug(`Initializing PUB channel for ${this.name}`);
30
+
31
+ if(this.channel) {
32
+ logger.warn(`PUB channel for ${this.name} already initialized`);
33
+ return;
34
+ }
35
+
36
+ this.channel = await conn.createConfirmChannel();
37
+ if (!this.channel) {
38
+ throw(new Error(`Cannot create PUB channel for ${this.name}`));
39
+ }
40
+
41
+ logger.debug(`Initialized PUB channel for ${this.name}`);
42
+ await this.channel.assertExchange(this.exchange, this.type, {durable: this.durable});
43
+
44
+ this.channel.on("error", (err) => {
45
+ logger.debug(`Connection error for ${this.name}`, err);
46
+ this.channel = null;
47
+ });
48
+
49
+ this.channel.on("close", () => {
50
+ logger.debug(`Connection closed for ${this.name}`);
51
+ this.channel = null;
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Publishes a message to AMQP
57
+ * @param {Objecgt} msg A json object to be published to AMQP
58
+ */
59
+ publish(msg) {
60
+ if (!this.channel) {
61
+ throw(new Error(`No channel for ${this.name} to publish`));
62
+ }
63
+ logger.debug("Publishing message", msg);
64
+ const content = Buffer.from(JSON.stringify(msg.content));
65
+ this.channel.publish(this.exchange, msg.key, content, {persistent: true}, (err, ok) => {
66
+ if (err) {
67
+ logger.error(`An error occured publishing message to ${this.name}`);
68
+ } else {
69
+ logger.debug("Message delivered:", msg);
70
+ }
71
+ });
72
+ }
73
+ }
74
+
75
+ module.exports = {
76
+ Publisher
77
+ }
@@ -0,0 +1,38 @@
1
+ const logger = require("../logger/s4-logger");
2
+ let enabled = false;
3
+
4
+ module.exports = {
5
+ debug(...args) {
6
+ if (enabled) {
7
+ logger.debug(":: AMQP ::", args);
8
+ }
9
+ },
10
+
11
+ log(...args) {
12
+ if (enabled) {
13
+ logger.debug(":: AMQP ::", args);
14
+ }
15
+ },
16
+
17
+ warn(...args) {
18
+ if (enabled) {
19
+ logger.warn(":: AMQP ::", args);
20
+ }
21
+ },
22
+
23
+ info(...args) {
24
+ if (enabled) {
25
+ logger.info(":: AMQP ::", args);
26
+ }
27
+ },
28
+
29
+ error(...args) {
30
+ if (enabled) {
31
+ logger.error(":: AMQP ::", args);
32
+ }
33
+ },
34
+
35
+ setEnabled(e) {
36
+ enabled = e;
37
+ }
38
+ }
@@ -0,0 +1,88 @@
1
+ const logger = require("./queue-logger");
2
+
3
+ /**
4
+ * A subscriber for AMQP service
5
+ */
6
+ class Subscriber {
7
+
8
+ /**
9
+ * Builds a subscriber
10
+ * @param {String} exchange name of the AMQP exchange.
11
+ * @param {String} queue name of the AMQP service.
12
+ * @param {String} type direct|fanout or other supported connection types.
13
+ * @param {boolean} durable durable connection or not.
14
+ * @param {String} key routing key. This will be used to bind the queue to the exchange.
15
+ * @param {String} name a usefull name to be printed in the logs.
16
+ */
17
+ constructor(exchange, queue, type, durable, key, name) {
18
+ this.exchange = exchange;
19
+ this.queue = queue;
20
+ this.type = type;
21
+ this.durable = durable;
22
+ this.key = key;
23
+ this.name = name;
24
+ this.channel = null;
25
+ this.callbacks = [];
26
+ }
27
+
28
+ /**
29
+ * Initializes the subscriber on the given connection.
30
+ * During this call, the exchange and the queues are asserted
31
+ * and bound via the key provided in the constructor.
32
+ * @param {Objecgt} conn aqmp connection object
33
+ */
34
+ async init (conn) {
35
+ logger.debug(`Initializing SUB channel for ${this.name}`);
36
+
37
+ if(this.channel) {
38
+ logger.warn(`SUB channel for ${this.name} already initialized`);
39
+ return;
40
+ }
41
+
42
+ this.channel = await conn.createChannel();
43
+ if (!this.channel) {
44
+ throw(new Error(`Cannot create SUB channel for ${this.name}`));
45
+ }
46
+
47
+ this.channel.on("error", (err) => {
48
+ logger.debug(`Connection error for ${this.name}`, err);
49
+ this.channel = null;
50
+ });
51
+
52
+ this.channel.on("close", () => {
53
+ logger.debug(`Connection closed for ${this.name}`);
54
+ this.channel = null;
55
+ });
56
+
57
+ logger.debug(`Initialized SUB channel for ${this.name}`);
58
+ await this.channel.assertExchange(this.exchange, this.type, {durable: this.durable});
59
+ await this.channel.assertQueue(this.queue, { durable: this.durable });
60
+
61
+ logger.debug(`Binding queue ${this.queue} on ${this.exchange} exchange for ${this.key} key`);
62
+ await this.channel.bindQueue(this.queue, this.exchange, this.key);
63
+
64
+ this.channel.consume(this.queue, (msg) => {
65
+ const { fields, content } = msg;
66
+ const payload = JSON.parse(content.toString());
67
+ logger.debug(`Got message from exchange '${fields.exchange}' with key '${fields.routingKey}'`);
68
+ logger.debug("Content", payload);
69
+ this.callbacks.forEach(cb => {
70
+ cb(payload);
71
+ });
72
+ this.channel.ack(msg);
73
+ }, { noAck: false });
74
+ }
75
+
76
+ /**
77
+ * Register a new callback to recive message through this
78
+ * subscriber object
79
+ * @param {function} cb A callback to be called when a message arrives
80
+ */
81
+ registerCallback(cb) {
82
+ this.callbacks.push(cb);
83
+ }
84
+ }
85
+
86
+ module.exports = {
87
+ Subscriber
88
+ }