@fluojs/microservices 1.0.0-beta.1

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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.ko.md +182 -0
  3. package/README.md +179 -0
  4. package/dist/decorators.d.ts +51 -0
  5. package/dist/decorators.d.ts.map +1 -0
  6. package/dist/decorators.js +106 -0
  7. package/dist/index.d.ts +15 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +13 -0
  10. package/dist/metadata.d.ts +9 -0
  11. package/dist/metadata.d.ts.map +1 -0
  12. package/dist/metadata.js +48 -0
  13. package/dist/module.d.ts +23 -0
  14. package/dist/module.d.ts.map +1 -0
  15. package/dist/module.js +55 -0
  16. package/dist/service.d.ts +116 -0
  17. package/dist/service.d.ts.map +1 -0
  18. package/dist/service.js +550 -0
  19. package/dist/status.d.ts +30 -0
  20. package/dist/status.d.ts.map +1 -0
  21. package/dist/status.js +79 -0
  22. package/dist/tokens.d.ts +7 -0
  23. package/dist/tokens.d.ts.map +1 -0
  24. package/dist/tokens.js +4 -0
  25. package/dist/transports/event-handler-logger.d.ts +3 -0
  26. package/dist/transports/event-handler-logger.d.ts.map +1 -0
  27. package/dist/transports/event-handler-logger.js +3 -0
  28. package/dist/transports/grpc-transport.d.ts +193 -0
  29. package/dist/transports/grpc-transport.d.ts.map +1 -0
  30. package/dist/transports/grpc-transport.js +1035 -0
  31. package/dist/transports/kafka-transport.d.ts +77 -0
  32. package/dist/transports/kafka-transport.d.ts.map +1 -0
  33. package/dist/transports/kafka-transport.js +289 -0
  34. package/dist/transports/mqtt-transport.d.ts +124 -0
  35. package/dist/transports/mqtt-transport.d.ts.map +1 -0
  36. package/dist/transports/mqtt-transport.js +460 -0
  37. package/dist/transports/nats-transport.d.ts +92 -0
  38. package/dist/transports/nats-transport.d.ts.map +1 -0
  39. package/dist/transports/nats-transport.js +218 -0
  40. package/dist/transports/rabbitmq-transport.d.ts +77 -0
  41. package/dist/transports/rabbitmq-transport.d.ts.map +1 -0
  42. package/dist/transports/rabbitmq-transport.js +263 -0
  43. package/dist/transports/redis-streams-transport.d.ts +136 -0
  44. package/dist/transports/redis-streams-transport.d.ts.map +1 -0
  45. package/dist/transports/redis-streams-transport.js +482 -0
  46. package/dist/transports/redis-transport.d.ts +73 -0
  47. package/dist/transports/redis-transport.d.ts.map +1 -0
  48. package/dist/transports/redis-transport.js +152 -0
  49. package/dist/transports/tcp-transport.d.ts +66 -0
  50. package/dist/transports/tcp-transport.d.ts.map +1 -0
  51. package/dist/transports/tcp-transport.js +283 -0
  52. package/dist/types.d.ts +105 -0
  53. package/dist/types.d.ts.map +1 -0
  54. package/dist/types.js +1 -0
  55. package/package.json +105 -0
