@discordjs/brokers 2.0.0-dev.1759363313-f510b5ffa → 2.0.0-dev.1759622511-ffbb7b693

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
@@ -43,7 +43,8 @@ These examples use [ES modules](https://nodejs.org/api/esm.html#enabling).
43
43
  import { PubSubRedisBroker } from '@discordjs/brokers';
44
44
  import Redis from 'ioredis';
45
45
 
46
- const broker = new PubSubRedisBroker(new Redis());
46
+ // Considering this only pushes events, the group and name are not important.
47
+ const broker = new PubSubRedisBroker(new Redis(), { group: 'noop', name: 'noop' });
47
48
 
48
49
  await broker.publish('test', 'Hello World!');
49
50
  await broker.destroy();
@@ -52,13 +53,22 @@ await broker.destroy();
52
53
  import { PubSubRedisBroker } from '@discordjs/brokers';
53
54
  import Redis from 'ioredis';
54
55
 
55
- const broker = new PubSubRedisBroker(new Redis());
56
+ const broker = new PubSubRedisBroker(new Redis(), {
57
+ // This is the consumer group name. You should make sure to not re-use this
58
+ // across different applications in your stack, unless you absolutely know
59
+ // what you're doing.
60
+ group: 'subscribers',
61
+ // With the assumption that this service will scale to more than one instance,
62
+ // you MUST ensure `UNIQUE_CONSUMER_ID` is unique across all of them and
63
+ // also deterministic (i.e. if instance-1 restarts, it should still be instance-1)
64
+ name: `consumer-${UNIQUE_CONSUMER_ID}`,
65
+ });
56
66
  broker.on('test', ({ data, ack }) => {
57
67
  console.log(data);
58
68
  void ack();
59
69
  });
60
70
 
61
- await broker.subscribe('subscribers', ['test']);
71
+ await broker.subscribe(['test']);
62
72
  ```
63
73
 
64
74
  ### RPC
@@ -68,7 +78,7 @@ await broker.subscribe('subscribers', ['test']);
68
78
  import { RPCRedisBroker } from '@discordjs/brokers';
69
79
  import Redis from 'ioredis';
70
80
 
71
- const broker = new RPCRedisBroker(new Redis());
81
+ const broker = new RPCRedisBroker(new Redis(), { group: 'noop', name: 'noop' });
72
82
 
73
83
  console.log(await broker.call('testcall', 'Hello World!'));
74
84
  await broker.destroy();
@@ -77,14 +87,18 @@ await broker.destroy();
77
87
  import { RPCRedisBroker } from '@discordjs/brokers';
78
88
  import Redis from 'ioredis';
79
89
 
80
- const broker = new RPCRedisBroker(new Redis());
90
+ const broker = new RPCRedisBroker(new Redis(), {
91
+ // Equivalent to the group/name in pubsub, refer to the previous example.
92
+ group: 'responders',
93
+ name: `consumer-${UNIQUE_ID}`,
94
+ });
81
95
  broker.on('testcall', ({ data, ack, reply }) => {
82
96
  console.log('responder', data);
83
97
  void ack();
84
98
  void reply(`Echo: ${data}`);
85
99
  });
86
100
 
87
- await broker.subscribe('responders', ['testcall']);
101
+ await broker.subscribe(['testcall']);
88
102
  ```
89
103
 
90
104
  ## Links
package/dist/index.d.mts CHANGED
@@ -57,11 +57,14 @@ interface IRPCBroker<TEvents extends Record<string, any[]>, TResponses extends R
57
57
  call<Event extends keyof TEvents>(event: Event, data: TEvents[Event], timeoutDuration?: number): Promise<TResponses[Event]>;
58
58
  }
59
59
 
60
+ type RedisReadGroupData = [Buffer, [Buffer, Buffer[]][]][];
60
61
  declare module 'ioredis' {
61
62
  interface Redis {
62
- xreadgroupBuffer(...args: (Buffer | string)[]): Promise<[Buffer, [Buffer, Buffer[]][]][] | null>;
63
+ xclaimBuffer(key: Buffer | string, group: Buffer | string, consumer: Buffer | string, minIdleTime: number, id: Buffer | string, ...args: (Buffer | string)[]): Promise<string[]>;
64
+ xreadgroupBuffer(...args: (Buffer | string)[]): Promise<RedisReadGroupData | null>;
63
65
  }
64
66
  }
67
+ declare const kUseRandomGroupName: unique symbol;
65
68
  /**
66
69
  * Options specific for a Redis broker
67
70
  */
@@ -71,28 +74,42 @@ interface RedisBrokerOptions extends BaseBrokerOptions {
71
74
  */
72
75
  blockTimeout?: number;
73
76
  /**
74
- * Consumer group name to use for this broker
77
+ * Consumer group name to use for this broker. For fanning out events, use {@link kUseRandomGroupName}
75
78
  *
76
79
  * @see {@link https://redis.io/commands/xreadgroup/}
77
80
  */
78
- group: string;
81
+ group: string | typeof kUseRandomGroupName;
79
82
  /**
80
83
  * Max number of messages to poll at once
81
84
  */
82
85
  maxChunk?: number;
86
+ /**
87
+ * How many times a message can be delivered to a consumer before it is considered dead.
88
+ * This is used to prevent messages from being stuck in the queue forever if a consumer is
89
+ * unable to process them.
90
+ */
91
+ maxDeliveredTimes?: number;
92
+ /**
93
+ * How long a message should be idle for before allowing it to be claimed by another consumer.
94
+ * Note that too high of a value can lead to a high delay in processing messages during a service downscale,
95
+ * while too low of a value can lead to messages being too eagerly claimed by other consumers during an instance
96
+ * restart (which is most likely not actually that problematic)
97
+ */
98
+ messageIdleTime?: number;
83
99
  /**
84
100
  * Unique consumer name.
85
101
  *
86
102
  * @see {@link https://redis.io/commands/xreadgroup/}
87
103
  */
88
- name?: string;
104
+ name: string;
89
105
  }
90
106
  /**
91
107
  * Default broker options for redis
92
108
  */
