@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
@@ -3,13 +3,18 @@
3
3
  *
4
4
  * The registry owns event definitions and registered handlers.
5
5
  * It does not perform event dispatching. Dispatching belongs to
6
- * EventEmitter and higher-level routing belongs to EventBus.
6
+ * EventEmitter (which stores its handlers in a registry) and
7
+ * higher-level routing belongs to EventBus.
7
8
  */
8
- import { DuplicateEventDefinitionError, EventDefinitionNotFoundError, DuplicateEventHandlerError, EventHandlerNotFoundError, } from "../eventErrors/eventError.base.js";
9
- import { registryRegister, registryRegisterHandler, registryUnregister, registryUnregisterHandler, } from "./eventRegistry.registration.js";
9
+ import { DuplicateEventDefinitionError, EventDefinitionNotFoundError, DuplicateEventHandlerError, EventHandlerNotFoundError, EventRegistryDisposedError, } from "../eventErrors/eventError.base.js";
10
+ import { DEFAULT_MAX_HANDLERS_PER_PATTERN } from "./eventRegistry.type.js";
11
+ import { normalizeRegistryEventType, registryRegister, registryRegisterHandler, registryUnregister, registryUnregisterHandler, } from "./eventRegistry.registration.js";
10
12
  import { getHandlersForEvent, getHandlersForType, getAllDefinitions, getAllHandlers, } from "./eventRegistry.queries.js";
11
13
  import { registryClear, registryDispose, registryNotify, } from "./eventRegistry.lifecycle.js";
12
- export { DuplicateEventDefinitionError, EventDefinitionNotFoundError, DuplicateEventHandlerError, EventHandlerNotFoundError, };
14
+ export { DuplicateEventDefinitionError, EventDefinitionNotFoundError, DuplicateEventHandlerError, EventHandlerNotFoundError, EventRegistryDisposedError, };
15
+ function defaultWarning(warning) {
16
+ console.warn(`[@zudojs/events] ${warning.message}`);
17
+ }
13
18
  /**
14
19
  * Main event registry.
15
20
  */
