@zudojs/events 1.1.0 → 1.3.0

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.md CHANGED
@@ -71,6 +71,9 @@ CREATED ──(first use / start)──▶ ACTIVE ◀──(start)── STOPPED
71
71
  - `stop()` moves the bus to `STOPPED`; publishing or subscribing then throws
72
72
  `EventBusStoppedError` until `start()` is called. Handlers and definitions are kept.
73
73
  - `dispose()` is final; every operation throws `EventBusDisposedError`.
74
+ - Both errors are defined in `@zudojs/errors` (as `EventError` subclasses) and
75
+ re-exported from `@zudojs/events`, so `instanceof` works whichever package you
76
+ import them from.
74
77
 
75
78
  ## Publish results
76
79
 
@@ -90,6 +93,33 @@ In the default `CONTINUE` error mode handler failures are collected in
90
93
  `EventHandlerError` rejects the publish. Errors thrown by a middleware itself are
91
94
  wrapped as `EventMiddlewareError`; handler errors and aborts pass through unwrapped.
92
95
 
96
+ ## Middleware
97
+
98
+ `bus.use()` accepts everything the `middleware` constructor option does: a
99
+ middleware function or `{ handle }` object, or a registered middleware from
100
+ `createEventMiddleware()` or a builder helper (`validateEventMiddleware`,
101
+ `beforeEvent`, `aroundEvent`, `timingEventMiddleware`, …). It returns a function
102
+ that removes the middleware again.
103
+
104
+ ```typescript
105
+ const remove = bus.use(validateEventMiddleware((event) => event.payload != null));
106
+ ```
107
+
108
+ ## Timeouts and aborts
109
+
110
+ A handler registered with `timeoutMs` fails with `EventTimeoutError` when it
111
+ does not settle in time, and the `context.signal` it received is aborted with
112
+ that error as its `reason`, so a handler that listens to the signal can stop
113
+ its work. Timing out one handler does not abort the dispatch or other handlers.
114
+
115
+ Pass `signal` to `publish()` to abort a sequential dispatch. The publish rejects
116
+ with `EventDispatchAbortedError` (carrying the partial `results` and `errors`)
117
+ when the signal is aborted before a handler starts **or while any handler is
118
+ running — including the last or only one**, even if that handler then returns
119
+ normally. In `PARALLEL` mode every handler has already started, so only a
120
+ signal that is aborted before dispatch begins rejects; handlers can still
121
+ observe `context.signal`.
122
+
93
123
  ## Registry
94
124
 
95
125
  `bus.register(defineEvent("order.placed"))` records a definition; with
@@ -8,8 +8,8 @@ import type { EventSubscription } from "../eventSubscription/eventSubscription.c
8
8
  import { EventEmitter } from "../eventEmitter/eventEmitter.core.js";
9
9
  import { EventRegistry } from "../eventRegistry/eventRegistry.store.js";
10
10
  import { EventError } from "../eventErrors/eventError.base.js";
11
- import type { EventMiddlewareLike, EventMiddlewareOptions } from "../eventMiddleware/eventMiddleware.type.js";
12
- import type { EventBusOptions, PublishOptions, EventPublishResult, EventBusListener } from "./eventBus.type.js";
11
+ import type { EventMiddlewareOptions } from "../eventMiddleware/eventMiddleware.type.js";
12
+ import type { EventBusMiddlewareItem, EventBusOptions, PublishOptions, EventPublishResult, EventBusListener } from "./eventBus.type.js";
13
13
  import { EventBusState } from "./eventBus.type.js";
14
14
  export { EventBusState } from "./eventBus.type.js";
15
15
  /**
@@ -40,7 +40,12 @@ export declare class EventBus {
40
40
  once<TEvent extends Event = Event>(eventType: EventTypePattern, handler: EventHandlerLike<TEvent>, options?: Omit<EventHandlerOptions, "eventType" | "once">): EventSubscription;
41
41
  onAny<TEvent extends Event = Event>(handler: EventHandlerLike<TEvent>, options?: Omit<EventHandlerOptions, "eventType">): EventSubscription;
42
42
  off(subscription: EventSubscription): boolean;
43
- use(middleware: EventMiddlewareLike, options?: EventMiddlewareOptions): () => void;
43
+ /**
44
+ * Adds bus middleware: a middleware function or object, or a
45
+ * registered middleware from createEventMiddleware() or a builder
46
+ * helper such as validateEventMiddleware().
47
+ */
48
+ use(middleware: EventBusMiddlewareItem, options?: EventMiddlewareOptions): () => void;
44
49
  publish<TEvent extends Event>(event: TEvent, options?: PublishOptions): Promise<EventPublishResult<TEvent>>;
45
50
  publishEvent<TPayload>(input: EventInput<TPayload>, options?: PublishOptions): Promise<EventPublishResult<Event<TPayload>>>;
