@zudojs/messaging 1.0.2 → 1.2.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 +28 -2
- package/dist/dispatcher/dispatcher.abort.d.ts +38 -0
- package/dist/dispatcher/dispatcher.abort.js +62 -0
- package/dist/dispatcher/dispatcherCore.js +31 -12
- package/dist/dispatcher/dispatcherType.type.d.ts +14 -0
- package/dist/messageBus/messageBusCore.js +9 -1
- package/dist/messageBus/messageBusType.type.d.ts +4 -0
- package/dist/messageHandler/messageHandlerType.type.d.ts +7 -2
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -88,8 +88,34 @@ one that failed, with its real duration.
|
|
|
88
88
|
Handlers receive the dispatch context as their second argument: the
|
|
89
89
|
`headers`, `state` and correlation/causation overrides passed through
|
|
90
90
|
`DispatchOptions.context`, plus anything a middleware stored in
|
|
91
|
-
`context.state`.
|
|
92
|
-
|
|
91
|
+
`context.state`.
|
|
92
|
+
|
|
93
|
+
`send()` also puts a `correlationId` or `causationId` given in
|
|
94
|
+
`options.context` on the message it builds (unless the input carries its
|
|
95
|
+
own), so `createDerivedMessage(message, …)` in a handler stays in the same
|
|
96
|
+
chain:
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
bus.on("order.placed", (message) => {
|
|
100
|
+
const next = createDerivedMessage(message, { type: "invoice.requested", payload: {} });
|
|
101
|
+
next.correlationId; // "req-42"
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await bus.send(
|
|
105
|
+
{ type: "order.placed", payload: order },
|
|
106
|
+
{ context: { correlationId: toCorrelationId("req-42") } },
|
|
107
|
+
);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Cancellation
|
|
111
|
+
|
|
112
|
+
A dispatch whose `AbortSignal` fires fails with `MessageDispatchAbortedError`
|
|
113
|
+
(`success: false`), whenever the abort lands: between handlers, or during the
|
|
114
|
+
last or only one — even if that handler ignores the signal and returns
|
|
115
|
+
normally. Like a timeout, it settles promptly rather than waiting for a handler
|
|
116
|
+
that ignores its signal. `handlerResults` still lists every handler that
|
|
117
|
+
finished. Before 1.2.0 an abort during the last handler was reported as
|
|
118
|
+
`success: true`.
|
|
93
119
|
|
|
94
120
|
## Timeouts
|
|
95
121
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning an aborted dispatch signal into a failed dispatch.
|
|
3
|
+
*
|
|
4
|
+
* A timeout rejected the dispatch as soon as it fired, but a caller's abort
|
|
5
|
+
* only stopped the *next* handler from starting: when it arrived during the
|
|
6
|
+
* last or only handler, and that handler returned normally, the dispatch was
|
|
7
|
+
* reported as `success: true`.
|
|
8
|
+
*
|
|
9
|
+
* @module dispatcher/dispatcher.abort
|
|
10
|
+
*/
|
|
11
|
+
import type { Message } from "../message/messageType.type.js";
|
|
12
|
+
/** A rejection armed on a signal, and the means to disarm it. */
|
|
13
|
+
export interface AbortRejection {
|
|
14
|
+
/** Rejects once the signal aborts; never resolves. */
|
|
15
|
+
readonly promise: Promise<never>;
|
|
16
|
+
/** Detaches from the signal and cancels a pending rejection. */
|
|
17
|
+
readonly dispose: () => void;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The error a dispatch fails with once `signal` has aborted.
|
|
21
|
+
*
|
|
22
|
+
* A timeout aborts with its own {@link MessageTimeoutError}, which is kept;
|
|
23
|
+
* any other abort is reported as {@link MessageDispatchAbortedError}.
|
|
24
|
+
*/
|
|
25
|
+
export declare function abortErrorFor(signal: AbortSignal, message: Message): Error;
|
|
26
|
+
/** Throws the dispatch's abort error when `signal` has aborted. */
|
|
27
|
+
export declare function assertDispatchNotAborted(signal: AbortSignal, message: Message): void;
|
|
28
|
+
/**
|
|
29
|
+
* A promise that rejects when `signal` aborts, so a dispatch settles
|
|
30
|
+
* promptly — as it does on a timeout — even while a handler that ignores its
|
|
31
|
+
* signal is still running.
|
|
32
|
+
*
|
|
33
|
+
* The rejection waits one macrotask. A handler that aborts and then returns
|
|
34
|
+
* synchronously has its result recorded first, so `handlerResults` still
|
|
35
|
+
* lists every handler that finished.
|
|
36
|
+
*/
|
|
37
|
+
export declare function abortRejection(signal: AbortSignal, message: Message): AbortRejection;
|
|
38
|
+
//# sourceMappingURL=dispatcher.abort.d.ts.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning an aborted dispatch signal into a failed dispatch.
|
|
3
|
+
*
|
|
4
|
+
* A timeout rejected the dispatch as soon as it fired, but a caller's abort
|
|
5
|
+
* only stopped the *next* handler from starting: when it arrived during the
|
|
6
|
+
* last or only handler, and that handler returned normally, the dispatch was
|
|
7
|
+
* reported as `success: true`.
|
|
8
|
+
*
|
|
9
|
+
* @module dispatcher/dispatcher.abort
|
|
10
|
+
*/
|
|
11
|
+
import { MessageDispatchAbortedError, MessageTimeoutError, } from "@zudojs/errors";
|
|
12
|
+
/**
|
|
13
|
+
* The error a dispatch fails with once `signal` has aborted.
|
|
14
|
+
*
|
|
15
|
+
* A timeout aborts with its own {@link MessageTimeoutError}, which is kept;
|
|
16
|
+
* any other abort is reported as {@link MessageDispatchAbortedError}.
|
|
17
|
+
*/
|
|
18
|
+
export function abortErrorFor(signal, message) {
|
|
19
|
+
if (signal.reason instanceof MessageTimeoutError)
|
|
20
|
+
return signal.reason;
|
|
21
|
+
return new MessageDispatchAbortedError(undefined, {
|
|
22
|
+
messageType: message.type,
|
|
23
|
+
messageId: message.id,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/** Throws the dispatch's abort error when `signal` has aborted. */
|
|
27
|
+
export function assertDispatchNotAborted(signal, message) {
|
|
28
|
+
if (signal.aborted)
|
|
29
|
+
throw abortErrorFor(signal, message);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A promise that rejects when `signal` aborts, so a dispatch settles
|
|
33
|
+
* promptly — as it does on a timeout — even while a handler that ignores its
|
|
34
|
+
* signal is still running.
|
|
35
|
+
*
|
|
36
|
+
* The rejection waits one macrotask. A handler that aborts and then returns
|
|
37
|
+
* synchronously has its result recorded first, so `handlerResults` still
|
|
38
|
+
* lists every handler that finished.
|
|
39
|
+
*/
|
|
40
|
+
export function abortRejection(signal, message) {
|
|
41
|
+
let pending;
|
|
42
|
+
let onAbort;
|
|
43
|
+
const promise = new Promise((_resolve, reject) => {
|
|
44
|
+
onAbort = () => {
|
|
45
|
+
pending = setImmediate(() => reject(abortErrorFor(signal, message)));
|
|
46
|
+
};
|
|
47
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
48
|
+
});
|
|
49
|
+
// The race owns this promise's outcome; a rejection that loses the race
|
|
50
|
+
// must not surface as unhandled.
|
|
51
|
+
promise.catch(() => { });
|
|
52
|
+
return {
|
|
53
|
+
promise,
|
|
54
|
+
dispose: () => {
|
|
55
|
+
if (onAbort)
|
|
56
|
+
signal.removeEventListener("abort", onAbort);
|
|
57
|
+
if (pending !== undefined)
|
|
58
|
+
clearImmediate(pending);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=dispatcher.abort.js.map
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* @module dispatcher/dispatcherCore
|
|
5
5
|
*/
|
|
6
6
|
import { createMessageContext } from "../messageContext/messageContextType.type.js";
|
|
7
|
+
import { resolveMessageHandler } from "../messageHandler/messageHandlerType.type.js";
|
|
7
8
|
import { HandlerRegistryStore } from "../handlerRegistry/handlerRegistryStore.js";
|
|
8
9
|
import { runMessagePipeline } from "../messageMiddleware/messageMiddlewarePipeline.js";
|
|
10
|
+
import { abortRejection, assertDispatchNotAborted, } from "./dispatcher.abort.js";
|
|
9
11
|
import { MessageDispatchAbortedError, MessageHandlerError, MessageMiddlewareError, MessageTimeoutError, MessageBusDisposedError, } from "@zudojs/errors";
|
|
10
12
|
/** Default priority for middleware that does not declare one. */
|
|
11
13
|
const DEFAULT_MIDDLEWARE_PRIORITY = 100;
|
|
@@ -51,6 +53,7 @@ export class DefaultDispatcher {
|
|
|
51
53
|
const handlers = this.registry.resolve(message.type);
|
|
52
54
|
const handlerResults = [];
|
|
53
55
|
let timer;
|
|
56
|
+
const aborted = abortRejection(context.signal, message);
|
|
54
57
|
try {
|
|
55
58
|
const run = runMessagePipeline(allMiddleware,
|
|
56
59
|
// Handlers receive the same context the middleware saw: the one
|
|
@@ -66,21 +69,32 @@ export class DefaultDispatcher {
|
|
|
66
69
|
});
|
|
67
70
|
// `DispatchOptions.timeout` was documented on the dispatcher but only
|
|
68
71
|
// ever honoured by the bus wrapper, so anyone holding a dispatcher
|
|
69
|
-
// directly got no timeout at all.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
72
|
+
// directly got no timeout at all. An abort settles the dispatch the
|
|
73
|
+
// same way: promptly, and as a failure.
|
|
74
|
+
const pipelineResult = await Promise.race([
|
|
75
|
+
run,
|
|
76
|
+
aborted.promise,
|
|
77
|
+
...(timeout > 0
|
|
78
|
+
? [
|
|
79
|
+
this.timeoutRejection(message, timeout, controller, (t) => {
|
|
80
|
+
timer = t;
|
|
81
|
+
}),
|
|
82
|
+
]
|
|
83
|
+
: []),
|
|
84
|
+
]);
|
|
85
|
+
// A handler that returned normally after the abort does not make the
|
|
86
|
+
// dispatch a success: it was cancelled.
|
|
87
|
+
assertDispatchNotAborted(context.signal, message);
|
|
78
88
|
return {
|
|
79
89
|
success: true,
|
|
80
90
|
value: pipelineResult.result,
|
|
81
91
|
message,
|
|
82
92
|
context,
|
|
83
|
-
|
|
93
|
+
// A copy, not the live array: on a timeout the dispatch settles
|
|
94
|
+
// while a handler is still running, and that handler later pushed
|
|
95
|
+
// a `success: true` record into the result the caller was already
|
|
96
|
+
// holding for a dispatch that had failed.
|
|
97
|
+
handlerResults: [...handlerResults],
|
|
84
98
|
middlewareResult: pipelineResult,
|
|
85
99
|
duration: performance.now() - dispatchStart,
|
|
86
100
|
};
|
|
@@ -91,13 +105,14 @@ export class DefaultDispatcher {
|
|
|
91
105
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
92
106
|
message,
|
|
93
107
|
context,
|
|
94
|
-
handlerResults,
|
|
108
|
+
handlerResults: [...handlerResults],
|
|
95
109
|
duration: performance.now() - dispatchStart,
|
|
96
110
|
};
|
|
97
111
|
}
|
|
98
112
|
finally {
|
|
99
113
|
if (timer !== undefined)
|
|
100
114
|
clearTimeout(timer);
|
|
115
|
+
aborted.dispose();
|
|
101
116
|
// Detach from the caller's signal, otherwise a long-lived signal
|
|
102
117
|
// shared across dispatches accumulates one listener per dispatch.
|
|
103
118
|
release();
|
|
@@ -190,7 +205,11 @@ export class DefaultDispatcher {
|
|
|
190
205
|
}
|
|
191
206
|
async executeHandler(handler, message, context) {
|
|
192
207
|
try {
|
|
193
|
-
|
|
208
|
+
// Handlers may be a plain function or an object with a `handle`
|
|
209
|
+
// method. Calling `handler.handler(...)` directly made the object
|
|
210
|
+
// form crash on every dispatch, so it goes through the same
|
|
211
|
+
// normaliser the public type advertises.
|
|
212
|
+
return await resolveMessageHandler(handler.handler)(message, context);
|
|
194
213
|
}
|
|
195
214
|
catch (error) {
|
|
196
215
|
throw new MessageHandlerError(`Handler "${handler.id}" failed: ${error instanceof Error ? error.message : String(error)}`, {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import type { Message } from "../message/messageType.type.js";
|
|
10
10
|
import type { MessageContext, MessageContextOptions } from "../messageContext/messageContextType.type.js";
|
|
11
11
|
import type { MessageMiddlewareLike, MessageMiddlewareOptions, MessageMiddlewarePipelineResult } from "../messageMiddleware/messageMiddlewareType.type.js";
|
|
12
|
+
import type { HandlerRegistryStore } from "../handlerRegistry/handlerRegistryStore.js";
|
|
12
13
|
/**
|
|
13
14
|
* Result of dispatching a message.
|
|
14
15
|
*/
|
|
@@ -90,5 +91,18 @@ export interface Dispatcher {
|
|
|
90
91
|
* @returns Whether a registration was removed.
|
|
91
92
|
*/
|
|
92
93
|
removeMiddleware(middlewareId: string): boolean;
|
|
94
|
+
/**
|
|
95
|
+
* The ids of every registered global middleware, in the order they run.
|
|
96
|
+
*/
|
|
97
|
+
listMiddleware(): readonly string[];
|
|
98
|
+
/**
|
|
99
|
+
* The handler registry this dispatcher resolves handlers from.
|
|
100
|
+
*/
|
|
101
|
+
getRegistry(): HandlerRegistryStore;
|
|
102
|
+
/**
|
|
103
|
+
* Releases the dispatcher: drops all middleware and rejects any further
|
|
104
|
+
* dispatch with `MessageBusDisposedError`.
|
|
105
|
+
*/
|
|
106
|
+
dispose(): void;
|
|
93
107
|
}
|
|
94
108
|
//# sourceMappingURL=dispatcherType.type.d.ts.map
|
|
@@ -46,7 +46,15 @@ export class InMemoryMessageBus {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
async send(input, options = {}) {
|
|
49
|
-
|
|
49
|
+
// Identifiers given through the dispatch context belong on the message
|
|
50
|
+
// too; otherwise `createDerivedMessage` in a handler starts a new chain.
|
|
51
|
+
const context = options.context;
|
|
52
|
+
const message = createMessage({
|
|
53
|
+
...input,
|
|
54
|
+
correlationId: input.correlationId ?? context?.correlationId,
|
|
55
|
+
causationId: input.causationId ?? context?.causationId,
|
|
56
|
+
});
|
|
57
|
+
return this.dispatch(message, options);
|
|
50
58
|
}
|
|
51
59
|
on(messageType, handler, options = {}) {
|
|
52
60
|
// A clock-derived id collided whenever two handlers for the same type
|
|
@@ -40,6 +40,10 @@ export interface MessageBus {
|
|
|
40
40
|
dispatch<TPayload, TResult>(message: Message<TPayload>, options?: DispatchOptions<TResult>): Promise<DispatchResult<TResult>>;
|
|
41
41
|
/**
|
|
42
42
|
* Convenience method: creates and dispatches a message from input.
|
|
43
|
+
*
|
|
44
|
+
* A `correlationId` or `causationId` given in `options.context` is also
|
|
45
|
+
* put on the message when the input carries none, so a message derived
|
|
46
|
+
* from it in a handler stays in the same chain.
|
|
43
47
|
*/
|
|
44
48
|
send<TPayload, TResult>(input: MessageInput<TPayload>, options?: DispatchOptions<TResult>): Promise<DispatchResult<TResult>>;
|
|
45
49
|
/**
|
|
@@ -35,8 +35,13 @@ export interface NamedMessageHandler<TMessage extends Message = Message, TResult
|
|
|
35
35
|
readonly id: string;
|
|
36
36
|
/** Human-readable name for debugging. */
|
|
37
37
|
readonly name: string;
|
|
38
|
-
/**
|
|
39
|
-
|
|
38
|
+
/**
|
|
39
|
+
* The handler itself: a function, or an object with a `handle` method.
|
|
40
|
+
*
|
|
41
|
+
* The dispatcher normalises both forms through
|
|
42
|
+
* {@link resolveMessageHandler} before invoking them.
|
|
43
|
+
*/
|
|
44
|
+
readonly handler: MessageHandlerLike<TMessage, TResult>;
|
|
40
45
|
/** Message types this handler processes. */
|
|
41
46
|
readonly messageTypes: readonly string[];
|
|
42
47
|
/** Execution priority (lower = earlier). Default: 100. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/messaging",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "In-process message bus infrastructure with handlers, middleware, and publish/subscribe patterns.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -19,16 +19,16 @@
|
|
|
19
19
|
"!dist/.tsbuildinfo"
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@zudojs/constants": "1.1.
|
|
23
|
-
"@zudojs/errors": "1.
|
|
24
|
-
"@zudojs/middleware": "1.0
|
|
22
|
+
"@zudojs/constants": "1.1.2",
|
|
23
|
+
"@zudojs/errors": "1.3.0",
|
|
24
|
+
"@zudojs/middleware": "1.1.0"
|
|
25
25
|
},
|
|
26
26
|
"engines": {
|
|
27
27
|
"node": ">=24.0.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"typescript": "7.0.2",
|
|
31
|
-
"vitest": "^
|
|
31
|
+
"vitest": "^5.0.1"
|
|
32
32
|
},
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"author": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"message-bus",
|
|
45
45
|
"pubsub"
|
|
46
46
|
],
|
|
47
|
-
"homepage": "https://
|
|
47
|
+
"homepage": "https://zudojs.oyinlola.site/docs/packages-messaging",
|
|
48
48
|
"bugs": {
|
|
49
49
|
"url": "https://github.com/oyinlola-tech/zudo/issues"
|
|
50
50
|
},
|