@actdim/msgmesh 1.2.8 → 1.2.9
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 +194 -3
- package/dist/adapters.d.ts +20 -0
- package/dist/adapters.d.ts.map +1 -0
- package/dist/adapters.es.js +33 -0
- package/dist/adapters.es.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,6 +4,39 @@
|
|
|
4
4
|
[](https://www.typescriptlang.org/)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Quick Start](#quick-start)
|
|
10
|
+
- [Installation](#installation)
|
|
11
|
+
- [Overview](#overview)
|
|
12
|
+
- [The Challenge](#the-challenge)
|
|
13
|
+
- [Analysis of Existing Solutions](#analysis-of-existing-solutions)
|
|
14
|
+
- [The Solution](#the-solution-actdimmsgmesh)
|
|
15
|
+
- [Implementation Foundation](#implementation-foundation)
|
|
16
|
+
- [Key Design Goals](#key-design-goals)
|
|
17
|
+
- [Architecture](#architecture)
|
|
18
|
+
- [Message Structure](#message-structure)
|
|
19
|
+
- [Type Definition Example](#type-definition-example)
|
|
20
|
+
- [Usage Patterns](#usage-patterns)
|
|
21
|
+
- [Global vs Local Usage](#global-vs-local-usage)
|
|
22
|
+
- [Creating a Message Bus](#creating-a-message-bus)
|
|
23
|
+
- [Type Utilities](#type-utilities)
|
|
24
|
+
- [API Reference](#api-reference)
|
|
25
|
+
- [Configuration](#configuration)
|
|
26
|
+
- [`send()`](#sending-messages-send)
|
|
27
|
+
- [`on()`](#subscribing-to-messages-on)
|
|
28
|
+
- [`once()`](#awaiting-a-single-message-once)
|
|
29
|
+
- [`stream()`](#streaming-messages-stream)
|
|
30
|
+
- [`provide()`](#providing-response-handlers-provide)
|
|
31
|
+
- [`request()`](#request-response-pattern-request)
|
|
32
|
+
- [Advanced Features](#advanced-features)
|
|
33
|
+
- [Message Replay](#message-replay)
|
|
34
|
+
- [Throttling and Debouncing](#throttling-and-debouncing)
|
|
35
|
+
- [Error Handling](#error-handling)
|
|
36
|
+
- [Headers and Metadata](#headers-and-metadata)
|
|
37
|
+
- [Service Adapters](#service-adapters)
|
|
38
|
+
- [Comparison](#comparison-with-other-solutions)
|
|
39
|
+
|
|
7
40
|
## Quick Start
|
|
8
41
|
|
|
9
42
|
Try @actdim/msgmesh instantly in your browser without any installation:
|
|
@@ -545,9 +578,6 @@ for await (const msg of messageStream) {
|
|
|
545
578
|
const taskStream = msgBus.stream({
|
|
546
579
|
channel: 'Test.DoSomeWork',
|
|
547
580
|
topic: '/^task-.*/',
|
|
548
|
-
options: {
|
|
549
|
-
timeout: 30000, // Stop streaming after 30s of inactivity
|
|
550
|
-
},
|
|
551
581
|
});
|
|
552
582
|
|
|
553
583
|
for await (const msg of taskStream) {
|
|
@@ -555,6 +585,39 @@ for await (const msg of taskStream) {
|
|
|
555
585
|
}
|
|
556
586
|
```
|
|
557
587
|
|
|
588
|
+
#### Timeout and Cancellation
|
|
589
|
+
|
|
590
|
+
The `timeout` option is an **inactivity timeout** — the timer resets on each received message. If no message arrives within the timeout window, the stream ends with a `TimeoutError`. This is useful for detecting when the producer has stopped sending.
|
|
591
|
+
|
|
592
|
+
For a hard time limit on the stream's total duration, use `AbortSignal.timeout()`.
|
|
593
|
+
|
|
594
|
+
```typescript
|
|
595
|
+
// Inactivity timeout: end stream if no messages for 5s
|
|
596
|
+
const stream1 = msgBus.stream({
|
|
597
|
+
channel: 'Test.Events',
|
|
598
|
+
options: {
|
|
599
|
+
timeout: 5000,
|
|
600
|
+
},
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// Total duration limit: end stream after 60s regardless of activity
|
|
604
|
+
const stream2 = msgBus.stream({
|
|
605
|
+
channel: 'Test.Events',
|
|
606
|
+
options: {
|
|
607
|
+
abortSignal: AbortSignal.timeout(60000),
|
|
608
|
+
},
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
// Both: inactivity 5s + hard limit 60s
|
|
612
|
+
const stream3 = msgBus.stream({
|
|
613
|
+
channel: 'Test.Events',
|
|
614
|
+
options: {
|
|
615
|
+
timeout: 5000,
|
|
616
|
+
abortSignal: AbortSignal.timeout(60000),
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
```
|
|
620
|
+
|
|
558
621
|
### Providing Response Handlers: `provide()`
|
|
559
622
|
|
|
560
623
|
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.
|
|
@@ -896,6 +959,134 @@ await msgBus.send({
|
|
|
896
959
|
});
|
|
897
960
|
```
|
|
898
961
|
|
|
962
|
+
### Service Adapters
|
|
963
|
+
|
|
964
|
+
Automatically register any service object (e.g. a Swagger-generated API client) as a message bus provider. The adapter system uses TypeScript's type system to map service methods to bus channels at compile time — channel names, payload types, and return types are all derived from the service class. No manual wiring, no runtime errors.
|
|
965
|
+
|
|
966
|
+
#### How It Works
|
|
967
|
+
|
|
968
|
+
Given a service class:
|
|
969
|
+
|
|
970
|
+
```typescript
|
|
971
|
+
class OrderApiClient {
|
|
972
|
+
static readonly name = 'OrderApiClient' as const;
|
|
973
|
+
readonly name = 'OrderApiClient' as const;
|
|
974
|
+
|
|
975
|
+
createOrder(items: Item[], priority: number): Promise<OrderResult> { /* ... */ }
|
|
976
|
+
getOrder(id: string): Promise<Order> { /* ... */ }
|
|
977
|
+
|
|
978
|
+
// Internal helper — should not be exposed on the bus
|
|
979
|
+
formatResponse() { /* ... */ }
|
|
980
|
+
}
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
The type utilities transform it into a bus structure:
|
|
984
|
+
|
|
985
|
+
```typescript
|
|
986
|
+
import {
|
|
987
|
+
ToMsgChannelPrefix,
|
|
988
|
+
ToMsgStruct,
|
|
989
|
+
BaseServiceSuffix,
|
|
990
|
+
registerAdapters,
|
|
991
|
+
getMsgChannelSelector,
|
|
992
|
+
MsgProviderAdapter
|
|
993
|
+
} from '@actdim/msgmesh/adapters';
|
|
994
|
+
|
|
995
|
+
// 1. Generate channel prefix from class name
|
|
996
|
+
// "OrderApiClient" → remove suffix "Client" → uppercase → "API.ORDER."
|
|
997
|
+
type ApiPrefix = 'API';
|
|
998
|
+
type OrderChannelPrefix = ToMsgChannelPrefix<
|
|
999
|
+
typeof OrderApiClient.name, // "OrderApiClient"
|
|
1000
|
+
ApiPrefix, // "API"
|
|
1001
|
+
BaseServiceSuffix // removes CLIENT, API, SERVICE, etc.
|
|
1002
|
+
>;
|
|
1003
|
+
// Result: "API.ORDER."
|
|
1004
|
+
|
|
1005
|
+
// 2. Transform service methods into bus struct (skip internal methods)
|
|
1006
|
+
type OrderApiStruct = ToMsgStruct<
|
|
1007
|
+
OrderApiClient,
|
|
1008
|
+
OrderChannelPrefix,
|
|
1009
|
+
'formatResponse' // skip this method
|
|
1010
|
+
>;
|
|
1011
|
+
// Result type (compile-time):
|
|
1012
|
+
// {
|
|
1013
|
+
// "API.ORDER.CREATEORDER": {
|
|
1014
|
+
// in: [items: Item[], priority: number]; // ← tuple from Parameters<>
|
|
1015
|
+
// out: OrderResult; // ← from ReturnType<>
|
|
1016
|
+
// };
|
|
1017
|
+
// "API.ORDER.GETORDER": {
|
|
1018
|
+
// in: [id: string];
|
|
1019
|
+
// out: Order;
|
|
1020
|
+
// };
|
|
1021
|
+
// }
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
All channel names, payload types, and return types are verified at compile time. If you rename a method, add a parameter, or change a return type — the compiler catches it immediately.
|
|
1025
|
+
|
|
1026
|
+
#### Registering Adapters
|
|
1027
|
+
|
|
1028
|
+
```typescript
|
|
1029
|
+
const services: Record<OrderChannelPrefix, any> = {
|
|
1030
|
+
'API.ORDER.': new OrderApiClient(),
|
|
1031
|
+
};
|
|
1032
|
+
|
|
1033
|
+
const adapters = Object.entries(services).map(([_, service]) => ({
|
|
1034
|
+
service,
|
|
1035
|
+
channelSelector: getMsgChannelSelector(services),
|
|
1036
|
+
}) as MsgProviderAdapter);
|
|
1037
|
+
|
|
1038
|
+
const msgBus = createMsgBus<OrderApiStruct>();
|
|
1039
|
+
const abortController = new AbortController();
|
|
1040
|
+
|
|
1041
|
+
// Register all methods as providers
|
|
1042
|
+
registerAdapters(msgBus, adapters, abortController.signal);
|
|
1043
|
+
|
|
1044
|
+
// Clean up when done
|
|
1045
|
+
abortController.abort();
|
|
1046
|
+
```
|
|
1047
|
+
|
|
1048
|
+
`registerAdapters()` iterates over each method of the service prototype, resolves the channel name via `channelSelector`, and calls `msgBus.provide()` for each one. The provider callback spreads `msg.payload` (a tuple) as arguments to the original method: `service[methodName](...msg.payload)`.
|
|
1049
|
+
|
|
1050
|
+
#### Calling Adapted Methods
|
|
1051
|
+
|
|
1052
|
+
Since method parameters are mapped to tuple types in the bus struct, use `payloadFn` for a natural function-call syntax:
|
|
1053
|
+
|
|
1054
|
+
```typescript
|
|
1055
|
+
// Type-safe call — fn signature matches createOrder(items, priority)
|
|
1056
|
+
const response = await msgBus.request({
|
|
1057
|
+
channel: 'API.ORDER.CREATEORDER',
|
|
1058
|
+
payloadFn: fn => fn([{ id: '1', qty: 2 }], 1),
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
console.log(response.payload); // OrderResult
|
|
1062
|
+
|
|
1063
|
+
// Also works with payload directly (tuple)
|
|
1064
|
+
const response2 = await msgBus.request({
|
|
1065
|
+
channel: 'API.ORDER.GETORDER',
|
|
1066
|
+
payload: ['order-123'],
|
|
1067
|
+
});
|
|
1068
|
+
```
|
|
1069
|
+
|
|
1070
|
+
#### Type Transformation Chain
|
|
1071
|
+
|
|
1072
|
+
```
|
|
1073
|
+
Service class ToMsgChannelPrefix ToMsgStruct
|
|
1074
|
+
───────────── ────────────────── ───────────
|
|
1075
|
+
OrderApiClient → "API.ORDER." → Bus struct
|
|
1076
|
+
.createOrder(a, b) ↑ ↓
|
|
1077
|
+
.getOrder(id) removes suffix "API.ORDER.CREATEORDER"
|
|
1078
|
+
.formatResponse() from class name in: [a, b] (Parameters<>)
|
|
1079
|
+
+ uppercases out: Result (ReturnType<>)
|
|
1080
|
+
"API.ORDER.GETORDER"
|
|
1081
|
+
in: [id]
|
|
1082
|
+
out: Order
|
|
1083
|
+
(formatResponse skipped)
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
#### Supported Service Suffixes
|
|
1087
|
+
|
|
1088
|
+
The following suffixes are automatically removed from class names: `CLIENT`, `API`, `SERVICE`, `FETCHER`, `CONTROLLER`, `LOADER`, `REPOSITORY`, `PROVIDER`.
|
|
1089
|
+
|
|
899
1090
|
## Comparison with Other Solutions
|
|
900
1091
|
|
|
901
1092
|
| Feature | @actdim/msgmesh | Event Emitters | RxJS |
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { MsgBus, MsgStruct, MsgStructFactory } from './contracts';
|
|
2
|
+
import { AddPrefix, Filter, Func, RemoveSuffix, Skip, ToUpper } from '@actdim/utico/typeCore';
|
|
3
|
+
export type MsgProviderAdapter = {
|
|
4
|
+
service: any;
|
|
5
|
+
channelSelector: (service: any, methodName: string) => string;
|
|
6
|
+
};
|
|
7
|
+
export declare function registerAdapters(msgBus: MsgBus<MsgStruct>, adapters: MsgProviderAdapter[], abortSignal?: AbortSignal): void;
|
|
8
|
+
export type BaseServiceSuffix = 'CLIENT' | 'API' | 'SERVICE' | 'FETCHER' | 'CONTROLLER' | 'LOADER' | 'REPOSITORY' | 'PROVIDER';
|
|
9
|
+
export type BaseWordSeparator = ".";
|
|
10
|
+
export type ToMsgChannelPrefix<TServiceName extends string, Prefix extends string, Suffix extends string = BaseServiceSuffix, WordSeparator extends string = BaseWordSeparator> = `${Prefix}${WordSeparator}${RemoveSuffix<Uppercase<TServiceName>, Suffix>}${WordSeparator}`;
|
|
11
|
+
type ToMsgStructSource<TService, TPrefix extends string, TSkip extends keyof TService = never> = Filter<ToUpper<AddPrefix<Skip<TService, TSkip>, TPrefix>>, Func>;
|
|
12
|
+
export type ToMsgStruct<TService, TPrefix extends string, TSkip extends keyof TService = never, TMsgStructSource = ToMsgStructSource<TService, TPrefix, TSkip>> = MsgStructFactory<{
|
|
13
|
+
[K in keyof TMsgStructSource as TMsgStructSource[K] extends Func ? (Uppercase<K extends string ? K : never>) : never]: {
|
|
14
|
+
in: TMsgStructSource[K] extends Func ? Parameters<TMsgStructSource[K]> : never;
|
|
15
|
+
out: TMsgStructSource[K] extends Func ? ReturnType<TMsgStructSource[K]> : never;
|
|
16
|
+
};
|
|
17
|
+
}>;
|
|
18
|
+
export declare function getMsgChannelSelector<TTPrefix extends string>(services: Record<TTPrefix, any>): (service: any, methodName: string) => string;
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=adapters.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapters.d.ts","sourceRoot":"","sources":["../src/adapters.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAY9F,MAAM,MAAM,kBAAkB,GAAG;IAC7B,OAAO,EAAE,GAAG,CAAC;IAEb,eAAe,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,KAAK,MAAM,CAAC;CACjE,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,kBAAkB,EAAE,EAAE,WAAW,CAAC,EAAE,WAAW,QAwBpH;AAED,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,KAAK,GAAG,SAAS,GAAG,SAAS,GAAG,YAAY,GAAG,QAAQ,GAAG,YAAY,GAAG,UAAU,CAAC;AAC/H,MAAM,MAAM,iBAAiB,GAAG,GAAG,CAAC;AAIpC,MAAM,MAAM,kBAAkB,CAC1B,YAAY,SAAS,MAAM,EAC3B,MAAM,SAAS,MAAM,EACrB,MAAM,SAAS,MAAM,GAAG,iBAAiB,EACzC,aAAa,SAAS,MAAM,GAAG,iBAAiB,IAChD,GAAG,MAAM,GAAG,aAAa,GAAG,YAAY,CAAC,SAAS,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC,GAAG,aAAa,EAAE,CAAC;AAEhG,KAAK,iBAAiB,CAAC,QAAQ,EAAE,OAAO,SAAS,MAAM,EAAE,KAAK,SAAS,MAAM,QAAQ,GAAG,KAAK,IAAI,MAAM,CACnG,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC,EAClD,IAAI,CACP,CAAC;AAEF,MAAM,MAAM,WAAW,CAAC,QAAQ,EAAE,OAAO,SAAS,MAAM,EAAE,KAAK,SAAS,MAAM,QAAQ,GAAG,KAAK,EAAE,gBAAgB,GAAG,iBAAiB,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,CAAC,IAAI,gBAAgB,CAAC;KAC9K,CAAC,IAAI,MAAM,gBAAgB,IAAI,gBAAgB,CAAC,CAAC,CAAC,SAAS,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC,SAAS,MAAM,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,GAAG,KAAK,GAAG;QACnH,EAAE,EAAE,gBAAgB,CAAC,CAAC,CAAC,SAAS,IAAI,GAAG,UAAU,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC;QAC/E,GAAG,EAAE,gBAAgB,CAAC,CAAC,CAAC,SAAS,IAAI,GAAG,UAAU,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC;KACnF;CACJ,CAAC,CAAC;AAEH,wBAAgB,qBAAqB,CAAC,QAAQ,SAAS,MAAM,EACzD,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,IAEvB,SAAS,GAAG,EAAE,YAAY,MAAM,YAO3C"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const s = (t) => Object.getOwnPropertyNames(t).filter(
|
|
2
|
+
(e) => e !== "constructor" && typeof t[e] == "function"
|
|
3
|
+
);
|
|
4
|
+
function l(t, e, n) {
|
|
5
|
+
if (e)
|
|
6
|
+
for (const o of e) {
|
|
7
|
+
const { service: r, channelSelector: c } = o;
|
|
8
|
+
if (!r || !c)
|
|
9
|
+
throw new Error("Service and channelSelector are required for an adapter");
|
|
10
|
+
for (const a of s(Object.getPrototypeOf(r))) {
|
|
11
|
+
const f = c?.(r, a);
|
|
12
|
+
f && t.provide({
|
|
13
|
+
channel: f,
|
|
14
|
+
topic: "/.*/",
|
|
15
|
+
callback: (i) => r[a](...i.payload || []),
|
|
16
|
+
options: {
|
|
17
|
+
abortSignal: n
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function p(t) {
|
|
24
|
+
return (e, n) => {
|
|
25
|
+
const o = Object.entries(t).find((r) => r[1] === e);
|
|
26
|
+
return o ? `${o[0]}${n.toUpperCase()}` : null;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
p as getMsgChannelSelector,
|
|
31
|
+
l as registerAdapters
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=adapters.es.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapters.es.js","sources":["../src/adapters.ts"],"sourcesContent":null,"names":["getMethodNames","client","name","registerAdapters","msgBus","adapters","abortSignal","adapter","service","channelSelector","methodName","channel","msg","getMsgChannelSelector","services","entry"],"mappings":"AAGA,MAAMA,IAAiB,CAACC,MAEb,OAAO,oBAAoBA,CAAM,EAAE;AAAA,EACtC,CAACC,MAASA,MAAS,iBAAiB,OAAOD,EAAOC,CAAI,KAAM;AAAA;AAa7D,SAASC,EAAiBC,GAA2BC,GAAgCC,GAA2B;AACnH,MAAID;AACA,eAAWE,KAAWF,GAAU;AAC5B,YAAM,EAAE,SAAAG,GAAS,iBAAAC,EAAA,IAAoBF;AACrC,UAAI,CAACC,KAAW,CAACC;AACb,cAAM,IAAI,MAAM,yDAAyD;AAE7E,iBAAWC,KAAcV,EAAe,OAAO,eAAeQ,CAAO,CAAC,GAAG;AACrE,cAAMG,IAAUF,IAAkBD,GAASE,CAAU;AACrD,QAAIC,KACAP,EAAO,QAAQ;AAAA,UACX,SAAAO;AAAA,UACA,OAAO;AAAA,UACP,UAAU,CAACC,MACCJ,EAAQE,CAAU,EAAW,GAAKE,EAAI,WAAW,CAAA,CAAa;AAAA,UAE1E,SAAS;AAAA,YACL,aAAAN;AAAA,UAAA;AAAA,QACJ,CACH;AAAA,MAET;AAAA,IACJ;AAER;AA0BO,SAASO,EACZC,GACF;AACE,SAAO,CAACN,GAAcE,MAAuB;AACzC,UAAMK,IAAQ,OAAO,QAAQD,CAAQ,EAAE,KAAK,CAACC,MAAUA,EAAM,CAAC,MAAMP,CAAO;AAC3E,WAAKO,IAGE,GAAGA,EAAM,CAAC,CAAC,GAAGL,EAAW,aAAa,KAFlC;AAAA,EAGf;AACJ;"}
|