UNPKG

@getanthill/datastore

Version:

Event-Sourced Datastore

171 lines 6.44 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const events_1 = require("events"); const eventsource_1 = __importDefault(require("eventsource")); const merge_1 = __importDefault(require("lodash/merge")); const amqp_1 = __importDefault(require("../services/amqp")); const utils_1 = require("./utils"); class Streams extends events_1.EventEmitter { constructor(config, core) { super(); this.config = { baseUrl: 'http://localhost:3001', token: 'token', debug: false, connector: 'http', }; this._streams = new Map(); this.config = (0, merge_1.default)({}, this.config, config); this._core = core; this._telemetry = this.config.telemetry; } getEventSource(url, headers) { return new eventsource_1.default(url, headers); } getAMQPClient(options) { return new amqp_1.default((0, merge_1.default)({}, this.config.amqp, { queue: { consumer: { name: options?.queueName, }, }, }), this._telemetry); } // HTTP Stream getStreamId(model, source, query = {}) { return `${model}:${source}:${JSON.stringify(query)}`; } async getStreamCloseMethod(model, source, streamId, query, options) { let streamHandler; if (this.config.connector === 'amqp') { streamHandler = this.streamAMQP.bind(this); } else { streamHandler = this.streamHTTP.bind(this); } return streamHandler((data, route, headers, opts) => this.emit(streamId, data, route, headers, opts), model, source, query, options); } /** * Streaming API * @beta */ async listen(model, source, query, options) { const streamId = this.getStreamId(model, source, query); if (this._streams.has(streamId)) { this._telemetry?.logger.debug('[Datastore] Stream already registered'); return Promise.resolve(this._streams.get(streamId)); } const close = await this.getStreamCloseMethod(model, source, streamId, query, options); this._streams.set(streamId, close); if (typeof options?.forward?.emit === 'function') { this.on(streamId, (event, route, headers, opts) => options.forward.emit(streamId, event, route, headers, opts)); } /* istanbul ignore next */ return () => this.close(streamId); } close(streamId) { const close = this._streams.get(streamId); typeof close === 'function' && close(); this._streams.delete(streamId); this.removeAllListeners(streamId); } closeAll() { for (const streamId of this._streams.keys()) { this.close(streamId); } this._telemetry?.logger.info('[Datastore#closeAll] All streams closed', { url: this.config.baseUrl, }); } async streamAMQP(handler, model, source, query = {}, options) { this._telemetry?.logger.info('[Datastore#streamAMQP] Initialization', { url: this.config.baseUrl, model, source, query, options, }); const amqp = this.getAMQPClient(options); await amqp.connect(); const modelName = model === 'all' ? '*' : model; const topic = source === 'events' ? `${modelName}/*/events/*` : `${modelName}/*/success/*`; amqp.on(topic, async (event, route, headers, opts) => { await handler(event, route, headers, opts); }); const schema = options?.queryAsJSONSchema === true ? query : (0, utils_1.objToJsonSchema)(query); await amqp.subscribe(topic, schema); return () => { amqp.end().catch((err) => { this._telemetry?.logger.error('[Streams#streamAMQP] Error on closing', { err, }); }); }; } /** * @deprecated in favor of streamHTTP */ stream(...args) { /* @ts-ignore */ return this.streamHTTP.call(this, ...args); } /** * Stream route to listen for specific documents update from the * Datastore */ /* istanbul ignore next */ async streamHTTP(handler, model = 'all', source = 'entities', query) { return new Promise((resolve) => { const projections = Array.isArray(query) ? query : []; if (query && !Array.isArray(query) && Object.keys(query).length > 0) { const match = {}; for (const key of Object.keys(query)) { match[`fullDocument.${key}`] = query[key]; } projections.push({ $match: match }); } const evtSource = this.getEventSource(this.config.baseUrl + this._core.getPath('stream', model, source, 'sse') + '?pipeline=' + JSON.stringify(projections), { withCredentials: true, headers: { authorization: this.config.token, }, }); evtSource.addEventListener('message', async function (event) { if (!event.data) { return; } await handler(JSON.parse(event.data)); }); evtSource.addEventListener('error', (err) => this._telemetry?.logger.warn('[Datastore#streamHTTP] Stream error', { url: this.config.baseUrl, model, source, err, })); const _close = () => { evtSource.close(); }; /** * The event is emitted on first line reception. This is * possible thanks to the keep alive message * (#serverSentEventKeepAlive: :\n\n) */ evtSource.addEventListener('open', (e) => { resolve(_close); }); }); } } Streams.ERRORS = { ERROR_STREAM_MAX_RECONNECTION_ATTEMPTS_REACHED: new Error('Max reconnection attempts reached for streaming'), }; exports.default = Streams; //# sourceMappingURL=Streams.js.map