@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 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
+ [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](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/)