UNPKG

@flosportsinc/nestjs-google-pubsub-connector

Version:
295 lines 11.9 kB
"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