@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 CHANGED
@@ -4,6 +4,39 @@
4
4
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.9+-blue.svg)](https://www.typescriptlang.org/)
5
5
  [![License: Proprietary](https://img.shields.io/badge/License-Proprietary-red.svg)](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;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actdim/msgmesh",
3
- "version": "1.2.8",
3
+ "version": "1.2.9",
4
4
  "description": "A type-safe, modular message mesh for scalable async communication in TypeScript",
5
5
  "author": "Pavel Borodaev",
6
6
  "license": "Proprietary",