@sera4/essentia 1.0.20 → 1.0.22
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 +2 -2
- package/package.json +3 -1
- package/package.tar.gz +0 -0
- package/queue/index.js +197 -0
- package/queue/publisher.js +89 -0
- package/queue/queue-logger.js +38 -0
- package/queue/subscriber.js +88 -0
package/index.js
CHANGED
|
@@ -7,6 +7,6 @@ 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
|
-
//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.
|
|
3
|
+
"version": "1.0.22",
|
|
4
4
|
"description": "A library of utilities for Teleporte Web Services",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -25,9 +25,11 @@
|
|
|
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",
|
|
32
|
+
"uuid": "^3.3.3",
|
|
31
33
|
"winston": "^3.2.1",
|
|
32
34
|
"winston-logstash": "^0.4.0"
|
|
33
35
|
}
|
package/package.tar.gz
CHANGED
|
Binary file
|
package/queue/index.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
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
|
+
async publishDirectMessage(exchange, key, msg) {
|
|
152
|
+
return new Promise((resolve, reject) => {
|
|
153
|
+
const p = this.pubs[exchange];
|
|
154
|
+
|
|
155
|
+
if (!key) {
|
|
156
|
+
reject(new Error(`Direct message require a key`));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!p) {
|
|
160
|
+
reject(new Error(`No PUB for ${exchange} found`));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (p.type !== 'direct') {
|
|
164
|
+
reject(new Error(`PUB ${exchange} is not direct`));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let content = msg;
|
|
168
|
+
if (content.constructor !== ({}).constructor) {
|
|
169
|
+
// if not in JSON format, let's make it so
|
|
170
|
+
content = {
|
|
171
|
+
msg: msg
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return p.publish({key, content});
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Adds a listener for a specific key.
|
|
181
|
+
* If a subscriber that was regitered was found
|
|
182
|
+
* with that key, the listener is added, otherwise
|
|
183
|
+
* it's ignored
|
|
184
|
+
* @param {*} key AMQP routing key
|
|
185
|
+
* @param {*} listener callback
|
|
186
|
+
*/
|
|
187
|
+
addMessageListener(key, listener) {
|
|
188
|
+
Object.keys(this.subs).forEach(k => {
|
|
189
|
+
const s = this.subs[k];
|
|
190
|
+
if (s.key === key) {
|
|
191
|
+
s.registerCallback(listener);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = new S4Queue();
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const logger = require("./queue-logger");
|
|
2
|
+
const uuidv4 = require("uuid/v4");
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A publisher for AMQP service
|
|
6
|
+
*/
|
|
7
|
+
class Publisher {
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
*
|
|
11
|
+
* @param {String} exchange AMQP exchange name.
|
|
12
|
+
* @param {String} type direct|fanout| etc (supported types by RabbitMQ).
|
|
13
|
+
* @param {boolean} durable true if the messages should persist service restarts.
|
|
14
|
+
* @param {String} name a name to identify this publisher in the logs.
|
|
15
|
+
*/
|
|
16
|
+
constructor(exchange, type, durable, name) {
|
|
17
|
+
this.exchange = exchange;
|
|
18
|
+
this.type = type;
|
|
19
|
+
this.durable = durable;
|
|
20
|
+
this.name = name;
|
|
21
|
+
this.channel = null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Initializes this publisher with a connection
|
|
26
|
+
* and asserts the exchange exists
|
|
27
|
+
* @param {Object} conn amqp connection object
|
|
28
|
+
*/
|
|
29
|
+
async init (conn) {
|
|
30
|
+
logger.debug(`Initializing PUB channel for ${this.name}`);
|
|
31
|
+
|
|
32
|
+
if(this.channel) {
|
|
33
|
+
logger.warn(`PUB channel for ${this.name} already initialized`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.channel = await conn.createConfirmChannel();
|
|
38
|
+
if (!this.channel) {
|
|
39
|
+
throw(new Error(`Cannot create PUB channel for ${this.name}`));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
logger.debug(`Initialized PUB channel for ${this.name}`);
|
|
43
|
+
await this.channel.assertExchange(this.exchange, this.type, {durable: this.durable});
|
|
44
|
+
|
|
45
|
+
this.channel.on("error", (err) => {
|
|
46
|
+
logger.debug(`Connection error for ${this.name}`, err);
|
|
47
|
+
this.channel = null;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
this.channel.on("close", () => {
|
|
51
|
+
logger.debug(`Connection closed for ${this.name}`);
|
|
52
|
+
this.channel = null;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Publishes a message to AMQP
|
|
58
|
+
* @param {Object} msg A json object to be published to AMQP.
|
|
59
|
+
* A tracking uuid for the message will be returned.
|
|
60
|
+
*/
|
|
61
|
+
async publish(msg) {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
|
|
64
|
+
if (!this.channel) {
|
|
65
|
+
reject(new Error(`No channel for ${this.name} to publish`));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
logger.debug("Publishing message", msg);
|
|
69
|
+
const content = {
|
|
70
|
+
tracking_id: uuidv4(),
|
|
71
|
+
data: Buffer.from(JSON.stringify(msg.content))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.channel.publish(this.exchange, msg.key, content, {persistent: true}, (err, ok) => {
|
|
75
|
+
if (err) {
|
|
76
|
+
logger.error(`An error occured publishing message to ${this.name}`);
|
|
77
|
+
reject(err);
|
|
78
|
+
} else {
|
|
79
|
+
logger.debug("Message delivered:", msg);
|
|
80
|
+
resolve(content.tracking_id);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = {
|
|
88
|
+
Publisher
|
|
89
|
+
}
|
|
@@ -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 {Object} 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
|
+
}
|