docs/docs/guides/developer-guide/events/index.mdx
Vendure emits events which can be subscribed to by plugins. These events are published by the EventBus and
likewise the EventBus is used to subscribe to events.
An event exists for virtually all significant actions which occur in the system, such as:
Product, Order, Customer) are created, updated or deletedOrder, Payment, Fulfillment or Refund changesA full list of the available events follows.
AccountRegistrationEventAccountVerifiedEventAdministratorEventAssetChannelEventAssetEventAttemptedLoginEventChangeChannelEventChannelEventCollectionEventCollectionModificationEventCountryEventCouponCodeEventCustomerAddressEventCustomerEventCustomerGroupChangeEventCustomerGroupEventFacetEventFacetValueEventFulfillmentEventFulfillmentStateTransitionEventGlobalSettingsEventHistoryEntryEventIdentifierChangeEventIdentifierChangeRequestEventInitializerEventLoginEventLogoutEventOrderEventOrderLineEventOrderPlacedEventOrderStateTransitionEventPasswordResetEventPasswordResetVerifiedEventPaymentMethodEventPaymentStateTransitionEventProductChannelEventProductEventProductOptionEventProductOptionGroupChangeEventProductOptionGroupEventProductVariantChannelEventProductVariantEventPromotionEventProvinceEventRefundStateTransitionEventRoleChangeEventRoleEventSearchEventSellerEventShippingMethodEventStockMovementEventTaxCategoryEventTaxRateEventTaxRateModificationEventZoneEventZoneMembersEventTo subscribe to an event, use the EventBus's .ofType() method. It is typical to set up subscriptions in the onModuleInit() or onApplicationBootstrap()
lifecycle hooks of a plugin or service (see NestJS Lifecycle events).
Here's an example where we subscribe to the ProductEvent and use it to trigger a rebuild of a static storefront:
import { OnModuleInit } from '@nestjs/common';
import { EventBus, ProductEvent, PluginCommonModule, VendurePlugin } from '@vendure/core';
import { StorefrontBuildService } from './services/storefront-build.service';
@VendurePlugin({
imports: [PluginCommonModule],
})
export class StorefrontBuildPlugin implements OnModuleInit {
constructor(
private eventBus: EventBus, // [!code highlight]
private storefrontBuildService: StorefrontBuildService,
) {}
onModuleInit() {
this.eventBus.ofType(ProductEvent).subscribe(event => { // [!code highlight]
this.storefrontBuildService.triggerBuild(); // [!code highlight]
}); // [!code highlight]
}
}
:::info
The EventBus.ofType() and related EventBus.filter() methods return an RxJS Observable.
This means that you can use any of the RxJS operators to transform the stream of events.
For example, to debounce the stream of events, you could do this:
import { debounceTime } from 'rxjs/operators'; // [!code highlight]
// ...
this.eventBus
.ofType(ProductEvent)
.pipe(debounceTime(1000)) // [!code highlight]
.subscribe(event => {
this.storefrontBuildService.triggerBuild();
});
:::
Using the .ofType() method allows us to subscribe to a single event type. If we want to subscribe to multiple event types, we can use the .filter() method instead:
import { Injectable, OnModuleInit } from '@nestjs/common';
import {
EventBus,
PluginCommonModule,
VendurePlugin,
ProductEvent,
ProductVariantEvent,
} from '@vendure/core';
@VendurePlugin({
imports: [PluginCommonModule],
})
export class MyPluginPlugin implements OnModuleInit {
constructor(private eventBus: EventBus) {}
onModuleInit() {
this.eventBus
.filter(event => event instanceof ProductEvent || event instanceof ProductVariantEvent) // [!code highlight]
.subscribe(event => {
// the event will be a ProductEvent or ProductVariantEvent
});
}
}
You can publish events using the EventBus.publish() method. This is useful if you want to trigger an event from within a plugin or service.
For example, to publish a ProductEvent:
import { Injectable } from '@nestjs/common';
import { EventBus, ProductEvent, RequestContext, Product } from '@vendure/core';
@Injectable()
export class MyPluginService {
constructor(private eventBus: EventBus) {}
async doSomethingWithProduct(ctx: RequestContext, product: Product) {
// ... do something
await this.eventBus.publish(new ProductEvent(ctx, product, 'updated')); // [!code highlight]
}
}
You can create your own custom events by extending the VendureEvent class. For example, to create a custom event which is triggered when a customer submits a review, you could do this:
import { ID, RequestContext, VendureEvent } from '@vendure/core';
import { ProductReviewInput } from '../types';
/**
* @description
* This event is fired whenever a ProductReview is submitted.
*/
export class ReviewSubmittedEvent extends VendureEvent {
constructor(
public ctx: RequestContext,
public input: ProductReviewInput,
) {
super();
}
}
The event would then be published from your plugin's ProductReviewService:
import { Injectable } from '@nestjs/common';
import { EventBus, ProductReviewService, RequestContext } from '@vendure/core';
import { ReviewSubmittedEvent } from '../events/review-submitted.event';
import { ProductReviewInput } from '../types';
@Injectable()
export class ProductReviewService {
constructor(
private eventBus: EventBus,
private productReviewService: ProductReviewService,
) {}
async submitReview(ctx: RequestContext, input: ProductReviewInput) {
this.eventBus.publish(new ReviewSubmittedEvent(ctx, input)); // [!code highlight]
// handle creation of the new review
// ...
}
}
There is a special event class VendureEntityEvent for events relating to the creation, update or deletion of entities. Let's say you have a custom entity (see defining a database entity) BlogPost and you want to trigger an event whenever a new BlogPost is created, updated or deleted:
import { ID, RequestContext, VendureEntityEvent } from '@vendure/core';
import { BlogPost } from '../entities/blog-post.entity';
import { CreateBlogPostInput, UpdateBlogPostInput } from '../types';
type BlogPostInputTypes = CreateBlogPostInput | UpdateBlogPostInput | ID | ID[];
/**
* This event is fired whenever a BlogPost is added, updated
* or deleted.
*/
export class BlogPostEvent extends VendureEntityEvent<BlogPost, BlogPostInputTypes> {
constructor(
ctx: RequestContext,
entity: BlogPost,
type: 'created' | 'updated' | 'deleted',
input?: BlogPostInputTypes,
) {
super(entity, type, ctx, input);
}
}
Using this event, you can subscribe to all BlogPost events, and for instance filter for only the created events:
import { Injectable, OnModuleInit } from '@nestjs/common';
import { EventBus, PluginCommonModule, VendurePlugin } from '@vendure/core';
import { filter } from 'rxjs/operators';
import { BlogPostEvent } from './events/blog-post-event';
@VendurePlugin({
imports: [PluginCommonModule],
// ...
})
export class BlogPlugin implements OnModuleInit {
constructor(private eventBus: EventBus) {}
onModuleInit() {
this.eventBus
.ofType(BlogPostEvent) // [!code highlight]
.pipe(filter(event => event.type === 'created')) // [!code highlight]
.subscribe(event => { // [!code highlight]
const blogPost = event.entity; // [!code highlight]
// do something with the newly created BlogPost // [!code highlight]
}); // [!code highlight]
}
}
:::note The following section is an advanced topic.
The API described in this section was added in Vendure v2.2.0. :::
When using the .ofType().subscribe() pattern, the event handler is non-blocking. This means that the code that publishes
the event (the "publishing code") will have no knowledge of any subscribers, and in fact any subscribers will be executed after the code that
published the event has completed (technically, any ongoing database transactions are completed before the event gets
emitted to the subscribers). This follows the typical Observer pattern and is a good fit for most use-cases.
However, there may be certain situations in which you want the event handler to cause the publishing code to block until the event handler has completed. This is done by using a "blocking event handler", which does not follow the Observer pattern, but rather it behaves more like a synchronous function call occurring within the publishing code.
You may want to use a blocking event handler in the following situations:
In these cases, you can use the EventBus.registerBlockingEventHandler() method:
import { Injectable, OnModuleInit } from '@nestjs/common';
import { EventBus, PluginCommonModule, VendurePlugin, CustomerEvent } from '@vendure/core';
import { CustomerSyncService } from './services/customer-sync.service';
@VendurePlugin({
imports: [PluginCommonModule],
})
export class MyPluginPlugin implements OnModuleInit {
constructor(
private eventBus: EventBus,
private customerSyncService: CustomerSyncService,
) {}
onModuleInit() {
this.eventBus.registerBlockingEventHandler({ // [!code highlight]
event: CustomerEvent, // [!code highlight]
id: 'sync-customer-details-handler', // [!code highlight]
handler: async event => { // [!code highlight]
// This hypothetical service method would do nothing // [!code highlight]
// more than adding a new job to the job queue. This gives us // [!code highlight]
// the guarantee that the job is added before the publishing // [!code highlight]
// code is able to continue, while minimizing the time spent // [!code highlight]
// in the event handler. // [!code highlight]
await this.customerSyncService.triggerCustomerSyncJob(event); // [!code highlight]
}, // [!code highlight]
}); // [!code highlight]
}
}
Key differences between event subscribers and blocking event handlers:
| Aspect | Event subscribers | Blocking event handlers |
|---|---|---|
| Execution | Executed after publishing code completes | Execute during the publishing code |
| Error handling | Errors do not affect publishing code | Errors propagated to publishing code |
| Transactions | Guaranteed to execute only after the publishing code transaction has completed | Executed within the transaction of the publishing code |
| Performance | Non-blocking: subscriber function performance has no effect on publishing code | Blocking: handler function will block execution of publishing code. Handler must be fast. |
Since blocking event handlers execute within the same transaction as the publishing code, it is important to ensure that they are fast.
If a single handler takes longer than 100ms to execute, a warning will be logged. Ideally they should be much faster than that - you can
set your Logger's logLevel to LogLevel.DEBUG to see the execution time of each handler.
If multiple handlers are registered for a single event, they will be executed sequentially, so the publishing code will be blocked until all handlers have completed.
If you register multiple handlers for the same event, they will be executed in the order in which they were registered.
If you need more control over this order, i.e. to guarantee that a particular handler will execute before another, you can use
the before or after options:
// In one part of your code base
this.eventBus.registerBlockingEventHandler({
type: CustomerEvent,
id: 'sync-customer-details-handler',
handler: async event => {
// ...
},
});
// In another part of your code base
this.eventBus.registerBlockingEventHandler({
type: CustomerEvent,
id: 'check-customer-details-handler',
handler: async event => {
// ...
},
before: 'sync-customer-details-handler', // [!code highlight]
});