UNPKG

@vendure/core

Version:

A modern, headless ecommerce framework

278 lines 12.7 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.EventBus = void 0; const common_1 = require("@nestjs/common"); const shared_utils_1 = require("@vendure/common/lib/shared-utils"); const rxjs_1 = require("rxjs"); const operators_1 = require("rxjs/operators"); const request_context_1 = require("../api/common/request-context"); const constants_1 = require("../common/constants"); const instrument_decorator_1 = require("../common/instrument-decorator"); const vendure_logger_1 = require("../config/logger/vendure-logger"); const transaction_subscriber_1 = require("../connection/transaction-subscriber"); /** * @description * The EventBus is used to globally publish events which can then be subscribed to. * * Events are published whenever certain actions take place within the Vendure server, for example: * * * when a Product is updated ({@link ProductEvent}) * * when an Order transitions state ({@link OrderStateTransitionEvent}) * * when a Customer registers a new account ({@link AccountRegistrationEvent}) * * Using the EventBus it is possible to subscribe to an take action when these events occur. * This is done with the `.ofType()` method, which takes an event type and returns an rxjs observable * stream of events: * * @example * ```ts * import { OnApplicationBootstrap } from '\@nestjs/common'; * import { EventBus, PluginCommonModule, VendurePlugin } from '\@vendure/core'; * import { filter } from 'rxjs/operators'; * * \@VendurePlugin({ * imports: [PluginCommonModule] * }) * export class MyPlugin implements OnApplicationBootstrap { * * constructor(private eventBus: EventBus) {} * * async onApplicationBootstrap() { * * this.eventBus * .ofType(OrderStateTransitionEvent) * .pipe( * filter(event => event.toState === 'PaymentSettled'), * ) * .subscribe((event) => { * // do some action when this event fires * }); * } * } * ``` * * @docsCategory events * */ let EventBus = class EventBus { constructor(transactionSubscriber) { this.transactionSubscriber = transactionSubscriber; this.eventStream = new rxjs_1.Subject(); this.destroy$ = new rxjs_1.Subject(); this.blockingEventHandlers = new Map(); } /** * @description * Publish an event which any subscribers can react to. * * @example * ```ts * await eventBus.publish(new SomeEvent()); * ``` */ async publish(event) { this.eventStream.next(event); await this.executeBlockingEventHandlers(event); } /** * @description * Returns an RxJS Observable stream of events of the given type. * If the event contains a {@link RequestContext} object, the subscriber * will only get called after any active database transactions are complete. * * This means that the subscriber function can safely access all updated * data related to the event. */ ofType(type) { return this.eventStream.asObservable().pipe((0, operators_1.takeUntil)(this.destroy$), (0, operators_1.filter)(e => e.constructor === type), (0, operators_1.mergeMap)(event => this.awaitActiveTransactions(event)), (0, operators_1.filter)(shared_utils_1.notNullOrUndefined)); } /** * @description * Returns an RxJS Observable stream of events filtered by a custom predicate. * If the event contains a {@link RequestContext} object, the subscriber * will only get called after any active database transactions are complete. * * This means that the subscriber function can safely access all updated * data related to the event. */ filter(predicate) { return this.eventStream.asObservable().pipe((0, operators_1.takeUntil)(this.destroy$), (0, operators_1.filter)(e => predicate(e)), (0, operators_1.mergeMap)(event => this.awaitActiveTransactions(event)), (0, operators_1.filter)(shared_utils_1.notNullOrUndefined)); } /** * @description * Register an event handler function which will be executed when an event of the given type is published, * and will block execution of the code which published the event until the handler has completed. * * This is useful when you need assurance that the event handler has successfully completed, and you want * the triggering code to fail if the handler fails. * * ::: warning * This API should be used with caution, as errors or performance issues in the handler can cause the * associated operation to be slow or fail entirely. For this reason, any handler which takes longer than * 100ms to execute will log a warning. Any non-trivial task to be performed in a blocking event handler * should be offloaded to a background job using the {@link JobQueueService}. * * Also, be aware that the handler will be executed in the _same database transaction_ as the code which published * the event (as long as you pass the `ctx` object from the event to any TransactionalConnection calls). * ::: * * @example * ```ts * eventBus.registerBlockingEventHandler({ * event: OrderStateTransitionEvent, * id: 'my-order-state-transition-handler', * handler: async (event) => { * // perform some synchronous task * } * }); * ``` * * @since 2.2.0 */ registerBlockingEventHandler(handlerOptions) { const events = Array.isArray(handlerOptions.event) ? handlerOptions.event : [handlerOptions.event]; for (const event of events) { let handlers = this.blockingEventHandlers.get(event); const handlerWithIdAlreadyExists = handlers === null || handlers === void 0 ? void 0 : handlers.some(h => h.id === handlerOptions.id); if (handlerWithIdAlreadyExists) { throw new Error(`A handler with the id "${handlerOptions.id}" is already registered for the event ${event.name}`); } if (handlers) { handlers.push(handlerOptions); } else { handlers = [handlerOptions]; } const orderedHandlers = this.orderEventHandlers(handlers); this.blockingEventHandlers.set(event, orderedHandlers); } } /** @internal */ onModuleDestroy() { this.destroy$.next(); } async executeBlockingEventHandlers(event) { const blockingHandlers = this.blockingEventHandlers.get(event.constructor); for (const options of blockingHandlers || []) { const timeStart = new Date().getTime(); await options.handler(event); const timeEnd = new Date().getTime(); const timeTaken = timeEnd - timeStart; vendure_logger_1.Logger.debug(`Blocking event handler ${options.id} took ${timeTaken}ms`); if (timeTaken > 100) { vendure_logger_1.Logger.warn([ `Blocking event handler ${options.id} took ${timeTaken}ms`, `Consider optimizing the handler by moving the logic to a background job or using a more efficient algorithm.`, ].join('\n')); } } } orderEventHandlers(handlers) { let orderedHandlers = []; const handlerMap = new Map(); // Create a map of handlers by ID for efficient lookup for (const handler of handlers) { handlerMap.set(handler.id, handler); } // Helper function to recursively add handlers in correct order const addHandler = (handler) => { // If the handler is already in the ordered list, skip it if (orderedHandlers.includes(handler)) { return; } // If an "after" handler is specified, add it recursively if (handler.after) { const afterHandler = handlerMap.get(handler.after); if (afterHandler) { if (afterHandler.after === handler.id) { throw new Error(`Circular dependency detected between event handlers ${handler.id} and ${afterHandler.id}`); } orderedHandlers = orderedHandlers.filter(h => h.id !== afterHandler.id); addHandler(afterHandler); } } // Add the current handler orderedHandlers.push(handler); // If a "before" handler is specified, add it recursively if (handler.before) { const beforeHandler = handlerMap.get(handler.before); if (beforeHandler) { if (beforeHandler.before === handler.id) { throw new Error(`Circular dependency detected between event handlers ${handler.id} and ${beforeHandler.id}`); } orderedHandlers = orderedHandlers.filter(h => h.id !== beforeHandler.id); addHandler(beforeHandler); } } }; // Start adding handlers from the original list for (const handler of handlers) { addHandler(handler); } return orderedHandlers; } /** * If the Event includes a RequestContext property, we need to check for any active transaction * associated with it, and if there is one, we await that transaction to either commit or rollback * before publishing the event. * * The reason for this is that if the transaction is still active when event subscribers execute, * this can cause a couple of issues: * * 1. If the transaction hasn't completed by the time the subscriber runs, the new data inside * the transaction will not be available to the subscriber. * 2. If the subscriber gets a reference to the EntityManager which has an active transaction, * and then the transaction completes, and then the subscriber attempts a DB operation using that * EntityManager, a fatal QueryRunnerAlreadyReleasedError will be thrown. * * For more context on these two issues, see: * * * https://github.com/vendure-ecommerce/vendure/issues/520 * * https://github.com/vendure-ecommerce/vendure/issues/1107 */ async awaitActiveTransactions(event) { const entry = Object.entries(event).find(([_, value]) => value instanceof request_context_1.RequestContext); if (!entry) { return event; } const [key, ctx] = entry; const transactionManager = ctx[constants_1.TRANSACTION_MANAGER_KEY]; if (!(transactionManager === null || transactionManager === void 0 ? void 0 : transactionManager.queryRunner)) { return event; } try { await this.transactionSubscriber.awaitCommit(transactionManager.queryRunner); // Copy context and remove transaction manager // This will prevent queries to released query runner const newContext = ctx.copy(); delete newContext[constants_1.TRANSACTION_MANAGER_KEY]; // Reassign new context event[key] = newContext; return event; } catch (e) { if (e instanceof transaction_subscriber_1.TransactionSubscriberError) { // Expected commit, but rollback or something else happened. // This is still reliable behavior, return undefined // as event should not be exposed from this transaction return; } throw e; } } }; exports.EventBus = EventBus; exports.EventBus = EventBus = __decorate([ (0, common_1.Injectable)(), (0, instrument_decorator_1.Instrument)(), __metadata("design:paramtypes", [transaction_subscriber_1.TransactionSubscriber]) ], EventBus); //# sourceMappingURL=event-bus.js.map