redis-consumer 1.1.10 → 1.2.0

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.
@@ -1,14 +1,29 @@
1
- import { RedisClientType, RedisScripts } from 'redis';
2
- import { StreamMessageReply as OriginalStreamMessageReply } from '@redis/client/dist/lib/commands/generic-transformers';
3
- import { StreamMessageData, StreamMessageId } from '.';
4
- import { RedisClient } from './client';
5
- export type StreamMessageReply<T = StreamMessageData> = Omit<OriginalStreamMessageReply, 'message'> & {
1
+ import type { RedisArgument, RedisClientType, RedisScripts } from 'redis';
2
+ import type { RedisClient } from './client.js';
3
+ export type StreamMessageId = string;
4
+ export type StreamMessageData = Record<string, RedisArgument>;
5
+ export interface StreamMessageReply<T = StreamMessageData> {
6
+ id: StreamMessageId;
6
7
  message: T;
7
- };
8
+ millisElapsedFromDelivery?: number;
9
+ deliveriesCounter?: number;
10
+ }
11
+ /** Called once per message, with that single message. */
8
12
  export type StreamProcessingFunction<T> = (data: StreamMessageReply<T>, stream: string) => void;
13
+ /** Called once per read, with the whole array of messages of that read. */
14
+ export type StreamBatchProcessingFunction<T> = (messages: StreamMessageReply<T>[], stream: string) => void;
15
+ /**
16
+ * How the consumer holds an executable.
17
+ *
18
+ * Which of the two shapes above it is actually called with depends on the
19
+ * `batchMessages` option, so the payload is intentionally untyped here. Annotate
20
+ * your own function as `StreamProcessingFunction<T>` or
21
+ * `StreamBatchProcessingFunction<T>` to have its parameter checked.
22
+ */
23
+ export type StreamExecutable = (payload: any, stream: string) => void;
9
24
  export interface StreamToListen {
10
25
  name: string;
11
- executable: StreamProcessingFunction<any>;
26
+ executable: StreamExecutable;
12
27
  id?: string;
13
28
  }
14
29
  export type StreamsToListen = StreamToListen[];
@@ -17,10 +32,15 @@ export interface ConsumerOptions {
17
32
  BLOCK?: number;
18
33
  retries?: number;
19
34
  retryTime?: string[];
35
+ /**
36
+ * When `true`, every read hands the whole batch of messages to the
37
+ * executable as a single array instead of one call per message.
38
+ */
39
+ batchMessages?: boolean;
20
40
  }
21
41
  export interface ProcessErrorData {
22
42
  stream: string;
23
- message: OriginalStreamMessageReply;
43
+ message: StreamMessageReply;
24
44
  retries: number;
25
45
  }
