@fluojs/event-bus 1.0.0-beta.7 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -75,7 +75,7 @@ export class UserService {
75
75
  export class AppModule {}
76
76
  ```
77
77
 
78
- `publish(event, options?)`는 `signal`, `timeoutMs`, `waitForHandlers`를 지원합니다. `waitForHandlers`의 기본값은 `true`이며, 기다리는 로컬 핸들러와 기다리는 트랜스포트 publish는 동일한 timeout 및 cancellation bound를 공유합니다. `waitForHandlers`를 `false`로 설정하면 publish가 즉시 반환되고 timeout bound를 적용하지 않습니다. Shutdown 중에는 이벤트 버스가 진행 중인 awaited publish 및 inbound transport handler 작업을 drain한 뒤 트랜스포트를 닫고, lifecycle이 stopping에 진입한 뒤의 새 publish 호출과 shutdown 시작 뒤 도착한 inbound transport callback은 무시합니다. Shutdown drain은 기본값이 5000ms인 `EventBusModule.forRoot({ shutdown: { drainTimeoutMs } })`로 제한됩니다. 활성 dispatch 작업이 이 bound 이후에도 멈춰 있으면 bus는 degraded status diagnostic을 기록하고 경고를 남긴 뒤, 애플리케이션 close를 무기한 hang시키지 않고 transport cleanup을 계속합니다.
78
+ `publish(event, options?)`는 `signal`, `timeoutMs`, `waitForHandlers`를 지원합니다. `waitForHandlers`의 기본값은 `true`이며, 기다리는 로컬 핸들러와 기다리는 트랜스포트 publish는 동일한 timeout 및 cancellation bound를 공유합니다. 이러한 bound가 실제 handler 또는 transport 작업이 끝나기 전에 호출자에게 반환되는 publish promise를 settle하더라도, shutdown은 해당 underlying awaited work가 settle되거나 shutdown drain bound가 만료될 때까지 계속 추적합니다. `waitForHandlers`를 `false`로 설정하면 publish가 즉시 반환되고 timeout bound를 적용하지 않습니다. Shutdown 중에는 이벤트 버스가 진행 중인 awaited publish 및 inbound transport handler 작업을 drain한 뒤 트랜스포트를 닫고, lifecycle이 stopping에 진입한 뒤의 새 publish 호출과 shutdown 시작 뒤 도착한 inbound transport callback은 무시합니다. Shutdown drain은 기본값이 5000ms인 `EventBusModule.forRoot({ shutdown: { drainTimeoutMs } })`로 제한됩니다. 활성 dispatch 작업이 이 bound 이후에도 멈춰 있으면 bus는 degraded status diagnostic을 기록하고 경고를 남긴 뒤, 애플리케이션 close를 무기한 hang시키지 않고 transport cleanup을 계속합니다.
79
79
 
80
80
  ## 일반적인 패턴
81
81
 
@@ -94,6 +94,46 @@ EventBusModule.forRoot({
94
94
  })
95
95
  ```
96
96
 
97
+ Redis Pub/Sub은 durable work queue가 아니라 fan-out transport입니다. 여러 애플리케이션 인스턴스가 같은 이벤트 채널을 구독하면 각 인스턴스가 같은 published fact를 볼 수 있습니다. 따라서 상태를 변경하거나 알림을 보내거나 외부 시스템을 호출하는 handler는 idempotent해야 합니다. Payload에 안정적인 event identifier 또는 business key를 담고, 이미 적용한 reaction을 기록하며, 반복 전달이 side effect를 두 번 실행하는 대신 같은 결과로 수렴하도록 만드세요.
98
+
99
+ `@OnEvent(...)` handler는 작고 bounded하게 유지하세요. 빠른 local projection, cache invalidation, 가벼운 notification처럼 publish timeout과 shutdown drain window 안에 끝낼 수 있는 reaction에 적합합니다. Reaction이 느리거나, failure-prone이거나, retry 가능하거나, operator-visible dead-letter handling이 필요하다면 해당 작업을 inline으로 수행하지 말고 event handler에서 `@fluojs/queue`의 durable job으로 hand off하세요. Handoff에는 애플리케이션이 소유한 unique claim을 사용하고, `queue.enqueue(...)`가 성공한 뒤에만 handoff를 enqueued로 표시하세요. Enqueue가 실패하면 pending claim을 해제해 이후 duplicate event가 안전하게 다시 시도할 수 있게 합니다.
100
+
101
+ 아래 예제의 `this.reactions` helper는 `@fluojs/event-bus`나 `@fluojs/queue` API가 아니라 애플리케이션이 소유한 claim store를 나타냅니다. Business key를 atomic하게 claim하고 stale pending claim을 애플리케이션의 retry policy에 따라 복구할 수 있는 저장소로 구현하세요.
102
+
103
+ ```typescript
104
+ import { Inject } from '@fluojs/core';
105
+ import { OnEvent } from '@fluojs/event-bus';
106
+ import { QueueLifecycleService } from '@fluojs/queue';
107
+
108
+ export class GenerateInvoiceJob {
109
+ constructor(public readonly orderId: string) {}
110
+ }
111
+
112
+ @Inject(QueueLifecycleService)
113
+ export class BillingEventsHandler {
114
+ constructor(private readonly queue: QueueLifecycleService) {}
115
+
116
+ @OnEvent(OrderPlacedEvent)
117
+ async enqueueInvoice(event: OrderPlacedEvent) {
118
+ const handoffKey = `${event.orderId}:invoice`;
119
+
120
+ if (!(await this.reactions.claimPending(handoffKey))) {
121
+ return;
122
+ }
123
+
124
+ try {
125
+ await this.queue.enqueue(new GenerateInvoiceJob(event.orderId));
126
+ await this.reactions.markEnqueued(handoffKey);
127
+ } catch (error) {
128
+ await this.reactions.releasePending(handoffKey);
129
+ throw error;
130
+ }
131
+ }
132
+ }
133
+ ```
134
+
135
+ 비즈니스 사실이 발생했음을 표현할 때는 event bus를 사용하세요. Reaction에 retry, backoff, workload isolation, dead-letter inspection이 필요하면 Queue를 사용하세요. Claim이 pending인 동안 프로세스가 종료될 수 있다면 애플리케이션의 retry policy에 맞게 stale pending record를 복구하도록 애플리케이션 소유 claim store를 설계하세요.
136
+
97
137
  ### 버전이 명시된 이벤트 키
98
138
 
99
139
  `static eventKey`를 사용하여 클래스 이름 변경이나 코드 압축(minification)과 관계없이 안정적인 채널 이름을 유지할 수 있습니다.
@@ -104,7 +144,7 @@ class UserRegisteredEvent {
104
144
  }
105
145
  ```
106
146
 
107
- 핸들러는 imported module의 singleton provider와 controller에서 발견됩니다. 각 핸들러는 격리된 clone payload를 받으며, class inheritance는 `instanceof` 매칭으로 지원됩니다. 외부 트랜스포트를 구성하면 subclass event publish는 publisher process에 해당 타입의 local handler가 없더라도 subclass channel과 prototype chain의 모든 inherited event channel로 fan-out됩니다. Subclass가 직접 `static eventKey`를 선언한 경우에만 그 값을 사용하며, 그렇지 않으면 subclass channel은 class name을 유지하고 base class는 자신의 stable key를 유지합니다.
147
+ 핸들러는 imported module의 singleton provider와 controller에서 발견됩니다. Discovery는 여러 provider가 같은 구현 class를 공유하더라도 서로 다른 singleton provider identity를 유지하며, 같은 provider token과 handler method가 중복 등록된 경우에는 한 번만 호출합니다. 각 핸들러는 격리된 clone payload를 받으며, class inheritance는 `instanceof` 매칭으로 지원됩니다. 외부 트랜스포트를 구성하면 subclass event publish는 publisher process에 해당 타입의 local handler가 없더라도 subclass channel과 prototype chain의 모든 inherited event channel로 fan-out됩니다. Subclass가 직접 `static eventKey`를 선언한 경우에만 그 값을 사용하며, 그렇지 않으면 subclass channel은 class name을 유지하고 base class는 자신의 stable key를 유지합니다.
108
148
 
109
149
  ## 공개 API 개요
110
150
 
@@ -120,7 +160,7 @@ class UserRegisteredEvent {
120
160
  - `EventBus`, `EventPublishOptions`, `EventBusModuleOptions`, `EventType`: 발행, 기본값, 트랜스포트, 안정적인 이벤트 키를 위한 타입 전용 계약입니다.
121
161
  - `EventBusLifecycleState`, `EventBusStatusAdapterInput`, `EventBusPlatformStatusSnapshot`: status snapshot 계약입니다.
122
162
 
123
- Transport bootstrap은 unique event channel마다 한 번만 subscribe합니다. `eventKey`가 있으면 transport channel 이름을 제어합니다. 잘못된 JSON transport message는 무시되며, shutdown 시작 뒤 도착한 inbound transport message는 local handler dispatch 전에 무시됩니다.
163
+ Transport bootstrap은 unique event channel마다 한 번만 subscribe합니다. `eventKey`가 있으면 transport channel 이름을 제어합니다. Bootstrap 중 이후 transport subscription이 실패하면 이벤트 버스는 이미 열린 channel을 rollback하기 위해 subscription error를 다시 던지기 전에 transport를 닫습니다. 잘못된 JSON transport message는 무시되며, shutdown 시작 뒤 도착한 inbound transport message는 local handler dispatch 전에 무시됩니다.
124
164
 
125
165
  ## 런타임별 및 통합 서브패스
126
166
 
package/README.md CHANGED
@@ -75,7 +75,7 @@ export class UserService {
75
75
  export class AppModule {}
76
76
  ```
77
77
 
78
- `publish(event, options?)` supports `signal`, `timeoutMs`, and `waitForHandlers`. `waitForHandlers` defaults to `true`; awaited local handlers and awaited transport publishes share the same timeout and cancellation bounds. When `waitForHandlers` is set to `false`, publishing returns immediately and skips timeout bounds. During shutdown, the event bus drains in-flight awaited publish and inbound transport handler work before closing the transport, ignores new publish calls after the lifecycle has started stopping, and ignores inbound transport callbacks that arrive after shutdown begins. Shutdown drain is bounded by `EventBusModule.forRoot({ shutdown: { drainTimeoutMs } })`, which defaults to 5000ms; if active dispatch work is still stuck after the bound, the bus records a degraded status diagnostic, logs a warning, and continues transport cleanup instead of hanging application close indefinitely.
78
+ `publish(event, options?)` supports `signal`, `timeoutMs`, and `waitForHandlers`. `waitForHandlers` defaults to `true`; awaited local handlers and awaited transport publishes share the same timeout and cancellation bounds. When those bounds settle the caller-facing publish promise before the underlying handler or transport work finishes, shutdown still tracks that underlying awaited work until it settles or the shutdown drain bound expires. When `waitForHandlers` is set to `false`, publishing returns immediately and skips timeout bounds. During shutdown, the event bus drains in-flight awaited publish and inbound transport handler work before closing the transport, ignores new publish calls after the lifecycle has started stopping, and ignores inbound transport callbacks that arrive after shutdown begins. Shutdown drain is bounded by `EventBusModule.forRoot({ shutdown: { drainTimeoutMs } })`, which defaults to 5000ms; if active dispatch work is still stuck after the bound, the bus records a degraded status diagnostic, logs a warning, and continues transport cleanup instead of hanging application close indefinitely.
79
79
 
80
80
  ## Common Patterns
81
81
 
@@ -94,6 +94,46 @@ EventBusModule.forRoot({
94
94
  })
95
95
  ```
96
96
 
97
+ Redis Pub/Sub is a fan-out transport, not a durable work queue. When multiple application instances subscribe to the same event channel, each instance can see the same published fact. Handlers that mutate state, send notifications, or call external systems should therefore be idempotent: carry a stable event identifier or business key in the payload, record which reactions have already been applied, and make repeat deliveries converge to the same result instead of performing the side effect twice.
98
+
99
+ Keep `@OnEvent(...)` handlers small and bounded. They are a good fit for fast local projections, cache invalidation, lightweight notifications, and other reactions that can finish within the publish timeout and shutdown drain window. If a reaction is slow, failure-prone, retryable, or needs operator-visible dead-letter handling, hand off a durable job to `@fluojs/queue` from the event handler instead of doing the work inline. Use an application-owned unique claim for the handoff, then mark the handoff as enqueued only after `queue.enqueue(...)` succeeds; if enqueue fails, release the pending claim so a later duplicate event can retry safely.
100
+
101
+ The `this.reactions` helper in the example below represents an application-owned claim store, not an API from `@fluojs/event-bus` or `@fluojs/queue`. Back it with storage that can atomically claim a business key and recover stale pending claims according to your application's retry policy.
102
+
103
+ ```typescript
104
+ import { Inject } from '@fluojs/core';
105
+ import { OnEvent } from '@fluojs/event-bus';
106
+ import { QueueLifecycleService } from '@fluojs/queue';
107
+
108
+ export class GenerateInvoiceJob {
109
+ constructor(public readonly orderId: string) {}
110
+ }
111
+
112
+ @Inject(QueueLifecycleService)
113
+ export class BillingEventsHandler {
114
+ constructor(private readonly queue: QueueLifecycleService) {}
115
+
116
+ @OnEvent(OrderPlacedEvent)
117
+ async enqueueInvoice(event: OrderPlacedEvent) {
118
+ const handoffKey = `${event.orderId}:invoice`;
119
+
120
+ if (!(await this.reactions.claimPending(handoffKey))) {
121
+ return;
122
+ }
123
+
124
+ try {
125
+ await this.queue.enqueue(new GenerateInvoiceJob(event.orderId));
126
+ await this.reactions.markEnqueued(handoffKey);
127
+ } catch (error) {
128
+ await this.reactions.releasePending(handoffKey);
129
+ throw error;
130
+ }
131
+ }
132
+ }
133
+ ```
134
+
135
+ Use the event bus to state that a business fact happened. Use Queue when the reaction needs retry, backoff, workload isolation, or dead-letter inspection. If the process can crash while a claim is pending, make the application-owned claim store recover stale pending records according to your application's retry policy.
136
+
97
137
  ### Versioned Event Keys
98
138
 
99
139
  Use static `eventKey` to ensure stable channel names regardless of class minification or renames.
@@ -104,7 +144,7 @@ class UserRegisteredEvent {
104
144
  }
105
145
  ```
106
146
 
107
- Handlers are discovered from singleton providers and controllers across imported modules. Each handler receives an isolated cloned payload, and class inheritance is supported through `instanceof` matching. With an external transport configured, publishing a subclass event fans out to the subclass channel and every inherited event channel in its prototype chain, even when the publisher process has no matching local handlers for those types. A subclass uses its own `static eventKey` only when it declares one directly; otherwise its class name remains the subclass channel while base classes keep their own stable keys.
147
+ Handlers are discovered from singleton providers and controllers across imported modules. Discovery keeps distinct singleton provider identities even when multiple providers share the same implementation class; duplicate registration of the same provider token and handler method is invoked only once. Each handler receives an isolated cloned payload, and class inheritance is supported through `instanceof` matching. With an external transport configured, publishing a subclass event fans out to the subclass channel and every inherited event channel in its prototype chain, even when the publisher process has no matching local handlers for those types. A subclass uses its own `static eventKey` only when it declares one directly; otherwise its class name remains the subclass channel while base classes keep their own stable keys.
108
148
 
109
149
  ## Public API Overview
110
150
 
@@ -120,7 +160,7 @@ Handlers are discovered from singleton providers and controllers across imported
120
160
  - `EventBus`, `EventPublishOptions`, `EventBusModuleOptions`, `EventType`: Type-only contracts for publishing, defaults, transports, and stable event keys.
121
161
  - `EventBusLifecycleState`, `EventBusStatusAdapterInput`, `EventBusPlatformStatusSnapshot`: Status snapshot contracts.
122
162
 
123
- Transport bootstrap subscribes once per unique event channel. `eventKey` controls the transport channel name when present. Invalid JSON transport messages are ignored, and inbound transport messages that arrive after shutdown starts are ignored before local handler dispatch.
163
+ Transport bootstrap subscribes once per unique event channel. `eventKey` controls the transport channel name when present. If a later transport subscription fails during bootstrap, the event bus closes the transport to roll back any channels that were already opened before rethrowing the subscription error. Invalid JSON transport messages are ignored, and inbound transport messages that arrive after shutdown starts are ignored before local handler dispatch.
124
164
 
125
165
  ## Runtime-Specific and Integration Subpaths
126
166
 
package/dist/service.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Container } from '@fluojs/di';
2
- import { type ApplicationLogger, type CompiledModule, type OnApplicationBootstrap, type OnApplicationShutdown } from '@fluojs/runtime';
2
+ import type { ApplicationLogger, CompiledModule, OnApplicationBootstrap, OnApplicationShutdown } from '@fluojs/runtime';
3
3
  import type { EventBus, EventBusModuleOptions, EventPublishOptions } from './types.js';
