@zudojs/events 0.1.0 → 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.
Files changed (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -11
  3. package/dist/eventBus/eventBus.core.d.ts +28 -2
  4. package/dist/eventBus/eventBus.core.js +109 -31
  5. package/dist/eventBus/eventBus.publish.d.ts +23 -5
  6. package/dist/eventBus/eventBus.publish.js +78 -32
  7. package/dist/eventBus/eventBus.registration.d.ts +24 -13
  8. package/dist/eventBus/eventBus.registration.js +31 -28
  9. package/dist/eventBus/eventBus.type.d.ts +75 -3
  10. package/dist/eventBus/eventBus.type.js +9 -0
  11. package/dist/eventBus/index.d.ts +2 -0
  12. package/dist/eventBus/index.js +2 -0
  13. package/dist/eventEmitter/eventEmitter.abort.d.ts +11 -2
  14. package/dist/eventEmitter/eventEmitter.abort.js +20 -5
  15. package/dist/eventEmitter/eventEmitter.core.d.ts +14 -3
  16. package/dist/eventEmitter/eventEmitter.core.js +72 -56
  17. package/dist/eventEmitter/eventEmitter.parallel.d.ts +7 -1
  18. package/dist/eventEmitter/eventEmitter.parallel.js +31 -13
  19. package/dist/eventEmitter/eventEmitter.sequential.d.ts +17 -1
  20. package/dist/eventEmitter/eventEmitter.sequential.js +19 -12
  21. package/dist/eventEmitter/eventEmitter.type.d.ts +60 -0
  22. package/dist/eventErrors/eventError.base.d.ts +50 -3
  23. package/dist/eventErrors/eventError.base.js +64 -3
  24. package/dist/eventHandler/eventHandler.core.d.ts +19 -0
  25. package/dist/eventHandler/eventHandler.core.js +67 -4
  26. package/dist/eventMiddleware/eventMiddleware.builder.js +5 -11
  27. package/dist/eventMiddleware/eventMiddleware.helper.js +0 -1
  28. package/dist/eventMiddleware/eventMiddleware.pipeline.d.ts +5 -0
  29. package/dist/eventMiddleware/eventMiddleware.pipeline.js +33 -9
  30. package/dist/eventRegistry/eventRegistry.lifecycle.d.ts +10 -5
  31. package/dist/eventRegistry/eventRegistry.lifecycle.js +23 -10
  32. package/dist/eventRegistry/eventRegistry.queries.d.ts +16 -7
  33. package/dist/eventRegistry/eventRegistry.queries.js +22 -13
  34. package/dist/eventRegistry/eventRegistry.registration.d.ts +14 -7
  35. package/dist/eventRegistry/eventRegistry.registration.js +120 -41
  36. package/dist/eventRegistry/eventRegistry.store.d.ts +12 -5
  37. package/dist/eventRegistry/eventRegistry.store.js +37 -12
  38. package/dist/eventRegistry/eventRegistry.type.d.ts +87 -2
  39. package/dist/eventRegistry/eventRegistry.type.js +5 -0
  40. package/dist/eventSubscription/eventSubscription.core.d.ts +5 -0
  41. package/dist/eventSubscription/eventSubscription.core.js +17 -3
  42. package/dist/eventTypes/eventDefinition.type.d.ts +11 -4
  43. package/dist/eventTypes/eventDefinition.type.js +49 -17
  44. package/dist/eventTypes/eventPayload.type.d.ts +5 -0
  45. package/dist/eventTypes/eventPayload.type.js +62 -22
  46. package/dist/eventTypes/eventType.type.d.ts +22 -5
  47. package/dist/eventTypes/eventType.type.js +33 -11
  48. package/dist/eventTypes/index.d.ts +2 -2
  49. package/dist/eventTypes/index.js +2 -2
  50. package/package.json +26 -14
  51. package/dist/.tsbuildinfo +0 -1
  52. package/dist/eventBus/eventBus.core.d.ts.map +0 -1
  53. package/dist/eventBus/eventBus.core.js.map +0 -1
  54. package/dist/eventBus/eventBus.factory.d.ts.map +0 -1
  55. package/dist/eventBus/eventBus.factory.js.map +0 -1
  56. package/dist/eventBus/eventBus.publish.d.ts.map +0 -1
  57. package/dist/eventBus/eventBus.publish.js.map +0 -1
  58. package/dist/eventBus/eventBus.registration.d.ts.map +0 -1
  59. package/dist/eventBus/eventBus.registration.js.map +0 -1
  60. package/dist/eventBus/eventBus.type.d.ts.map +0 -1
  61. package/dist/eventBus/eventBus.type.js.map +0 -1
  62. package/dist/eventBus/index.d.ts.map +0 -1
  63. package/dist/eventBus/index.js.map +0 -1
  64. package/dist/eventEmitter/eventEmitter.abort.d.ts.map +0 -1
  65. package/dist/eventEmitter/eventEmitter.abort.js.map +0 -1
  66. package/dist/eventEmitter/eventEmitter.core.d.ts.map +0 -1
  67. package/dist/eventEmitter/eventEmitter.core.js.map +0 -1
  68. package/dist/eventEmitter/eventEmitter.parallel.d.ts.map +0 -1
  69. package/dist/eventEmitter/eventEmitter.parallel.js.map +0 -1
  70. package/dist/eventEmitter/eventEmitter.sequential.d.ts.map +0 -1
  71. package/dist/eventEmitter/eventEmitter.sequential.js.map +0 -1
  72. package/dist/eventEmitter/eventEmitter.type.d.ts.map +0 -1
  73. package/dist/eventEmitter/eventEmitter.type.js.map +0 -1
  74. package/dist/eventEmitter/index.d.ts.map +0 -1
  75. package/dist/eventEmitter/index.js.map +0 -1
  76. package/dist/eventErrors/eventError.base.d.ts.map +0 -1
  77. package/dist/eventErrors/eventError.base.js.map +0 -1
  78. package/dist/eventErrors/index.d.ts.map +0 -1
  79. package/dist/eventErrors/index.js.map +0 -1
  80. package/dist/eventHandler/eventHandler.core.d.ts.map +0 -1
  81. package/dist/eventHandler/eventHandler.core.js.map +0 -1
  82. package/dist/eventHandler/index.d.ts.map +0 -1
  83. package/dist/eventHandler/index.js.map +0 -1
  84. package/dist/eventMiddleware/eventMiddleware.builder.d.ts.map +0 -1
  85. package/dist/eventMiddleware/eventMiddleware.builder.js.map +0 -1
  86. package/dist/eventMiddleware/eventMiddleware.helper.d.ts.map +0 -1
  87. package/dist/eventMiddleware/eventMiddleware.helper.js.map +0 -1
  88. package/dist/eventMiddleware/eventMiddleware.pipeline.d.ts.map +0 -1
  89. package/dist/eventMiddleware/eventMiddleware.pipeline.js.map +0 -1
  90. package/dist/eventMiddleware/eventMiddleware.type.d.ts.map +0 -1
  91. package/dist/eventMiddleware/eventMiddleware.type.js.map +0 -1
  92. package/dist/eventMiddleware/index.d.ts.map +0 -1
  93. package/dist/eventMiddleware/index.js.map +0 -1
  94. package/dist/eventRegistry/eventRegistry.lifecycle.d.ts.map +0 -1
  95. package/dist/eventRegistry/eventRegistry.lifecycle.js.map +0 -1
  96. package/dist/eventRegistry/eventRegistry.queries.d.ts.map +0 -1
  97. package/dist/eventRegistry/eventRegistry.queries.js.map +0 -1
  98. package/dist/eventRegistry/eventRegistry.registration.d.ts.map +0 -1
  99. package/dist/eventRegistry/eventRegistry.registration.js.map +0 -1
  100. package/dist/eventRegistry/eventRegistry.store.d.ts.map +0 -1
  101. package/dist/eventRegistry/eventRegistry.store.js.map +0 -1
  102. package/dist/eventRegistry/eventRegistry.type.d.ts.map +0 -1
  103. package/dist/eventRegistry/eventRegistry.type.js.map +0 -1
  104. package/dist/eventRegistry/index.d.ts.map +0 -1
  105. package/dist/eventRegistry/index.js.map +0 -1
  106. package/dist/eventSubscription/eventSubscription.core.d.ts.map +0 -1
  107. package/dist/eventSubscription/eventSubscription.core.js.map +0 -1
  108. package/dist/eventSubscription/index.d.ts.map +0 -1
  109. package/dist/eventSubscription/index.js.map +0 -1
  110. package/dist/eventTypes/eventDefinition.type.d.ts.map +0 -1
  111. package/dist/eventTypes/eventDefinition.type.js.map +0 -1
  112. package/dist/eventTypes/eventPayload.type.d.ts.map +0 -1
  113. package/dist/eventTypes/eventPayload.type.js.map +0 -1
  114. package/dist/eventTypes/eventType.type.d.ts.map +0 -1
  115. package/dist/eventTypes/eventType.type.js.map +0 -1
  116. package/dist/eventTypes/index.d.ts.map +0 -1
  117. package/dist/eventTypes/index.js.map +0 -1
  118. package/dist/index.d.ts.map +0 -1
  119. package/dist/index.js.map +0 -1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zudojs Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -11,32 +11,91 @@ npm install @zudojs/events
11
11
  ## Quick Start
12
12
 
13
13
  ```typescript
14
- import { createEventBus } from "@zudojs/events";
14
+ import { createEventBus, defineEvent } from "@zudojs/events";
15
+
16
+ interface UserCreated {
17
+ readonly id: string;
18
+ readonly name: string;
19
+ }
15
20
 
16
21
  const bus = createEventBus();
17
22
 
23
+ const UserCreatedEvent = defineEvent<"user.created", UserCreated>("user.created");
24
+
18
25
  bus.on("user.created", (event) => {
19
- console.log("New user:", event.payload.id);
26
+ console.log("New user:", (event.payload as UserCreated).id);
20
27
  });
21
28
 
22
- await bus.emit({
29
+ // Publish from an event definition …
30
+ await bus.publish(UserCreatedEvent.create({ id: "123", name: "Alice" }));
31
+
32
+ // … or from plain event input.
33
+ await bus.publishEvent({
23
34
  type: "user.created",
24
- payload: { id: "123", name: "Alice" },
35
+ payload: { id: "124", name: "Bob" },
25
36
  });
26
37
  ```
27
38
 
39
+ `bus.emit()` accepts either a full `Event` or an `EventInput` and behaves like
40
+ `publish` / `publishEvent` respectively.
41
+
28
42
  ## Features
29
43
 
30
- - Event bus with middleware pipeline
31
- - Event emitter with typed payloads
32
- - Event registry for documentation and validation
33
- - Wildcard event subscriptions
34
- - Async event handlers
35
- - Event replay and history
44
+ - Event bus with a middleware pipeline (`use()`, constructor and per-publish middleware)
45
+ - Sequential or parallel handler dispatch with `THROW` / `CONTINUE` error modes
46
+ - Handler priorities, one-time handlers, per-handler timeouts
47
+ - Wildcard subscriptions (`"user.*"`, `"*"`)
48
+ - Event registry for typed definitions; handlers registered on the registry are dispatched by the bus
49
+ - Deep-frozen events (`freezeEvents`, on by default) so handlers cannot alter what other handlers see
50
+ - Listener-leak warnings (`maxListeners`) and an `onError` hook for fire-and-forget publishes
51
+ - Typed error classes from `@zudojs/errors` (`EventHandlerError`, `EventMiddlewareError`, `EventDispatchAbortedError`, …)
52
+
53
+ ## Lifecycle
54
+
55
+ ```
56
+ CREATED ──(first use / start)──▶ ACTIVE ◀──(start)── STOPPED
57
+ │ ▲
58
+ └──────(stop)─────────┘
59
+ ▼
60
+ DISPOSED
61
+ ```
62
+
63
+ - A `CREATED` bus starts itself on the first `publish` / `on`.
64
+ - `stop()` moves the bus to `STOPPED`; publishing or subscribing then throws
65
+ `EventBusStoppedError` until `start()` is called. Handlers and definitions are kept.
66
+ - `dispose()` is final; every operation throws `EventBusDisposedError`.
67
+
68
+ ## Publish results
69
+
70
+ ```typescript
71
+ const result = await bus.publish(event);
72
+
73
+ result.handled; // true when at least one handler succeeded
74
+ result.handlerCount; // handlers invoked
75
+ result.succeeded; // handlers that completed
76
+ result.failed; // handlers that threw
77
+ result.errors; // EventHandlerError[] (cause = the raw thrown value)
78
+ result.shortCircuited; // true when a middleware did not call next()
79
+ ```
80
+
81
+ In the default `CONTINUE` error mode handler failures are collected in
82
+ `result.errors` (and forwarded to the `onError` option). In `THROW` mode the first
83
+ `EventHandlerError` rejects the publish. Errors thrown by a middleware itself are
84
+ wrapped as `EventMiddlewareError`; handler errors and aborts pass through unwrapped.
85
+
86
+ ## Registry
87
+
88
+ `bus.register(defineEvent("order.placed"))` records a definition; with
89
+ `requireRegistration: true` the bus rejects unregistered types with
90
+ `EventTypeNotFoundError`. `bus.unregister(type)` removes only the definition; pass
91
+ `{ removeHandlers: true }` to also drop handlers subscribed to exactly that type.
92
+
93
+ Event types are normalised (trimmed, lower-cased) everywhere, so `"User.Created"`
94
+ and `"user.created"` refer to the same event.
36
95
 
37
96
  ## Use Cases
38
97
 
39
98
  - Decoupling application components
40
99
  - Audit logging and change tracking
41
100
  - Real-time notifications
42
- - CQRS event sourcing
101
+ - CQRS event publishing (see `@zudojs/cqrs`)
@@ -14,6 +14,11 @@ import { EventBusState } from "./eventBus.type.js";
14
14
  export { EventBusState } from "./eventBus.type.js";
15
15
  /**
16
16
  * High-level event bus.
17
+ *
18
+ * Lifecycle: CREATED → ACTIVE ⇄ STOPPED → DISPOSED. A CREATED bus
19
+ * starts itself on first use; a STOPPED bus rejects publishing and
20
+ * subscribing until start() is called; a DISPOSED bus rejects
21
+ * everything.
17
22
  */
18
23
  export declare class EventBus {
19
24
  private readonly emitter;
@@ -24,6 +29,11 @@ export declare class EventBus {
24
29
  private state;
25
30
  constructor(options?: EventBusOptions);
26
31
  start(): this;
32
+ /**
33
+ * Stops the bus. Publishing and subscribing throw
34
+ * EventBusStoppedError until start() is called again; handlers
35
+ * and definitions are kept.
36
+ */
27
37
  stop(): this;
28
38
  register<TType extends EventType, TPayload>(definition: EventDefinition<TType, TPayload>): import("../index.js").RegisteredEventDefinition<TType, TPayload>;
29
39
  on<TEvent extends Event = Event>(eventType: EventTypePattern, handler: EventHandlerLike<TEvent>, options?: Omit<EventHandlerOptions, "eventType">): EventSubscription;
@@ -33,10 +43,22 @@ export declare class EventBus {
33
43
  use(middleware: EventMiddlewareLike, options?: EventMiddlewareOptions): () => void;
34
44
  publish<TEvent extends Event>(event: TEvent, options?: PublishOptions): Promise<EventPublishResult<TEvent>>;
35
45
  publishEvent<TPayload>(input: EventInput<TPayload>, options?: PublishOptions): Promise<EventPublishResult<Event<TPayload>>>;
36
- emit<TEvent extends Event>(event: TEvent, options?: PublishOptions): Promise<EventPublishResult<TEvent>>;
46
+ /**
47
+ * Publishes an event or event input. A full Event is published
48
+ * as is; an EventInput ({ type, payload, ... }) is turned into
49
+ * an event first.
50
+ */
51
+ emit<TPayload>(input: Event<TPayload> | EventInput<TPayload>, options?: PublishOptions): Promise<EventPublishResult<Event<TPayload>>>;
37
52
  getDefinition<TType extends EventType, TPayload = unknown>(eventType: TType): import("../index.js").RegisteredEventDefinition<TType, TPayload> | undefined;
38
53
  hasEvent(eventType: EventType): boolean;
39
- unregister(eventType: EventType): boolean;
54
+ /**
55
+ * Removes an event definition. Handlers are not affected unless
56
+ * `removeHandlers` is true, in which case every handler whose
57
+ * pattern is exactly this event type is unregistered too.
58
+ */
59
+ unregister(eventType: EventType, options?: {
60
+ readonly removeHandlers?: boolean;
61
+ }): boolean;
40
62
  getRegistry(): EventRegistry;
41
63
  getEmitter(): EventEmitter;
42
64
  getDefinitions(): readonly import("../index.js").RegisteredEventDefinition<string, unknown>[];
@@ -48,6 +70,10 @@ export declare class EventBus {
48
70
  subscribe(listener: EventBusListener): () => void;
49
71
  dispose(): void;
50
72
  toError(error: unknown, event?: Event): EventError;
73
+ private publishDependencies;
74
+ /**
75
+ * Auto-starts a CREATED bus; rejects a STOPPED or DISPOSED one.
76
+ */
51
77
  private ensureUsable;
52
78
  private ensureNotDisposed;
53
79
  private notify;
@@ -1,16 +1,23 @@
1
1
  /**
2
2
  * Event bus core class for Zudojs.
3
3
  */
4
+ import { isEvent } from "../eventTypes/eventDefinition.type.js";
4
5
  import { EventEmitter } from "../eventEmitter/eventEmitter.core.js";
5
6
  import { EventErrorMode } from "../eventEmitter/eventEmitter.type.js";
6
7
  import { EventRegistry } from "../eventRegistry/eventRegistry.store.js";
7
- import { EventError, toEventError } from "../eventErrors/eventError.base.js";
8
+ import { normalizeRegistryEventType } from "../eventRegistry/eventRegistry.registration.js";
9
+ import { EventBusDisposedError, EventBusStoppedError, EventError, toEventError, } from "../eventErrors/eventError.base.js";
8
10
  import { EventBusState } from "./eventBus.type.js";
9
11
  export { EventBusState } from "./eventBus.type.js";
10
12
  import { busOn, busOnce, busOnAny, busOff, busUse, registerMiddlewareItem, } from "./eventBus.registration.js";
11
13
  import { busPublish, busPublishEvent } from "./eventBus.publish.js";
12
14
  /**
13
15
  * High-level event bus.
16
+ *
17
+ * Lifecycle: CREATED → ACTIVE ⇄ STOPPED → DISPOSED. A CREATED bus
18
+ * starts itself on first use; a STOPPED bus rejects publishing and
19
+ * subscribing until start() is called; a DISPOSED bus rejects
20
+ * everything.
14
21
  */
15
22
  export class EventBus {
16
23
  emitter;
@@ -22,11 +29,27 @@ export class EventBus {
22
29
  constructor(options = {}) {
23
30
  this.options = {
24
31
  requireRegistration: options.requireRegistration ?? false,
32
+ onError: options.onError,
25
33
  };
26
- this.registry = new EventRegistry(options.registry);
34
+ this.registry = new EventRegistry({
35
+ ...options.registry,
36
+ maxHandlersPerPattern: options.emitter?.maxListeners,
37
+ onWarning: options.onWarning,
38
+ onError: options.onError
39
+ ? (error, context) => options.onError?.(error, {
40
+ source: context.source,
41
+ })
42
+ : undefined,
43
+ });
44
+ /**
45
+ * The emitter stores its handlers in the bus registry so
46
+ * handlers registered through either surface are dispatched.
47
+ */
27
48
  this.emitter = new EventEmitter({
28
- ...options.emitter,
49
+ mode: options.emitter?.mode,
29
50
  errorMode: options.emitter?.errorMode ?? EventErrorMode.CONTINUE,
51
+ freezeEvents: options.emitter?.freezeEvents,
52
+ store: this.registry,
30
53
  });
31
54
  this.busMiddleware = (options.middleware ?? []).map((m, index) => registerMiddlewareItem(m, index));
32
55
  }
@@ -42,12 +65,17 @@ export class EventBus {
42
65
  });
43
66
  return this;
44
67
  }
68
+ /**
69
+ * Stops the bus. Publishing and subscribing throw
70
+ * EventBusStoppedError until start() is called again; handlers
71
+ * and definitions are kept.
72
+ */
45
73
  stop() {
46
74
  this.ensureNotDisposed();
47
75
  if (this.state !== EventBusState.ACTIVE) {
48
76
  return this;
49
77
  }
50
- this.state = EventBusState.CREATED;
78
+ this.state = EventBusState.STOPPED;
51
79
  this.notify({
52
80
  type: "stopped",
53
81
  timestamp: new Date(),
@@ -55,17 +83,17 @@ export class EventBus {
55
83
  return this;
56
84
  }
57
85
  register(definition) {
58
- this.ensureUsable();
86
+ this.ensureUsable("register");
59
87
  return this.registry.register(definition);
60
88
  }
61
89
  on(eventType, handler, options = {}) {
62
- return busOn(this.emitter, eventType, handler, options, () => this.ensureUsable());
90
+ return busOn(this.emitter, eventType, handler, options, () => this.ensureUsable("subscribe"));
63
91
  }
64
92
  once(eventType, handler, options = {}) {
65
- return busOnce(this.emitter, eventType, handler, options, () => this.ensureUsable());
93
+ return busOnce(this.emitter, eventType, handler, options, () => this.ensureUsable("subscribe"));
66
94
  }
67
95
  onAny(handler, options = {}) {
68
- return busOnAny(this.emitter, handler, options, () => this.ensureUsable());
96
+ return busOnAny(this.emitter, handler, options, () => this.ensureUsable("subscribe"));
69
97
  }
70
98
  off(subscription) {
71
99
  return busOff(this.emitter, subscription, () => this.ensureNotDisposed());
@@ -75,41 +103,67 @@ export class EventBus {
75
103
  return busUse(this.busMiddleware, middleware, options);
76
104
  }
77
105
  async publish(event, options = {}) {
78
- return busPublish(event, options, this.emitter, this.registry, this.busMiddleware, this.options.requireRegistration, () => this.ensureUsable(), (e) => this.notify(e));
106
+ return busPublish(event, options, this.publishDependencies());
79
107
  }
80
108
  async publishEvent(input, options = {}) {
81
- return busPublishEvent(input, options, this.emitter, this.registry, this.busMiddleware, this.options.requireRegistration, () => this.ensureUsable(), (e) => this.notify(e));
82
- }
83
- async emit(event, options = {}) {
84
- return this.publish(event, options);
109
+ return busPublishEvent(input, options, this.publishDependencies());
110
+ }
111
+ /**
112
+ * Publishes an event or event input. A full Event is published
113
+ * as is; an EventInput ({ type, payload, ... }) is turned into
114
+ * an event first.
115
+ */
116
+ async emit(input, options = {}) {
117
+ if (isEvent(input)) {
118
+ return this.publish(input, options);
119
+ }
120
+ return this.publishEvent(input, options);
85
121
  }
86
122
  getDefinition(eventType) {
87
- this.ensureUsable();
123
+ this.ensureNotDisposed();
88
124
  return this.registry.get(eventType);
89
125
  }
90
126
  hasEvent(eventType) {
91
- this.ensureUsable();
127
+ this.ensureNotDisposed();
92
128
  return this.registry.has(eventType);
93
129
  }
94
- unregister(eventType) {
95
- this.ensureUsable();
96
- return this.registry.unregister(eventType);
130
+ /**
131
+ * Removes an event definition. Handlers are not affected unless
132
+ * `removeHandlers` is true, in which case every handler whose
133
+ * pattern is exactly this event type is unregistered too.
134
+ */
135
+ unregister(eventType, options = {}) {
136
+ this.ensureNotDisposed();
137
+ const removed = this.registry.unregister(eventType);
138
+ if (options.removeHandlers) {
139
+ /**
140
+ * Compare patterns on the full handler list: the matching
141
+ * query skips disabled handlers, which must be dropped too.
142
+ */
143
+ const type = normalizeRegistryEventType(eventType);
144
+ for (const handler of this.registry.getHandlers()) {
145
+ if (handler.eventType === type) {
146
+ this.registry.unregisterHandler(handler.id);
147
+ }
148
+ }
149
+ }
150
+ return removed;
97
151
  }
98
152
  getRegistry() {
99
- this.ensureUsable();
153
+ this.ensureNotDisposed();
100
154
  return this.registry;
101
155
  }
102
156
  getEmitter() {
103
- this.ensureUsable();
157
+ this.ensureNotDisposed();
104
158
  return this.emitter;
105
159
  }
106
160
  getDefinitions() {
107
- this.ensureUsable();
161
+ this.ensureNotDisposed();
108
162
  return this.registry.getDefinitions();
109
163
  }
110
164
  getHandlers() {
111
- this.ensureUsable();
112
- return this.emitter.getRegistrations();
165
+ this.ensureNotDisposed();
166
+ return this.registry.getHandlers();
113
167
  }
114
168
  getState() {
115
169
  return this.state;
@@ -121,7 +175,7 @@ export class EventBus {
121
175
  return this.registry.eventCount;
122
176
  }
123
177
  get handlerCount() {
124
- return this.emitter.listenerCount;
178
+ return this.registry.handlerCount;
125
179
  }
126
180
  subscribe(listener) {
127
181
  this.ensureNotDisposed();
@@ -145,17 +199,32 @@ export class EventBus {
145
199
  eventId: event?.id,
146
200
  });
147
201
  }
148
- ensureUsable() {
202
+ publishDependencies() {
203
+ return {
204
+ emitter: this.emitter,
205
+ registry: this.registry,
206
+ busMiddleware: this.busMiddleware,
207
+ requireRegistration: this.options.requireRegistration,
208
+ ensureUsable: () => this.ensureUsable("publish"),
209
+ notify: (e) => this.notify(e),
210
+ onError: this.options.onError,
211
+ };
212
+ }
213
+ /**
214
+ * Auto-starts a CREATED bus; rejects a STOPPED or DISPOSED one.
215
+ */
216
+ ensureUsable(operation) {
149
217
  this.ensureNotDisposed();
218
+ if (this.state === EventBusState.STOPPED) {
219
+ throw new EventBusStoppedError(operation);
220
+ }
150
221
  if (this.state === EventBusState.CREATED) {
151
222
  this.start();
152
223
  }
153
224
  }
154
225
  ensureNotDisposed() {
155
226
  if (this.state === EventBusState.DISPOSED) {
156
- throw new EventError("Event bus has already been disposed.", {
157
- code: "EVENT_BUS_DISPOSED",
158
- });
227
+ throw new EventBusDisposedError();
159
228
  }
160
229
  }
161
230
  notify(event) {
@@ -163,11 +232,20 @@ export class EventBus {
163
232
  try {
164
233
  listener(event);
165
234
  }
166
- catch {
235
+ catch (error) {
167
236
  /**
168
- * Observers must never be able to break
169
- * event bus operations.
237
+ * Observers must never be able to break event bus
238
+ * operations; failures go to the onError hook.
170
239
  */
240
+ try {
241
+ this.options.onError?.(error, {
242
+ source: "observer",
243
+ event: event.event,
244
+ });
245
+ }
246
+ catch {
247
+ // Ignore failures of the error hook itself.
248
+ }
171
249
  }
172
250
  }
173
251
  }
@@ -2,16 +2,34 @@
2
2
  * Event bus publish methods for Zudojs.
3
3
  */
4
4
  import type { Event, EventInput } from "../eventTypes/eventDefinition.type.js";
5
- import { EventEmitter } from "../eventEmitter/eventEmitter.core.js";
6
- import { EventRegistry } from "../eventRegistry/eventRegistry.store.js";
5
+ import type { EventEmitter } from "../eventEmitter/eventEmitter.core.js";
6
+ import type { EventEmitResult } from "../eventEmitter/eventEmitter.type.js";
7
+ import type { EventRegistry } from "../eventRegistry/eventRegistry.store.js";
7
8
  import type { RegisteredEventMiddleware } from "../eventMiddleware/eventMiddleware.type.js";
8
- import type { PublishOptions, EventPublishResult, EventBusEvent } from "./eventBus.type.js";
9
+ import type { PublishOptions, EventPublishResult, EventBusEvent, EventBusErrorContext } from "./eventBus.type.js";
10
+ /**
11
+ * Dependencies the publish functions need from the bus.
12
+ */
13
+ export interface BusPublishDependencies {
14
+ readonly emitter: EventEmitter;
15
+ readonly registry: EventRegistry;
16
+ readonly busMiddleware: readonly RegisteredEventMiddleware[];
17
+ readonly requireRegistration: boolean;
18
+ readonly ensureUsable: () => void;
19
+ readonly notify: (e: EventBusEvent) => void;
20
+ readonly onError?: (error: unknown, context: EventBusErrorContext) => void;
21
+ }
22
+ /**
23
+ * Determines whether a middleware pipeline result is the emit
24
+ * result produced by the terminal handler dispatch.
25
+ */
26
+ export declare function isEventEmitResult(value: unknown): value is EventEmitResult;
9
27
  /**
10
28
  * Publishes an event through the bus.
11
29
  */
12
- export declare function busPublish<TEvent extends Event>(event: TEvent, options: PublishOptions, emitter: EventEmitter, registry: EventRegistry, busMiddleware: RegisteredEventMiddleware[], requireRegistration: boolean, ensureUsable: () => void, notify: (e: EventBusEvent) => void): Promise<EventPublishResult<TEvent>>;
30
+ export declare function busPublish<TEvent extends Event>(event: TEvent, options: PublishOptions, deps: BusPublishDependencies): Promise<EventPublishResult<TEvent>>;
13
31
  /**
14
32
  * Creates and publishes an event from input data.
15
33
  */
16
- export declare function busPublishEvent<TPayload>(input: EventInput<TPayload>, options: PublishOptions, emitter: EventEmitter, registry: EventRegistry, busMiddleware: RegisteredEventMiddleware[], requireRegistration: boolean, ensureUsable: () => void, notify: (e: EventBusEvent) => void): Promise<EventPublishResult<Event<TPayload>>>;
34
+ export declare function busPublishEvent<TPayload>(input: EventInput<TPayload>, options: PublishOptions, deps: BusPublishDependencies): Promise<EventPublishResult<Event<TPayload>>>;
17
35
  //# sourceMappingURL=eventBus.publish.d.ts.map
@@ -1,78 +1,124 @@
1
1
  /**
2
2
  * Event bus publish methods for Zudojs.
3
3
  */
4
- import { createEvent } from "../eventTypes/eventDefinition.type.js";
5
- import { EventEmitter } from "../eventEmitter/eventEmitter.core.js";
6
- import { EventRegistry } from "../eventRegistry/eventRegistry.store.js";
7
- import { EventDispatchAbortedError, EventError, toEventError, } from "../eventErrors/eventError.base.js";
4
+ import { createEvent, isEvent } from "../eventTypes/eventDefinition.type.js";
5
+ import { EventDispatchAbortedError, EventTypeNotFoundError, InvalidEventError, } from "../eventErrors/eventError.base.js";
8
6
  import { createEventMiddlewareContext } from "../eventMiddleware/eventMiddleware.helper.js";
9
7
  import { executeEventMiddlewarePipeline } from "../eventMiddleware/eventMiddleware.pipeline.js";
10
8
  import { registerMiddlewareItem } from "./eventBus.registration.js";
9
+ /**
10
+ * Determines whether a middleware pipeline result is the emit
11
+ * result produced by the terminal handler dispatch.
12
+ */
13
+ export function isEventEmitResult(value) {
14
+ return (typeof value === "object" &&
15
+ value !== null &&
16
+ typeof value.handled === "boolean" &&
17
+ Array.isArray(value.results) &&
18
+ Array.isArray(value.errors));
19
+ }
11
20
  /**
12
21
  * Publishes an event through the bus.
13
22
  */
14
- export async function busPublish(event, options, emitter, registry, busMiddleware, requireRegistration, ensureUsable, notify) {
15
- ensureUsable();
23
+ export async function busPublish(event, options, deps) {
24
+ deps.ensureUsable();
25
+ if (!isEvent(event)) {
26
+ throw new InvalidEventError("publish() requires an Event (use publishEvent() for event input).");
27
+ }
16
28
  if (options.signal?.aborted) {
17
29
  throw new EventDispatchAbortedError("Event dispatch was aborted.", {
18
- eventType: event?.type,
19
- eventId: event?.id,
20
- });
21
- }
22
- if (requireRegistration && !registry.has(event.type)) {
23
- throw new EventError(`Event type "${event.type}" is not registered.`, {
24
30
  eventType: event.type,
25
31
  eventId: event.id,
26
32
  });
27
33
  }
34
+ if (deps.requireRegistration && !deps.registry.has(event.type)) {
35
+ throw new EventTypeNotFoundError(event.type);
36
+ }
28
37
  const allMiddleware = [
29
- ...busMiddleware,
30
- ...(options.middleware ?? []).map((m, index) => registerMiddlewareItem(m, index + busMiddleware.length)),
38
+ ...deps.busMiddleware,
39
+ ...(options.middleware ?? []).map((m, index) => registerMiddlewareItem(m, index, "publish-mw")),
31
40
  ];
32
41
  const middlewareContext = createEventMiddlewareContext(event, {
33
42
  signal: options.signal,
34
43
  metadata: options.metadata,
35
44
  });
45
+ /**
46
+ * The emit result is captured here, at the terminal, rather
47
+ * than read back from the pipeline's return value: a middleware
48
+ * that awaits next() and returns nothing (or something else)
49
+ * has still dispatched the handlers, and their outcome must not
50
+ * be reported as a short-circuit.
51
+ */
52
+ let emitResult;
36
53
  const terminal = async () => {
37
- return emitter.emit(event, {
54
+ const result = await deps.emitter.emit(event, {
38
55
  mode: options.mode,
39
56
  errorMode: options.errorMode,
40
57
  signal: options.signal,
41
58
  metadata: options.metadata,
42
59
  });
60
+ emitResult = result;
61
+ return result;
43
62
  };
44
- let result;
45
- let middlewareResult;
63
+ let middlewareExecutions;
46
64
  if (allMiddleware.length > 0) {
47
65
  const pipelineResult = await executeEventMiddlewarePipeline(allMiddleware, middlewareContext, terminal);
48
- result = pipelineResult.result;
49
- middlewareResult = {
50
- result: pipelineResult.result,
51
- executions: pipelineResult.executions,
52
- };
66
+ middlewareExecutions = pipelineResult.executions;
53
67
  }
54
68
  else {
55
- result = await terminal();
69
+ await terminal();
56
70
  }
57
- notify({
71
+ deps.notify({
58
72
  type: "published",
59
73
  event,
60
74
  timestamp: new Date(),
61
75
  });
76
+ if (emitResult === undefined) {
77
+ /**
78
+ * A middleware short-circuited (did not call next()); no
79
+ * handler ran.
80
+ */
81
+ return {
82
+ event,
83
+ handled: false,
84
+ handlerCount: 0,
85
+ succeeded: 0,
86
+ failed: 0,
87
+ results: [],
88
+ errors: [],
89
+ shortCircuited: true,
90
+ middlewareExecutions,
91
+ };
92
+ }
93
+ if (deps.onError && emitResult.errors.length > 0) {
94
+ for (const error of emitResult.errors) {
95
+ try {
96
+ deps.onError(error, { source: "handler", event });
97
+ }
98
+ catch {
99
+ /**
100
+ * A failing error hook must not break publishing.
101
+ */
102
+ }
103
+ }
104
+ }
62
105
  return {
63
- event,
64
- handled: result.handled,
65
- handlerCount: result.results.length,
66
- results: result.results.map((execution) => execution.result),
67
- errors: result.errors,
68
- middlewareExecutions: middlewareResult?.executions,
106
+ event: emitResult.event,
107
+ handled: emitResult.handled,
108
+ handlerCount: emitResult.results.length,
109
+ succeeded: emitResult.succeeded,
110
+ failed: emitResult.failed,
111
+ results: emitResult.results.map((execution) => execution.result),
112
+ errors: emitResult.errors,
113
+ shortCircuited: false,
114
+ middlewareExecutions,
69
115
  };
70
116
  }
71
117
  /**
72
118
  * Creates and publishes an event from input data.
73
119
  */
74
- export async function busPublishEvent(input, options, emitter, registry, busMiddleware, requireRegistration, ensureUsable, notify) {
120
+ export async function busPublishEvent(input, options, deps) {
75
121
  const event = createEvent(input);
76
- return busPublish(event, options, emitter, registry, busMiddleware, requireRegistration, ensureUsable, notify);
122
+ return busPublish(event, options, deps);
77
123
  }
78
124
  //# sourceMappingURL=eventBus.publish.js.map