UNPKG

@getanthill/datastore

Version:

Event-Sourced Datastore

254 lines 11.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const amqplib_1 = __importDefault(require("amqplib")); const merge_1 = __importDefault(require("lodash/merge")); const omit_1 = __importDefault(require("lodash/omit")); const broker_1 = __importDefault(require("./broker")); const REGEXP_MATCH_SLASHES = /\//g; const REGEXP_MATCH_WILDCARD = /\*/g; class AMQPClient extends broker_1.default { constructor(config, telemetry) { super(config, telemetry); this._connection = null; this._channel = null; this._queue = []; this._alreadyConnected = false; this._connectionId = 0; this._connectionUrls = []; this._closing = false; this._consumerTag = ''; this._consuming = new Map(); this._consumerTags = new Map(); this._connectionUrls = Array.isArray(config.url) === false ? [config.url] : config.url; } get connection() { if (this._connection === null) { throw AMQPClient.ERRORS.NOT_CONNECTED; } return this._connection; } get channel() { if (this._channel === null) { throw AMQPClient.ERRORS.NOT_CONNECTED; } return this._channel; } mapRoutingKey(topic, schema) { var _a; const topicWithNamespace = this.topicWithNamespace(topic); return { original: topic, topic: topicWithNamespace.replace(broker_1.default.REGEXP_MATCH_PATH_PARAMETERS, '+'), routingKey: topicWithNamespace .replace(REGEXP_MATCH_SLASHES, '.') .replace(broker_1.default.REGEXP_MATCH_PATH_PARAMETERS, '*'), regexp: new RegExp(topicWithNamespace .replace(broker_1.default.REGEXP_MATCH_PATH_PARAMETERS, '([^\\/]+)') .replace(REGEXP_MATCH_WILDCARD, '.*')), paramNames: ((_a = broker_1.default.REGEXP_MATCH_PATH_PARAMETERS.exec(topic)) !== null && _a !== void 0 ? _a : []).map((p) => p.slice(1, -1)), params: {}, validate: this.ajv.compile(schema), }; } async connect() { var _a, _b, _c; if (this._connection !== null) { return this; } try { (_a = this.telemetry) === null || _a === void 0 ? void 0 : _a.logger.debug('[AMQP] Connecting...'); const connectionUrl = this._connectionUrls[this._connectionId]; this._connectionId = (this._connectionId + 1) % this._connectionUrls.length; this._connection = await amqplib_1.default.connect(connectionUrl + '?heartbeat=10', this.config.options); this._alreadyConnected = true; (_b = this.telemetry) === null || _b === void 0 ? void 0 : _b.logger.debug('[AMQP] Connected ✅', { url: connectionUrl, }); this._connection.on('error', (err) => { var _a; (_a = this.telemetry) === null || _a === void 0 ? void 0 : _a.logger.error('[AMQP] Error', err); }); this._connection.on('close', () => { var _a, _b; if (this._closing === true) { return; } (_a = this.telemetry) === null || _a === void 0 ? void 0 : _a.logger.debug('[AMQP] Reconnecting...'); this._channel = null; this._connection = null; setTimeout(this.connect.bind(this), (_b = this.config.failover) === null || _b === void 0 ? void 0 : _b.reconnectionTimeoutInMilliseconds); }); this._channel = await this._connection.createChannel(); await this.channel.prefetch(this.config.channel.prefetch); await this.init(); await this.resubscribe(); await this.emptyQueue(); } catch (err) { if (this._alreadyConnected === false && this._connectionId === 0) { throw err; } setTimeout(this.connect.bind(this), (_c = this.config.failover) === null || _c === void 0 ? void 0 : _c.reconnectionTimeoutInMilliseconds); } return this; } async init() { await Promise.all([ this.channel.assertExchange(this.config.exchange.consumer.name, this.config.exchange.consumer.type, this.config.exchange.consumer.options), this.channel.assertExchange(this.config.exchange.producer.name, this.config.exchange.producer.type, this.config.exchange.producer.options), this.channel.assertQueue(this.config.queue.consumer.name, this.config.queue.consumer.options), this.config.queue.errors.isEnabled === true && this.channel.assertQueue(this.config.queue.errors.name, this.config.queue.errors.options), ]); if (this.config.queue.errors.isEnabled === true) { await this.channel.bindQueue(this.config.queue.errors.name, this.config.exchange.producer.name, '*.*.errors'); } return this; } async end() { this._closing = true; this._channel !== null && this._consumerTag && (await this.channel.cancel(this._consumerTag)); this._consumerTag = ''; this._consuming = new Map(); this._consumerTags = new Map(); this._channel !== null && (await this._channel.close()); this._channel = null; this._connection !== null && (await this._connection.close()); this._connection = null; this._closing = false; return this; } authenticate(tokens, handler) { return (event, route, headers, opts) => { var _a; const token = headers === null || headers === void 0 ? void 0 : headers['authorization']; if (tokens.find((t) => t.token === token)) { return handler(event, route, headers, opts); } typeof (opts === null || opts === void 0 ? void 0 : opts.ack) === 'function' && opts.ack(); (_a = this.telemetry) === null || _a === void 0 ? void 0 : _a.logger.debug('[events#authenticate] Authentication failed', { event }); }; } nackIfFirstSeen(message) { var _a; ((_a = message === null || message === void 0 ? void 0 : message.fields) === null || _a === void 0 ? void 0 : _a.redelivered) === true ? this.channel.ack(message) : this.channel.nack(message); } onMessage(message) { var _a, _b, _c, _d, _e; if (!message) { return; } try { const { fields, content, properties } = message; (_a = this.telemetry) === null || _a === void 0 ? void 0 : _a.logger.debug('[amqp#onMessage] Received a new message', { fields, properties, }); const routingKey = fields.routingKey.replace(/\./g, '/'); const route = this.getRoute(routingKey); if (route === null) { (_b = this.telemetry) === null || _b === void 0 ? void 0 : _b.logger.debug('[services#amqp] No route matching the routing key', { routingKey, acked: false }); return this.nackIfFirstSeen(message); } const event = JSON.parse(content.toString()); const headers = (_c = properties.headers) !== null && _c !== void 0 ? _c : {}; (_d = this.telemetry) === null || _d === void 0 ? void 0 : _d.logger.debug('[services#amqp] Handling event', { routingKey, event, headers: (0, omit_1.default)(headers, 'authorization'), }); const isValid = route.validate(event); if (isValid === false) { this.logOnInvalid(event, route); return this.channel.ack(message); } this.emit(route.original, event, this.parseTopic(routingKey, route), headers, { source: 'amqp', original: message, delivery: fields.redelivered === true ? 1 : 0, ack: () => this.channel.ack(message), nack: () => this.channel.nack(message), }); } catch (err) { (_e = this.telemetry) === null || _e === void 0 ? void 0 : _e.logger.warn('[services#amqp] Failed processing message', err); this.nackIfFirstSeen(message); } } async emptyQueue() { while (this._queue.length > 0) { const { topic, payload, options } = this._queue.shift(); await this.publish(topic, payload, options); } } async publish(topic, payload, options = {}) { try { return await this.channel.publish(this.config.exchange.producer.name, this.topicWithNamespace(topic).replace(REGEXP_MATCH_SLASHES, '.'), Buffer.from(JSON.stringify(payload)), (0, merge_1.default)({}, options, { headers: this.config.headers })); } catch (err) { this._queue.push({ topic, payload, options }); } } async consume(queueConfig, consumeOptions) { var _a; if (consumeOptions === void 0) { consumeOptions = (_a = this.config.consume) === null || _a === void 0 ? void 0 : _a.options; } if (this._consuming.has(queueConfig.name)) { return this._consuming.get(queueConfig.name); } const promise = new Promise((resolve, reject) => { this.channel .consume(queueConfig.name, this.onMessage.bind(this), consumeOptions) .then(({ consumerTag }) => { this._consumerTag = consumerTag; resolve(); }) .catch(reject); }); this._consuming.set(queueConfig.name, promise); return promise; } async resubscribe() { const subscriptions = Array.from(this._consumerTags.values()); this._consumerTag = ''; this._consuming = new Map(); this._consumerTags = new Map(); return Promise.all(subscriptions.map(({ topic, schema, queueConfig }) => this.subscribe(topic, schema, queueConfig))); } async subscribe(topic, schema, queueConfig = this.config.queue.consumer) { const mappedTopic = this.mapRoutingKey(topic, schema); this.topics.set(mappedTopic.routingKey, mappedTopic); this.addChannelToSpec(topic, schema); await this.channel.bindQueue(queueConfig.name, this.config.exchange.consumer.name, mappedTopic.routingKey); this._consumerTags.set(topic, { topic, schema, queueConfig }); await this.consume(queueConfig); return this; } async next(topic, timeout = 5000, cb = async (opts) => opts.ack()) { let resolve; let _timeout; const promise = new Promise((_resolve, _reject) => { resolve = _resolve; _timeout = setTimeout(() => _reject(new Error('[amqp#next] Message timeout')), timeout); }); const handler = async (event, route, headers, opts) => { clearTimeout(_timeout); resolve(event); this.removeListener(topic, handler); opts !== undefined && (await cb(opts)); }; this.on(topic, handler); return promise; } } AMQPClient.ERRORS = { NOT_CONNECTED: new Error('AMQP disconnected') }; exports.default = AMQPClient; //# sourceMappingURL=amqp.js.map