@zudojs/messaging 0.1.0 → 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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +79 -7
  3. package/dist/dispatcher/dispatcherCore.d.ts +38 -3
  4. package/dist/dispatcher/dispatcherCore.js +149 -23
  5. package/dist/dispatcher/dispatcherType.type.d.ts +16 -5
  6. package/dist/handlerRegistry/handlerRegistryStore.d.ts +10 -0
  7. package/dist/handlerRegistry/handlerRegistryStore.js +26 -2
  8. package/dist/handlerRegistry/handlerRegistryType.type.d.ts +6 -3
  9. package/dist/message/index.d.ts +1 -1
  10. package/dist/message/index.js +1 -1
  11. package/dist/message/messageFactory.d.ts +17 -1
  12. package/dist/message/messageFactory.js +33 -0
  13. package/dist/messageBus/messageBusCore.d.ts +4 -3
  14. package/dist/messageBus/messageBusCore.js +20 -25
  15. package/dist/messageBus/messageBusType.type.d.ts +12 -4
  16. package/dist/messageMiddleware/messageMiddlewarePipeline.js +17 -6
  17. package/dist/messageMiddleware/messageMiddlewareType.type.d.ts +6 -0
  18. package/package.json +21 -13
  19. package/dist/.tsbuildinfo +0 -1
  20. package/dist/dispatcher/dispatcherCore.d.ts.map +0 -1
  21. package/dist/dispatcher/dispatcherCore.js.map +0 -1
  22. package/dist/dispatcher/dispatcherType.type.d.ts.map +0 -1
  23. package/dist/dispatcher/dispatcherType.type.js.map +0 -1
  24. package/dist/dispatcher/index.d.ts.map +0 -1
  25. package/dist/dispatcher/index.js.map +0 -1
  26. package/dist/errors/index.d.ts.map +0 -1
  27. package/dist/errors/index.js.map +0 -1
  28. package/dist/handlerRegistry/handlerRegistryStore.d.ts.map +0 -1
  29. package/dist/handlerRegistry/handlerRegistryStore.js.map +0 -1
  30. package/dist/handlerRegistry/handlerRegistryType.type.d.ts.map +0 -1
  31. package/dist/handlerRegistry/handlerRegistryType.type.js.map +0 -1
  32. package/dist/handlerRegistry/index.d.ts.map +0 -1
  33. package/dist/handlerRegistry/index.js.map +0 -1
  34. package/dist/index.d.ts.map +0 -1
  35. package/dist/index.js.map +0 -1
  36. package/dist/message/index.d.ts.map +0 -1
  37. package/dist/message/index.js.map +0 -1
  38. package/dist/message/messageFactory.d.ts.map +0 -1
  39. package/dist/message/messageFactory.js.map +0 -1
  40. package/dist/message/messageType.type.d.ts.map +0 -1
  41. package/dist/message/messageType.type.js.map +0 -1
  42. package/dist/messageBus/index.d.ts.map +0 -1
  43. package/dist/messageBus/index.js.map +0 -1
  44. package/dist/messageBus/messageBusCore.d.ts.map +0 -1
  45. package/dist/messageBus/messageBusCore.js.map +0 -1
  46. package/dist/messageBus/messageBusType.type.d.ts.map +0 -1
  47. package/dist/messageBus/messageBusType.type.js.map +0 -1
  48. package/dist/messageContext/index.d.ts.map +0 -1
  49. package/dist/messageContext/index.js.map +0 -1
  50. package/dist/messageContext/messageContextType.type.d.ts.map +0 -1
  51. package/dist/messageContext/messageContextType.type.js.map +0 -1
  52. package/dist/messageHandler/index.d.ts.map +0 -1
  53. package/dist/messageHandler/index.js.map +0 -1
  54. package/dist/messageHandler/messageHandlerType.type.d.ts.map +0 -1
  55. package/dist/messageHandler/messageHandlerType.type.js.map +0 -1
  56. package/dist/messageMiddleware/index.d.ts.map +0 -1
  57. package/dist/messageMiddleware/index.js.map +0 -1
  58. package/dist/messageMiddleware/messageMiddlewarePipeline.d.ts.map +0 -1
  59. package/dist/messageMiddleware/messageMiddlewarePipeline.js.map +0 -1
  60. package/dist/messageMiddleware/messageMiddlewareType.type.d.ts.map +0 -1
  61. package/dist/messageMiddleware/messageMiddlewareType.type.js.map +0 -1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zudojs Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -15,23 +15,95 @@ import { createMessageBus } from "@zudojs/messaging";
15
15
 
16
16
  const bus = createMessageBus();
17
17
 
18
- bus.subscribe("user.created", (message) => {
18
+ bus.on("user.created", (message) => {
19
19
  console.log("New user:", message.payload);
20
20
  });
21
21
 
22
- await bus.publish({
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 pub/sub
31
- - Middleware pipeline for messages
32
- - Message handlers with dependencies
33
- - In-memory transport
34
- - Message serialization
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
- use<TMessage extends Message = Message, TResult = unknown>(middleware: MessageMiddlewareLike<TMessage, TResult>): void;
24
- removeMiddleware(_middlewareId: string): boolean;
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, MessageDispatchError, } from "@zudojs/errors";
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 signal = this.resolveSignal(options.signal);
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
- ...this.globalMiddleware,
30
- ...(options.middleware ?? []),
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 pipelineResult = await runMessagePipeline(allMiddleware, async (msg, mwCtx) => this.executeHandlers(msg, handlers, handlerResults, mwCtx.signal), message, {
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(message) {
123
+ validateNotDisposed(_message) {
62
124
  if (this.disposed)
63
- throw new MessageDispatchError(message.type, "Dispatcher has been disposed.");
125
+ throw new MessageBusDisposedError();
64
126
  }
65
- resolveSignal(signal) {
66
- const resolved = signal ?? new AbortController().signal;
67
- if (resolved.aborted)
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
- return resolved;
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 result = await this.executeHandler(handler, message, signal);
75
- results.push(result);
76
- handlerResults.push({
77
- handlerId: handler.id,
78
- success: true,
79
- value: result,
80
- duration: 0,
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
- use(middleware) {
102
- this.globalMiddleware.push(middleware);
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
- removeMiddleware(_middlewareId) {
105
- return false;
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
- /** Timeout in ms. 0 = no timeout. Default: 0. */
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
- /** Allow multiple handlers per message type. Default: true. */
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.
@@ -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
@@ -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
  */