@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.
- package/LICENSE +21 -0
- package/README.md +70 -11
- package/dist/eventBus/eventBus.core.d.ts +28 -2
- package/dist/eventBus/eventBus.core.js +109 -31
- package/dist/eventBus/eventBus.publish.d.ts +23 -5
- package/dist/eventBus/eventBus.publish.js +78 -32
- package/dist/eventBus/eventBus.registration.d.ts +24 -13
- package/dist/eventBus/eventBus.registration.js +31 -28
- package/dist/eventBus/eventBus.type.d.ts +75 -3
- package/dist/eventBus/eventBus.type.js +9 -0
- package/dist/eventBus/index.d.ts +2 -0
- package/dist/eventBus/index.js +2 -0
- package/dist/eventEmitter/eventEmitter.abort.d.ts +11 -2
- package/dist/eventEmitter/eventEmitter.abort.js +20 -5
- package/dist/eventEmitter/eventEmitter.core.d.ts +14 -3
- package/dist/eventEmitter/eventEmitter.core.js +72 -56
- package/dist/eventEmitter/eventEmitter.parallel.d.ts +7 -1
- package/dist/eventEmitter/eventEmitter.parallel.js +31 -13
- package/dist/eventEmitter/eventEmitter.sequential.d.ts +17 -1
- package/dist/eventEmitter/eventEmitter.sequential.js +19 -12
- package/dist/eventEmitter/eventEmitter.type.d.ts +60 -0
- package/dist/eventErrors/eventError.base.d.ts +50 -3
- package/dist/eventErrors/eventError.base.js +64 -3
- package/dist/eventHandler/eventHandler.core.d.ts +19 -0
- package/dist/eventHandler/eventHandler.core.js +67 -4
- package/dist/eventMiddleware/eventMiddleware.builder.js +5 -11
- package/dist/eventMiddleware/eventMiddleware.helper.js +0 -1
- package/dist/eventMiddleware/eventMiddleware.pipeline.d.ts +5 -0
- package/dist/eventMiddleware/eventMiddleware.pipeline.js +33 -9
- package/dist/eventRegistry/eventRegistry.lifecycle.d.ts +10 -5
- package/dist/eventRegistry/eventRegistry.lifecycle.js +23 -10
- package/dist/eventRegistry/eventRegistry.queries.d.ts +16 -7
- package/dist/eventRegistry/eventRegistry.queries.js +22 -13
- package/dist/eventRegistry/eventRegistry.registration.d.ts +14 -7
- package/dist/eventRegistry/eventRegistry.registration.js +120 -41
- package/dist/eventRegistry/eventRegistry.store.d.ts +12 -5
- package/dist/eventRegistry/eventRegistry.store.js +37 -12
- package/dist/eventRegistry/eventRegistry.type.d.ts +87 -2
- package/dist/eventRegistry/eventRegistry.type.js +5 -0
- package/dist/eventSubscription/eventSubscription.core.d.ts +5 -0
- package/dist/eventSubscription/eventSubscription.core.js +17 -3
- package/dist/eventTypes/eventDefinition.type.d.ts +11 -4
- package/dist/eventTypes/eventDefinition.type.js +49 -17
- package/dist/eventTypes/eventPayload.type.d.ts +5 -0
- package/dist/eventTypes/eventPayload.type.js +62 -22
- package/dist/eventTypes/eventType.type.d.ts +22 -5
- package/dist/eventTypes/eventType.type.js +33 -11
- package/dist/eventTypes/index.d.ts +2 -2
- package/dist/eventTypes/index.js +2 -2
- package/package.json +26 -14
- package/dist/.tsbuildinfo +0 -1
- package/dist/eventBus/eventBus.core.d.ts.map +0 -1
- package/dist/eventBus/eventBus.core.js.map +0 -1
- package/dist/eventBus/eventBus.factory.d.ts.map +0 -1
- package/dist/eventBus/eventBus.factory.js.map +0 -1
- package/dist/eventBus/eventBus.publish.d.ts.map +0 -1
- package/dist/eventBus/eventBus.publish.js.map +0 -1
- package/dist/eventBus/eventBus.registration.d.ts.map +0 -1
- package/dist/eventBus/eventBus.registration.js.map +0 -1
- package/dist/eventBus/eventBus.type.d.ts.map +0 -1
- package/dist/eventBus/eventBus.type.js.map +0 -1
- package/dist/eventBus/index.d.ts.map +0 -1
- package/dist/eventBus/index.js.map +0 -1
- package/dist/eventEmitter/eventEmitter.abort.d.ts.map +0 -1
- package/dist/eventEmitter/eventEmitter.abort.js.map +0 -1
- package/dist/eventEmitter/eventEmitter.core.d.ts.map +0 -1
- package/dist/eventEmitter/eventEmitter.core.js.map +0 -1
- package/dist/eventEmitter/eventEmitter.parallel.d.ts.map +0 -1
- package/dist/eventEmitter/eventEmitter.parallel.js.map +0 -1
- package/dist/eventEmitter/eventEmitter.sequential.d.ts.map +0 -1
- package/dist/eventEmitter/eventEmitter.sequential.js.map +0 -1
- package/dist/eventEmitter/eventEmitter.type.d.ts.map +0 -1
- package/dist/eventEmitter/eventEmitter.type.js.map +0 -1
- package/dist/eventEmitter/index.d.ts.map +0 -1
- package/dist/eventEmitter/index.js.map +0 -1
- package/dist/eventErrors/eventError.base.d.ts.map +0 -1
- package/dist/eventErrors/eventError.base.js.map +0 -1
- package/dist/eventErrors/index.d.ts.map +0 -1
- package/dist/eventErrors/index.js.map +0 -1
- package/dist/eventHandler/eventHandler.core.d.ts.map +0 -1
- package/dist/eventHandler/eventHandler.core.js.map +0 -1
- package/dist/eventHandler/index.d.ts.map +0 -1
- package/dist/eventHandler/index.js.map +0 -1
- package/dist/eventMiddleware/eventMiddleware.builder.d.ts.map +0 -1
- package/dist/eventMiddleware/eventMiddleware.builder.js.map +0 -1
- package/dist/eventMiddleware/eventMiddleware.helper.d.ts.map +0 -1
- package/dist/eventMiddleware/eventMiddleware.helper.js.map +0 -1
- package/dist/eventMiddleware/eventMiddleware.pipeline.d.ts.map +0 -1
- package/dist/eventMiddleware/eventMiddleware.pipeline.js.map +0 -1
- package/dist/eventMiddleware/eventMiddleware.type.d.ts.map +0 -1
- package/dist/eventMiddleware/eventMiddleware.type.js.map +0 -1
- package/dist/eventMiddleware/index.d.ts.map +0 -1
- package/dist/eventMiddleware/index.js.map +0 -1
- package/dist/eventRegistry/eventRegistry.lifecycle.d.ts.map +0 -1
- package/dist/eventRegistry/eventRegistry.lifecycle.js.map +0 -1
- package/dist/eventRegistry/eventRegistry.queries.d.ts.map +0 -1
- package/dist/eventRegistry/eventRegistry.queries.js.map +0 -1
- package/dist/eventRegistry/eventRegistry.registration.d.ts.map +0 -1
- package/dist/eventRegistry/eventRegistry.registration.js.map +0 -1
- package/dist/eventRegistry/eventRegistry.store.d.ts.map +0 -1
- package/dist/eventRegistry/eventRegistry.store.js.map +0 -1
- package/dist/eventRegistry/eventRegistry.type.d.ts.map +0 -1
- package/dist/eventRegistry/eventRegistry.type.js.map +0 -1
- package/dist/eventRegistry/index.d.ts.map +0 -1
- package/dist/eventRegistry/index.js.map +0 -1
- package/dist/eventSubscription/eventSubscription.core.d.ts.map +0 -1
- package/dist/eventSubscription/eventSubscription.core.js.map +0 -1
- package/dist/eventSubscription/index.d.ts.map +0 -1
- package/dist/eventSubscription/index.js.map +0 -1
- package/dist/eventTypes/eventDefinition.type.d.ts.map +0 -1
- package/dist/eventTypes/eventDefinition.type.js.map +0 -1
- package/dist/eventTypes/eventPayload.type.d.ts.map +0 -1
- package/dist/eventTypes/eventPayload.type.js.map +0 -1
- package/dist/eventTypes/eventType.type.d.ts.map +0 -1
- package/dist/eventTypes/eventType.type.js.map +0 -1
- package/dist/eventTypes/index.d.ts.map +0 -1
- package/dist/eventTypes/index.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- 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
|
|
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 {
|
|
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
|
-
|
|
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()
|
|
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
|
|
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 {
|
|
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
|
-
|
|
160
|
+
const failures = [];
|
|
157
161
|
for (const subscription of subscriptions) {
|
|
158
|
-
|
|
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
|
|
36
|
-
return new Date(
|
|
37
|
+
if (timestamp === undefined) {
|
|
38
|
+
return new Date();
|
|
37
39
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
63
|
-
throw new
|
|
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
|
|
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
|
-
|
|
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 (
|
|
36
|
-
return
|
|
46
|
+
if (typeof value !== "object") {
|
|
47
|
+
return false;
|
|
37
48
|
}
|
|
38
|
-
if (
|
|
39
|
-
return
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
|
101
|
-
if (
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
/**
|