4
4
  /**
5
5
  * Lifecycle-managed in-process event bus with optional external transport fan-out.
@@ -24,6 +24,7 @@ export declare class EventBusLifecycleService implements EventBus, OnApplication
24
24
  private shutdownDrainTimeouts;
25
25
  private readonly activeDispatches;
26
26
  private readonly transport;
27
+ private transportClosed;
27
28
  constructor(runtimeContainer: Container, compiledModules: readonly CompiledModule[], logger: ApplicationLogger, moduleOptions: EventBusModuleOptions);
28
29
  onApplicationBootstrap(): Promise<void>;
29
30
  onApplicationShutdown(): Promise<void>;
@@ -45,6 +46,7 @@ export declare class EventBusLifecycleService implements EventBus, OnApplication
45
46
  private canPublishInCurrentLifecycle;
46
47
  private drainActiveDispatches;
47
48
  private trackActiveDispatch;
49
+ private trackActiveDispatchWork;
48
50
  private awaitShutdownDrain;
49
51
  private resolveShutdownDrainTimeoutMs;
50
52
  private matchEventDescriptors;
@@ -63,6 +65,9 @@ export declare class EventBusLifecycleService implements EventBus, OnApplication
63
65
  private logTransportPublishCancelledBeforeDispatch;
64
66
  private logBoundedTransportPublishError;
65
67
  private subscribeTransportChannels;
68
+ private rollbackTransportSubscriptionsAfterBootstrapFailure;
69
+ private closeTransportOrRecordFailure;
70
+ private closeTransport;
66
71
  private subscribeTransportChannel;
67
72
  private canDispatchIncomingTransportMessage;
68
73
  private dispatchIncomingTransportMessage;
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAY,MAAM,YAAY,CAAC;AACtD,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC3B,MAAM,iBAAiB,CAAC;AAMzB,OAAO,KAAK,EACV,QAAQ,EACR,qBAAqB,EAGrB,mBAAmB,EAEpB,MAAM,YAAY,CAAC;AAgEpB;;;;;GAKG;AACH,qBACa,wBAAyB,YAAW,QAAQ,EAAE,sBAAsB,EAAE,qBAAqB;IAepG,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,aAAa;IAjBhC,OAAO,CAAC,WAAW,CAAgC;IACnD,OAAO,CAAC,gBAAgB,CAA4B;IACpD,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,cAAc,CAAsF;IAC5G,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAsC;IACvE,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAqB;IACxD,OAAO,CAAC,sBAAsB,CAAK;IACnC,OAAO,CAAC,wBAAwB,CAAK;IACrC,OAAO,CAAC,0BAA0B,CAAK;IACvC,OAAO,CAAC,qBAAqB,CAAK;IAClC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA4B;IAC7D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgC;gBAGvC,gBAAgB,EAAE,SAAS,EAC3B,eAAe,EAAE,SAAS,cAAc,EAAE,EAC1C,MAAM,EAAE,iBAAiB,EACzB,aAAa,EAAE,qBAAqB;IAKjD,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAavC,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAsB5C;;;;OAIG;IACH,4BAA4B;IAe5B;;;;;;OAMG;IACG,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;YAY5D,cAAc;IA+B5B,OAAO,CAAC,4BAA4B;YAItB,qBAAqB;YAcrB,mBAAmB;YAUnB,kBAAkB;IAiBhC,OAAO,CAAC,6BAA6B;IAIrC,OAAO,CAAC,qBAAqB;IAI7B,OAAO,CAAC,qBAAqB;IAW7B,OAAO,CAAC,+BAA+B;IAWvC,OAAO,CAAC,8BAA8B;YAMxB,yBAAyB;YAazB,gBAAgB;IAqB9B,OAAO,CAAC,qBAAqB;IAY7B,OAAO,CAAC,kBAAkB;YAQZ,gBAAgB;IAW9B,OAAO,CAAC,oBAAoB;IAY5B,OAAO,CAAC,2BAA2B;IAkBnC,OAAO,CAAC,gBAAgB;YAiBV,kBAAkB;IA8BhC,OAAO,CAAC,0CAA0C;IAOlD,OAAO,CAAC,+BAA+B;YAwBzB,0BAA0B;YAmB1B,yBAAyB;IAiCvC,OAAO,CAAC,mCAAmC;YAI7B,gCAAgC;YAmBhC,uBAAuB;YAUvB,uBAAuB;IAmBrC,OAAO,CAAC,iCAAiC;IAOzC,OAAO,CAAC,yBAAyB;YAwBnB,qBAAqB;IAuBnC,OAAO,CAAC,sBAAsB;IAqB9B,OAAO,CAAC,kBAAkB;IAiB1B,OAAO,CAAC,gBAAgB;IAmBxB,OAAO,CAAC,0BAA0B;IAyBlC,OAAO,CAAC,+BAA+B;IAevC,OAAO,CAAC,8BAA8B;IA4BtC,OAAO,CAAC,uBAAuB;IAe/B,OAAO,CAAC,mBAAmB;YAsCb,aAAa;YA4Bb,sBAAsB;CAsBrC"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAY,MAAM,YAAY,CAAC;AACtD,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAMxH,OAAO,KAAK,EACV,QAAQ,EACR,qBAAqB,EAGrB,mBAAmB,EAEpB,MAAM,YAAY,CAAC;AAgEpB;;;;;GAKG;AACH,qBACa,wBAAyB,YAAW,QAAQ,EAAE,sBAAsB,EAAE,qBAAqB;IAgBpG,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,aAAa;IAlBhC,OAAO,CAAC,WAAW,CAAgC;IACnD,OAAO,CAAC,gBAAgB,CAA4B;IACpD,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,cAAc,CAAsF;IAC5G,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAsC;IACvE,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAqB;IACxD,OAAO,CAAC,sBAAsB,CAAK;IACnC,OAAO,CAAC,wBAAwB,CAAK;IACrC,OAAO,CAAC,0BAA0B,CAAK;IACvC,OAAO,CAAC,qBAAqB,CAAK;IAClC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA4B;IAC7D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgC;IAC1D,OAAO,CAAC,eAAe,CAAS;gBAGb,gBAAgB,EAAE,SAAS,EAC3B,eAAe,EAAE,SAAS,cAAc,EAAE,EAC1C,MAAM,EAAE,iBAAiB,EACzB,aAAa,EAAE,qBAAqB;IAKjD,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAavC,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAiB5C;;;;OAIG;IACH,4BAA4B;IAe5B;;;;;;OAMG;IACG,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;YAY5D,cAAc;IA+B5B,OAAO,CAAC,4BAA4B;YAItB,qBAAqB;YAcrB,mBAAmB;IAUjC,OAAO,CAAC,uBAAuB;YAajB,kBAAkB;IAiBhC,OAAO,CAAC,6BAA6B;IAIrC,OAAO,CAAC,qBAAqB;IAI7B,OAAO,CAAC,qBAAqB;IAW7B,OAAO,CAAC,+BAA+B;IAWvC,OAAO,CAAC,8BAA8B;YAMxB,yBAAyB;YAazB,gBAAgB;IAqB9B,OAAO,CAAC,qBAAqB;IAY7B,OAAO,CAAC,kBAAkB;YAQZ,gBAAgB;IAW9B,OAAO,CAAC,oBAAoB;IAY5B,OAAO,CAAC,2BAA2B;IAkBnC,OAAO,CAAC,gBAAgB;YAiBV,kBAAkB;IAkChC,OAAO,CAAC,0CAA0C;IAOlD,OAAO,CAAC,+BAA+B;YAwBzB,0BAA0B;YAwB1B,mDAAmD;YAInD,6BAA6B;YAa7B,cAAc;YAYd,yBAAyB;IAiCvC,OAAO,CAAC,mCAAmC;YAI7B,gCAAgC;YAmBhC,uBAAuB;YAUvB,uBAAuB;IAmBrC,OAAO,CAAC,iCAAiC;IAOzC,OAAO,CAAC,yBAAyB;YAwBnB,qBAAqB;IAuBnC,OAAO,CAAC,sBAAsB;IAqB9B,OAAO,CAAC,kBAAkB;IAiB1B,OAAO,CAAC,gBAAgB;IAmBxB,OAAO,CAAC,0BAA0B;IAyBlC,OAAO,CAAC,+BAA+B;IAevC,OAAO,CAAC,8BAA8B;IA4BtC,OAAO,CAAC,uBAAuB;IAe/B,OAAO,CAAC,mBAAmB;YAsCb,aAAa;YA4Bb,sBAAsB;CAsBrC"}
package/dist/service.js CHANGED
@@ -68,6 +68,7 @@ class EventBusLifecycleService {
68
68
  shutdownDrainTimeouts = 0;
69
69
  activeDispatches = new Set();
70
70
  transport;
71
+ transportClosed = false;
71
72
  constructor(runtimeContainer, compiledModules, logger, moduleOptions) {
72
73
  this.runtimeContainer = runtimeContainer;
73
74
  this.compiledModules = compiledModules;
@@ -88,19 +89,14 @@ class EventBusLifecycleService {
88
89
  }
89
90
  async onApplicationShutdown() {
90
91
  this.lifecycleState = 'stopping';
92
+ let transportClosedCleanly = true;
91
93
  if (this.activeDispatches.size > 0) {
92
94
  await this.drainActiveDispatches();
93
95
  }
94
96
  if (this.transport) {
95
- try {
96
- await this.transport.close();
97
- } catch (error) {
98
- this.transportCloseFailures += 1;
99
- this.lifecycleState = 'failed';
100
- this.logger.error('EventBusTransport failed to close.', error, 'EventBusLifecycleService');
101
- }
97
+ transportClosedCleanly = await this.closeTransportOrRecordFailure('EventBusTransport failed to close.');
102
98
  }
103
- if (this.lifecycleState !== 'failed') {
99
+ if (transportClosedCleanly) {
104
100
  this.lifecycleState = 'stopped';
105
101
  }
106
102
  }
@@ -181,6 +177,14 @@ class EventBusLifecycleService {
181
177
  this.activeDispatches.delete(dispatchWorkflow);
182
178
  }
183
179
  }
180
+ trackActiveDispatchWork(dispatchWork) {
181
+ const trackedWork = dispatchWork.then(() => undefined, () => undefined);
182
+ this.activeDispatches.add(trackedWork);
183
+ void trackedWork.finally(() => {
184
+ this.activeDispatches.delete(trackedWork);
185
+ });
186
+ return dispatchWork;
187
+ }
184
188
  async awaitShutdownDrain(activePublishes, timeoutMs) {
185
189
  let timeoutId;
186
190
  const timeout = new Promise(resolve => {
@@ -311,7 +315,9 @@ class EventBusLifecycleService {
311
315
  return;
312
316
  }
313
317
  try {
314
- await this.awaitInvocationBounds(this.transport.publish(channel, payload), publishOptions);
318
+ const publishWork = this.transport.publish(channel, payload);
319
+ const boundedPublishWork = publishOptions.waitForHandlers ? this.trackActiveDispatchWork(publishWork) : publishWork;
320
+ await this.awaitInvocationBounds(boundedPublishWork, publishOptions);
315
321
  } catch (error) {
316
322
  this.transportPublishFailures += 1;
317
323
  this.logBoundedTransportPublishError(channel, error);
@@ -344,9 +350,37 @@ class EventBusLifecycleService {
344
350
  channelDescriptors.push(descriptor);
345
351
  descriptorsByChannel.set(channel, channelDescriptors);
346
352
  }
347
- for (const [channel, channelDescriptors] of descriptorsByChannel) {
348
- await this.subscribeTransportChannel(channel, channelDescriptors);
353
+ try {
354
+ for (const [channel, channelDescriptors] of descriptorsByChannel) {
355
+ await this.subscribeTransportChannel(channel, channelDescriptors);
356
+ }
357
+ } catch (error) {
358
+ await this.rollbackTransportSubscriptionsAfterBootstrapFailure();
359
+ throw error;
360
+ }
361
+ }
362
+ async rollbackTransportSubscriptionsAfterBootstrapFailure() {
363
+ await this.closeTransportOrRecordFailure('EventBusTransport failed to close after bootstrap subscription failure.');
364
+ }
365
+ async closeTransportOrRecordFailure(message) {
366
+ try {
367
+ await this.closeTransport();
368
+ return true;
369
+ } catch (error) {
370
+ this.transportCloseFailures += 1;
371
+ this.lifecycleState = 'failed';
372
+ this.logger.error(message, error, 'EventBusLifecycleService');
373
+ return false;
374
+ }
375
+ }
376
+ async closeTransport() {
377
+ const transport = this.transport;
378
+ if (!transport || this.transportClosed) {
379
+ return;
349
380
  }
381
+ await transport.close();
382
+ this.transportClosed = true;
383
+ this.subscribedChannels.clear();
350
384
  }
351
385
  async subscribeTransportChannel(channel, channelDescriptors) {
352
386
  try {
@@ -391,7 +425,7 @@ class EventBusLifecycleService {
391
425
  this.logPublishCancelledBeforeDispatch(descriptor);
392
426
  return;
393
427
  }
394
- const invocation = this.invokeHandler(descriptor, event);
428
+ const invocation = this.trackActiveDispatchWork(this.invokeHandler(descriptor, event));
395
429
  try {
396
430
  await this.awaitInvocationBounds(invocation, publishOptions);
397
431
  } catch (error) {
@@ -475,7 +509,7 @@ class EventBusLifecycleService {
475
509
  };
476
510
  }
477
511
  discoverHandlerDescriptors() {
478
- const seen = new WeakMap();
512
+ const seen = new Map();
479
513
  const descriptors = [];
480
514
  for (const candidate of this.discoveryCandidates()) {
481
515
  const entries = getEventHandlerMetadataEntries(candidate.targetType.prototype);
@@ -484,7 +518,7 @@ class EventBusLifecycleService {
484
518
  }
485
519
  for (const entry of entries) {
486
520
  const eventType = entry.metadata.eventType;
487
- if (this.isDuplicateHandlerRegistration(seen, candidate.targetType, entry.propertyKey, eventType)) {
521
+ if (this.isDuplicateHandlerRegistration(seen, candidate.token, entry.propertyKey, eventType)) {
488
522
  continue;
489
523
  }
490
524
  descriptors.push(this.createHandlerDescriptor(candidate, entry.propertyKey, eventType));
@@ -501,11 +535,11 @@ class EventBusLifecycleService {
501
535
  }
502
536
  return true;
503
537
  }
504
- isDuplicateHandlerRegistration(seen, targetType, methodKey, eventType) {
505
- let methodsByKey = seen.get(targetType);
538
+ isDuplicateHandlerRegistration(seen, token, methodKey, eventType) {
539
+ let methodsByKey = seen.get(token);
506
540
  if (!methodsByKey) {
507
541
  methodsByKey = new Map();
508
- seen.set(targetType, methodsByKey);
542
+ seen.set(token, methodsByKey);
509
543
  }
510
544
  let seenEventTypes = methodsByKey.get(methodKey);
511
545
  if (!seenEventTypes) {
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "pubsub",
9
9
  "in-process"
10
10
  ],
11
- "version": "1.0.0-beta.7",
11
+ "version": "1.0.1",
12
12
  "private": false,
13
13
  "license": "MIT",
14
14
  "repository": {
@@ -39,9 +39,9 @@
39
39
  "dist"
40
40
  ],
41
41
  "dependencies": {
42
- "@fluojs/core": "^1.0.0-beta.6",
43
- "@fluojs/di": "^1.0.0-beta.8",
44
- "@fluojs/runtime": "^1.0.0-beta.12"
42
+ "@fluojs/core": "^1.0.3",
43
+ "@fluojs/di": "^1.1.0",
44
+ "@fluojs/runtime": "^1.1.8"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "ioredis": "^5.0.0"