@@ -17,23 +22,34 @@ export class EventRegistry {
17
22
  definitions = new Map();
18
23
  handlers = new Map();
19
24
  listeners = new Set();
25
+ warnedPatterns = new Set();
20
26
  options;
21
27
  disposed = false;
22
28
  constructor(options = {}) {
29
+ const maxHandlersPerPattern = options.maxHandlersPerPattern ?? DEFAULT_MAX_HANDLERS_PER_PATTERN;
30
+ if (typeof maxHandlersPerPattern !== "number" ||
31
+ !Number.isFinite(maxHandlersPerPattern) ||
32
+ maxHandlersPerPattern < 0) {
33
+ throw new RangeError("maxHandlersPerPattern must be a non-negative finite number.");
34
+ }
23
35
  this.options = {
24
36
  allowDuplicateDefinitions: options.allowDuplicateDefinitions ?? false,
25
- allowDuplicateHandlerIds: options.allowDuplicateHandlerIds ?? false,
37
+ onDuplicateHandlerId: options.onDuplicateHandlerId ??
38
+ (options.allowDuplicateHandlerIds ? "replace" : "throw"),
39
+ maxHandlersPerPattern,
40
+ onWarning: options.onWarning ?? defaultWarning,
41
+ onError: options.onError,
26
42
  };
27
43
  }
28
44
  register(definition) {
29
45
  return registryRegister(definition, this.definitions, this.options, () => this.ensureActive(), (c) => this.notify(c));
30
46
  }
31
47
  registerHandler(eventType, handler, options = {}) {
32
- return registryRegisterHandler(eventType, handler, options, this.handlers, this.options, () => this.ensureActive(), (c) => this.notify(c));
48
+ return registryRegisterHandler(eventType, handler, options, this.handlers, this.options, () => this.ensureActive(), (c) => this.notify(c), this.warnedPatterns);
33
49
  }
34
50
  get(eventType) {
35
51
  this.ensureActive();
36
- return this.definitions.get(eventType);
52
+ return this.definitions.get(normalizeRegistryEventType(eventType));
37
53
  }
38
54
  require(eventType) {
39
55
  const definition = this.get(eventType);
@@ -44,14 +60,14 @@ export class EventRegistry {
44
60
  }
45
61
  has(eventType) {
46
62
  this.ensureActive();
47
- return this.definitions.has(eventType);
63
+ return this.definitions.has(normalizeRegistryEventType(eventType));
48
64
  }
49
65
  unregister(eventType) {
50
66
  return registryUnregister(eventType, this.definitions, () => this.ensureActive(), (c) => this.notify(c));
51
67
  }
52
68
  getHandler(handlerId) {
53
69
  this.ensureActive();
54
- return this.handlers.get(handlerId);
70
+ return this.handlers.get(handlerId)?.registration;
55
71
  }
56
72
  requireHandler(handlerId) {
57
73
  const handler = this.getHandler(handlerId);
@@ -64,8 +80,16 @@ export class EventRegistry {
64
80
  this.ensureActive();
65
81
  return this.handlers.has(handlerId);
66
82
  }
83
+ /**
84
+ * Returns the subscription created for a handler id, if the
85
+ * handler is still registered.
86
+ */
87
+ getHandlerSubscription(handlerId) {
88
+ this.ensureActive();
89
+ return this.handlers.get(handlerId)?.subscription;
90
+ }
67
91
  unregisterHandler(handlerId) {
68
- return registryUnregisterHandler(handlerId, this.handlers, () => this.ensureActive(), (c) => this.notify(c));
92
+ return registryUnregisterHandler(handlerId, this.handlers, () => this.ensureActive());
69
93
  }
70
94
  getDefinitions() {
71
95
  this.ensureActive();
@@ -98,6 +122,7 @@ export class EventRegistry {
98
122
  }
99
123
  clear() {
100
124
  registryClear(this.definitions, this.handlers, () => this.ensureActive(), (c) => this.notify(c));
125
+ this.warnedPatterns.clear();
101
126
  }
102
127
  dispose() {
103
128
  registryDispose(this.disposed, this.definitions, this.handlers, this.listeners, () => this.ensureActive(), (c) => this.notify(c));
@@ -107,11 +132,11 @@ export class EventRegistry {
107
132
  return this.disposed;
108
133
  }
109
134
  notify(change) {
110
- registryNotify(change, this.listeners);
135
+ registryNotify(change, this.listeners, this.options.onError);
111
136
  }
112
137
  ensureActive() {
113
138
  if (this.disposed) {
114
- throw new Error("EventRegistry has already been disposed.");
139
+ throw new EventRegistryDisposedError();
115
140
  }
116
141
  }
117
142
  }
@@ -1,11 +1,73 @@
1
1
  /**
2
2
  * Event registry type definitions for Zudojs.
3
3
  */
4
- import type { EventDefinition, EventType } from "../eventTypes/eventDefinition.type.js";
5
- import type { RegisteredEventHandler } from "../eventHandler/eventHandler.core.js";
4
+ import type { Event, EventDefinition, EventType } from "../eventTypes/eventDefinition.type.js";
5
+ import type { EventTypePattern } from "../eventTypes/eventType.type.js";
6
+ import type { EventHandlerLike, EventHandlerOptions, RegisteredEventHandler } from "../eventHandler/eventHandler.core.js";
7
+ import type { EventSubscription } from "../eventSubscription/eventSubscription.core.js";
8
+ /**
9
+ * Policy applied when a handler is registered with an id that
10
+ * is already in use.
11
+ *
12
+ * - "throw" → DuplicateEventHandlerError (default)
13
+ * - "replace" → the previous handler is unregistered (its
14
+ * subscription is cancelled) and the new one stored
15
+ */
16
+ export type DuplicateHandlerIdPolicy = "throw" | "replace";
17
+ /**
18
+ * Default number of handlers allowed per event pattern before a
19
+ * leak warning is emitted.
20
+ */
21
+ export declare const DEFAULT_MAX_HANDLERS_PER_PATTERN = 100;
22
+ /**
23
+ * Warning emitted when a registry limit is exceeded.
24
+ */
25
+ export interface EventRegistryWarning {
26
+ readonly type: "handler.limit";
27
+ readonly pattern: EventTypePattern;
28
+ readonly count: number;
29
+ readonly limit: number;
30
+ readonly message: string;
31
+ }
32
+ /**
33
+ * Context passed to onError hooks.
34
+ */
35
+ export interface EventRegistryErrorContext {
36
+ readonly source: "observer";
37
+ readonly change?: EventRegistryChange;
38
+ }
6
39
  export interface EventRegistryOptions {
40
+ /**
41
+ * Allow re-registering an event type. The previous definition
42
+ * is replaced.
43
+ */
7
44
  readonly allowDuplicateDefinitions?: boolean;
45
+ /**
46
+ * @deprecated Use `onDuplicateHandlerId: "replace"`. When true,
47
+ * registering a handler id that already exists replaces the
48
+ * previous handler (it never allowed two handlers to coexist).
49
+ */
8
50
  readonly allowDuplicateHandlerIds?: boolean;
51
+ /**
52
+ * What to do when a handler id is already registered.
53
+ * Defaults to "throw".
54
+ */
55
+ readonly onDuplicateHandlerId?: DuplicateHandlerIdPolicy;
56
+ /**
57
+ * Maximum handlers per event pattern before a leak warning is
58
+ * emitted through `onWarning` (or console.warn). Use 0 to
59
+ * disable. Defaults to 100.
60
+ */
61
+ readonly maxHandlersPerPattern?: number;
62
+ /**
63
+ * Receives limit warnings. Defaults to console.warn.
64
+ */
65
+ readonly onWarning?: (warning: EventRegistryWarning) => void;
66
+ /**
67
+ * Receives errors thrown by registry observers, which are
68
+ * otherwise swallowed so they cannot break registry mutations.
69
+ */
70
+ readonly onError?: (error: unknown, context: EventRegistryErrorContext) => void;
9
71
  }
10
72
  export declare enum EventRegistryChangeType {
11
73
  EVENT_REGISTERED = "event.registered",
@@ -25,4 +87,27 @@ export interface RegisteredEventDefinition<TType extends EventType = EventType,
25
87
  readonly definition: EventDefinition<TType, TPayload>;
26
88
  readonly registeredAt: Date;
27
89
  }
90
+ /**
91
+ * Internal handler entry: the registration plus the subscription
92
+ * handed to the caller, so the registry can cancel it.
93
+ */
94
+ export interface EventHandlerEntry {
95
+ readonly registration: RegisteredEventHandler;
96
+ readonly subscription: EventSubscription;
97
+ }
98
+ /**
99
+ * Minimal handler store contract used by EventEmitter.
100
+ *
101
+ * EventRegistry implements it; the emitter stores its handlers in
102
+ * a registry so that a bus has a single source of truth for
103
+ * handlers.
104
+ */
105
+ export interface EventHandlerStore {
106
+ registerHandler<TEvent extends Event = Event>(eventType: EventTypePattern, handler: EventHandlerLike<TEvent>, options?: Omit<EventHandlerOptions, "eventType">): EventSubscription;
107
+ unregisterHandler(handlerId: string): boolean;
108
+ hasHandler(handlerId: string): boolean;
109
+ getHandlers(): readonly RegisteredEventHandler[];
110
+ getHandlersForEvent(event: Event): readonly RegisteredEventHandler[];
111
+ readonly handlerCount: number;
112
+ }
28
113
  //# sourceMappingURL=eventRegistry.type.d.ts.map
@@ -1,6 +1,11 @@
1
1
  /**
2
2
  * Event registry type definitions for Zudojs.
3
3
  */
4
+ /**
5
+ * Default number of handlers allowed per event pattern before a
6
+ * leak warning is emitted.
7
+ */
8
+ export const DEFAULT_MAX_HANDLERS_PER_PATTERN = 100;
4
9
  export var EventRegistryChangeType;
5
10
  (function (EventRegistryChangeType) {
6
11
  EventRegistryChangeType["EVENT_REGISTERED"] = "event.registered";
@@ -135,6 +135,11 @@ export declare class EventSubscriptionGroup implements EventSubscription {
135
135
  remove(subscription: EventSubscription): boolean;
136
136
  /**
137
137
  * Unsubscribes every subscription in the group.
138
+ *
139
+ * Every subscription is attempted even if one throws; the
140
+ * failures are then re-thrown together as an AggregateError and
141
+ * the failed subscriptions stay in the group so the call can be
142
+ * retried.
138
143
  */
139
144
  unsubscribe(): void;
140
145
  /**
@@ -146,17 +146,31 @@ export class EventSubscriptionGroup {
146
146
  }
147
147
  /**
148
148
  * Unsubscribes every subscription in the group.
149
+ *
150
+ * Every subscription is attempted even if one throws; the
151
+ * failures are then re-thrown together as an AggregateError and
152
+ * the failed subscriptions stay in the group so the call can be
153
+ * retried.
149
154
  */
150
155
  unsubscribe() {
151
156
  if (!this.active) {
152
157
  return;
153
158
  }
154
- this.currentState = EventSubscriptionState.CANCELLED;
155
159
  const subscriptions = [...this.subscriptions];
156
- this.subscriptions.clear();
160
+ const failures = [];
157
161
  for (const subscription of subscriptions) {
158
- subscription.unsubscribe();
162
+ try {
163
+ subscription.unsubscribe();
164
+ this.subscriptions.delete(subscription);
165
+ }
166
+ catch (error) {
167
+ failures.push(error);
168
+ }
159
169
  }
170
+ if (failures.length > 0) {
171
+ throw new AggregateError(failures, `${failures.length} of ${subscriptions.length} subscriptions failed to unsubscribe.`);
172
+ }
173
+ this.currentState = EventSubscriptionState.CANCELLED;
160
174
  }
161
175
  /**
162
176
  * Returns all subscriptions in the group.
@@ -95,9 +95,10 @@ export interface Event<TPayload = EventPayload> {
95
95
  */
96
96
  export interface EventInput<TPayload = EventPayload> {
97
97
  /**
98
- * Optional event identifier.
98
+ * Optional event identifier. Plain strings are accepted and
99
+ * branded on the created event.
99
100
  */
100
- readonly id?: EventId;
101
+ readonly id?: EventId | string;
101
102
  /**
102
103
  * Event type.
103
104
  */
@@ -115,9 +116,9 @@ export interface EventInput<TPayload = EventPayload> {
115
116
  */
116
117
  readonly source?: EventSource;
117
118
  /**
118
- * Optional correlation identifier.
119
+ * Optional correlation identifier. Plain strings are accepted.
119
120
  */
120
- readonly correlationId?: EventCorrelationId;
121
+ readonly correlationId?: EventCorrelationId | string;
121
122
  /**
122
123
  * Optional causation identifier.
123
124
  */
@@ -148,6 +149,12 @@ export declare function isEvent(value: unknown): value is Event;
148
149
  export declare function createEventId(): EventId;
149
150
  /**
150
151
  * Creates an immutable event.
152
+ *
153
+ * The event type is normalized (trimmed, lower-cased, separators
154
+ * collapsed) and validated; invalid types, ids and timestamps
155
+ * throw InvalidEventError. The top-level event object is frozen;
156
+ * the payload is left as supplied (use deepFreeze / the emitter's
157
+ * freezeEvents option for deep immutability).
151
158
  */
152
159
  export declare function createEvent<TPayload = EventPayload>(input: EventInput<TPayload>): Event<TPayload>;
153
160
  /**
@@ -6,6 +6,8 @@
6
6
  *
7
7
  * This module intentionally contains no dispatching logic.
8
8
  */
9
+ import { InvalidEventError } from "../eventErrors/eventError.base.js";
10
+ import { normalizeEventType } from "./eventType.type.js";
9
11
  /**
10
12
  * Determines whether an unknown value satisfies the Event
11
13
  * contract.
@@ -32,17 +34,39 @@ export function createEventId() {
32
34
  * Normalizes an event timestamp.
33
35
  */
34
36
  function normalizeTimestamp(timestamp) {
35
- if (timestamp instanceof Date) {
36
- return new Date(timestamp.getTime());
37
+ if (timestamp === undefined) {
38
+ return new Date();
37
39
  }
38
- if (typeof timestamp === "number") {
39
- const date = new Date(timestamp);
40
- if (Number.isNaN(date.getTime())) {
41
- throw new TypeError("Invalid event timestamp.");
42
- }
43
- return date;
40
+ const date = timestamp instanceof Date
41
+ ? new Date(timestamp.getTime())
42
+ : typeof timestamp === "number"
43
+ ? new Date(timestamp)
44
+ : undefined;
45
+ if (date === undefined || Number.isNaN(date.getTime())) {
46
+ throw new InvalidEventError("Invalid event timestamp.");
47
+ }
48
+ return date;
49
+ }
50
+ /**
51
+ * Normalizes and validates an event type, converting validation
52
+ * failures into InvalidEventError.
53
+ */
54
+ function normalizeInputType(type, eventId) {
55
+ if (typeof type !== "string" || type.trim().length === 0) {
56
+ throw new InvalidEventError("Event type must be a non-empty string.", {
57
+ eventId,
58
+ });
59
+ }
60
+ try {
61
+ return normalizeEventType(type);
62
+ }
63
+ catch (error) {
64
+ throw new InvalidEventError(`Invalid event type "${type}".`, {
65
+ eventType: type,
66
+ eventId,
67
+ cause: error,
68
+ });
44
69
  }
45
- return new Date();
46
70
  }
47
71
  /**
48
72
  * Freezes event metadata.
@@ -57,14 +81,24 @@ function normalizeMetadata(metadata) {
57
81
  }
58
82
  /**
59
83
  * Creates an immutable event.
84
+ *
85
+ * The event type is normalized (trimmed, lower-cased, separators
86
+ * collapsed) and validated; invalid types, ids and timestamps
87
+ * throw InvalidEventError. The top-level event object is frozen;
88
+ * the payload is left as supplied (use deepFreeze / the emitter's
89
+ * freezeEvents option for deep immutability).
60
90
  */
61
91
  export function createEvent(input) {
62
- if (typeof input.type !== "string" || input.type.trim().length === 0) {
63
- throw new TypeError("Event type must be a non-empty string.");
92
+ if (typeof input !== "object" || input === null) {
93
+ throw new InvalidEventError("Event input must be an object.");
94
+ }
95
+ if (input.id !== undefined && typeof input.id !== "string") {
96
+ throw new InvalidEventError("Event id must be a string.");
64
97
  }
98
+ const type = normalizeInputType(input.type, input.id);
65
99
  const event = {
66
100
  id: input.id ?? createEventId(),
67
- type: input.type,
101
+ type,
68
102
  payload: input.payload,
69
103
  timestamp: normalizeTimestamp(input.timestamp),
70
104
  source: input.source,
@@ -78,15 +112,13 @@ export function createEvent(input) {
78
112
  * Creates a typed event definition.
79
113
  */
80
114
  export function defineEvent(type) {
81
- if (type.trim().length === 0) {
82
- throw new TypeError("Event type must be a non-empty string.");
83
- }
115
+ const normalized = normalizeInputType(type);
84
116
  return Object.freeze({
85
- type,
117
+ type: normalized,
86
118
  create(payload, options = {}) {
87
119
  return createEvent({
88
120
  ...options,
89
- type,
121
+ type: normalized,
90
122
  payload,
91
123
  });
92
124
  },
@@ -93,6 +93,11 @@ export declare function createJsonEventPayload<const TPayload extends JsonEventP
93
93
  export declare function cloneEventPayload<TPayload extends EventPayload>(payload: TPayload): TPayload;
94
94
  /**
95
95
  * Deeply freezes an event payload.
96
+ *
97
+ * Cyclic and shared references are handled (each object is
98
+ * visited once), and an already frozen parent is still traversed
99
+ * so that unfrozen children are frozen too. ArrayBuffer views are
100
+ * left untouched because they cannot be frozen.
96
101
  */
97
102
  export declare function deepFreeze<T>(value: T): T;
98
103
  /**
@@ -24,6 +24,17 @@ export function isObjectEventPayload(value) {
24
24
  * event payload.
25
25
  */
26
26
  export function isJsonEventPayload(value) {
27
+ return isJsonValue(value, new WeakSet(), 0);
28
+ }
29
+ /**
30
+ * Maximum nesting depth accepted by isJsonEventPayload.
31
+ */
32
+ const MAX_JSON_PAYLOAD_DEPTH = 256;
33
+ function isPlainObject(value) {
34
+ const prototype = Object.getPrototypeOf(value);
35
+ return prototype === Object.prototype || prototype === null;
36
+ }
37
+ function isJsonValue(value, visited, depth) {
27
38
  if (value === null ||
28
39
  typeof value === "string" ||
29
40
  typeof value === "boolean") {
@@ -32,13 +43,31 @@ export function isJsonEventPayload(value) {
32
43
  if (typeof value === "number") {
33
44
  return Number.isFinite(value);
34
45
  }
35
- if (Array.isArray(value)) {
36
- return value.every((item) => isJsonEventPayload(item));
46
+ if (typeof value !== "object") {
47
+ return false;
37
48
  }
38
- if (typeof value === "object") {
39
- return Object.values(value).every((item) => isJsonEventPayload(item));
49
+ if (depth > MAX_JSON_PAYLOAD_DEPTH) {
50
+ return false;
51
+ }
52
+ if (visited.has(value)) {
53
+ // A cycle can never be serialized to JSON.
54
+ return false;
55
+ }
56
+ visited.add(value);
57
+ try {
58
+ if (Array.isArray(value)) {
59
+ return value.every((item) => isJsonValue(item, visited, depth + 1));
60
+ }
61
+ if (!isPlainObject(value)) {
62
+ // Date, Map, Set, class instances, typed arrays, ... are not
63
+ // JSON values even though JSON.stringify may accept them.
64
+ return false;
65
+ }
66
+ return Object.values(value).every((item) => isJsonValue(item, visited, depth + 1));
67
+ }
68
+ finally {
69
+ visited.delete(value);
40
70
  }
41
- return false;
42
71
  }
43
72
  /**
44
73
  * Creates a payload object from an input object.
@@ -87,36 +116,47 @@ export function cloneEventPayload(payload) {
87
116
  }
88
117
  /**
89
118
  * Deeply freezes an event payload.
119
+ *
120
+ * Cyclic and shared references are handled (each object is
121
+ * visited once), and an already frozen parent is still traversed
122
+ * so that unfrozen children are frozen too. ArrayBuffer views are
123
+ * left untouched because they cannot be frozen.
90
124
  */
91
125
  export function deepFreeze(value) {
92
- if (value === null || typeof value !== "object") {
93
- return value;
126
+ freezeRecursively(value, new WeakSet());
127
+ return value;
128
+ }
129
+ function freezeRecursively(value, visited) {
130
+ if ((typeof value !== "object" && typeof value !== "function") ||
131
+ value === null) {
132
+ return;
133
+ }
134
+ if (visited.has(value)) {
135
+ return;
94
136
  }
95
- if (Object.isFrozen(value)) {
96
- return value;
137
+ visited.add(value);
138
+ if (ArrayBuffer.isView(value)) {
139
+ return;
97
140
  }
98
141
  const object = value;
99
142
  for (const key of Reflect.ownKeys(object)) {
100
- const child = object[key];
101
- if (child !== null &&
102
- typeof child === "object" &&
103
- !Object.isFrozen(child)) {
104
- deepFreeze(child);
143
+ const descriptor = Object.getOwnPropertyDescriptor(object, key);
144
+ if (descriptor && "value" in descriptor) {
145
+ freezeRecursively(descriptor.value, visited);
105
146
  }
106
147
  }
107
- return Object.freeze(value);
148
+ Object.freeze(value);
108
149
  }
109
150
  /**
110
151
  * Removes undefined properties from an object payload.
111
152
  */
112
153
  export function stripUndefinedValues(payload) {
113
- const result = {};
114
- for (const [key, value] of Object.entries(payload)) {
115
- if (value !== undefined) {
116
- result[key] = value;
117
- }
118
- }
119
- return result;
154
+ /**
155
+ * Object.fromEntries defines every entry as an own data
156
+ * property, so a key such as "__proto__" (e.g. from JSON.parse)
157
+ * is copied as data instead of replacing the result's prototype.
158
+ */
159
+ return Object.fromEntries(Object.entries(payload).filter(([, value]) => value !== undefined));
120
160
  }
121
161
  /**
122
162
  * Merges two object payloads.
@@ -6,6 +6,8 @@
6
6
  * and strongly typed event-type helpers.
7
7
  */
8
8
  import type { Event, EventType } from "../eventTypes/eventDefinition.type.js";
9
+ import type { EventPayloadMap } from "./eventPayload.type.js";
10
+ export type { EventPayloadMap } from "./eventPayload.type.js";
9
11
  /**
10
12
  * A collection of event types.
11
13
  */
@@ -20,10 +22,6 @@ export type EventTypeList = readonly EventType[];
20
22
  * "*"
21
23
  */
22
24
  export type EventTypePattern = EventType | `${string}.*` | "*";
23
- /**
24
- * Type-safe mapping between event types and payloads.
25
- */
26
- export type EventPayloadMap = Record<EventType, unknown>;
27
25
  /**
28
26
  * Extracts the event type keys from a payload map.
29
27
  */
@@ -39,7 +37,9 @@ export type EventUnion<TMap extends EventPayloadMap> = {
39
37
  /**
40
38
  * Validates an event type string.
41
39
  *
42
- * Zudojs event types use dot-separated lowercase names.
40
+ * Zudojs event types use dot-separated lowercase names. Wildcards
41
+ * are not part of an event type; they are only valid in patterns
42
+ * (see isValidEventTypePattern).
43
43
  *
44
44
  * Examples:
45
45
  *
@@ -51,6 +51,10 @@ export type EventUnion<TMap extends EventPayloadMap> = {
51
51
  export declare function isValidEventType(value: unknown): value is EventType;
52
52
  /**
53
53
  * Validates an event type pattern.
54
+ *
55
+ * Supported forms are an exact event type, a namespace wildcard
56
+ * ("user.*") and the catch-all "*". Wildcards in any other
57
+ * position ("user.*.created", "user.cre*") are rejected.
54
58
  */
55
59
  export declare function isValidEventTypePattern(value: unknown): value is EventTypePattern;
56
60
  /**
@@ -92,6 +96,13 @@ export declare function getEventAction(type: EventType): string;
92
96
  * Returns all segments in an event type.
93
97
  */
94
98
  export declare function getEventTypeSegments(type: EventType): readonly string[];
99
+ /**
100
+ * Normalizes an event type pattern.
101
+ *
102
+ * "*" is returned unchanged, "User.*" becomes "user.*" and any
103
+ * other value is normalized as an event type.
104
+ */
105
+ export declare function normalizeEventTypePattern(pattern: string): EventTypePattern;
95
106
  /**
96
107
  * Checks whether an event type matches a pattern.
97
108
  *
@@ -99,8 +110,14 @@ export declare function getEventTypeSegments(type: EventType): readonly string[]
99
110
  *
100
111
  * "user.created" matches "user.created"
101
112
  * "user.created" matches "user.*"
113
+ * "user" matches "user.*" (a namespace pattern also
114
+ * matches the bare namespace event)
102
115
  * "user.created" matches "*"
103
116
  * "order.created" does not match "user.*"
117
+ *
118
+ * Both arguments are expected to be normalized (see
119
+ * normalizeEventType / normalizeEventTypePattern); no
120
+ * normalization happens here.
104
121
  */
105
122
  export declare function matchesEventType(type: EventType, pattern: EventTypePattern): boolean;
106
123
  /**