@actdim/msgmesh 1.2.5 → 1.2.7
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 +826 -1
- package/dist/contracts.d.ts +16 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.es.js +51 -23
- package/dist/contracts.es.js.map +1 -1
- package/dist/core.d.ts.map +1 -1
- package/dist/core.es.js +196 -154
- package/dist/core.es.js.map +1 -1
- package/dist/util.es.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1 +1,826 @@
|
|
|
1
|
-
# @actdim/msgmesh
|
|
1
|
+
# @actdim/msgmesh - A type-safe, modular message mesh for scalable async communication in TypeScript
|
|
2
|
+
|
|
3
|
+
## Quick Start
|
|
4
|
+
|
|
5
|
+
Try @actdim/msgmesh instantly in your browser without any installation:
|
|
6
|
+
|
|
7
|
+
[](https://stackblitz.com/~/github.com/actdim/msgmesh)
|
|
8
|
+
|
|
9
|
+
Once the project loads, run the tests to see the message bus in action:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm run test
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Overview
|
|
16
|
+
|
|
17
|
+
### The Challenge
|
|
18
|
+
|
|
19
|
+
Modern client-side TypeScript applications require robust event handling mechanisms. Events may be needed within a single component or for communication between components, serving as a decoupling layer independent of component hierarchy. As applications grow in complexity and scale, the convenience, performance, and flexibility of the event system become critical factors. A well-designed messaging system enables extensibility, maintainability, and scalability without losing control over component interactions or system observability. Such a system becomes one of the pillars of high-quality application architecture.
|
|
20
|
+
|
|
21
|
+
In our case, this message bus serves as the foundation of the @actdim/dynstruct architectural framework.
|
|
22
|
+
|
|
23
|
+
### Analysis of Existing Solutions
|
|
24
|
+
|
|
25
|
+
When examining popular messaging systems in the frontend ecosystem, particularly for React-based applications, several categories emerge:
|
|
26
|
+
|
|
27
|
+
#### Event Emitters
|
|
28
|
+
|
|
29
|
+
- **Pros**: Simple to understand, typically local in scope
|
|
30
|
+
- **Cons**:
|
|
31
|
+
- Limited capabilities and scalability
|
|
32
|
+
- Weak support for interaction structures and declarative approaches
|
|
33
|
+
- Poor type safety (fictitious typing, manual implementation required)
|
|
34
|
+
- Incomplete Promise integration
|
|
35
|
+
- Lack of abstraction levels
|
|
36
|
+
|
|
37
|
+
#### Message Buses
|
|
38
|
+
|
|
39
|
+
- **Pros**: Reduce component coupling, beneficial for development and testing
|
|
40
|
+
- **Cons**:
|
|
41
|
+
- Underdeveloped type system despite TypeScript's power
|
|
42
|
+
- Often feel like academic experiments porting backend message buses to frontend
|
|
43
|
+
- Poor integration with common development patterns (limited adapters for rate limiting, throttling, debouncing, retry logic)
|
|
44
|
+
- More complex to maintain
|
|
45
|
+
|
|
46
|
+
#### Reactive Event Streams & Observer Pattern
|
|
47
|
+
|
|
48
|
+
- **Pros**: Powerful for compositions and complex data flows
|
|
49
|
+
- **Cons**:
|
|
50
|
+
- Complex to understand, maintain, and debug
|
|
51
|
+
- Strong architectural influence requiring paradigm shift (similar to procedural-to-functional programming transition)
|
|
52
|
+
- Often tightly embedded throughout the system as an integral part
|
|
53
|
+
- Creates hard dependencies across types, code style, tests, DI, error handling, and even team thinking
|
|
54
|
+
- Essentially becomes the "language" of the application
|
|
55
|
+
|
|
56
|
+
#### React State Management Systems
|
|
57
|
+
|
|
58
|
+
- **Pros**: Purpose-built for React ecosystem
|
|
59
|
+
- **Cons**:
|
|
60
|
+
- Tight coupling with React (hooks, lifecycle), making usage outside components difficult
|
|
61
|
+
- Significant boilerplate code slowing development and complicating maintenance
|
|
62
|
+
- Often enforce immutability paradigm, which looks elegant on paper but creates more problems and wrapper code than value in practice
|
|
63
|
+
- Rarely provide configuration for event/stream connections (possibly due to weak or inconvenient payload typing)
|
|
64
|
+
|
|
65
|
+
### The Solution: @actdim/msgmesh
|
|
66
|
+
|
|
67
|
+
@actdim/msgmesh addresses these shortcomings by providing a message bus that is:
|
|
68
|
+
|
|
69
|
+
- **Flexible and extensible**: Adapts to various use cases without imposing rigid patterns
|
|
70
|
+
- **Scalable**: Grows with your application without losing manageability
|
|
71
|
+
- **Minimally opinionated**: Doesn't force a specific paradigm
|
|
72
|
+
- **Simple to understand**: Clear mental model and API
|
|
73
|
+
- **Local in impact**: Doesn't permeate every aspect of your codebase
|
|
74
|
+
|
|
75
|
+
### Implementation Foundation
|
|
76
|
+
|
|
77
|
+
@actdim/msgmesh is built on top of **RxJS**, leveraging the power and quality of this battle-tested library while hiding its complexity and architectural influence (see the comparison section above). This approach provides the best of both worlds: robust reactive stream processing under the hood with a simple, intuitive API on the surface.
|
|
78
|
+
|
|
79
|
+
**Key RxJS components utilized:**
|
|
80
|
+
|
|
81
|
+
- **Subjects/Observables**: Power the queue management system and state control, implementing the publish-subscribe (pub/sub) pattern efficiently
|
|
82
|
+
- **Async Scheduler**: Ensures the message bus operates independently from individual message handlers, preventing blocking and maintaining system responsiveness
|
|
83
|
+
- **Pipe Operators**: Enable flexible message flow behaviors within channels (throttling, debouncing, filtering, etc.) without exposing reactive programming complexity
|
|
84
|
+
|
|
85
|
+
By abstracting RxJS behind a clean API, @actdim/msgmesh delivers enterprise-grade stream processing capabilities without requiring developers to adopt reactive programming paradigms or deal with the steep learning curve typically associated with RxJS.
|
|
86
|
+
|
|
87
|
+
### Key Design Goals
|
|
88
|
+
|
|
89
|
+
#### Observability
|
|
90
|
+
|
|
91
|
+
- Comprehensive logging and tracing capabilities
|
|
92
|
+
- Ability to subscribe to any event at any time
|
|
93
|
+
- Minimal system complexity and coupling
|
|
94
|
+
- Maintained control and visibility
|
|
95
|
+
|
|
96
|
+
#### Lifecycle Management
|
|
97
|
+
|
|
98
|
+
- Convenient subscription and unsubscription with various configuration options
|
|
99
|
+
- Automatic cleanup
|
|
100
|
+
- Integration with React lifecycle (when needed)
|
|
101
|
+
- Support for AbortSignal and AbortController patterns
|
|
102
|
+
|
|
103
|
+
## Architecture
|
|
104
|
+
|
|
105
|
+
### Message Structure
|
|
106
|
+
|
|
107
|
+
The message bus is defined by a type structure consisting of three levels:
|
|
108
|
+
|
|
109
|
+
#### 1. Channels
|
|
110
|
+
|
|
111
|
+
Channels organize messages by task class, domain, event type, or any other logical grouping. Channels use string identifiers with dot notation recommended for namespacing.
|
|
112
|
+
|
|
113
|
+
**Reserved System Channel**: `MSGBUS.ERROR` - for system-level errors
|
|
114
|
+
|
|
115
|
+
#### 2. Groups
|
|
116
|
+
|
|
117
|
+
Groups connect related messages within a single channel. Standard groups include:
|
|
118
|
+
|
|
119
|
+
- **`in`**: For requests or arbitrary messages/events (default for most operations)
|
|
120
|
+
- **`out`**: For responses to requests
|
|
121
|
+
- **`error`**: Reserved system group for channel-specific errors
|
|
122
|
+
|
|
123
|
+
You can define custom groups for message multiplexing and input type overloading.
|
|
124
|
+
|
|
125
|
+
#### 3. Message Types
|
|
126
|
+
|
|
127
|
+
Each group defines a message structure (payload type). For standard buses, types can be any valid TypeScript type. For persistent message buses (work in progress), types must be serializable.
|
|
128
|
+
|
|
129
|
+
**Note**: You don't need to wrap `out` types in `Promise` - async handling is automatically supported at the API level.
|
|
130
|
+
|
|
131
|
+
### Type Definition Example
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
import { RequireExtends, MsgStruct } from '@actdim/msgmesh';
|
|
135
|
+
|
|
136
|
+
export type MyBusStruct = RequireExtends<
|
|
137
|
+
{
|
|
138
|
+
'Test.ComputeSum': {
|
|
139
|
+
in: { a: number; b: number };
|
|
140
|
+
out: number;
|
|
141
|
+
};
|
|
142
|
+
'Test.DoSomeWork': {
|
|
143
|
+
in: string;
|
|
144
|
+
out: void;
|
|
145
|
+
};
|
|
146
|
+
'Test.TestTaskWithRepeat': {
|
|
147
|
+
in: string;
|
|
148
|
+
out: void;
|
|
149
|
+
};
|
|
150
|
+
'Test.Multiplexer': {
|
|
151
|
+
in1: string;
|
|
152
|
+
in2: number;
|
|
153
|
+
out: number;
|
|
154
|
+
};
|
|
155
|
+
},
|
|
156
|
+
MsgStruct
|
|
157
|
+
>;
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Usage Patterns
|
|
161
|
+
|
|
162
|
+
### Global vs Local Usage
|
|
163
|
+
|
|
164
|
+
@actdim/msgmesh can be used in two primary ways:
|
|
165
|
+
|
|
166
|
+
#### Global Application-Level Bus
|
|
167
|
+
|
|
168
|
+
Maintain a system-wide type structure for messages/events, organizing them by:
|
|
169
|
+
|
|
170
|
+
- Tasks (component ownership)
|
|
171
|
+
- Groups (in/out, input type overloading)
|
|
172
|
+
- Topics (additional filtering)
|
|
173
|
+
|
|
174
|
+
#### Local Component/Module-Level Bus
|
|
175
|
+
|
|
176
|
+
Use within any logical grouping of components or modules.
|
|
177
|
+
|
|
178
|
+
**Important**: You only need **one bus instance** for the entire application. The bus routes messages based on keys, so as long as key uniqueness is maintained, a single instance can handle messages from any locally-defined schema.
|
|
179
|
+
|
|
180
|
+
### Creating a Message Bus
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
import { createMsgBus, MsgBus } from '@actdim/msgmesh';
|
|
184
|
+
import { KeysOf } from '@actdim/utico/typeCore';
|
|
185
|
+
|
|
186
|
+
// Basic bus creation
|
|
187
|
+
const msgBus = createMsgBus<MyBusStruct>();
|
|
188
|
+
|
|
189
|
+
// With custom headers (if needed)
|
|
190
|
+
type CustomHeaders = MsgHeaders & {
|
|
191
|
+
userId?: string;
|
|
192
|
+
sessionId?: string;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const msgBusWithHeaders = createMsgBus<MyBusStruct, CustomHeaders>();
|
|
196
|
+
|
|
197
|
+
// Note: The instance can process messages from other structures too
|
|
198
|
+
// We only type the API for development convenience
|
|
199
|
+
// You can compose structures as needed, just ensure they don't overlap (unless intentional)
|
|
200
|
+
|
|
201
|
+
type AppBusStruct = ComponentBusStruct & ApiBusStruct;
|
|
202
|
+
const appMsgBus = createMsgBus<AppBusStruct>();
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Type Utilities
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
// Export bus type for dependency injection or props
|
|
209
|
+
export type MyMsgBus = MsgBus<MyBusStruct, CustomHeaders>;
|
|
210
|
+
|
|
211
|
+
// Generic string literal type for channels - useful for component constraints
|
|
212
|
+
type MyMsgChannels<TChannel extends keyof MyBusStruct | Array<keyof MyBusStruct>> = KeysOf<
|
|
213
|
+
MyBusStruct,
|
|
214
|
+
TChannel
|
|
215
|
+
>;
|
|
216
|
+
|
|
217
|
+
// Example: Restricting a component to specific channels
|
|
218
|
+
// Helper types are necessary for IntelliSense with dynamic types
|
|
219
|
+
// All API checks are enforced at compile time - you cannot violate defined contracts
|
|
220
|
+
type Behavior = {
|
|
221
|
+
messages: MyMsgChannels<'Test.ComputeSum' | 'Test.DoSomeWork'>;
|
|
222
|
+
};
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## API Reference
|
|
226
|
+
|
|
227
|
+
### Configuration
|
|
228
|
+
|
|
229
|
+
You can configure channels with various options:
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
import { MsgBusConfig } from '@actdim/msgmesh';
|
|
233
|
+
|
|
234
|
+
const config: MsgBusConfig<MyBusStruct> = {
|
|
235
|
+
'Test.ComputeSum': {
|
|
236
|
+
replayBufferSize: 10, // Number of messages to buffer for replay
|
|
237
|
+
replayWindowTime: 5000, // Time window for replay (ms)
|
|
238
|
+
delay: 100, // Delay before processing (ms)
|
|
239
|
+
throttle: {
|
|
240
|
+
// Throttle configuration
|
|
241
|
+
duration: 1000,
|
|
242
|
+
leading: true,
|
|
243
|
+
trailing: true,
|
|
244
|
+
},
|
|
245
|
+
debounce: 500, // Debounce delay (ms)
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const msgBus = createMsgBus<MyBusStruct>(config);
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Subscribing to Messages: `on()`
|
|
253
|
+
|
|
254
|
+
Subscribe to messages on a specific channel and group with optional topic filtering.
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
// Basic subscription
|
|
258
|
+
msgBus.on({
|
|
259
|
+
channel: 'Test.ComputeSum',
|
|
260
|
+
callback: (msg) => {
|
|
261
|
+
// msg.payload is typed as { a: number; b: number }
|
|
262
|
+
console.log('Received:', msg.payload);
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Subscribe to specific group
|
|
267
|
+
msgBus.on({
|
|
268
|
+
channel: 'Test.ComputeSum',
|
|
269
|
+
group: 'out', // Listen for responses
|
|
270
|
+
callback: (msg) => {
|
|
271
|
+
// msg.payload is typed as number
|
|
272
|
+
console.log('Result:', msg.payload);
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// With topic filtering (regex pattern)
|
|
277
|
+
msgBus.on({
|
|
278
|
+
channel: 'Test.DoSomeWork',
|
|
279
|
+
topic: '/^task-.*/', // Match topics starting with "task-"
|
|
280
|
+
callback: (msg) => {
|
|
281
|
+
console.log('Task message:', msg.payload);
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// With options
|
|
286
|
+
msgBus.on({
|
|
287
|
+
channel: 'Test.ComputeSum',
|
|
288
|
+
callback: (msg) => {
|
|
289
|
+
console.log('Message:', msg.payload);
|
|
290
|
+
},
|
|
291
|
+
options: {
|
|
292
|
+
fetchCount: 5, // Auto-unsubscribe after 5 messages
|
|
293
|
+
throttle: {
|
|
294
|
+
// Throttle the callback
|
|
295
|
+
duration: 1000,
|
|
296
|
+
leading: true,
|
|
297
|
+
trailing: false,
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
#### Automatic Unsubscription
|
|
304
|
+
|
|
305
|
+
**Limit message count**: Use `fetchCount` to automatically unsubscribe after receiving a specific number of messages.
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
msgBus.on({
|
|
309
|
+
channel: 'Test.ComputeSum',
|
|
310
|
+
callback: (msg) => {
|
|
311
|
+
console.log(msg.payload);
|
|
312
|
+
},
|
|
313
|
+
options: {
|
|
314
|
+
fetchCount: 10, // Unsubscribe after 10 messages
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
#### Manual Unsubscription with AbortSignal
|
|
320
|
+
|
|
321
|
+
Use `AbortSignal` for controlled unsubscription. This allows combining abort signals from multiple `AbortController` instances.
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
const abortController = new AbortController();
|
|
325
|
+
|
|
326
|
+
msgBus.on({
|
|
327
|
+
channel: "Test.ComputeSum",
|
|
328
|
+
callback: (msg) => {
|
|
329
|
+
console.log(msg.payload);
|
|
330
|
+
},
|
|
331
|
+
options: {
|
|
332
|
+
abortSignal: abortController.signal
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// Later: unsubscribe
|
|
337
|
+
abortController.abort();
|
|
338
|
+
|
|
339
|
+
// Combining multiple abort signals
|
|
340
|
+
const controller1 = new AbortController();
|
|
341
|
+
const controller2 = new AbortController();
|
|
342
|
+
|
|
343
|
+
const combinedSignal = AbortSignal.any([
|
|
344
|
+
controller1.signal,
|
|
345
|
+
controller2.signal
|
|
346
|
+
]);
|
|
347
|
+
|
|
348
|
+
msgBus.on({
|
|
349
|
+
channel: "Test.ComputeSum",
|
|
350
|
+
options: {
|
|
351
|
+
abortSignal: combinedSignal
|
|
352
|
+
},
|
|
353
|
+
callback: (msg) => {
|
|
354
|
+
console.log(msg.payload);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// React integration example - cleanup on unmount
|
|
359
|
+
import { useEffect } from 'react';
|
|
360
|
+
|
|
361
|
+
function MyComponent() {
|
|
362
|
+
useEffect(() => {
|
|
363
|
+
const controller = new AbortController();
|
|
364
|
+
|
|
365
|
+
msgBus.on({
|
|
366
|
+
channel: "Test.Events",
|
|
367
|
+
callback: handleEvent,
|
|
368
|
+
options: {
|
|
369
|
+
abortSignal: controller.signal
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Clean up when component unmounts
|
|
374
|
+
return () => {
|
|
375
|
+
controller.abort();
|
|
376
|
+
};
|
|
377
|
+
}, []);
|
|
378
|
+
|
|
379
|
+
return <div>Component content</div>;
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Awaiting a Single Message: `once()`
|
|
384
|
+
|
|
385
|
+
Subscribe and await the first (next) message on a specific channel and group, similar to `on()` but returns a Promise.
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
// Wait for one message
|
|
389
|
+
const msg = await msgBus.once({
|
|
390
|
+
channel: 'Test.ComputeSum',
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
console.log('Received:', msg.payload); // Typed as { a: number; b: number }
|
|
394
|
+
|
|
395
|
+
// With group specification
|
|
396
|
+
const response = await msgBus.once({
|
|
397
|
+
channel: 'Test.ComputeSum',
|
|
398
|
+
group: 'out',
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
console.log('Result:', response.payload); // Typed as number
|
|
402
|
+
|
|
403
|
+
// With topic filtering
|
|
404
|
+
const taskMsg = await msgBus.once({
|
|
405
|
+
channel: 'Test.DoSomeWork',
|
|
406
|
+
topic: '/^priority-.*/', // Match topics starting with "priority-"
|
|
407
|
+
});
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
#### Timeout Configuration
|
|
411
|
+
|
|
412
|
+
Configure timeout duration via the `timeout` option. The `abortSignal` option also works with `once()`.
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
try {
|
|
416
|
+
const msg = await msgBus.once({
|
|
417
|
+
channel: 'Test.ComputeSum',
|
|
418
|
+
options: {
|
|
419
|
+
timeout: 5000, // 5 second timeout
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
console.log('Received:', msg.payload);
|
|
423
|
+
} catch (error) {
|
|
424
|
+
if (error instanceof TimeoutError) {
|
|
425
|
+
console.error('Timeout waiting for message');
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// With abort signal
|
|
430
|
+
const abortController = new AbortController();
|
|
431
|
+
|
|
432
|
+
const messagePromise = msgBus.once({
|
|
433
|
+
channel: 'Test.ComputeSum',
|
|
434
|
+
options: {
|
|
435
|
+
timeout: 10000,
|
|
436
|
+
abortSignal: abortController.signal,
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// Can cancel from elsewhere
|
|
441
|
+
setTimeout(() => abortController.abort('User cancelled'), 2000);
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
const msg = await messagePromise;
|
|
445
|
+
} catch (error) {
|
|
446
|
+
if (error instanceof AbortError) {
|
|
447
|
+
console.error('Aborted:', error.cause);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Providing Response Handlers: `provide()`
|
|
453
|
+
|
|
454
|
+
Register a handler for messages on a selected channel and group (typically `in`), which generates a response message for the `out` group of the same channel. This is essentially a subscription with automatic response handling.
|
|
455
|
+
|
|
456
|
+
The callback can be asynchronous and its result is automatically used to form the response.
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
// Simple provider
|
|
460
|
+
msgBus.provide({
|
|
461
|
+
channel: 'Test.ComputeSum',
|
|
462
|
+
callback: (msg) => {
|
|
463
|
+
// msg.payload is typed as { a: number; b: number }
|
|
464
|
+
// Return type is inferred as number (from 'out' type)
|
|
465
|
+
return msg.payload.a + msg.payload.b;
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// Async provider
|
|
470
|
+
msgBus.provide({
|
|
471
|
+
channel: 'Test.DoSomeWork',
|
|
472
|
+
callback: async (msg) => {
|
|
473
|
+
// msg.payload is typed as string
|
|
474
|
+
await performWork(msg.payload);
|
|
475
|
+
// Return type is void (from 'out' type)
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// With topic filtering
|
|
480
|
+
msgBus.provide({
|
|
481
|
+
channel: 'Test.ComputeSum',
|
|
482
|
+
topic: '/^calc-.*/',
|
|
483
|
+
callback: (msg) => {
|
|
484
|
+
return msg.payload.a + msg.payload.b;
|
|
485
|
+
},
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// With options
|
|
489
|
+
msgBus.provide({
|
|
490
|
+
channel: 'Test.ComputeSum',
|
|
491
|
+
callback: (msg) => {
|
|
492
|
+
return msg.payload.a + msg.payload.b;
|
|
493
|
+
},
|
|
494
|
+
options: {
|
|
495
|
+
fetchCount: 100, // Handle 100 requests then unsubscribe
|
|
496
|
+
abortSignal: someController.signal,
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### Sending Messages: `send()`
|
|
502
|
+
|
|
503
|
+
Send a message to the bus for a specific channel and group (default is `in`). The payload type is enforced according to the bus structure.
|
|
504
|
+
|
|
505
|
+
```typescript
|
|
506
|
+
// Basic send
|
|
507
|
+
await msgBus.send({
|
|
508
|
+
channel: 'Test.ComputeSum',
|
|
509
|
+
payload: { a: 10, b: 20 }, // Typed and validated
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// With group specification
|
|
513
|
+
await msgBus.send({
|
|
514
|
+
channel: 'Test.Multiplexer',
|
|
515
|
+
group: 'in1',
|
|
516
|
+
payload: 'hello', // Typed as string for 'in1' group
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
await msgBus.send({
|
|
520
|
+
channel: 'Test.Multiplexer',
|
|
521
|
+
group: 'in2',
|
|
522
|
+
payload: 42, // Typed as number for 'in2' group
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
// With topic
|
|
526
|
+
await msgBus.send({
|
|
527
|
+
channel: 'Test.DoSomeWork',
|
|
528
|
+
topic: 'priority-high',
|
|
529
|
+
payload: 'urgent task',
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// With custom headers
|
|
533
|
+
await msgBus.send({
|
|
534
|
+
channel: 'Test.ComputeSum',
|
|
535
|
+
payload: { a: 5, b: 15 },
|
|
536
|
+
headers: {
|
|
537
|
+
correlationId: 'task-123',
|
|
538
|
+
priority: 1,
|
|
539
|
+
},
|
|
540
|
+
});
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
#### Important Notes
|
|
544
|
+
|
|
545
|
+
1. **Response Handling**: Any message sent to a non-`out` group can receive a response through the bus, which will be routed to the `out` group of the same channel.
|
|
546
|
+
|
|
547
|
+
2. **Topic Specification**: You can specify a topic when sending to enable fine-grained filtering by subscribers.
|
|
548
|
+
|
|
549
|
+
### Request-Response Pattern: `request()`
|
|
550
|
+
|
|
551
|
+
Send a message and automatically await a response from a handler (registered via `provide()`) on the same channel's `out` group. Returns a Promise that resolves with the response message.
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
// Basic request
|
|
555
|
+
const response = await msgBus.request({
|
|
556
|
+
channel: 'Test.ComputeSum',
|
|
557
|
+
payload: { a: 10, b: 20 },
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
console.log('Result:', response.payload); // Typed as number
|
|
561
|
+
|
|
562
|
+
// With group overloading (using different input groups)
|
|
563
|
+
const response1 = await msgBus.request({
|
|
564
|
+
channel: 'Test.Multiplexer',
|
|
565
|
+
group: 'in1',
|
|
566
|
+
payload: 'hello',
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
const response2 = await msgBus.request({
|
|
570
|
+
channel: 'Test.Multiplexer',
|
|
571
|
+
group: 'in2',
|
|
572
|
+
payload: 42,
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// Both responses have payload with type - number ('out' group)
|
|
576
|
+
|
|
577
|
+
// With timeout
|
|
578
|
+
try {
|
|
579
|
+
const response = await msgBus.request({
|
|
580
|
+
channel: 'Test.ComputeSum',
|
|
581
|
+
payload: { a: 5, b: 15 },
|
|
582
|
+
options: {
|
|
583
|
+
timeout: 5000, // Overall timeout
|
|
584
|
+
},
|
|
585
|
+
});
|
|
586
|
+
} catch (error) {
|
|
587
|
+
if (error instanceof TimeoutError) {
|
|
588
|
+
console.error('Request timed out');
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// With separate send and response timeouts
|
|
593
|
+
const response = await msgBus.request({
|
|
594
|
+
channel: 'Test.ComputeSum',
|
|
595
|
+
payload: { a: 5, b: 15 },
|
|
596
|
+
options: {
|
|
597
|
+
sendTimeout: 1000, // Timeout for sending the message
|
|
598
|
+
responseTimeout: 5000, // Timeout for receiving the response
|
|
599
|
+
},
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// With headers for correlation
|
|
603
|
+
const response = await msgBus.request({
|
|
604
|
+
channel: 'Test.ComputeSum',
|
|
605
|
+
payload: { a: 5, b: 15 },
|
|
606
|
+
headers: {
|
|
607
|
+
sourceId: 'component-123',
|
|
608
|
+
correlationId: 'request-456',
|
|
609
|
+
},
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
// The response will include matching headers
|
|
613
|
+
console.log(response.headers.requestId); // Original message ID
|
|
614
|
+
console.log(response.headers.correlationId); // Preserved correlation ID
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
#### Key Features
|
|
618
|
+
|
|
619
|
+
1. **Input Type Overloading**: Use different input groups within the same channel to support multiple request signatures while maintaining a single response type.
|
|
620
|
+
|
|
621
|
+
2. **Timeout Control**: Configure response timeout via the `responseTimeout` option to prevent indefinite waiting.
|
|
622
|
+
|
|
623
|
+
3. **Header Propagation**: Headers like `correlationId` are automatically propagated from request to response for tracing.
|
|
624
|
+
|
|
625
|
+
### Streaming Messages: `stream()`
|
|
626
|
+
|
|
627
|
+
Create an async iterable iterator for consuming messages as a stream.
|
|
628
|
+
|
|
629
|
+
```typescript
|
|
630
|
+
// Basic streaming
|
|
631
|
+
const messageStream = msgBus.stream({
|
|
632
|
+
channel: 'Test.ComputeSum',
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
for await (const msg of messageStream) {
|
|
636
|
+
console.log('Received:', msg.payload);
|
|
637
|
+
// Process messages as they arrive
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// With topic filtering
|
|
641
|
+
const taskStream = msgBus.stream({
|
|
642
|
+
channel: 'Test.DoSomeWork',
|
|
643
|
+
topic: '/^task-.*/',
|
|
644
|
+
options: {
|
|
645
|
+
timeout: 30000, // Stop streaming after 30s of inactivity
|
|
646
|
+
},
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
for await (const msg of taskStream) {
|
|
650
|
+
await processTask(msg.payload);
|
|
651
|
+
}
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
## Advanced Features
|
|
655
|
+
|
|
656
|
+
### Message Replay
|
|
657
|
+
|
|
658
|
+
Configure channels to buffer and replay messages for late subscribers.
|
|
659
|
+
|
|
660
|
+
```typescript
|
|
661
|
+
const msgBus = createMsgBus<MyBusStruct>({
|
|
662
|
+
'Test.Events': {
|
|
663
|
+
replayBufferSize: 50, // Keep last 50 messages
|
|
664
|
+
replayWindowTime: 60000, // Keep messages for 60 seconds
|
|
665
|
+
},
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
// Send messages
|
|
669
|
+
for (let i = 0; i < 100; i++) {
|
|
670
|
+
await msgBus.send({
|
|
671
|
+
channel: 'Test.Events',
|
|
672
|
+
payload: `Message ${i}`,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Late subscriber receives last 50 messages
|
|
677
|
+
msgBus.on({
|
|
678
|
+
channel: 'Test.Events',
|
|
679
|
+
callback: (msg) => {
|
|
680
|
+
console.log('Replayed:', msg.payload);
|
|
681
|
+
},
|
|
682
|
+
});
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
### Throttling and Debouncing
|
|
686
|
+
|
|
687
|
+
Control message processing rate at both channel and subscription levels.
|
|
688
|
+
|
|
689
|
+
```typescript
|
|
690
|
+
// Channel-level throttling
|
|
691
|
+
const msgBus = createMsgBus<MyBusStruct>({
|
|
692
|
+
'Test.Updates': {
|
|
693
|
+
throttle: {
|
|
694
|
+
duration: 1000,
|
|
695
|
+
leading: true,
|
|
696
|
+
trailing: true,
|
|
697
|
+
},
|
|
698
|
+
},
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
// Subscription-level debouncing
|
|
702
|
+
msgBus.on({
|
|
703
|
+
channel: 'Test.Updates',
|
|
704
|
+
callback: (msg) => {
|
|
705
|
+
updateUI(msg.payload);
|
|
706
|
+
},
|
|
707
|
+
options: {
|
|
708
|
+
debounce: 500, // Wait 500ms of inactivity before processing
|
|
709
|
+
},
|
|
710
|
+
});
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
### Error Handling
|
|
714
|
+
|
|
715
|
+
The bus includes built-in error handling and a reserved error channel.
|
|
716
|
+
|
|
717
|
+
```typescript
|
|
718
|
+
// Subscribe to errors for a specific channel
|
|
719
|
+
msgBus.on({
|
|
720
|
+
channel: 'Test.ComputeSum',
|
|
721
|
+
group: 'error',
|
|
722
|
+
callback: (msg) => {
|
|
723
|
+
console.error('Error in ComputeSum:', msg.payload.error);
|
|
724
|
+
},
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
// Subscribe to all system errors
|
|
728
|
+
msgBus.on({
|
|
729
|
+
channel: 'MSGBUS.ERROR',
|
|
730
|
+
callback: (msg) => {
|
|
731
|
+
console.error('System error:', msg.payload);
|
|
732
|
+
},
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
// Errors in providers are automatically caught and routed
|
|
736
|
+
msgBus.provide({
|
|
737
|
+
channel: 'Test.ComputeSum',
|
|
738
|
+
callback: (msg) => {
|
|
739
|
+
if (msg.payload.a < 0) {
|
|
740
|
+
throw new Error('Negative numbers not allowed');
|
|
741
|
+
}
|
|
742
|
+
return msg.payload.a + msg.payload.b;
|
|
743
|
+
},
|
|
744
|
+
});
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
### Headers and Metadata
|
|
748
|
+
|
|
749
|
+
Messages support rich metadata through headers.
|
|
750
|
+
|
|
751
|
+
```typescript
|
|
752
|
+
import { MsgHeaders } from '@actdim/msgmesh';
|
|
753
|
+
|
|
754
|
+
// Standard headers
|
|
755
|
+
type StandardHeaders = {
|
|
756
|
+
sourceId?: string; // Sender identifier
|
|
757
|
+
targetId?: string; // Recipient identifier
|
|
758
|
+
correlationId?: string; // Activity/trace identifier
|
|
759
|
+
traceId?: string; // Distributed trace identifier
|
|
760
|
+
requestId?: string; // Original request identifier
|
|
761
|
+
inResponseToId?: string; // Reply reference
|
|
762
|
+
publishedAt?: number; // Timestamp (Unix epoch, ms)
|
|
763
|
+
priority?: number; // Message priority
|
|
764
|
+
ttl?: number; // Time to live (ms)
|
|
765
|
+
tags?: string | string[]; // Message tags
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
// Custom headers
|
|
769
|
+
type MyHeaders = MsgHeaders & {
|
|
770
|
+
userId: string;
|
|
771
|
+
tenantId: string;
|
|
772
|
+
version: string;
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
const msgBus = createMsgBus<MyBusStruct, MyHeaders>();
|
|
776
|
+
|
|
777
|
+
await msgBus.send({
|
|
778
|
+
channel: 'Test.ComputeSum',
|
|
779
|
+
payload: { a: 10, b: 20 },
|
|
780
|
+
headers: {
|
|
781
|
+
userId: 'user-123',
|
|
782
|
+
tenantId: 'tenant-456',
|
|
783
|
+
version: '1.0',
|
|
784
|
+
correlationId: 'trace-789',
|
|
785
|
+
priority: 1,
|
|
786
|
+
},
|
|
787
|
+
});
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
## Comparison with Other Solutions
|
|
791
|
+
|
|
792
|
+
| Feature | @actdim/msgmesh | Event Emitters | RxJS |
|
|
793
|
+
| ---------------- | --------------- | -------------- | ----------- |
|
|
794
|
+
| Type Safety | ✅ Full | ⚠️ Limited | ✅ Full |
|
|
795
|
+
| Learning Curve | Low | Low | High |
|
|
796
|
+
| Async Support | ✅ Native | ⚠️ Limited | ✅ Full |
|
|
797
|
+
| Request-Response | ✅ Built-in | ❌ Manual | ⚠️ Complex |
|
|
798
|
+
| Boilerplate | Minimal | Minimal | Medium |
|
|
799
|
+
| Paradigm Shift | None | None | Significant |
|
|
800
|
+
| Scalability | ✅ Excellent | ⚠️ Limited | ✅ Good |
|
|
801
|
+
|
|
802
|
+
## Conclusion
|
|
803
|
+
|
|
804
|
+
@actdim/msgmesh provides a powerful, type-safe, and flexible message bus solution for TypeScript applications. It combines the simplicity of event emitters with the power of message-oriented middleware, while maintaining excellent type safety and developer experience.
|
|
805
|
+
|
|
806
|
+
Key benefits:
|
|
807
|
+
|
|
808
|
+
- **Type Safety**: Full TypeScript support with compile-time checks
|
|
809
|
+
- **Flexibility**: Works at any scale - from single components to entire applications
|
|
810
|
+
- **Observability**: Built-in support for logging, tracing, and debugging
|
|
811
|
+
- **Developer Experience**: Minimal boilerplate, clear API, excellent IntelliSense support
|
|
812
|
+
- **Performance**: Single-instance architecture with efficient message routing
|
|
813
|
+
- **Integration**: Works seamlessly with React, async operations, and existing patterns
|
|
814
|
+
|
|
815
|
+
The message bus serves as a solid foundation for the @actdim/dynstruct architectural framework, enabling the development of scalable, maintainable, and testable applications.
|
|
816
|
+
|
|
817
|
+
## TODO
|
|
818
|
+
|
|
819
|
+
- rate limiting (for single channel, using signal after auto-'ack') and backpressure (for "in" and "out" channel pair), real send promise
|
|
820
|
+
|
|
821
|
+
## Further Reading
|
|
822
|
+
|
|
823
|
+
- [GitHub Repository](https://github.com/actdim/msgmesh)
|
|
824
|
+
- [@actdim/dynstruct Documentation](https://github.com/actdim/dynstruct)
|
|
825
|
+
- [Type Safety Best Practices](https://www.typescriptlang.org/docs/handbook/2/types-from-types.html)
|
|
826
|
+
- [Message-Oriented Middleware Patterns](https://www.enterpriseintegrationpatterns.com/)
|