@zudojs/events 1.0.0 → 1.1.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 +9 -2
- package/dist/eventBus/eventBus.core.js +8 -3
- package/dist/eventBus/eventBus.publish.js +12 -6
- package/dist/eventBus/eventBus.type.d.ts +1 -1
- package/dist/eventEmitter/eventEmitter.core.js +27 -5
- package/dist/eventEmitter/eventEmitter.parallel.js +3 -3
- package/dist/eventEmitter/eventEmitter.sequential.js +6 -4
- package/dist/eventEmitter/eventEmitter.type.d.ts +1 -1
- package/dist/eventMiddleware/eventMiddleware.pipeline.d.ts +2 -1
- package/dist/eventMiddleware/eventMiddleware.pipeline.js +38 -56
- package/dist/eventRegistry/eventRegistry.store.js +11 -1
- package/dist/eventRegistry/eventRegistry.type.d.ts +2 -2
- package/dist/eventTypes/eventPayload.type.js +6 -7
- package/dist/eventTypes/eventSnapshot.freeze.d.ts +37 -0
- package/dist/eventTypes/eventSnapshot.freeze.js +117 -0
- package/dist/eventTypes/index.d.ts +1 -0
- package/dist/eventTypes/index.js +1 -0
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Event-driven architecture with event bus, emitter, middleware, and registry for decoupled communication.
|
|
4
4
|
|
|
5
|
+
<!-- zudo-docs:start -->
|
|
6
|
+
|
|
7
|
+
**Documentation:** [zudojs.oyinlola.site/docs/packages-events](https://zudojs.oyinlola.site/docs/packages-events) · **For AI agents:** [Markdown version](https://zudojs.oyinlola.site/docs/packages-events.md), [llms.txt](https://zudojs.oyinlola.site/llms.txt)
|
|
8
|
+
|
|
9
|
+
<!-- zudo-docs:end -->
|
|
10
|
+
|
|
5
11
|
## Installation
|
|
6
12
|
|
|
7
13
|
```bash
|
|
@@ -46,8 +52,9 @@ await bus.publishEvent({
|
|
|
46
52
|
- Handler priorities, one-time handlers, per-handler timeouts
|
|
47
53
|
- Wildcard subscriptions (`"user.*"`, `"*"`)
|
|
48
54
|
- Event registry for typed definitions; handlers registered on the registry are dispatched by the bus
|
|
49
|
-
- Deep-frozen events (`freezeEvents`, on by default) so handlers cannot alter what other handlers see
|
|
50
|
-
-
|
|
55
|
+
- Deep-frozen events (`freezeEvents`, on by default) so handlers cannot alter what other handlers see: handlers receive a frozen *copy* (`createFrozenEventSnapshot`), so the publisher's own objects are never frozen, and Map, Set and Date values (including `event.timestamp`) become read-only variants that throw on mutation. Class instances are passed by reference.
|
|
56
|
+
- A handler unsubscribed by an earlier handler in the same dispatch, or left over after the bus is disposed mid-dispatch, does not run
|
|
57
|
+
- Listener-leak warnings (`maxListeners`, reported through `onWarning` or, by default, `process.emitWarning` with type `ZudojsEventsWarning`) and an `onError` hook for fire-and-forget publishes
|
|
51
58
|
- Typed error classes from `@zudojs/errors` (`EventHandlerError`, `EventMiddlewareError`, `EventDispatchAbortedError`, …)
|
|
52
59
|
|
|
53
60
|
## Lifecycle
|
|
@@ -5,6 +5,7 @@ import { isEvent } from "../eventTypes/eventDefinition.type.js";
|
|
|
5
5
|
import { EventEmitter } from "../eventEmitter/eventEmitter.core.js";
|
|
6
6
|
import { EventErrorMode } from "../eventEmitter/eventEmitter.type.js";
|
|
7
7
|
import { EventRegistry } from "../eventRegistry/eventRegistry.store.js";
|
|
8
|
+
import { normalizeRegistryEventType } from "../eventRegistry/eventRegistry.registration.js";
|
|
8
9
|
import { EventBusDisposedError, EventBusStoppedError, EventError, toEventError, } from "../eventErrors/eventError.base.js";
|
|
9
10
|
import { EventBusState } from "./eventBus.type.js";
|
|
10
11
|
export { EventBusState } from "./eventBus.type.js";
|
|
@@ -135,9 +136,13 @@ export class EventBus {
|
|
|
135
136
|
this.ensureNotDisposed();
|
|
136
137
|
const removed = this.registry.unregister(eventType);
|
|
137
138
|
if (options.removeHandlers) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Compare patterns on the full handler list: the matching
|
|
141
|
+
* query skips disabled handlers, which must be dropped too.
|
|
142
|
+
*/
|
|
143
|
+
const type = normalizeRegistryEventType(eventType);
|
|
144
|
+
for (const handler of this.registry.getHandlers()) {
|
|
145
|
+
if (handler.eventType === type) {
|
|
141
146
|
this.registry.unregisterHandler(handler.id);
|
|
142
147
|
}
|
|
143
148
|
}
|
|
@@ -42,25 +42,31 @@ export async function busPublish(event, options, deps) {
|
|
|
42
42
|
signal: options.signal,
|
|
43
43
|
metadata: options.metadata,
|
|
44
44
|
});
|
|
45
|
+
/**
|
|
46
|
+
* The emit result is captured here, at the terminal, rather
|
|
47
|
+
* than read back from the pipeline's return value: a middleware
|
|
48
|
+
* that awaits next() and returns nothing (or something else)
|
|
49
|
+
* has still dispatched the handlers, and their outcome must not
|
|
50
|
+
* be reported as a short-circuit.
|
|
51
|
+
*/
|
|
52
|
+
let emitResult;
|
|
45
53
|
const terminal = async () => {
|
|
46
|
-
|
|
54
|
+
const result = await deps.emitter.emit(event, {
|
|
47
55
|
mode: options.mode,
|
|
48
56
|
errorMode: options.errorMode,
|
|
49
57
|
signal: options.signal,
|
|
50
58
|
metadata: options.metadata,
|
|
51
59
|
});
|
|
60
|
+
emitResult = result;
|
|
61
|
+
return result;
|
|
52
62
|
};
|
|
53
|
-
let emitResult;
|
|
54
63
|
let middlewareExecutions;
|
|
55
64
|
if (allMiddleware.length > 0) {
|
|
56
65
|
const pipelineResult = await executeEventMiddlewarePipeline(allMiddleware, middlewareContext, terminal);
|
|
57
66
|
middlewareExecutions = pipelineResult.executions;
|
|
58
|
-
if (isEventEmitResult(pipelineResult.result)) {
|
|
59
|
-
emitResult = pipelineResult.result;
|
|
60
|
-
}
|
|
61
67
|
}
|
|
62
68
|
else {
|
|
63
|
-
|
|
69
|
+
await terminal();
|
|
64
70
|
}
|
|
65
71
|
deps.notify({
|
|
66
72
|
type: "published",
|
|
@@ -52,7 +52,7 @@ export interface EventBusOptions {
|
|
|
52
52
|
readonly requireRegistration?: boolean;
|
|
53
53
|
readonly middleware?: readonly EventBusMiddlewareItem[];
|
|
54
54
|
/**
|
|
55
|
-
* Receives leak warnings. Defaults to
|
|
55
|
+
* Receives leak warnings. Defaults to `process.emitWarning` (type `ZudojsEventsWarning`).
|
|
56
56
|
*/
|
|
57
57
|
readonly onWarning?: (warning: EventRegistryWarning) => void;
|
|
58
58
|
/**
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* bus and its registry share one set of handlers.
|
|
7
7
|
*/
|
|
8
8
|
import { createEvent, isEvent } from "../eventTypes/eventDefinition.type.js";
|
|
9
|
-
import {
|
|
9
|
+
import { createFrozenEventSnapshot } from "../eventTypes/eventSnapshot.freeze.js";
|
|
10
10
|
import { createEventHandlerContext } from "../eventHandler/eventHandler.core.js";
|
|
11
11
|
import { EventSubscriptionGroup } from "../eventSubscription/eventSubscription.core.js";
|
|
12
12
|
import { EventRegistry } from "../eventRegistry/eventRegistry.store.js";
|
|
@@ -68,8 +68,12 @@ export class EventEmitter {
|
|
|
68
68
|
}
|
|
69
69
|
const mode = options.mode ?? this.options.mode;
|
|
70
70
|
const errorMode = options.errorMode ?? this.options.errorMode;
|
|
71
|
-
const
|
|
72
|
-
|
|
71
|
+
const handlers = this.store.getHandlersForEvent(event);
|
|
72
|
+
// Handlers receive a frozen COPY: freezing the event in place froze
|
|
73
|
+
// the publisher's own objects, even when nobody was subscribed.
|
|
74
|
+
const dispatched = this.options.freezeEvents && handlers.length > 0
|
|
75
|
+
? createFrozenEventSnapshot(event)
|
|
76
|
+
: event;
|
|
73
77
|
const context = createEventHandlerContext(dispatched, {
|
|
74
78
|
signal: options.signal,
|
|
75
79
|
metadata: options.metadata,
|
|
@@ -87,7 +91,17 @@ export class EventEmitter {
|
|
|
87
91
|
};
|
|
88
92
|
}
|
|
89
93
|
const hooks = {
|
|
90
|
-
|
|
94
|
+
// A bus disposed mid-dispatch has no registered handlers left.
|
|
95
|
+
isRegistered: (handlerId) => {
|
|
96
|
+
if (this.disposed)
|
|
97
|
+
return false;
|
|
98
|
+
try {
|
|
99
|
+
return this.store.hasHandler(handlerId);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
},
|
|
91
105
|
removeOnce: (handlerId) => {
|
|
92
106
|
this.store.unregisterHandler(handlerId);
|
|
93
107
|
},
|
|
@@ -138,7 +152,15 @@ export class EventEmitter {
|
|
|
138
152
|
if (this.disposed) {
|
|
139
153
|
return;
|
|
140
154
|
}
|
|
141
|
-
|
|
155
|
+
/**
|
|
156
|
+
* A shared store (the bus registry) may already have been
|
|
157
|
+
* disposed, in which case its handlers are gone and querying
|
|
158
|
+
* it would throw; disposing the emitter must still succeed.
|
|
159
|
+
*/
|
|
160
|
+
const store = this.store;
|
|
161
|
+
if (typeof store.isDisposed !== "function" || !store.isDisposed()) {
|
|
162
|
+
this.removeAllListeners();
|
|
163
|
+
}
|
|
142
164
|
this.disposed = true;
|
|
143
165
|
}
|
|
144
166
|
isDisposed() {
|
|
@@ -22,12 +22,12 @@ export async function emitParallel(handlers, event, context, errorMode, results,
|
|
|
22
22
|
* an overlapping dispatch cannot invoke them a second time.
|
|
23
23
|
*/
|
|
24
24
|
const runnable = handlers.filter((handler) => {
|
|
25
|
-
if (!handler.once) {
|
|
26
|
-
return true;
|
|
27
|
-
}
|
|
28
25
|
if (!hooks.isRegistered(handler.id)) {
|
|
29
26
|
return false;
|
|
30
27
|
}
|
|
28
|
+
if (!handler.once) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
31
|
hooks.removeOnce(handler.id);
|
|
32
32
|
return true;
|
|
33
33
|
});
|
|
@@ -13,11 +13,13 @@ export async function emitSequential(handlers, event, context, errorMode, result
|
|
|
13
13
|
if (context.signal.aborted) {
|
|
14
14
|
throw createAbortError(event, results, errors);
|
|
15
15
|
}
|
|
16
|
+
// Checked for every handler, not only once-handlers: a handler
|
|
17
|
+
// unsubscribed (or a bus disposed) by an earlier handler in this
|
|
18
|
+
// same dispatch must not run.
|
|
19
|
+
if (!hooks.isRegistered(handler.id)) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
16
22
|
if (handler.once) {
|
|
17
|
-
if (!hooks.isRegistered(handler.id)) {
|
|
18
|
-
// Already consumed by an overlapping dispatch.
|
|
19
|
-
continue;
|
|
20
|
-
}
|
|
21
23
|
hooks.removeOnce(handler.id);
|
|
22
24
|
}
|
|
23
25
|
const started = performance.now();
|
|
@@ -44,7 +44,7 @@ export interface EventEmitterOptions {
|
|
|
44
44
|
readonly maxListeners?: number;
|
|
45
45
|
/**
|
|
46
46
|
* Receives leak warnings. Only applies to the private store.
|
|
47
|
-
* Defaults to
|
|
47
|
+
* Defaults to `process.emitWarning` (type `ZudojsEventsWarning`).
|
|
48
48
|
*/
|
|
49
49
|
readonly onWarning?: (warning: EventRegistryWarning) => void;
|
|
50
50
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Event middleware pipeline execution for Zudojs
|
|
2
|
+
* Event middleware pipeline execution for Zudojs, built on `compose` from
|
|
3
|
+
* `@zudojs/middleware`.
|
|
3
4
|
*/
|
|
4
5
|
import type { Event } from "../eventTypes/eventDefinition.type.js";
|
|
5
6
|
import type { EventMiddlewareContext, EventMiddlewareNext, RegisteredEventMiddleware, EventMiddlewarePipelineResult } from "./eventMiddleware.type.js";
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Event middleware pipeline execution for Zudojs
|
|
2
|
+
* Event middleware pipeline execution for Zudojs, built on `compose` from
|
|
3
|
+
* `@zudojs/middleware`.
|
|
3
4
|
*/
|
|
5
|
+
import { compose } from "@zudojs/middleware";
|
|
4
6
|
import { EventDispatchAbortedError, EventMiddlewareError, toEventError, } from "../eventErrors/eventError.base.js";
|
|
5
|
-
import { sortEventMiddleware, executeEventMiddleware
|
|
7
|
+
import { sortEventMiddleware, executeEventMiddleware } from "./eventMiddleware.helper.js";
|
|
6
8
|
/**
|
|
7
9
|
* Executes a middleware pipeline.
|
|
8
10
|
*
|
|
@@ -21,47 +23,37 @@ import { sortEventMiddleware, executeEventMiddleware, } from "./eventMiddleware.
|
|
|
21
23
|
*/
|
|
22
24
|
export async function executeEventMiddlewarePipeline(middleware, context, terminal) {
|
|
23
25
|
const started = performance.now();
|
|
24
|
-
const activeMiddleware = sortEventMiddleware(middleware.filter((item) => item.enabled));
|
|
25
26
|
const executions = [];
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return terminal();
|
|
44
|
-
}
|
|
27
|
+
const stages = sortEventMiddleware(middleware.filter((item) => item.enabled))
|
|
28
|
+
.map((current) => toStage(current, executions));
|
|
29
|
+
const run = compose(stages, async (ctx) => {
|
|
30
|
+
throwIfAborted(ctx);
|
|
31
|
+
return terminal();
|
|
32
|
+
}, { maxDepth: Number.POSITIVE_INFINITY });
|
|
33
|
+
const result = await run(context);
|
|
34
|
+
return { result, executions, duration: performance.now() - started };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Adapts one registered middleware to the shared composer: checks the
|
|
38
|
+
* abort signal, rejects a second next() with an EventMiddlewareError,
|
|
39
|
+
* records the execution, and wraps only the middleware's own errors.
|
|
40
|
+
*/
|
|
41
|
+
function toStage(current, executions) {
|
|
42
|
+
return async (context, advance) => {
|
|
43
|
+
throwIfAborted(context);
|
|
45
44
|
const middlewareStarted = performance.now();
|
|
45
|
+
const eventType = context.event?.type;
|
|
46
|
+
const eventId = context.event?.id;
|
|
46
47
|
let nextCalled = false;
|
|
47
|
-
/**
|
|
48
|
-
* Errors that surfaced through next() belong to downstream
|
|
49
|
-
* code, not to this middleware; they must pass through
|
|
50
|
-
* unwrapped.
|
|
51
|
-
*/
|
|
52
48
|
let downstreamThrew = false;
|
|
53
49
|
let downstreamError;
|
|
54
50
|
const next = async () => {
|
|
55
51
|
if (nextCalled) {
|
|
56
|
-
throw new EventMiddlewareError(`Middleware "${current.id}" called next() more than once.`, {
|
|
57
|
-
middlewareId: current.id,
|
|
58
|
-
eventType: context.event?.type,
|
|
59
|
-
eventId: context.event?.id,
|
|
60
|
-
});
|
|
52
|
+
throw new EventMiddlewareError(`Middleware "${current.id}" called next() more than once.`, { middlewareId: current.id, eventType, eventId });
|
|
61
53
|
}
|
|
62
54
|
nextCalled = true;
|
|
63
55
|
try {
|
|
64
|
-
return await
|
|
56
|
+
return await advance();
|
|
65
57
|
}
|
|
66
58
|
catch (error) {
|
|
67
59
|
downstreamThrew = true;
|
|
@@ -79,39 +71,29 @@ export async function executeEventMiddlewarePipeline(middleware, context, termin
|
|
|
79
71
|
return result;
|
|
80
72
|
}
|
|
81
73
|
catch (error) {
|
|
82
|
-
if (downstreamThrew && error === downstreamError)
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
if (error instanceof EventMiddlewareError ||
|
|
74
|
+
if ((downstreamThrew && error === downstreamError) ||
|
|
75
|
+
error instanceof EventMiddlewareError ||
|
|
86
76
|
error instanceof EventDispatchAbortedError) {
|
|
87
77
|
throw error;
|
|
88
78
|
}
|
|
89
79
|
throw new EventMiddlewareError(`Event middleware "${current.id}" failed.`, {
|
|
90
80
|
middlewareId: current.id,
|
|
91
|
-
eventType
|
|
92
|
-
eventId
|
|
93
|
-
cause: toEventError(error, {
|
|
94
|
-
eventType: context.event?.type,
|
|
95
|
-
eventId: context.event?.id,
|
|
96
|
-
}),
|
|
81
|
+
eventType,
|
|
82
|
+
eventId,
|
|
83
|
+
cause: toEventError(error, { eventType, eventId }),
|
|
97
84
|
});
|
|
98
85
|
}
|
|
99
86
|
};
|
|
100
|
-
const result = await dispatch(0);
|
|
101
|
-
return {
|
|
102
|
-
result,
|
|
103
|
-
executions,
|
|
104
|
-
duration: performance.now() - started,
|
|
105
|
-
};
|
|
106
87
|
}
|
|
107
88
|
/**
|
|
108
|
-
*
|
|
109
|
-
* aborted signal.
|
|
89
|
+
* Throws the abort error when the pipeline observes an aborted signal.
|
|
110
90
|
*/
|
|
111
|
-
function
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
91
|
+
function throwIfAborted(context) {
|
|
92
|
+
if (context.signal.aborted) {
|
|
93
|
+
throw new EventDispatchAbortedError("Event dispatch was aborted.", {
|
|
94
|
+
eventType: context.event?.type,
|
|
95
|
+
eventId: context.event?.id,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
116
98
|
}
|
|
117
99
|
//# sourceMappingURL=eventMiddleware.pipeline.js.map
|
|
@@ -12,8 +12,18 @@ import { normalizeRegistryEventType, registryRegister, registryRegisterHandler,
|
|
|
12
12
|
import { getHandlersForEvent, getHandlersForType, getAllDefinitions, getAllHandlers, } from "./eventRegistry.queries.js";
|
|
13
13
|
import { registryClear, registryDispose, registryNotify, } from "./eventRegistry.lifecycle.js";
|
|
14
14
|
export { DuplicateEventDefinitionError, EventDefinitionNotFoundError, DuplicateEventHandlerError, EventHandlerNotFoundError, EventRegistryDisposedError, };
|
|
15
|
+
/**
|
|
16
|
+
* Default leak-warning sink: Node's process warning channel (the same one
|
|
17
|
+
* `EventEmitter` uses for `MaxListenersExceededWarning`), which honours
|
|
18
|
+
* `--no-warnings` and `process.on("warning")`, instead of writing to the
|
|
19
|
+
* console directly. A no-op where `process.emitWarning` is unavailable.
|
|
20
|
+
*/
|
|
15
21
|
function defaultWarning(warning) {
|
|
16
|
-
|
|
22
|
+
const emit = globalThis.process?.emitWarning;
|
|
23
|
+
emit?.(`[@zudojs/events] ${warning.message}`, {
|
|
24
|
+
type: "ZudojsEventsWarning",
|
|
25
|
+
code: "ZUDOJS_EVENTS_HANDLER_LIMIT",
|
|
26
|
+
});
|
|
17
27
|
}
|
|
18
28
|
/**
|
|
19
29
|
* Main event registry.
|
|
@@ -55,12 +55,12 @@ export interface EventRegistryOptions {
|
|
|
55
55
|
readonly onDuplicateHandlerId?: DuplicateHandlerIdPolicy;
|
|
56
56
|
/**
|
|
57
57
|
* Maximum handlers per event pattern before a leak warning is
|
|
58
|
-
* emitted through `onWarning` (or
|
|
58
|
+
* emitted through `onWarning` (or `process.emitWarning`). Use 0 to
|
|
59
59
|
* disable. Defaults to 100.
|
|
60
60
|
*/
|
|
61
61
|
readonly maxHandlersPerPattern?: number;
|
|
62
62
|
/**
|
|
63
|
-
* Receives limit warnings. Defaults to
|
|
63
|
+
* Receives limit warnings. Defaults to `process.emitWarning` (type `ZudojsEventsWarning`).
|
|
64
64
|
*/
|
|
65
65
|
readonly onWarning?: (warning: EventRegistryWarning) => void;
|
|
66
66
|
/**
|
|
@@ -151,13 +151,12 @@ function freezeRecursively(value, visited) {
|
|
|
151
151
|
* Removes undefined properties from an object payload.
|
|
152
152
|
*/
|
|
153
153
|
export function stripUndefinedValues(payload) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
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));
|
|
161
160
|
}
|
|
162
161
|
/**
|
|
163
162
|
* Merges two object payloads.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Immutable event snapshots.
|
|
3
|
+
*
|
|
4
|
+
* The emitter used to deep-freeze the event IN PLACE, which froze the
|
|
5
|
+
* publisher's own objects (a later `cart.total = 20` threw) and left
|
|
6
|
+
* Map, Set and Date internals mutable, because `Object.freeze` does not
|
|
7
|
+
* reach them. A snapshot is a frozen COPY: the caller's graph is never
|
|
8
|
+
* touched, and Map, Set and Date are copied into read-only variants
|
|
9
|
+
* whose mutators throw.
|
|
10
|
+
*/
|
|
11
|
+
/** A Date whose setters throw. */
|
|
12
|
+
export declare class FrozenEventDate extends Date {
|
|
13
|
+
}
|
|
14
|
+
/** A Map whose mutators throw once construction has finished. */
|
|
15
|
+
export declare class FrozenEventMap<K, V> extends Map<K, V> {
|
|
16
|
+
set(): this;
|
|
17
|
+
delete(): boolean;
|
|
18
|
+
clear(): void;
|
|
19
|
+
}
|
|
20
|
+
/** A Set whose mutators throw once construction has finished. */
|
|
21
|
+
export declare class FrozenEventSet<T> extends Set<T> {
|
|
22
|
+
add(): this;
|
|
23
|
+
delete(): boolean;
|
|
24
|
+
clear(): void;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Returns a deeply frozen copy of `value`, leaving `value` untouched.
|
|
28
|
+
*
|
|
29
|
+
* Plain objects, arrays and errors are copied (own properties, including
|
|
30
|
+
* a `__proto__` data key, are defined rather than assigned) and frozen.
|
|
31
|
+
* Map, Set and Date become {@link FrozenEventMap}, {@link FrozenEventSet}
|
|
32
|
+
* and {@link FrozenEventDate}, which still pass `instanceof Map` / `Set`
|
|
33
|
+
* / `Date` but throw on mutation. Class instances and other exotic
|
|
34
|
+
* objects are passed by reference. Cycles are preserved.
|
|
35
|
+
*/
|
|
36
|
+
export declare function createFrozenEventSnapshot<T>(value: T): T;
|
|
37
|
+
//# sourceMappingURL=eventSnapshot.freeze.d.ts.map
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Immutable event snapshots.
|
|
3
|
+
*
|
|
4
|
+
* The emitter used to deep-freeze the event IN PLACE, which froze the
|
|
5
|
+
* publisher's own objects (a later `cart.total = 20` threw) and left
|
|
6
|
+
* Map, Set and Date internals mutable, because `Object.freeze` does not
|
|
7
|
+
* reach them. A snapshot is a frozen COPY: the caller's graph is never
|
|
8
|
+
* touched, and Map, Set and Date are copied into read-only variants
|
|
9
|
+
* whose mutators throw.
|
|
10
|
+
*/
|
|
11
|
+
/** Throws the TypeError every read-only mutator raises. */
|
|
12
|
+
function readOnly(kind) {
|
|
13
|
+
throw new TypeError(`Cannot modify a frozen event ${kind}.`);
|
|
14
|
+
}
|
|
15
|
+
/** A Date whose setters throw. */
|
|
16
|
+
export class FrozenEventDate extends Date {
|
|
17
|
+
}
|
|
18
|
+
for (const key of Object.getOwnPropertyNames(Date.prototype)) {
|
|
19
|
+
if (key.startsWith("set")) {
|
|
20
|
+
Object.defineProperty(FrozenEventDate.prototype, key, {
|
|
21
|
+
value: () => readOnly("Date"),
|
|
22
|
+
configurable: true,
|
|
23
|
+
writable: true,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** A Map whose mutators throw once construction has finished. */
|
|
28
|
+
export class FrozenEventMap extends Map {
|
|
29
|
+
set() {
|
|
30
|
+
return readOnly("Map");
|
|
31
|
+
}
|
|
32
|
+
delete() {
|
|
33
|
+
return readOnly("Map");
|
|
34
|
+
}
|
|
35
|
+
clear() {
|
|
36
|
+
readOnly("Map");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** A Set whose mutators throw once construction has finished. */
|
|
40
|
+
export class FrozenEventSet extends Set {
|
|
41
|
+
add() {
|
|
42
|
+
return readOnly("Set");
|
|
43
|
+
}
|
|
44
|
+
delete() {
|
|
45
|
+
return readOnly("Set");
|
|
46
|
+
}
|
|
47
|
+
clear() {
|
|
48
|
+
readOnly("Set");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Whether a value is a plain object (literal or null-prototype). */
|
|
52
|
+
function isPlainRecord(value) {
|
|
53
|
+
const proto = Object.getPrototypeOf(value);
|
|
54
|
+
return proto === Object.prototype || proto === null;
|
|
55
|
+
}
|
|
56
|
+
function snapshot(value, seen) {
|
|
57
|
+
if (typeof value !== "object" || value === null)
|
|
58
|
+
return value;
|
|
59
|
+
if (seen.has(value))
|
|
60
|
+
return seen.get(value);
|
|
61
|
+
if (value instanceof Date) {
|
|
62
|
+
const copy = Object.freeze(new FrozenEventDate(value.getTime()));
|
|
63
|
+
seen.set(value, copy);
|
|
64
|
+
return copy;
|
|
65
|
+
}
|
|
66
|
+
if (value instanceof Map) {
|
|
67
|
+
const copy = new FrozenEventMap();
|
|
68
|
+
seen.set(value, copy);
|
|
69
|
+
for (const [key, item] of value) {
|
|
70
|
+
Map.prototype.set.call(copy, key, snapshot(item, seen));
|
|
71
|
+
}
|
|
72
|
+
return Object.freeze(copy);
|
|
73
|
+
}
|
|
74
|
+
if (value instanceof Set) {
|
|
75
|
+
const copy = new FrozenEventSet();
|
|
76
|
+
seen.set(value, copy);
|
|
77
|
+
for (const item of value) {
|
|
78
|
+
Set.prototype.add.call(copy, snapshot(item, seen));
|
|
79
|
+
}
|
|
80
|
+
return Object.freeze(copy);
|
|
81
|
+
}
|
|
82
|
+
const isArray = Array.isArray(value);
|
|
83
|
+
const isError = value instanceof Error;
|
|
84
|
+
// Instances of other classes (and exotic built-ins such as typed
|
|
85
|
+
// arrays, RegExp, URL) cannot be copied faithfully — private fields and
|
|
86
|
+
// internal slots do not survive — so they are passed by reference.
|
|
87
|
+
if (!isArray && !isError && !isPlainRecord(value))
|
|
88
|
+
return value;
|
|
89
|
+
const copy = isArray
|
|
90
|
+
? []
|
|
91
|
+
: Object.create(Object.getPrototypeOf(value));
|
|
92
|
+
seen.set(value, copy);
|
|
93
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
94
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
95
|
+
if (!descriptor)
|
|
96
|
+
continue;
|
|
97
|
+
if ("value" in descriptor) {
|
|
98
|
+
descriptor.value = snapshot(descriptor.value, seen);
|
|
99
|
+
}
|
|
100
|
+
Object.defineProperty(copy, key, descriptor);
|
|
101
|
+
}
|
|
102
|
+
return Object.freeze(copy);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Returns a deeply frozen copy of `value`, leaving `value` untouched.
|
|
106
|
+
*
|
|
107
|
+
* Plain objects, arrays and errors are copied (own properties, including
|
|
108
|
+
* a `__proto__` data key, are defined rather than assigned) and frozen.
|
|
109
|
+
* Map, Set and Date become {@link FrozenEventMap}, {@link FrozenEventSet}
|
|
110
|
+
* and {@link FrozenEventDate}, which still pass `instanceof Map` / `Set`
|
|
111
|
+
* / `Date` but throw on mutation. Class instances and other exotic
|
|
112
|
+
* objects are passed by reference. Cycles are preserved.
|
|
113
|
+
*/
|
|
114
|
+
export function createFrozenEventSnapshot(value) {
|
|
115
|
+
return snapshot(value, new Map());
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=eventSnapshot.freeze.js.map
|
|
@@ -6,4 +6,5 @@
|
|
|
6
6
|
export { type EventId, type EventType, type EventTimestamp, type EventSource, type EventCorrelationId, type EventCausationId, type EventPayload, type Event, type EventInput, type EventDefinition, isEvent, createEventId, createEvent, defineEvent, withEventMetadata, createDerivedEvent, getEventType, getEventPayload, describeEvent, } from "./eventDefinition.type.js";
|
|
7
7
|
export { type ObjectEventPayload, type PrimitiveEventPayload, type JsonEventPayload, type EventPayloadMap, type PayloadOf, type PayloadMap, type EventPayloadFactory, type EventPayloadOptions, isPrimitiveEventPayload, isObjectEventPayload, isJsonEventPayload, createEventPayload, createObjectEventPayload, createJsonEventPayload, cloneEventPayload, deepFreeze, stripUndefinedValues, mergeEventPayloads, staticPayload, definePayloadFactory, validateEventPayload, describeEventPayload, } from "./eventPayload.type.js";
|
|
8
8
|
export { type EventTypeList, type EventTypePattern, type EventTypeOf, type EventUnion, isValidEventType, isValidEventTypePattern, normalizeEventType, normalizeEventTypePattern, tryNormalizeEventType, assertEventType, createEventType, createEventTypePattern, getEventNamespace, getEventAction, getEventTypeSegments, matchesEventType, isSameEventNamespace, isChildEventType, eventMatchesType, filterEventsByType, defineEventTypes, defineEventType, } from "./eventType.type.js";
|
|
9
|
+
export { createFrozenEventSnapshot, FrozenEventDate, FrozenEventMap, FrozenEventSet, } from "./eventSnapshot.freeze.js";
|
|
9
10
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/eventTypes/index.js
CHANGED
|
@@ -6,4 +6,5 @@
|
|
|
6
6
|
export { isEvent, createEventId, createEvent, defineEvent, withEventMetadata, createDerivedEvent, getEventType, getEventPayload, describeEvent, } from "./eventDefinition.type.js";
|
|
7
7
|
export { isPrimitiveEventPayload, isObjectEventPayload, isJsonEventPayload, createEventPayload, createObjectEventPayload, createJsonEventPayload, cloneEventPayload, deepFreeze, stripUndefinedValues, mergeEventPayloads, staticPayload, definePayloadFactory, validateEventPayload, describeEventPayload, } from "./eventPayload.type.js";
|
|
8
8
|
export { isValidEventType, isValidEventTypePattern, normalizeEventType, normalizeEventTypePattern, tryNormalizeEventType, assertEventType, createEventType, createEventTypePattern, getEventNamespace, getEventAction, getEventTypeSegments, matchesEventType, isSameEventNamespace, isChildEventType, eventMatchesType, filterEventsByType, defineEventTypes, defineEventType, } from "./eventType.type.js";
|
|
9
|
+
export { createFrozenEventSnapshot, FrozenEventDate, FrozenEventMap, FrozenEventSet, } from "./eventSnapshot.freeze.js";
|
|
9
10
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/events",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.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,8 +20,9 @@
|
|
|
20
20
|
],
|
|
21
21
|
"sideEffects": false,
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@zudojs/
|
|
24
|
-
"@zudojs/
|
|
23
|
+
"@zudojs/constants": "1.1.0",
|
|
24
|
+
"@zudojs/errors": "1.1.0",
|
|
25
|
+
"@zudojs/middleware": "1.0.2"
|
|
25
26
|
},
|
|
26
27
|
"engines": {
|
|
27
28
|
"node": ">=24.0.0"
|
|
@@ -31,6 +32,10 @@
|
|
|
31
32
|
"typescript": "7.0.2"
|
|
32
33
|
},
|
|
33
34
|
"license": "MIT",
|
|
35
|
+
"author": {
|
|
36
|
+
"name": "Oluwayemi Oyinlola",
|
|
37
|
+
"url": "https://github.com/oyinlola-tech"
|
|
38
|
+
},
|
|
34
39
|
"publishConfig": {
|
|
35
40
|
"access": "public"
|
|
36
41
|
},
|