redis-consumer 1.1.9 → 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.
package/dist/index.js CHANGED
@@ -1,22 +1,3 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
- for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
- };
16
- Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.RedisClient = void 0;
18
- __exportStar(require("redis"), exports);
19
- var client_1 = require("./client");
20
- Object.defineProperty(exports, "RedisClient", { enumerable: true, get: function () { return client_1.RedisClient; } });
21
- __exportStar(require("./consumer"), exports);
22
- __exportStar(require("./producer"), exports);
1
+ export { RedisClient } from './client.js';
2
+ export * from './consumer.js';
3
+ export * from './producer.js';
@@ -1,8 +1,8 @@
1
- import { RedisScripts } from 'redis';
2
- import { StreamMessageData, StreamMessageId } from '.';
3
- import { RedisClient } from './client';
1
+ import type { RedisScripts } from 'redis';
2
+ import type { StreamMessageData, StreamMessageId } from './consumer.js';
3
+ import type { RedisClient } from './client.js';
4
4
  export declare class RedisProducer<S extends RedisScripts = RedisScripts> {
5
5
  private client;
6
6
  constructor(client: RedisClient<S>);
7
- add(stream: StreamMessageId, message: StreamMessageData): void;
7
+ add(stream: StreamMessageId, message: StreamMessageData): Promise<StreamMessageId>;
8
8
  }
