@vendure/core
Version:
A modern, headless ecommerce framework
278 lines • 12.7 kB
JavaScript
;
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 and 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/vendurehq/vendure/issues/520
* * https://github.com/vendurehq/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