@zudojs/messaging 1.1.0 → 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 +19 -9
- package/dist/messageBus/messageBusCore.js +9 -1
- package/dist/messageBus/messageBusType.type.d.ts +4 -0
- 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
|
|
@@ -7,6 +7,7 @@ import { createMessageContext } from "../messageContext/messageContextType.type.
|
|
|
7
7
|
import { resolveMessageHandler } from "../messageHandler/messageHandlerType.type.js";
|
|
8
8
|
import { HandlerRegistryStore } from "../handlerRegistry/handlerRegistryStore.js";
|
|
9
9
|
import { runMessagePipeline } from "../messageMiddleware/messageMiddlewarePipeline.js";
|
|
10
|
+
import { abortRejection, assertDispatchNotAborted, } from "./dispatcher.abort.js";
|
|
10
11
|
import { MessageDispatchAbortedError, MessageHandlerError, MessageMiddlewareError, MessageTimeoutError, MessageBusDisposedError, } from "@zudojs/errors";
|
|
11
12
|
/** Default priority for middleware that does not declare one. */
|
|
12
13
|
const DEFAULT_MIDDLEWARE_PRIORITY = 100;
|
|
@@ -52,6 +53,7 @@ export class DefaultDispatcher {
|
|
|
52
53
|
const handlers = this.registry.resolve(message.type);
|
|
53
54
|
const handlerResults = [];
|
|
54
55
|
let timer;
|
|
56
|
+
const aborted = abortRejection(context.signal, message);
|
|
55
57
|
try {
|
|
56
58
|
const run = runMessagePipeline(allMiddleware,
|
|
57
59
|
// Handlers receive the same context the middleware saw: the one
|
|
@@ -67,15 +69,22 @@ export class DefaultDispatcher {
|
|
|
67
69
|
});
|
|
68
70
|
// `DispatchOptions.timeout` was documented on the dispatcher but only
|
|
69
71
|
// ever honoured by the bus wrapper, so anyone holding a dispatcher
|
|
70
|
-
// directly got no timeout at all.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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);
|
|
79
88
|
return {
|
|
80
89
|
success: true,
|
|
81
90
|
value: pipelineResult.result,
|
|
@@ -103,6 +112,7 @@ export class DefaultDispatcher {
|
|
|
103
112
|
finally {
|
|
104
113
|
if (timer !== undefined)
|
|
105
114
|
clearTimeout(timer);
|
|
115
|
+
aborted.dispose();
|
|
106
116
|
// Detach from the caller's signal, otherwise a long-lived signal
|
|
107
117
|
// shared across dispatches accumulates one listener per dispatch.
|
|
108
118
|
release();
|
|
@@ -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
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/messaging",
|
|
3
|
-
"version": "1.
|
|
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
|
},
|