package/dist/producer.js CHANGED
@@ -1,13 +1,9 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RedisProducer = void 0;
4
- class RedisProducer {
1
+ export class RedisProducer {
5
2
  client;
6
3
  constructor(client) {
7
4
  this.client = client;
8
5
  }
9
6
  add(stream, message) {
10
- this.client.xAdd(stream, '*', message);
7
+ return this.client.xAdd(stream, '*', message);
11
8
  }
12
9
  }
13
- exports.RedisProducer = RedisProducer;
@@ -1,16 +1,12 @@
1
- /// <reference types="node" />
2
- /// <reference types="node" />
3
- import { StreamProcessingFunction, RedisConsumer } from './consumer';
4
- import { StreamMessageReply } from '@redis/client/dist/lib/commands/generic-transformers';
5
- import { RedisScripts } from 'redis';
6
- import { EventEmitter } from 'stream';
1
+ import type { RedisScripts } from 'redis';
2
+ import type { RedisConsumer, StreamExecutable, StreamMessageReply } from './consumer.js';
7
3
  interface RetryState {
8
4
  lastError: Error;
9
5
  timestamps: number[];
10
6
  retries: number;
11
7
  message: StreamMessageReply;
12
8
  stream: string;
13
- executable: StreamProcessingFunction<any>;
9
+ executable: StreamExecutable;
14
10
  }
15
11
  export type RetryFailedMessage = Omit<RetryState, 'executable' | 'lastError'>;
16
12
  export type RetryMessage = Omit<RetryFailedMessage, 'timestamps' | 'lastError'> & {
@@ -18,15 +14,22 @@ export type RetryMessage = Omit<RetryFailedMessage, 'timestamps' | 'lastError'>
18
14
  };
19
15
  interface RetryProcessorOptions {
20
16
  maxRetry: number;
21
- retryTime?: string[];
17
+ retryTime?: string[] | undefined;
22
18
  }
23
- export declare class RetryProcessor<S extends RedisScripts = RedisScripts> extends EventEmitter {
19
+ export declare class RetryProcessor<S extends RedisScripts = RedisScripts> {
24
20
  private consumer;
25
21
  private state;
22
+ /**
23
+ * Aborts the sleeps between retries. Without it a retry waiting on a long
24
+ * `retryTime` (15m by default) would keep the process alive after `close()`.
25
+ */
26
+ private abort;
26
27
  private retryTime;
27
- private maxRetry;
28
+ maxRetry: number;
28
29
  constructor(consumer: RedisConsumer<S>, options: RetryProcessorOptions);
29
- add(error: Error, stream: string, message: StreamMessageReply, executable: StreamProcessingFunction<any>): void;
30
+ /** Cancels the pending retries and forgets their state. */
31
+ stop(): void;
32
+ add(error: Error, stream: string, message: StreamMessageReply, executable: StreamExecutable): void;
30
33
  private processRetry;
31
34
  private calcTimeoutTime;
32
35
  private emitRetryFail;
@@ -1,30 +1,47 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RetryProcessor = void 0;
4
- const helpers_1 = require("./helpers");
5
- const stream_1 = require("stream");
6
- class RetryProcessor extends stream_1.EventEmitter {
1
+ import { timeout } from './helpers.js';
2
+ /**
3
+ * Message ids are only unique per stream, so retries must be tracked per stream
4
+ * as well - otherwise a message of one stream would shadow one of another.
5
+ */
6
+ function retryKey(stream, id) {
7
+ return stream + '\u0000' + id;
8
+ }
9
+ export class RetryProcessor {
7
10
  consumer;
8
11
  state = new Map();
12
+ /**
13
+ * Aborts the sleeps between retries. Without it a retry waiting on a long
14
+ * `retryTime` (15m by default) would keep the process alive after `close()`.
15
+ */
16
+ abort = new AbortController();
9
17
  retryTime;
10
18
  maxRetry;
11
19
  constructor(consumer, options) {
12
- super();
13
20
  this.consumer = consumer;
14
21
  this.retryTime = options.retryTime || ['15s', '1m', '15m'];
15
22
  this.maxRetry = options.maxRetry;
16
23
  }
24
+ /** Cancels the pending retries and forgets their state. */
25
+ stop() {
26
+ this.abort.abort();
27
+ this.state.clear();
28
+ }
17
29
  add(error, stream, message, executable) {
18
- const id = message.id;
19
- if (this.state.has(id))
30
+ if (this.abort.signal.aborted)
31
+ return;
32
+ const key = retryKey(stream, message.id);
33
+ if (this.state.has(key))
20
34
  return;
21
- this.state.set(id, { lastError: error, timestamps: [], retries: 0, message, stream, executable });
22
- this.processRetry(id);
35
+ this.state.set(key, { lastError: error, timestamps: [], retries: 0, message, stream, executable });
36
+ void this.processRetry(key);
23
37
  }
24
- async processRetry(id) {
25
- const stateObj = this.state.get(id);
38
+ async processRetry(key) {
39
+ const stateObj = this.state.get(key);
40
+ // Cleared by `stop()`, or already finished.
41
+ if (!stateObj || this.abort.signal.aborted)
42
+ return;
26
43
  if (stateObj.retries >= this.maxRetry) {
27
- this.state.delete(id);
44
+ this.state.delete(key);
28
45
  this.emitRetryFail(stateObj);
29
46
  return;
30
47
  }
@@ -32,14 +49,16 @@ class RetryProcessor extends stream_1.EventEmitter {
32
49
  const timestamp = new Date().getTime();
33
50
  stateObj.timestamps.push(timestamp);
34
51
  const timeoutTime = this.calcTimeoutTime(stateObj);
35
- await (0, helpers_1.timeout)(timeoutTime);
52
+ await timeout(timeoutTime, this.abort.signal);
53
+ if (this.abort.signal.aborted)
54
+ return;
36
55
  const fnc = stateObj.executable;
37
56
  const message = stateObj.message;
38
57
  try {
39
58
  this.emitRetry(stateObj);
40
59
  await fnc(message, stateObj.stream);
41
- this.consumer.addAckMessage(stateObj.stream, id);
42
- this.state.delete(id);
60
+ this.consumer.addAckMessage(stateObj.stream, message.id);
61
+ this.state.delete(key);
43
62
  }
44
63
  catch (err) {
45
64
  if (err instanceof Error) {
@@ -50,14 +69,25 @@ class RetryProcessor extends stream_1.EventEmitter {
50
69
  stateObj.lastError = newErr;
51
70
  }
52
71
  this.emitProcessFailed(stateObj);
53
- this.processRetry(id);
72
+ await this.processRetry(key);
73
+ return;
74
+ }
75
+ // The read loop is parked in `XREADGROUP`, so without an explicit flush the
76
+ // acknowledgement queued above would only reach Redis once the next message
77
+ // arrives - leaving the retried message in the pending list until then.
78
+ try {
79
+ await this.consumer.flushAcks();
80
+ }
81
+ catch (err) {
82
+ // The ids stay queued, so the read loop's next flush retries them.
83
+ this.consumer.client.emit('listen-error', err);
54
84
  }
55
- return;
56
85
  }
57
86
  calcTimeoutTime(stateObj) {
58
87
  const retry = stateObj.retries;
59
- const index = (retry > this.retryTime.length ? this.retryTime.length : retry) - 1;
60
- const timeString = this.retryTime[index];
88
+ const index = Math.min(retry, this.retryTime.length) - 1;
89
+ // Falls back to no delay if `retryTime` is empty or shorter than expected.
90
+ const timeString = this.retryTime[index] ?? '0s';
61
91
  let hours = +timeString.replace(/(\d+)h/, '$1');
62
92
  let minutes = +timeString.replace(/(\d+)m/, '$1');
63
93
  let seconds = +timeString.replace(/(\d+)s/, '$1');
@@ -81,4 +111,3 @@ class RetryProcessor extends stream_1.EventEmitter {
81
111
  this.consumer.client.emit('process-error', lastError, { stream, message, retries });
82
112
  }
83
113
  }
84
- exports.RetryProcessor = RetryProcessor;
package/package.json CHANGED
@@ -1,18 +1,23 @@
1
1
  {
2
2
  "name": "redis-consumer",
3
- "version": "1.1.9",
4
- "description": "Simple NodeJs consumer and producer for Redis Streams",
3
+ "version": "1.2.0",
4
+ "description": "NodeJs consumer and producer for Redis Streams",
5
+ "engines": {
6
+ "node": ">=24"
7
+ },
8
+ "type": "module",
5
9
  "main": "./dist/index.js",
6
10
  "types": "./dist/index.d.ts",
7
11
  "files": [
8
12
  "dist/"
9
13
  ],
10
14
  "scripts": {
11
- "build": "tsc"
15
+ "build": "tsc",
16
+ "typecheck:example": "tsc -p tsconfig.example.json"
12
17
  },
13
18
  "repository": {
14
19
  "type": "git",
15
- "url": "git+https://github.com/tomasky/redis-streams-nodejs.git"
20
+ "url": "git+https://github.com/tomasky/redis-consumer.git"
16
21
  },
17
22
  "author": "Sebastiaan Hekner",
18
23
  "keywords": [
@@ -29,14 +34,14 @@
29
34
  ],
30
35
  "license": "ISC",
31
36
  "bugs": {
32
- "url": "https://github.com/Seboeb/redis-streams-nodejs/issues"
37
+ "url": "https://github.com/tomasky/redis-consumer/issues"
33
38
  },
34
- "homepage": "https://github.com/tomasky/redis-streams-nodejs#readme",
39
+ "homepage": "https://github.com/tomasky/redis-consumer#readme",
35
40
  "dependencies": {
36
- "redis": "^4.7.1"
41
+ "redis": "^6.2.1"
37
42
  },
38
43
  "devDependencies": {
39
- "@types/node": "^22.15.0",
44
+ "@types/node": "latest",
40
45
  "typescript": "latest"
41
46
  }
42
47
  }