@zudojs/messaging 0.0.1 → 1.0.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 +79 -7
- package/dist/dispatcher/dispatcherCore.d.ts +38 -3
- package/dist/dispatcher/dispatcherCore.js +149 -23
- package/dist/dispatcher/dispatcherType.type.d.ts +16 -5
- package/dist/handlerRegistry/handlerRegistryStore.d.ts +10 -0
- package/dist/handlerRegistry/handlerRegistryStore.js +26 -2
- package/dist/handlerRegistry/handlerRegistryType.type.d.ts +6 -3
- package/dist/message/index.d.ts +1 -1
- package/dist/message/index.js +1 -1
- package/dist/message/messageFactory.d.ts +17 -1
- package/dist/message/messageFactory.js +33 -0
- package/dist/messageBus/messageBusCore.d.ts +4 -3
- package/dist/messageBus/messageBusCore.js +20 -25
- package/dist/messageBus/messageBusType.type.d.ts +12 -4
- package/dist/messageMiddleware/messageMiddlewarePipeline.js +17 -6
- package/dist/messageMiddleware/messageMiddlewareType.type.d.ts +6 -0
- package/package.json +14 -6
- package/dist/.tsbuildinfo +0 -1
- package/dist/dispatcher/dispatcherCore.d.ts.map +0 -1
- package/dist/dispatcher/dispatcherCore.js.map +0 -1
- package/dist/dispatcher/dispatcherType.type.d.ts.map +0 -1
- package/dist/dispatcher/dispatcherType.type.js.map +0 -1
- package/dist/dispatcher/index.d.ts.map +0 -1
- package/dist/dispatcher/index.js.map +0 -1
- package/dist/errors/index.d.ts.map +0 -1
- package/dist/errors/index.js.map +0 -1
- package/dist/handlerRegistry/handlerRegistryStore.d.ts.map +0 -1
- package/dist/handlerRegistry/handlerRegistryStore.js.map +0 -1
- package/dist/handlerRegistry/handlerRegistryType.type.d.ts.map +0 -1
- package/dist/handlerRegistry/handlerRegistryType.type.js.map +0 -1
- package/dist/handlerRegistry/index.d.ts.map +0 -1
- package/dist/handlerRegistry/index.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/message/index.d.ts.map +0 -1
- package/dist/message/index.js.map +0 -1
- package/dist/message/messageFactory.d.ts.map +0 -1
- package/dist/message/messageFactory.js.map +0 -1
- package/dist/message/messageType.type.d.ts.map +0 -1
- package/dist/message/messageType.type.js.map +0 -1
- package/dist/messageBus/index.d.ts.map +0 -1
- package/dist/messageBus/index.js.map +0 -1
- package/dist/messageBus/messageBusCore.d.ts.map +0 -1
- package/dist/messageBus/messageBusCore.js.map +0 -1
- package/dist/messageBus/messageBusType.type.d.ts.map +0 -1
- package/dist/messageBus/messageBusType.type.js.map +0 -1
- package/dist/messageContext/index.d.ts.map +0 -1
- package/dist/messageContext/index.js.map +0 -1
- package/dist/messageContext/messageContextType.type.d.ts.map +0 -1
- package/dist/messageContext/messageContextType.type.js.map +0 -1
- package/dist/messageHandler/index.d.ts.map +0 -1
- package/dist/messageHandler/index.js.map +0 -1
- package/dist/messageHandler/messageHandlerType.type.d.ts.map +0 -1
- package/dist/messageHandler/messageHandlerType.type.js.map +0 -1
- package/dist/messageMiddleware/index.d.ts.map +0 -1
- package/dist/messageMiddleware/index.js.map +0 -1
- package/dist/messageMiddleware/messageMiddlewarePipeline.d.ts.map +0 -1
- package/dist/messageMiddleware/messageMiddlewarePipeline.js.map +0 -1
- package/dist/messageMiddleware/messageMiddlewareType.type.d.ts.map +0 -1
- package/dist/messageMiddleware/messageMiddlewareType.type.js.map +0 -1
package/README.md
CHANGED
|
@@ -15,23 +15,95 @@ import { createMessageBus } from "@zudojs/messaging";
|
|
|
15
15
|
|
|
16
16
|
const bus = createMessageBus();
|
|
17
17
|
|
|
18
|
-
bus.
|
|
18
|
+
bus.on("user.created", (message) => {
|
|
19
19
|
console.log("New user:", message.payload);
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
-
await bus.
|
|
22
|
+
await bus.send({
|
|
23
23
|
type: "user.created",
|
|
24
24
|
payload: { id: "123" },
|
|
25
25
|
});
|
|
26
|
+
|
|
27
|
+
bus.dispose();
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Use `send` to build and dispatch a message from plain input, or
|
|
31
|
+
`dispatch` when you already hold a `Message`. Both run the middleware
|
|
32
|
+
pipeline before reaching handlers. A disposed bus rejects further use
|
|
33
|
+
with `MessageBusDisposedError`.
|
|
34
|
+
|
|
35
|
+
## Message identifiers
|
|
36
|
+
|
|
37
|
+
`MessageId`, `MessageCorrelationId` and `MessageCausationId` are branded
|
|
38
|
+
types, so a plain string from a transport frame or a database row is not
|
|
39
|
+
one. `createMessageId()` mints a new identifier; `toMessageId`,
|
|
40
|
+
`toCorrelationId` and `toCausationId` brand an existing string, rejecting
|
|
41
|
+
blank values:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { createMessage, toMessageId, toCorrelationId } from "@zudojs/messaging";
|
|
45
|
+
|
|
46
|
+
const message = createMessage({
|
|
47
|
+
id: toMessageId(frame.id),
|
|
48
|
+
type: frame.type,
|
|
49
|
+
payload: frame.payload,
|
|
50
|
+
correlationId: toCorrelationId(frame.correlationId),
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Middleware
|
|
55
|
+
|
|
56
|
+
`use` returns the id `removeMiddleware` takes, so middleware can be taken off
|
|
57
|
+
again. Middleware runs in ascending `priority` order (default 100), with
|
|
58
|
+
registration order breaking ties:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
const id = bus.use(logging, { id: "logging", priority: 10 });
|
|
62
|
+
bus.use(validation, { priority: 20 });
|
|
63
|
+
|
|
64
|
+
bus.removeMiddleware(id); // true
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`enabled: false` registers middleware without running it.
|
|
68
|
+
|
|
69
|
+
## Handlers
|
|
70
|
+
|
|
71
|
+
By default a message type may have many handlers and all of them run. Pass
|
|
72
|
+
`allowMultipleHandlers: false` for a command/query bus, where a second handler
|
|
73
|
+
for the same type is a wiring mistake and is rejected at registration:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
const bus = createMessageBus({ allowMultipleHandlers: false });
|
|
26
77
|
```
|
|
27
78
|
|
|
79
|
+
`DispatchResult.handlerResults` records every handler that ran, including the
|
|
80
|
+
one that failed, with its real duration.
|
|
81
|
+
|
|
82
|
+
## Timeouts
|
|
83
|
+
|
|
84
|
+
`timeout` is honoured by the dispatcher itself, so it applies whether you hold
|
|
85
|
+
a bus or a dispatcher. On expiry the dispatch context's `AbortSignal` is
|
|
86
|
+
aborted — a handler watching it can wind down — and the result carries a
|
|
87
|
+
`MessageTimeoutError`:
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
const result = await bus.dispatch(message, { timeout: 5_000 });
|
|
91
|
+
if (!result.success && result.error instanceof MessageTimeoutError) {
|
|
92
|
+
// …
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`createMessageBus({ defaultTimeout })` sets the default for every dispatch.
|
|
97
|
+
|
|
28
98
|
## Features
|
|
29
99
|
|
|
30
|
-
- Message bus with
|
|
31
|
-
- Middleware pipeline
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
100
|
+
- Message bus with handler registration and dispatch
|
|
101
|
+
- Middleware pipeline with priorities, removal and per-execution telemetry
|
|
102
|
+
- Named message handlers with priorities
|
|
103
|
+
- Optional single-handler enforcement per message type
|
|
104
|
+
- Per-dispatch timeouts that abort the handler through `AbortSignal`
|
|
105
|
+
- Correlation and causation tracking
|
|
106
|
+
- Branded message identifiers
|
|
35
107
|
|
|
36
108
|
## Use Cases
|
|
37
109
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @module dispatcher/dispatcherCore
|
|
5
5
|
*/
|
|
6
6
|
import type { Message } from "../message/messageType.type.js";
|
|
7
|
-
import type { MessageMiddlewareLike } from "../messageMiddleware/messageMiddlewareType.type.js";
|
|
7
|
+
import type { MessageMiddlewareLike, MessageMiddlewareOptions } from "../messageMiddleware/messageMiddlewareType.type.js";
|
|
8
8
|
import type { Dispatcher, DispatchResult, DispatchOptions } from "./dispatcherType.type.js";
|
|
9
9
|
import { HandlerRegistryStore } from "../handlerRegistry/handlerRegistryStore.js";
|
|
10
10
|
/**
|
|
@@ -12,16 +12,51 @@ import { HandlerRegistryStore } from "../handlerRegistry/handlerRegistryStore.js
|
|
|
12
12
|
*/
|
|
13
13
|
export declare class DefaultDispatcher implements Dispatcher {
|
|
14
14
|
private readonly registry;
|
|
15
|
+
/**
|
|
16
|
+
* Registered global middleware, in registration order.
|
|
17
|
+
*
|
|
18
|
+
* Held as {@link RegisteredMessageMiddleware} rather than bare functions:
|
|
19
|
+
* without an identity, `removeMiddleware` had nothing to match on and
|
|
20
|
+
* unconditionally returned false, and the `priority` the interface accepts
|
|
21
|
+
* had nowhere to live.
|
|
22
|
+
*/
|
|
15
23
|
private readonly globalMiddleware;
|
|
24
|
+
private middlewareSequence;
|
|
16
25
|
private disposed;
|
|
17
26
|
constructor(registry?: HandlerRegistryStore);
|
|
18
27
|
dispatch<TPayload, TResult>(message: Message<TPayload>, options?: DispatchOptions<TResult>): Promise<DispatchResult<TResult>>;
|
|
28
|
+
/** A promise that rejects with {@link MessageTimeoutError} and aborts. */
|
|
29
|
+
private timeoutRejection;
|
|
30
|
+
/** Global middleware ordered by priority, with disabled entries dropped. */
|
|
31
|
+
private orderedMiddleware;
|
|
19
32
|
private validateNotDisposed;
|
|
33
|
+
/**
|
|
34
|
+
* Resolves the signal handlers observe.
|
|
35
|
+
*
|
|
36
|
+
* The dispatcher's own controller is chained to the caller's signal so a
|
|
37
|
+
* timeout can abort the work without the caller losing its own cancellation.
|
|
38
|
+
*/
|
|
20
39
|
private resolveSignal;
|
|
21
40
|
private executeHandlers;
|
|
22
41
|
private executeHandler;
|
|
23
|
-
|
|
24
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Registers global middleware.
|
|
44
|
+
*
|
|
45
|
+
* @param middleware - The middleware to register.
|
|
46
|
+
* @param options - Identity, priority and enablement.
|
|
47
|
+
* @returns The identifier {@link DefaultDispatcher.removeMiddleware} takes.
|
|
48
|
+
* @throws {MessageMiddlewareError} when the requested id is already taken.
|
|
49
|
+
*/
|
|
50
|
+
use<TMessage extends Message = Message, TResult = unknown>(middleware: MessageMiddlewareLike<TMessage, TResult>, options?: MessageMiddlewareOptions): string;
|
|
51
|
+
/**
|
|
52
|
+
* Removes previously registered global middleware.
|
|
53
|
+
*
|
|
54
|
+
* @param middlewareId - The id returned by {@link DefaultDispatcher.use}.
|
|
55
|
+
* @returns Whether a registration was removed.
|
|
56
|
+
*/
|
|
57
|
+
removeMiddleware(middlewareId: string): boolean;
|
|
58
|
+
/** The ids of every registered global middleware, in priority order. */
|
|
59
|
+
listMiddleware(): readonly string[];
|
|
25
60
|
getRegistry(): HandlerRegistryStore;
|
|
26
61
|
dispose(): void;
|
|
27
62
|
}
|
|
@@ -6,13 +6,24 @@
|
|
|
6
6
|
import { createMessageContext } from "../messageContext/messageContextType.type.js";
|
|
7
7
|
import { HandlerRegistryStore } from "../handlerRegistry/handlerRegistryStore.js";
|
|
8
8
|
import { runMessagePipeline } from "../messageMiddleware/messageMiddlewarePipeline.js";
|
|
9
|
-
import { MessageDispatchAbortedError, MessageHandlerError,
|
|
9
|
+
import { MessageDispatchAbortedError, MessageHandlerError, MessageMiddlewareError, MessageTimeoutError, MessageBusDisposedError, } from "@zudojs/errors";
|
|
10
|
+
/** Default priority for middleware that does not declare one. */
|
|
11
|
+
const DEFAULT_MIDDLEWARE_PRIORITY = 100;
|
|
10
12
|
/**
|
|
11
13
|
* Default dispatcher implementation.
|
|
12
14
|
*/
|
|
13
15
|
export class DefaultDispatcher {
|
|
14
16
|
registry;
|
|
17
|
+
/**
|
|
18
|
+
* Registered global middleware, in registration order.
|
|
19
|
+
*
|
|
20
|
+
* Held as {@link RegisteredMessageMiddleware} rather than bare functions:
|
|
21
|
+
* without an identity, `removeMiddleware` had nothing to match on and
|
|
22
|
+
* unconditionally returned false, and the `priority` the interface accepts
|
|
23
|
+
* had nowhere to live.
|
|
24
|
+
*/
|
|
15
25
|
globalMiddleware = [];
|
|
26
|
+
middlewareSequence = 0;
|
|
16
27
|
disposed = false;
|
|
17
28
|
constructor(registry) {
|
|
18
29
|
this.registry = registry ?? new HandlerRegistryStore();
|
|
@@ -20,23 +31,44 @@ export class DefaultDispatcher {
|
|
|
20
31
|
async dispatch(message, options = {}) {
|
|
21
32
|
const dispatchStart = performance.now();
|
|
22
33
|
this.validateNotDisposed(message);
|
|
23
|
-
const
|
|
34
|
+
const timeout = options.timeout ?? 0;
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
const signal = this.resolveSignal(options.signal, controller);
|
|
24
37
|
const context = createMessageContext(message, {
|
|
25
38
|
...options.context,
|
|
26
39
|
signal,
|
|
27
40
|
});
|
|
41
|
+
const ordered = this.orderedMiddleware();
|
|
42
|
+
const perDispatch = options.middleware ?? [];
|
|
28
43
|
const allMiddleware = [
|
|
29
|
-
...
|
|
30
|
-
...
|
|
44
|
+
...ordered.map((entry) => entry.middleware),
|
|
45
|
+
...perDispatch,
|
|
46
|
+
];
|
|
47
|
+
const middlewareIds = [
|
|
48
|
+
...ordered.map((entry) => entry.id),
|
|
49
|
+
...perDispatch.map((_mw, index) => `dispatch:${index}`),
|
|
31
50
|
];
|
|
32
51
|
const handlers = this.registry.resolve(message.type);
|
|
33
52
|
const handlerResults = [];
|
|
53
|
+
let timer;
|
|
34
54
|
try {
|
|
35
|
-
const
|
|
55
|
+
const run = runMessagePipeline(allMiddleware, async (msg, mwCtx) => this.executeHandlers(msg, handlers, handlerResults, mwCtx.signal), message, {
|
|
36
56
|
signal: context.signal,
|
|
37
57
|
metadata: options.context?.headers,
|
|
38
58
|
state: options.context?.state,
|
|
59
|
+
middlewareIds,
|
|
39
60
|
});
|
|
61
|
+
// `DispatchOptions.timeout` was documented on the dispatcher but only
|
|
62
|
+
// ever honoured by the bus wrapper, so anyone holding a dispatcher
|
|
63
|
+
// directly got no timeout at all.
|
|
64
|
+
const pipelineResult = timeout > 0
|
|
65
|
+
? await Promise.race([
|
|
66
|
+
run,
|
|
67
|
+
this.timeoutRejection(message, timeout, controller, (t) => {
|
|
68
|
+
timer = t;
|
|
69
|
+
}),
|
|
70
|
+
])
|
|
71
|
+
: await run;
|
|
40
72
|
return {
|
|
41
73
|
success: true,
|
|
42
74
|
value: pipelineResult.result,
|
|
@@ -57,28 +89,82 @@ export class DefaultDispatcher {
|
|
|
57
89
|
duration: performance.now() - dispatchStart,
|
|
58
90
|
};
|
|
59
91
|
}
|
|
92
|
+
finally {
|
|
93
|
+
if (timer !== undefined)
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** A promise that rejects with {@link MessageTimeoutError} and aborts. */
|
|
98
|
+
timeoutRejection(message, timeout, controller, keepTimer) {
|
|
99
|
+
return new Promise((_resolve, reject) => {
|
|
100
|
+
const timer = setTimeout(() => {
|
|
101
|
+
const error = new MessageTimeoutError(timeout, {
|
|
102
|
+
messageType: message.type,
|
|
103
|
+
messageId: message.id,
|
|
104
|
+
});
|
|
105
|
+
// Abort first so a handler watching its signal can wind down rather
|
|
106
|
+
// than running on past the dispatch that gave up on it.
|
|
107
|
+
controller.abort(error);
|
|
108
|
+
reject(error);
|
|
109
|
+
}, timeout);
|
|
110
|
+
if (timer.unref)
|
|
111
|
+
timer.unref();
|
|
112
|
+
keepTimer(timer);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/** Global middleware ordered by priority, with disabled entries dropped. */
|
|
116
|
+
orderedMiddleware() {
|
|
117
|
+
return this.globalMiddleware
|
|
118
|
+
.filter((entry) => entry.enabled)
|
|
119
|
+
.map((entry, index) => ({ entry, index }))
|
|
120
|
+
.sort((a, b) => a.entry.priority - b.entry.priority || a.index - b.index)
|
|
121
|
+
.map(({ entry }) => entry);
|
|
60
122
|
}
|
|
61
|
-
validateNotDisposed(
|
|
123
|
+
validateNotDisposed(_message) {
|
|
62
124
|
if (this.disposed)
|
|
63
|
-
throw new
|
|
125
|
+
throw new MessageBusDisposedError();
|
|
64
126
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Resolves the signal handlers observe.
|
|
129
|
+
*
|
|
130
|
+
* The dispatcher's own controller is chained to the caller's signal so a
|
|
131
|
+
* timeout can abort the work without the caller losing its own cancellation.
|
|
132
|
+
*/
|
|
133
|
+
resolveSignal(signal, controller) {
|
|
134
|
+
if (signal?.aborted)
|
|
68
135
|
throw new MessageDispatchAbortedError();
|
|
69
|
-
|
|
136
|
+
if (signal) {
|
|
137
|
+
signal.addEventListener("abort", () => controller.abort(signal.reason), {
|
|
138
|
+
once: true,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return controller.signal;
|
|
70
142
|
}
|
|
71
143
|
async executeHandlers(message, handlers, handlerResults, signal) {
|
|
72
144
|
const results = [];
|
|
73
145
|
for (const handler of handlers) {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
146
|
+
const start = performance.now();
|
|
147
|
+
try {
|
|
148
|
+
const result = await this.executeHandler(handler, message, signal);
|
|
149
|
+
results.push(result);
|
|
150
|
+
handlerResults.push({
|
|
151
|
+
handlerId: handler.id,
|
|
152
|
+
success: true,
|
|
153
|
+
value: result,
|
|
154
|
+
duration: performance.now() - start,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
// A failing handler used to leave no trace at all in handlerResults,
|
|
159
|
+
// so a caller inspecting them could not tell which handler broke.
|
|
160
|
+
handlerResults.push({
|
|
161
|
+
handlerId: handler.id,
|
|
162
|
+
success: false,
|
|
163
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
164
|
+
duration: performance.now() - start,
|
|
165
|
+
});
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
82
168
|
}
|
|
83
169
|
return results.length === 1 ? results[0] : results;
|
|
84
170
|
}
|
|
@@ -98,17 +184,57 @@ export class DefaultDispatcher {
|
|
|
98
184
|
});
|
|
99
185
|
}
|
|
100
186
|
}
|
|
101
|
-
|
|
102
|
-
|
|
187
|
+
/**
|
|
188
|
+
* Registers global middleware.
|
|
189
|
+
*
|
|
190
|
+
* @param middleware - The middleware to register.
|
|
191
|
+
* @param options - Identity, priority and enablement.
|
|
192
|
+
* @returns The identifier {@link DefaultDispatcher.removeMiddleware} takes.
|
|
193
|
+
* @throws {MessageMiddlewareError} when the requested id is already taken.
|
|
194
|
+
*/
|
|
195
|
+
use(middleware, options = {}) {
|
|
196
|
+
const id = options.id ?? `middleware:${++this.middlewareSequence}`;
|
|
197
|
+
if (this.globalMiddleware.some((entry) => entry.id === id)) {
|
|
198
|
+
throw new MessageMiddlewareError(`Middleware "${id}" is already registered.`, { middlewareId: id });
|
|
199
|
+
}
|
|
200
|
+
const entry = {
|
|
201
|
+
id,
|
|
202
|
+
priority: options.priority ?? DEFAULT_MIDDLEWARE_PRIORITY,
|
|
203
|
+
enabled: options.enabled ?? true,
|
|
204
|
+
middleware: middleware,
|
|
205
|
+
...(options.description !== undefined
|
|
206
|
+
? { description: options.description }
|
|
207
|
+
: {}),
|
|
208
|
+
};
|
|
209
|
+
this.globalMiddleware.push(entry);
|
|
210
|
+
return id;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Removes previously registered global middleware.
|
|
214
|
+
*
|
|
215
|
+
* @param middlewareId - The id returned by {@link DefaultDispatcher.use}.
|
|
216
|
+
* @returns Whether a registration was removed.
|
|
217
|
+
*/
|
|
218
|
+
removeMiddleware(middlewareId) {
|
|
219
|
+
const index = this.globalMiddleware.findIndex((entry) => entry.id === middlewareId);
|
|
220
|
+
if (index === -1)
|
|
221
|
+
return false;
|
|
222
|
+
this.globalMiddleware.splice(index, 1);
|
|
223
|
+
return true;
|
|
103
224
|
}
|
|
104
|
-
|
|
105
|
-
|
|
225
|
+
/** The ids of every registered global middleware, in priority order. */
|
|
226
|
+
listMiddleware() {
|
|
227
|
+
return this.globalMiddleware
|
|
228
|
+
.map((entry, index) => ({ entry, index }))
|
|
229
|
+
.sort((a, b) => a.entry.priority - b.entry.priority || a.index - b.index)
|
|
230
|
+
.map(({ entry }) => entry.id);
|
|
106
231
|
}
|
|
107
232
|
getRegistry() {
|
|
108
233
|
return this.registry;
|
|
109
234
|
}
|
|
110
235
|
dispose() {
|
|
111
236
|
this.disposed = true;
|
|
237
|
+
this.globalMiddleware.length = 0;
|
|
112
238
|
}
|
|
113
239
|
}
|
|
114
240
|
export function createDispatcher(registry) {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { Message } from "../message/messageType.type.js";
|
|
10
10
|
import type { MessageContext, MessageContextOptions } from "../messageContext/messageContextType.type.js";
|
|
11
|
-
import type { MessageMiddlewareLike, MessageMiddlewarePipelineResult } from "../messageMiddleware/messageMiddlewareType.type.js";
|
|
11
|
+
import type { MessageMiddlewareLike, MessageMiddlewareOptions, MessageMiddlewarePipelineResult } from "../messageMiddleware/messageMiddlewareType.type.js";
|
|
12
12
|
/**
|
|
13
13
|
* Result of dispatching a message.
|
|
14
14
|
*/
|
|
@@ -53,7 +53,12 @@ export interface DispatchOptions<TResult = unknown> {
|
|
|
53
53
|
readonly context?: MessageContextOptions;
|
|
54
54
|
/** Middleware to apply for this dispatch. */
|
|
55
55
|
readonly middleware?: readonly MessageMiddlewareLike[];
|
|
56
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Timeout in ms. 0 = no timeout. Default: 0.
|
|
58
|
+
*
|
|
59
|
+
* On expiry the dispatch is aborted through `context.signal` and the result
|
|
60
|
+
* carries a `MessageTimeoutError`.
|
|
61
|
+
*/
|
|
57
62
|
readonly timeout?: number;
|
|
58
63
|
/** AbortSignal for cancellation. */
|
|
59
64
|
readonly signal?: AbortSignal;
|
|
@@ -71,12 +76,18 @@ export interface Dispatcher {
|
|
|
71
76
|
dispatch<TPayload, TResult>(message: Message<TPayload>, options?: DispatchOptions<TResult>): Promise<DispatchResult<TResult>>;
|
|
72
77
|
/**
|
|
73
78
|
* Registers middleware for all dispatches.
|
|
79
|
+
*
|
|
80
|
+
* Middleware runs in ascending `priority` order (default 100), registration
|
|
81
|
+
* order breaking ties.
|
|
82
|
+
*
|
|
83
|
+
* @returns The identifier {@link Dispatcher.removeMiddleware} takes, which
|
|
84
|
+
* is `options.id` when one was supplied.
|
|
74
85
|
*/
|
|
75
|
-
use<TMessage extends Message = Message, TResult = unknown>(middleware: MessageMiddlewareLike<TMessage, TResult>, options?:
|
|
76
|
-
priority?: number;
|
|
77
|
-
}): void;
|
|
86
|
+
use<TMessage extends Message = Message, TResult = unknown>(middleware: MessageMiddlewareLike<TMessage, TResult>, options?: MessageMiddlewareOptions): string;
|
|
78
87
|
/**
|
|
79
88
|
* Removes middleware by ID.
|
|
89
|
+
*
|
|
90
|
+
* @returns Whether a registration was removed.
|
|
80
91
|
*/
|
|
81
92
|
removeMiddleware(middlewareId: string): boolean;
|
|
82
93
|
}
|
|
@@ -16,6 +16,16 @@ export declare class HandlerRegistryStore {
|
|
|
16
16
|
constructor(options?: HandlerRegistryOptions);
|
|
17
17
|
register<TMessage extends Message, TResult>(handler: NamedMessageHandler<TMessage, TResult>): void;
|
|
18
18
|
private validateNotDuplicate;
|
|
19
|
+
/**
|
|
20
|
+
* Rejects a second handler for a type when the registry is single-handler.
|
|
21
|
+
*
|
|
22
|
+
* `allowMultipleHandlers` was stored and never read, so a registry created
|
|
23
|
+
* with `allowMultipleHandlers: false` silently fanned a command out to
|
|
24
|
+
* every handler that had claimed its type.
|
|
25
|
+
*
|
|
26
|
+
* @throws {MessageError} when the type already has a handler.
|
|
27
|
+
*/
|
|
28
|
+
private validateSingleHandlerPerType;
|
|
19
29
|
private indexHandlerTypes;
|
|
20
30
|
unregister(handlerId: string): boolean;
|
|
21
31
|
private removeHandlerFromTypeIndex;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module handlerRegistry/handlerRegistryStore
|
|
5
5
|
*/
|
|
6
|
-
import { DuplicateMessageHandlerError } from "@zudojs/errors";
|
|
6
|
+
import { createMessageError, DuplicateMessageHandlerError, ErrorCode, } from "@zudojs/errors";
|
|
7
7
|
/**
|
|
8
8
|
* In-memory store for registered message handlers.
|
|
9
9
|
*/
|
|
@@ -15,12 +15,12 @@ export class HandlerRegistryStore {
|
|
|
15
15
|
this.options = {
|
|
16
16
|
allowDuplicateHandlerIds: false,
|
|
17
17
|
allowMultipleHandlers: true,
|
|
18
|
-
requireTypeRegistration: false,
|
|
19
18
|
...options,
|
|
20
19
|
};
|
|
21
20
|
}
|
|
22
21
|
register(handler) {
|
|
23
22
|
this.validateNotDuplicate(handler.id);
|
|
23
|
+
this.validateSingleHandlerPerType(handler);
|
|
24
24
|
const entry = {
|
|
25
25
|
handler,
|
|
26
26
|
registeredAt: new Date(),
|
|
@@ -34,6 +34,30 @@ export class HandlerRegistryStore {
|
|
|
34
34
|
throw new DuplicateMessageHandlerError(handlerId);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Rejects a second handler for a type when the registry is single-handler.
|
|
39
|
+
*
|
|
40
|
+
* `allowMultipleHandlers` was stored and never read, so a registry created
|
|
41
|
+
* with `allowMultipleHandlers: false` silently fanned a command out to
|
|
42
|
+
* every handler that had claimed its type.
|
|
43
|
+
*
|
|
44
|
+
* @throws {MessageError} when the type already has a handler.
|
|
45
|
+
*/
|
|
46
|
+
validateSingleHandlerPerType(handler) {
|
|
47
|
+
if (this.options.allowMultipleHandlers !== false)
|
|
48
|
+
return;
|
|
49
|
+
for (const messageType of handler.messageTypes) {
|
|
50
|
+
const existing = this.handlersByType.get(messageType);
|
|
51
|
+
const taken = existing && existing.size > 0 ? [...existing][0] : undefined;
|
|
52
|
+
if (taken !== undefined) {
|
|
53
|
+
throw createMessageError(`Message type "${messageType}" already has handler "${taken}" and the registry does not allow multiple handlers.`, {
|
|
54
|
+
code: ErrorCode.CONFLICT,
|
|
55
|
+
messageType,
|
|
56
|
+
handlerId: handler.id,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
37
61
|
indexHandlerTypes(handler) {
|
|
38
62
|
for (const messageType of handler.messageTypes) {
|
|
39
63
|
let typeSet = this.handlersByType.get(messageType);
|
|
@@ -15,10 +15,13 @@ import type { Message } from "../message/messageType.type.js";
|
|
|
15
15
|
export interface HandlerRegistryOptions {
|
|
16
16
|
/** Allow duplicate handler IDs. Default: false. */
|
|
17
17
|
readonly allowDuplicateHandlerIds?: boolean;
|
|
18
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Allow multiple handlers per message type. Default: true.
|
|
20
|
+
*
|
|
21
|
+
* Set false for a command/query registry, where a second handler for the
|
|
22
|
+
* same type is a wiring mistake rather than a fan-out.
|
|
23
|
+
*/
|
|
19
24
|
readonly allowMultipleHandlers?: boolean;
|
|
20
|
-
/** Require message type registration before handler. Default: false. */
|
|
21
|
-
readonly requireTypeRegistration?: boolean;
|
|
22
25
|
}
|
|
23
26
|
/**
|
|
24
27
|
* Registered handler entry.
|
package/dist/message/index.d.ts
CHANGED
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
* Core message types, factory functions, and identity primitives.
|
|
5
5
|
*/
|
|
6
6
|
export type { MessageId, MessageType, MessageTimestamp, MessageSource, MessageCorrelationId, MessageCausationId, MessagePayload, Message, MessageInput, } from "./messageType.type.js";
|
|
7
|
-
export { createMessageId, createMessage, createDerivedMessage, isMessage, getMessageType, getMessagePayload, describeMessage, } from "./messageFactory.js";
|
|
7
|
+
export { createMessageId, toMessageId, toCorrelationId, toCausationId, createMessage, createDerivedMessage, isMessage, getMessageType, getMessagePayload, describeMessage, } from "./messageFactory.js";
|
|
8
8
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/message/index.js
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Core message types, factory functions, and identity primitives.
|
|
5
5
|
*/
|
|
6
|
-
export { createMessageId, createMessage, createDerivedMessage, isMessage, getMessageType, getMessagePayload, describeMessage, } from "./messageFactory.js";
|
|
6
|
+
export { createMessageId, toMessageId, toCorrelationId, toCausationId, createMessage, createDerivedMessage, isMessage, getMessageType, getMessagePayload, describeMessage, } from "./messageFactory.js";
|
|
7
7
|
//# sourceMappingURL=index.js.map
|
|
@@ -3,12 +3,28 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module message/messageFactory
|
|
5
5
|
*/
|
|
6
|
-
import type { Message, MessageId, MessageType, MessagePayload, MessageInput } from "./messageType.type.js";
|
|
6
|
+
import type { Message, MessageId, MessageCorrelationId, MessageCausationId, MessageType, MessagePayload, MessageInput } from "./messageType.type.js";
|
|
7
7
|
/**
|
|
8
8
|
* Creates a unique message identifier.
|
|
9
9
|
* Returns a branded MessageId type from @zudojs/constants.
|
|
10
10
|
*/
|
|
11
11
|
export declare function createMessageId(): MessageId;
|
|
12
|
+
/**
|
|
13
|
+
* Brands an existing string as a {@link MessageId}.
|
|
14
|
+
*
|
|
15
|
+
* Identifiers arriving from outside the process — a transport frame, a
|
|
16
|
+
* database row, a log record — are plain strings. This is the supported way
|
|
17
|
+
* to restore the branded type without an unchecked cast.
|
|
18
|
+
*/
|
|
19
|
+
export declare function toMessageId(value: string): MessageId;
|
|
20
|
+
/**
|
|
21
|
+
* Brands an existing string as a {@link MessageCorrelationId}.
|
|
22
|
+
*/
|
|
23
|
+
export declare function toCorrelationId(value: string): MessageCorrelationId;
|
|
24
|
+
/**
|
|
25
|
+
* Brands an existing string as a {@link MessageCausationId}.
|
|
26
|
+
*/
|
|
27
|
+
export declare function toCausationId(value: string): MessageCausationId;
|
|
12
28
|
/**
|
|
13
29
|
* Creates an immutable message.
|
|
14
30
|
*/
|
|
@@ -10,6 +10,39 @@
|
|
|
10
10
|
export function createMessageId() {
|
|
11
11
|
return `msg:${crypto.randomUUID()}`;
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Rejects an empty or blank identifier.
|
|
15
|
+
*/
|
|
16
|
+
function assertNonEmptyId(value, kind) {
|
|
17
|
+
if (value.trim().length === 0) {
|
|
18
|
+
throw new TypeError(`A ${kind} must be a non-empty string.`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Brands an existing string as a {@link MessageId}.
|
|
23
|
+
*
|
|
24
|
+
* Identifiers arriving from outside the process — a transport frame, a
|
|
25
|
+
* database row, a log record — are plain strings. This is the supported way
|
|
26
|
+
* to restore the branded type without an unchecked cast.
|
|
27
|
+
*/
|
|
28
|
+
export function toMessageId(value) {
|
|
29
|
+
assertNonEmptyId(value, "MessageId");
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Brands an existing string as a {@link MessageCorrelationId}.
|
|
34
|
+
*/
|
|
35
|
+
export function toCorrelationId(value) {
|
|
36
|
+
assertNonEmptyId(value, "CorrelationId");
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Brands an existing string as a {@link MessageCausationId}.
|
|
41
|
+
*/
|
|
42
|
+
export function toCausationId(value) {
|
|
43
|
+
assertNonEmptyId(value, "CausationId");
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
13
46
|
/**
|
|
14
47
|
* Normalizes a message timestamp.
|
|
15
48
|
*/
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { Message, MessageInput } from "../message/messageType.type.js";
|
|
7
7
|
import type { MessageHandler, NamedMessageHandler } from "../messageHandler/messageHandlerType.type.js";
|
|
8
|
-
import type { MessageMiddlewareLike } from "../messageMiddleware/messageMiddlewareType.type.js";
|
|
8
|
+
import type { MessageMiddlewareLike, MessageMiddlewareOptions } from "../messageMiddleware/messageMiddlewareType.type.js";
|
|
9
9
|
import type { MessageBus, MessageBusOptions } from "./messageBusType.type.js";
|
|
10
10
|
import type { DispatchResult, DispatchOptions } from "../dispatcher/dispatcherType.type.js";
|
|
11
11
|
/** Default in-memory message bus. */
|
|
@@ -13,12 +13,12 @@ export declare class InMemoryMessageBus implements MessageBus {
|
|
|
13
13
|
private readonly dispatcher;
|
|
14
14
|
private readonly registry;
|
|
15
15
|
private readonly defaultTimeout;
|
|
16
|
+
private handlerSequence;
|
|
16
17
|
private _disposed;
|
|
17
18
|
constructor(options?: MessageBusOptions);
|
|
18
19
|
private registerGlobalMiddleware;
|
|
19
20
|
dispatch<TPayload, TResult>(message: Message<TPayload>, options?: DispatchOptions<TResult>): Promise<DispatchResult<TResult>>;
|
|
20
21
|
private validateNotDisposed;
|
|
21
|
-
private dispatchWithTimeout;
|
|
22
22
|
send<TPayload, TResult>(input: MessageInput<TPayload>, options?: DispatchOptions<TResult>): Promise<DispatchResult<TResult>>;
|
|
23
23
|
on<TPayload, TResult>(messageType: string, handler: MessageHandler<Message<TPayload>, TResult>, options?: {
|
|
24
24
|
id?: string;
|
|
@@ -26,7 +26,8 @@ export declare class InMemoryMessageBus implements MessageBus {
|
|
|
26
26
|
}): void;
|
|
27
27
|
addHandler<TMessage extends Message, TResult>(handler: NamedMessageHandler<TMessage, TResult>): void;
|
|
28
28
|
off(handlerId: string): boolean;
|
|
29
|
-
use<TMessage extends Message = Message, TResult = unknown>(middleware: MessageMiddlewareLike<TMessage, TResult
|
|
29
|
+
use<TMessage extends Message = Message, TResult = unknown>(middleware: MessageMiddlewareLike<TMessage, TResult>, options?: MessageMiddlewareOptions): string;
|
|
30
|
+
removeMiddleware(middlewareId: string): boolean;
|
|
30
31
|
hasHandlers(messageType: string): boolean;
|
|
31
32
|
get handlerCount(): number;
|
|
32
33
|
dispose(): void;
|