@@ -0,0 +1,218 @@
1
+ import { logTransportEventHandlerFailure } from './event-handler-logger.js';
2
+
3
+ /** Options for configuring the NATS microservice transport. */
4
+
5
+ /**
6
+ * NATS transport for request-response messages and fire-and-forget event delivery.
7
+ *
8
+ * The adapter maps Fluo message traffic onto separate event and request subjects while
9
+ * preserving JSON framing and NATS request timeout behavior.
10
+ */
11
+ export class NatsMicroserviceTransport {
12
+ closing = false;
13
+ handler;
14
+ logger;
15
+ listening = false;
16
+ eventSubject;
17
+ messageSubject;
18
+ pending = new Map();
19
+ requestTimeoutMs;
20
+ subscriptions = [];
21
+ logEventHandlerFailure(error) {
22
+ logTransportEventHandlerFailure(this.logger, 'NatsMicroserviceTransport', error);
23
+ }
24
+ handleEventMessageSafely(message) {
25
+ void this.handleEventMessage(message).catch(error => {
26
+ this.logEventHandlerFailure(error);
27
+ });
28
+ }
29
+
30
+ /**
31
+ * Creates a NATS transport using a client and codec supplied by the application.
32
+ *
33
+ * @param options Subject names, codec, client, and request-timeout settings.
34
+ */
35
+ constructor(options) {
36
+ this.options = options;
37
+ this.eventSubject = options.eventSubject ?? 'fluo.microservices.events';
38
+ this.messageSubject = options.messageSubject ?? 'fluo.microservices.messages';
39
+ this.requestTimeoutMs = options.requestTimeoutMs ?? 3_000;
40
+ }
41
+ setLogger(logger) {
42
+ this.logger = logger;
43
+ }
44
+
45
+ /**
46
+ * Subscribes to the configured NATS event and message subjects.
47
+ *
48
+ * @param handler Runtime callback invoked for inbound event and message packets.
49
+ * @returns A promise that resolves once subscriptions are active.
50
+ */
51
+ async listen(handler) {
52
+ this.closing = false;
53
+ this.handler = handler;
54
+ if (this.listening) {
55
+ return;
56
+ }
57
+ const eventSubscription = this.options.client.subscribe(this.eventSubject, message => {
58
+ this.handleEventMessageSafely(message);
59
+ });
60
+ const messageSubscription = this.options.client.subscribe(this.messageSubject, message => {
61
+ void this.handleRequestMessage(message);
62
+ });
63
+ this.subscriptions = [eventSubscription, messageSubscription];
64
+ this.listening = true;
65
+ }
66
+
67
+ /**
68
+ * Sends one request-response message through NATS request/reply.
69
+ *
70
+ * @param pattern Pattern identifying the remote message handler.
71
+ * @param payload Serializable request payload.
72
+ * @returns The decoded remote handler response payload.
73
+ */
74
+ async send(pattern, payload) {
75
+ if (this.closing) {
76
+ throw new Error('NATS microservice transport is closing. Wait for close() to complete before send().');
77
+ }
78
+ if (!this.listening) {
79
+ throw new Error('NatsMicroserviceTransport is not listening. Call listen() before send().');
80
+ }
81
+ const request = {
82
+ kind: 'message',
83
+ pattern,
84
+ payload
85
+ };
86
+ const requestId = crypto.randomUUID();
87
+ return await new Promise((resolve, reject) => {
88
+ let settled = false;
89
+ const cleanup = () => {
90
+ if (settled) {
91
+ return;
92
+ }
93
+ settled = true;
94
+ this.pending.delete(requestId);
95
+ };
96
+ const entry = {
97
+ reject: error => {
98
+ cleanup();
99
+ reject(error);
100
+ },
101
+ resolve: value => {
102
+ cleanup();
103
+ resolve(value);
104
+ }
105
+ };
106
+ this.pending.set(requestId, entry);
107
+ void Promise.resolve().then(async () => {
108
+ if (this.closing) {
109
+ entry.reject(new Error('NATS microservice transport closed before request dispatch.'));
110
+ return;
111
+ }
112
+ const responseMessage = await this.options.client.request(this.messageSubject, this.encode(request), {
113
+ timeout: this.requestTimeoutMs
114
+ });
115
+ const response = this.decode(responseMessage.data);
116
+ if (response.error) {
117
+ entry.reject(new Error(response.error));
118
+ return;
119
+ }
120
+ entry.resolve(response.payload);
121
+ }).catch(error => {
122
+ entry.reject(error instanceof Error ? error : new Error('Failed to send NATS request.'));
123
+ });
124
+ });
125
+ }
126
+
127
+ /**
128
+ * Emits one fire-and-forget event through the configured NATS event subject.
129
+ *
130
+ * @param pattern Pattern identifying the remote event handler.
131
+ * @param payload Serializable event payload.
132
+ * @returns A promise that resolves once the event is published.
133
+ */
134
+ async emit(pattern, payload) {
135
+ const event = {
136
+ kind: 'event',
137
+ pattern,
138
+ payload
139
+ };
140
+ this.options.client.publish(this.eventSubject, this.encode(event));
141
+ }
142
+
143
+ /**
144
+ * Unsubscribes from NATS subjects and closes the client when supported.
145
+ *
146
+ * @returns A promise that resolves once shutdown cleanup completes.
147
+ */
148
+ async close() {
149
+ this.closing = true;
150
+ let closeError;
151
+ try {
152
+ for (const subscription of this.subscriptions) {
153
+ subscription.unsubscribe();
154
+ }
155
+ this.options.client.close?.();
156
+ } catch (error) {
157
+ closeError = error;
158
+ } finally {
159
+ this.subscriptions = [];
160
+ this.listening = false;
161
+ this.handler = undefined;
162
+ for (const pending of [...this.pending.values()]) {
163
+ pending.reject(new Error('NATS microservice transport closed before response.'));
164
+ }
165
+ }
166
+ if (closeError) {
167
+ throw closeError;
168
+ }
169
+ }
170
+ async handleEventMessage(message) {
171
+ if (!this.handler) {
172
+ return;
173
+ }
174
+ const packet = this.decode(message.data);
175
+ if (packet.kind !== 'event') {
176
+ return;
177
+ }
178
+ try {
179
+ await this.handler({
180
+ kind: 'event',
181
+ pattern: packet.pattern,
182
+ payload: packet.payload
183
+ });
184
+ } catch (error) {
185
+ this.logEventHandlerFailure(error);
186
+ }
187
+ }
188
+ async handleRequestMessage(message) {
189
+ if (!this.handler) {
190
+ return;
191
+ }
192
+ const packet = this.decode(message.data);
193
+ if (packet.kind !== 'message') {
194
+ return;
195
+ }
196
+ try {
197
+ const payload = await this.handler({
198
+ kind: 'message',
199
+ pattern: packet.pattern,
200
+ payload: packet.payload
201
+ });
202
+ message.respond(this.encode({
203
+ payload
204
+ }));
205
+ } catch (error) {
206
+ const errorMessage = error instanceof Error ? error.message : 'Unhandled microservice error';
207
+ message.respond(this.encode({
208
+ error: errorMessage
209
+ }));
210
+ }
211
+ }
212
+ decode(data) {
213
+ return JSON.parse(this.options.codec.decode(data));
214
+ }
215
+ encode(value) {
216
+ return this.options.codec.encode(JSON.stringify(value));
217
+ }
218
+ }
@@ -0,0 +1,77 @@
1
+ import type { MicroserviceTransport, TransportHandler } from '../types.js';
2
+ interface RabbitMqConsumerLike {
3
+ consume(queue: string, handler: (message: string) => Promise<void> | void): Promise<void>;
4
+ cancel(queue: string): Promise<void>;
5
+ }
6
+ interface RabbitMqPublisherLike {
7
+ publish(queue: string, message: string): Promise<void>;
8
+ }
9
+ /** Options for configuring the RabbitMQ microservice transport. */
10
+ export interface RabbitMqMicroserviceTransportOptions {
11
+ consumer: RabbitMqConsumerLike;
12
+ eventQueue?: string;
13
+ messageQueue?: string;
14
+ publisher: RabbitMqPublisherLike;
15
+ requestTimeoutMs?: number;
16
+ responseQueue?: string;
17
+ }
18
+ /**
19
+ * RabbitMQ transport for queue-backed request-response and event delivery.
20
+ *
21
+ * The adapter uses dedicated event, message, and response queues so applications can keep
22
+ * a queue-oriented topology while still consuming the generic Fluo transport API.
23
+ */
24
+ export declare class RabbitMqMicroserviceTransport implements MicroserviceTransport {
25
+ private readonly options;
26
+ private handler;
27
+ private listening;
28
+ private listenPromise;
29
+ private readonly eventQueue;
30
+ private readonly messageQueue;
31
+ private readonly pending;
32
+ private readonly requestTimeoutMs;
33
+ private readonly responseQueue;
34
+ private readonly subscribedQueues;
35
+ /**
36
+ * Creates a RabbitMQ transport with explicit consumer and publisher collaborators.
37
+ *
38
+ * @param options Queue names and timeout settings for request-response traffic.
39
+ */
40
+ constructor(options: RabbitMqMicroserviceTransportOptions);
41
+ /**
42
+ * Starts consuming the configured event, message, and response queues.
43
+ *
44
+ * @param handler Runtime callback invoked for inbound event and message packets.
45
+ * @returns A promise that resolves once queue consumers are registered.
46
+ */
47
+ listen(handler: TransportHandler): Promise<void>;
48
+ /**
49
+ * Sends one request-response message through RabbitMQ.
50
+ *
51
+ * @param pattern Pattern identifying the remote message handler.
52
+ * @param payload Serializable request payload.
53
+ * @param signal Optional abort signal used to cancel the request.
54
+ * @returns The remote handler response payload.
55
+ */
56
+ send(pattern: string, payload: unknown, signal?: AbortSignal): Promise<unknown>;
57
+ /**
58
+ * Emits one fire-and-forget event through RabbitMQ.
59
+ *
60
+ * @param pattern Pattern identifying the remote event handler.
61
+ * @param payload Serializable event payload.
62
+ * @returns A promise that resolves once the event is published.
63
+ */
64
+ emit(pattern: string, payload: unknown): Promise<void>;
65
+ /**
66
+ * Cancels queue consumers and rejects any in-flight requests.
67
+ *
68
+ * @returns A promise that resolves once shutdown cleanup completes.
69
+ */
70
+ close(): Promise<void>;
71
+ private handleInboundMessage;
72
+ private handleRequest;
73
+ private handleResponse;
74
+ private rejectPendingRequests;
75
+ }
76
+ export {};
77
+ //# sourceMappingURL=rabbitmq-transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rabbitmq-transport.d.ts","sourceRoot":"","sources":["../../src/transports/rabbitmq-transport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE3E,UAAU,oBAAoB;IAC5B,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1F,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACtC;AAED,UAAU,qBAAqB;IAC7B,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACxD;AAWD,mEAAmE;AACnE,MAAM,WAAW,oCAAoC;IACnD,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,qBAAqB,CAAC;IACjC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;GAKG;AACH,qBAAa,6BAA8B,YAAW,qBAAqB;IAoB7D,OAAO,CAAC,QAAQ,CAAC,OAAO;IAnBpC,OAAO,CAAC,OAAO,CAA+B;IAC9C,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,aAAa,CAA4B;IACjD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAInB;IACL,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqB;IAEtD;;;;OAIG;gBAC0B,OAAO,EAAE,oCAAoC;IAO1E;;;;;OAKG;IACG,MAAM,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IA2CtD;;;;;;;OAOG;IACG,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC;IAoErF;;;;;;OAMG;IACG,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAU5D;;;;OAIG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YA6Bd,oBAAoB;YAiCpB,aAAa;IAmC3B,OAAO,CAAC,cAAc;IAqBtB,OAAO,CAAC,qBAAqB;CAO9B"}
@@ -0,0 +1,263 @@
1
+ /** Options for configuring the RabbitMQ microservice transport. */
2
+
3
+ /**
4
+ * RabbitMQ transport for queue-backed request-response and event delivery.
5
+ *
6
+ * The adapter uses dedicated event, message, and response queues so applications can keep
7
+ * a queue-oriented topology while still consuming the generic Fluo transport API.
8
+ */
9
+ export class RabbitMqMicroserviceTransport {
10
+ handler;
11
+ listening = false;
12
+ listenPromise;
13
+ eventQueue;
14
+ messageQueue;
15
+ pending = new Map();
16
+ requestTimeoutMs;
17
+ responseQueue;
18
+ subscribedQueues = new Set();
19
+
20
+ /**
21
+ * Creates a RabbitMQ transport with explicit consumer and publisher collaborators.
22
+ *
23
+ * @param options Queue names and timeout settings for request-response traffic.
24
+ */
25
+ constructor(options) {
26
+ this.options = options;
27
+ this.eventQueue = options.eventQueue ?? 'fluo.microservices.events';
28
+ this.messageQueue = options.messageQueue ?? 'fluo.microservices.messages';
29
+ this.responseQueue = options.responseQueue ?? `fluo.microservices.responses.${crypto.randomUUID()}`;
30
+ this.requestTimeoutMs = options.requestTimeoutMs ?? 3_000;
31
+ }
32
+
33
+ /**
34
+ * Starts consuming the configured event, message, and response queues.
35
+ *
36
+ * @param handler Runtime callback invoked for inbound event and message packets.
37
+ * @returns A promise that resolves once queue consumers are registered.
38
+ */
39
+ async listen(handler) {
40
+ this.handler = handler;
41
+ if (this.listening) {
42
+ return;
43
+ }
44
+ if (this.listenPromise) {
45
+ await this.listenPromise;
46
+ return;
47
+ }
48
+ this.listenPromise = (async () => {
49
+ const queues = new Set([this.eventQueue, this.messageQueue, this.responseQueue]);
50
+ try {
51
+ for (const queue of queues) {
52
+ await this.options.consumer.consume(queue, message => {
53
+ void this.handleInboundMessage(message).catch(() => undefined);
54
+ });
55
+ this.subscribedQueues.add(queue);
56
+ }
57
+ } catch (error) {
58
+ for (const queue of this.subscribedQueues) {
59
+ await this.options.consumer.cancel(queue).catch(() => undefined);
60
+ }
61
+ this.subscribedQueues.clear();
62
+ throw error;
63
+ }
64
+ this.listening = true;
65
+ })();
66
+ try {
67
+ await this.listenPromise;
68
+ } finally {
69
+ this.listenPromise = undefined;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Sends one request-response message through RabbitMQ.
75
+ *
76
+ * @param pattern Pattern identifying the remote message handler.
77
+ * @param payload Serializable request payload.
78
+ * @param signal Optional abort signal used to cancel the request.
79
+ * @returns The remote handler response payload.
80
+ */
81
+ async send(pattern, payload, signal) {
82
+ if (this.listenPromise) {
83
+ await this.listenPromise;
84
+ }
85
+ if (!this.listening) {
86
+ throw new Error('RabbitMqMicroserviceTransport is not listening. Call listen() before send().');
87
+ }
88
+ const requestId = crypto.randomUUID();
89
+ const message = {
90
+ kind: 'message',
91
+ pattern,
92
+ payload,
93
+ replyTo: this.responseQueue,
94
+ requestId
95
+ };
96
+ return await new Promise((resolve, reject) => {
97
+ let abortHandler;
98
+ const timeout = setTimeout(() => {
99
+ cleanup();
100
+ reject(new Error(`RabbitMQ request timed out after ${this.requestTimeoutMs}ms waiting for pattern "${pattern}".`));
101
+ }, this.requestTimeoutMs);
102
+ const cleanup = () => {
103
+ clearTimeout(timeout);
104
+ this.pending.delete(requestId);
105
+ if (abortHandler && signal) {
106
+ signal.removeEventListener('abort', abortHandler);
107
+ }
108
+ };
109
+ this.pending.set(requestId, {
110
+ reject: error => {
111
+ cleanup();
112
+ reject(error);
113
+ },
114
+ resolve: value => {
115
+ cleanup();
116
+ resolve(value);
117
+ },
118
+ timeout
119
+ });
120
+ if (signal) {
121
+ if (signal.aborted) {
122
+ cleanup();
123
+ reject(new Error('RabbitMQ request aborted before publish.'));
124
+ return;
125
+ }
126
+ abortHandler = () => {
127
+ cleanup();
128
+ reject(new Error('RabbitMQ request aborted.'));
129
+ };
130
+ signal.addEventListener('abort', abortHandler, {
131
+ once: true
132
+ });
133
+ }
134
+ void this.options.publisher.publish(this.messageQueue, JSON.stringify(message)).catch(error => {
135
+ cleanup();
136
+ reject(error instanceof Error ? error : new Error('Failed to publish RabbitMQ request.'));
137
+ });
138
+ });
139
+ }
140
+
141
+ /**
142
+ * Emits one fire-and-forget event through RabbitMQ.
143
+ *
144
+ * @param pattern Pattern identifying the remote event handler.
145
+ * @param payload Serializable event payload.
146
+ * @returns A promise that resolves once the event is published.
147
+ */
148
+ async emit(pattern, payload) {
149
+ const message = {
150
+ kind: 'event',
151
+ pattern,
152
+ payload
153
+ };
154
+ await this.options.publisher.publish(this.eventQueue, JSON.stringify(message));
155
+ }
156
+
157
+ /**
158
+ * Cancels queue consumers and rejects any in-flight requests.
159
+ *
160
+ * @returns A promise that resolves once shutdown cleanup completes.
161
+ */
162
+ async close() {
163
+ let closeError;
164
+ if (this.listenPromise) {
165
+ await this.listenPromise;
166
+ }
167
+ try {
168
+ if (this.listening) {
169
+ for (const queue of this.subscribedQueues) {
170
+ try {
171
+ await this.options.consumer.cancel(queue);
172
+ } catch (error) {
173
+ closeError ??= error;
174
+ }
175
+ }
176
+ }
177
+ } finally {
178
+ this.subscribedQueues.clear();
179
+ this.rejectPendingRequests(new Error('RabbitMQ microservice transport closed before response.'));
180
+ this.listening = false;
181
+ this.handler = undefined;
182
+ }
183
+ if (closeError) {
184
+ throw closeError;
185
+ }
186
+ }
187
+ async handleInboundMessage(rawMessage) {
188
+ let message;
189
+ try {
190
+ message = JSON.parse(rawMessage);
191
+ } catch {
192
+ return;
193
+ }
194
+ if (message.kind === 'response') {
195
+ this.handleResponse(message);
196
+ return;
197
+ }
198
+ if (!this.handler) {
199
+ return;
200
+ }
201
+ if (message.kind === 'event') {
202
+ await this.handler({
203
+ kind: 'event',
204
+ pattern: message.pattern,
205
+ payload: message.payload
206
+ });
207
+ return;
208
+ }
209
+ if (message.kind === 'message' && message.requestId) {
210
+ await this.handleRequest(message);
211
+ }
212
+ }
213
+ async handleRequest(message) {
214
+ if (!this.handler || !message.requestId) {
215
+ return;
216
+ }
217
+ const replyQueue = typeof message.replyTo === 'string' && message.replyTo.length > 0 ? message.replyTo : this.responseQueue;
218
+ try {
219
+ const payload = await this.handler({
220
+ kind: 'message',
221
+ pattern: message.pattern,
222
+ payload: message.payload,
223
+ requestId: message.requestId
224
+ });
225
+ await this.options.publisher.publish(replyQueue, JSON.stringify({
226
+ kind: 'response',
227
+ pattern: message.pattern,
228
+ payload,
229
+ requestId: message.requestId
230
+ }));
231
+ } catch (error) {
232
+ const errorMessage = error instanceof Error ? error.message : 'Unhandled microservice error';
233
+ await this.options.publisher.publish(replyQueue, JSON.stringify({
234
+ error: errorMessage,
235
+ kind: 'response',
236
+ pattern: message.pattern,
237
+ requestId: message.requestId
238
+ }));
239
+ }
240
+ }
241
+ handleResponse(message) {
242
+ if (!message.requestId) {
243
+ return;
244
+ }
245
+ const pending = this.pending.get(message.requestId);
246
+ if (!pending) {
247
+ return;
248
+ }
249
+ this.pending.delete(message.requestId);
250
+ if (message.error) {
251
+ pending.reject(new Error(message.error));
252
+ return;
253
+ }
254
+ pending.resolve(message.payload);
255
+ }
256
+ rejectPendingRequests(error) {
257
+ for (const [requestId, entry] of this.pending) {
258
+ clearTimeout(entry.timeout);
259
+ this.pending.delete(requestId);
260
+ entry.reject(error);
261
+ }
262
+ }
263
+ }
@@ -0,0 +1,136 @@
1
+ import type { MicroserviceTransport, MicroserviceTransportLogger, TransportHandler } from '../types.js';
2
+ interface StreamReadGroupResult {
3
+ readonly id: string;
4
+ readonly fields: Readonly<Record<string, string>>;
5
+ }
6
+ /** Optional Redis Streams write controls used for bounded retention. */
7
+ export interface RedisStreamWriteOptions {
8
+ /** Approximate maximum stream length preserved by Redis after this append. */
9
+ readonly maxLenApproximate?: number;
10
+ }
11
+ /** Minimal Redis Streams client contract required by {@link RedisStreamsMicroserviceTransport}. */
12
+ export interface RedisStreamClientLike {
13
+ xadd(stream: string, fields: Record<string, string>, options?: RedisStreamWriteOptions): Promise<string>;
14
+ xreadgroup(group: string, consumer: string, streams: readonly string[], options?: {
15
+ blockMs?: number;
16
+ count?: number;
17
+ }): Promise<readonly StreamReadGroupResult[] | null>;
18
+ xack(stream: string, group: string, id: string): Promise<void>;
19
+ get?(key: string): Promise<string | null>;
20
+ incr?(key: string): Promise<number>;
21
+ decr?(key: string): Promise<number>;
22
+ set?(key: string, value: string): Promise<unknown>;
23
+ xdel?(stream: string, id: string): Promise<void>;
24
+ del?(stream: string): Promise<void>;
25
+ xgroupCreate(stream: string, group: string, startId: string, mkstream: boolean): Promise<void>;
26
+ xgroupDestroy(stream: string, group: string): Promise<void>;
27
+ }
28
+ /** Options for configuring the Redis Streams microservice transport. */
29
+ export interface RedisStreamsMicroserviceTransportOptions {
30
+ readerClient: RedisStreamClientLike;
31
+ writerClient: RedisStreamClientLike;
32
+ /**
33
+ * Approximate maximum request stream length applied at publish time.
34
+ *
35
+ * Disabled by default so pending request entries are never trimmed before `xack`/recovery.
36
+ */
37
+ messageRetentionMaxLen?: number;
38
+ /**
39
+ * Approximate maximum event stream length applied at publish time.
40
+ *
41
+ * Disabled by default so pending event entries are never trimmed before consumer-group recovery.
42
+ */
43
+ eventRetentionMaxLen?: number;
44
+ /** Approximate maximum per-consumer response stream length. Defaults to `1_000`. */
45
+ responseRetentionMaxLen?: number;
46
+ consumerGroup?: string;
47
+ namespace?: string;
48
+ requestTimeoutMs?: number;
49
+ pollBlockMs?: number;
50
+ }
51
+ /**
52
+ * Redis Streams transport for durable request-response messages and event fan-out.
53
+ *
54
+ * The adapter uses consumer groups and a per-consumer response stream so callers can combine
55
+ * at-least-once delivery with request timeouts while preserving Fluo's transport abstraction.
56
+ */
57
+ export declare class RedisStreamsMicroserviceTransport implements MicroserviceTransport {
58
+ private readonly options;
59
+ private closing;
60
+ private readonly consumerId;
61
+ private handler;
62
+ private logger;
63
+ private listening;
64
+ private listenPromise;
65
+ private messageGroupLeaseRegistered;
66
+ private ownsMessageGroup;
67
+ private readonly pending;
68
+ private pollPromises;
69
+ private readonly namespace;
70
+ private readonly consumerGroup;
71
+ private readonly requestTimeoutMs;
72
+ private readonly pollBlockMs;
73
+ private readonly messageRetentionMaxLen;
74
+ private readonly eventRetentionMaxLen;
75
+ private readonly responseRetentionMaxLen;
76
+ /**
77
+ * Creates a Redis Streams transport with dedicated reader and writer clients.
78
+ *
79
+ * @param options Namespace, consumer-group, polling, and timeout settings.
80
+ */
81
+ constructor(options: RedisStreamsMicroserviceTransportOptions);
82
+ setLogger(logger: MicroserviceTransportLogger): void;
83
+ /**
84
+ * Creates consumer groups and starts polling the request, event, and response streams.
85
+ *
86
+ * @param handler Runtime callback invoked for inbound event and message packets.
87
+ * @returns A promise that resolves once all stream consumers are initialized.
88
+ */
89
+ listen(handler: TransportHandler): Promise<void>;
90
+ /**
91
+ * Sends one request-response message through Redis Streams.
92
+ *
93
+ * @param pattern Pattern identifying the remote message handler.
94
+ * @param payload Serializable request payload.
95
+ * @param signal Optional abort signal used to cancel the request.
96
+ * @returns The remote handler response payload.
97
+ */
98
+ send(pattern: string, payload: unknown, signal?: AbortSignal): Promise<unknown>;
99
+ /**
100
+ * Emits one fire-and-forget event through Redis Streams.
101
+ *
102
+ * @param pattern Pattern identifying the remote event handlers.
103
+ * @param payload Serializable event payload.
104
+ * @returns A promise that resolves once the event frame is appended to the stream.
105
+ */
106
+ emit(pattern: string, payload: unknown): Promise<void>;
107
+ /**
108
+ * Stops polling and tears down the owned request, event, and response consumer resources.
109
+ *
110
+ * @returns A promise that resolves once shutdown cleanup finishes.
111
+ */
112
+ close(): Promise<void>;
113
+ private ensureConsumerGroup;
114
+ private registerMessageGroupLease;
115
+ private releaseMessageGroupLease;
116
+ private pollStream;
117
+ private handleStreamEntry;
118
+ private handleInboundRequest;
119
+ private publishFrame;
120
+ private cleanupAcknowledgedEntry;
121
+ private handleInboundEvent;
122
+ private handleInboundResponse;
123
+ private parseFields;
124
+ private logEventHandlerFailure;
125
+ private isBusyGroupError;
126
+ private get messageStream();
127
+ private get eventStream();
128
+ private get responseStream();
129
+ private get messageGroup();
130
+ private get messageGroupOwnerKey();
131
+ private get messageGroupRefCountKey();
132
+ private get eventGroup();
133
+ private get responseGroup();
134
+ }
135
+ export {};
136
+ //# sourceMappingURL=redis-streams-transport.d.ts.map