11import { EventEmitter } from 'events' ;
2- import { default as IORedis } from 'ioredis' ;
2+ import type { default as IORedis } from 'ioredis' ;
33import { ConnectionOptions , RedisOptions , RedisClient } from '../interfaces' ;
44import { IRedisClient } from '../interfaces/redis-client' ;
55import {
@@ -14,6 +14,8 @@ import { version as packageVersion } from '../version';
1414import * as scripts from '../scripts' ;
1515import { DatabaseType } from '../types' ;
1616import { createIORedisClient , isIRedisClient } from './ioredis-client' ;
17+ import { createNodeRedisClient } from './node-redis-client' ;
18+ import { createBunRedisClient } from './bun-redis-client' ;
1719import {
1820 ConnectionClosedError ,
1921 CONNECTION_CLOSED_ERROR_MSG ,
@@ -67,6 +69,87 @@ export interface RawCommand {
6769 keys : number ;
6870}
6971
72+ type IORedisModule = { default : typeof IORedis } ;
73+
74+ /**
75+ * Lazily loads the optional `ioredis` driver. Users on another Redis driver
76+ * (node-redis, Bun built-in, …) or on the PostgreSQL backend never hit this
77+ * path, so they never need `ioredis` installed.
78+ *
79+ * Only reached when no {@link RedisConnection.clientFactory} is set and the
80+ * caller did not pass an already-constructed client instance. In native ESM
81+ * environments where `require` is unavailable, callers should provide a client
82+ * instance or a `clientFactory` instead.
83+ */
84+ function loadIORedis ( ) : typeof IORedis {
85+ try {
86+ if ( typeof require === 'function' ) {
87+ const mod = require ( 'ioredis' ) as IORedisModule | typeof IORedis ;
88+ // ioredis exports the constructor both as the module itself (CJS) and
89+ // under `default` (ESM interop); normalise to the constructor.
90+ return ( mod as IORedisModule ) . default ?? ( mod as typeof IORedis ) ;
91+ }
92+ } catch {
93+ // Fall through to the friendly error below.
94+ }
95+ throw new Error (
96+ "BullMQ could not load the optional 'ioredis' package. " +
97+ 'Install it with `npm install ioredis`, or provide a different Redis ' +
98+ 'client instance (e.g. node-redis) via the connection option. In a ' +
99+ 'native ESM environment, pass an already-constructed client instance ' +
100+ 'instead of connection options.' ,
101+ ) ;
102+ }
103+
104+ /**
105+ * Wraps a raw client instance passed through the `connection` option in the
106+ * matching {@link IRedisClient} adapter, auto-detecting the underlying driver.
107+ *
108+ * This lets consumers pass a native node-redis or Bun client directly (without
109+ * manually calling `createNodeRedisClient` / `createBunRedisClient` or setting a
110+ * global {@link RedisConnection.clientFactory}), so those users never need
111+ * `ioredis` installed. ioredis instances keep their existing code path, so the
112+ * behaviour is fully backwards compatible.
113+ *
114+ * Detection is purely structural (no driver package is imported), keying off
115+ * markers that are unique to each client:
116+ * - ioredis exposes `defineCommand` (used to register Lua scripts);
117+ * node-redis and Bun do not.
118+ * - node-redis (`@redis/client`) exposes `sendCommand` plus `isOpen`/`isReady`.
119+ * - Bun's built-in `RedisClient` exposes `send` plus a `connected` flag.
120+ */
121+ function wrapRedisInstance ( instance : any ) : IRedisClient {
122+ // Already an adapted IRedisClient (ioredis proxy, node-redis, Bun, or a
123+ // custom implementation) — use as-is.
124+ if ( isIRedisClient ( instance ) ) {
125+ return instance ;
126+ }
127+
128+ const hasDefineCommand = typeof instance . defineCommand === 'function' ;
129+
130+ // node-redis (@redis/client): `sendCommand` + `isOpen`/`isReady`, and no
131+ // ioredis-style `defineCommand`.
132+ if (
133+ ! hasDefineCommand &&
134+ typeof instance . sendCommand === 'function' &&
135+ ( 'isOpen' in instance || 'isReady' in instance )
136+ ) {
137+ return createNodeRedisClient ( instance ) ;
138+ }
139+
140+ // Bun's built-in RedisClient: `send` + `connected`, and no `defineCommand`.
141+ if (
142+ ! hasDefineCommand &&
143+ typeof instance . send === 'function' &&
144+ 'connected' in instance
145+ ) {
146+ return createBunRedisClient ( instance ) ;
147+ }
148+
149+ // Default: treat as an ioredis instance (backwards compatible).
150+ return createIORedisClient ( instance ) ;
151+ }
152+
70153export class RedisConnection extends EventEmitter {
71154 static minimumVersion = '5.0.0' ;
72155 static recommendedMinimumVersion = '6.2.0' ;
@@ -156,10 +239,10 @@ export class RedisConnection extends EventEmitter {
156239 this . opts . maxRetriesPerRequest = null ;
157240 }
158241 } else {
159- // Wrap raw ioredis instances in the IRedisClient adapter if not already wrapped
160- this . _client = isIRedisClient ( opts )
161- ? opts
162- : createIORedisClient ( opts as any ) ;
242+ // Wrap raw client instances in the matching IRedisClient adapter,
243+ // auto-detecting the driver (ioredis / node-redis / Bun) so callers can
244+ // pass a native client directly without setting a clientFactory.
245+ this . _client = wrapRedisInstance ( opts ) ;
163246
164247 // Test if the redis instance is using keyPrefix
165248 // and if so, throw an error.
@@ -321,7 +404,10 @@ export class RedisConnection extends EventEmitter {
321404 this . _client = RedisConnection . clientFactory ( this . opts ) ;
322405 } else {
323406 const { url, ...rest } = this . opts ;
324- const ioredisClient = url ? new IORedis ( url , rest ) : new IORedis ( rest ) ;
407+ const IORedisCtor = loadIORedis ( ) ;
408+ const ioredisClient = url
409+ ? new IORedisCtor ( url , rest )
410+ : new IORedisCtor ( rest ) ;
325411 this . _client = createIORedisClient ( ioredisClient ) ;
326412 }
327413 }
0 commit comments