Files
travel-app/node_modules/bullmq/dist/cjs/classes/node-redis-client.js
2026-08-09 21:56:00 +00:00

772 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createNodeRedisClient = createNodeRedisClient;
const tslib_1 = require("tslib");
const crypto_1 = require("crypto");
const events_1 = require("events");
const stream_1 = require("stream");
const connection_closed_error_1 = require("./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.');
}
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 events_1.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 = (0, crypto_1.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 connection_closed_error_1.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 connection_closed_error_1.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 connection_closed_error_1.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 connection_closed_error_1.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 stream_1.Readable({
objectMode: true,
async read() {
var _a, e_1, _b, _c;
try {
if (connectPromise) {
await connectPromise;
}
try {
for (var _d = true, _e = tslib_1.__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];
});
}
}