UNPKG

@eang/core

Version:

eang - model driven enterprise event processing

246 lines 9.94 kB
import { jetstream, jetstreamManager } from '@nats-io/jetstream'; import { connectable, Subject } from 'rxjs'; import { connect } from '@nats-io/transport-node'; import { parseMsg } from './functions.js'; export class NatsStreamService { // public readonly tenants = {} as TenantStreams consumerData = {}; nc; js; jsm; // private eventSubj = new Subject<any>() // private event$ = connectable(this.eventSubj.asObservable().pipe(), { // connector: () => new Subject(), // resetOnDisconnect: false // }) constructor() { } async initialize(opts) { this.nc = await connect(opts); this.jsm = await jetstreamManager(this.nc); this.js = jetstream(this.nc); // Log out infos about existing jet streams and consumers // const streams = await this.jsm?.streams.list().next() // streams?.forEach(async (si, i) => { // this.logger.debug(`nss: ${i}. stream: ${JSON.stringify(si.config)}`) // const consumers = await this.jsm?.consumers.list(si.config.name).next() // consumers?.forEach((ci, i) => { // this.logger.debug(`nss: ${i}. consumer: ${JSON.stringify(ci.config)}`) // }) // }) // this.logger.trace('nss: Finished initializing stream!') return { nc: this.nc, js: this.js, jsm: this.jsm }; } async ensureStream(streamName, subjects) { const existingStreams = await this.jsm.streams.list().next(); const existingStream = existingStreams.find((si) => si.config.name === streamName); if (existingStream) { return { created: false, streamInfo: existingStream }; } else { const newStream = await this.jsm?.streams.add({ name: streamName, subjects: subjects }); return { created: true, streamInfo: newStream }; } } async deleteStream(streamName) { const deleted = await this.jsm.streams.delete(streamName); return deleted; } async ensureConsumer(streamName, consumerName, filter_subjects = [`eang.>`], opts) { let existingConsumerInfo = undefined; let consumerConfig = { ack_policy: 'explicit', filter_subjects }; if (consumerName !== undefined) { consumerConfig.durable_name = consumerName; } if (opts) { consumerConfig = { ...consumerConfig, ...opts }; } if (consumerName) { const consumers = await this.jsm?.consumers.list(streamName).next(); existingConsumerInfo = consumers?.find((ci) => ci.config.durable_name === consumerName); if (existingConsumerInfo) { // Check if filter subjects differ const existingFilterSubjects = existingConsumerInfo?.config.filter_subjects || []; const filterSubjectsDiffer = JSON.stringify(existingFilterSubjects.sort()) !== JSON.stringify(filter_subjects.sort()); if (filterSubjectsDiffer) { // Update the consumer with new filter subjects await this.jsm?.consumers.update(streamName, consumerName, consumerConfig); existingConsumerInfo = (await this.jsm?.consumers.info(streamName, consumerName)) || undefined; } const consumer = await this.js?.consumers.get(streamName, consumerName); if (!consumer || !existingConsumerInfo) { throw new Error('Failed to get consumer or consumer info'); } return { consumer, consumerInfo: existingConsumerInfo }; } else { const consumerInfo = await this.jsm?.consumers.add(streamName, consumerConfig); const consumer = await this.js?.consumers.get(streamName, consumerName); if (!consumer || !consumerInfo) { throw new Error('Failed to create consumer or get consumer info'); } return { consumer, consumerInfo }; } } else { const consumer = await this.js?.consumers.get(streamName, consumerConfig); if (!consumer) { throw new Error('Failed to get consumer'); } const consumerInfo = await consumer.info(); return { consumer, consumerInfo }; } } async ensureConsumerStream(consumerName, subscription, opts) { let consumerData = undefined; if (consumerName) { consumerData = this.consumerData[consumerName]; if (consumerData) { return consumerData; } } // add a new durable consumer per tenant if (!this.jsm) { throw new Error('JetStreamManager is not initialized'); } const { consumer, consumerInfo } = await this.ensureConsumer('eang', consumerName, subscription, opts); const eventSubj = new Subject(); const event$ = connectable(eventSubj.asObservable().pipe(), { connector: () => new Subject(), resetOnDisconnect: false }); event$.connect(); if (!consumerInfo) { throw new Error('Consumer info is undefined'); } consumerName = consumerInfo.name; consumerData = { consumer, consumerInfo, consumerName, eventSubj, event$, startToConsume: async () => { const messages = await consumer.consume({ max_messages: 1 }); for await (const m of messages) { const eangEvent = parseMsg(m); eventSubj.next(eangEvent); // m.ack() } } }; if (!consumerData) { throw new Error('Consumer data is undefined'); } this.consumerData[consumerName] = consumerData; return consumerData; } async deleteConsumerStream(consumerName) { try { if (this.consumerData[consumerName]) { this.consumerData[consumerName].eventSubj.complete(); delete this.consumerData[consumerName]; } const deleted = await this.jsm.consumers.delete('eang', consumerName); return deleted; } catch (e) { if (e.message === 'consumer not found') { return true; } console.error(e); return false; } } async startToConsume(consumerName) { const consumerData = this.consumerData[consumerName]; if (!consumerData) { throw new Error(`Consumer '${consumerName}' not found. Ensure the consumer is created first.`); } const messages = await consumerData.consumer.consume({ max_messages: 1 }); for await (const m of messages) { const eangEvent = parseMsg(m); consumerData.eventSubj.next(eangEvent); m.ack(); } } async publish(events, options) { const pubAcks = []; for (const e of events) { if (!e.tenant) { e.tenant = options.tenant; } if (!e.organizationalUnit && options.organizationalUnit !== undefined) { e.organizationalUnit = options.organizationalUnit; } if (!e.organizationalUnit) { e.organizationalUnit = e.tenant; } if (!e.user) { e.user = options.user; } const { subject, payload, opts } = getEangMsgFromEvent(e, options?.jetStreamOptions); const pAck = await this.js.publish(subject, payload, opts); pubAcks.push(pAck); } return pubAcks; } } function getEangMsgFromEvent(event, options) { const { entity, tenant, organizationalUnit, user, context } = event; let subject; let payload; function optimizePayload(key, value) { if ((key === 'tags' || key === 'attributes') && typeof value === 'object' && value && Object.keys(value).length === 0) { return undefined; } if (key === 'key' || key === 'typeOf' || key === 'entityType') { return undefined; } if (key === 'entityType') { return undefined; } if (key === 'fromObjKey' || key === 'fromObjTypeOf' || key === 'toObjKey' || key === 'toObjTypeOf') { return undefined; } return value; } if (entity.entityType === 'cnx') { subject = `eang.cnx.${tenant}.${organizationalUnit}.${user}.${entity.typeOf}.${entity.instanceOf}.${entity.key}.${event.eventType}.${entity.fromObjTypeOf}.${entity.fromObjInstanceOf}.${entity.fromObjKey}.${entity.toObjTypeOf}.${entity.toObjInstanceOf}.${entity.toObjKey}`; // TODO: handle content-type... default is JSON and does not require header payload = JSON.stringify({ cnx: entity, context }, optimizePayload); } else if (entity.entityType === 'obj') { subject = `eang.obj.${tenant}.${organizationalUnit}.${user}.${entity.typeOf}.${entity.instanceOf}.${entity.key}.${event.eventType}`; payload = JSON.stringify({ obj: entity, context }, optimizePayload); } else { throw new Error('Invalid object type'); } let msgId; if (event.eventType === 'update') { msgId = `${entity.id}.TODO HASH OF CHANGES.update`; } else { msgId = `${entity.id}.${event.eventType}`; } const opts = { msgID: msgId }; if (options) { Object.assign(opts, options); } return { subject, payload, opts }; } //# sourceMappingURL=NatsStreamingService.js.map