bullmq 5.77.0 → 5.77.2

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.
@@ -3,90 +3,101 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createIORedisClient = createIORedisClient;
4
4
  exports.isIRedisClient = isIRedisClient;
5
5
  /**
6
- * Augments an ioredis Redis / Cluster instance so that it conforms to
7
- * {@link IRedisClient}.
6
+ * Per-raw-client cache so repeated calls to `createIORedisClient` with the
7
+ * same underlying ioredis instance return the same proxy. This preserves
8
+ * event-listener identity for the BullMQ-facing client.
9
+ */
10
+ const proxyCache = new WeakMap();
11
+ /**
12
+ * Wraps an ioredis `Redis` / `Cluster` instance with a `Proxy` so it conforms
13
+ * to {@link IRedisClient}.
8
14
  *
9
- * Since ioredis already exposes every Redis command that BullMQ uses
10
- * (hgetall, zrange, xread, pipeline, multi, nodes, …), the adapter
11
- * only needs to:
15
+ * For backwards compatibility BullMQ continues to accept a raw `IORedis`
16
+ * instance through tehe `connection` option, even though internally it relies
17
+ * on the `IRedisClient` adapter interface. The returned proxy:
12
18
  *
13
- * - `runCommand` execute a previously defined Lua script by name
14
- * - `duplicate` – ensure the returned instance is also augmented
15
- * - `pipeline/multi` add `runCommand` to the returned ChainableCommander
16
- * - translate structured option objects to ioredis varargs where needed
19
+ * - exposes `runCommand` (Lua script dispatch by name)
20
+ * - exposes structured-options variants of `hset`, `set`, `zrange`,
21
+ * `zrevrange`, `xadd`, `xread`, `xtrim`, `scan` (backward-compatible:
22
+ * they still accept native ioredis varargs if called that way)
23
+ * - returns augmented {@link IRedisTransaction}s from `pipeline()` / `multi()`
24
+ * - wraps the result of `duplicate()` in a new proxy
17
25
  *
18
- * **Side-effect warning:** This function mutates the provided instance
19
- * in-place for zero-overhead augmentation. When a user passes their own
20
- * ioredis client to BullMQ, the overrides are written onto that shared
21
- * object. All overrides are **backward-compatible**: they detect whether
22
- * they are called with the ioredis native varargs style or the IRedisClient
23
- * structured-options style and dispatch accordingly. External code that
24
- * calls e.g. `client.hset(key, 'f1', 'v1')` will continue to work after
25
- * augmentation.
26
+ * The underlying ioredis instance is **not** mutated. Properties and methods
27
+ * not in the override table are forwarded to the raw client via the proxy
28
+ * traps, with `this === target` so EventEmitter / Commander internals work
29
+ * normally.
26
30
  */