26
46
  export declare class RedisConsumer<S extends RedisScripts = RedisScripts> {
@@ -32,6 +52,14 @@ export declare class RedisConsumer<S extends RedisScripts = RedisScripts> {
32
52
  private BLOCK;
33
53
  private COUNT;
34
54
  private RETRIES;
55
+ private batchMessages;
56
+ private ready;
57
+ private listenPromise;
58
+ private stopped;
59
+ /** Tail of the serialised acknowledgement flush chain. */
60
+ private ackFlush;
61
+ /** Aborts the read loop's error back-off so `close()` does not have to wait it out. */
62
+ private abort;
35
63
  constructor(client: RedisClient<S>, options?: ConsumerOptions);
36
64
  set block(block: number);
37
65
  set count(count: number);
@@ -40,13 +68,37 @@ export declare class RedisConsumer<S extends RedisScripts = RedisScripts> {
40
68
  block: number;
41
69
  count: number;
42
70
  };
43
- listen(streams: StreamToListen | StreamsToListen): Promise<void>;
71
+ listen(streams: StreamToListen | StreamsToListen, flag?: boolean): Promise<void>;
72
+ /**
73
+ * Stops reading and closes the consumer's connection, so the process can exit.
74
+ */
75
+ close(): Promise<void>;
44
76
  addAckMessage(stream: string, id: StreamMessageId): void;
77
+ /**
78
+ * Sends the queued acknowledgements (`XACK` + `XDEL`) to Redis.
79
+ *
80
+ * Call this after {@link addAckMessage} to make the acknowledgement leave the
81
+ * pending list right away instead of waiting for the read loop's next flush.
82
+ *
83
+ * Flushes are serialised, so the read loop and the retry processor can both
84
+ * call this without interleaving their `XACK`/`XDEL` batches. Ids are only
85
+ * dropped once Redis accepted them, so a failed flush is retried by the next
86
+ * one.
87
+ */
88
+ flushAcks(): Promise<void>;
45
89
  private listenForStreams;
46
90
  private hasStreamState;
47
91
  private getStreamState;
48
92
  private initStreamState;
49
93
  private readStreams;
94
+ /**
95
+ * Runs the executable for a single read and acknowledges its messages only
96
+ * when processing succeeded, so a failed message stays in the pending list.
97
+ *
98
+ * In batch mode `payload` is the whole array. A failed batch call cannot tell
99
+ * which message failed, so every message of the read is retried on its own.
100
+ */
101
+ private handle;
50
102
  private processStreamMessages;
51
103
  private acknowlegdeMessages;
52
104
  }
package/dist/consumer.js CHANGED
@@ -1,164 +1,274 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RedisConsumer = void 0;
4
- const retry_processor_1 = require("./retry-processor");
5
- let errs = "error";
6
- class RedisConsumer {
7
- client;
8
- originalClient;
9
- state;
10
- retryProcessor;
11
- successfullMessages = new Map();
12
- BLOCK;
13
- COUNT;
14
- RETRIES;
15
- constructor(client, options = {}) {
16
- this.originalClient = client;
17
- this.client = client.duplicate();
18
- this.state = {};
19
- this.COUNT = options.COUNT ?? 1;
20
- this.BLOCK = options.BLOCK ?? 0;
21
- this.RETRIES = options.retries ?? 3;
22
- this.retryProcessor = new retry_processor_1.RetryProcessor(this, {
23
- retryTime: options.retryTime,
24
- maxRetry: this.RETRIES,
25
- });
26
- this.client.connect();
27
- }
28
- set block(block) {
29
- this.BLOCK = block;
30
- }
31
- set count(count) {
32
- this.COUNT = count;
33
- }
34
- set retries(retries) {
35
- this.RETRIES = retries;
36
- }
37
- get settings() {
38
- return { block: this.BLOCK, count: this.COUNT };
39
- }
40
- async listen(streams, flag) {
41
- if (!Array.isArray(streams)) {
42
- streams = [streams];
43
- }
44
- for (const stream of streams) {
45
- const groupExists = await this.originalClient.groupExists(stream.name);
46
- if (!groupExists) {
47
- await this.originalClient.createGroup(stream.name);
48
- }
49
- if (this.hasStreamState(stream.name)) continue;
50
- this.initStreamState(stream);
51
- }
52
- this.listenForStreams(flag);
53
- }
54
- addAckMessage(stream, id) {
55
- if (this.successfullMessages.has(stream)) {
56
- const ackMessages = this.successfullMessages.get(stream);
57
- ackMessages.push(id);
58
- } else {
59
- this.successfullMessages.set(stream, [id]);
60
- }
61
- }
62
- async listenForStreams(flag) {
63
- const state = this.state;
64
- const streamsToListen = [];
65
- for (const stream in state) {
66
- streamsToListen.push({ key: stream, id: state[stream].nextId });
67
- }
68
- const streamsMessages = await this.readStreams(streamsToListen);
69
- if (!streamsMessages) {
70
- await this.acknowlegdeMessages();
71
- this.listenForStreams(flag);
72
- return;
73
- }
74
- for (const streamMessages of streamsMessages) {
75
- await this.processStreamMessages(streamMessages, flag);
76
- }
77
- this.listenForStreams(flag);
78
- }
79
- hasStreamState(name) {
80
- return !!this.getStreamState(name);
81
- }
82
- getStreamState(name) {
83
- return this.state[name];
84
- }
85
- initStreamState(stream) {
86
- const name = stream.name;
87
- const executable = stream.executable;
88
- let lastSuccessId;
89
- if (stream.id) {
90
- lastSuccessId = stream.id;
91
- } else {
92
- lastSuccessId = "0-0";
93
- }
94
- let nextId = lastSuccessId;
95
- this.state[name] = { nextId, lastSuccessId, executable, recovering: true };
96
- }
97
- async readStreams(streamsToListen) {
98
- const messages = await this.client.xReadGroup(
99
- this.originalClient.groupName,
100
- this.originalClient.clientName,
101
- streamsToListen,
102
- { BLOCK: this.BLOCK, COUNT: this.COUNT },
103
- );
104
- return messages;
105
- }
106
-
107
- async handle(fnc, message, stream) {
108
- try {
109
- await fnc(message, stream);
110
- } catch (err) {
111
- if (errs == "error") errs = err;
112
- this.client.emit("process-error", err, { stream, message, retries: 0 });
113
- if (this.RETRIES === 0) return;
114
- if (err instanceof Error) {
115
- this.retryProcessor.add(err, stream, message, fnc);
116
- } else {
117
- const newErr = new Error(String(err));
118
- this.retryProcessor.add(newErr, stream, message, fnc);
119
- }
120
- } finally {
121
- if (errs && errs != "error") (console.error(errs), (errs = ""));
122
- }
123
- }
124
- async processStreamMessages(streamMessages, flag) {
125
- const stream = streamMessages.name;
126
- const state = this.getStreamState(stream);
127
- if (!state)
128
- throw new Error("No state was found for stream processing of " + stream);
129
- const fnc = state.executable;
130
- const messages = streamMessages.messages;
131
- if (flag) await this.handle(fnc, messages, stream);
132
- for (const message of messages) {
133
- if (!flag) await this.handle(fnc, message, stream);
134
- this.addAckMessage(stream, message.id);
135
- }
136
- await this.acknowlegdeMessages();
137
- const recovering = state.recovering;
138
- if (recovering && messages.length === 0) {
139
- state.nextId = ">";
140
- state.recovering = false;
141
- } else if (recovering) {
142
- const lastMessage = messages.slice(-1);
143
- const lastId = lastMessage[0].id;
144
- state.nextId = lastId;
145
- }
146
- return true;
147
- }
148
- async acknowlegdeMessages() {
149
- for (const item of this.successfullMessages) {
150
- const stream = item[0];
151
- const ackMessages = item[1];
152
- const id = await this.originalClient.xAck(
153
- stream,
154
- this.originalClient.groupName,
155
- ackMessages,
156
- );
157
- if (id) {
158
- await this.originalClient.xDel(stream, ackMessages);
159
- this.successfullMessages.delete(stream);
160
- }
161
- }
162
- }
1
+ import { RetryProcessor } from './retry-processor.js';
2
+ import { timeout } from './helpers.js';
3
+ /** Delay before retrying a read that failed, to avoid a hot error loop. */
4
+ const LISTEN_ERROR_RETRY_DELAY = 1000;
5
+ /**
6
+ * Wraps a batch executable so that a single retried message is delivered to it
7
+ * as a one-item batch, keeping retry bookkeeping per message.
8
+ */
9
+ function singleMessage(fnc) {
10
+ return (message, stream) => fnc([message], stream);
11
+ }
12
+ export class RedisConsumer {
13
+ client;
14
+ originalClient;
15
+ state;
16
+ retryProcessor;
17
+ successfullMessages = new Map();
18
+ BLOCK;
19
+ COUNT;
20
+ RETRIES;
21
+ batchMessages;
22
+ ready;
23
+ listenPromise = null;
24
+ stopped = false;
25
+ /** Tail of the serialised acknowledgement flush chain. */
26
+ ackFlush = Promise.resolve();
27
+ /** Aborts the read loop's error back-off so `close()` does not have to wait it out. */
28
+ abort = new AbortController();
29
+ constructor(client, options = {}) {
30
+ this.originalClient = client;
31
+ this.client = client.duplicate();
32
+ this.state = {};
33
+ this.COUNT = options.COUNT ?? 5000;
34
+ this.BLOCK = options.BLOCK ?? 0;
35
+ this.RETRIES = options.retries ?? 3;
36
+ this.batchMessages = options.batchMessages ?? false;
37
+ this.retryProcessor = new RetryProcessor(this, {
38
+ retryTime: options.retryTime,
39
+ maxRetry: this.RETRIES,
40
+ });
41
+ // node-redis rejects commands issued before the connection is open, so keep
42
+ // the promise around and let the read loop await it before the first read.
43
+ this.ready = this.client.connect();
44
+ // Also handled here, so a consumer that is never listened on or closed
45
+ // cannot take the process down with an unhandled rejection.
46
+ this.ready.catch(() => undefined);
47
+ }
48
+ set block(block) {
49
+ this.BLOCK = block;
50
+ }
51
+ set count(count) {
52
+ this.COUNT = count;
53
+ }
54
+ set retries(retries) {
55
+ this.RETRIES = retries;
56
+ this.retryProcessor.maxRetry = retries;
57
+ }
58
+ get settings() {
59
+ return { block: this.BLOCK, count: this.COUNT };
60
+ }
61
+ async listen(streams, flag) {
62
+ // `flag` is the legacy positional form of `batchMessages`.
63
+ const batch = flag ?? this.batchMessages;
64
+ const streamList = Array.isArray(streams) ? streams : [streams];
65
+ for (const stream of streamList) {
66
+ // Checked before any Redis call, so a repeated listen() stays cheap.
67
+ if (this.hasStreamState(stream.name))
68
+ continue;
69
+ await this.originalClient.createGroup(stream.name);
70
+ this.initStreamState(stream);
71
+ }
72
+ // Nothing to read from: don't start a loop that would only fail.
73
+ if (Object.keys(this.state).length === 0)
74
+ return;
75
+ // A second loop would consume every stream a second time.
76
+ if (this.listenPromise)
77
+ return;
78
+ this.listenPromise = this.listenForStreams(batch);
79
+ }
80
+ /**
81
+ * Stops reading and closes the consumer's connection, so the process can exit.
82
+ */
83
+ async close() {
84
+ this.stopped = true;
85
+ // Pending retries sleep inside a timer; without this they would keep the
86
+ // event loop alive for up to the longest `retryTime`.
87
+ this.retryProcessor.stop();
88
+ this.abort.abort();
89
+ try {
90
+ await this.ready;
91
+ }
92
+ catch {
93
+ // The client never connected, so there is nothing to close.
94
+ return;
95
+ }
96
+ // Best effort: get acknowledgements that are still queued to Redis before
97
+ // the connections go away, so a message processed just before close() is not
98
+ // left in the pending list.
99
+ await this.flushAcks().catch(() => undefined);
100
+ // `destroy()` throws when the socket is already closed, which is the state
101
+ // we are aiming for anyway.
102
+ if (this.client.isOpen)
103
+ this.client.destroy();
104
+ await this.listenPromise;
105
+ }
106
+ addAckMessage(stream, id) {
107
+ const ackMessages = this.successfullMessages.get(stream);
108
+ if (ackMessages) {
109
+ ackMessages.push(id);
110
+ }
111
+ else {
112
+ this.successfullMessages.set(stream, [id]);
113
+ }
114
+ }
115
+ /**
116
+ * Sends the queued acknowledgements (`XACK` + `XDEL`) to Redis.
117
+ *
118
+ * Call this after {@link addAckMessage} to make the acknowledgement leave the
119
+ * pending list right away instead of waiting for the read loop's next flush.
120
+ *
121
+ * Flushes are serialised, so the read loop and the retry processor can both
122
+ * call this without interleaving their `XACK`/`XDEL` batches. Ids are only
123
+ * dropped once Redis accepted them, so a failed flush is retried by the next
124
+ * one.
125
+ */
126
+ flushAcks() {
127
+ const run = this.ackFlush.then(() => this.acknowlegdeMessages(),
128
+ // A previous flush failed, which must not block this one.
129
+ () => this.acknowlegdeMessages());
130
+ // Keep the internal chain usable even when this flush rejects.
131
+ this.ackFlush = run.catch(() => undefined);
132
+ return run;
133
+ }
134
+ async listenForStreams(batch) {
135
+ try {
136
+ await this.ready;
137
+ }
138
+ catch (err) {
139
+ this.client.emit('listen-error', err);
140
+ return;
141
+ }
142
+ while (!this.stopped) {
143
+ try {
144
+ const streamsToListen = [];
145
+ for (const [key, streamState] of Object.entries(this.state)) {
146
+ streamsToListen.push({ key, id: streamState.nextId });
147
+ }
148
+ const streamsMessages = await this.readStreams(streamsToListen);
149
+ if (streamsMessages) {
150
+ for (const streamMessages of streamsMessages) {
151
+ await this.processStreamMessages(streamMessages, batch);
152
+ }
153
+ }
154
+ await this.flushAcks();
155
+ }
156
+ catch (err) {
157
+ // A deliberate close() rejects the in-flight read; that is not an error.
158
+ if (this.stopped)
159
+ break;
160
+ this.client.emit('listen-error', err);
161
+ await timeout(LISTEN_ERROR_RETRY_DELAY, this.abort.signal);
162
+ }
163
+ }
164
+ }
165
+ hasStreamState(name) {
166
+ return !!this.getStreamState(name);
167
+ }
168
+ getStreamState(name) {
169
+ return this.state[name] ?? null;
170
+ }
171
+ initStreamState(stream) {
172
+ const startId = stream.id ?? '0-0';
173
+ this.state[stream.name] = {
174
+ nextId: startId,
175
+ executable: stream.executable,
176
+ // '>' means "new messages only": there is no pending list to drain first.
177
+ recovering: startId !== '>',
178
+ };
179
+ }
180
+ async readStreams(streamsToListen) {
181
+ const messages = await this.client.xReadGroup(this.originalClient.groupName, this.originalClient.clientName, streamsToListen, { BLOCK: this.BLOCK, COUNT: this.COUNT });
182
+ if (!messages)
183
+ return null;
184
+ return messages;
185
+ }
186
+ /**
187
+ * Runs the executable for a single read and acknowledges its messages only
188
+ * when processing succeeded, so a failed message stays in the pending list.
189
+ *
190
+ * In batch mode `payload` is the whole array. A failed batch call cannot tell
191
+ * which message failed, so every message of the read is retried on its own.
192
+ */
193
+ async handle(fnc, payload, readMessages, stream, batch) {
194
+ try {
195
+ await fnc(payload, stream);
196
+ }
197
+ catch (err) {
198
+ const error = err instanceof Error ? err : new Error(String(err));
199
+ for (const message of readMessages) {
200
+ this.client.emit('process-error', error, { stream, message, retries: 0 });
201
+ if (this.RETRIES === 0)
202
+ continue;
203
+ this.retryProcessor.add(error, stream, message, batch ? singleMessage(fnc) : fnc);
204
+ }
205
+ return;
206
+ }
207
+ for (const message of readMessages) {
208
+ this.addAckMessage(stream, message.id);
209
+ }
210
+ }
211
+ async processStreamMessages(streamMessages, batch) {
212
+ const stream = streamMessages.name;
213
+ const state = this.getStreamState(stream);
214
+ if (!state)
215
+ throw new Error('No state was found for stream processing of ' + stream);
216
+ const fnc = state.executable;
217
+ const messages = streamMessages.messages;
218
+ if (batch) {
219
+ // A read that only served an empty pending list has nothing to hand over.
220
+ if (messages.length > 0) {
221
+ await this.handle(fnc, messages, messages, stream, true);
222
+ }
223
+ }
224
+ else {
225
+ for (const message of messages) {
226
+ await this.handle(fnc, message, [message], stream, false);
227
+ }
228
+ }
229
+ if (state.recovering) {
230
+ if (messages.length === 0) {
231
+ state.nextId = '>';
232
+ state.recovering = false;
233
+ }
234
+ else {
235
+ const lastMessage = messages[messages.length - 1];
236
+ if (lastMessage)
237
+ state.nextId = lastMessage.id;
238
+ }
239
+ }
240
+ }
241
+ async acknowlegdeMessages() {
242
+ // Snapshot the entries: ids queued while this flush is awaiting belong to
243
+ // the next flush, not to this one.
244
+ const batches = [...this.successfullMessages];
245
+ if (batches.length === 0)
246
+ return;
247
+ // Detach before awaiting. Otherwise an `addAckMessage` for the same stream
248
+ // would append to the array being flushed and then be dropped by the
249
+ // removal below without ever reaching Redis.
250
+ for (const [stream] of batches)
251
+ this.successfullMessages.delete(stream);
252
+ // `XACK` and `XDEL` are independent, and node-redis pipelines commands that
253
+ // are in flight at the same time. Issuing all of them before awaiting costs
254
+ // one round trip for the whole flush instead of two per stream.
255
+ const inFlight = [];
256
+ for (const [stream, ids] of batches) {
257
+ // `XACK` reports ids that were already acknowledged as 0. The entry is
258
+ // deleted regardless, so a redelivered message cannot keep the queue
259
+ // growing without bound.
260
+ inFlight.push(this.originalClient.xAck(stream, this.originalClient.groupName, ids), this.originalClient.xDel(stream, ids));
261
+ }
262
+ try {
263
+ await Promise.all(inFlight);
264
+ }
265
+ catch (err) {
266
+ // Put the ids back so the next flush retries them.
267
+ for (const [stream, ids] of batches) {
268
+ for (const id of ids)
269
+ this.addAckMessage(stream, id);
270
+ }
271
+ throw err;
272
+ }
273
+ }
163
274
  }
164
- exports.RedisConsumer = RedisConsumer;
package/dist/helpers.d.ts CHANGED
@@ -1,2 +1,5 @@
1
- export declare const timeout: (ms: number) => Promise<unknown>;
2
- export type PropType<T, K extends keyof T> = T[K];
1
+ /**
2
+ * Resolves after `ms`, or as soon as `signal` aborts. The timer is always
3
+ * cleared, so a pending sleep never keeps the process alive.
4
+ */
5
+ export declare const timeout: (ms: number, signal?: AbortSignal) => Promise<void>;
package/dist/helpers.js CHANGED
@@ -1,5 +1,59 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.timeout = void 0;
4
- const timeout = async (ms) => new Promise(resolve => setTimeout(resolve, ms));
5
- exports.timeout = timeout;
1
+ /**
2
+ * Pending cancellations, one set per signal.
3
+ *
4
+ * `AbortSignal` is an `EventTarget`, and adding a listener scans the existing
5
+ * ones for a duplicate - so attaching one listener per sleep is O(n) per call
6
+ * and O(n^2) for a batch. A failed read of 20k messages arms 20k retry sleeps
7
+ * against the same signal, which took ~14s before a single timer fired and
8
+ * blocked the read loop for that whole time. Sharing one listener per signal
9
+ * keeps it linear.
10
+ */
11
+ const cancellations = new WeakMap();
12
+ /** Registers `cancel` to run when `signal` aborts. Returns an unregister fn. */
13
+ function onAbort(signal, cancel) {
14
+ let pending = cancellations.get(signal);
15
+ if (!pending) {
16
+ pending = new Set();
17
+ cancellations.set(signal, pending);
18
+ signal.addEventListener('abort', () => {
19
+ const waiting = cancellations.get(signal);
20
+ cancellations.delete(signal);
21
+ if (!waiting)
22
+ return;
23
+ // Snapshot: each callback unregisters itself.
24
+ const callbacks = [...waiting];
25
+ waiting.clear();
26
+ for (const callback of callbacks)
27
+ callback();
28
+ }, { once: true });
29
+ }
30
+ pending.add(cancel);
31
+ return () => pending.delete(cancel);
32
+ }
33
+ /**
34
+ * Resolves after `ms`, or as soon as `signal` aborts. The timer is always
35
+ * cleared, so a pending sleep never keeps the process alive.
36
+ */
37
+ export const timeout = async (ms, signal) => {
38
+ if (!signal) {
39
+ await new Promise(resolve => setTimeout(resolve, ms));
40
+ return;
41
+ }
42
+ if (signal.aborted)
43
+ return;
44
+ await new Promise(resolve => {
45
+ let settled = false;
46
+ const done = () => {
47
+ if (settled)
48
+ return;
49
+ settled = true;
50
+ clearTimeout(timer);
51
+ unregister();
52
+ resolve();
53
+ };
54
+ // No `await` between the `aborted` check above and this registration, so the
55
+ // signal cannot fire before `done` is reachable.
56
+ const unregister = onAbort(signal, done);
57
+ const timer = setTimeout(done, ms);
58
+ });
59
+ };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,5 @@
1
- import { StreamMessageReply } from '@redis/client/dist/lib/commands/generic-transformers';
2
- import { PropType } from './helpers';
3
- export * from 'redis';
4
- export { RedisClient } from './client';
5
- export * from './consumer';
6
- export * from './producer';
7
- export { RetryFailedMessage } from './retry-processor';
8
- export type StreamMessageId = PropType<StreamMessageReply, 'id'>;
9
- export type StreamMessageData = PropType<StreamMessageReply, 'message'>;
1
+ export type * from 'redis';
2
+ export { RedisClient } from './client.js';
3
+ export * from './consumer.js';
4
+ export * from './producer.js';
5
+ export type { RetryFailedMessage, RetryMessage } from './retry-processor.js';