46
51
  /**
@@ -7,6 +7,7 @@ import { EventErrorMode } from "../eventEmitter/eventEmitter.type.js";
7
7
  import { EventRegistry } from "../eventRegistry/eventRegistry.store.js";
8
8
  import { normalizeRegistryEventType } from "../eventRegistry/eventRegistry.registration.js";
9
9
  import { EventBusDisposedError, EventBusStoppedError, EventError, toEventError, } from "../eventErrors/eventError.base.js";
10
+ import { warnObserverError } from "../eventErrors/eventWarning.helper.js";
10
11
  import { EventBusState } from "./eventBus.type.js";
11
12
  export { EventBusState } from "./eventBus.type.js";
12
13
  import { busOn, busOnce, busOnAny, busOff, busUse, registerMiddlewareItem, } from "./eventBus.registration.js";
@@ -34,6 +35,7 @@ export class EventBus {
34
35
  this.registry = new EventRegistry({
35
36
  ...options.registry,
36
37
  maxHandlersPerPattern: options.emitter?.maxListeners,
38
+ enforceHandlerLimit: options.emitter?.enforceHandlerLimit,
37
39
  onWarning: options.onWarning,
38
40
  onError: options.onError
39
41
  ? (error, context) => options.onError?.(error, {
@@ -98,6 +100,11 @@ export class EventBus {
98
100
  off(subscription) {
99
101
  return busOff(this.emitter, subscription, () => this.ensureNotDisposed());
100
102
  }
103
+ /**
104
+ * Adds bus middleware: a middleware function or object, or a
105
+ * registered middleware from createEventMiddleware() or a builder
106
+ * helper such as validateEventMiddleware().
107
+ */
101
108
  use(middleware, options = {}) {
102
109
  this.ensureNotDisposed();
103
110
  return busUse(this.busMiddleware, middleware, options);
@@ -235,16 +242,23 @@ export class EventBus {
235
242
  catch (error) {
236
243
  /**
237
244
  * Observers must never be able to break event bus
238
- * operations; failures go to the onError hook.
245
+ * operations; failures go to the onError hook, or to Node's
246
+ * process warning channel when no hook is configured, so a
247
+ * broken observer is never silently discarded.
239
248
  */
240
- try {
241
- this.options.onError?.(error, {
242
- source: "observer",
243
- event: event.event,
244
- });
249
+ if (this.options.onError) {
250
+ try {
251
+ this.options.onError(error, {
252
+ source: "observer",
253
+ event: event.event,
254
+ });
255
+ }
256
+ catch {
257
+ // Ignore failures of the error hook itself.
258
+ }
245
259
  }
246
- catch {
247
- // Ignore failures of the error hook itself.
260
+ else {
261
+ warnObserverError(error, "An event bus observer", this.listeners);
248
262
  }
249
263
  }
250
264
  }
@@ -5,7 +5,7 @@ import type { Event, EventDefinition, EventType } from "../eventTypes/eventDefin
5
5
  import type { EventTypePattern } from "../eventTypes/eventType.type.js";
6
6
  import type { EventHandlerLike, EventHandlerOptions } from "../eventHandler/eventHandler.core.js";
7
7
  import type { EventSubscription } from "../eventSubscription/eventSubscription.core.js";
8
- import type { EventMiddlewareLike, EventMiddlewareOptions, RegisteredEventMiddleware } from "../eventMiddleware/eventMiddleware.type.js";
8
+ import type { EventMiddlewareOptions, RegisteredEventMiddleware } from "../eventMiddleware/eventMiddleware.type.js";
9
9
  import type { RegisteredEventDefinition } from "../eventRegistry/eventRegistry.type.js";
10
10
  import type { EventBusMiddlewareItem } from "./eventBus.type.js";
11
11
  /**
@@ -39,8 +39,15 @@ export declare function busOff(emitter: {
39
39
  * Adds middleware to the bus. The middleware is validated
40
40
  * eagerly (invalid middleware or a non-finite priority throw
41
41
  * here, not on the next publish).
42
+ *
43
+ * Accepts the same items as the `middleware` constructor option: a
44
+ * plain middleware function or object, or a registered middleware
45
+ * made by createEventMiddleware() or a builder helper such as
46
+ * validateEventMiddleware(). For a registered middleware its own id,
47
+ * description, priority and enabled flag are kept unless `options`
48
+ * overrides them.
42
49
  */
43
- export declare function busUse(busMiddleware: RegisteredEventMiddleware[], middleware: EventMiddlewareLike, options?: EventMiddlewareOptions): () => void;
50
+ export declare function busUse(busMiddleware: RegisteredEventMiddleware[], middleware: EventBusMiddlewareItem, options?: EventMiddlewareOptions): () => void;
44
51
  /**
45
52
  * Determines whether a value is an already registered middleware
46
53
  * (created by createEventMiddleware or a builder helper).
@@ -42,9 +42,23 @@ export function busOff(emitter, subscription, ensureNotDisposed) {
42
42
  * Adds middleware to the bus. The middleware is validated
43
43
  * eagerly (invalid middleware or a non-finite priority throw
44
44
  * here, not on the next publish).
45
+ *
46
+ * Accepts the same items as the `middleware` constructor option: a
47
+ * plain middleware function or object, or a registered middleware
48
+ * made by createEventMiddleware() or a builder helper such as
49
+ * validateEventMiddleware(). For a registered middleware its own id,
50
+ * description, priority and enabled flag are kept unless `options`
51
+ * overrides them.
45
52
  */
46
53
  export function busUse(busMiddleware, middleware, options = {}) {
47
- const registered = createEventMiddleware(middleware, options);
54
+ const registered = isRegisteredEventMiddleware(middleware)
55
+ ? createEventMiddleware(middleware.middleware, {
56
+ id: options.id ?? middleware.id,
57
+ description: options.description ?? middleware.description,
58
+ priority: options.priority ?? middleware.priority,
59
+ enabled: options.enabled ?? middleware.enabled,
60
+ })
61
+ : createEventMiddleware(middleware, options);
48
62
  busMiddleware.push(registered);
49
63
  return () => {
50
64
  const idx = busMiddleware.indexOf(registered);
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Event bus type definitions for Zudojs.
3
3
  */
4
+ import type { EventHandlerError } from "@zudojs/errors";
4
5
  import type { Event } from "../eventTypes/eventDefinition.type.js";
5
6
  import type { EventEmitterMode, EventErrorMode } from "../eventEmitter/eventEmitter.type.js";
6
7
  import type { EventMiddlewareLike, RegisteredEventMiddleware } from "../eventMiddleware/eventMiddleware.type.js";
@@ -36,6 +37,11 @@ export interface EventBusOptions {
36
37
  * emitted (0 disables). Defaults to 100.
37
38
  */
38
39
  readonly maxListeners?: number;
40
+ /**
41
+ * Refuse a registration that would exceed `maxListeners` instead of
42
+ * warning about it. Defaults to `false`.
43
+ */
44
+ readonly enforceHandlerLimit?: boolean;
39
45
  };
40
46
  readonly registry?: {
41
47
  readonly allowDuplicateDefinitions?: boolean;
@@ -88,7 +94,11 @@ export interface EventPublishResult<TEvent extends Event = Event> {
88
94
  */
89
95
  readonly failed: number;
90
96
  readonly results: readonly unknown[];
91
- readonly errors: readonly unknown[];
97
+ /**
98
+ * Handler failures, each wrapped as EventHandlerError (its
99
+ * `cause` is the raw thrown value).
100
+ */
101
+ readonly errors: readonly EventHandlerError[];
92
102
  /**
93
103
  * True when a middleware did not call next(), so no handler ran.
94
104
  */
@@ -32,6 +32,7 @@ export class EventEmitter {
32
32
  options.store ??
33
33
  new EventRegistry({
34
34
  maxHandlersPerPattern: options.maxListeners,
35
+ enforceHandlerLimit: options.enforceHandlerLimit,
35
36
  onWarning: options.onWarning,
36
37
  });
37
38
  }
@@ -8,11 +8,12 @@
8
8
  */
9
9
  import type { Event } from "../eventTypes/eventDefinition.type.js";
10
10
  import type { EventHandlerContext, RegisteredEventHandler } from "../eventHandler/eventHandler.core.js";
11
+ import { type EventHandlerError } from "../eventErrors/eventError.base.js";
11
12
  import type { EventHandlerExecutionResult } from "./eventEmitter.type.js";
12
13
  import { EventErrorMode } from "./eventEmitter.type.js";
13
14
  import type { DispatchHooks } from "./eventEmitter.sequential.js";
14
15
  /**
15
16
  * Executes handlers concurrently.
16
17
  */
17
- export declare function emitParallel<TEvent extends Event>(handlers: readonly RegisteredEventHandler<TEvent>[], event: TEvent, context: EventHandlerContext<TEvent>, errorMode: EventErrorMode, results: EventHandlerExecutionResult[], errors: unknown[], hooks: DispatchHooks): Promise<void>;
18
+ export declare function emitParallel<TEvent extends Event>(handlers: readonly RegisteredEventHandler<TEvent>[], event: TEvent, context: EventHandlerContext<TEvent>, errorMode: EventErrorMode, results: EventHandlerExecutionResult[], errors: EventHandlerError[], hooks: DispatchHooks): Promise<void>;
18
19
  //# sourceMappingURL=eventEmitter.parallel.d.ts.map
@@ -7,7 +7,7 @@
7
7
  * aborted when dispatch begins rejects the emit.
8
8
  */
9
9
  import { executeRegisteredEventHandler } from "../eventHandler/eventHandler.core.js";
10
- import { createEventHandlerError } from "../eventErrors/eventError.base.js";
10
+ import { createEventHandlerError, } from "../eventErrors/eventError.base.js";
11
11
  import { EventErrorMode } from "./eventEmitter.type.js";
12
12
  import { createAbortError } from "./eventEmitter.abort.js";
13
13
  /**
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import type { Event } from "../eventTypes/eventDefinition.type.js";
5
5
  import type { EventHandlerContext, RegisteredEventHandler } from "../eventHandler/eventHandler.core.js";
6
+ import { type EventHandlerError } from "../eventErrors/eventError.base.js";
6
7
  import type { EventHandlerExecutionResult } from "./eventEmitter.type.js";
7
8
  import { EventErrorMode } from "./eventEmitter.type.js";
8
9
  /**
@@ -23,6 +24,11 @@ export interface DispatchHooks {
23
24
  }
24
25
  /**
25
26
  * Executes handlers sequentially.
27
+ *
28
+ * Rejects with EventDispatchAbortedError when the dispatch signal is
29
+ * aborted before a handler starts or while any handler — including
30
+ * the last or only one — is running, even if that handler then
31
+ * returns normally.
26
32
  */
27
- export declare function emitSequential<TEvent extends Event>(handlers: readonly RegisteredEventHandler<TEvent>[], event: TEvent, context: EventHandlerContext<TEvent>, errorMode: EventErrorMode, results: EventHandlerExecutionResult[], errors: unknown[], hooks: DispatchHooks): Promise<void>;
33
+ export declare function emitSequential<TEvent extends Event>(handlers: readonly RegisteredEventHandler<TEvent>[], event: TEvent, context: EventHandlerContext<TEvent>, errorMode: EventErrorMode, results: EventHandlerExecutionResult[], errors: EventHandlerError[], hooks: DispatchHooks): Promise<void>;
28
34
  //# sourceMappingURL=eventEmitter.sequential.d.ts.map
@@ -2,11 +2,16 @@
2
2
  * Sequential event handler dispatch for Zudojs.
3
3
  */
4
4
  import { executeRegisteredEventHandler } from "../eventHandler/eventHandler.core.js";
5
- import { createEventHandlerError } from "../eventErrors/eventError.base.js";
5
+ import { createEventHandlerError, } from "../eventErrors/eventError.base.js";
6
6
  import { EventErrorMode } from "./eventEmitter.type.js";
7
7
  import { createAbortError } from "./eventEmitter.abort.js";
8
8
  /**
9
9
  * Executes handlers sequentially.
10
+ *
11
+ * Rejects with EventDispatchAbortedError when the dispatch signal is
12
+ * aborted before a handler starts or while any handler — including
13
+ * the last or only one — is running, even if that handler then
14
+ * returns normally.
10
15
  */
11
16
  export async function emitSequential(handlers, event, context, errorMode, results, errors, hooks) {
12
17
  for (const handler of handlers) {
@@ -49,5 +54,12 @@ export async function emitSequential(handlers, event, context, errorMode, result
49
54
  }
50
55
  }
51
56
  }
57
+ // The check at the top of the loop only runs before the *next*
58
+ // handler, so an abort during the last (or only) handler used to
59
+ // resolve as a normal, successful dispatch — while the same abort
60
+ // with a handler still to come rejected.
61
+ if (context.signal.aborted) {
62
+ throw createAbortError(event, results, errors);
63
+ }
52
64
  }
53
65
  //# sourceMappingURL=eventEmitter.sequential.js.map
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Event emitter type definitions for Zudojs.
3
3
  */
4
+ import type { EventHandlerError } from "@zudojs/errors";
4
5
  import type { Event } from "../eventTypes/eventDefinition.type.js";
5
6
  import type { RegisteredEventHandler } from "../eventHandler/eventHandler.core.js";
6
7
  import type { EventSubscription } from "../eventSubscription/eventSubscription.core.js";
@@ -42,6 +43,11 @@ export interface EventEmitterOptions {
42
43
  * Defaults to 100.
43
44
  */
44
45
  readonly maxListeners?: number;
46
+ /**
47
+ * Refuse a registration that would exceed `maxListeners` instead of warning
48
+ * about it. Only applies to the private store. Defaults to `false`.
49
+ */
50
+ readonly enforceHandlerLimit?: boolean;
45
51
  /**
46
52
  * Receives leak warnings. Only applies to the private store.
47
53
  * Defaults to `process.emitWarning` (type `ZudojsEventsWarning`).
@@ -82,7 +88,7 @@ export interface EventEmitResult<TEvent extends Event = Event> {
82
88
  * Handler failures wrapped as EventHandlerError (cause holds the
83
89
  * raw thrown value).
84
90
  */
85
- readonly errors: readonly unknown[];
91
+ readonly errors: readonly EventHandlerError[];
86
92
  /**
87
93
  * Number of handlers that completed successfully.
88
94
  */
@@ -2,11 +2,12 @@
2
2
  * @zudojs/events/eventErrors/eventError.base
3
3
  *
4
4
  * Event error types are centralized in @zudojs/errors and
5
- * re-exported here. A few event-bus specific errors that the
6
- * errors package does not define yet live in this file.
5
+ * re-exported here, including EventBusStoppedError and
6
+ * EventBusDisposedError. EventDispatchAbortedError is extended here
7
+ * to carry the partial results of an aborted dispatch.
7
8
  */
8
- import { EventError, EventDispatchAbortedError as BaseEventDispatchAbortedError } from "@zudojs/errors";
9
- export { EventError, createEventError, isEventError, toEventError, EventPublishError, InvalidEventError, EventTypeNotFoundError, EventHandlerError, createEventHandlerError, EventHandlerNotFoundError, DuplicateEventHandlerError, DuplicateEventDefinitionError, EventDefinitionNotFoundError, EventEmitterDisposedError, EventRegistryDisposedError, EventSubscriptionClosedError, EventTimeoutError, EventMiddlewareError, EventSerializationError, EventDeserializationError, } from "@zudojs/errors";
9
+ import { EventDispatchAbortedError as BaseEventDispatchAbortedError } from "@zudojs/errors";
10
+ export { EventError, createEventError, isEventError, toEventError, EventPublishError, InvalidEventError, EventTypeNotFoundError, EventHandlerError, createEventHandlerError, EventHandlerNotFoundError, DuplicateEventHandlerError, DuplicateEventDefinitionError, EventDefinitionNotFoundError, EventEmitterDisposedError, EventRegistryDisposedError, EventSubscriptionClosedError, EventListenerLimitExceededError, EventTimeoutError, EventMiddlewareError, EventSerializationError, EventDeserializationError, EventBusDisposedError, EventBusStoppedError, } from "@zudojs/errors";
10
11
  /**
11
12
  * Options for EventDispatchAbortedError.
12
13
  */
@@ -32,24 +33,4 @@ export declare class EventDispatchAbortedError extends BaseEventDispatchAbortedE
32
33
  readonly errors: readonly unknown[];
33
34
  constructor(message?: string, options?: EventDispatchAbortedErrorOptions);
34
35
  }
35
- /**
36
- * Error thrown when an EventBus is used after dispose().
37
- */
38
- export declare class EventBusDisposedError extends EventError {
39
- constructor();
40
- }
41
- /**
42
- * Error thrown when publishing or subscribing on a stopped
43
- * EventBus. Call start() to resume.
44
- */
45
- export declare class EventBusStoppedError extends EventError {
46
- constructor(operation: string);
47
- }
48
- /**
49
- * Error thrown when the listener limit of an emitter or registry
50
- * is exceeded and the limit is configured to be enforced.
51
- */
52
- export declare class EventListenerLimitExceededError extends EventError {
53
- constructor(pattern: string, limit: number);
54
- }
55
36
  //# sourceMappingURL=eventError.base.d.ts.map
@@ -2,11 +2,12 @@
2
2
  * @zudojs/events/eventErrors/eventError.base
3
3
  *
4
4
  * Event error types are centralized in @zudojs/errors and
5
- * re-exported here. A few event-bus specific errors that the
6
- * errors package does not define yet live in this file.
5
+ * re-exported here, including EventBusStoppedError and
6
+ * EventBusDisposedError. EventDispatchAbortedError is extended here
7
+ * to carry the partial results of an aborted dispatch.
7
8
  */
8
- import { ErrorCode, EventError, EventDispatchAbortedError as BaseEventDispatchAbortedError, } from "@zudojs/errors";
9
- export { EventError, createEventError, isEventError, toEventError, EventPublishError, InvalidEventError, EventTypeNotFoundError, EventHandlerError, createEventHandlerError, EventHandlerNotFoundError, DuplicateEventHandlerError, DuplicateEventDefinitionError, EventDefinitionNotFoundError, EventEmitterDisposedError, EventRegistryDisposedError, EventSubscriptionClosedError, EventTimeoutError, EventMiddlewareError, EventSerializationError, EventDeserializationError, } from "@zudojs/errors";
9
+ import { EventDispatchAbortedError as BaseEventDispatchAbortedError } from "@zudojs/errors";
10
+ export { EventError, createEventError, isEventError, toEventError, EventPublishError, InvalidEventError, EventTypeNotFoundError, EventHandlerError, createEventHandlerError, EventHandlerNotFoundError, DuplicateEventHandlerError, DuplicateEventDefinitionError, EventDefinitionNotFoundError, EventEmitterDisposedError, EventRegistryDisposedError, EventSubscriptionClosedError, EventListenerLimitExceededError, EventTimeoutError, EventMiddlewareError, EventSerializationError, EventDeserializationError, EventBusDisposedError, EventBusStoppedError, } from "@zudojs/errors";
10
11
  /**
11
12
  * Error thrown when event dispatch is aborted through an
12
13
  * AbortSignal. Carries the partial results and errors gathered
@@ -24,46 +25,4 @@ export class EventDispatchAbortedError extends BaseEventDispatchAbortedError {
24
25
  this.errors = Object.freeze([...(options.errors ?? [])]);
25
26
  }
26
27
  }
27
- /**
28
- * Error thrown when an EventBus is used after dispose().
29
- */
30
- export class EventBusDisposedError extends EventError {
31
- constructor() {
32
- super("Event bus has already been disposed.", {
33
- code: ErrorCode.LIFECYCLE_DISPOSED,
34
- statusCode: 500,
35
- expose: false,
36
- isOperational: false,
37
- });
38
- }
39
- }
40
- /**
41
- * Error thrown when publishing or subscribing on a stopped
42
- * EventBus. Call start() to resume.
43
- */
44
- export class EventBusStoppedError extends EventError {
45
- constructor(operation) {
46
- super(`Cannot ${operation} on a stopped event bus. Call start() first.`, {
47
- code: ErrorCode.LIFECYCLE_STATE,
48
- statusCode: 500,
49
- expose: false,
50
- isOperational: true,
51
- metadata: { operation },
52
- });
53
- }
54
- }
55
- /**
56
- * Error thrown when the listener limit of an emitter or registry
57
- * is exceeded and the limit is configured to be enforced.
58
- */
59
- export class EventListenerLimitExceededError extends EventError {
60
- constructor(pattern, limit) {
61
- super(`Listener limit of ${limit} exceeded for event pattern "${pattern}".`, {
62
- code: ErrorCode.LIFECYCLE_STATE,
63
- statusCode: 500,
64
- expose: false,
65
- metadata: { pattern, limit },
66
- });
67
- }
68
- }
69
28
  //# sourceMappingURL=eventError.base.js.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Process-level warning sink for @zudojs/events.
3
+ *
4
+ * @module eventErrors/eventWarning
5
+ */
6
+ /** Warning code for a registry or emitter handler-limit breach. */
7
+ export declare const EVENT_HANDLER_LIMIT_WARNING_CODE = "ZUDOJS_EVENTS_HANDLER_LIMIT";
8
+ /** Warning code for a bus or registry observer that threw. */
9
+ export declare const EVENT_OBSERVER_ERROR_WARNING_CODE = "ZUDOJS_EVENTS_OBSERVER_ERROR";
10
+ /**
11
+ * Emits a diagnostic on Node's process warning channel — the same one
12
+ * `EventEmitter` uses for `MaxListenersExceededWarning`, so it honours
13
+ * `--no-warnings` and `process.on("warning")` instead of writing to the
14
+ * console. A no-op where `process.emitWarning` is unavailable.
15
+ *
16
+ * @param message - The warning text, emitted with a package prefix.
17
+ * @param code - The machine-readable warning code.
18
+ */
19
+ export declare function emitEventsWarning(message: string, code: string): void;
20
+ /**
21
+ * Default sink for an observer that threw: with no `onError` hook
22
+ * configured the failure would otherwise be swallowed entirely.
23
+ *
24
+ * Emitted at most once per scope, the way the handler-limit warning is
25
+ * emitted at most once per pattern, so a broken observer on a busy bus
26
+ * reports itself without flooding the warning channel.
27
+ *
28
+ * @param error - The value the observer threw.
29
+ * @param source - A short description of which observer channel failed.
30
+ * @param scope - The bus or registry the observer belongs to.
31
+ */
32
+ export declare function warnObserverError(error: unknown, source: string, scope: object): void;
33
+ //# sourceMappingURL=eventWarning.helper.d.ts.map
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Process-level warning sink for @zudojs/events.
3
+ *
4
+ * @module eventErrors/eventWarning
5
+ */
6
+ /** Warning code for a registry or emitter handler-limit breach. */
7
+ export const EVENT_HANDLER_LIMIT_WARNING_CODE = "ZUDOJS_EVENTS_HANDLER_LIMIT";
8
+ /** Warning code for a bus or registry observer that threw. */
9
+ export const EVENT_OBSERVER_ERROR_WARNING_CODE = "ZUDOJS_EVENTS_OBSERVER_ERROR";
10
+ /**
11
+ * Emits a diagnostic on Node's process warning channel — the same one
12
+ * `EventEmitter` uses for `MaxListenersExceededWarning`, so it honours
13
+ * `--no-warnings` and `process.on("warning")` instead of writing to the
14
+ * console. A no-op where `process.emitWarning` is unavailable.
15
+ *
16
+ * @param message - The warning text, emitted with a package prefix.
17
+ * @param code - The machine-readable warning code.
18
+ */
19
+ export function emitEventsWarning(message, code) {
20
+ const emit = globalThis.process?.emitWarning;
21
+ emit?.(`[@zudojs/events] ${message}`, {
22
+ type: "ZudojsEventsWarning",
23
+ code,
24
+ });
25
+ }
26
+ /** Scopes that have already reported a failing observer. */
27
+ const warnedObserverScopes = new WeakSet();
28
+ /**
29
+ * Default sink for an observer that threw: with no `onError` hook
30
+ * configured the failure would otherwise be swallowed entirely.
31
+ *
32
+ * Emitted at most once per scope, the way the handler-limit warning is
33
+ * emitted at most once per pattern, so a broken observer on a busy bus
34
+ * reports itself without flooding the warning channel.
35
+ *
36
+ * @param error - The value the observer threw.
37
+ * @param source - A short description of which observer channel failed.
38
+ * @param scope - The bus or registry the observer belongs to.
39
+ */
40
+ export function warnObserverError(error, source, scope) {
41
+ if (warnedObserverScopes.has(scope))
42
+ return;
43
+ warnedObserverScopes.add(scope);
44
+ const detail = error instanceof Error ? error.message : String(error);
45
+ emitEventsWarning(`${source} threw and was ignored: ${detail}. ` +
46
+ "Configure the `onError` option to handle observer failures. " +
47
+ "This warning is emitted once.", EVENT_OBSERVER_ERROR_WARNING_CODE);
48
+ }
49
+ //# sourceMappingURL=eventWarning.helper.js.map
@@ -172,8 +172,15 @@ export declare function executeEventHandler<TEvent extends Event>(handler: Event
172
172
  /**
173
173
  * Executes a registered handler, applying its timeout when one
174
174
  * is configured. A timed-out execution rejects with
175
- * EventTimeoutError; the underlying handler keeps running but its
176
- * eventual result is ignored.
175
+ * EventTimeoutError and aborts the `context.signal` the handler
176
+ * received (its `reason` is that EventTimeoutError), so a handler
177
+ * that observes the signal can stop its work. A handler that
178
+ * ignores the signal keeps running, but its eventual result is
179
+ * ignored.
180
+ *
181
+ * The handler's signal also follows the dispatch signal: aborting
182
+ * the publish aborts it too. Timing out one handler does not abort
183
+ * the dispatch or any other handler.
177
184
  */
178
185
  export declare function executeRegisteredEventHandler<TEvent extends Event>(registration: RegisteredEventHandler<TEvent>, event: TEvent, context: EventHandlerContext<TEvent>): Promise<EventHandlerResult>;
179
186
  /**
@@ -117,8 +117,15 @@ export async function executeEventHandler(handler, event, context) {
117
117
  /**
118
118
  * Executes a registered handler, applying its timeout when one
119
119
  * is configured. A timed-out execution rejects with
120
- * EventTimeoutError; the underlying handler keeps running but its
121
- * eventual result is ignored.
120
+ * EventTimeoutError and aborts the `context.signal` the handler
121
+ * received (its `reason` is that EventTimeoutError), so a handler
122
+ * that observes the signal can stop its work. A handler that
123
+ * ignores the signal keeps running, but its eventual result is
124
+ * ignored.
125
+ *
126
+ * The handler's signal also follows the dispatch signal: aborting
127
+ * the publish aborts it too. Timing out one handler does not abort
128
+ * the dispatch or any other handler.
122
129
  */
123
130
  export async function executeRegisteredEventHandler(registration, event, context) {
124
131
  const timeoutMs = registration.timeoutMs;
@@ -126,17 +133,24 @@ export async function executeRegisteredEventHandler(registration, event, context
126
133
  return executeEventHandler(registration.handler, event, context);
127
134
  }
128
135
  let timer;
136
+ const deadline = new AbortController();
137
+ const handlerContext = Object.freeze({
138
+ ...context,
139
+ signal: AbortSignal.any([context.signal, deadline.signal]),
140
+ });
129
141
  const timeout = new Promise((_resolve, reject) => {
130
142
  timer = setTimeout(() => {
131
- reject(new EventTimeoutError(timeoutMs, {
143
+ const error = new EventTimeoutError(timeoutMs, {
132
144
  eventType: event.type,
133
145
  eventId: event.id,
134
- }));
146
+ });
147
+ deadline.abort(error);
148
+ reject(error);
135
149
  }, timeoutMs);
136
150
  });
137
151
  try {
138
152
  return await Promise.race([
139
- executeEventHandler(registration.handler, event, context),
153
+ executeEventHandler(registration.handler, event, handlerContext),
140
154
  timeout,
141
155
  ]);
142
156
  }
@@ -18,7 +18,9 @@ export declare function registryDispose(disposed: boolean, definitions: Map<Even
18
18
  * Notifies registry listeners.
19
19
  *
20
20
  * Observer failures never break registry mutations; they are
21
- * forwarded to the `onError` hook when one is configured.
21
+ * forwarded to the `onError` hook when one is configured, and to
22
+ * Node's process warning channel when one is not, so a broken
23
+ * observer is never silently discarded.
22
24
  */
23
25
  export declare function registryNotify(change: EventRegistryChange, listeners: Set<EventRegistryListener>, onError?: (error: unknown, context: EventRegistryErrorContext) => void): void;
24
26
  //# sourceMappingURL=eventRegistry.lifecycle.d.ts.map
@@ -2,6 +2,7 @@
2
2
  * Event registry lifecycle methods for Zudojs.
3
3
  */
4
4
  import { registryUnregister } from "./eventRegistry.registration.js";
5
+ import { warnObserverError } from "../eventErrors/eventWarning.helper.js";
5
6
  /**
6
7
  * Clears all handlers and definitions from the registry.
7
8
  *
@@ -34,7 +35,9 @@ export function registryDispose(disposed, definitions, handlers, listeners, ensu
34
35
  * Notifies registry listeners.
35
36
  *
36
37
  * Observer failures never break registry mutations; they are
37
- * forwarded to the `onError` hook when one is configured.
38
+ * forwarded to the `onError` hook when one is configured, and to
39
+ * Node's process warning channel when one is not, so a broken
40
+ * observer is never silently discarded.
38
41
  */
39
42
  export function registryNotify(change, listeners, onError) {
40
43
  for (const listener of listeners) {
@@ -52,6 +55,9 @@ export function registryNotify(change, listeners, onError) {
52
55
  */
53
56
  }
54
57
  }
58
+ else {
59
+ warnObserverError(error, "An event registry observer", listeners);
60
+ }
55
61
  }
56
62
  }
57
63
  }
@@ -23,6 +23,7 @@ export declare function registryRegister<TType extends EventType, TPayload>(defi
23
23
  export declare function registryRegisterHandler<TEvent extends Event = Event>(eventType: EventTypePattern, handler: EventHandlerLike<TEvent>, handlerOptions: Omit<EventHandlerOptions, "eventType">, handlers: Map<string, EventHandlerEntry>, options: {
24
24
  onDuplicateHandlerId: DuplicateHandlerIdPolicy;
25
25
  maxHandlersPerPattern: number;
26
+ enforceHandlerLimit?: boolean;
26
27
  onWarning: (warning: EventRegistryWarning) => void;
27
28
  }, ensureActive: () => void, notify: (change: EventRegistryChange) => void, warnedPatterns: Set<string>): EventSubscription;
28
29
  /**
@@ -4,7 +4,7 @@
4
4
  import { normalizeEventType } from "../eventTypes/eventType.type.js";
5
5
  import { createEventHandler } from "../eventHandler/eventHandler.core.js";
6
6
  import { createEventSubscription } from "../eventSubscription/eventSubscription.core.js";
7
- import { DuplicateEventDefinitionError, DuplicateEventHandlerError, InvalidEventError, } from "../eventErrors/eventError.base.js";
7
+ import { DuplicateEventDefinitionError, DuplicateEventHandlerError, EventListenerLimitExceededError, InvalidEventError, } from "../eventErrors/eventError.base.js";
8
8
  import { EventRegistryChangeType } from "./eventRegistry.type.js";
9
9
  /**
10
10
  * Normalizes an event type for registry lookups, converting
@@ -104,7 +104,7 @@ export function registryRegisterHandler(eventType, handler, handlerOptions, hand
104
104
  description: registration.description,
105
105
  });
106
106
  handlers.set(registration.id, { registration, subscription });
107
- checkHandlerLimit(registration.eventType, handlers, options.maxHandlersPerPattern, options.onWarning, warnedPatterns);
107
+ checkHandlerLimit(registration.eventType, handlers, options.maxHandlersPerPattern, options.onWarning, warnedPatterns, options.enforceHandlerLimit === true, registration.id);
108
108
  notify({
109
109
  type: EventRegistryChangeType.HANDLER_REGISTERED,
110
110
  eventType: registration.eventType,
@@ -114,11 +114,19 @@ export function registryRegisterHandler(eventType, handler, handlerOptions, hand
114
114
  return subscription;
115
115
  }
116
116
  /**
117
- * Emits a leak warning (once per pattern) when the number of
118
- * handlers for a pattern exceeds the configured limit.
117
+ * Reports a pattern whose handler count has passed the configured limit.
118
+ *
119
+ * Warns once per pattern by default. Under `enforceHandlerLimit` it instead
120
+ * removes the handler just registered and throws
121
+ * {@link EventListenerLimitExceededError}, so a refused registration leaves
122
+ * the registry exactly as it was — and it throws on every breach, not only
123
+ * the first, because each one is a separate fault.
119
124
  */
120
- function checkHandlerLimit(pattern, handlers, limit, onWarning, warnedPatterns) {
121
- if (limit <= 0 || warnedPatterns.has(pattern)) {
125
+ function checkHandlerLimit(pattern, handlers, limit, onWarning, warnedPatterns, enforce, registrationId) {
126
+ if (limit <= 0) {
127
+ return;
128
+ }
129
+ if (!enforce && warnedPatterns.has(pattern)) {
122
130
  return;
123
131
  }
124
132
  let count = 0;
@@ -130,6 +138,10 @@ function checkHandlerLimit(pattern, handlers, limit, onWarning, warnedPatterns)
130
138
  if (count <= limit) {
131
139
  return;
132
140
  }
141
+ if (enforce) {
142
+ handlers.delete(registrationId);
143
+ throw new EventListenerLimitExceededError(pattern, count, limit);
144
+ }
133
145
  warnedPatterns.add(pattern);
134
146
  const warning = {
135
147
  type: "handler.limit",
@@ -7,6 +7,7 @@
7
7
  * higher-level routing belongs to EventBus.
8
8
  */
9
9
  import { DuplicateEventDefinitionError, EventDefinitionNotFoundError, DuplicateEventHandlerError, EventHandlerNotFoundError, EventRegistryDisposedError, } from "../eventErrors/eventError.base.js";
10
+ import { EVENT_HANDLER_LIMIT_WARNING_CODE, emitEventsWarning, } from "../eventErrors/eventWarning.helper.js";
10
11
  import { DEFAULT_MAX_HANDLERS_PER_PATTERN } from "./eventRegistry.type.js";
11
12
  import { normalizeRegistryEventType, registryRegister, registryRegisterHandler, registryUnregister, registryUnregisterHandler, } from "./eventRegistry.registration.js";
12
13
  import { getHandlersForEvent, getHandlersForType, getAllDefinitions, getAllHandlers, } from "./eventRegistry.queries.js";
@@ -19,11 +20,7 @@ export { DuplicateEventDefinitionError, EventDefinitionNotFoundError, DuplicateE
19
20
  * console directly. A no-op where `process.emitWarning` is unavailable.
20
21
  */
21
22
  function defaultWarning(warning) {
22
- const emit = globalThis.process?.emitWarning;
23
- emit?.(`[@zudojs/events] ${warning.message}`, {
24
- type: "ZudojsEventsWarning",
25
- code: "ZUDOJS_EVENTS_HANDLER_LIMIT",
26
- });
23
+ emitEventsWarning(warning.message, EVENT_HANDLER_LIMIT_WARNING_CODE);
27
24
  }
28
25
  /**
29
26
  * Main event registry.
@@ -47,6 +44,7 @@ export class EventRegistry {
47
44
  onDuplicateHandlerId: options.onDuplicateHandlerId ??
48
45
  (options.allowDuplicateHandlerIds ? "replace" : "throw"),
49
46
  maxHandlersPerPattern,
47
+ enforceHandlerLimit: options.enforceHandlerLimit ?? false,
50
48
  onWarning: options.onWarning ?? defaultWarning,
51
49
  onError: options.onError,
52
50
  };
@@ -59,6 +59,16 @@ export interface EventRegistryOptions {
59
59
  * disable. Defaults to 100.
60
60
  */
61
61
  readonly maxHandlersPerPattern?: number;
62
+ /**
63
+ * Refuse a registration that would exceed `maxHandlersPerPattern` instead of
64
+ * warning about it. Defaults to `false`.
65
+ *
66
+ * The default is a one-shot warning, which reports a suspected leak without
67
+ * interrupting a working application. Set this where a breached limit is a
68
+ * fault you would rather fail on: `register` then throws
69
+ * `EventListenerLimitExceededError` and the handler is not registered.
70
+ */
71
+ readonly enforceHandlerLimit?: boolean;
62
72
  /**
63
73
  * Receives limit warnings. Defaults to `process.emitWarning` (type `ZudojsEventsWarning`).
64
74
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/events",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Event-driven architecture with event bus, emitter, middleware, and registry for decoupled communication.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -20,15 +20,15 @@
20
20
  ],
21
21
  "sideEffects": false,
22
22
  "dependencies": {
23
- "@zudojs/constants": "1.1.0",
24
- "@zudojs/errors": "1.1.0",
25
- "@zudojs/middleware": "1.0.2"
23
+ "@zudojs/constants": "1.1.2",
24
+ "@zudojs/errors": "1.3.0",
25
+ "@zudojs/middleware": "1.1.0"
26
26
  },
27
27
  "engines": {
28
28
  "node": ">=24.0.0"
29
29
  },
30
30
  "devDependencies": {
31
- "vitest": "^4.1.11",
31
+ "vitest": "^5.0.1",
32
32
  "typescript": "7.0.2"
33
33
  },
34
34
  "license": "MIT",
@@ -45,7 +45,7 @@
45
45
  "event-bus",
46
46
  "pubsub"
47
47
  ],
48
- "homepage": "https://github.com/oyinlola-tech/zudo#readme",
48
+ "homepage": "https://zudojs.oyinlola.site/docs/packages-events",
49
49
  "bugs": {
50
50
  "url": "https://github.com/oyinlola-tech/zudo/issues"
51
51
  },