769 lines
28 KiB
JavaScript
769 lines
28 KiB
JavaScript
import { __asyncValues } from "tslib";
|
||
import { createHash } from 'crypto';
|
||
import { EventEmitter } from 'events';
|
||
import { Readable } from 'stream';
|
||
import { ConnectionClosedError } from './errors/connection-closed-error';
|
||
function normalizeScriptArgs(args) {
|
||
return args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
||
}
|
||
function isConnectionClosedError(err) {
|
||
return ((err === null || err === void 0 ? void 0 : err.message) === 'Disconnects client' ||
|
||
(err === null || err === void 0 ? void 0 : err.message) === 'The client is closed' ||
|
||
(err === null || err === void 0 ? void 0 : err.message) === 'Connection is closed.');
|
||
}
|
||
export function createNodeRedisClient(client) {
|
||
return new NodeRedisAdapter(client);
|
||
}
|
||
/**
|
||
* Full wrapper (not augmentation) because node-redis's API is structurally
|
||
* different from ioredis and cannot be patched in-place.
|
||
*/
|
||
class NodeRedisAdapter extends EventEmitter {
|
||
/**
|
||
* Expose connection status using the vocabulary that
|
||
* {@link RedisConnection.waitUntilReady} expects:
|
||
* 'wait' → not yet connected, call connect()
|
||
* 'ready' → usable
|
||
* 'end' → permanently closed
|
||
*/
|
||
get status() {
|
||
if (this.statusOverride) {
|
||
return this.statusOverride;
|
||
}
|
||
if (this.raw.isReady) {
|
||
return 'ready';
|
||
}
|
||
if (this.raw.isOpen) {
|
||
return 'connect';
|
||
}
|
||
// Distinguish "never connected" from "disconnected after use"
|
||
return this.hasConnected ? 'end' : 'wait';
|
||
}
|
||
set status(val) {
|
||
// Allow RedisConnection to forcibly set 'end'
|
||
if (val === 'end') {
|
||
this.destroying = true;
|
||
if (this.raw.isOpen) {
|
||
try {
|
||
this.raw.quit().catch(() => { });
|
||
}
|
||
catch (_a) {
|
||
// already closed
|
||
}
|
||
}
|
||
}
|
||
this.statusOverride = val;
|
||
}
|
||
get options() {
|
||
var _a;
|
||
return (_a = this.raw.options) !== null && _a !== void 0 ? _a : {};
|
||
}
|
||
set options(val) {
|
||
// no-op – callers sometimes assign
|
||
}
|
||
constructor(raw) {
|
||
super();
|
||
this.raw = raw;
|
||
this.scripts = new Map();
|
||
this.hasConnected = false;
|
||
this.destroying = false;
|
||
this.isCluster = false; // TODO: cluster support
|
||
// Track first connection so status can distinguish 'wait' vs 'end'.
|
||
// When a connectionName is set (via duplicate()), delay the 'ready'
|
||
// event until CLIENT SETNAME completes so that callers waiting for
|
||
// 'ready' (e.g. RedisConnection.waitUntilReady) see the name already
|
||
// applied.
|
||
raw.on('ready', () => {
|
||
this.hasConnected = true;
|
||
if (this.connectionName) {
|
||
this.raw.clientSetName(this.connectionName).then(() => this.emit('ready'), () => this.emit('ready'));
|
||
}
|
||
else {
|
||
this.emit('ready');
|
||
}
|
||
});
|
||
raw.on('error', (err) => {
|
||
// Suppress the expected DisconnectsClientError that node-redis emits
|
||
// when destroy() is called intentionally (e.g. during close/disconnect).
|
||
if (this.destroying && isConnectionClosedError(err)) {
|
||
return;
|
||
}
|
||
this.emit('error', err);
|
||
});
|
||
raw.on('end', () => this.emit('close'));
|
||
raw.on('reconnecting', () => this.emit('reconnecting'));
|
||
// Auto-connect eagerly, like ioredis does in its constructor.
|
||
// This ensures commands can be issued immediately without an
|
||
// explicit connect() call. The promise is stored so that
|
||
// connect() is idempotent and callers can still await it.
|
||
if (!raw.isOpen) {
|
||
this.connectPromise = raw.connect().then(() => {
|
||
this.connectPromise = undefined;
|
||
}, (err) => {
|
||
this.connectPromise = undefined;
|
||
// Don't throw — errors surface via the 'error' event.
|
||
});
|
||
}
|
||
}
|
||
// ---------------------------------------------------------------
|
||
// Connection lifecycle
|
||
// ---------------------------------------------------------------
|
||
async connect() {
|
||
if (this.connectPromise) {
|
||
return this.connectPromise;
|
||
}
|
||
if (!this.raw.isOpen) {
|
||
this.connectPromise = this.raw.connect().then(() => {
|
||
this.connectPromise = undefined;
|
||
}, (err) => {
|
||
this.connectPromise = undefined;
|
||
throw err;
|
||
});
|
||
return this.connectPromise;
|
||
}
|
||
if (!this.raw.isReady) {
|
||
await new Promise((resolve, reject) => {
|
||
const onReady = () => {
|
||
cleanup();
|
||
resolve();
|
||
};
|
||
const onError = (err) => {
|
||
cleanup();
|
||
reject(err);
|
||
};
|
||
const onEnd = () => {
|
||
cleanup();
|
||
reject(new Error('Connection ended before ready event'));
|
||
};
|
||
const cleanup = () => {
|
||
this.off('ready', onReady);
|
||
this.off('error', onError);
|
||
this.off('end', onEnd);
|
||
};
|
||
this.once('ready', onReady);
|
||
this.once('error', onError);
|
||
this.once('end', onEnd);
|
||
});
|
||
}
|
||
}
|
||
disconnect(reconnect = false) {
|
||
this.destroying = true;
|
||
if (!reconnect) {
|
||
this.statusOverride = 'end';
|
||
}
|
||
try {
|
||
if (this.raw.isOpen) {
|
||
// Use destroy() for immediate teardown. This interrupts any pending
|
||
// blocking commands (e.g. BZPOPMIN) without waiting for them to
|
||
// complete. The resulting "Disconnects client" rejections are handled
|
||
// by BullMQ's isNotConnectionError() checks.
|
||
this.raw.destroy();
|
||
}
|
||
}
|
||
catch (_a) {
|
||
// Swallow errors from already-closed connections
|
||
}
|
||
this.emit('close');
|
||
if (reconnect) {
|
||
this.statusOverride = undefined;
|
||
this.emit('reconnecting');
|
||
this.connect()
|
||
.catch(err => {
|
||
if (!isConnectionClosedError(err)) {
|
||
this.emit('error', err);
|
||
}
|
||
})
|
||
.finally(() => {
|
||
this.destroying = false;
|
||
});
|
||
}
|
||
else {
|
||
// Emit both 'close' and 'end' so that all listeners are unblocked.
|
||
// RedisConnection.close() listens for 'close', RedisConnection.disconnect() listens for 'end'.
|
||
this.emit('end');
|
||
}
|
||
}
|
||
async quit() {
|
||
if (this.destroying || this.statusOverride === 'end') {
|
||
setImmediate(() => {
|
||
this.emit('end');
|
||
this.emit('close');
|
||
});
|
||
return 'OK';
|
||
}
|
||
this.destroying = true;
|
||
try {
|
||
if (this.raw.isOpen) {
|
||
try {
|
||
await this.raw.quit();
|
||
}
|
||
catch (_a) {
|
||
// Swallow errors from already-closing connections
|
||
}
|
||
}
|
||
}
|
||
catch (_b) {
|
||
// Swallow errors from already-closing connections
|
||
}
|
||
this.statusOverride = 'end';
|
||
// Emit on next tick so callers can register listeners after await quit()
|
||
setImmediate(() => {
|
||
this.emit('end');
|
||
this.emit('close');
|
||
});
|
||
return 'OK';
|
||
}
|
||
duplicate(...args) {
|
||
const dup = this.raw.duplicate();
|
||
const adapter = new NodeRedisAdapter(dup);
|
||
// Copy registered scripts to the duplicate
|
||
for (const [name, script] of this.scripts) {
|
||
adapter.scripts.set(name, script);
|
||
adapter[name] = (...args) => adapter.runCommand(name, args);
|
||
}
|
||
// Handle connectionName option (ioredis calls CLIENT SETNAME automatically).
|
||
// Setting connectionName BEFORE auto-connect resolves ensures the
|
||
// constructor's 'ready' handler applies CLIENT SETNAME before emitting
|
||
// 'ready', so callers (like RedisConnection.waitUntilReady) see the name
|
||
// already set when the connection is reported as ready.
|
||
const opts = args[0];
|
||
if (opts && typeof opts === 'object' && opts.connectionName) {
|
||
adapter.connectionName = opts.connectionName;
|
||
}
|
||
return adapter;
|
||
}
|
||
// ---------------------------------------------------------------
|
||
// Lua script engine
|
||
// ---------------------------------------------------------------
|
||
defineCommand(name, definition) {
|
||
const sha = createHash('sha1').update(definition.lua).digest('hex');
|
||
this.scripts.set(name, {
|
||
sha,
|
||
lua: definition.lua,
|
||
numberOfKeys: definition.numberOfKeys,
|
||
});
|
||
// Mimic ioredis behavior: add a callable property on the instance
|
||
// so that `(client as any)[name]` is defined (used by ScriptLoader cache check)
|
||
this[name] = (...args) => this.runCommand(name, args);
|
||
// Pre-load the script into Redis so that EVALSHA in transactions works
|
||
// immediately. This mirrors what ioredis does under the hood.
|
||
this.raw.scriptLoad(definition.lua).catch(() => {
|
||
// Ignore errors here – runCommand has NOSCRIPT fallback for non-tx path
|
||
});
|
||
}
|
||
async runCommand(name, args) {
|
||
var _a, _b;
|
||
const script = this.scripts.get(name);
|
||
if (!script) {
|
||
throw new Error(`BullMQ: unknown command "${name}"`);
|
||
}
|
||
const commandArgs = normalizeScriptArgs(args);
|
||
const { sha, lua, numberOfKeys } = script;
|
||
const keys = commandArgs.slice(0, numberOfKeys).map(String);
|
||
// Preserve Buffer arguments (e.g. msgpack data) – only stringify non-Buffers.
|
||
// Convert undefined/null to '' to match ioredis behavior (keeps arg positions).
|
||
const argv = commandArgs.slice(numberOfKeys).map((a) => {
|
||
if (Buffer.isBuffer(a)) {
|
||
return a;
|
||
}
|
||
if (a === undefined || a === null) {
|
||
return '';
|
||
}
|
||
return String(a);
|
||
});
|
||
try {
|
||
return await this.raw.evalSha(sha, { keys, arguments: argv });
|
||
}
|
||
catch (err) {
|
||
if (this.destroying && isConnectionClosedError(err)) {
|
||
return null;
|
||
}
|
||
if (isConnectionClosedError(err)) {
|
||
throw new ConnectionClosedError(err.message, err);
|
||
}
|
||
// NOSCRIPT – fall back to EVAL which also caches the script
|
||
if ((_b = (_a = err === null || err === void 0 ? void 0 : err.message) === null || _a === void 0 ? void 0 : _a.includes) === null || _b === void 0 ? void 0 : _b.call(_a, 'NOSCRIPT')) {
|
||
try {
|
||
return await this.raw.eval(lua, { keys, arguments: argv });
|
||
}
|
||
catch (evalErr) {
|
||
if (this.destroying && isConnectionClosedError(evalErr)) {
|
||
return null;
|
||
}
|
||
if (isConnectionClosedError(evalErr)) {
|
||
throw new ConnectionClosedError(evalErr.message, evalErr);
|
||
}
|
||
throw evalErr;
|
||
}
|
||
}
|
||
throw err;
|
||
}
|
||
}
|
||
// ---------------------------------------------------------------
|
||
// Pipeline / Transaction
|
||
// ---------------------------------------------------------------
|
||
multi() {
|
||
return new NodeRedisTransaction(this.raw.multi(), this.scripts);
|
||
}
|
||
pipeline() {
|
||
// node-redis doesn't have a separate pipeline concept;
|
||
// use multi() which behaves similarly for batching.
|
||
return this.multi();
|
||
}
|
||
// ---------------------------------------------------------------
|
||
// Hash commands
|
||
// ---------------------------------------------------------------
|
||
async hgetall(key) {
|
||
const result = await this.raw.hGetAll(key);
|
||
return result !== null && result !== void 0 ? result : {};
|
||
}
|
||
async hget(key, field) {
|
||
var _a;
|
||
return (_a = (await this.raw.hGet(key, field))) !== null && _a !== void 0 ? _a : null;
|
||
}
|
||
async hmget(key, ...fields) {
|
||
const result = await this.raw.hmGet(key, fields);
|
||
return result.map((v) => v !== null && v !== void 0 ? v : null);
|
||
}
|
||
async hset(key, dataOrField, ...rest) {
|
||
if (typeof dataOrField === 'object') {
|
||
// Record-based call: hset(key, { field: value, ... })
|
||
return await this.raw.hSet(key, dataOrField);
|
||
}
|
||
// Varargs call (ioredis compat): hset(key, field, value, field, value, ...)
|
||
const record = {};
|
||
record[dataOrField] = String(rest[0]);
|
||
for (let i = 1; i < rest.length; i += 2) {
|
||
record[String(rest[i])] = String(rest[i + 1]);
|
||
}
|
||
return await this.raw.hSet(key, record);
|
||
}
|
||
async hdel(key, ...fields) {
|
||
return await this.raw.hDel(key, fields);
|
||
}
|
||
async hexists(key, field) {
|
||
const exists = await this.raw.hExists(key, field);
|
||
return exists ? 1 : 0;
|
||
}
|
||
// ---------------------------------------------------------------
|
||
// String commands
|
||
// ---------------------------------------------------------------
|
||
async get(key) {
|
||
var _a;
|
||
return (_a = (await this.raw.get(key))) !== null && _a !== void 0 ? _a : null;
|
||
}
|
||
async set(key, value, options) {
|
||
const opts = {};
|
||
if ((options === null || options === void 0 ? void 0 : options.PX) != null) {
|
||
opts.PX = options.PX;
|
||
}
|
||
else if ((options === null || options === void 0 ? void 0 : options.EX) != null) {
|
||
opts.EX = options.EX;
|
||
}
|
||
return await this.raw.set(key, String(value), opts);
|
||
}
|
||
async del(...keys) {
|
||
if (keys.length === 0) {
|
||
return 0;
|
||
}
|
||
return await this.raw.del(keys);
|
||
}
|
||
// ---------------------------------------------------------------
|
||
// Sorted set commands
|
||
// ---------------------------------------------------------------
|
||
async zrange(key, start, end, options) {
|
||
if (options === null || options === void 0 ? void 0 : options.WITHSCORES) {
|
||
// node-redis v5 uses a separate method for WITHSCORES
|
||
const items = await this.raw.zRangeWithScores(key, start, end);
|
||
// Flatten to [member, score, member, score, …] like ioredis
|
||
const flat = [];
|
||
for (const item of items) {
|
||
flat.push(item.value, String(item.score));
|
||
}
|
||
return flat;
|
||
}
|
||
return await this.raw.zRange(key, start, end);
|
||
}
|
||
async zrevrange(key, start, end, options) {
|
||
if (options === null || options === void 0 ? void 0 : options.WITHSCORES) {
|
||
const items = await this.raw.zRangeWithScores(key, start, end, {
|
||
REV: true,
|
||
});
|
||
const flat = [];
|
||
for (const item of items) {
|
||
flat.push(item.value, String(item.score));
|
||
}
|
||
return flat;
|
||
}
|
||
return await this.raw.zRange(key, start, end, { REV: true });
|
||
}
|
||
async zcard(key) {
|
||
return await this.raw.zCard(key);
|
||
}
|
||
async zscore(key, member) {
|
||
const score = await this.raw.zScore(key, member);
|
||
return score != null ? String(score) : null;
|
||
}
|
||
// ---------------------------------------------------------------
|
||
// List commands
|
||
// ---------------------------------------------------------------
|
||
async lrange(key, start, end) {
|
||
return await this.raw.lRange(key, start, end);
|
||
}
|
||
async llen(key) {
|
||
return await this.raw.lLen(key);
|
||
}
|
||
async ltrim(key, start, end) {
|
||
await this.raw.lTrim(key, start, end);
|
||
return 'OK';
|
||
}
|
||
async lpos(key, value) {
|
||
var _a;
|
||
return (_a = (await this.raw.lPos(key, value))) !== null && _a !== void 0 ? _a : null;
|
||
}
|
||
// ---------------------------------------------------------------
|
||
// Set commands
|
||
// ---------------------------------------------------------------
|
||
async smembers(key) {
|
||
return await this.raw.sMembers(key);
|
||
}
|
||
// ---------------------------------------------------------------
|
||
// Stream commands
|
||
// ---------------------------------------------------------------
|
||
async xadd(key, id, fields, options) {
|
||
const opts = {};
|
||
if ((options === null || options === void 0 ? void 0 : options.MAXLEN) != null) {
|
||
opts.TRIM = {
|
||
strategy: 'MAXLEN',
|
||
threshold: options.MAXLEN,
|
||
strategyModifier: options.approximate === false ? undefined : '~',
|
||
};
|
||
}
|
||
// node-redis xAdd rejects numeric field values — stringify all values
|
||
const strFields = {};
|
||
for (const [k, v] of Object.entries(fields)) {
|
||
strFields[k] = String(v);
|
||
}
|
||
return await this.raw.xAdd(key, id, strFields, opts);
|
||
}
|
||
async xread(streams, options) {
|
||
const opts = {};
|
||
if ((options === null || options === void 0 ? void 0 : options.BLOCK) != null) {
|
||
opts.BLOCK = options.BLOCK;
|
||
}
|
||
if ((options === null || options === void 0 ? void 0 : options.COUNT) != null) {
|
||
opts.COUNT = options.COUNT;
|
||
}
|
||
const streamArgs = streams.map(s => ({ key: s.key, id: s.id }));
|
||
let result;
|
||
try {
|
||
result = await this.raw.xRead(streamArgs, opts);
|
||
}
|
||
catch (err) {
|
||
if (this.destroying && isConnectionClosedError(err)) {
|
||
return null;
|
||
}
|
||
if (isConnectionClosedError(err)) {
|
||
throw new ConnectionClosedError(err.message, err);
|
||
}
|
||
throw err;
|
||
}
|
||
if (!result) {
|
||
return null;
|
||
}
|
||
// Normalize to ioredis format: [[streamName, [[id, [field, value, …]], …]], …]
|
||
return result.map((stream) => [
|
||
stream.name,
|
||
stream.messages.map((msg) => [
|
||
msg.id,
|
||
Object.entries(msg.message).flat(),
|
||
]),
|
||
]);
|
||
}
|
||
async xtrim(key, strategy, threshold, options) {
|
||
const strategyModifier = (options === null || options === void 0 ? void 0 : options.approximate) === false ? undefined : '~';
|
||
return await this.raw.xTrim(key, strategy, threshold, {
|
||
strategyModifier,
|
||
});
|
||
}
|
||
// ---------------------------------------------------------------
|
||
// Blocking commands
|
||
// ---------------------------------------------------------------
|
||
async bzpopmin(key, timeout) {
|
||
let result;
|
||
try {
|
||
result = await this.raw.bzPopMin(key, timeout);
|
||
}
|
||
catch (err) {
|
||
if (this.destroying && isConnectionClosedError(err)) {
|
||
return null;
|
||
}
|
||
if (isConnectionClosedError(err)) {
|
||
throw new ConnectionClosedError(err.message, err);
|
||
}
|
||
throw err;
|
||
}
|
||
if (!result) {
|
||
return null;
|
||
}
|
||
return [result.key, result.value, String(result.score)];
|
||
}
|
||
// ---------------------------------------------------------------
|
||
// Server / admin commands
|
||
// ---------------------------------------------------------------
|
||
async info() {
|
||
return await this.raw.info();
|
||
}
|
||
async clientSetName(name) {
|
||
return await this.raw.clientSetName(name);
|
||
}
|
||
async clientList() {
|
||
return await this.raw.sendCommand(['CLIENT', 'LIST']);
|
||
}
|
||
// ---------------------------------------------------------------
|
||
// Key scanning
|
||
// ---------------------------------------------------------------
|
||
async scan(cursor, options) {
|
||
const opts = {};
|
||
if (options === null || options === void 0 ? void 0 : options.MATCH) {
|
||
opts.MATCH = options.MATCH;
|
||
}
|
||
if (options === null || options === void 0 ? void 0 : options.COUNT) {
|
||
opts.COUNT = options.COUNT;
|
||
}
|
||
const result = await this.raw.scan(String(cursor), opts);
|
||
return [String(result.cursor), result.keys];
|
||
}
|
||
scanStream(options) {
|
||
const raw = this.raw;
|
||
const connectPromise = this.connectPromise;
|
||
const scanOpts = {};
|
||
if (options.match) {
|
||
scanOpts.MATCH = options.match;
|
||
}
|
||
if (options.count) {
|
||
scanOpts.COUNT = options.count;
|
||
}
|
||
const readable = new Readable({
|
||
objectMode: true,
|
||
async read() {
|
||
var _a, e_1, _b, _c;
|
||
try {
|
||
if (connectPromise) {
|
||
await connectPromise;
|
||
}
|
||
try {
|
||
for (var _d = true, _e = __asyncValues(raw.scanIterator(scanOpts)), _f; _f = await _e.next(), _a = _f.done, !_a; _d = true) {
|
||
_c = _f.value;
|
||
_d = false;
|
||
const keys = _c;
|
||
if (!readable.push(Array.isArray(keys) ? keys : [keys])) {
|
||
return; // backpressure
|
||
}
|
||
}
|
||
}
|
||
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
||
finally {
|
||
try {
|
||
if (!_d && !_a && (_b = _e.return)) await _b.call(_e);
|
||
}
|
||
finally { if (e_1) throw e_1.error; }
|
||
}
|
||
readable.push(null); // EOF
|
||
}
|
||
catch (err) {
|
||
readable.destroy(err);
|
||
}
|
||
},
|
||
});
|
||
return readable;
|
||
}
|
||
// ---------------------------------------------------------------
|
||
// Extra Redis commands (not part of IRedisClient but used by tests
|
||
// and occasionally by user code that accesses the raw client).
|
||
// ---------------------------------------------------------------
|
||
async keys(pattern) {
|
||
return await this.raw.keys(pattern);
|
||
}
|
||
async exists(...keys) {
|
||
if (keys.length === 0) {
|
||
return 0;
|
||
}
|
||
return await this.raw.exists(keys);
|
||
}
|
||
async zadd(key, ...args) {
|
||
// ioredis: zadd(key, score, member, score, member, ...)
|
||
const members = [];
|
||
for (let i = 0; i < args.length; i += 2) {
|
||
members.push({ score: Number(args[i]), value: String(args[i + 1]) });
|
||
}
|
||
return await this.raw.zAdd(key, members);
|
||
}
|
||
async zrem(key, ...members) {
|
||
return await this.raw.zRem(key, members);
|
||
}
|
||
async xlen(key) {
|
||
return await this.raw.xLen(key);
|
||
}
|
||
async xrevrange(key, end, start, ...rest) {
|
||
const opts = {};
|
||
// ioredis: xrevrange(key, end, start, 'COUNT', n)
|
||
if (rest[0] === 'COUNT') {
|
||
opts.COUNT = Number(rest[1]);
|
||
}
|
||
const result = await this.raw.xRevRange(key, end, start, opts);
|
||
// Normalize to ioredis format: [[id, [field, value, …]], …]
|
||
return result.map((msg) => [
|
||
msg.id,
|
||
Object.entries(msg.message).flat(),
|
||
]);
|
||
}
|
||
async sadd(key, ...members) {
|
||
return await this.raw.sAdd(key, members.map(String));
|
||
}
|
||
async scard(key) {
|
||
return await this.raw.sCard(key);
|
||
}
|
||
async lpush(key, ...values) {
|
||
return await this.raw.lPush(key, values);
|
||
}
|
||
async rpop(key) {
|
||
return await this.raw.rPop(key);
|
||
}
|
||
async incr(key) {
|
||
return await this.raw.incr(key);
|
||
}
|
||
async incrby(key, increment) {
|
||
return await this.raw.incrBy(key, increment);
|
||
}
|
||
async flushall() {
|
||
return await this.raw.flushAll();
|
||
}
|
||
}
|
||
// ---------------------------------------------------------------------------
|
||
// Transaction / Pipeline wrapper
|
||
// ---------------------------------------------------------------------------
|
||
class NodeRedisTransaction {
|
||
constructor(raw, scripts) {
|
||
this.raw = raw;
|
||
this.scripts = scripts;
|
||
this.transformers = [];
|
||
}
|
||
addIdentityTransformer() {
|
||
this.transformers.push((v) => v);
|
||
}
|
||
hgetall(key) {
|
||
this.raw.hGetAll(key);
|
||
this.addIdentityTransformer();
|
||
return this;
|
||
}
|
||
hset(key, data) {
|
||
this.raw.hSet(key, data);
|
||
this.addIdentityTransformer();
|
||
return this;
|
||
}
|
||
hscan(key, cursor, options) {
|
||
const opts = {};
|
||
if ((options === null || options === void 0 ? void 0 : options.COUNT) != null) {
|
||
opts.COUNT = options.COUNT;
|
||
}
|
||
this.raw.hScan(key, String(cursor), opts);
|
||
// Transform node-redis { cursor, entries: [{field, value}] }
|
||
// to ioredis [cursor, [field, value, field, value, ...]]
|
||
this.transformers.push((val) => {
|
||
if (!val) {
|
||
return ['0', []];
|
||
}
|
||
const flat = [];
|
||
for (const entry of val.entries || []) {
|
||
flat.push(entry.field, entry.value);
|
||
}
|
||
return [String(val.cursor), flat];
|
||
});
|
||
return this;
|
||
}
|
||
smembers(key) {
|
||
this.raw.sMembers(key);
|
||
this.addIdentityTransformer();
|
||
return this;
|
||
}
|
||
sscan(key, cursor, options) {
|
||
const opts = {};
|
||
if ((options === null || options === void 0 ? void 0 : options.COUNT) != null) {
|
||
opts.COUNT = options.COUNT;
|
||
}
|
||
this.raw.sScan(key, String(cursor), opts);
|
||
// Transform node-redis { cursor, members: [...] }
|
||
// to ioredis [cursor, [member, member, ...]]
|
||
this.transformers.push((val) => {
|
||
if (!val) {
|
||
return ['0', []];
|
||
}
|
||
return [String(val.cursor), val.members || []];
|
||
});
|
||
return this;
|
||
}
|
||
zrange(key, start, end) {
|
||
this.raw.zRange(key, start, end);
|
||
this.addIdentityTransformer();
|
||
return this;
|
||
}
|
||
lrange(key, start, end) {
|
||
this.raw.lRange(key, start, end);
|
||
this.addIdentityTransformer();
|
||
return this;
|
||
}
|
||
llen(key) {
|
||
this.raw.lLen(key);
|
||
this.addIdentityTransformer();
|
||
return this;
|
||
}
|
||
del(...keys) {
|
||
if (keys.length > 0) {
|
||
this.raw.del(keys);
|
||
this.addIdentityTransformer();
|
||
}
|
||
return this;
|
||
}
|
||
runCommand(name, args) {
|
||
const script = this.scripts.get(name);
|
||
if (!script) {
|
||
throw new Error(`BullMQ: unknown command "${name}" in transaction`);
|
||
}
|
||
const commandArgs = normalizeScriptArgs(args);
|
||
const { sha, lua, numberOfKeys } = script;
|
||
const cmdKeys = commandArgs.slice(0, numberOfKeys).map(String);
|
||
const argv = commandArgs.slice(numberOfKeys).map((a) => {
|
||
if (Buffer.isBuffer(a)) {
|
||
return a;
|
||
}
|
||
if (a === undefined || a === null) {
|
||
return '';
|
||
}
|
||
return String(a);
|
||
});
|
||
// Use evalSha in pipeline; NOSCRIPT handling happens at exec()-time
|
||
// node-redis multi supports evalSha
|
||
this.raw.evalSha(sha, { keys: cmdKeys, arguments: argv });
|
||
this.addIdentityTransformer();
|
||
return this;
|
||
}
|
||
async exec() {
|
||
const results = await this.raw.exec();
|
||
if (!results) {
|
||
return null;
|
||
}
|
||
// node-redis multi.exec() returns values directly (or throws on error).
|
||
// Normalize to ioredis format: [Error | null, value][]
|
||
// Apply per-command transformers for format differences (hscan, sscan).
|
||
return results.map((result, i) => {
|
||
if (result instanceof Error) {
|
||
return [result, null];
|
||
}
|
||
const transformer = this.transformers[i];
|
||
const value = transformer ? transformer(result) : result;
|
||
return [null, value];
|
||
});
|
||
}
|
||
}
|