UNPKG

@litert/redis

Version:

A redis protocol implement for Node.js.

430 lines 15.8 kB
"use strict"; /* eslint-disable max-lines */ /** * Copyright 2025 Angus.Fenying <fenying@litert.org> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ProtocolClient = void 0; const C = require("./Common"); const node_events_1 = require("node:events"); const $Net = require("node:net"); const E = require("./Errors"); var ERequestState; (function (ERequestState) { ERequestState[ERequestState["PENDING"] = 0] = "PENDING"; ERequestState[ERequestState["DONE"] = 1] = "DONE"; ERequestState[ERequestState["TIMEOUT"] = 2] = "TIMEOUT"; })(ERequestState || (ERequestState = {})); class ProtocolClient extends node_events_1.EventEmitter { _cfg; _aclUser; _passwd; _db = 0; _ready = false; _executingQueue = []; _decoder; _encoder; get host() { return this._cfg.host; } get port() { return this._cfg.port; } _socket = null; _promise4Socket; constructor(opts) { super(); this._cfg = opts; this._decoder = opts.decoderFactory(); this._encoder = opts.encoderFactory(); switch (this._cfg.mode) { case C.EClientMode.SUBSCRIBER: { this._decoder.on('data', (type, data) => { const it = this._executingQueue.shift(); if (it) { if (it.state !== ERequestState.PENDING) { return; } if (it.timeout) { clearTimeout(it.timeout); } } switch (type) { case C.EDataType.LIST: switch (data[0][1].toString()) { case 'message': this.emit('message', data[1][1].toString(), data[2][1]); return; case 'pmessage': this.emit('message', data[2][1].toString(), data[3][1], data[1][1].toString()); return; default: } it.callback(null, data); break; case C.EDataType.FAILURE: it.callback(new E.E_COMMAND_FAILURE(data.toString())); break; case C.EDataType.INTEGER: it.callback(null, parseInt(data)); break; case C.EDataType.MESSAGE: it.callback(null, data.toString()); break; case C.EDataType.NULL: it.callback(null, null); break; case C.EDataType.STRING: it.callback(null, data); break; } }); break; } case C.EClientMode.PIPELINE: { this._decoder.on('data', (type, data) => { const i = this._executingQueue[0]; if (!i.result) { this._executingQueue.shift(); if (i.state !== ERequestState.PENDING) { return; } if (i.timeout) { clearTimeout(i.timeout); } switch (type) { case C.EDataType.FAILURE: i.callback(new E.E_COMMAND_FAILURE(data.toString())); break; case C.EDataType.INTEGER: i.callback(null, parseInt(data)); break; case C.EDataType.MESSAGE: i.callback(null, data.toString()); break; case C.EDataType.NULL: i.callback(null, null); break; case C.EDataType.LIST: case C.EDataType.STRING: i.callback(null, data); break; } return; } const offset = i.result.length - i.expected--; switch (type) { case C.EDataType.FAILURE: i.result[offset] = new E.E_COMMAND_FAILURE(data.toString()); break; case C.EDataType.INTEGER: i.result[offset] = parseInt(data); break; case C.EDataType.MESSAGE: i.result[offset] = data.toString(); break; case C.EDataType.NULL: i.result[offset] = null; break; case C.EDataType.LIST: case C.EDataType.STRING: i.result[offset] = data; break; } if (!i.expected) { this._executingQueue.shift(); if (i.state === ERequestState.PENDING) { i.callback(null, i.result); i.state = ERequestState.DONE; if (i.timeout) { clearTimeout(i.timeout); } } } }); break; } case C.EClientMode.SIMPLE: { this._decoder.on('data', (type, data) => { const i = this._executingQueue.shift(); if (i.state !== ERequestState.PENDING) { return; } if (i.timeout) { clearTimeout(i.timeout); } switch (type) { case C.EDataType.FAILURE: i.callback(new E.E_COMMAND_FAILURE(data.toString())); break; case C.EDataType.INTEGER: i.callback(null, parseInt(data)); break; case C.EDataType.MESSAGE: i.callback(null, data.toString()); break; case C.EDataType.NULL: i.callback(null, null); break; case C.EDataType.LIST: case C.EDataType.STRING: i.callback(null, data); break; } }); break; } } } /** * Use an existing socket or create a new one. */ async _getConnection() { if (this._socket) { return this._socket; } if (this._promise4Socket) { return this._promise4Socket; } return this._promise4Socket = this._redisConnect(); } async _redisConnect() { try { this._socket = (await this._netConnect(this.host, this.port, this._cfg.connectTimeout)) .on('close', () => { this._socket = null; this._ready = false; this._decoder.reset(); const deadItems = this._executingQueue; this._executingQueue = []; /** * Reject all promises of pending commands. */ for (const x of deadItems) { try { x.callback(new E.E_CONN_LOST()); } catch (e) { this.emit('error', e); } } this.emit('close'); }) .on('error', (e) => { this.emit('error', e); }) .on('data', (data) => this._decoder.update(data)); if (!this._ready) { this._ready = true; try { if (this._passwd) { await this.auth(this._passwd, this._aclUser); } if (this._db) { await this.select(this._db); } } catch (e) { this.close(); throw e; } this.emit('ready'); } return this._socket; } finally { delete this._promise4Socket; } } async auth(password, username) { if (username) { await this._command('AUTH', [username, password]); this._aclUser = username; } else { await this._command('AUTH', [password]); } this._passwd = password; } async select(db) { await this._command('SELECT', [db]); this._db = db; } /** * Create a network socket and make it connect to the server. * * @param {string} host The host of server. * @param {number} port The port of server. * @param {number} timeout The timeout for the connecting to server. * @returns {Promise<$Net.Socket>} */ _netConnect(host, port, timeout) { return new Promise((resolve, reject) => { const socket = new $Net.Socket(); socket.on('connect', () => { // return a clean socket here socket.removeAllListeners(); socket.setTimeout(0); resolve(socket); }); socket.on('error', (e) => { reject(new E.E_CONNECT_FAILED(e)); }); socket.connect(port, host); socket.setTimeout(timeout, () => { socket.destroy(new E.E_CONNECT_TIMEOUT()); }); }); } connect(cb) { return this._unifyAsync(async (callback) => { await this._getConnection(); callback(null); }, cb); } close(cb) { if (this._socket) { this._socket.end(); if (cb) { this.once('close', cb); } } else { cb?.(); } } /** * Make async process same in both callback and promise styles. * @param fn The body of async process * @param cb The optional callback function if not promise style. * @returns Return a promise if cb is not provided. */ _unifyAsync(fn, cb) { if (cb) { try { fn(cb).catch(cb); } catch (e) { /** * fn() itself may throw an error if not an async function. */ cb(e); } return; } else { return new Promise((resolve, reject) => { try { fn((e, v) => { if (e) { reject(e); } else { resolve(v); } }).catch(reject); } catch (e) { /** * fn() itself may throw an error if not an async function. */ reject(e); } }); } } command(cmd, args, cb) { return this._command(cmd, args, cb); } _checkQueueSize() { if (this._cfg.queueSize && this._executingQueue.length >= this._cfg.queueSize) { if (this._cfg.actionOnQueueFull === 'error') { throw new E.E_COMMAND_QUEUE_FULL(); } this._socket.destroy(new E.E_COMMAND_QUEUE_FULL()); } } _command(cmd, args, cb) { return this._unifyAsync(async (callback) => { this._checkQueueSize(); const socket = this._socket ?? await this._getConnection(); socket.write(this._encoder.encodeCommand(cmd, args)); const handle = { callback, state: ERequestState.PENDING, }; this._executingQueue.push(handle); if (this._cfg.commandTimeout > 0) { this._setTimeoutForRequest(handle, () => new E.E_COMMAND_TIMEOUT({ mode: 'mono', cmd, argsQty: args.length })); } }, cb); } _bulkCommands(cmdList, cb) { return this._unifyAsync(async (callback) => { this._checkQueueSize(); const socket = this._socket ?? await this._getConnection(); socket.write(Buffer.concat(cmdList.map((x) => this._encoder.encodeCommand(x.cmd, x.args)))); const handle = { callback, expected: cmdList.length, state: ERequestState.PENDING, result: new Array(cmdList.length), }; this._executingQueue.push(handle); if (this._cfg.commandTimeout > 0) { this._setTimeoutForRequest(handle, () => new E.E_COMMAND_TIMEOUT({ mode: 'bulk', cmdQty: handle.expected })); } }, cb); } async _commitExec(qty) { return this._unifyAsync(async (callback) => { const socket = this._socket ?? await this._getConnection(); socket.write(this._encoder.encodeCommand('EXEC', [])); const handle = { callback, expected: qty, state: ERequestState.PENDING, result: [], }; this._executingQueue.push(handle); if (this._cfg.commandTimeout > 0) { this._setTimeoutForRequest(handle, () => new E.E_COMMAND_TIMEOUT({ mode: 'bulk', cmdQty: handle.expected })); } }); } _setTimeoutForRequest(handle, mkError) { handle.timeout = setTimeout(() => { delete handle.timeout; switch (handle.state) { case ERequestState.PENDING: handle.state = ERequestState.TIMEOUT; handle.callback(mkError()); break; case ERequestState.DONE: case ERequestState.TIMEOUT: default: break; } }, this._cfg.commandTimeout); } } exports.ProtocolClient = ProtocolClient; //# sourceMappingURL=ProtocolClient.js.map