@nebulae/backend-node-tools
Version:
Tools collection for NebulaE Microservices Node Backends
278 lines (255 loc) • 9.89 kB
JavaScript
'use strict'
const { BehaviorSubject, Observable, from, of } = require('rxjs');
const {
switchMap,
timeout,
filter,
map,
first,
mapTo,
mergeMap,
reduce
} = require('rxjs/operators');
const { ConsoleLogger } = require('../log');
const uuidv4 = require('uuid/v4');
class PubSubBroker {
constructor({ replyTimeOut, topicSubscriptionSuffix = "default-suffix" }) {
this.replyTimeOut = replyTimeOut;
/**
* Rx Subject for every message reply
*/
this.incomingMessages$ = new BehaviorSubject();
this.senderId = uuidv4();
/**
* Map of verified topics
*/
this.verifiedTopics = {};
this.listeningTopics = {};
this.topicSubscriptionSuffix = topicSubscriptionSuffix;
const { PubSub } = require('@google-cloud/pubsub');
this.pubsubClient = new PubSub({});
}
/**
* Sends a Message to the given topic
* @param {string} topic topic to publish
* @param {string} type message type
* @param {Object} message payload
* @param {Object} ops {correlationId}
*/
send$(topic, type, payload, ops = {}) {
return this.getTopic$(topic).pipe(
switchMap(t => this.publish$(t, type, payload, ops))
);
}
/**
* Sends a Message to the given topic and wait for a reply
* @param {string} topic send topic
* @param {string} responseTopic response topic
* @param {string} type message(payload) type
* @param {Object} message payload
* @param {number} timeout wait timeout millis
* @param {boolean} ignoreSelfEvents ignore messages comming from this clien
* @param {Object} ops {correlationId}
*
* Returns an Observable that resolves the message response
*/
sendAndGetReply$(topic, responseTopic, type, payload, timeout = this.replyTimeout, ignoreSelfEvents = true, ops = {}) {
return this.forward$(topic, type, payload, ops).pipe(
switchMap((messageId) => this.getMessageReply$(responseTopic, messageId, timeout, ignoreSelfEvents))
);
}
/**
* Returns an observable that waits for the message response or throws an error if timeout is exceded
* @param {string} topic response topic
* @param {string} correlationId
* @param {number} replyTimeout
*/
getMessageReply$(topic, correlationId, replyTimeout = this.replyTimeout, ignoreSelfEvents = true) {
return this.configMessageListener$([topic]).pipe(
switchMap(() =>
this.incomingMessages$.pipe(
filter(msg => msg),
filter(msg => !ignoreSelfEvents || msg.attributes.senderId !== this.senderId),
filter(msg => msg && msg.correlationId === correlationId),
map(msg => msg.data),
timeout(replyTimeout),
first()
)
)
);
}
/**
* Returns an Observable that will emit any message
* @param {string[] ?} topics topic to listen
* @param {string[] ?} types message types to listen
* @param {boolean ?} ignoreSelfEvents
*/
getMessageListener$(topics = [], types = [], ignoreSelfEvents = true) {
return this.configMessageListener$(topics).pipe(
switchMap(() =>
this.incomingMessages$.pipe(
filter(msg => msg),
filter(msg => !ignoreSelfEvents || msg.attributes.senderId !== this.senderId),
filter(msg => topics.length === 0 || topics.indexOf(msg.topic) > -1),
filter(msg => types.length === 0 || types.indexOf(msg.type) > -1)
)
)
);
}
/**
* Config the broker to listen to several topics
* Returns an observable that resolves to a stream of subscribed topics
* @param {Array} topics topics to listen
*/
configMessageListener$(topics) {
return Observable.create((observer) => {
from(topics).pipe(
filter(topicName => Object.keys(this.listeningTopics).indexOf(topicName) === -1),
mergeMap(topicName => {
const subscriptionName = `${topicName}_${this.topicSubscriptionSuffix}`;
return this.getSubscription$(topicName, subscriptionName).pipe(
map(subsription => {
return { topicName, subsription, subscriptionName };
})
)
})
).subscribe(
({ topicName, subsription, subscriptionName }) => {
this.listeningTopics[topicName] = subscriptionName;
subsription.on(`message`, message => {
ConsoleLogger.d(`PubSubBroker: Received message ${message.id}:`);
this.incomingMessages$.next(
{
id: message.id,
data: JSON.parse(message.data),
attributes: message.attributes,
publishTime: message.publishTime,
correlationId: message.attributes.correlationId,
topic: topicName,
type: message.attributes.type,
}
);
message.ack();
});
observer.next(topicName);
},
(err) => {
console.error('Failed to obtain sales-gatewayReplies subscription', err);
observer.error(err);
},
() => {
observer.complete();
}
)
}).pipe(
reduce((acc, topic) => {
acc.push(topic);
return acc;
}, [])
);
}
/**
* Gets an observable that resolves to the topic object
* @param {string} topicName
*/
getTopic$(topicName) {
//Tries to get a cached topic
const cachedTopic = this.verifiedTopics[topicName];
if (!cachedTopic) {
//if not cached, then tries to know if the topic exists
const topic = this.pubsubClient.topic(topicName);
return from(topic.exists()).pipe(
map(data => data[0]),
switchMap(exists => {
if (exists) {
//if it does exists, then store it on the cache and return it
this.verifiedTopics[topicName] = topic;
ConsoleLogger.i(`PubSubBroker.getTopic$: Topic ${topicName} already existed and has been set into the cache`);
return of(topic);
} else {
//if it does NOT exists, then create it, store it in the cache and return it
return this.createTopic$(topicName);
}
})
);
}
//return cached topic
return of(cachedTopic);
}
/**
* Creates a Topic and return an observable that resolves to the created topic
* @param {string} topicName
*/
createTopic$(topicName) {
return from(this.pubsubClient.createTopic(topicName)).pipe(
switchMap(data => {
this.verifiedTopics[topicName] = this.pubsubClient.topic(topicName);
return of(this.verifiedTopics[topicName]);
})
);
}
/**
* Publish data throught a topic
* Returns an Observable that resolves to the sent message ID
* @param {Topic} topic
* @param {string} type message(data) type
* @param {Object} data
* @param {Object} ops {correlationId}
*/
publish$(topic, type, data, { correlationId = "" } = {}) {
if (!data || !topic) {
ConsoleLogger.i(`PubSubBroker.publish: databuffer is null: t:${topic}, tt=${type}, correlationId=${correlationId}, d=${data}`);
return of(`PubSubBroker.publish: databuffer is null: t:${topic}, d=${data}`);
}
const dataBuffer = Buffer.from(JSON.stringify(data));
return from(
topic.publish(
dataBuffer,
{
type,
senderId: this.senderId,
correlationId
}))
;
}
/**
* Returns an Observable that resolves to the subscription
* @param {string} topicName
* @param {string} subscriptionName
*/
getSubscription$(topicName, subscriptionName) {
return this.getTopic$(topicName).pipe(
switchMap(topic => from(
topic.subscription(subscriptionName)
.get({ autoCreate: true }))
),
map(results => results[0])
);
}
/**
* Stops broker
*/
disconnectBroker() {
return Observable.create((observer) => {
from(Object.entries(listeningTopics)).pipe(
mergeMap(([topicName, subscriptionName]) => this.getSubscription$(topicName, subscriptionName))
).subscribe(
(subscription) => {
subscription.removeListener(`message`);
observer.next(`Removed listener for ${subscription}`);
},
(error) => {
console.error(`Error disconnecting Broker`, error);
observer.error(error);
},
() => {
observer.complete();
}
);
});
}
}
/**
* @returns {PubSubBroker}
*/
module.exports = PubSubBroker;