Skip to main content

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 or Refund changes

A full list of the available events follows.

Event types

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:

src/plugins/storefront-build/storefront-build.plugin.ts
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();
});
}
}
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';

// ...

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:

src/plugins/my-plugin/my-plugin.plugin.ts
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:

src/plugins/my-plugin/services/my-plugin.service.ts
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:

src/plugins/reviews/events/review-submitted.event.ts
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:

src/plugins/reviews/services/product-review.service.ts
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:

src/plugins/blog/events/blog-post-event.ts
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:

src/plugins/blog/blog-plugin.ts
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

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:

  • 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:

src/plugins/my-plugin/my-plugin.plugin.ts
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:

AspectEvent subscribersBlocking event handlers
ExecutionExecuted after publishing code completesExecute during the publishing code
Error handlingErrors do not affect publishing codeErrors propagated to publishing code
TransactionsGuaranteed to execute only after the publishing code transaction has completedExecuted within the transaction of the publishing code
PerformanceNon-blocking: subscriber function performance has no effect on publishing codeBlocking: 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',
});