93
109
  declare const DefaultRedisBrokerOptions: {
94
- readonly name: string;
95
110
  readonly maxChunk: 10;
111
+ readonly maxDeliveredTimes: 3;
112
+ readonly messageIdleTime: 3000;
96
113
  readonly blockTimeout: 5000;
97
114
  readonly encode: (data: unknown) => Buffer;
98
115
  readonly decode: (data: Buffer<ArrayBufferLike>) => unknown;
@@ -118,6 +135,13 @@ declare abstract class BaseRedisBroker<TEvents extends Record<string, any[]>, TR
118
135
  * Internal copy of the Redis client being used to read incoming payloads
119
136
  */
120
137
  protected readonly streamReadClient: Redis;
138
+ /**
139
+ * The group being used by this broker.
140
+ *
141
+ * @privateRemarks
142
+ * Stored as its own field to do the "use random group" resolution in the constructor.
143
+ */
144
+ protected readonly group: string;
121
145
  /**
122
146
  * Whether this broker is currently polling events
123
147
  */
@@ -132,9 +156,12 @@ declare abstract class BaseRedisBroker<TEvents extends Record<string, any[]>, TR
132
156
  */
133
157
  unsubscribe(events: (keyof TEvents)[]): Promise<void>;
134
158
  /**
135
- * Begins polling for events, firing them to {@link BaseRedisBroker.listen}
159
+ * Begins polling for events, firing them to {@link BaseRedisBroker.emitEvent}
136
160
  */
137
161
  protected listen(): Promise<void>;
162
+ private readGroup;
163
+ private processMessages;
164
+ private claimAndEmitDeadEvents;
138
165
  /**
139
166
  * Destroys the broker, closing all connections
140
167
  */
@@ -196,8 +223,9 @@ interface RPCRedisBrokerOptions extends RedisBrokerOptions {
196
223
  */
197
224
  declare const DefaultRPCRedisBrokerOptions: {
198
225
  readonly timeout: 5000;
199
- readonly name: string;
200
226
  readonly maxChunk: 10;
227
+ readonly maxDeliveredTimes: 3;
228
+ readonly messageIdleTime: 3000;
201
229
  readonly blockTimeout: 5000;
202
230
  readonly encode: (data: unknown) => Buffer;
203
231
  readonly decode: (data: Buffer<ArrayBufferLike>) => unknown;
@@ -255,4 +283,4 @@ declare class RPCRedisBroker<TEvents extends Record<string, any[]>, TResponses e
255
283
  */
256
284
  declare const version: string;
257
285
 
258
- export { type BaseBrokerOptions, BaseRedisBroker, DefaultBrokerOptions, DefaultRPCRedisBrokerOptions, DefaultRedisBrokerOptions, type IBaseBroker, type IPubSubBroker, type IRPCBroker, PubSubRedisBroker, RPCRedisBroker, type RPCRedisBrokerOptions, type RedisBrokerOptions, type ToEventMap, version };
286
+ export { type BaseBrokerOptions, BaseRedisBroker, DefaultBrokerOptions, DefaultRPCRedisBrokerOptions, DefaultRedisBrokerOptions, type IBaseBroker, type IPubSubBroker, type IRPCBroker, PubSubRedisBroker, RPCRedisBroker, type RPCRedisBrokerOptions, type RedisBrokerOptions, type ToEventMap, kUseRandomGroupName, version };
package/dist/index.d.ts CHANGED
@@ -57,11 +57,14 @@ interface IRPCBroker<TEvents extends Record<string, any[]>, TResponses extends R
57
57
  call<Event extends keyof TEvents>(event: Event, data: TEvents[Event], timeoutDuration?: number): Promise<TResponses[Event]>;
58
58
  }
59
59
 
60
+ type RedisReadGroupData = [Buffer, [Buffer, Buffer[]][]][];
60
61
  declare module 'ioredis' {
61
62
  interface Redis {
62
- xreadgroupBuffer(...args: (Buffer | string)[]): Promise<[Buffer, [Buffer, Buffer[]][]][] | null>;
63
+ xclaimBuffer(key: Buffer | string, group: Buffer | string, consumer: Buffer | string, minIdleTime: number, id: Buffer | string, ...args: (Buffer | string)[]): Promise<string[]>;
64
+ xreadgroupBuffer(...args: (Buffer | string)[]): Promise<RedisReadGroupData | null>;
63
65
  }
64
66
  }
67
+ declare const kUseRandomGroupName: unique symbol;
65
68
  /**
66
69
  * Options specific for a Redis broker
67
70
  */
@@ -71,28 +74,42 @@ interface RedisBrokerOptions extends BaseBrokerOptions {
71
74
  */
72
75
  blockTimeout?: number;
73
76
  /**
74
- * Consumer group name to use for this broker
77
+ * Consumer group name to use for this broker. For fanning out events, use {@link kUseRandomGroupName}
75
78
  *
76
79
  * @see {@link https://redis.io/commands/xreadgroup/}
77
80
  */
78
- group: string;
81
+ group: string | typeof kUseRandomGroupName;
79
82
  /**
80
83
  * Max number of messages to poll at once
81
84
  */
82
85
  maxChunk?: number;
86
+ /**
87
+ * How many times a message can be delivered to a consumer before it is considered dead.
88
+ * This is used to prevent messages from being stuck in the queue forever if a consumer is
89
+ * unable to process them.
90
+ */
91
+ maxDeliveredTimes?: number;
92
+ /**
93
+ * How long a message should be idle for before allowing it to be claimed by another consumer.
94
+ * Note that too high of a value can lead to a high delay in processing messages during a service downscale,
95
+ * while too low of a value can lead to messages being too eagerly claimed by other consumers during an instance
96
+ * restart (which is most likely not actually that problematic)
97
+ */
98
+ messageIdleTime?: number;
83
99
  /**
84
100
  * Unique consumer name.
85
101
  *
86
102
  * @see {@link https://redis.io/commands/xreadgroup/}
87
103
  */
88
- name?: string;
104
+ name: string;
89
105
  }
90
106
  /**
91
107
  * Default broker options for redis
92
108
  */
93
109
  declare const DefaultRedisBrokerOptions: {
94
- readonly name: string;
95
110
  readonly maxChunk: 10;
111
+ readonly maxDeliveredTimes: 3;
112
+ readonly messageIdleTime: 3000;
96
113
  readonly blockTimeout: 5000;
97
114
  readonly encode: (data: unknown) => Buffer;
98
115
  readonly decode: (data: Buffer<ArrayBufferLike>) => unknown;
@@ -118,6 +135,13 @@ declare abstract class BaseRedisBroker<TEvents extends Record<string, any[]>, TR
118
135
  * Internal copy of the Redis client being used to read incoming payloads
119
136
  */
120
137
  protected readonly streamReadClient: Redis;
138
+ /**
139
+ * The group being used by this broker.
140
+ *
141
+ * @privateRemarks
142
+ * Stored as its own field to do the "use random group" resolution in the constructor.
143
+ */
144
+ protected readonly group: string;
121
145
  /**
122
146
  * Whether this broker is currently polling events
123
147
  */
@@ -132,9 +156,12 @@ declare abstract class BaseRedisBroker<TEvents extends Record<string, any[]>, TR
132
156
  */
133
157
  unsubscribe(events: (keyof TEvents)[]): Promise<void>;
134
158
  /**
135
- * Begins polling for events, firing them to {@link BaseRedisBroker.listen}
159
+ * Begins polling for events, firing them to {@link BaseRedisBroker.emitEvent}
136
160
  */
137
161
  protected listen(): Promise<void>;
162
+ private readGroup;
163
+ private processMessages;
164
+ private claimAndEmitDeadEvents;
138
165
  /**
139
166
  * Destroys the broker, closing all connections
140
167
  */
@@ -196,8 +223,9 @@ interface RPCRedisBrokerOptions extends RedisBrokerOptions {
196
223
  */
197
224
  declare const DefaultRPCRedisBrokerOptions: {
198
225
  readonly timeout: 5000;
199
- readonly name: string;
200
226
  readonly maxChunk: 10;
227
+ readonly maxDeliveredTimes: 3;
228
+ readonly messageIdleTime: 3000;
201
229
  readonly blockTimeout: 5000;
202
230
  readonly encode: (data: unknown) => Buffer;
203
231
  readonly decode: (data: Buffer<ArrayBufferLike>) => unknown;
@@ -255,4 +283,4 @@ declare class RPCRedisBroker<TEvents extends Record<string, any[]>, TResponses e
255
283
  */
256
284
  declare const version: string;
257
285
 
258
- export { type BaseBrokerOptions, BaseRedisBroker, DefaultBrokerOptions, DefaultRPCRedisBrokerOptions, DefaultRedisBrokerOptions, type IBaseBroker, type IPubSubBroker, type IRPCBroker, PubSubRedisBroker, RPCRedisBroker, type RPCRedisBrokerOptions, type RedisBrokerOptions, type ToEventMap, version };
286
+ export { type BaseBrokerOptions, BaseRedisBroker, DefaultBrokerOptions, DefaultRPCRedisBrokerOptions, DefaultRedisBrokerOptions, type IBaseBroker, type IPubSubBroker, type IRPCBroker, PubSubRedisBroker, RPCRedisBroker, type RPCRedisBrokerOptions, type RedisBrokerOptions, type ToEventMap, kUseRandomGroupName, version };
package/dist/index.js CHANGED
@@ -27,6 +27,7 @@ __export(index_exports, {
27
27
  DefaultRedisBrokerOptions: () => DefaultRedisBrokerOptions,
28
28
  PubSubRedisBroker: () => PubSubRedisBroker,
29
29
  RPCRedisBroker: () => RPCRedisBroker,
30
+ kUseRandomGroupName: () => kUseRandomGroupName,
30
31
  version: () => version
31
32
  });
32
33
  module.exports = __toCommonJS(index_exports);
@@ -50,10 +51,12 @@ var DefaultBrokerOptions = {
50
51
  };
51
52
 
52
53
  // src/brokers/redis/BaseRedis.ts
54
+ var kUseRandomGroupName = Symbol.for("djs.brokers.useRandomGroupName");
53
55
  var DefaultRedisBrokerOptions = {
54
56
  ...DefaultBrokerOptions,
55
- name: (0, import_node_crypto.randomBytes)(20).toString("hex"),
56
57
  maxChunk: 10,
58
+ maxDeliveredTimes: 3,
59
+ messageIdleTime: 3e3,
57
60
  blockTimeout: 5e3
58
61
  };
59
62
  var BaseRedisBroker = class extends import_async_event_emitter.AsyncEventEmitter {
@@ -61,6 +64,7 @@ var BaseRedisBroker = class extends import_async_event_emitter.AsyncEventEmitter
61
64
  super();
62
65
  this.redisClient = redisClient;
63
66
  this.options = { ...DefaultRedisBrokerOptions, ...options };
67
+ this.group = this.options.group === kUseRandomGroupName ? (0, import_node_crypto.randomBytes)(16).toString("hex") : this.options.group;
64
68
  redisClient.defineCommand("xcleangroup", {
65
69
  numberOfKeys: 1,
66
70
  lua: (0, import_node_fs.readFileSync)((0, import_node_path.resolve)(__dirname, "..", "scripts", "xcleangroup.lua"), "utf8")
@@ -86,6 +90,13 @@ var BaseRedisBroker = class extends import_async_event_emitter.AsyncEventEmitter
86
90
  * Internal copy of the Redis client being used to read incoming payloads
87
91
  */
88
92
  streamReadClient;
93
+ /**
94
+ * The group being used by this broker.
95
+ *
96
+ * @privateRemarks
97
+ * Stored as its own field to do the "use random group" resolution in the constructor.
98
+ */
99
+ group;
89
100
  /**
90
101
  * Whether this broker is currently polling events
91
102
  */
@@ -99,7 +110,7 @@ var BaseRedisBroker = class extends import_async_event_emitter.AsyncEventEmitter
99
110
  events.map(async (event) => {
100
111
  this.subscribedEvents.add(event);
101
112
  try {
102
- return await this.redisClient.xgroup("CREATE", event, this.options.group, 0, "MKSTREAM");
113
+ return await this.redisClient.xgroup("CREATE", event, this.group, 0, "MKSTREAM");
103
114
  } catch (error) {
104
115
  if (!(error instanceof import_ioredis.ReplyError)) {
105
116
  throw error;
@@ -125,7 +136,7 @@ var BaseRedisBroker = class extends import_async_event_emitter.AsyncEventEmitter
125
136
  }
126
137
  }
127
138
  /**
128
- * Begins polling for events, firing them to {@link BaseRedisBroker.listen}
139
+ * Begins polling for events, firing them to {@link BaseRedisBroker.emitEvent}
129
140
  */
130
141
  async listen() {
131
142
  if (this.listening) {
@@ -134,34 +145,16 @@ var BaseRedisBroker = class extends import_async_event_emitter.AsyncEventEmitter
134
145
  this.listening = true;
135
146
  while (this.subscribedEvents.size > 0) {
136
147
  try {
137
- const data = await this.streamReadClient.xreadgroupBuffer(
138
- "GROUP",
139
- this.options.group,
140
- this.options.name,
141
- "COUNT",
142
- String(this.options.maxChunk),
143
- "BLOCK",
144
- String(this.options.blockTimeout),
145
- "STREAMS",
146
- ...this.subscribedEvents,
147
- ...Array.from({ length: this.subscribedEvents.size }, () => ">")
148
- );
148
+ await this.claimAndEmitDeadEvents();
149
+ } catch (error) {
150
+ this.emit("error", error);
151
+ }
152
+ try {
153
+ const data = await this.readGroup(">", this.options.blockTimeout);
149
154
  if (!data) {
150
155
  continue;
151
156
  }
152
- for (const [event, info] of data) {
153
- for (const [id, packet] of info) {
154
- const idx = packet.findIndex((value, idx2) => value.toString("utf8") === "data" && idx2 % 2 === 0);
155
- if (idx < 0) {
156
- continue;
157
- }
158
- const data2 = packet[idx + 1];
159
- if (!data2) {
160
- continue;
161
- }
162
- this.emitEvent(id, this.options.group, event.toString("utf8"), this.options.decode(data2));
163
- }
164
- }
157
+ await this.processMessages(data);
165
158
  } catch (error) {
166
159
  this.emit("error", error);
167
160
  break;
@@ -169,6 +162,81 @@ var BaseRedisBroker = class extends import_async_event_emitter.AsyncEventEmitter
169
162
  }
170
163
  this.listening = false;
171
164
  }
165
+ async readGroup(fromId, block) {
166
+ const data = await this.streamReadClient.xreadgroupBuffer(
167
+ "GROUP",
168
+ this.group,
169
+ this.options.name,
170
+ "COUNT",
171
+ String(this.options.maxChunk),
172
+ "BLOCK",
173
+ String(block),
174
+ "STREAMS",
175
+ ...this.subscribedEvents,
176
+ ...Array.from({ length: this.subscribedEvents.size }, () => fromId)
177
+ );
178
+ return data ?? [];
179
+ }
180
+ async processMessages(data) {
181
+ for (const [event, messages] of data) {
182
+ const eventName = event.toString("utf8");
183
+ for (const [id, packet] of messages) {
184
+ const idx = packet.findIndex((value, idx2) => value.toString("utf8") === "data" && idx2 % 2 === 0);
185
+ if (idx < 0) continue;
186
+ const payload = packet[idx + 1];
187
+ if (!payload) continue;
188
+ this.emitEvent(id, this.group, eventName, this.options.decode(payload));
189
+ }
190
+ }
191
+ }
192
+ async claimAndEmitDeadEvents() {
193
+ for (const stream of this.subscribedEvents) {
194
+ const pending = await this.streamReadClient.xpending(
195
+ stream,
196
+ this.group,
197
+ "-",
198
+ "+",
199
+ this.options.maxChunk
200
+ // See: https://redis.io/docs/latest/commands/xpending/#extended-form-of-xpending
201
+ );
202
+ for (const [id, consumer, idleMs, deliveredTimes] of pending) {
203
+ if (idleMs < this.options.messageIdleTime) {
204
+ continue;
205
+ }
206
+ if (deliveredTimes > this.options.maxDeliveredTimes) {
207
+ await this.streamReadClient.xdel(stream, this.group, id);
208
+ continue;
209
+ }
210
+ if (consumer !== this.options.name) {
211
+ const claimed = await this.streamReadClient.xclaimBuffer(
212
+ stream,
213
+ this.group,
214
+ this.options.name,
215
+ Math.max(this.options.messageIdleTime, 1),
216
+ id,
217
+ "JUSTID"
218
+ );
219
+ if (!claimed?.length) {
220
+ continue;
221
+ }
222
+ }
223
+ const entries = await this.streamReadClient.xrangeBuffer(stream, id, id);
224
+ if (!entries?.length) {
225
+ continue;
226
+ }
227
+ const [msgId, fields] = entries[0];
228
+ const idx = fields.findIndex((value, idx2) => value.toString("utf8") === "data" && idx2 % 2 === 0);
229
+ if (idx < 0) {
230
+ continue;
231
+ }
232
+ const payload = fields[idx + 1];
233
+ if (!payload) {
234
+ continue;
235
+ }
236
+ this.emitEvent(msgId, this.group, stream, this.options.decode(payload));
237
+ }
238
+ }
239
+ }
172
240
  /**
173
241
  * Destroys the broker, closing all connections
174
242
  */
@@ -256,7 +324,7 @@ var RPCRedisBroker = class extends BaseRedisBroker {
256
324
  const payload = {
257
325
  data,
258
326
  ack: /* @__PURE__ */ __name(async () => {
259
- await this.redisClient.xack(event, this.options.group, id);
327
+ await this.redisClient.xack(event, this.group, id);
260
328
  }, "ack"),
261
329
  reply: /* @__PURE__ */ __name(async (data2) => {
262
330
  await this.redisClient.publish(`${event}:${id.toString()}`, this.options.encode(data2));
@@ -267,7 +335,7 @@ var RPCRedisBroker = class extends BaseRedisBroker {
267
335
  };
268
336
 
269
337
  // src/index.ts
270
- var version = "2.0.0-dev.1759363313-f510b5ffa";
338
+ var version = "2.0.0-dev.1759622511-ffbb7b693";
271
339
  // Annotate the CommonJS export names for ESM import in node:
272
340
  0 && (module.exports = {
273
341
  BaseRedisBroker,
@@ -276,6 +344,7 @@ var version = "2.0.0-dev.1759363313-f510b5ffa";
276
344
  DefaultRedisBrokerOptions,
277
345
  PubSubRedisBroker,
278
346
  RPCRedisBroker,
347
+ kUseRandomGroupName,
279
348
  version
280
349
  });
281
350
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/brokers/redis/BaseRedis.ts","../src/brokers/Broker.ts","../src/brokers/redis/PubSubRedis.ts","../src/brokers/redis/RPCRedis.ts"],"sourcesContent":["export * from './brokers/redis/BaseRedis.js';\nexport * from './brokers/redis/PubSubRedis.js';\nexport * from './brokers/redis/RPCRedis.js';\n\nexport * from './brokers/Broker.js';\n\n/**\n * The {@link https://github.com/discordjs/discord.js/blob/main/packages/brokers#readme | @discordjs/brokers} version\n * that you are currently using.\n *\n * @privateRemarks This needs to explicitly be `string` so it is not typed as a \"const string\" that gets injected by esbuild.\n */\nexport const version = '2.0.0-dev.1759363313-f510b5ffa' as string;\n","import type { Buffer } from 'node:buffer';\nimport { randomBytes } from 'node:crypto';\nimport { readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { AsyncEventEmitter } from '@vladfrangu/async_event_emitter';\nimport type { Redis } from 'ioredis';\nimport { ReplyError } from 'ioredis';\nimport type { BaseBrokerOptions, IBaseBroker, ToEventMap } from '../Broker.js';\nimport { DefaultBrokerOptions } from '../Broker.js';\n\n// For some reason ioredis doesn't have this typed, but it exists\ndeclare module 'ioredis' {\n\tinterface Redis {\n\t\txreadgroupBuffer(...args: (Buffer | string)[]): Promise<[Buffer, [Buffer, Buffer[]][]][] | null>;\n\t}\n}\n\n/**\n * Options specific for a Redis broker\n */\nexport interface RedisBrokerOptions extends BaseBrokerOptions {\n\t/**\n\t * How long to block for messages when polling\n\t */\n\tblockTimeout?: number;\n\n\t/**\n\t * Consumer group name to use for this broker\n\t *\n\t * @see {@link https://redis.io/commands/xreadgroup/}\n\t */\n\tgroup: string;\n\n\t/**\n\t * Max number of messages to poll at once\n\t */\n\tmaxChunk?: number;\n\n\t/**\n\t * Unique consumer name.\n\t *\n\t * @see {@link https://redis.io/commands/xreadgroup/}\n\t */\n\tname?: string;\n}\n\n/**\n * Default broker options for redis\n */\nexport const DefaultRedisBrokerOptions = {\n\t...DefaultBrokerOptions,\n\tname: randomBytes(20).toString('hex'),\n\tmaxChunk: 10,\n\tblockTimeout: 5_000,\n} as const satisfies Required<Omit<RedisBrokerOptions, 'group'>>;\n\n/**\n * Helper class with shared Redis logic\n */\nexport abstract class BaseRedisBroker<\n\t\tTEvents extends Record<string, any[]>,\n\t\tTResponses extends Record<keyof TEvents, any> | undefined = undefined,\n\t>\n\textends AsyncEventEmitter<ToEventMap<TEvents, TResponses>>\n\timplements IBaseBroker<TEvents>\n{\n\t/**\n\t * Used for Redis queues, see the 3rd argument taken by {@link https://redis.io/commands/xadd | xadd}\n\t */\n\tpublic static readonly STREAM_DATA_KEY = 'data' as const;\n\n\t/**\n\t * Options this broker is using\n\t */\n\tprotected readonly options: Required<RedisBrokerOptions>;\n\n\t/**\n\t * Events this broker has subscribed to\n\t */\n\tprotected readonly subscribedEvents = new Set<string>();\n\n\t/**\n\t * Internal copy of the Redis client being used to read incoming payloads\n\t */\n\tprotected readonly streamReadClient: Redis;\n\n\t/**\n\t * Whether this broker is currently polling events\n\t */\n\tprotected listening = false;\n\n\tpublic constructor(\n\t\tprotected readonly redisClient: Redis,\n\t\toptions: RedisBrokerOptions,\n\t) {\n\t\tsuper();\n\t\tthis.options = { ...DefaultRedisBrokerOptions, ...options };\n\t\tredisClient.defineCommand('xcleangroup', {\n\t\t\tnumberOfKeys: 1,\n\t\t\tlua: readFileSync(resolve(__dirname, '..', 'scripts', 'xcleangroup.lua'), 'utf8'),\n\t\t});\n\t\tthis.streamReadClient = redisClient.duplicate();\n\t}\n\n\t/**\n\t * {@inheritDoc IBaseBroker.subscribe}\n\t */\n\tpublic async subscribe(events: (keyof TEvents)[]): Promise<void> {\n\t\tawait Promise.all(\n\t\t\t// @ts-expect-error: Intended\n\t\t\tevents.map(async (event) => {\n\t\t\t\tthis.subscribedEvents.add(event as string);\n\t\t\t\ttry {\n\t\t\t\t\treturn await this.redisClient.xgroup('CREATE', event as string, this.options.group, 0, 'MKSTREAM');\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (!(error instanceof ReplyError)) {\n\t\t\t\t\t\tthrow error;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}),\n\t\t);\n\t\tvoid this.listen();\n\t}\n\n\t/**\n\t * {@inheritDoc IBaseBroker.unsubscribe}\n\t */\n\tpublic async unsubscribe(events: (keyof TEvents)[]): Promise<void> {\n\t\tconst commands: unknown[][] = Array.from({ length: events.length * 2 });\n\t\tfor (let idx = 0; idx < commands.length; idx += 2) {\n\t\t\tconst event = events[idx / 2];\n\t\t\tcommands[idx] = ['xgroup', 'delconsumer', event as string, this.options.group, this.options.name];\n\t\t\tcommands[idx + 1] = ['xcleangroup', event as string, this.options.group];\n\t\t}\n\n\t\tawait this.redisClient.pipeline(commands).exec();\n\n\t\tfor (const event of events) {\n\t\t\tthis.subscribedEvents.delete(event as string);\n\t\t}\n\t}\n\n\t/**\n\t * Begins polling for events, firing them to {@link BaseRedisBroker.listen}\n\t */\n\tprotected async listen(): Promise<void> {\n\t\tif (this.listening) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.listening = true;\n\n\t\twhile (this.subscribedEvents.size > 0) {\n\t\t\ttry {\n\t\t\t\tconst data = await this.streamReadClient.xreadgroupBuffer(\n\t\t\t\t\t'GROUP',\n\t\t\t\t\tthis.options.group,\n\t\t\t\t\tthis.options.name,\n\t\t\t\t\t'COUNT',\n\t\t\t\t\tString(this.options.maxChunk),\n\t\t\t\t\t'BLOCK',\n\t\t\t\t\tString(this.options.blockTimeout),\n\t\t\t\t\t'STREAMS',\n\t\t\t\t\t...this.subscribedEvents,\n\t\t\t\t\t...Array.from({ length: this.subscribedEvents.size }, () => '>'),\n\t\t\t\t);\n\n\t\t\t\tif (!data) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tfor (const [event, info] of data) {\n\t\t\t\t\tfor (const [id, packet] of info) {\n\t\t\t\t\t\tconst idx = packet.findIndex((value, idx) => value.toString('utf8') === 'data' && idx % 2 === 0);\n\t\t\t\t\t\tif (idx < 0) {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst data = packet[idx + 1];\n\t\t\t\t\t\tif (!data) {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tthis.emitEvent(id, this.options.group, event.toString('utf8'), this.options.decode(data));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\t// @ts-expect-error: Intended\n\t\t\t\tthis.emit('error', error);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tthis.listening = false;\n\t}\n\n\t/**\n\t * Destroys the broker, closing all connections\n\t */\n\tpublic async destroy() {\n\t\tawait this.unsubscribe([...this.subscribedEvents]);\n\t\tthis.streamReadClient.disconnect();\n\t\tthis.redisClient.disconnect();\n\t}\n\n\t/**\n\t * Handles an incoming Redis event\n\t */\n\tprotected abstract emitEvent(id: Buffer, group: string, event: string, data: unknown): unknown;\n}\n","import { Buffer } from 'node:buffer';\nimport { encode, decode } from '@msgpack/msgpack';\nimport type { AsyncEventEmitter } from '@vladfrangu/async_event_emitter';\n\n/**\n * Base options for a broker implementation\n */\nexport interface BaseBrokerOptions {\n\t/**\n\t * Function to use for decoding messages\n\t */\n\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\tdecode?: (data: Buffer) => unknown;\n\t/**\n\t * Function to use for encoding messages\n\t */\n\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\tencode?: (data: unknown) => Buffer;\n}\n\n/**\n * Default broker options\n */\nexport const DefaultBrokerOptions = {\n\tencode: (data): Buffer => {\n\t\tconst encoded = encode(data);\n\t\treturn Buffer.from(encoded.buffer, encoded.byteOffset, encoded.byteLength);\n\t},\n\tdecode: (data): unknown => decode(data),\n} as const satisfies Required<BaseBrokerOptions>;\n\nexport type ToEventMap<\n\tTRecord extends Record<string, any[]>,\n\tTResponses extends Record<keyof TRecord, any> | undefined = undefined,\n> = {\n\t[TKey in keyof TRecord]: [\n\t\tevent: TResponses extends Record<keyof TRecord, any>\n\t\t\t? { ack(): Promise<void>; reply(data: TResponses[TKey]): Promise<void> }\n\t\t\t: { ack(): Promise<void>; data: TRecord[TKey] },\n\t];\n};\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface IBaseBroker<TEvents extends {}> {\n\t/**\n\t * Subscribes to the given events\n\t */\n\tsubscribe(events: (keyof TEvents)[]): Promise<void>;\n\t/**\n\t * Unsubscribes from the given events\n\t */\n\tunsubscribe(events: (keyof TEvents)[]): Promise<void>;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface IPubSubBroker<TEvents extends {}>\n\textends IBaseBroker<TEvents>,\n\t\tAsyncEventEmitter<ToEventMap<TEvents>> {\n\t/**\n\t * Publishes an event\n\t */\n\tpublish<Event extends keyof TEvents>(event: Event, data: TEvents[Event]): Promise<void>;\n}\n\nexport interface IRPCBroker<TEvents extends Record<string, any[]>, TResponses extends Record<keyof TEvents, any>>\n\textends IBaseBroker<TEvents>,\n\t\tAsyncEventEmitter<ToEventMap<TEvents, TResponses>> {\n\t/**\n\t * Makes an RPC call\n\t */\n\tcall<Event extends keyof TEvents>(\n\t\tevent: Event,\n\t\tdata: TEvents[Event],\n\t\ttimeoutDuration?: number,\n\t): Promise<TResponses[Event]>;\n}\n","import type { Buffer } from 'node:buffer';\nimport type { IPubSubBroker } from '../Broker.js';\nimport { BaseRedisBroker } from './BaseRedis.js';\n\n/**\n * PubSub broker powered by Redis\n *\n * @example\n * ```ts\n * // publisher.js\n * import { PubSubRedisBroker } from '@discordjs/brokers';\n * import Redis from 'ioredis';\n *\n * const broker = new PubSubRedisBroker(new Redis());\n *\n * await broker.publish('test', 'Hello World!');\n * await broker.destroy();\n *\n * // subscriber.js\n * import { PubSubRedisBroker } from '@discordjs/brokers';\n * import Redis from 'ioredis';\n *\n * const broker = new PubSubRedisBroker(new Redis());\n * \tbroker.on('test', ({ data, ack }) => {\n * \tconsole.log(data);\n * \tvoid ack();\n * });\n *\n * await broker.subscribe('subscribers', ['test']);\n * ```\n */\nexport class PubSubRedisBroker<TEvents extends Record<string, any>>\n\textends BaseRedisBroker<TEvents>\n\timplements IPubSubBroker<TEvents>\n{\n\t/**\n\t * {@inheritDoc IPubSubBroker.publish}\n\t */\n\tpublic async publish<Event extends keyof TEvents>(event: Event, data: TEvents[Event]): Promise<void> {\n\t\tawait this.redisClient.xadd(event as string, '*', BaseRedisBroker.STREAM_DATA_KEY, this.options.encode(data));\n\t}\n\n\tprotected emitEvent(id: Buffer, group: string, event: string, data: unknown) {\n\t\tconst payload: { ack(): Promise<void>; data: unknown } = {\n\t\t\tdata,\n\t\t\tack: async () => {\n\t\t\t\tawait this.redisClient.xack(event, group, id);\n\t\t\t},\n\t\t};\n\n\t\t// @ts-expect-error: Intended\n\t\tthis.emit(event, payload);\n\t}\n}\n","import type { Buffer } from 'node:buffer';\nimport { clearTimeout, setTimeout } from 'node:timers';\nimport type Redis from 'ioredis/built/Redis.js';\nimport type { IRPCBroker } from '../Broker.js';\nimport type { RedisBrokerOptions } from './BaseRedis.js';\nimport { BaseRedisBroker, DefaultRedisBrokerOptions } from './BaseRedis.js';\n\ninterface InternalPromise {\n\treject(error: any): void;\n\tresolve(data: any): void;\n\ttimeout: NodeJS.Timeout;\n}\n\n/**\n * Options specific for an RPC Redis broker\n */\nexport interface RPCRedisBrokerOptions extends RedisBrokerOptions {\n\ttimeout?: number;\n}\n\n/**\n * Default values used for the {@link RPCRedisBrokerOptions}\n */\nexport const DefaultRPCRedisBrokerOptions = {\n\t...DefaultRedisBrokerOptions,\n\ttimeout: 5_000,\n} as const satisfies Required<Omit<RPCRedisBrokerOptions, 'group'>>;\n\n/**\n * RPC broker powered by Redis\n *\n * @example\n * ```ts\n * // caller.js\n * import { RPCRedisBroker } from '@discordjs/brokers';\n * import Redis from 'ioredis';\n *\n * const broker = new RPCRedisBroker(new Redis());\n *\n * console.log(await broker.call('testcall', 'Hello World!'));\n * await broker.destroy();\n *\n * // responder.js\n * import { RPCRedisBroker } from '@discordjs/brokers';\n * import Redis from 'ioredis';\n *\n * const broker = new RPCRedisBroker(new Redis());\n * broker.on('testcall', ({ data, ack, reply }) => {\n * \tconsole.log('responder', data);\n * \tvoid ack();\n * \tvoid reply(`Echo: ${data}`);\n * });\n *\n * await broker.subscribe('responders', ['testcall']);\n * ```\n */\nexport class RPCRedisBroker<TEvents extends Record<string, any[]>, TResponses extends Record<keyof TEvents, any>>\n\textends BaseRedisBroker<TEvents, TResponses>\n\timplements IRPCBroker<TEvents, TResponses>\n{\n\t/**\n\t * Options this broker is using\n\t */\n\tprotected override readonly options: Required<RPCRedisBrokerOptions>;\n\n\t/**\n\t * @internal\n\t */\n\tprotected readonly promises = new Map<string, InternalPromise>();\n\n\tpublic constructor(redisClient: Redis, options: RPCRedisBrokerOptions) {\n\t\tsuper(redisClient, options);\n\t\tthis.options = { ...DefaultRPCRedisBrokerOptions, ...options };\n\n\t\tthis.streamReadClient.on('messageBuffer', (channel: Buffer, message: Buffer) => {\n\t\t\tconst [, id] = channel.toString().split(':');\n\t\t\tif (id && this.promises.has(id)) {\n\t\t\t\t// eslint-disable-next-line @typescript-eslint/unbound-method\n\t\t\t\tconst { resolve, timeout } = this.promises.get(id)!;\n\t\t\t\tresolve(this.options.decode(message));\n\t\t\t\tclearTimeout(timeout);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * {@inheritDoc IRPCBroker.call}\n\t */\n\tpublic async call<Event extends keyof TEvents>(\n\t\tevent: Event,\n\t\tdata: TEvents[Event],\n\t\ttimeoutDuration: number = this.options.timeout,\n\t): Promise<TResponses[Event]> {\n\t\tconst id = await this.redisClient.xadd(\n\t\t\tevent as string,\n\t\t\t'*',\n\t\t\tBaseRedisBroker.STREAM_DATA_KEY,\n\t\t\tthis.options.encode(data),\n\t\t);\n\t\t// This id! assertion is valid. From redis docs:\n\t\t// \"The command returns a Null reply when used with the NOMKSTREAM option and the key doesn't exist.\"\n\t\t// See: https://redis.io/commands/xadd/\n\t\tconst rpcChannel = `${event as string}:${id!}`;\n\n\t\t// Construct the error here for better stack traces\n\t\tconst timedOut = new Error(`timed out after ${timeoutDuration}ms`);\n\n\t\tawait this.streamReadClient.subscribe(rpcChannel);\n\t\treturn new Promise<TResponses[Event]>((resolve, reject) => {\n\t\t\tconst timeout = setTimeout(() => reject(timedOut), timeoutDuration).unref();\n\n\t\t\tthis.promises.set(id!, { resolve, reject, timeout });\n\t\t\t// eslint-disable-next-line promise/prefer-await-to-then\n\t\t}).finally(() => {\n\t\t\tvoid this.streamReadClient.unsubscribe(rpcChannel);\n\t\t\tthis.promises.delete(id!);\n\t\t});\n\t}\n\n\tprotected emitEvent(id: Buffer, event: string, data: unknown) {\n\t\tconst payload: { ack(): Promise<void>; data: unknown; reply(data: unknown): Promise<void> } = {\n\t\t\tdata,\n\t\t\tack: async () => {\n\t\t\t\tawait this.redisClient.xack(event, this.options.group, id);\n\t\t\t},\n\t\t\treply: async (data) => {\n\t\t\t\tawait this.redisClient.publish(`${event}:${id.toString()}`, this.options.encode(data));\n\t\t\t},\n\t\t};\n\n\t\t// @ts-expect-error: Intended\n\t\tthis.emit(event, payload);\n\t}\n}\n"],"mappings":"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","names":["idx","data","resolve","data"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/brokers/redis/BaseRedis.ts","../src/brokers/Broker.ts","../src/brokers/redis/PubSubRedis.ts","../src/brokers/redis/RPCRedis.ts"],"sourcesContent":["export * from './brokers/redis/BaseRedis.js';\nexport * from './brokers/redis/PubSubRedis.js';\nexport * from './brokers/redis/RPCRedis.js';\n\nexport * from './brokers/Broker.js';\n\n/**\n * The {@link https://github.com/discordjs/discord.js/blob/main/packages/brokers#readme | @discordjs/brokers} version\n * that you are currently using.\n *\n * @privateRemarks This needs to explicitly be `string` so it is not typed as a \"const string\" that gets injected by esbuild.\n */\nexport const version = '2.0.0-dev.1759622511-ffbb7b693' as string;\n","import type { Buffer } from 'node:buffer';\nimport { randomBytes } from 'node:crypto';\nimport { readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { AsyncEventEmitter } from '@vladfrangu/async_event_emitter';\nimport type { Redis } from 'ioredis';\nimport { ReplyError } from 'ioredis';\nimport type { BaseBrokerOptions, IBaseBroker, ToEventMap } from '../Broker.js';\nimport { DefaultBrokerOptions } from '../Broker.js';\n\ntype RedisReadGroupData = [Buffer, [Buffer, Buffer[]][]][];\n\n// For some reason ioredis doesn't have those typed, but they exist\ndeclare module 'ioredis' {\n\tinterface Redis {\n\t\txclaimBuffer(\n\t\t\tkey: Buffer | string,\n\t\t\tgroup: Buffer | string,\n\t\t\tconsumer: Buffer | string,\n\t\t\tminIdleTime: number,\n\t\t\tid: Buffer | string,\n\t\t\t...args: (Buffer | string)[]\n\t\t): Promise<string[]>;\n\t\txreadgroupBuffer(...args: (Buffer | string)[]): Promise<RedisReadGroupData | null>;\n\t}\n}\n\nexport const kUseRandomGroupName = Symbol.for('djs.brokers.useRandomGroupName');\n\n/**\n * Options specific for a Redis broker\n */\nexport interface RedisBrokerOptions extends BaseBrokerOptions {\n\t/**\n\t * How long to block for messages when polling\n\t */\n\tblockTimeout?: number;\n\t/**\n\t * Consumer group name to use for this broker. For fanning out events, use {@link kUseRandomGroupName}\n\t *\n\t * @see {@link https://redis.io/commands/xreadgroup/}\n\t */\n\tgroup: string | typeof kUseRandomGroupName;\n\t/**\n\t * Max number of messages to poll at once\n\t */\n\tmaxChunk?: number;\n\t/**\n\t * How many times a message can be delivered to a consumer before it is considered dead.\n\t * This is used to prevent messages from being stuck in the queue forever if a consumer is\n\t * unable to process them.\n\t */\n\tmaxDeliveredTimes?: number;\n\t/**\n\t * How long a message should be idle for before allowing it to be claimed by another consumer.\n\t * Note that too high of a value can lead to a high delay in processing messages during a service downscale,\n\t * while too low of a value can lead to messages being too eagerly claimed by other consumers during an instance\n\t * restart (which is most likely not actually that problematic)\n\t */\n\tmessageIdleTime?: number;\n\t/**\n\t * Unique consumer name.\n\t *\n\t * @see {@link https://redis.io/commands/xreadgroup/}\n\t */\n\tname: string;\n}\n\n/**\n * Default broker options for redis\n */\nexport const DefaultRedisBrokerOptions = {\n\t...DefaultBrokerOptions,\n\tmaxChunk: 10,\n\tmaxDeliveredTimes: 3,\n\tmessageIdleTime: 3_000,\n\tblockTimeout: 5_000,\n} as const satisfies Required<Omit<RedisBrokerOptions, 'group' | 'name'>>;\n\n/**\n * Helper class with shared Redis logic\n */\nexport abstract class BaseRedisBroker<\n\t\tTEvents extends Record<string, any[]>,\n\t\tTResponses extends Record<keyof TEvents, any> | undefined = undefined,\n\t>\n\textends AsyncEventEmitter<ToEventMap<TEvents, TResponses>>\n\timplements IBaseBroker<TEvents>\n{\n\t/**\n\t * Used for Redis queues, see the 3rd argument taken by {@link https://redis.io/commands/xadd | xadd}\n\t */\n\tpublic static readonly STREAM_DATA_KEY = 'data' as const;\n\n\t/**\n\t * Options this broker is using\n\t */\n\tprotected readonly options: Required<RedisBrokerOptions>;\n\n\t/**\n\t * Events this broker has subscribed to\n\t */\n\tprotected readonly subscribedEvents = new Set<string>();\n\n\t/**\n\t * Internal copy of the Redis client being used to read incoming payloads\n\t */\n\tprotected readonly streamReadClient: Redis;\n\n\t/**\n\t * The group being used by this broker.\n\t *\n\t * @privateRemarks\n\t * Stored as its own field to do the \"use random group\" resolution in the constructor.\n\t */\n\tprotected readonly group: string;\n\n\t/**\n\t * Whether this broker is currently polling events\n\t */\n\tprotected listening = false;\n\n\tpublic constructor(\n\t\tprotected readonly redisClient: Redis,\n\t\toptions: RedisBrokerOptions,\n\t) {\n\t\tsuper();\n\t\tthis.options = { ...DefaultRedisBrokerOptions, ...options };\n\t\tthis.group = this.options.group === kUseRandomGroupName ? randomBytes(16).toString('hex') : this.options.group;\n\t\tredisClient.defineCommand('xcleangroup', {\n\t\t\tnumberOfKeys: 1,\n\t\t\tlua: readFileSync(resolve(__dirname, '..', 'scripts', 'xcleangroup.lua'), 'utf8'),\n\t\t});\n\t\tthis.streamReadClient = redisClient.duplicate();\n\t}\n\n\t/**\n\t * {@inheritDoc IBaseBroker.subscribe}\n\t */\n\tpublic async subscribe(events: (keyof TEvents)[]): Promise<void> {\n\t\tawait Promise.all(\n\t\t\t// @ts-expect-error: Intended\n\t\t\tevents.map(async (event) => {\n\t\t\t\tthis.subscribedEvents.add(event as string);\n\t\t\t\ttry {\n\t\t\t\t\treturn await this.redisClient.xgroup('CREATE', event as string, this.group, 0, 'MKSTREAM');\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (!(error instanceof ReplyError)) {\n\t\t\t\t\t\tthrow error;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}),\n\t\t);\n\t\tvoid this.listen();\n\t}\n\n\t/**\n\t * {@inheritDoc IBaseBroker.unsubscribe}\n\t */\n\tpublic async unsubscribe(events: (keyof TEvents)[]): Promise<void> {\n\t\tconst commands: unknown[][] = Array.from({ length: events.length * 2 });\n\t\tfor (let idx = 0; idx < commands.length; idx += 2) {\n\t\t\tconst event = events[idx / 2];\n\t\t\tcommands[idx] = ['xgroup', 'delconsumer', event as string, this.options.group, this.options.name];\n\t\t\tcommands[idx + 1] = ['xcleangroup', event as string, this.options.group];\n\t\t}\n\n\t\tawait this.redisClient.pipeline(commands).exec();\n\n\t\tfor (const event of events) {\n\t\t\tthis.subscribedEvents.delete(event as string);\n\t\t}\n\t}\n\n\t/**\n\t * Begins polling for events, firing them to {@link BaseRedisBroker.emitEvent}\n\t */\n\tprotected async listen(): Promise<void> {\n\t\tif (this.listening) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.listening = true;\n\n\t\t// Enter regular polling\n\t\twhile (this.subscribedEvents.size > 0) {\n\t\t\ttry {\n\t\t\t\tawait this.claimAndEmitDeadEvents();\n\t\t\t} catch (error) {\n\t\t\t\t// @ts-expect-error: Intended\n\t\t\t\tthis.emit('error', error);\n\t\t\t\t// We don't break here to keep the loop running even if dead event processing fails\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\t// As per docs, '>' means \"give me a new message\"\n\t\t\t\tconst data = await this.readGroup('>', this.options.blockTimeout);\n\t\t\t\tif (!data) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tawait this.processMessages(data);\n\t\t\t} catch (error) {\n\t\t\t\t// @ts-expect-error: Intended\n\t\t\t\tthis.emit('error', error);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tthis.listening = false;\n\t}\n\n\tprivate async readGroup(fromId: string, block: number): Promise<RedisReadGroupData> {\n\t\tconst data = await this.streamReadClient.xreadgroupBuffer(\n\t\t\t'GROUP',\n\t\t\tthis.group,\n\t\t\tthis.options.name,\n\t\t\t'COUNT',\n\t\t\tString(this.options.maxChunk),\n\t\t\t'BLOCK',\n\t\t\tString(block),\n\t\t\t'STREAMS',\n\t\t\t...this.subscribedEvents,\n\t\t\t...Array.from({ length: this.subscribedEvents.size }, () => fromId),\n\t\t);\n\n\t\treturn data ?? [];\n\t}\n\n\tprivate async processMessages(data: RedisReadGroupData): Promise<void> {\n\t\tfor (const [event, messages] of data) {\n\t\t\tconst eventName = event.toString('utf8');\n\n\t\t\tfor (const [id, packet] of messages) {\n\t\t\t\tconst idx = packet.findIndex((value, idx) => value.toString('utf8') === 'data' && idx % 2 === 0);\n\t\t\t\tif (idx < 0) continue;\n\n\t\t\t\tconst payload = packet[idx + 1];\n\t\t\t\tif (!payload) continue;\n\n\t\t\t\tthis.emitEvent(id, this.group, eventName, this.options.decode(payload));\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async claimAndEmitDeadEvents(): Promise<void> {\n\t\tfor (const stream of this.subscribedEvents) {\n\t\t\t// Get up to N oldest pending messages (note: a pending message is a message that has been read, but never ACKed)\n\t\t\tconst pending = (await this.streamReadClient.xpending(\n\t\t\t\tstream,\n\t\t\t\tthis.group,\n\t\t\t\t'-',\n\t\t\t\t'+',\n\t\t\t\tthis.options.maxChunk,\n\t\t\t\t// See: https://redis.io/docs/latest/commands/xpending/#extended-form-of-xpending\n\t\t\t)) as [id: string, consumer: string, idleMs: number, deliveredTimes: number][];\n\n\t\t\tfor (const [id, consumer, idleMs, deliveredTimes] of pending) {\n\t\t\t\t// Technically xclaim checks for us anyway, but why not avoid an extra call?\n\t\t\t\tif (idleMs < this.options.messageIdleTime) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (deliveredTimes > this.options.maxDeliveredTimes) {\n\t\t\t\t\t// This message is dead. It has repeatedly failed being processed by a consumer.\n\t\t\t\t\tawait this.streamReadClient.xdel(stream, this.group, id);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Try to claim the message if we don't already own it (this may fail if another consumer has already claimed it)\n\t\t\t\tif (consumer !== this.options.name) {\n\t\t\t\t\tconst claimed = await this.streamReadClient.xclaimBuffer(\n\t\t\t\t\t\tstream,\n\t\t\t\t\t\tthis.group,\n\t\t\t\t\t\tthis.options.name,\n\t\t\t\t\t\tMath.max(this.options.messageIdleTime, 1),\n\t\t\t\t\t\tid,\n\t\t\t\t\t\t'JUSTID',\n\t\t\t\t\t);\n\n\t\t\t\t\t// Another consumer got the message before us\n\t\t\t\t\tif (!claimed?.length) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fetch message body\n\t\t\t\tconst entries = await this.streamReadClient.xrangeBuffer(stream, id, id);\n\t\t\t\t// No idea how this could happen, frankly!\n\t\t\t\tif (!entries?.length) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst [msgId, fields] = entries[0]!;\n\t\t\t\tconst idx = fields.findIndex((value, idx) => value.toString('utf8') === 'data' && idx % 2 === 0);\n\t\t\t\tif (idx < 0) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst payload = fields[idx + 1];\n\t\t\t\tif (!payload) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tthis.emitEvent(msgId, this.group, stream, this.options.decode(payload));\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Destroys the broker, closing all connections\n\t */\n\tpublic async destroy() {\n\t\tawait this.unsubscribe([...this.subscribedEvents]);\n\t\tthis.streamReadClient.disconnect();\n\t\tthis.redisClient.disconnect();\n\t}\n\n\t/**\n\t * Handles an incoming Redis event\n\t */\n\tprotected abstract emitEvent(id: Buffer, group: string, event: string, data: unknown): unknown;\n}\n","import { Buffer } from 'node:buffer';\nimport { encode, decode } from '@msgpack/msgpack';\nimport type { AsyncEventEmitter } from '@vladfrangu/async_event_emitter';\n\n/**\n * Base options for a broker implementation\n */\nexport interface BaseBrokerOptions {\n\t/**\n\t * Function to use for decoding messages\n\t */\n\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\tdecode?: (data: Buffer) => unknown;\n\t/**\n\t * Function to use for encoding messages\n\t */\n\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\tencode?: (data: unknown) => Buffer;\n}\n\n/**\n * Default broker options\n */\nexport const DefaultBrokerOptions = {\n\tencode: (data): Buffer => {\n\t\tconst encoded = encode(data);\n\t\treturn Buffer.from(encoded.buffer, encoded.byteOffset, encoded.byteLength);\n\t},\n\tdecode: (data): unknown => decode(data),\n} as const satisfies Required<BaseBrokerOptions>;\n\nexport type ToEventMap<\n\tTRecord extends Record<string, any[]>,\n\tTResponses extends Record<keyof TRecord, any> | undefined = undefined,\n> = {\n\t[TKey in keyof TRecord]: [\n\t\tevent: TResponses extends Record<keyof TRecord, any>\n\t\t\t? { ack(): Promise<void>; reply(data: TResponses[TKey]): Promise<void> }\n\t\t\t: { ack(): Promise<void>; data: TRecord[TKey] },\n\t];\n};\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface IBaseBroker<TEvents extends {}> {\n\t/**\n\t * Subscribes to the given events\n\t */\n\tsubscribe(events: (keyof TEvents)[]): Promise<void>;\n\t/**\n\t * Unsubscribes from the given events\n\t */\n\tunsubscribe(events: (keyof TEvents)[]): Promise<void>;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface IPubSubBroker<TEvents extends {}>\n\textends IBaseBroker<TEvents>,\n\t\tAsyncEventEmitter<ToEventMap<TEvents>> {\n\t/**\n\t * Publishes an event\n\t */\n\tpublish<Event extends keyof TEvents>(event: Event, data: TEvents[Event]): Promise<void>;\n}\n\nexport interface IRPCBroker<TEvents extends Record<string, any[]>, TResponses extends Record<keyof TEvents, any>>\n\textends IBaseBroker<TEvents>,\n\t\tAsyncEventEmitter<ToEventMap<TEvents, TResponses>> {\n\t/**\n\t * Makes an RPC call\n\t */\n\tcall<Event extends keyof TEvents>(\n\t\tevent: Event,\n\t\tdata: TEvents[Event],\n\t\ttimeoutDuration?: number,\n\t): Promise<TResponses[Event]>;\n}\n","import type { Buffer } from 'node:buffer';\nimport type { IPubSubBroker } from '../Broker.js';\nimport { BaseRedisBroker } from './BaseRedis.js';\n\n/**\n * PubSub broker powered by Redis\n *\n * @example\n * ```ts\n * // publisher.js\n * import { PubSubRedisBroker } from '@discordjs/brokers';\n * import Redis from 'ioredis';\n *\n * const broker = new PubSubRedisBroker(new Redis());\n *\n * await broker.publish('test', 'Hello World!');\n * await broker.destroy();\n *\n * // subscriber.js\n * import { PubSubRedisBroker } from '@discordjs/brokers';\n * import Redis from 'ioredis';\n *\n * const broker = new PubSubRedisBroker(new Redis());\n * \tbroker.on('test', ({ data, ack }) => {\n * \tconsole.log(data);\n * \tvoid ack();\n * });\n *\n * await broker.subscribe('subscribers', ['test']);\n * ```\n */\nexport class PubSubRedisBroker<TEvents extends Record<string, any>>\n\textends BaseRedisBroker<TEvents>\n\timplements IPubSubBroker<TEvents>\n{\n\t/**\n\t * {@inheritDoc IPubSubBroker.publish}\n\t */\n\tpublic async publish<Event extends keyof TEvents>(event: Event, data: TEvents[Event]): Promise<void> {\n\t\tawait this.redisClient.xadd(event as string, '*', BaseRedisBroker.STREAM_DATA_KEY, this.options.encode(data));\n\t}\n\n\tprotected emitEvent(id: Buffer, group: string, event: string, data: unknown) {\n\t\tconst payload: { ack(): Promise<void>; data: unknown } = {\n\t\t\tdata,\n\t\t\tack: async () => {\n\t\t\t\tawait this.redisClient.xack(event, group, id);\n\t\t\t},\n\t\t};\n\n\t\t// @ts-expect-error: Intended\n\t\tthis.emit(event, payload);\n\t}\n}\n","import type { Buffer } from 'node:buffer';\nimport { clearTimeout, setTimeout } from 'node:timers';\nimport type Redis from 'ioredis/built/Redis.js';\nimport type { IRPCBroker } from '../Broker.js';\nimport type { RedisBrokerOptions } from './BaseRedis.js';\nimport { BaseRedisBroker, DefaultRedisBrokerOptions } from './BaseRedis.js';\n\ninterface InternalPromise {\n\treject(error: any): void;\n\tresolve(data: any): void;\n\ttimeout: NodeJS.Timeout;\n}\n\n/**\n * Options specific for an RPC Redis broker\n */\nexport interface RPCRedisBrokerOptions extends RedisBrokerOptions {\n\ttimeout?: number;\n}\n\n/**\n * Default values used for the {@link RPCRedisBrokerOptions}\n */\nexport const DefaultRPCRedisBrokerOptions = {\n\t...DefaultRedisBrokerOptions,\n\ttimeout: 5_000,\n} as const satisfies Required<Omit<RPCRedisBrokerOptions, 'group' | 'name'>>;\n\n/**\n * RPC broker powered by Redis\n *\n * @example\n * ```ts\n * // caller.js\n * import { RPCRedisBroker } from '@discordjs/brokers';\n * import Redis from 'ioredis';\n *\n * const broker = new RPCRedisBroker(new Redis());\n *\n * console.log(await broker.call('testcall', 'Hello World!'));\n * await broker.destroy();\n *\n * // responder.js\n * import { RPCRedisBroker } from '@discordjs/brokers';\n * import Redis from 'ioredis';\n *\n * const broker = new RPCRedisBroker(new Redis());\n * broker.on('testcall', ({ data, ack, reply }) => {\n * \tconsole.log('responder', data);\n * \tvoid ack();\n * \tvoid reply(`Echo: ${data}`);\n * });\n *\n * await broker.subscribe('responders', ['testcall']);\n * ```\n */\nexport class RPCRedisBroker<TEvents extends Record<string, any[]>, TResponses extends Record<keyof TEvents, any>>\n\textends BaseRedisBroker<TEvents, TResponses>\n\timplements IRPCBroker<TEvents, TResponses>\n{\n\t/**\n\t * Options this broker is using\n\t */\n\tprotected override readonly options: Required<RPCRedisBrokerOptions>;\n\n\t/**\n\t * @internal\n\t */\n\tprotected readonly promises = new Map<string, InternalPromise>();\n\n\tpublic constructor(redisClient: Redis, options: RPCRedisBrokerOptions) {\n\t\tsuper(redisClient, options);\n\t\tthis.options = { ...DefaultRPCRedisBrokerOptions, ...options };\n\n\t\tthis.streamReadClient.on('messageBuffer', (channel: Buffer, message: Buffer) => {\n\t\t\tconst [, id] = channel.toString().split(':');\n\t\t\tif (id && this.promises.has(id)) {\n\t\t\t\t// eslint-disable-next-line @typescript-eslint/unbound-method\n\t\t\t\tconst { resolve, timeout } = this.promises.get(id)!;\n\t\t\t\tresolve(this.options.decode(message));\n\t\t\t\tclearTimeout(timeout);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * {@inheritDoc IRPCBroker.call}\n\t */\n\tpublic async call<Event extends keyof TEvents>(\n\t\tevent: Event,\n\t\tdata: TEvents[Event],\n\t\ttimeoutDuration: number = this.options.timeout,\n\t): Promise<TResponses[Event]> {\n\t\tconst id = await this.redisClient.xadd(\n\t\t\tevent as string,\n\t\t\t'*',\n\t\t\tBaseRedisBroker.STREAM_DATA_KEY,\n\t\t\tthis.options.encode(data),\n\t\t);\n\t\t// This id! assertion is valid. From redis docs:\n\t\t// \"The command returns a Null reply when used with the NOMKSTREAM option and the key doesn't exist.\"\n\t\t// See: https://redis.io/commands/xadd/\n\t\tconst rpcChannel = `${event as string}:${id!}`;\n\n\t\t// Construct the error here for better stack traces\n\t\tconst timedOut = new Error(`timed out after ${timeoutDuration}ms`);\n\n\t\tawait this.streamReadClient.subscribe(rpcChannel);\n\t\treturn new Promise<TResponses[Event]>((resolve, reject) => {\n\t\t\tconst timeout = setTimeout(() => reject(timedOut), timeoutDuration).unref();\n\n\t\t\tthis.promises.set(id!, { resolve, reject, timeout });\n\t\t\t// eslint-disable-next-line promise/prefer-await-to-then\n\t\t}).finally(() => {\n\t\t\tvoid this.streamReadClient.unsubscribe(rpcChannel);\n\t\t\tthis.promises.delete(id!);\n\t\t});\n\t}\n\n\tprotected emitEvent(id: Buffer, event: string, data: unknown) {\n\t\tconst payload: { ack(): Promise<void>; data: unknown; reply(data: unknown): Promise<void> } = {\n\t\t\tdata,\n\t\t\tack: async () => {\n\t\t\t\tawait this.redisClient.xack(event, this.group, id);\n\t\t\t},\n\t\t\treply: async (data) => {\n\t\t\t\tawait this.redisClient.publish(`${event}:${id.toString()}`, this.options.encode(data));\n\t\t\t},\n\t\t};\n\n\t\t// @ts-expect-error: Intended\n\t\tthis.emit(event, payload);\n\t}\n}\n"],"mappings":"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","names":["idx","resolve","data"]}
package/dist/index.mjs CHANGED
@@ -27,10 +27,12 @@ var DefaultBrokerOptions = {
27
27
  };
28
28
 
29
29
  // src/brokers/redis/BaseRedis.ts
30
+ var kUseRandomGroupName = Symbol.for("djs.brokers.useRandomGroupName");
30
31
  var DefaultRedisBrokerOptions = {
31
32
  ...DefaultBrokerOptions,
32
- name: randomBytes(20).toString("hex"),
33
33
  maxChunk: 10,
34
+ maxDeliveredTimes: 3,
35
+ messageIdleTime: 3e3,
34
36
  blockTimeout: 5e3
35
37
  };
36
38
  var BaseRedisBroker = class extends AsyncEventEmitter {
@@ -38,6 +40,7 @@ var BaseRedisBroker = class extends AsyncEventEmitter {
38
40
  super();
39
41
  this.redisClient = redisClient;
40
42
  this.options = { ...DefaultRedisBrokerOptions, ...options };
43
+ this.group = this.options.group === kUseRandomGroupName ? randomBytes(16).toString("hex") : this.options.group;
41
44
  redisClient.defineCommand("xcleangroup", {
42
45
  numberOfKeys: 1,
43
46
  lua: readFileSync(resolve(__dirname, "..", "scripts", "xcleangroup.lua"), "utf8")
@@ -63,6 +66,13 @@ var BaseRedisBroker = class extends AsyncEventEmitter {
63
66
  * Internal copy of the Redis client being used to read incoming payloads
64
67
  */
65
68
  streamReadClient;
69
+ /**
70
+ * The group being used by this broker.
71
+ *
72
+ * @privateRemarks
73
+ * Stored as its own field to do the "use random group" resolution in the constructor.
74
+ */
75
+ group;
66
76
  /**
67
77
  * Whether this broker is currently polling events
68
78
  */
@@ -76,7 +86,7 @@ var BaseRedisBroker = class extends AsyncEventEmitter {
76
86
  events.map(async (event) => {
77
87
  this.subscribedEvents.add(event);
78
88
  try {
79
- return await this.redisClient.xgroup("CREATE", event, this.options.group, 0, "MKSTREAM");
89
+ return await this.redisClient.xgroup("CREATE", event, this.group, 0, "MKSTREAM");
80
90
  } catch (error) {
81
91
  if (!(error instanceof ReplyError)) {
82
92
  throw error;
@@ -102,7 +112,7 @@ var BaseRedisBroker = class extends AsyncEventEmitter {
102
112
  }
103
113
  }
104
114
  /**
105
- * Begins polling for events, firing them to {@link BaseRedisBroker.listen}
115
+ * Begins polling for events, firing them to {@link BaseRedisBroker.emitEvent}
106
116
  */
107
117
  async listen() {
108
118
  if (this.listening) {
@@ -111,34 +121,16 @@ var BaseRedisBroker = class extends AsyncEventEmitter {
111
121
  this.listening = true;
112
122
  while (this.subscribedEvents.size > 0) {
113
123
  try {
114
- const data = await this.streamReadClient.xreadgroupBuffer(
115
- "GROUP",
116
- this.options.group,
117
- this.options.name,
118
- "COUNT",
119
- String(this.options.maxChunk),
120
- "BLOCK",
121
- String(this.options.blockTimeout),
122
- "STREAMS",
123
- ...this.subscribedEvents,
124
- ...Array.from({ length: this.subscribedEvents.size }, () => ">")
125
- );
124
+ await this.claimAndEmitDeadEvents();
125
+ } catch (error) {
126
+ this.emit("error", error);
127
+ }
128
+ try {
129
+ const data = await this.readGroup(">", this.options.blockTimeout);
126
130
  if (!data) {
127
131
  continue;
128
132
  }
129
- for (const [event, info] of data) {
130
- for (const [id, packet] of info) {
131
- const idx = packet.findIndex((value, idx2) => value.toString("utf8") === "data" && idx2 % 2 === 0);
132
- if (idx < 0) {
133
- continue;
134
- }
135
- const data2 = packet[idx + 1];
136
- if (!data2) {
137
- continue;
138
- }
139
- this.emitEvent(id, this.options.group, event.toString("utf8"), this.options.decode(data2));
140
- }
141
- }
133
+ await this.processMessages(data);
142
134
  } catch (error) {
143
135
  this.emit("error", error);
144
136
  break;
@@ -146,6 +138,81 @@ var BaseRedisBroker = class extends AsyncEventEmitter {
146
138
  }
147
139
  this.listening = false;
148
140
  }
141
+ async readGroup(fromId, block) {
142
+ const data = await this.streamReadClient.xreadgroupBuffer(
143
+ "GROUP",
144
+ this.group,
145
+ this.options.name,
146
+ "COUNT",
147
+ String(this.options.maxChunk),
148
+ "BLOCK",
149
+ String(block),
150
+ "STREAMS",
151
+ ...this.subscribedEvents,
152
+ ...Array.from({ length: this.subscribedEvents.size }, () => fromId)
153
+ );
154
+ return data ?? [];
155
+ }
156
+ async processMessages(data) {
157
+ for (const [event, messages] of data) {
158
+ const eventName = event.toString("utf8");
159
+ for (const [id, packet] of messages) {
160
+ const idx = packet.findIndex((value, idx2) => value.toString("utf8") === "data" && idx2 % 2 === 0);
161
+ if (idx < 0) continue;
162
+ const payload = packet[idx + 1];
163
+ if (!payload) continue;
164
+ this.emitEvent(id, this.group, eventName, this.options.decode(payload));
165
+ }
166
+ }
167
+ }
168
+ async claimAndEmitDeadEvents() {
169
+ for (const stream of this.subscribedEvents) {
170
+ const pending = await this.streamReadClient.xpending(
171
+ stream,
172
+ this.group,
173
+ "-",
174
+ "+",
175
+ this.options.maxChunk
176
+ // See: https://redis.io/docs/latest/commands/xpending/#extended-form-of-xpending
177
+ );
178
+ for (const [id, consumer, idleMs, deliveredTimes] of pending) {
179
+ if (idleMs < this.options.messageIdleTime) {
180
+ continue;
181
+ }
182
+ if (deliveredTimes > this.options.maxDeliveredTimes) {
183
+ await this.streamReadClient.xdel(stream, this.group, id);
184
+ continue;
185
+ }
186
+ if (consumer !== this.options.name) {
187
+ const claimed = await this.streamReadClient.xclaimBuffer(
188
+ stream,
189
+ this.group,
190
+ this.options.name,
191
+ Math.max(this.options.messageIdleTime, 1),
192
+ id,
193
+ "JUSTID"
194
+ );
195
+ if (!claimed?.length) {
196
+ continue;
197
+ }
198
+ }
199
+ const entries = await this.streamReadClient.xrangeBuffer(stream, id, id);
200
+ if (!entries?.length) {
201
+ continue;
202
+ }
203
+ const [msgId, fields] = entries[0];
204
+ const idx = fields.findIndex((value, idx2) => value.toString("utf8") === "data" && idx2 % 2 === 0);
205
+ if (idx < 0) {
206
+ continue;
207
+ }
208
+ const payload = fields[idx + 1];
209
+ if (!payload) {
210
+ continue;
211
+ }
212
+ this.emitEvent(msgId, this.group, stream, this.options.decode(payload));
213
+ }
214
+ }
215
+ }
149
216
  /**
150
217
  * Destroys the broker, closing all connections
151
218
  */
@@ -233,7 +300,7 @@ var RPCRedisBroker = class extends BaseRedisBroker {
233
300
  const payload = {
234
301
  data,
235
302
  ack: /* @__PURE__ */ __name(async () => {
236
- await this.redisClient.xack(event, this.options.group, id);
303
+ await this.redisClient.xack(event, this.group, id);
237
304
  }, "ack"),
238
305
  reply: /* @__PURE__ */ __name(async (data2) => {
239
306
  await this.redisClient.publish(`${event}:${id.toString()}`, this.options.encode(data2));
@@ -244,7 +311,7 @@ var RPCRedisBroker = class extends BaseRedisBroker {
244
311
  };
245
312
 
246
313
  // src/index.ts
247
- var version = "2.0.0-dev.1759363313-f510b5ffa";
314
+ var version = "2.0.0-dev.1759622511-ffbb7b693";
248
315
  export {
249
316
  BaseRedisBroker,
250
317
  DefaultBrokerOptions,
@@ -252,6 +319,7 @@ export {
252
319
  DefaultRedisBrokerOptions,
253
320
  PubSubRedisBroker,
254
321
  RPCRedisBroker,
322
+ kUseRandomGroupName,
255
323
  version
256
324
  };
257
325
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../node_modules/.pnpm/tsup@8.5.0_@microsoft+api-extractor@7.52.11_@types+node@22.17.2__jiti@2.5.1_postcss@8.5.6_typescript@5.9.2_yaml@2.8.1/node_modules/tsup/assets/esm_shims.js","../src/brokers/redis/BaseRedis.ts","../src/brokers/Broker.ts","../src/brokers/redis/PubSubRedis.ts","../src/brokers/redis/RPCRedis.ts","../src/index.ts"],"sourcesContent":["// Shim globals in esm bundle\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst getFilename = () => fileURLToPath(import.meta.url)\nconst getDirname = () => path.dirname(getFilename())\n\nexport const __dirname = /* @__PURE__ */ getDirname()\nexport const __filename = /* @__PURE__ */ getFilename()\n","import type { Buffer } from 'node:buffer';\nimport { randomBytes } from 'node:crypto';\nimport { readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { AsyncEventEmitter } from '@vladfrangu/async_event_emitter';\nimport type { Redis } from 'ioredis';\nimport { ReplyError } from 'ioredis';\nimport type { BaseBrokerOptions, IBaseBroker, ToEventMap } from '../Broker.js';\nimport { DefaultBrokerOptions } from '../Broker.js';\n\n// For some reason ioredis doesn't have this typed, but it exists\ndeclare module 'ioredis' {\n\tinterface Redis {\n\t\txreadgroupBuffer(...args: (Buffer | string)[]): Promise<[Buffer, [Buffer, Buffer[]][]][] | null>;\n\t}\n}\n\n/**\n * Options specific for a Redis broker\n */\nexport interface RedisBrokerOptions extends BaseBrokerOptions {\n\t/**\n\t * How long to block for messages when polling\n\t */\n\tblockTimeout?: number;\n\n\t/**\n\t * Consumer group name to use for this broker\n\t *\n\t * @see {@link https://redis.io/commands/xreadgroup/}\n\t */\n\tgroup: string;\n\n\t/**\n\t * Max number of messages to poll at once\n\t */\n\tmaxChunk?: number;\n\n\t/**\n\t * Unique consumer name.\n\t *\n\t * @see {@link https://redis.io/commands/xreadgroup/}\n\t */\n\tname?: string;\n}\n\n/**\n * Default broker options for redis\n */\nexport const DefaultRedisBrokerOptions = {\n\t...DefaultBrokerOptions,\n\tname: randomBytes(20).toString('hex'),\n\tmaxChunk: 10,\n\tblockTimeout: 5_000,\n} as const satisfies Required<Omit<RedisBrokerOptions, 'group'>>;\n\n/**\n * Helper class with shared Redis logic\n */\nexport abstract class BaseRedisBroker<\n\t\tTEvents extends Record<string, any[]>,\n\t\tTResponses extends Record<keyof TEvents, any> | undefined = undefined,\n\t>\n\textends AsyncEventEmitter<ToEventMap<TEvents, TResponses>>\n\timplements IBaseBroker<TEvents>\n{\n\t/**\n\t * Used for Redis queues, see the 3rd argument taken by {@link https://redis.io/commands/xadd | xadd}\n\t */\n\tpublic static readonly STREAM_DATA_KEY = 'data' as const;\n\n\t/**\n\t * Options this broker is using\n\t */\n\tprotected readonly options: Required<RedisBrokerOptions>;\n\n\t/**\n\t * Events this broker has subscribed to\n\t */\n\tprotected readonly subscribedEvents = new Set<string>();\n\n\t/**\n\t * Internal copy of the Redis client being used to read incoming payloads\n\t */\n\tprotected readonly streamReadClient: Redis;\n\n\t/**\n\t * Whether this broker is currently polling events\n\t */\n\tprotected listening = false;\n\n\tpublic constructor(\n\t\tprotected readonly redisClient: Redis,\n\t\toptions: RedisBrokerOptions,\n\t) {\n\t\tsuper();\n\t\tthis.options = { ...DefaultRedisBrokerOptions, ...options };\n\t\tredisClient.defineCommand('xcleangroup', {\n\t\t\tnumberOfKeys: 1,\n\t\t\tlua: readFileSync(resolve(__dirname, '..', 'scripts', 'xcleangroup.lua'), 'utf8'),\n\t\t});\n\t\tthis.streamReadClient = redisClient.duplicate();\n\t}\n\n\t/**\n\t * {@inheritDoc IBaseBroker.subscribe}\n\t */\n\tpublic async subscribe(events: (keyof TEvents)[]): Promise<void> {\n\t\tawait Promise.all(\n\t\t\t// @ts-expect-error: Intended\n\t\t\tevents.map(async (event) => {\n\t\t\t\tthis.subscribedEvents.add(event as string);\n\t\t\t\ttry {\n\t\t\t\t\treturn await this.redisClient.xgroup('CREATE', event as string, this.options.group, 0, 'MKSTREAM');\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (!(error instanceof ReplyError)) {\n\t\t\t\t\t\tthrow error;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}),\n\t\t);\n\t\tvoid this.listen();\n\t}\n\n\t/**\n\t * {@inheritDoc IBaseBroker.unsubscribe}\n\t */\n\tpublic async unsubscribe(events: (keyof TEvents)[]): Promise<void> {\n\t\tconst commands: unknown[][] = Array.from({ length: events.length * 2 });\n\t\tfor (let idx = 0; idx < commands.length; idx += 2) {\n\t\t\tconst event = events[idx / 2];\n\t\t\tcommands[idx] = ['xgroup', 'delconsumer', event as string, this.options.group, this.options.name];\n\t\t\tcommands[idx + 1] = ['xcleangroup', event as string, this.options.group];\n\t\t}\n\n\t\tawait this.redisClient.pipeline(commands).exec();\n\n\t\tfor (const event of events) {\n\t\t\tthis.subscribedEvents.delete(event as string);\n\t\t}\n\t}\n\n\t/**\n\t * Begins polling for events, firing them to {@link BaseRedisBroker.listen}\n\t */\n\tprotected async listen(): Promise<void> {\n\t\tif (this.listening) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.listening = true;\n\n\t\twhile (this.subscribedEvents.size > 0) {\n\t\t\ttry {\n\t\t\t\tconst data = await this.streamReadClient.xreadgroupBuffer(\n\t\t\t\t\t'GROUP',\n\t\t\t\t\tthis.options.group,\n\t\t\t\t\tthis.options.name,\n\t\t\t\t\t'COUNT',\n\t\t\t\t\tString(this.options.maxChunk),\n\t\t\t\t\t'BLOCK',\n\t\t\t\t\tString(this.options.blockTimeout),\n\t\t\t\t\t'STREAMS',\n\t\t\t\t\t...this.subscribedEvents,\n\t\t\t\t\t...Array.from({ length: this.subscribedEvents.size }, () => '>'),\n\t\t\t\t);\n\n\t\t\t\tif (!data) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tfor (const [event, info] of data) {\n\t\t\t\t\tfor (const [id, packet] of info) {\n\t\t\t\t\t\tconst idx = packet.findIndex((value, idx) => value.toString('utf8') === 'data' && idx % 2 === 0);\n\t\t\t\t\t\tif (idx < 0) {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst data = packet[idx + 1];\n\t\t\t\t\t\tif (!data) {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tthis.emitEvent(id, this.options.group, event.toString('utf8'), this.options.decode(data));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\t// @ts-expect-error: Intended\n\t\t\t\tthis.emit('error', error);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tthis.listening = false;\n\t}\n\n\t/**\n\t * Destroys the broker, closing all connections\n\t */\n\tpublic async destroy() {\n\t\tawait this.unsubscribe([...this.subscribedEvents]);\n\t\tthis.streamReadClient.disconnect();\n\t\tthis.redisClient.disconnect();\n\t}\n\n\t/**\n\t * Handles an incoming Redis event\n\t */\n\tprotected abstract emitEvent(id: Buffer, group: string, event: string, data: unknown): unknown;\n}\n","import { Buffer } from 'node:buffer';\nimport { encode, decode } from '@msgpack/msgpack';\nimport type { AsyncEventEmitter } from '@vladfrangu/async_event_emitter';\n\n/**\n * Base options for a broker implementation\n */\nexport interface BaseBrokerOptions {\n\t/**\n\t * Function to use for decoding messages\n\t */\n\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\tdecode?: (data: Buffer) => unknown;\n\t/**\n\t * Function to use for encoding messages\n\t */\n\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\tencode?: (data: unknown) => Buffer;\n}\n\n/**\n * Default broker options\n */\nexport const DefaultBrokerOptions = {\n\tencode: (data): Buffer => {\n\t\tconst encoded = encode(data);\n\t\treturn Buffer.from(encoded.buffer, encoded.byteOffset, encoded.byteLength);\n\t},\n\tdecode: (data): unknown => decode(data),\n} as const satisfies Required<BaseBrokerOptions>;\n\nexport type ToEventMap<\n\tTRecord extends Record<string, any[]>,\n\tTResponses extends Record<keyof TRecord, any> | undefined = undefined,\n> = {\n\t[TKey in keyof TRecord]: [\n\t\tevent: TResponses extends Record<keyof TRecord, any>\n\t\t\t? { ack(): Promise<void>; reply(data: TResponses[TKey]): Promise<void> }\n\t\t\t: { ack(): Promise<void>; data: TRecord[TKey] },\n\t];\n};\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface IBaseBroker<TEvents extends {}> {\n\t/**\n\t * Subscribes to the given events\n\t */\n\tsubscribe(events: (keyof TEvents)[]): Promise<void>;\n\t/**\n\t * Unsubscribes from the given events\n\t */\n\tunsubscribe(events: (keyof TEvents)[]): Promise<void>;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface IPubSubBroker<TEvents extends {}>\n\textends IBaseBroker<TEvents>,\n\t\tAsyncEventEmitter<ToEventMap<TEvents>> {\n\t/**\n\t * Publishes an event\n\t */\n\tpublish<Event extends keyof TEvents>(event: Event, data: TEvents[Event]): Promise<void>;\n}\n\nexport interface IRPCBroker<TEvents extends Record<string, any[]>, TResponses extends Record<keyof TEvents, any>>\n\textends IBaseBroker<TEvents>,\n\t\tAsyncEventEmitter<ToEventMap<TEvents, TResponses>> {\n\t/**\n\t * Makes an RPC call\n\t */\n\tcall<Event extends keyof TEvents>(\n\t\tevent: Event,\n\t\tdata: TEvents[Event],\n\t\ttimeoutDuration?: number,\n\t): Promise<TResponses[Event]>;\n}\n","import type { Buffer } from 'node:buffer';\nimport type { IPubSubBroker } from '../Broker.js';\nimport { BaseRedisBroker } from './BaseRedis.js';\n\n/**\n * PubSub broker powered by Redis\n *\n * @example\n * ```ts\n * // publisher.js\n * import { PubSubRedisBroker } from '@discordjs/brokers';\n * import Redis from 'ioredis';\n *\n * const broker = new PubSubRedisBroker(new Redis());\n *\n * await broker.publish('test', 'Hello World!');\n * await broker.destroy();\n *\n * // subscriber.js\n * import { PubSubRedisBroker } from '@discordjs/brokers';\n * import Redis from 'ioredis';\n *\n * const broker = new PubSubRedisBroker(new Redis());\n * \tbroker.on('test', ({ data, ack }) => {\n * \tconsole.log(data);\n * \tvoid ack();\n * });\n *\n * await broker.subscribe('subscribers', ['test']);\n * ```\n */\nexport class PubSubRedisBroker<TEvents extends Record<string, any>>\n\textends BaseRedisBroker<TEvents>\n\timplements IPubSubBroker<TEvents>\n{\n\t/**\n\t * {@inheritDoc IPubSubBroker.publish}\n\t */\n\tpublic async publish<Event extends keyof TEvents>(event: Event, data: TEvents[Event]): Promise<void> {\n\t\tawait this.redisClient.xadd(event as string, '*', BaseRedisBroker.STREAM_DATA_KEY, this.options.encode(data));\n\t}\n\n\tprotected emitEvent(id: Buffer, group: string, event: string, data: unknown) {\n\t\tconst payload: { ack(): Promise<void>; data: unknown } = {\n\t\t\tdata,\n\t\t\tack: async () => {\n\t\t\t\tawait this.redisClient.xack(event, group, id);\n\t\t\t},\n\t\t};\n\n\t\t// @ts-expect-error: Intended\n\t\tthis.emit(event, payload);\n\t}\n}\n","import type { Buffer } from 'node:buffer';\nimport { clearTimeout, setTimeout } from 'node:timers';\nimport type Redis from 'ioredis/built/Redis.js';\nimport type { IRPCBroker } from '../Broker.js';\nimport type { RedisBrokerOptions } from './BaseRedis.js';\nimport { BaseRedisBroker, DefaultRedisBrokerOptions } from './BaseRedis.js';\n\ninterface InternalPromise {\n\treject(error: any): void;\n\tresolve(data: any): void;\n\ttimeout: NodeJS.Timeout;\n}\n\n/**\n * Options specific for an RPC Redis broker\n */\nexport interface RPCRedisBrokerOptions extends RedisBrokerOptions {\n\ttimeout?: number;\n}\n\n/**\n * Default values used for the {@link RPCRedisBrokerOptions}\n */\nexport const DefaultRPCRedisBrokerOptions = {\n\t...DefaultRedisBrokerOptions,\n\ttimeout: 5_000,\n} as const satisfies Required<Omit<RPCRedisBrokerOptions, 'group'>>;\n\n/**\n * RPC broker powered by Redis\n *\n * @example\n * ```ts\n * // caller.js\n * import { RPCRedisBroker } from '@discordjs/brokers';\n * import Redis from 'ioredis';\n *\n * const broker = new RPCRedisBroker(new Redis());\n *\n * console.log(await broker.call('testcall', 'Hello World!'));\n * await broker.destroy();\n *\n * // responder.js\n * import { RPCRedisBroker } from '@discordjs/brokers';\n * import Redis from 'ioredis';\n *\n * const broker = new RPCRedisBroker(new Redis());\n * broker.on('testcall', ({ data, ack, reply }) => {\n * \tconsole.log('responder', data);\n * \tvoid ack();\n * \tvoid reply(`Echo: ${data}`);\n * });\n *\n * await broker.subscribe('responders', ['testcall']);\n * ```\n */\nexport class RPCRedisBroker<TEvents extends Record<string, any[]>, TResponses extends Record<keyof TEvents, any>>\n\textends BaseRedisBroker<TEvents, TResponses>\n\timplements IRPCBroker<TEvents, TResponses>\n{\n\t/**\n\t * Options this broker is using\n\t */\n\tprotected override readonly options: Required<RPCRedisBrokerOptions>;\n\n\t/**\n\t * @internal\n\t */\n\tprotected readonly promises = new Map<string, InternalPromise>();\n\n\tpublic constructor(redisClient: Redis, options: RPCRedisBrokerOptions) {\n\t\tsuper(redisClient, options);\n\t\tthis.options = { ...DefaultRPCRedisBrokerOptions, ...options };\n\n\t\tthis.streamReadClient.on('messageBuffer', (channel: Buffer, message: Buffer) => {\n\t\t\tconst [, id] = channel.toString().split(':');\n\t\t\tif (id && this.promises.has(id)) {\n\t\t\t\t// eslint-disable-next-line @typescript-eslint/unbound-method\n\t\t\t\tconst { resolve, timeout } = this.promises.get(id)!;\n\t\t\t\tresolve(this.options.decode(message));\n\t\t\t\tclearTimeout(timeout);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * {@inheritDoc IRPCBroker.call}\n\t */\n\tpublic async call<Event extends keyof TEvents>(\n\t\tevent: Event,\n\t\tdata: TEvents[Event],\n\t\ttimeoutDuration: number = this.options.timeout,\n\t): Promise<TResponses[Event]> {\n\t\tconst id = await this.redisClient.xadd(\n\t\t\tevent as string,\n\t\t\t'*',\n\t\t\tBaseRedisBroker.STREAM_DATA_KEY,\n\t\t\tthis.options.encode(data),\n\t\t);\n\t\t// This id! assertion is valid. From redis docs:\n\t\t// \"The command returns a Null reply when used with the NOMKSTREAM option and the key doesn't exist.\"\n\t\t// See: https://redis.io/commands/xadd/\n\t\tconst rpcChannel = `${event as string}:${id!}`;\n\n\t\t// Construct the error here for better stack traces\n\t\tconst timedOut = new Error(`timed out after ${timeoutDuration}ms`);\n\n\t\tawait this.streamReadClient.subscribe(rpcChannel);\n\t\treturn new Promise<TResponses[Event]>((resolve, reject) => {\n\t\t\tconst timeout = setTimeout(() => reject(timedOut), timeoutDuration).unref();\n\n\t\t\tthis.promises.set(id!, { resolve, reject, timeout });\n\t\t\t// eslint-disable-next-line promise/prefer-await-to-then\n\t\t}).finally(() => {\n\t\t\tvoid this.streamReadClient.unsubscribe(rpcChannel);\n\t\t\tthis.promises.delete(id!);\n\t\t});\n\t}\n\n\tprotected emitEvent(id: Buffer, event: string, data: unknown) {\n\t\tconst payload: { ack(): Promise<void>; data: unknown; reply(data: unknown): Promise<void> } = {\n\t\t\tdata,\n\t\t\tack: async () => {\n\t\t\t\tawait this.redisClient.xack(event, this.options.group, id);\n\t\t\t},\n\t\t\treply: async (data) => {\n\t\t\t\tawait this.redisClient.publish(`${event}:${id.toString()}`, this.options.encode(data));\n\t\t\t},\n\t\t};\n\n\t\t// @ts-expect-error: Intended\n\t\tthis.emit(event, payload);\n\t}\n}\n","export * from './brokers/redis/BaseRedis.js';\nexport * from './brokers/redis/PubSubRedis.js';\nexport * from './brokers/redis/RPCRedis.js';\n\nexport * from './brokers/Broker.js';\n\n/**\n * The {@link https://github.com/discordjs/discord.js/blob/main/packages/brokers#readme | @discordjs/brokers} version\n * that you are currently using.\n *\n * @privateRemarks This needs to explicitly be `string` so it is not typed as a \"const string\" that gets injected by esbuild.\n */\nexport const version = '2.0.0-dev.1759363313-f510b5ffa' as string;\n"],"mappings":";;;;AACA,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAE9B,IAAM,cAAc,6BAAM,cAAc,YAAY,GAAG,GAAnC;AACpB,IAAM,aAAa,6BAAM,KAAK,QAAQ,YAAY,CAAC,GAAhC;AAEZ,IAAM,YAA4B,2BAAW;;;ACNpD,SAAS,mBAAmB;AAC5B,SAAS,oBAAoB;AAC7B,SAAS,eAAe;AACxB,SAAS,yBAAyB;AAElC,SAAS,kBAAkB;;;ACN3B,SAAS,UAAAA,eAAc;AACvB,SAAS,QAAQ,cAAc;AAsBxB,IAAM,uBAAuB;AAAA,EACnC,QAAQ,wBAAC,SAAiB;AACzB,UAAM,UAAU,OAAO,IAAI;AAC3B,WAAOC,QAAO,KAAK,QAAQ,QAAQ,QAAQ,YAAY,QAAQ,UAAU;AAAA,EAC1E,GAHQ;AAAA,EAIR,QAAQ,wBAAC,SAAkB,OAAO,IAAI,GAA9B;AACT;;;ADoBO,IAAM,4BAA4B;AAAA,EACxC,GAAG;AAAA,EACH,MAAM,YAAY,EAAE,EAAE,SAAS,KAAK;AAAA,EACpC,UAAU;AAAA,EACV,cAAc;AACf;AAKO,IAAe,kBAAf,cAIE,kBAET;AAAA,EA0BQ,YACa,aACnB,SACC;AACD,UAAM;AAHa;AAInB,SAAK,UAAU,EAAE,GAAG,2BAA2B,GAAG,QAAQ;AAC1D,gBAAY,cAAc,eAAe;AAAA,MACxC,cAAc;AAAA,MACd,KAAK,aAAa,QAAQ,WAAW,MAAM,WAAW,iBAAiB,GAAG,MAAM;AAAA,IACjF,CAAC;AACD,SAAK,mBAAmB,YAAY,UAAU;AAAA,EAC/C;AAAA,EAtGD,OAiEA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAIC,OAAuB,kBAAkB;AAAA;AAAA;AAAA;AAAA,EAKtB;AAAA;AAAA;AAAA;AAAA,EAKA,mBAAmB,oBAAI,IAAY;AAAA;AAAA;AAAA;AAAA,EAKnC;AAAA;AAAA;AAAA;AAAA,EAKT,YAAY;AAAA;AAAA;AAAA;AAAA,EAkBtB,MAAa,UAAU,QAA0C;AAChE,UAAM,QAAQ;AAAA;AAAA,MAEb,OAAO,IAAI,OAAO,UAAU;AAC3B,aAAK,iBAAiB,IAAI,KAAe;AACzC,YAAI;AACH,iBAAO,MAAM,KAAK,YAAY,OAAO,UAAU,OAAiB,KAAK,QAAQ,OAAO,GAAG,UAAU;AAAA,QAClG,SAAS,OAAO;AACf,cAAI,EAAE,iBAAiB,aAAa;AACnC,kBAAM;AAAA,UACP;AAAA,QACD;AAAA,MACD,CAAC;AAAA,IACF;AACA,SAAK,KAAK,OAAO;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAa,YAAY,QAA0C;AAClE,UAAM,WAAwB,MAAM,KAAK,EAAE,QAAQ,OAAO,SAAS,EAAE,CAAC;AACtE,aAAS,MAAM,GAAG,MAAM,SAAS,QAAQ,OAAO,GAAG;AAClD,YAAM,QAAQ,OAAO,MAAM,CAAC;AAC5B,eAAS,GAAG,IAAI,CAAC,UAAU,eAAe,OAAiB,KAAK,QAAQ,OAAO,KAAK,QAAQ,IAAI;AAChG,eAAS,MAAM,CAAC,IAAI,CAAC,eAAe,OAAiB,KAAK,QAAQ,KAAK;AAAA,IACxE;AAEA,UAAM,KAAK,YAAY,SAAS,QAAQ,EAAE,KAAK;AAE/C,eAAW,SAAS,QAAQ;AAC3B,WAAK,iBAAiB,OAAO,KAAe;AAAA,IAC7C;AAAA,EACD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAgB,SAAwB;AACvC,QAAI,KAAK,WAAW;AACnB;AAAA,IACD;AAEA,SAAK,YAAY;AAEjB,WAAO,KAAK,iBAAiB,OAAO,GAAG;AACtC,UAAI;AACH,cAAM,OAAO,MAAM,KAAK,iBAAiB;AAAA,UACxC;AAAA,UACA,KAAK,QAAQ;AAAA,UACb,KAAK,QAAQ;AAAA,UACb;AAAA,UACA,OAAO,KAAK,QAAQ,QAAQ;AAAA,UAC5B;AAAA,UACA,OAAO,KAAK,QAAQ,YAAY;AAAA,UAChC;AAAA,UACA,GAAG,KAAK;AAAA,UACR,GAAG,MAAM,KAAK,EAAE,QAAQ,KAAK,iBAAiB,KAAK,GAAG,MAAM,GAAG;AAAA,QAChE;AAEA,YAAI,CAAC,MAAM;AACV;AAAA,QACD;AAEA,mBAAW,CAAC,OAAO,IAAI,KAAK,MAAM;AACjC,qBAAW,CAAC,IAAI,MAAM,KAAK,MAAM;AAChC,kBAAM,MAAM,OAAO,UAAU,CAAC,OAAOC,SAAQ,MAAM,SAAS,MAAM,MAAM,UAAUA,OAAM,MAAM,CAAC;AAC/F,gBAAI,MAAM,GAAG;AACZ;AAAA,YACD;AAEA,kBAAMC,QAAO,OAAO,MAAM,CAAC;AAC3B,gBAAI,CAACA,OAAM;AACV;AAAA,YACD;AAEA,iBAAK,UAAU,IAAI,KAAK,QAAQ,OAAO,MAAM,SAAS,MAAM,GAAG,KAAK,QAAQ,OAAOA,KAAI,CAAC;AAAA,UACzF;AAAA,QACD;AAAA,MACD,SAAS,OAAO;AAEf,aAAK,KAAK,SAAS,KAAK;AACxB;AAAA,MACD;AAAA,IACD;AAEA,SAAK,YAAY;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAa,UAAU;AACtB,UAAM,KAAK,YAAY,CAAC,GAAG,KAAK,gBAAgB,CAAC;AACjD,SAAK,iBAAiB,WAAW;AACjC,SAAK,YAAY,WAAW;AAAA,EAC7B;AAMD;;;AElLO,IAAM,oBAAN,cACE,gBAET;AAAA,EAlCA,OAkCA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAIC,MAAa,QAAqC,OAAc,MAAqC;AACpG,UAAM,KAAK,YAAY,KAAK,OAAiB,KAAK,gBAAgB,iBAAiB,KAAK,QAAQ,OAAO,IAAI,CAAC;AAAA,EAC7G;AAAA,EAEU,UAAU,IAAY,OAAe,OAAe,MAAe;AAC5E,UAAM,UAAmD;AAAA,MACxD;AAAA,MACA,KAAK,mCAAY;AAChB,cAAM,KAAK,YAAY,KAAK,OAAO,OAAO,EAAE;AAAA,MAC7C,GAFK;AAAA,IAGN;AAGA,SAAK,KAAK,OAAO,OAAO;AAAA,EACzB;AACD;;;ACpDA,SAAS,cAAc,kBAAkB;AAsBlC,IAAM,+BAA+B;AAAA,EAC3C,GAAG;AAAA,EACH,SAAS;AACV;AA8BO,IAAM,iBAAN,cACE,gBAET;AAAA,EA3DA,OA2DA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAI6B;AAAA;AAAA;AAAA;AAAA,EAKT,WAAW,oBAAI,IAA6B;AAAA,EAExD,YAAY,aAAoB,SAAgC;AACtE,UAAM,aAAa,OAAO;AAC1B,SAAK,UAAU,EAAE,GAAG,8BAA8B,GAAG,QAAQ;AAE7D,SAAK,iBAAiB,GAAG,iBAAiB,CAAC,SAAiB,YAAoB;AAC/E,YAAM,CAAC,EAAE,EAAE,IAAI,QAAQ,SAAS,EAAE,MAAM,GAAG;AAC3C,UAAI,MAAM,KAAK,SAAS,IAAI,EAAE,GAAG;AAEhC,cAAM,EAAE,SAAAC,UAAS,QAAQ,IAAI,KAAK,SAAS,IAAI,EAAE;AACjD,QAAAA,SAAQ,KAAK,QAAQ,OAAO,OAAO,CAAC;AACpC,qBAAa,OAAO;AAAA,MACrB;AAAA,IACD,CAAC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAa,KACZ,OACA,MACA,kBAA0B,KAAK,QAAQ,SACV;AAC7B,UAAM,KAAK,MAAM,KAAK,YAAY;AAAA,MACjC;AAAA,MACA;AAAA,MACA,gBAAgB;AAAA,MAChB,KAAK,QAAQ,OAAO,IAAI;AAAA,IACzB;AAIA,UAAM,aAAa,GAAG,KAAe,IAAI,EAAG;AAG5C,UAAM,WAAW,IAAI,MAAM,mBAAmB,eAAe,IAAI;AAEjE,UAAM,KAAK,iBAAiB,UAAU,UAAU;AAChD,WAAO,IAAI,QAA2B,CAACA,UAAS,WAAW;AAC1D,YAAM,UAAU,WAAW,MAAM,OAAO,QAAQ,GAAG,eAAe,EAAE,MAAM;AAE1E,WAAK,SAAS,IAAI,IAAK,EAAE,SAAAA,UAAS,QAAQ,QAAQ,CAAC;AAAA,IAEpD,CAAC,EAAE,QAAQ,MAAM;AAChB,WAAK,KAAK,iBAAiB,YAAY,UAAU;AACjD,WAAK,SAAS,OAAO,EAAG;AAAA,IACzB,CAAC;AAAA,EACF;AAAA,EAEU,UAAU,IAAY,OAAe,MAAe;AAC7D,UAAM,UAAwF;AAAA,MAC7F;AAAA,MACA,KAAK,mCAAY;AAChB,cAAM,KAAK,YAAY,KAAK,OAAO,KAAK,QAAQ,OAAO,EAAE;AAAA,MAC1D,GAFK;AAAA,MAGL,OAAO,8BAAOC,UAAS;AACtB,cAAM,KAAK,YAAY,QAAQ,GAAG,KAAK,IAAI,GAAG,SAAS,CAAC,IAAI,KAAK,QAAQ,OAAOA,KAAI,CAAC;AAAA,MACtF,GAFO;AAAA,IAGR;AAGA,SAAK,KAAK,OAAO,OAAO;AAAA,EACzB;AACD;;;ACzHO,IAAM,UAAU;","names":["Buffer","Buffer","idx","data","resolve","data"]}
1
+ {"version":3,"sources":["../../../node_modules/.pnpm/tsup@8.5.0_@microsoft+api-extractor@7.52.11_@types+node@22.17.2__jiti@2.5.1_postcss@8.5.6_typescript@5.9.2_yaml@2.8.1/node_modules/tsup/assets/esm_shims.js","../src/brokers/redis/BaseRedis.ts","../src/brokers/Broker.ts","../src/brokers/redis/PubSubRedis.ts","../src/brokers/redis/RPCRedis.ts","../src/index.ts"],"sourcesContent":["// Shim globals in esm bundle\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst getFilename = () => fileURLToPath(import.meta.url)\nconst getDirname = () => path.dirname(getFilename())\n\nexport const __dirname = /* @__PURE__ */ getDirname()\nexport const __filename = /* @__PURE__ */ getFilename()\n","import type { Buffer } from 'node:buffer';\nimport { randomBytes } from 'node:crypto';\nimport { readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { AsyncEventEmitter } from '@vladfrangu/async_event_emitter';\nimport type { Redis } from 'ioredis';\nimport { ReplyError } from 'ioredis';\nimport type { BaseBrokerOptions, IBaseBroker, ToEventMap } from '../Broker.js';\nimport { DefaultBrokerOptions } from '../Broker.js';\n\ntype RedisReadGroupData = [Buffer, [Buffer, Buffer[]][]][];\n\n// For some reason ioredis doesn't have those typed, but they exist\ndeclare module 'ioredis' {\n\tinterface Redis {\n\t\txclaimBuffer(\n\t\t\tkey: Buffer | string,\n\t\t\tgroup: Buffer | string,\n\t\t\tconsumer: Buffer | string,\n\t\t\tminIdleTime: number,\n\t\t\tid: Buffer | string,\n\t\t\t...args: (Buffer | string)[]\n\t\t): Promise<string[]>;\n\t\txreadgroupBuffer(...args: (Buffer | string)[]): Promise<RedisReadGroupData | null>;\n\t}\n}\n\nexport const kUseRandomGroupName = Symbol.for('djs.brokers.useRandomGroupName');\n\n/**\n * Options specific for a Redis broker\n */\nexport interface RedisBrokerOptions extends BaseBrokerOptions {\n\t/**\n\t * How long to block for messages when polling\n\t */\n\tblockTimeout?: number;\n\t/**\n\t * Consumer group name to use for this broker. For fanning out events, use {@link kUseRandomGroupName}\n\t *\n\t * @see {@link https://redis.io/commands/xreadgroup/}\n\t */\n\tgroup: string | typeof kUseRandomGroupName;\n\t/**\n\t * Max number of messages to poll at once\n\t */\n\tmaxChunk?: number;\n\t/**\n\t * How many times a message can be delivered to a consumer before it is considered dead.\n\t * This is used to prevent messages from being stuck in the queue forever if a consumer is\n\t * unable to process them.\n\t */\n\tmaxDeliveredTimes?: number;\n\t/**\n\t * How long a message should be idle for before allowing it to be claimed by another consumer.\n\t * Note that too high of a value can lead to a high delay in processing messages during a service downscale,\n\t * while too low of a value can lead to messages being too eagerly claimed by other consumers during an instance\n\t * restart (which is most likely not actually that problematic)\n\t */\n\tmessageIdleTime?: number;\n\t/**\n\t * Unique consumer name.\n\t *\n\t * @see {@link https://redis.io/commands/xreadgroup/}\n\t */\n\tname: string;\n}\n\n/**\n * Default broker options for redis\n */\nexport const DefaultRedisBrokerOptions = {\n\t...DefaultBrokerOptions,\n\tmaxChunk: 10,\n\tmaxDeliveredTimes: 3,\n\tmessageIdleTime: 3_000,\n\tblockTimeout: 5_000,\n} as const satisfies Required<Omit<RedisBrokerOptions, 'group' | 'name'>>;\n\n/**\n * Helper class with shared Redis logic\n */\nexport abstract class BaseRedisBroker<\n\t\tTEvents extends Record<string, any[]>,\n\t\tTResponses extends Record<keyof TEvents, any> | undefined = undefined,\n\t>\n\textends AsyncEventEmitter<ToEventMap<TEvents, TResponses>>\n\timplements IBaseBroker<TEvents>\n{\n\t/**\n\t * Used for Redis queues, see the 3rd argument taken by {@link https://redis.io/commands/xadd | xadd}\n\t */\n\tpublic static readonly STREAM_DATA_KEY = 'data' as const;\n\n\t/**\n\t * Options this broker is using\n\t */\n\tprotected readonly options: Required<RedisBrokerOptions>;\n\n\t/**\n\t * Events this broker has subscribed to\n\t */\n\tprotected readonly subscribedEvents = new Set<string>();\n\n\t/**\n\t * Internal copy of the Redis client being used to read incoming payloads\n\t */\n\tprotected readonly streamReadClient: Redis;\n\n\t/**\n\t * The group being used by this broker.\n\t *\n\t * @privateRemarks\n\t * Stored as its own field to do the \"use random group\" resolution in the constructor.\n\t */\n\tprotected readonly group: string;\n\n\t/**\n\t * Whether this broker is currently polling events\n\t */\n\tprotected listening = false;\n\n\tpublic constructor(\n\t\tprotected readonly redisClient: Redis,\n\t\toptions: RedisBrokerOptions,\n\t) {\n\t\tsuper();\n\t\tthis.options = { ...DefaultRedisBrokerOptions, ...options };\n\t\tthis.group = this.options.group === kUseRandomGroupName ? randomBytes(16).toString('hex') : this.options.group;\n\t\tredisClient.defineCommand('xcleangroup', {\n\t\t\tnumberOfKeys: 1,\n\t\t\tlua: readFileSync(resolve(__dirname, '..', 'scripts', 'xcleangroup.lua'), 'utf8'),\n\t\t});\n\t\tthis.streamReadClient = redisClient.duplicate();\n\t}\n\n\t/**\n\t * {@inheritDoc IBaseBroker.subscribe}\n\t */\n\tpublic async subscribe(events: (keyof TEvents)[]): Promise<void> {\n\t\tawait Promise.all(\n\t\t\t// @ts-expect-error: Intended\n\t\t\tevents.map(async (event) => {\n\t\t\t\tthis.subscribedEvents.add(event as string);\n\t\t\t\ttry {\n\t\t\t\t\treturn await this.redisClient.xgroup('CREATE', event as string, this.group, 0, 'MKSTREAM');\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (!(error instanceof ReplyError)) {\n\t\t\t\t\t\tthrow error;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}),\n\t\t);\n\t\tvoid this.listen();\n\t}\n\n\t/**\n\t * {@inheritDoc IBaseBroker.unsubscribe}\n\t */\n\tpublic async unsubscribe(events: (keyof TEvents)[]): Promise<void> {\n\t\tconst commands: unknown[][] = Array.from({ length: events.length * 2 });\n\t\tfor (let idx = 0; idx < commands.length; idx += 2) {\n\t\t\tconst event = events[idx / 2];\n\t\t\tcommands[idx] = ['xgroup', 'delconsumer', event as string, this.options.group, this.options.name];\n\t\t\tcommands[idx + 1] = ['xcleangroup', event as string, this.options.group];\n\t\t}\n\n\t\tawait this.redisClient.pipeline(commands).exec();\n\n\t\tfor (const event of events) {\n\t\t\tthis.subscribedEvents.delete(event as string);\n\t\t}\n\t}\n\n\t/**\n\t * Begins polling for events, firing them to {@link BaseRedisBroker.emitEvent}\n\t */\n\tprotected async listen(): Promise<void> {\n\t\tif (this.listening) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.listening = true;\n\n\t\t// Enter regular polling\n\t\twhile (this.subscribedEvents.size > 0) {\n\t\t\ttry {\n\t\t\t\tawait this.claimAndEmitDeadEvents();\n\t\t\t} catch (error) {\n\t\t\t\t// @ts-expect-error: Intended\n\t\t\t\tthis.emit('error', error);\n\t\t\t\t// We don't break here to keep the loop running even if dead event processing fails\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\t// As per docs, '>' means \"give me a new message\"\n\t\t\t\tconst data = await this.readGroup('>', this.options.blockTimeout);\n\t\t\t\tif (!data) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tawait this.processMessages(data);\n\t\t\t} catch (error) {\n\t\t\t\t// @ts-expect-error: Intended\n\t\t\t\tthis.emit('error', error);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tthis.listening = false;\n\t}\n\n\tprivate async readGroup(fromId: string, block: number): Promise<RedisReadGroupData> {\n\t\tconst data = await this.streamReadClient.xreadgroupBuffer(\n\t\t\t'GROUP',\n\t\t\tthis.group,\n\t\t\tthis.options.name,\n\t\t\t'COUNT',\n\t\t\tString(this.options.maxChunk),\n\t\t\t'BLOCK',\n\t\t\tString(block),\n\t\t\t'STREAMS',\n\t\t\t...this.subscribedEvents,\n\t\t\t...Array.from({ length: this.subscribedEvents.size }, () => fromId),\n\t\t);\n\n\t\treturn data ?? [];\n\t}\n\n\tprivate async processMessages(data: RedisReadGroupData): Promise<void> {\n\t\tfor (const [event, messages] of data) {\n\t\t\tconst eventName = event.toString('utf8');\n\n\t\t\tfor (const [id, packet] of messages) {\n\t\t\t\tconst idx = packet.findIndex((value, idx) => value.toString('utf8') === 'data' && idx % 2 === 0);\n\t\t\t\tif (idx < 0) continue;\n\n\t\t\t\tconst payload = packet[idx + 1];\n\t\t\t\tif (!payload) continue;\n\n\t\t\t\tthis.emitEvent(id, this.group, eventName, this.options.decode(payload));\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async claimAndEmitDeadEvents(): Promise<void> {\n\t\tfor (const stream of this.subscribedEvents) {\n\t\t\t// Get up to N oldest pending messages (note: a pending message is a message that has been read, but never ACKed)\n\t\t\tconst pending = (await this.streamReadClient.xpending(\n\t\t\t\tstream,\n\t\t\t\tthis.group,\n\t\t\t\t'-',\n\t\t\t\t'+',\n\t\t\t\tthis.options.maxChunk,\n\t\t\t\t// See: https://redis.io/docs/latest/commands/xpending/#extended-form-of-xpending\n\t\t\t)) as [id: string, consumer: string, idleMs: number, deliveredTimes: number][];\n\n\t\t\tfor (const [id, consumer, idleMs, deliveredTimes] of pending) {\n\t\t\t\t// Technically xclaim checks for us anyway, but why not avoid an extra call?\n\t\t\t\tif (idleMs < this.options.messageIdleTime) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (deliveredTimes > this.options.maxDeliveredTimes) {\n\t\t\t\t\t// This message is dead. It has repeatedly failed being processed by a consumer.\n\t\t\t\t\tawait this.streamReadClient.xdel(stream, this.group, id);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Try to claim the message if we don't already own it (this may fail if another consumer has already claimed it)\n\t\t\t\tif (consumer !== this.options.name) {\n\t\t\t\t\tconst claimed = await this.streamReadClient.xclaimBuffer(\n\t\t\t\t\t\tstream,\n\t\t\t\t\t\tthis.group,\n\t\t\t\t\t\tthis.options.name,\n\t\t\t\t\t\tMath.max(this.options.messageIdleTime, 1),\n\t\t\t\t\t\tid,\n\t\t\t\t\t\t'JUSTID',\n\t\t\t\t\t);\n\n\t\t\t\t\t// Another consumer got the message before us\n\t\t\t\t\tif (!claimed?.length) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fetch message body\n\t\t\t\tconst entries = await this.streamReadClient.xrangeBuffer(stream, id, id);\n\t\t\t\t// No idea how this could happen, frankly!\n\t\t\t\tif (!entries?.length) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst [msgId, fields] = entries[0]!;\n\t\t\t\tconst idx = fields.findIndex((value, idx) => value.toString('utf8') === 'data' && idx % 2 === 0);\n\t\t\t\tif (idx < 0) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst payload = fields[idx + 1];\n\t\t\t\tif (!payload) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tthis.emitEvent(msgId, this.group, stream, this.options.decode(payload));\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Destroys the broker, closing all connections\n\t */\n\tpublic async destroy() {\n\t\tawait this.unsubscribe([...this.subscribedEvents]);\n\t\tthis.streamReadClient.disconnect();\n\t\tthis.redisClient.disconnect();\n\t}\n\n\t/**\n\t * Handles an incoming Redis event\n\t */\n\tprotected abstract emitEvent(id: Buffer, group: string, event: string, data: unknown): unknown;\n}\n","import { Buffer } from 'node:buffer';\nimport { encode, decode } from '@msgpack/msgpack';\nimport type { AsyncEventEmitter } from '@vladfrangu/async_event_emitter';\n\n/**\n * Base options for a broker implementation\n */\nexport interface BaseBrokerOptions {\n\t/**\n\t * Function to use for decoding messages\n\t */\n\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\tdecode?: (data: Buffer) => unknown;\n\t/**\n\t * Function to use for encoding messages\n\t */\n\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\tencode?: (data: unknown) => Buffer;\n}\n\n/**\n * Default broker options\n */\nexport const DefaultBrokerOptions = {\n\tencode: (data): Buffer => {\n\t\tconst encoded = encode(data);\n\t\treturn Buffer.from(encoded.buffer, encoded.byteOffset, encoded.byteLength);\n\t},\n\tdecode: (data): unknown => decode(data),\n} as const satisfies Required<BaseBrokerOptions>;\n\nexport type ToEventMap<\n\tTRecord extends Record<string, any[]>,\n\tTResponses extends Record<keyof TRecord, any> | undefined = undefined,\n> = {\n\t[TKey in keyof TRecord]: [\n\t\tevent: TResponses extends Record<keyof TRecord, any>\n\t\t\t? { ack(): Promise<void>; reply(data: TResponses[TKey]): Promise<void> }\n\t\t\t: { ack(): Promise<void>; data: TRecord[TKey] },\n\t];\n};\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface IBaseBroker<TEvents extends {}> {\n\t/**\n\t * Subscribes to the given events\n\t */\n\tsubscribe(events: (keyof TEvents)[]): Promise<void>;\n\t/**\n\t * Unsubscribes from the given events\n\t */\n\tunsubscribe(events: (keyof TEvents)[]): Promise<void>;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface IPubSubBroker<TEvents extends {}>\n\textends IBaseBroker<TEvents>,\n\t\tAsyncEventEmitter<ToEventMap<TEvents>> {\n\t/**\n\t * Publishes an event\n\t */\n\tpublish<Event extends keyof TEvents>(event: Event, data: TEvents[Event]): Promise<void>;\n}\n\nexport interface IRPCBroker<TEvents extends Record<string, any[]>, TResponses extends Record<keyof TEvents, any>>\n\textends IBaseBroker<TEvents>,\n\t\tAsyncEventEmitter<ToEventMap<TEvents, TResponses>> {\n\t/**\n\t * Makes an RPC call\n\t */\n\tcall<Event extends keyof TEvents>(\n\t\tevent: Event,\n\t\tdata: TEvents[Event],\n\t\ttimeoutDuration?: number,\n\t): Promise<TResponses[Event]>;\n}\n","import type { Buffer } from 'node:buffer';\nimport type { IPubSubBroker } from '../Broker.js';\nimport { BaseRedisBroker } from './BaseRedis.js';\n\n/**\n * PubSub broker powered by Redis\n *\n * @example\n * ```ts\n * // publisher.js\n * import { PubSubRedisBroker } from '@discordjs/brokers';\n * import Redis from 'ioredis';\n *\n * const broker = new PubSubRedisBroker(new Redis());\n *\n * await broker.publish('test', 'Hello World!');\n * await broker.destroy();\n *\n * // subscriber.js\n * import { PubSubRedisBroker } from '@discordjs/brokers';\n * import Redis from 'ioredis';\n *\n * const broker = new PubSubRedisBroker(new Redis());\n * \tbroker.on('test', ({ data, ack }) => {\n * \tconsole.log(data);\n * \tvoid ack();\n * });\n *\n * await broker.subscribe('subscribers', ['test']);\n * ```\n */\nexport class PubSubRedisBroker<TEvents extends Record<string, any>>\n\textends BaseRedisBroker<TEvents>\n\timplements IPubSubBroker<TEvents>\n{\n\t/**\n\t * {@inheritDoc IPubSubBroker.publish}\n\t */\n\tpublic async publish<Event extends keyof TEvents>(event: Event, data: TEvents[Event]): Promise<void> {\n\t\tawait this.redisClient.xadd(event as string, '*', BaseRedisBroker.STREAM_DATA_KEY, this.options.encode(data));\n\t}\n\n\tprotected emitEvent(id: Buffer, group: string, event: string, data: unknown) {\n\t\tconst payload: { ack(): Promise<void>; data: unknown } = {\n\t\t\tdata,\n\t\t\tack: async () => {\n\t\t\t\tawait this.redisClient.xack(event, group, id);\n\t\t\t},\n\t\t};\n\n\t\t// @ts-expect-error: Intended\n\t\tthis.emit(event, payload);\n\t}\n}\n","import type { Buffer } from 'node:buffer';\nimport { clearTimeout, setTimeout } from 'node:timers';\nimport type Redis from 'ioredis/built/Redis.js';\nimport type { IRPCBroker } from '../Broker.js';\nimport type { RedisBrokerOptions } from './BaseRedis.js';\nimport { BaseRedisBroker, DefaultRedisBrokerOptions } from './BaseRedis.js';\n\ninterface InternalPromise {\n\treject(error: any): void;\n\tresolve(data: any): void;\n\ttimeout: NodeJS.Timeout;\n}\n\n/**\n * Options specific for an RPC Redis broker\n */\nexport interface RPCRedisBrokerOptions extends RedisBrokerOptions {\n\ttimeout?: number;\n}\n\n/**\n * Default values used for the {@link RPCRedisBrokerOptions}\n */\nexport const DefaultRPCRedisBrokerOptions = {\n\t...DefaultRedisBrokerOptions,\n\ttimeout: 5_000,\n} as const satisfies Required<Omit<RPCRedisBrokerOptions, 'group' | 'name'>>;\n\n/**\n * RPC broker powered by Redis\n *\n * @example\n * ```ts\n * // caller.js\n * import { RPCRedisBroker } from '@discordjs/brokers';\n * import Redis from 'ioredis';\n *\n * const broker = new RPCRedisBroker(new Redis());\n *\n * console.log(await broker.call('testcall', 'Hello World!'));\n * await broker.destroy();\n *\n * // responder.js\n * import { RPCRedisBroker } from '@discordjs/brokers';\n * import Redis from 'ioredis';\n *\n * const broker = new RPCRedisBroker(new Redis());\n * broker.on('testcall', ({ data, ack, reply }) => {\n * \tconsole.log('responder', data);\n * \tvoid ack();\n * \tvoid reply(`Echo: ${data}`);\n * });\n *\n * await broker.subscribe('responders', ['testcall']);\n * ```\n */\nexport class RPCRedisBroker<TEvents extends Record<string, any[]>, TResponses extends Record<keyof TEvents, any>>\n\textends BaseRedisBroker<TEvents, TResponses>\n\timplements IRPCBroker<TEvents, TResponses>\n{\n\t/**\n\t * Options this broker is using\n\t */\n\tprotected override readonly options: Required<RPCRedisBrokerOptions>;\n\n\t/**\n\t * @internal\n\t */\n\tprotected readonly promises = new Map<string, InternalPromise>();\n\n\tpublic constructor(redisClient: Redis, options: RPCRedisBrokerOptions) {\n\t\tsuper(redisClient, options);\n\t\tthis.options = { ...DefaultRPCRedisBrokerOptions, ...options };\n\n\t\tthis.streamReadClient.on('messageBuffer', (channel: Buffer, message: Buffer) => {\n\t\t\tconst [, id] = channel.toString().split(':');\n\t\t\tif (id && this.promises.has(id)) {\n\t\t\t\t// eslint-disable-next-line @typescript-eslint/unbound-method\n\t\t\t\tconst { resolve, timeout } = this.promises.get(id)!;\n\t\t\t\tresolve(this.options.decode(message));\n\t\t\t\tclearTimeout(timeout);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * {@inheritDoc IRPCBroker.call}\n\t */\n\tpublic async call<Event extends keyof TEvents>(\n\t\tevent: Event,\n\t\tdata: TEvents[Event],\n\t\ttimeoutDuration: number = this.options.timeout,\n\t): Promise<TResponses[Event]> {\n\t\tconst id = await this.redisClient.xadd(\n\t\t\tevent as string,\n\t\t\t'*',\n\t\t\tBaseRedisBroker.STREAM_DATA_KEY,\n\t\t\tthis.options.encode(data),\n\t\t);\n\t\t// This id! assertion is valid. From redis docs:\n\t\t// \"The command returns a Null reply when used with the NOMKSTREAM option and the key doesn't exist.\"\n\t\t// See: https://redis.io/commands/xadd/\n\t\tconst rpcChannel = `${event as string}:${id!}`;\n\n\t\t// Construct the error here for better stack traces\n\t\tconst timedOut = new Error(`timed out after ${timeoutDuration}ms`);\n\n\t\tawait this.streamReadClient.subscribe(rpcChannel);\n\t\treturn new Promise<TResponses[Event]>((resolve, reject) => {\n\t\t\tconst timeout = setTimeout(() => reject(timedOut), timeoutDuration).unref();\n\n\t\t\tthis.promises.set(id!, { resolve, reject, timeout });\n\t\t\t// eslint-disable-next-line promise/prefer-await-to-then\n\t\t}).finally(() => {\n\t\t\tvoid this.streamReadClient.unsubscribe(rpcChannel);\n\t\t\tthis.promises.delete(id!);\n\t\t});\n\t}\n\n\tprotected emitEvent(id: Buffer, event: string, data: unknown) {\n\t\tconst payload: { ack(): Promise<void>; data: unknown; reply(data: unknown): Promise<void> } = {\n\t\t\tdata,\n\t\t\tack: async () => {\n\t\t\t\tawait this.redisClient.xack(event, this.group, id);\n\t\t\t},\n\t\t\treply: async (data) => {\n\t\t\t\tawait this.redisClient.publish(`${event}:${id.toString()}`, this.options.encode(data));\n\t\t\t},\n\t\t};\n\n\t\t// @ts-expect-error: Intended\n\t\tthis.emit(event, payload);\n\t}\n}\n","export * from './brokers/redis/BaseRedis.js';\nexport * from './brokers/redis/PubSubRedis.js';\nexport * from './brokers/redis/RPCRedis.js';\n\nexport * from './brokers/Broker.js';\n\n/**\n * The {@link https://github.com/discordjs/discord.js/blob/main/packages/brokers#readme | @discordjs/brokers} version\n * that you are currently using.\n *\n * @privateRemarks This needs to explicitly be `string` so it is not typed as a \"const string\" that gets injected by esbuild.\n */\nexport const version = '2.0.0-dev.1759622511-ffbb7b693' as string;\n"],"mappings":"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","names":["Buffer","Buffer","idx","resolve","data"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@discordjs/brokers",
4
- "version": "2.0.0-dev.1759363313-f510b5ffa",
4
+ "version": "2.0.0-dev.1759622511-ffbb7b693",
5
5
  "description": "Powerful set of message brokers",
6
6
  "exports": {
7
7
  ".": {