@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 @@
1
+ {"version":3,"file":"redis-streams-transport.d.ts","sourceRoot":"","sources":["../../src/transports/redis-streams-transport.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,482 @@
1
+ import { logTransportEventHandlerFailure } from './event-handler-logger.js';
2
+
3
+ /** Optional Redis Streams write controls used for bounded retention. */
4
+
5
+ /** Minimal Redis Streams client contract required by {@link RedisStreamsMicroserviceTransport}. */
6
+
7
+ /** Options for configuring the Redis Streams microservice transport. */
8
+
9
+ function delay(ms) {
10
+ return new Promise(resolve => {
11
+ setTimeout(resolve, ms);
12
+ });
13
+ }
14
+
15
+ /**
16
+ * Redis Streams transport for durable request-response messages and event fan-out.
17
+ *
18
+ * The adapter uses consumer groups and a per-consumer response stream so callers can combine
19
+ * at-least-once delivery with request timeouts while preserving Fluo's transport abstraction.
20
+ */
21
+ export class RedisStreamsMicroserviceTransport {
22
+ closing = false;
23
+ consumerId;
24
+ handler;
25
+ logger;
26
+ listening = false;
27
+ listenPromise;
28
+ messageGroupLeaseRegistered = false;
29
+ ownsMessageGroup = false;
30
+ pending = new Map();
31
+ pollPromises = [];
32
+ namespace;
33
+ consumerGroup;
34
+ requestTimeoutMs;
35
+ pollBlockMs;
36
+ messageRetentionMaxLen;
37
+ eventRetentionMaxLen;
38
+ responseRetentionMaxLen;
39
+
40
+ /**
41
+ * Creates a Redis Streams transport with dedicated reader and writer clients.
42
+ *
43
+ * @param options Namespace, consumer-group, polling, and timeout settings.
44
+ */
45
+ constructor(options) {
46
+ this.options = options;
47
+ this.consumerId = crypto.randomUUID();
48
+ this.namespace = options.namespace ?? 'fluo:streams';
49
+ this.consumerGroup = options.consumerGroup ?? 'fluo-handlers';
50
+ this.requestTimeoutMs = options.requestTimeoutMs ?? 3_000;
51
+ this.pollBlockMs = options.pollBlockMs ?? 500;
52
+ this.messageRetentionMaxLen = options.messageRetentionMaxLen;
53
+ this.eventRetentionMaxLen = options.eventRetentionMaxLen;
54
+ this.responseRetentionMaxLen = options.responseRetentionMaxLen ?? 1_000;
55
+ }
56
+ setLogger(logger) {
57
+ this.logger = logger;
58
+ }
59
+
60
+ /**
61
+ * Creates consumer groups and starts polling the request, event, and response streams.
62
+ *
63
+ * @param handler Runtime callback invoked for inbound event and message packets.
64
+ * @returns A promise that resolves once all stream consumers are initialized.
65
+ */
66
+ async listen(handler) {
67
+ this.closing = false;
68
+ this.handler = handler;
69
+ if (this.listening) {
70
+ return;
71
+ }
72
+ if (this.listenPromise) {
73
+ await this.listenPromise;
74
+ return;
75
+ }
76
+ this.listenPromise = (async () => {
77
+ this.ownsMessageGroup = await this.ensureConsumerGroup(this.messageStream, this.messageGroup);
78
+ await this.registerMessageGroupLease(this.ownsMessageGroup);
79
+ await this.options.readerClient.xgroupCreate(this.eventStream, this.eventGroup, '$', true);
80
+ await this.options.readerClient.xgroupCreate(this.responseStream, this.responseGroup, '$', true);
81
+ this.pollPromises = [this.pollStream(this.messageStream, this.messageGroup), this.pollStream(this.eventStream, this.eventGroup), this.pollStream(this.responseStream, this.responseGroup)];
82
+ this.listening = true;
83
+ })();
84
+ try {
85
+ await this.listenPromise;
86
+ } finally {
87
+ this.listenPromise = undefined;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Sends one request-response message through Redis Streams.
93
+ *
94
+ * @param pattern Pattern identifying the remote message handler.
95
+ * @param payload Serializable request payload.
96
+ * @param signal Optional abort signal used to cancel the request.
97
+ * @returns The remote handler response payload.
98
+ */
99
+ async send(pattern, payload, signal) {
100
+ if (this.closing) {
101
+ throw new Error('RedisStreamsMicroserviceTransport is closing. Wait for close() to complete before send().');
102
+ }
103
+ if (!this.listening) {
104
+ throw new Error('RedisStreamsMicroserviceTransport is not listening. Call listen() before send().');
105
+ }
106
+ const requestId = crypto.randomUUID();
107
+ return await new Promise((resolve, reject) => {
108
+ let abortHandler;
109
+ let timeout;
110
+ const cleanup = () => {
111
+ if (timeout) {
112
+ clearTimeout(timeout);
113
+ }
114
+ if (signal && abortHandler) {
115
+ signal.removeEventListener('abort', abortHandler);
116
+ }
117
+ this.pending.delete(requestId);
118
+ };
119
+ const entry = {
120
+ resolve: value => {
121
+ cleanup();
122
+ resolve(value);
123
+ },
124
+ reject: error => {
125
+ cleanup();
126
+ reject(error);
127
+ }
128
+ };
129
+ this.pending.set(requestId, entry);
130
+ timeout = setTimeout(() => {
131
+ entry.reject(new Error(`Redis Streams request timed out after ${String(this.requestTimeoutMs)}ms waiting for pattern "${pattern}".`));
132
+ }, this.requestTimeoutMs);
133
+ if (signal) {
134
+ if (signal.aborted) {
135
+ entry.reject(new Error('Redis Streams request aborted before publish.'));
136
+ return;
137
+ }
138
+ abortHandler = () => {
139
+ entry.reject(new Error('Redis Streams request aborted.'));
140
+ };
141
+ signal.addEventListener('abort', abortHandler, {
142
+ once: true
143
+ });
144
+ }
145
+ void Promise.resolve().then(async () => {
146
+ if (this.closing) {
147
+ entry.reject(new Error('Redis Streams microservice transport closed before request dispatch.'));
148
+ return;
149
+ }
150
+ const frame = {
151
+ kind: 'message',
152
+ pattern,
153
+ payload,
154
+ replyStream: this.responseStream,
155
+ requestId
156
+ };
157
+ await this.publishFrame(this.messageStream, {
158
+ kind: frame.kind,
159
+ pattern: frame.pattern,
160
+ payload: JSON.stringify(frame.payload),
161
+ replyStream: frame.replyStream,
162
+ requestId: frame.requestId
163
+ }, this.messageRetentionMaxLen);
164
+ }).catch(error => {
165
+ entry.reject(error instanceof Error ? error : new Error('Failed to publish Redis Streams request.'));
166
+ });
167
+ });
168
+ }
169
+
170
+ /**
171
+ * Emits one fire-and-forget event through Redis Streams.
172
+ *
173
+ * @param pattern Pattern identifying the remote event handlers.
174
+ * @param payload Serializable event payload.
175
+ * @returns A promise that resolves once the event frame is appended to the stream.
176
+ */
177
+ async emit(pattern, payload) {
178
+ const frame = {
179
+ kind: 'event',
180
+ pattern,
181
+ payload
182
+ };
183
+ await this.publishFrame(this.eventStream, {
184
+ kind: frame.kind,
185
+ pattern: frame.pattern,
186
+ payload: JSON.stringify(frame.payload)
187
+ }, this.eventRetentionMaxLen);
188
+ }
189
+
190
+ /**
191
+ * Stops polling and tears down the owned request, event, and response consumer resources.
192
+ *
193
+ * @returns A promise that resolves once shutdown cleanup finishes.
194
+ */
195
+ async close() {
196
+ this.closing = true;
197
+ let closeError;
198
+ if (this.listenPromise) {
199
+ await this.listenPromise;
200
+ }
201
+ try {
202
+ const settled = await Promise.allSettled(this.pollPromises);
203
+ const shouldDestroyMessageGroup = await this.releaseMessageGroupLease();
204
+ for (const result of settled) {
205
+ if (result.status === 'rejected') {
206
+ closeError ??= result.reason;
207
+ }
208
+ }
209
+ if (shouldDestroyMessageGroup) {
210
+ try {
211
+ await this.options.readerClient.xgroupDestroy(this.messageStream, this.messageGroup);
212
+ } catch (error) {
213
+ closeError ??= error;
214
+ }
215
+ }
216
+ try {
217
+ await this.options.readerClient.xgroupDestroy(this.eventStream, this.eventGroup);
218
+ } catch (error) {
219
+ closeError ??= error;
220
+ }
221
+ try {
222
+ await this.options.readerClient.xgroupDestroy(this.responseStream, this.responseGroup);
223
+ } catch (error) {
224
+ closeError ??= error;
225
+ }
226
+ try {
227
+ await this.options.readerClient.del?.(this.responseStream);
228
+ } catch (error) {
229
+ closeError ??= error;
230
+ }
231
+ } finally {
232
+ this.listening = false;
233
+ this.handler = undefined;
234
+ this.messageGroupLeaseRegistered = false;
235
+ this.ownsMessageGroup = false;
236
+ this.pollPromises = [];
237
+ for (const pending of [...this.pending.values()]) {
238
+ pending.reject(new Error('Redis Streams microservice transport closed before response.'));
239
+ }
240
+ }
241
+ if (closeError) {
242
+ throw closeError;
243
+ }
244
+ }
245
+ async ensureConsumerGroup(stream, group) {
246
+ try {
247
+ await this.options.readerClient.xgroupCreate(stream, group, '$', true);
248
+ return true;
249
+ } catch (error) {
250
+ if (this.isBusyGroupError(error)) {
251
+ return false;
252
+ }
253
+ throw error;
254
+ }
255
+ }
256
+ async registerMessageGroupLease(createdGroup) {
257
+ const {
258
+ readerClient
259
+ } = this.options;
260
+ if (!readerClient.incr || !readerClient.decr || !readerClient.get || !readerClient.set || !readerClient.del) {
261
+ // Without the optional KV helpers we cannot coordinate shared-group ownership safely
262
+ // across listeners, so the fallback path keeps the request consumer group alive.
263
+ this.messageGroupLeaseRegistered = false;
264
+ return;
265
+ }
266
+ if (!createdGroup) {
267
+ // Joining an existing request group cannot prove that every active listener participates
268
+ // in the same lease protocol, so shared groups must be retained conservatively.
269
+ this.messageGroupLeaseRegistered = false;
270
+ return;
271
+ }
272
+ if (createdGroup) {
273
+ await readerClient.set(this.messageGroupOwnerKey, this.consumerId);
274
+ }
275
+ await readerClient.incr(this.messageGroupRefCountKey);
276
+ this.messageGroupLeaseRegistered = true;
277
+ }
278
+ async releaseMessageGroupLease() {
279
+ const {
280
+ readerClient
281
+ } = this.options;
282
+ if (!this.messageGroupLeaseRegistered || !readerClient.decr || !readerClient.get || !readerClient.del) {
283
+ // Fallback/no-lease clients intentionally retain the shared request group because
284
+ // destroying it here could break another active listener that joined via BUSYGROUP.
285
+ return false;
286
+ }
287
+ const remainingListeners = await readerClient.decr(this.messageGroupRefCountKey);
288
+ if (remainingListeners > 0) {
289
+ return false;
290
+ }
291
+ await readerClient.del(this.messageGroupRefCountKey);
292
+ const owner = await readerClient.get(this.messageGroupOwnerKey);
293
+ await readerClient.del(this.messageGroupOwnerKey);
294
+ if (!owner) {
295
+ return false;
296
+ }
297
+
298
+ // Even the original creator cannot prove that no fallback listener is still attached to
299
+ // the shared request group, so the conservative mixed-fleet policy retains the group.
300
+ return false;
301
+ }
302
+ async pollStream(stream, group) {
303
+ while (!this.closing) {
304
+ try {
305
+ const entries = await this.options.readerClient.xreadgroup(group, this.consumerId, [stream], {
306
+ blockMs: this.pollBlockMs,
307
+ count: 10
308
+ });
309
+ if (!entries || entries.length === 0) {
310
+ await delay(this.pollBlockMs);
311
+ continue;
312
+ }
313
+ for (const entry of entries) {
314
+ const parsed = this.parseFields(entry.fields);
315
+ if (!parsed) {
316
+ await this.options.readerClient.xack(stream, group, entry.id);
317
+ continue;
318
+ }
319
+ const shouldAcknowledge = await this.handleStreamEntry(stream, parsed);
320
+ if (shouldAcknowledge) {
321
+ await this.options.readerClient.xack(stream, group, entry.id);
322
+ await this.cleanupAcknowledgedEntry(stream, entry.id);
323
+ }
324
+ }
325
+ } catch {
326
+ if (!this.closing) {
327
+ await delay(this.pollBlockMs);
328
+ }
329
+ }
330
+ }
331
+ }
332
+ async handleStreamEntry(stream, message) {
333
+ if (stream === this.messageStream) {
334
+ await this.handleInboundRequest(message);
335
+ return true;
336
+ }
337
+ if (stream === this.eventStream) {
338
+ return await this.handleInboundEvent(message);
339
+ }
340
+ this.handleInboundResponse(message);
341
+ return true;
342
+ }
343
+ async handleInboundRequest(message) {
344
+ if (!this.handler) {
345
+ return;
346
+ }
347
+ if (message.kind !== 'message' || !message.requestId) {
348
+ return;
349
+ }
350
+ const requestId = message.requestId;
351
+ const replyStream = message.replyStream && message.replyStream.length > 0 ? message.replyStream : this.responseStream;
352
+ if (!this.handler) {
353
+ return;
354
+ }
355
+ try {
356
+ const payload = await this.handler({
357
+ kind: 'message',
358
+ pattern: message.pattern,
359
+ payload: message.payload,
360
+ requestId: message.requestId
361
+ });
362
+ await this.publishFrame(replyStream, {
363
+ kind: 'response',
364
+ pattern: message.pattern,
365
+ payload: JSON.stringify(payload),
366
+ requestId
367
+ }, this.responseRetentionMaxLen);
368
+ } catch (error) {
369
+ const errorMessage = error instanceof Error ? error.message : 'Unhandled microservice error';
370
+ await this.publishFrame(replyStream, {
371
+ error: errorMessage,
372
+ kind: 'response',
373
+ pattern: message.pattern,
374
+ requestId
375
+ }, this.responseRetentionMaxLen);
376
+ }
377
+ }
378
+ async publishFrame(stream, fields, maxLenApproximate) {
379
+ if (typeof maxLenApproximate === 'number' && maxLenApproximate > 0) {
380
+ await this.options.writerClient.xadd(stream, fields, {
381
+ maxLenApproximate
382
+ });
383
+ return;
384
+ }
385
+ await this.options.writerClient.xadd(stream, fields);
386
+ }
387
+ async cleanupAcknowledgedEntry(stream, id) {
388
+ if (stream !== this.messageStream && stream !== this.responseStream) {
389
+ return;
390
+ }
391
+ await this.options.readerClient.xdel?.(stream, id);
392
+ }
393
+ async handleInboundEvent(message) {
394
+ if (!this.handler) {
395
+ return true;
396
+ }
397
+ if (message.kind !== 'event') {
398
+ return true;
399
+ }
400
+ try {
401
+ await this.handler({
402
+ kind: 'event',
403
+ pattern: message.pattern,
404
+ payload: message.payload
405
+ });
406
+ return true;
407
+ } catch (error) {
408
+ this.logEventHandlerFailure(error);
409
+ return false;
410
+ }
411
+ }
412
+ handleInboundResponse(message) {
413
+ if (message.kind !== 'response' || !message.requestId) {
414
+ return;
415
+ }
416
+ const pending = this.pending.get(message.requestId);
417
+ if (!pending) {
418
+ return;
419
+ }
420
+ if (message.error) {
421
+ pending.reject(new Error(message.error));
422
+ return;
423
+ }
424
+ pending.resolve(message.payload);
425
+ }
426
+ parseFields(fields) {
427
+ const kind = fields.kind;
428
+ const pattern = fields.pattern;
429
+ if (kind !== 'event' && kind !== 'message' && kind !== 'response') {
430
+ return undefined;
431
+ }
432
+ if (!pattern) {
433
+ return undefined;
434
+ }
435
+ let payload;
436
+ if (fields.payload !== undefined) {
437
+ try {
438
+ payload = JSON.parse(fields.payload);
439
+ } catch {
440
+ return undefined;
441
+ }
442
+ }
443
+ return {
444
+ error: fields.error,
445
+ kind,
446
+ pattern,
447
+ payload,
448
+ replyStream: fields.replyStream,
449
+ requestId: fields.requestId
450
+ };
451
+ }
452
+ logEventHandlerFailure(error) {
453
+ logTransportEventHandlerFailure(this.logger, 'RedisStreamsMicroserviceTransport', error);
454
+ }
455
+ isBusyGroupError(error) {
456
+ return error instanceof Error && error.message.includes('BUSYGROUP');
457
+ }
458
+ get messageStream() {
459
+ return `${this.namespace}:messages`;
460
+ }
461
+ get eventStream() {
462
+ return `${this.namespace}:events`;
463
+ }
464
+ get responseStream() {
465
+ return `${this.namespace}:responses:${this.consumerId}`;
466
+ }
467
+ get messageGroup() {
468
+ return this.consumerGroup;
469
+ }
470
+ get messageGroupOwnerKey() {
471
+ return `${this.namespace}:groups:${this.consumerGroup}:owner`;
472
+ }
473
+ get messageGroupRefCountKey() {
474
+ return `${this.namespace}:groups:${this.consumerGroup}:listeners`;
475
+ }
476
+ get eventGroup() {
477
+ return `${this.consumerGroup}:events:${this.consumerId}`;
478
+ }
479
+ get responseGroup() {
480
+ return `${this.consumerGroup}:responses:${this.consumerId}`;
481
+ }
482
+ }
@@ -0,0 +1,73 @@
1
+ import type { MicroserviceTransport, MicroserviceTransportLogger, TransportHandler } from '../types.js';
2
+ interface RedisLike {
3
+ off?(event: 'message', listener: (channel: string, message: string) => void): unknown;
4
+ on(event: 'message', listener: (channel: string, message: string) => void): unknown;
5
+ publish(channel: string, message: string): Promise<unknown>;
6
+ subscribe(...channels: string[]): Promise<unknown>;
7
+ unsubscribe(...channels: string[]): Promise<unknown>;
8
+ }
9
+ /** Options for configuring the Redis Pub/Sub microservice transport. */
10
+ export interface RedisPubSubMicroserviceTransportOptions {
11
+ namespace?: string;
12
+ publishClient: RedisLike;
13
+ requestTimeoutMs?: number;
14
+ subscribeClient: RedisLike;
15
+ }
16
+ /**
17
+ * Redis Pub/Sub transport for fire-and-forget microservice events.
18
+ *
19
+ * This adapter intentionally supports `emit()` only. Request-response flows must use
20
+ * a transport with durable reply semantics such as TCP, Kafka, or Redis Streams.
21
+ */
22
+ export declare class RedisPubSubMicroserviceTransport implements MicroserviceTransport {
23
+ private readonly options;
24
+ private handler;
25
+ private logger;
26
+ private listening;
27
+ private listenPromise;
28
+ private readonly messageListener;
29
+ private readonly namespace;
30
+ private logEventHandlerFailure;
31
+ /**
32
+ * Creates a Redis Pub/Sub transport using dedicated publish and subscribe clients.
33
+ *
34
+ * @param options Namespace and Redis client settings for the transport.
35
+ */
36
+ constructor(options: RedisPubSubMicroserviceTransportOptions);
37
+ setLogger(logger: MicroserviceTransportLogger): void;
38
+ /**
39
+ * Subscribes to the namespaced event channel and registers the runtime handler.
40
+ *
41
+ * @param handler Runtime callback invoked for inbound event packets.
42
+ * @returns A promise that resolves once the Redis subscription is active.
43
+ */
44
+ listen(handler: TransportHandler): Promise<void>;
45
+ /**
46
+ * Publishes one fire-and-forget event through Redis Pub/Sub.
47
+ *
48
+ * @param pattern Pattern identifying the remote event handler.
49
+ * @param payload Serializable payload to publish.
50
+ * @returns A promise that resolves once Redis accepts the publication.
51
+ */
52
+ emit(pattern: string, payload: unknown): Promise<void>;
53
+ /**
54
+ * Rejects request-response usage for the Pub/Sub transport.
55
+ *
56
+ * @param pattern Unused request pattern.
57
+ * @param payload Unused request payload.
58
+ * @param signal Unused abort signal.
59
+ * @returns Never resolves successfully.
60
+ * @throws {Error} Always, because Pub/Sub has no reply channel contract.
61
+ */
62
+ send(pattern: string, payload: unknown, signal?: AbortSignal): Promise<unknown>;
63
+ /**
64
+ * Unsubscribes from the event channel and detaches the Redis message listener.
65
+ *
66
+ * @returns A promise that resolves once shutdown cleanup completes.
67
+ */
68
+ close(): Promise<void>;
69
+ private handleIncoming;
70
+ private get eventChannel();
71
+ }
72
+ export {};
73
+ //# sourceMappingURL=redis-transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis-transport.d.ts","sourceRoot":"","sources":["../../src/transports/redis-transport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,2BAA2B,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AASxG,UAAU,SAAS;IACjB,GAAG,CAAC,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC;IACtF,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC;IACpF,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5D,SAAS,CAAC,GAAG,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnD,WAAW,CAAC,GAAG,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtD;AAED,wEAAwE;AACxE,MAAM,WAAW,uCAAuC;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,SAAS,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,SAAS,CAAC;CAC5B;AAED;;;;;GAKG;AACH,qBAAa,gCAAiC,YAAW,qBAAqB;IAmBhE,OAAO,CAAC,QAAQ,CAAC,OAAO;IAlBpC,OAAO,CAAC,OAAO,CAA+B;IAC9C,OAAO,CAAC,MAAM,CAA0C;IACxD,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,aAAa,CAA4B;IACjD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAE9B;IACF,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IAEnC,OAAO,CAAC,sBAAsB;IAI9B;;;;OAIG;gBAC0B,OAAO,EAAE,uCAAuC;IAI7E,SAAS,CAAC,MAAM,EAAE,2BAA2B,GAAG,IAAI;IAIpD;;;;;OAKG;IACG,MAAM,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IA+BtD;;;;;;OAMG;IACG,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAU5D;;;;;;;;OAQG;IACG,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC;IAOrF;;;;OAIG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAwBd,cAAc;IA4B5B,OAAO,KAAK,YAAY,GAEvB;CACF"}