Kaynağa Gözat

docs(core): Add docs on blocking event handlers

Relates to #2735
Michael Bromley 1 yıl önce
ebeveyn
işleme
d75c17b627

+ 193 - 78
docs/docs/guides/developer-guide/events/index.mdx

@@ -1,5 +1,5 @@
 ---
-title: "Events"
+title: 'Events'
 ---
 
 Vendure emits events which can be subscribed to by plugins. These events are published by the [EventBus](/reference/typescript-api/events/event-bus/) and
@@ -7,10 +7,10 @@ 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
+-   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.
 
@@ -19,66 +19,66 @@ A full list of the available events follows.
 <div class="row">
 <div class="col col--6">
 
- - [`AccountRegistrationEvent`](/reference/typescript-api/events/event-types#accountregistrationevent)
- - [`AccountVerifiedEvent`](/reference/typescript-api/events/event-types#accountverifiedevent)
- - [`AdministratorEvent`](/reference/typescript-api/events/event-types#administratorevent)
- - [`AssetChannelEvent`](/reference/typescript-api/events/event-types#assetchannelevent)
- - [`AssetEvent`](/reference/typescript-api/events/event-types#assetevent)
- - [`AttemptedLoginEvent`](/reference/typescript-api/events/event-types#attemptedloginevent)
- - [`ChangeChannelEvent`](/reference/typescript-api/events/event-types#changechannelevent)
- - [`ChannelEvent`](/reference/typescript-api/events/event-types#channelevent)
- - [`CollectionEvent`](/reference/typescript-api/events/event-types#collectionevent)
- - [`CollectionModificationEvent`](/reference/typescript-api/events/event-types#collectionmodificationevent)
- - [`CountryEvent`](/reference/typescript-api/events/event-types#countryevent)
- - [`CouponCodeEvent`](/reference/typescript-api/events/event-types#couponcodeevent)
- - [`CustomerAddressEvent`](/reference/typescript-api/events/event-types#customeraddressevent)
- - [`CustomerEvent`](/reference/typescript-api/events/event-types#customerevent)
- - [`CustomerGroupChangeEvent`](/reference/typescript-api/events/event-types#customergroupchangeevent)
- - [`CustomerGroupEvent`](/reference/typescript-api/events/event-types#customergroupevent)
- - [`FacetEvent`](/reference/typescript-api/events/event-types#facetevent)
- - [`FacetValueEvent`](/reference/typescript-api/events/event-types#facetvalueevent)
- - [`FulfillmentEvent`](/reference/typescript-api/events/event-types#fulfillmentevent)
- - [`FulfillmentStateTransitionEvent`](/reference/typescript-api/events/event-types#fulfillmentstatetransitionevent)
- - [`GlobalSettingsEvent`](/reference/typescript-api/events/event-types#globalsettingsevent)
- - [`HistoryEntryEvent`](/reference/typescript-api/events/event-types#historyentryevent)
- - [`IdentifierChangeEvent`](/reference/typescript-api/events/event-types#identifierchangeevent)
- - [`IdentifierChangeRequestEvent`](/reference/typescript-api/events/event-types#identifierchangerequestevent)
- - [`InitializerEvent`](/reference/typescript-api/events/event-types#initializerevent)
- - [`LoginEvent`](/reference/typescript-api/events/event-types#loginevent)
- - [`LogoutEvent`](/reference/typescript-api/events/event-types#logoutevent)
- - [`OrderEvent`](/reference/typescript-api/events/event-types#orderevent)
+-   [`AccountRegistrationEvent`](/reference/typescript-api/events/event-types#accountregistrationevent)
+-   [`AccountVerifiedEvent`](/reference/typescript-api/events/event-types#accountverifiedevent)
+-   [`AdministratorEvent`](/reference/typescript-api/events/event-types#administratorevent)
+-   [`AssetChannelEvent`](/reference/typescript-api/events/event-types#assetchannelevent)
+-   [`AssetEvent`](/reference/typescript-api/events/event-types#assetevent)
+-   [`AttemptedLoginEvent`](/reference/typescript-api/events/event-types#attemptedloginevent)
+-   [`ChangeChannelEvent`](/reference/typescript-api/events/event-types#changechannelevent)
+-   [`ChannelEvent`](/reference/typescript-api/events/event-types#channelevent)
+-   [`CollectionEvent`](/reference/typescript-api/events/event-types#collectionevent)
+-   [`CollectionModificationEvent`](/reference/typescript-api/events/event-types#collectionmodificationevent)
+-   [`CountryEvent`](/reference/typescript-api/events/event-types#countryevent)
+-   [`CouponCodeEvent`](/reference/typescript-api/events/event-types#couponcodeevent)
+-   [`CustomerAddressEvent`](/reference/typescript-api/events/event-types#customeraddressevent)
+-   [`CustomerEvent`](/reference/typescript-api/events/event-types#customerevent)
+-   [`CustomerGroupChangeEvent`](/reference/typescript-api/events/event-types#customergroupchangeevent)
+-   [`CustomerGroupEvent`](/reference/typescript-api/events/event-types#customergroupevent)
+-   [`FacetEvent`](/reference/typescript-api/events/event-types#facetevent)
+-   [`FacetValueEvent`](/reference/typescript-api/events/event-types#facetvalueevent)
+-   [`FulfillmentEvent`](/reference/typescript-api/events/event-types#fulfillmentevent)
+-   [`FulfillmentStateTransitionEvent`](/reference/typescript-api/events/event-types#fulfillmentstatetransitionevent)
+-   [`GlobalSettingsEvent`](/reference/typescript-api/events/event-types#globalsettingsevent)
+-   [`HistoryEntryEvent`](/reference/typescript-api/events/event-types#historyentryevent)
+-   [`IdentifierChangeEvent`](/reference/typescript-api/events/event-types#identifierchangeevent)
+-   [`IdentifierChangeRequestEvent`](/reference/typescript-api/events/event-types#identifierchangerequestevent)
+-   [`InitializerEvent`](/reference/typescript-api/events/event-types#initializerevent)
+-   [`LoginEvent`](/reference/typescript-api/events/event-types#loginevent)
+-   [`LogoutEvent`](/reference/typescript-api/events/event-types#logoutevent)
+-   [`OrderEvent`](/reference/typescript-api/events/event-types#orderevent)
 
 </div>
 <div class="col col--6">
 
- - [`OrderLineEvent`](/reference/typescript-api/events/event-types#orderlineevent)
- - [`OrderPlacedEvent`](/reference/typescript-api/events/event-types#orderplacedevent)
- - [`OrderStateTransitionEvent`](/reference/typescript-api/events/event-types#orderstatetransitionevent)
- - [`PasswordResetEvent`](/reference/typescript-api/events/event-types#passwordresetevent)
- - [`PasswordResetVerifiedEvent`](/reference/typescript-api/events/event-types#passwordresetverifiedevent)
- - [`PaymentMethodEvent`](/reference/typescript-api/events/event-types#paymentmethodevent)
- - [`PaymentStateTransitionEvent`](/reference/typescript-api/events/event-types#paymentstatetransitionevent)
- - [`ProductChannelEvent`](/reference/typescript-api/events/event-types#productchannelevent)
- - [`ProductEvent`](/reference/typescript-api/events/event-types#productevent)
- - [`ProductOptionEvent`](/reference/typescript-api/events/event-types#productoptionevent)
- - [`ProductOptionGroupChangeEvent`](/reference/typescript-api/events/event-types#productoptiongroupchangeevent)
- - [`ProductOptionGroupEvent`](/reference/typescript-api/events/event-types#productoptiongroupevent)
- - [`ProductVariantChannelEvent`](/reference/typescript-api/events/event-types#productvariantchannelevent)
- - [`ProductVariantEvent`](/reference/typescript-api/events/event-types#productvariantevent)
- - [`PromotionEvent`](/reference/typescript-api/events/event-types#promotionevent)
- - [`ProvinceEvent`](/reference/typescript-api/events/event-types#provinceevent)
- - [`RefundStateTransitionEvent`](/reference/typescript-api/events/event-types#refundstatetransitionevent)
- - [`RoleChangeEvent`](/reference/typescript-api/events/event-types#rolechangeevent)
- - [`RoleEvent`](/reference/typescript-api/events/event-types#roleevent)
- - [`SearchEvent`](/reference/typescript-api/events/event-types#searchevent)
- - [`SellerEvent`](/reference/typescript-api/events/event-types#sellerevent)
- - [`ShippingMethodEvent`](/reference/typescript-api/events/event-types#shippingmethodevent)
- - [`StockMovementEvent`](/reference/typescript-api/events/event-types#stockmovementevent)
- - [`TaxCategoryEvent`](/reference/typescript-api/events/event-types#taxcategoryevent)
- - [`TaxRateEvent`](/reference/typescript-api/events/event-types#taxrateevent)
- - [`TaxRateModificationEvent`](/reference/typescript-api/events/event-types#taxratemodificationevent)
- - [`ZoneEvent`](/reference/typescript-api/events/event-types#zoneevent)
- - [`ZoneMembersEvent`](/reference/typescript-api/events/event-types#zonemembersevent)
+-   [`OrderLineEvent`](/reference/typescript-api/events/event-types#orderlineevent)
+-   [`OrderPlacedEvent`](/reference/typescript-api/events/event-types#orderplacedevent)
+-   [`OrderStateTransitionEvent`](/reference/typescript-api/events/event-types#orderstatetransitionevent)
+-   [`PasswordResetEvent`](/reference/typescript-api/events/event-types#passwordresetevent)
+-   [`PasswordResetVerifiedEvent`](/reference/typescript-api/events/event-types#passwordresetverifiedevent)
+-   [`PaymentMethodEvent`](/reference/typescript-api/events/event-types#paymentmethodevent)
+-   [`PaymentStateTransitionEvent`](/reference/typescript-api/events/event-types#paymentstatetransitionevent)
+-   [`ProductChannelEvent`](/reference/typescript-api/events/event-types#productchannelevent)
+-   [`ProductEvent`](/reference/typescript-api/events/event-types#productevent)
+-   [`ProductOptionEvent`](/reference/typescript-api/events/event-types#productoptionevent)
+-   [`ProductOptionGroupChangeEvent`](/reference/typescript-api/events/event-types#productoptiongroupchangeevent)
+-   [`ProductOptionGroupEvent`](/reference/typescript-api/events/event-types#productoptiongroupevent)
+-   [`ProductVariantChannelEvent`](/reference/typescript-api/events/event-types#productvariantchannelevent)
+-   [`ProductVariantEvent`](/reference/typescript-api/events/event-types#productvariantevent)
+-   [`PromotionEvent`](/reference/typescript-api/events/event-types#promotionevent)
+-   [`ProvinceEvent`](/reference/typescript-api/events/event-types#provinceevent)
+-   [`RefundStateTransitionEvent`](/reference/typescript-api/events/event-types#refundstatetransitionevent)
+-   [`RoleChangeEvent`](/reference/typescript-api/events/event-types#rolechangeevent)
+-   [`RoleEvent`](/reference/typescript-api/events/event-types#roleevent)
+-   [`SearchEvent`](/reference/typescript-api/events/event-types#searchevent)
+-   [`SellerEvent`](/reference/typescript-api/events/event-types#sellerevent)
+-   [`ShippingMethodEvent`](/reference/typescript-api/events/event-types#shippingmethodevent)
+-   [`StockMovementEvent`](/reference/typescript-api/events/event-types#stockmovementevent)
+-   [`TaxCategoryEvent`](/reference/typescript-api/events/event-types#taxcategoryevent)
+-   [`TaxRateEvent`](/reference/typescript-api/events/event-types#taxrateevent)
+-   [`TaxRateModificationEvent`](/reference/typescript-api/events/event-types#taxratemodificationevent)
+-   [`ZoneEvent`](/reference/typescript-api/events/event-types#zoneevent)
+-   [`ZoneMembersEvent`](/reference/typescript-api/events/event-types#zonemembersevent)
 
 </div>
 </div>
@@ -86,7 +86,7 @@ A full list of the available events follows.
 ## 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](https://docs.nestjs.com/fundamentals/lifecycle-events).
+lifecycle hooks of a plugin or service (see [NestJS Lifecycle events](https://docs.nestjs.com/fundamentals/lifecycle-events)).
 
 Here's an example where we subscribe to the `ProductEvent` and use it to trigger a rebuild of a static storefront:
 
@@ -103,15 +103,14 @@ export class StorefrontBuildPlugin implements OnModuleInit {
     constructor(
         // highlight-next-line
         private eventBus: EventBus,
-        private storefrontBuildService: StorefrontBuildService) {}
+        private storefrontBuildService: StorefrontBuildService,
+    ) {}
 
     onModuleInit() {
         // highlight-start
-        this.eventBus
-            .ofType(ProductEvent)
-            .subscribe(event => {
-                this.storefrontBuildService.triggerBuild();
-            });
+        this.eventBus.ofType(ProductEvent).subscribe(event => {
+            this.storefrontBuildService.triggerBuild();
+        });
         // highlight-end
     }
 }
@@ -131,12 +130,13 @@ import { debounceTime } from 'rxjs/operators';
 
 this.eventBus
     .ofType(ProductEvent)
-     // highlight-next-line
+    // highlight-next-line
     .pipe(debounceTime(1000))
     .subscribe(event => {
         this.storefrontBuildService.triggerBuild();
     });
 ```
+
 :::
 
 ### Subscribing to multiple event types
@@ -145,7 +145,13 @@ Using the `.ofType()` method allows us to subscribe to a single event type. If w
 
 ```ts title="src/plugins/my-plugin/my-plugin.plugin.ts"
 import { Injectable, OnModuleInit } from '@nestjs/common';
-import { EventBus, PluginCommonModule, VendurePlugin, ProductEvent, ProductVariantEvent } from '@vendure/core';
+import {
+    EventBus,
+    PluginCommonModule,
+    VendurePlugin,
+    ProductEvent,
+    ProductVariantEvent,
+} from '@vendure/core';
 
 @VendurePlugin({
     imports: [PluginCommonModule],
@@ -156,8 +162,7 @@ export class MyPluginPlugin implements OnModuleInit {
     onModuleInit() {
         this.eventBus
             // highlight-start
-            .filter(event =>
-                event instanceof ProductEvent || event instanceof ProductVariantEvent)
+            .filter(event => event instanceof ProductEvent || event instanceof ProductVariantEvent)
             // highlight-end
             .subscribe(event => {
                 // the event will be a ProductEvent or ProductVariantEvent
@@ -183,7 +188,7 @@ export class MyPluginService {
     async doSomethingWithProduct(ctx: RequestContext, product: Product) {
         // ... do something
         // highlight-next-line
-        this.eventBus.publish(new ProductEvent(ctx, product, 'updated'));
+        await this.eventBus.publish(new ProductEvent(ctx, product, 'updated'));
     }
 }
 ```
@@ -221,7 +226,10 @@ import { ProductReviewInput } from '../types';
 
 @Injectable()
 export class ProductReviewService {
-    constructor(private eventBus: EventBus, private productReviewService: ProductReviewService) {}
+    constructor(
+        private eventBus: EventBus,
+        private productReviewService: ProductReviewService,
+    ) {}
 
     async submitReview(ctx: RequestContext, input: ProductReviewInput) {
         // highlight-next-line
@@ -278,14 +286,121 @@ export class BlogPlugin implements OnModuleInit {
     onModuleInit() {
         this.eventBus
             // highlight-start
-            .ofType(BlogPostEvent).pipe(
-                filter(event => event.type === 'created'),
-            )
+            .ofType(BlogPostEvent)
+            .pipe(filter(event => event.type === 'created'))
             .subscribe(event => {
                 const blogPost = event.entity;
                 // do something with the newly created BlogPost
             });
-            // highlight-end
+        // highlight-end
+    }
+}
+```
+
+## 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](https://en.wikipedia.org/wiki/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:
+
+```ts title="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() {
+        // highlight-start
+        this.eventBus.registerBlockingEventHandler({
+            type: 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);
+            },
+        });
+        // highlight-end
     }
 }
 ```
+
+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:
+
+```ts
+// 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 => {
+        // ...
+    },
+    // highlight-next-line
+    before: 'sync-customer-details-handler',
+});
+```

+ 5 - 0
packages/core/src/event-bus/event-bus.ts

@@ -160,11 +160,16 @@ export class EventBus implements OnModuleDestroy {
      * 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({