@zudojs/events 1.1.0 → 1.2.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.
@@ -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, {
@@ -235,16 +237,23 @@ export class EventBus {
235
237
  catch (error) {
236
238
  /**
237
239
  * Observers must never be able to break event bus
238
- * operations; failures go to the onError hook.
240
+ * operations; failures go to the onError hook, or to Node's
241
+ * process warning channel when no hook is configured, so a
242
+ * broken observer is never silently discarded.
239
243
  */
240
- try {
241
- this.options.onError?.(error, {
242
- source: "observer",
243
- event: event.event,
244
- });
244
+ if (this.options.onError) {
245
+ try {
246
+ this.options.onError(error, {
247
+ source: "observer",
248
+ event: event.event,
249
+ });
250
+ }
251
+ catch {
252
+ // Ignore failures of the error hook itself.
253
+ }
245
254
  }
246
- catch {
247
- // Ignore failures of the error hook itself.
255
+ else {
256
+ warnObserverError(error, "An event bus observer", this.listeners);
248
257
  }
249
258
  }
250
259
  }
@@ -36,6 +36,11 @@ export interface EventBusOptions {
36
36
  * emitted (0 disables). Defaults to 100.
37
37
  */
38
38
  readonly maxListeners?: number;
39
+ /**
40
+ * Refuse a registration that would exceed `maxListeners` instead of
41
+ * warning about it. Defaults to `false`.
42
+ */
43
+ readonly enforceHandlerLimit?: boolean;
39
44
  };
40
45
  readonly registry?: {
41
46
  readonly allowDuplicateDefinitions?: boolean;
@@ -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
  }
@@ -42,6 +42,11 @@ export interface EventEmitterOptions {
42
42
  * Defaults to 100.
43
43
  */
44
44
  readonly maxListeners?: number;
45
+ /**
46
+ * Refuse a registration that would exceed `maxListeners` instead of warning
47
+ * about it. Only applies to the private store. Defaults to `false`.
48
+ */
49
+ readonly enforceHandlerLimit?: boolean;
45
50
  /**
46
51
  * Receives leak warnings. Only applies to the private store.
47
52
  * Defaults to `process.emitWarning` (type `ZudojsEventsWarning`).
@@ -6,7 +6,7 @@
6
6
  * errors package does not define yet live in this file.
7
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
+ export { EventError, createEventError, isEventError, toEventError, EventPublishError, InvalidEventError, EventTypeNotFoundError, EventHandlerError, createEventHandlerError, EventHandlerNotFoundError, DuplicateEventHandlerError, DuplicateEventDefinitionError, EventDefinitionNotFoundError, EventEmitterDisposedError, EventRegistryDisposedError, EventSubscriptionClosedError, EventListenerLimitExceededError, EventTimeoutError, EventMiddlewareError, EventSerializationError, EventDeserializationError, } from "@zudojs/errors";
10
10
  /**
11
11
  * Options for EventDispatchAbortedError.
12
12
  */
@@ -45,11 +45,4 @@ export declare class EventBusDisposedError extends EventError {
45
45
  export declare class EventBusStoppedError extends EventError {
46
46
  constructor(operation: string);
47
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
48
  //# sourceMappingURL=eventError.base.d.ts.map
@@ -6,7 +6,7 @@
6
6
  * errors package does not define yet live in this file.
7
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
+ export { EventError, createEventError, isEventError, toEventError, EventPublishError, InvalidEventError, EventTypeNotFoundError, EventHandlerError, createEventHandlerError, EventHandlerNotFoundError, DuplicateEventHandlerError, DuplicateEventDefinitionError, EventDefinitionNotFoundError, EventEmitterDisposedError, EventRegistryDisposedError, EventSubscriptionClosedError, EventListenerLimitExceededError, EventTimeoutError, EventMiddlewareError, EventSerializationError, EventDeserializationError, } from "@zudojs/errors";
10
10
  /**
11
11
  * Error thrown when event dispatch is aborted through an
12
12
  * AbortSignal. Carries the partial results and errors gathered
@@ -52,18 +52,4 @@ export class EventBusStoppedError extends EventError {
52
52
  });
53
53
  }
54
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
55
  //# 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
@@ -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.2.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,9 +20,9 @@
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.1",
24
+ "@zudojs/errors": "1.2.0",
25
+ "@zudojs/middleware": "1.0.3"
26
26
  },
27
27
  "engines": {
28
28
  "node": ">=24.0.0"