UNPKG

atomic-jetstream

Version:

NATS JetStream module for Brobridge Atomic

355 lines (277 loc) 7.48 kB
const events = require('events'); const util = require('util'); const nats = require('nats'); module.exports = class Client extends events.EventEmitter { constructor(nc = null, opts = {}) { super(); this.opts = Object.assign({ servers: '0.0.0.0:32803', maxPingOut: 3, maxReconnectAttempts: -1, pingInterval: 10000, }, opts); this.status = 'disconnected'; this.nc = nc; if (nc) { this.waitStatus(); } } isInitialized() { return (this.nc) ? true : false; } clone() { return new Client(this.nc, this.opts); } compareArrary(arr1, arr2) { return arr1.length === arr2.length && arr1.every((v, i) => v === arr2[i]); } async connect() { let opts = { servers: this.opts.servers, maxPingOut: this.opts.maxPingOut, maxReconnectAttempts: this.opts.maxReconnectAttempts, pingInterval: this.opts.pingInterval, }; this.nc = await nats.connect(opts); this.waitStatus(); // Updating states this.status = 'connected'; this.emit('connected'); } async disconnect() { if (!this.nc) return; await this.nc.close(); } async waitStatus() { try { for await (const s of this.nc.status()) { switch(s.type) { case nats.Events.Disconnect: this.status = 'disconnected'; this.emit('disconnect'); break; case nats.Events.Reconnect: this.status = 'reconnecting'; this.emit('connected'); break; case nats.DebugEvents.Reconnecting: this.status = 'reconnecting'; this.emit('reconnect'); break; } } } catch(e) { console.error(e); } } async createStream(streamName, opts = {}) { let jsm = await this.nc.jetstreamManager(); try { await jsm.streams.add(Object.assign(opts, { name: streamName, })); } catch(e) { throw e; } } async ensureStream(streamName, opts = {}) { let jsm = await this.nc.jetstreamManager(); try { let info = await jsm.streams.info(streamName) } catch(e) { // Not found if (e.code === '404') { console.log('creating new stream', streamName); return await this.createStream(streamName, opts); } throw e; } } async findStreamBySubject(subject) { let jsm = await this.nc.jetstreamManager(); return await jsm.streams.find(subject) } async findStreamBySubjects(subjects = []) { let streamName = null; for (let subject of subjects) { let found = await this.findStreamBySubject(subject) if (!found) { return null; } if (!streamName) { streamName = found; continue } if (streamName !== found) { throw new Error('multiple streams found by subjects'); } } return streamName; } async ensureConsumer(streamName, consumerName, opts = {}) { let jsm = await this.nc.jetstreamManager(); try { let info = await jsm.consumers.info(streamName, consumerName) // Check consumer confiuration or update let updated = false; for (let k in opts.config) { switch(k) { case 'name': case 'deliver_subject': case 'filter_subjects': case 'filter_subject': continue; } // Something's updated if (opts.config[k] != info.config[k]) { console.warn('consumer config(' + k + ') has changed', opts.config[k], info.config[k]); info.config[k] = opts.config[k]; updated = true; } } // Check filters let origFilters = info.config.filter_subjects || [ info.config.filter_subject ]; if (!this.compareArrary(opts.filters, origFilters)) { if (opts.filters.length == 1) { info.config.filter_subject = opts.filters[0]; info.config.filter_subjects = undefined; } else { info.config.filter_subject = undefined; info.config.filter_subjects = opts.filters; } console.warn('consumer config(filter_subjects) has changed', origFilters, 'to', opts.filters); updated = true; } if (updated) { console.warn('updating consumer (durable=' + consumerName + ')'); await jsm.consumers.update(streamName, consumerName, info.config); } return; } catch(e) { if (e.code !== '404') { throw e; } } try { // Not found consumer so creating new one opts.durable(consumerName); let cOpts = opts.getOpts(); // Not found, so trying to create consumer console.log('attempt to create new consumer (durable=' + consumerName + ')', cOpts.config.filter_subjects || cOpts.config.deliver_subject); await jsm.consumers.add(streamName, cOpts.config); console.log('consumer was created (durable=' + consumerName + ')'); } catch(e) { if (e.code === '400' && e.api_error.err_code === 10148) { console.log('consumer already exists (durable=' + consumerName + ')'); return; } throw e; } } async publish(subject, payload) { let js = this.nc.jetstream(); await js.publish(subject, payload); } async publishString(subject, payload) { let js = this.nc.jetstream(); let sc = nats.StringCodec(); await js.publish(subject, sc.encode(payload)); } async publishJSON(subject, payload = {}) { let js = this.nc.jetstream(); let sc = nats.JSONCodec(); await js.publish(subject, sc.encode(payload)); } decode(data) { let sc = nats.StringCodec(); return sc.decode(data); } async subscribe(subjects = [], opts = {}, callback) { // Preparing consumer options let cOpts = nats.consumerOpts(); cOpts.deliverTo(nats.createInbox()); // Preparing stream let streamName = opts.stream; if (!streamName) { // Find stream by subject streamName = await this.findStreamBySubjects(subjects); } if (!streamName) { throw new Error('no stream has specified subject'); } cOpts.bindStream(streamName); // Preparing subjects if (!Array.isArray(subjects)) { subjects = [subjects]; } if (subjects.length === 0) { throw new Error('no subject has specified'); } subjects.forEach((subject) => { cOpts.filterSubject(subject); }); switch(opts.ack) { case 'auto': cOpts.ackNone(); break; case 'all': cOpts.ackAll(); break; case 'manual': cOpts.ackExplicit(); cOpts.manualAck(); break; } switch(opts.delivery) { case 'last': cOpts.deliverLast(); break; case 'new': cOpts.deliverNew(); break; case 'all': cOpts.deliverAll(); break; case 'lastPerSubject': cOpts.deliverLastPerSubject(); break; case 'startSeq': cOpts.startSequence(opts.startSeq || 1); break; case 'startTime': cOpts.startTime(opts.startTime || new Date()); break; } if (opts.maxDeliver > 0) { cOpts.maxDeliver(opts.maxDeliver); } if (!opts.queue) { cOpts.flowControl(); cOpts.idleHeartbeat(5000); } if (opts.ackWait) { cOpts.ackWait(opts.ackWait || 10000); } if (opts.maxAckPending) { cOpts.maxAckPending(opts.maxAckPending || 2000); } if (opts.durable) { cOpts.durable(opts.durable); } if (opts.queue && opts.durable) { cOpts.queue(opts.durable); // ensure consumer await this.ensureConsumer(streamName, opts.durable, cOpts); } // Subscribe let js = this.nc.jetstream(); let sub = await js.subscribe(subjects[0], cOpts); (async () => { for await (const m of sub) { callback(m); } })() return sub; } }