Events
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:
- When entities (e.g.
Product
,Order
,Customer
) are created, updated or deleted - When a user registers an account
- When a user logs in or out
- When the state of an
Order
,Payment
,Fulfillment
orRefund
changes
A full list of the available events follows.
Event types
AccountRegistrationEvent
AccountVerifiedEvent
AdministratorEvent
AssetChannelEvent
AssetEvent
AttemptedLoginEvent
ChangeChannelEvent
ChannelEvent
CollectionEvent
CollectionModificationEvent
CountryEvent
CouponCodeEvent
CustomerAddressEvent
CustomerEvent
CustomerGroupChangeEvent
CustomerGroupEvent
FacetEvent
FacetValueEvent
FulfillmentEvent
FulfillmentStateTransitionEvent
GlobalSettingsEvent
HistoryEntryEvent
IdentifierChangeEvent
IdentifierChangeRequestEvent
InitializerEvent
LoginEvent
LogoutEvent
OrderEvent
OrderLineEvent
OrderPlacedEvent
OrderStateTransitionEvent
PasswordResetEvent
PasswordResetVerifiedEvent
PaymentMethodEvent
PaymentStateTransitionEvent
ProductChannelEvent
ProductEvent
ProductOptionEvent
ProductOptionGroupChangeEvent
ProductOptionGroupEvent
ProductVariantChannelEvent
ProductVariantEvent
PromotionEvent
ProvinceEvent
RefundStateTransitionEvent
RoleChangeEvent
RoleEvent
SearchEvent
SellerEvent
ShippingMethodEvent
StockMovementEvent
TaxCategoryEvent
TaxRateEvent
TaxRateModificationEvent
ZoneEvent
ZoneMembersEvent
Subscribing to events
To 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,
private storefrontBuildService: StorefrontBuildService,
) {}
onModuleInit() {
this.eventBus.ofType(ProductEvent).subscribe(event => {
this.storefrontBuildService.triggerBuild();
});
}
}
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';
// ...
this.eventBus
.ofType(ProductEvent)
.pipe(debounceTime(1000))
.subscribe(event => {
this.storefrontBuildService.triggerBuild();
});
Subscribing to multiple event types
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)
.subscribe(event => {
// the event will be a ProductEvent or ProductVariantEvent
});
}
}
Publishing events
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'));
}
}
Creating custom events
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));
// handle creation of the new review
// ...
}
}
Entity events
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)
.pipe(filter(event => event.type === 'created'))
.subscribe(event => {
const blogPost = event.entity;
// do something with the newly created BlogPost
});
}
}
Blocking event handlers
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:
- The event handler is so critical that you need to ensure that it has completed before the publishing code continues. For example, if the event handler must manipulate some financial records.
- Errors in the event handler code should cause the publishing code to fail (and any database transaction to be rolled back).
- You want to guard against the edge case that a server instance gets shut down (due to e.g. a fatal error or an auto-scaling event) before event subscribers have been invoked.
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({
event: CustomerEvent,
id: 'sync-customer-details-handler',
handler: async event => {
// This hypothetical service method would do nothing
// more than adding a new job to the job queue. This gives us
// the guarantee that the job is added before the publishing
// code is able to continue, while minimizing the time spent
// in the event handler.
await this.customerSyncService.triggerCustomerSyncJob(event);
},
});
}
}
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. |
Performance considerations
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.
Order of execution
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',
});