27
31
  function createIORedisClient(client) {
28
- const adapter = client;
29
- if (adapter.__bullmq_iredis) {
30
- return adapter;
32
+ // If the caller already passed a proxy produced by this function, return
33
+ // it as-is. Wrapping a proxy in a second proxy would defeat the WeakMap
34
+ // cache (the inner raw client is no longer reachable from the outer
35
+ // argument) and break listener-identity / equality checks for callers
36
+ // that hold on to the original wrapper.
37
+ if (client.__bullmq_iredis === true) {
38
+ return client;
31
39
  }
32
- adapter.__bullmq_iredis = true;
33
- // Ensure isCluster is a boolean. ioredis Cluster sets it to true;
34
- // plain Redis leaves it undefined.
35
- if (typeof adapter.isCluster !== 'boolean') {
36
- adapter.isCluster = false;
40
+ const cached = proxyCache.get(client);
41
+ if (cached) {
42
+ return cached;
37
43
  }
38
- // Lua script engine
39
- adapter.runCommand = function (name, args) {
40
- return adapter[name](args);
44
+ const isCluster = client.isCluster === true;
45
+ // Cache bound prototype methods so the returned function identity is
46
+ // stable across accesses (important for `once`/`removeListener` patterns).
47
+ const boundCache = new Map();
48
+ // Override table — properties returned by the proxy without touching the
49
+ // underlying ioredis instance. The arrow functions close over `client`
50
+ // directly so ioredis internals always see the raw instance as `this`.
51
+ const overrides = Object.create(null);
52
+ overrides.__bullmq_iredis = true;
53
+ overrides.isCluster = isCluster;
54
+ // Lua script engine.
55
+ overrides.runCommand = (name, args) => {
56
+ return client[name](args);
41
57
  };
42
- // Pipeline / Multi — add runCommand + structured overrides
43
- const origPipeline = client.pipeline.bind(client);
44
- adapter.pipeline = function (...args) {
45
- return augmentTransaction(origPipeline(...args));
58
+ // Pipeline / Multi — wrap the ChainableCommander with structured overrides.
59
+ overrides.pipeline = (...args) => {
60
+ return augmentTransaction(client.pipeline(...args));
46
61
  };
47
- const origMulti = client.multi.bind(client);
48
- adapter.multi = function (...args) {
49
- return augmentTransaction(origMulti(...args));
62
+ overrides.multi = (...args) => {
63
+ return augmentTransaction(client.multi(...args));
50
64
  };
51
- // Duplicateensure the result is also augmented.
65
+ // duplicatewrap the new raw client with a fresh proxy.
52
66
  // ioredis Cluster.duplicate(startupNodes?, options?) expects connection
53
67
  // options under `redisOptions`, while Redis.duplicate(options?) takes them
54
- // at the top level. We normalise the call so that callers can always pass
55
- // a simple `{ connectionName }` object regardless of the client type.
68
+ // at the top level. Normalise so callers can always pass `{ connectionName }`.
56
69
  if (typeof client.duplicate === 'function') {
57
- const origDuplicate = client.duplicate.bind(client);
58
- const clientIsCluster = !!client.isCluster;
59
- adapter.duplicate = function (opts) {
70
+ overrides.duplicate = (opts) => {
60
71
  var _a;
61
- if (clientIsCluster) {
72
+ if (isCluster) {
62
73
  const existingRedisOpts = ((_a = client.options) === null || _a === void 0 ? void 0 : _a.redisOptions) || {};
63
74
  const mergedRedisOpts = opts
64
75
  ? Object.assign(Object.assign({}, existingRedisOpts), opts) : existingRedisOpts;
65
- return createIORedisClient(origDuplicate(undefined, { redisOptions: mergedRedisOpts }));
76
+ return createIORedisClient(client.duplicate(undefined, {
77
+ redisOptions: mergedRedisOpts,
78
+ }));
66
79
  }
67
- return createIORedisClient(origDuplicate(opts));
80
+ return createIORedisClient(client.duplicate(opts));
68
81
  };
69
82
  }
70
83
  // --- Structured → ioredis varargs translations ---
84
+ // Each override accepts both the IRedisClient structured-options form and
85
+ // the native ioredis varargs form, dispatching by argument shape.
71
86
  // hset: structured { f1: v1 } → ioredis hset(key, f1, v1, …)
72
- // Backward-compatible: if second arg is a string, caller is using ioredis varargs.
73
- const origHset = client.hset.bind(client);
74
- adapter.hset = function (key, dataOrField, ...rest) {
87
+ overrides.hset = (key, dataOrField, ...rest) => {
75
88
  if (typeof dataOrField === 'string') {
76
- return origHset(key, dataOrField, ...rest);
89
+ return client.hset(key, dataOrField, ...rest);
77
90
  }
78
91
  const args = [key];
79
92
  for (const [f, v] of Object.entries(dataOrField)) {
80
93
  args.push(f, v);
81
94
  }
82
- return origHset(...args);
95
+ return client.hset(...args);
83
96
  };
84
97
  // set: structured { PX?: n } → ioredis set(key, value, 'PX', n)
85
- // Backward-compatible: if third arg is a string, caller is using ioredis varargs.
86
- const origSet = client.set.bind(client);
87
- adapter.set = function (key, value, optionsOrModifier, ...rest) {
98
+ overrides.set = (key, value, optionsOrModifier, ...rest) => {
88
99
  if (typeof optionsOrModifier === 'string' || optionsOrModifier == null) {
89
- return origSet(key, value, ...(optionsOrModifier != null ? [optionsOrModifier, ...rest] : []));
100
+ return client.set(key, value, ...(optionsOrModifier != null ? [optionsOrModifier, ...rest] : []));
90
101
  }
91
102
  const args = [key, value];
92
103
  if (optionsOrModifier.PX != null) {
@@ -95,39 +106,32 @@ function createIORedisClient(client) {
95
106
  else if (optionsOrModifier.EX != null) {
96
107
  args.push('EX', optionsOrModifier.EX);
97
108
  }
98
- return origSet(...args);
109
+ return client.set(...args);
99
110
  };
100
111
  // zrange: structured { WITHSCORES? } → ioredis zrange(key, start, end, 'WITHSCORES')
101
- // Backward-compatible: if fourth arg is a string, caller is using ioredis varargs.
102
- const origZrange = client.zrange.bind(client);
103
- adapter.zrange = function (key, start, end, optionsOrStr, ...rest) {
112
+ overrides.zrange = (key, start, end, optionsOrStr, ...rest) => {
104
113
  if (typeof optionsOrStr === 'string') {
105
- return origZrange(key, start, end, optionsOrStr, ...rest);
114
+ return client.zrange(key, start, end, optionsOrStr, ...rest);
106
115
  }
107
116
  if (optionsOrStr === null || optionsOrStr === void 0 ? void 0 : optionsOrStr.WITHSCORES) {
108
- return origZrange(key, start, end, 'WITHSCORES');
117
+ return client.zrange(key, start, end, 'WITHSCORES');
109
118
  }
110
- return origZrange(key, start, end);
119
+ return client.zrange(key, start, end);
111
120
  };
112
121
  // zrevrange: structured { WITHSCORES? } → ioredis zrevrange(key, start, end, 'WITHSCORES')
113
- // Backward-compatible: if fourth arg is a string, caller is using ioredis varargs.
114
- const origZrevrange = client.zrevrange.bind(client);
115
- adapter.zrevrange = function (key, start, end, optionsOrStr, ...rest) {
122
+ overrides.zrevrange = (key, start, end, optionsOrStr, ...rest) => {
116
123
  if (typeof optionsOrStr === 'string') {
117
- return origZrevrange(key, start, end, optionsOrStr, ...rest);
124
+ return client.zrevrange(key, start, end, optionsOrStr, ...rest);
118
125
  }
119
126
  if (optionsOrStr === null || optionsOrStr === void 0 ? void 0 : optionsOrStr.WITHSCORES) {
120
- return origZrevrange(key, start, end, 'WITHSCORES');
127
+ return client.zrevrange(key, start, end, 'WITHSCORES');
121
128
  }
122
- return origZrevrange(key, start, end);
129
+ return client.zrevrange(key, start, end);
123
130
  };
124
131
  // xadd: structured (key, id, { field: value }, { MAXLEN? }) → ioredis varargs
125
- // Backward-compatible: if third arg is a string, caller is using ioredis varargs
126
- // (e.g. xadd(key, id, field1, val1, field2, val2)).
127
- const origXadd = client.xadd.bind(client);
128
- adapter.xadd = function (key, idOrModifier, fieldsOrArg, ...rest) {
132
+ overrides.xadd = (key, idOrModifier, fieldsOrArg, ...rest) => {
129
133
  if (typeof fieldsOrArg === 'string') {
130
- return origXadd(key, idOrModifier, fieldsOrArg, ...rest);
134
+ return client.xadd(key, idOrModifier, fieldsOrArg, ...rest);
131
135
  }
132
136
  const options = rest[0];
133
137
  const args = [key];
@@ -142,15 +146,12 @@ function createIORedisClient(client) {
142
146
  for (const [f, v] of Object.entries(fieldsOrArg)) {
143
147
  args.push(f, v);
144
148
  }
145
- return origXadd(...args);
149
+ return client.xadd(...args);
146
150
  };
147
151
  // xread: structured ([{ key, id }], { BLOCK?, COUNT? }) → ioredis varargs
148
- // Backward-compatible: if first arg is a string, caller is using ioredis varargs
149
- // (e.g. xread('BLOCK', 5000, 'STREAMS', key, id)).
150
- const origXread = client.xread.bind(client);
151
- adapter.xread = function (streamsOrModifier, ...rest) {
152
+ overrides.xread = (streamsOrModifier, ...rest) => {
152
153
  if (typeof streamsOrModifier === 'string') {
153
- return origXread(streamsOrModifier, ...rest);
154
+ return client.xread(streamsOrModifier, ...rest);
154
155
  }
155
156
  const options = rest[0];
156
157
  const args = [];
@@ -167,15 +168,12 @@ function createIORedisClient(client) {
167
168
  for (const s of streamsOrModifier) {
168
169
  args.push(s.id);
169
170
  }
170
- return origXread(...args);
171
+ return client.xread(...args);
171
172
  };
172
173
  // xtrim: structured (key, 'MAXLEN', threshold, { approximate? })
173
- // ioredis native is the same positional shape, so this is already compatible.
174
- const origXtrim = client.xtrim.bind(client);
175
- adapter.xtrim = function (key, strategy, thresholdOrApprox, ...rest) {
176
- // Varargs passthrough: xtrim(key, 'MAXLEN', '~', 1000) or xtrim(key, 'MAXLEN', 1000)
174
+ overrides.xtrim = (key, strategy, thresholdOrApprox, ...rest) => {
177
175
  if (typeof thresholdOrApprox === 'string' || rest.length === 0) {
178
- return origXtrim(key, strategy, thresholdOrApprox, ...rest);
176
+ return client.xtrim(key, strategy, thresholdOrApprox, ...rest);
179
177
  }
180
178
  const options = rest[0];
181
179
  const args = [key, strategy];
@@ -183,35 +181,21 @@ function createIORedisClient(client) {
183
181
  args.push('~');
184
182
  }
185
183
  args.push(thresholdOrApprox);
186
- return origXtrim(...args);
187
- };
188
- // bzpopmin
189
- //
190
- // We deliberately do NOT override ioredis' native `bzpopmin` here.
191
- // ioredis returns `[key, member, score]` (a tuple) and that is also the
192
- // shape required by IRedisClient. Overriding the method on a shared user
193
- // instance would change the observable return shape for any non-BullMQ
194
- // code that uses `bzpopmin` on the same client.
195
- // clientSetName / clientList
196
- adapter.clientSetName = function (name) {
197
- return client.client('SETNAME', name);
184
+ return client.xtrim(...args);
198
185
  };
199
- adapter.clientList = function () {
200
- return client.client('LIST');
201
- };
202
- // scan(cursor, { MATCH?, COUNT? })
203
- // Must detect if called with varargs (ioredis internal, e.g. from scanStream)
204
- // vs. structured options (IRedisClient interface).
205
- const origScan = client.scan.bind(client);
206
- adapter.scan = function (cursor, ...rest) {
207
- // If called with varargs style (e.g. from scanStream internally),
208
- // pass through unchanged.
186
+ // bzpopmin is not overridden — ioredis already returns
187
+ // `[key, member, score]`, which matches IRedisClient.
188
+ // clientSetName / clientList helpers that forward to CLIENT subcommands.
189
+ overrides.clientSetName = (name) => client.client('SETNAME', name);
190
+ overrides.clientList = () => client.client('LIST');
191
+ // scan(cursor, { MATCH?, COUNT? }) — accepts either structured options or
192
+ // ioredis varargs (used internally by `scanStream`).
193
+ overrides.scan = (cursor, ...rest) => {
209
194
  if (rest.length === 0 ||
210
195
  typeof rest[0] === 'string' ||
211
196
  typeof rest[0] === 'function') {
212
- return origScan(cursor, ...rest);
197
+ return client.scan(cursor, ...rest);
213
198
  }
214
- // Structured options: { MATCH?, COUNT? }
215
199
  const options = rest[0];
216
200
  const args = [cursor];
217
201
  if ((options === null || options === void 0 ? void 0 : options.MATCH) != null) {
@@ -220,9 +204,65 @@ function createIORedisClient(client) {
220
204
  if ((options === null || options === void 0 ? void 0 : options.COUNT) != null) {
221
205
  args.push('COUNT', options.COUNT);
222
206
  }
223
- return origScan(...args);
207
+ return client.scan(...args);
224
208
  };
225
- return adapter;
209
+ const proxy = new Proxy(client, {
210
+ get(target, prop) {
211
+ if (prop in overrides) {
212
+ return overrides[prop];
213
+ }
214
+ // Read against the raw target so getters on the prototype (e.g.
215
+ // ioredis' EventEmitter internals) see `this === target` rather than
216
+ // the proxy. This avoids infinite recursion through the proxy traps.
217
+ const value = Reflect.get(target, prop, target);
218
+ if (typeof value !== 'function') {
219
+ return value;
220
+ }
221
+ // Own properties (including ioredis commands installed via
222
+ // `defineCommand` and test-time spies set via `obj.method = spy`)
223
+ // are bound fresh on each access so reassignment is honoured.
224
+ if (Object.prototype.hasOwnProperty.call(target, prop)) {
225
+ return value.bind(target);
226
+ }
227
+ // Prototype methods (EventEmitter, Commander, ...) are cached so
228
+ // identity is stable across accesses.
229
+ const cachedBound = boundCache.get(prop);
230
+ if (cachedBound !== undefined) {
231
+ return cachedBound;
232
+ }
233
+ const bound = value.bind(target);
234
+ boundCache.set(prop, bound);
235
+ return bound;
236
+ },
237
+ set(target, prop, value) {
238
+ // Two assignment paths:
239
+ // - Properties present in the override table are reassigned in the
240
+ // table itself, so subsequent `get` traps return the new value
241
+ // (used by sinon-style spies that stub `runCommand`, `pipeline`,
242
+ // etc. on the proxy).
243
+ // - All other properties are written through to the raw ioredis
244
+ // instance via `Reflect.set`, and any stale bound-method entry is
245
+ // invalidated so the next access rebinds the new function.
246
+ if (prop in overrides) {
247
+ overrides[prop] = value;
248
+ return true;
249
+ }
250
+ boundCache.delete(prop);
251
+ return Reflect.set(target, prop, value);
252
+ },
253
+ deleteProperty(target, prop) {
254
+ if (prop in overrides) {
255
+ return false;
256
+ }
257
+ boundCache.delete(prop);
258
+ return Reflect.deleteProperty(target, prop);
259
+ },
260
+ has(target, prop) {
261
+ return prop in overrides || Reflect.has(target, prop);
262
+ },
263
+ });
264
+ proxyCache.set(client, proxy);
265
+ return proxy;
226
266
  }
227
267
  /**
228
268
  * Adds `runCommand` and structured overrides to an ioredis ChainableCommander
@@ -275,7 +315,7 @@ function isIRedisClient(obj) {
275
315
  if (!obj || typeof obj !== 'object') {
276
316
  return false;
277
317
  }
278
- // Fast path for in-place ioredis augmentation.
318
+ // Fast path for ioredis instances already wrapped by `createIORedisClient`.
279
319
  if (obj.__bullmq_iredis === true) {
280
320
  return true;
281
321
  }