@flosportsinc/nestjs-google-pubsub-connector
Version:
A NestJS Connector for Google PubSub
295 lines • 11.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GooglePubSubTransport = void 0;
const common_1 = require("@nestjs/common");
const microservices_1 = require("@nestjs/microservices");
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const client_1 = require("../client");
const google_pubsub_context_1 = require("../ctx-host/google-pubsub.context");
const deserializers_1 = require("../deserializers");
const errors_1 = require("../errors");
const transport_error_exception_1 = require("../errors/transport-error.exception");
const interfaces_1 = require("../interfaces");
const basic_ack_strategy_1 = require("../strategies/basic-ack.strategy");
const basic_nack_strategy_1 = require("../strategies/basic-nack.strategy");
const basic_subscription_naming_strategy_1 = require("../strategies/basic-subscription-naming-strategy");
const basic_topic_naming_strategy_1 = require("../strategies/basic-topic-naming-strategy");
class GooglePubSubTransport extends microservices_1.Server {
/**
* Logger
*/
logger = new common_1.Logger('GooglePubSubTransport');
/**
* Convert the incoming message into a ReadPacket
*/
deserializer;
/**
* Google PubSub client handle
*/
googlePubSubClient;
/**
* This function will be used to determine subscription names when only a topic name is given
*/
subscriptionNamingStrategy;
/**
* Modifies topic names dynamically
*/
topicNamingStrategy;
/**
* This function is called after an incoming message is handled and allows control over when/how
* a message is either acked or nacked
*/
ackStrategy;
/**
* This function is called after an incoming message encounters an and allows control over
* when/how a message is either acked or nacked
*/
nackStrategy;
/**
* Whether to create subscriptions that do not already exist
*/
createSubscriptions;
/**
* Whether to automatically ack handled messages
*/
autoAck;
/**
* Whether to automatically nack rejected messages
*/
autoNack;
/**
* Subscription for all message listeners
*/
listenerSubscription = null;
iterators = null;
/**
* GooglePubSubSubscriptions keyed by pattern
*/
subscriptions = new Map();
/**
* Subscription Iterators for one-at-a-time processing keyed by pattern
*/
synchronousSubscriptions = new Map();
constructor(options) {
super();
this.googlePubSubClient = options?.client ?? new client_1.ClientGooglePubSub();
this.createSubscriptions = options?.createSubscriptions ?? false;
this.autoAck = options?.autoAck ?? true;
this.autoNack = options?.autoNack ?? false;
this.subscriptionNamingStrategy =
options?.subscriptionNamingStrategy ?? new basic_subscription_naming_strategy_1.BasicSubscriptionNamingStrategy();
this.topicNamingStrategy = options?.topicNamingStrategy ?? new basic_topic_naming_strategy_1.BasicTopicNamingStrategy();
this.ackStrategy = options?.ackStrategy ?? new basic_ack_strategy_1.BasicAckStrategy();
this.nackStrategy = options?.nackStrategy ?? new basic_nack_strategy_1.BasicNackStrategy();
this.deserializer = new deserializers_1.GooglePubSubMessageDeserializer();
}
listen(callback) {
void this.bindHandlers(callback);
}
/**
* Pull messages from the subscription iterators marked with {@link oneAtATime}
*/
startPullSyncMessages() {
if (this.iterators) {
void Promise.all(this.iterators.map((iterator) => this.handleMessageSync(iterator)));
}
}
/**
* Bind message handlers to subscription instances
* @param callback - The callback to be invoked when all handlers have been bound
*/
async bindHandlers(callback) {
// Set up our subscriptions from any decorated topics
await (0, rxjs_1.from)(this.messageHandlers)
.pipe((0, operators_1.mergeMap)(([pattern]) => this.getSubscriptionFromPattern(pattern)))
.toPromise();
// Group all of our event listeners into an array
const listeners = Array.from(this.subscriptions, this.subscribeMessageEvent);
this.listenerSubscription = (0, rxjs_1.merge)(...listeners)
.pipe((0, operators_1.map)(this.deserializeAndAddContext), (0, operators_1.mergeMap)(this.handleMessage))
.subscribe();
this.iterators = Array.from(this.synchronousSubscriptions, this.getSubscriptionIterator, this);
//for one at a time messages, pull events from their iterators and handle them
// synchronously
this.startPullSyncMessages();
callback();
}
/**
* Resolve subscriptions and create them if `createSubscription` is true
* @param pattern - The pattern from the \@GooglePubSubMessageHandler decorator
*/
async getSubscriptionFromPattern(pattern) {
const metadata = this.parsePattern(pattern);
const subscriptionName = this.getSubscriptionName(metadata, pattern);
const subscription = await this.getOrCreateSubscription(subscriptionName, metadata.topicName, metadata.createOptions, pattern);
if (subscription) {
this.logger.log(`Mapped {${subscription.name}} handler`);
if (metadata.oneAtATime) {
this.synchronousSubscriptions.set(pattern, subscription);
}
else {
this.subscriptions.set(pattern, subscription);
}
}
}
/**
* Parse a metadata pattern, throwing an exception if it cannot be parsed.
*
* @throws InvalidPatternMetadataException
* Thrown if the JSON pattern cannot be parsed.
*/
parsePattern = (pattern) => {
try {
return JSON.parse(pattern);
}
catch {
throw new errors_1.InvalidPatternMetadataException(pattern);
}
};
/**
* Get the name for the subscription based on the given metadata.
*
* @throws InvalidPatternMetadataException
* This exception is thrown if a subscription name cannot be generated.
*/
getSubscriptionName = (metadata, pattern) => {
const subscriptionNameDeps = GooglePubSubTransport.createSubscriptionNameDependencies(metadata, pattern);
return this.subscriptionNamingStrategy.generateSubscriptionName(subscriptionNameDeps);
};
/**
* Create the dependency object for producing a subscription name.
*
* @throws InvalidPatternMetadataException
* Thrown if `topicName` and `subscriptionName` are both `undefined`.
*/
static createSubscriptionNameDependencies(metadata, pattern) {
const topicName = metadata.topicName;
const subscriptionName = metadata.subscriptionName;
if (topicName && subscriptionName) {
return {
_tag: interfaces_1.NamingDependencyTag.TOPIC_AND_SUBSCRIPTION_NAMES,
topicName,
subscriptionName,
};
}
if (topicName) {
return {
_tag: interfaces_1.NamingDependencyTag.TOPIC_NAME_ONLY,
topicName,
};
}
if (subscriptionName) {
return {
_tag: interfaces_1.NamingDependencyTag.SUBSCRIPTION_NAME_ONLY,
subscriptionName,
};
}
throw new errors_1.InvalidPatternMetadataException(pattern);
}
/**
* Get the subscription from the pattern metadata.
*
* @remarks
* If PubSub Client cannot create the subscription, or if the application is not configured to
* create subscriptions, this method will return null when the subscription does not already
* exist.
*
* @throws InvalidPatternMetadataException
* Thrown if attempting to create a subscription, but a topic name is not provided.
*/
getOrCreateSubscription = async (subscriptionName, topicName, createOptions, pattern) => {
const subscriptionExists = (await this.googlePubSubClient
.subscriptionExists(subscriptionName)
.toPromise());
if (subscriptionExists)
return this.googlePubSubClient.getSubscription(subscriptionName);
if (!this.createSubscriptions)
return null;
if (!topicName) {
throw new errors_1.InvalidPatternMetadataException(pattern);
}
const _topicName = this.topicNamingStrategy.generateTopicName(topicName);
const topic = this.googlePubSubClient.getTopic(_topicName);
return (await this.googlePubSubClient
.createSubscription(subscriptionName, topic, createOptions)
.toPromise());
};
/**
* Subscribe to a Subscription and include pattern with each message
*/
subscribeMessageEvent = ([pattern, subscription]) => {
return this.googlePubSubClient
.listenForMessages(subscription)
.pipe((0, operators_1.map)((message) => [
pattern,
message,
]));
};
/**
*
* @param pattern - The subscription pattern
* @param subscription - The subscription
* @returns The pattern and an iterator for the subscription
*/
getSubscriptionIterator([pattern, subscription]) {
return [pattern, this.googlePubSubClient.getMessageIterator(subscription)];
}
/**
* Convert each message into a ReadPacket and include pattern and Context
*/
deserializeAndAddContext = ([pattern, message]) => {
return [
pattern,
this.deserializer.deserialize(message, { metadata: pattern }),
new google_pubsub_context_1.GooglePubSubContext([message, pattern, this.autoAck, this.autoNack]),
];
};
/**
* Pull messages from the iterator and pass them to the subscription handler
* @param pattern - The subscription name
* @param iterator - The message iterator
*/
async handleMessageSync([pattern, iterator]) {
for await (const [message] of iterator) {
if (message) {
const data = this.deserializeAndAddContext([pattern, message]);
await (0, rxjs_1.firstValueFrom)(this.handleMessage(data));
}
}
}
/**
* Pass ReadPacket to internal `handleEvent` method
*/
handleMessage = ([pattern, packet, ctx]) => {
return (0, rxjs_1.of)(this.getHandlerByPattern(pattern)).pipe((0, operators_1.mergeMap)((handler) => {
if (handler == null) {
throw new transport_error_exception_1.TransportError('Handler should never be nullish.', pattern, Array.from(this.messageHandlers.keys()));
}
return (0, rxjs_1.from)(handler(packet, ctx)).pipe((0, operators_1.mergeMap)((i) => this.transformToObservable(i)), (0, operators_1.mapTo)(null));
}), (0, operators_1.catchError)((err) => {
return (0, rxjs_1.of)(err);
}), (0, operators_1.map)((err) => {
const ack = packet.data.ack.bind(packet.data);
const nack = packet.data.nack.bind(packet.data);
if (err) {
void this.nackStrategy.nack(err, ack, nack, ctx);
}
else {
void this.ackStrategy.ack(ack, nack, ctx);
}
}));
};
/**
* This is called on transport close by the NestJS internals
*/
async close() {
this.listenerSubscription?.unsubscribe();
await (0, rxjs_1.firstValueFrom)(this.googlePubSubClient.close());
}
getHandlerByPattern(pattern) {
return this.messageHandlers.get(pattern) ?? null;
}
}
exports.GooglePubSubTransport = GooglePubSubTransport;
//# sourceMappingURL=server-google-pubsub.js.map