UNPKG

@iobroker/db-base

Version:

This Library contains base classes that are used by the database classes for ioBroker

440 lines 16.3 kB
// @ts-expect-error no types import Resp from 'respjs'; import { EventEmitter } from 'node:events'; import { QUEUED_STR_BUF, OK_STR_BUF } from './constants.js'; /** * Class to handle a redis connection and provide events to react on for * all incoming Redis commands */ export class RedisHandler extends EventEmitter { socket; logScope; handleBuffers; options; log; socketId; initialized; stop; activeMultiCalls; writeQueue; responseId = 0; resp; /** * Initialize and register all data handlers to send out events on new commands * * @param socket Network Socket/Connection * @param options options objects, currently mainly for logger */ constructor(socket, options) { super(); options = options || {}; this.options = options; this.log = options.log || console; this.logScope = options.logScope || ''; if (this.logScope.length) { this.logScope += ' '; } this.socket = socket; this.socketId = `${this.logScope + socket.remoteAddress}:${socket.remotePort}`; this.initialized = false; this.stop = false; this.activeMultiCalls = []; this.writeQueue = []; this.handleBuffers = false; const respOptions = {}; if (options.handleAsBuffers) { this.handleBuffers = true; respOptions.bufBulk = true; } this.resp = new Resp(respOptions); this.resp.on('error', (err) => { this.log.error(`${this.socketId} (Init=${this.initialized}) Redis error:${err}`); if (this.initialized) { this.sendError(null, new Error(`PARSER ERROR ${err}`)); // TODO } else { this.close(); } }); this.resp.on('data', (data) => this._handleCommand(data)); socket.on('data', data => { if (this.options.enhancedLogging) { this.log.silly(`${this.socketId} New Redis request: ${data.length > 1024 ? `${data .toString() .replace(/[\r\n]+/g, '') .substring(0, 100)} -- ${data.length} bytes` : data.toString().replace(/[\r\n]+/g, '')}`); } this.resp.write(data); }); socket.on('error', err => { if (!this.stop) { this.log.debug(`${this.socketId} Redis Socket error: ${err.stack}`); } if (this.socket) { this.socket.destroy(); } }); } /** * Handle one incoming command, assign responseId and emit event to be handled * * @param data Array RESP data */ _handleCommand(data) { let command = data.splice(0, 1)[0]; if (this.handleBuffers) { // Command and pot. first parameter should always be Buffer command = command.toString('utf-8'); if (Buffer.isBuffer(data[0])) { data[0] = data[0].toString('utf-8'); } // Binary data only relevant for GET and SET in our case if (command !== 'set' && data.length > 1) { for (let i = 1; i < data.length; i++) { if (Buffer.isBuffer(data[i])) { data[i] = data[i].toString('utf-8'); } } } } if (this.responseId === Number.MAX_VALUE) { this.responseId = 0; } const responseId = ++this.responseId; if (this.options.enhancedLogging) { this.log.silly(`${this.socketId} Parser result: id=${responseId}, command=${command}, data=${JSON.stringify(data).length > 1024 ? `${JSON.stringify(data).substring(0, 100)} -- ${JSON.stringify(data).length} bytes` : JSON.stringify(data)}`); } if (command === 'multi') { if (this.activeMultiCalls.length && !this.activeMultiCalls[0].execCalled) { // should never happen this.log.warn(`${this.socketId} Conflicting multi call`); } this._handleMulti(); return; } // multi active and exec not called yet if (this.activeMultiCalls.length && !this.activeMultiCalls[0].execCalled && command !== 'exec') { // store all response ids so we know which need to be in the multi call this.activeMultiCalls[0].responseIds.push(responseId); // add it for the correct order will be overwritten with correct response this.activeMultiCalls[0].responseMap.set(responseId, null); } else { // multi response ids should not be pushed - we will answer combined this.writeQueue.push({ id: responseId, data: false }); } if (command === 'exec') { this._handleExec(responseId); return; } if (command === 'info') { this.initialized = true; } if (this.listenerCount(command) !== 0) { setImmediate(() => this.emit(command, data, responseId)); } else { this.sendError(responseId, new Error(`${command} NOT SUPPORTED`)); } } /** * Check if the response to a certain command can be send out directly or * if it needs to wait till earlier responses are ready * * @param responseId ID of the response * @param data Buffer to send out */ _sendQueued(responseId, data) { let idx = 0; while (this.writeQueue.length && idx < this.writeQueue.length) { // we found the queue entry that matches with the responseId, so store the data so be sent out if (this.writeQueue[idx].id === responseId) { this.writeQueue[idx].data = data; // if we found it not on first index we are done because we can not send it out, // need to wait for index 0 to have a complete answer if (idx > 0) { break; } } // when data for queue entry 0 are preset (!== false) we can send it, remove the first entry // and check the other entries if they have completed responses too if (idx === 0 && this.writeQueue[idx].data !== false) { const response = this.writeQueue.shift(); if (this.options.enhancedLogging) { this.log.silly(`${this.socketId} Redis response (${response.id}): ${response.data.length > 1024 ? `${data.length} bytes` : response.data.toString().replace(/[\r\n]+/g, '')}`); } this._write(response.data); // We sended out first queue entry but no further response is ready // and we do not need to check the whole queue, so we are done here if (this.writeQueue.length && this.writeQueue[idx].data === false) { break; } } else { // we have not found the response on the current index, try next one idx++; } } if (idx > 0) { if (this.options.enhancedLogging) { this.log.silly(`${this.socketId} Redis response (${responseId}): Response queued`); } } } /** * Really write out a response to the network connection * * @param data Buffer to send out */ _write(data) { this.socket.write(data); } /** * Guard to make sure a response is valid to be sent out * * @param responseId ID of the response * @param data Buffer to send out */ sendResponse(responseId, data) { // handle responses without a specific request like publishing data, so send directly if (responseId === null) { if (this.options.enhancedLogging) { this.log.silly(`${this.socketId} Redis response DIRECT: ${data.toString().replace(/[\r\n]+/g, '')}`); } return this._write(data); } if (!responseId) { throw new Error('Invalid implementation: no responseId provided!'); } if (!data) { this.log.warn(`${this.socketId} Not able to write ${JSON.stringify(data)}`); data = Resp.encodeError(new Error(`INVALID RESPONSE: ${JSON.stringify(data)}`)); } setImmediate(() => this._sendQueued(responseId, data)); } /** * Close network connection */ close() { this.log.silly(`${this.socketId} close Redis connection`); this.stop = true; this.socket.end(); } /** * Return if socket/handler active or closed * * @returns is Handler/Connection active (not closed) */ isActive() { return !this.stop; } /** * Encode RESP's Null value to RESP buffer and send out * * @param responseId ID of the response */ sendNull(responseId) { for (let i = 0; i < this.activeMultiCalls.length; i++) { if (this.activeMultiCalls[i].responseIds.includes(responseId)) { this._handleMultiResponse(responseId, i, Resp.encodeNull()); return; } } this.sendResponse(responseId, Resp.encodeNull()); } /** * Encode RESP's Null Array value to RESP buffer and send out * * @param responseId ID of the response */ sendNullArray(responseId) { for (let i = 0; i < this.activeMultiCalls.length; i++) { if (this.activeMultiCalls[i].responseIds.includes(responseId)) { this._handleMultiResponse(responseId, i, Resp.encodeNullArray()); return; } } this.sendResponse(responseId, Resp.encodeNullArray()); } /** * Encode string to RESP buffer and send out * * @param responseId ID od the response * @param str String to encode */ sendString(responseId, str) { for (let i = 0; i < this.activeMultiCalls.length; i++) { if (this.activeMultiCalls[i].responseIds.includes(responseId)) { this._handleMultiResponse(responseId, i, Resp.encodeString(str)); return; } } this.sendResponse(responseId, Resp.encodeString(str)); } /** * Encode error object to RESP buffer and send out * * @param responseId ID of the response * @param error Error object with error details to send out */ sendError(responseId, error) { this.log.warn(`${this.socketId} Error from InMemDB: ${error.stack}`); for (let i = 0; i < this.activeMultiCalls.length; i++) { if (this.activeMultiCalls[i].responseIds.includes(responseId)) { this._handleMultiResponse(responseId, i, Resp.encodeError(error)); return; } } this.sendResponse(responseId, Resp.encodeError(error)); } /** * Encode integer to RESP buffer and send out * * @param responseId ID of the response * @param num Integer to send out */ sendInteger(responseId, num) { for (let i = 0; i < this.activeMultiCalls.length; i++) { if (this.activeMultiCalls[i].responseIds.includes(responseId)) { this._handleMultiResponse(responseId, i, Resp.encodeInteger(num)); return; } } this.sendResponse(responseId, Resp.encodeInteger(num)); } /** * Encode RESP's bulk string to RESP buffer and send out * * @param responseId ID of the response * @param str String to send out */ sendBulk(responseId, str) { for (let i = 0; i < this.activeMultiCalls.length; i++) { if (this.activeMultiCalls[i].responseIds.includes(responseId)) { this._handleMultiResponse(responseId, i, Resp.encodeBulk(str)); return; } } this.sendResponse(responseId, Resp.encodeBulk(str)); } /** * Encode RESP's bulk buffer to RESP buffer. * * @param responseId ID of the response * @param buf Buffer to send out */ sendBufBulk(responseId, buf) { for (let i = 0; i < this.activeMultiCalls.length; i++) { if (this.activeMultiCalls[i].responseIds.includes(responseId)) { this._handleMultiResponse(responseId, i, Resp.encodeBufBulk(buf)); return; } } this.sendResponse(responseId, Resp.encodeBufBulk(buf)); } /** * Encode an Array depending on the type of the elements * * @param arr Array to encode * @returns Array with Buffers with encoded values */ encodeRespArray(arr) { const returnArr = new Array(arr.length); arr.forEach((value, i) => { if (Array.isArray(value)) { returnArr[i] = this.encodeRespArray(value); } else if (Buffer.isBuffer(value)) { returnArr[i] = Resp.encodeBufBulk(value); } else if (value === null) { returnArr[i] = Resp.encodeNull(); } else if (typeof value === 'number') { returnArr[i] = Resp.encodeInteger(value); } else { returnArr[i] = Resp.encodeBulk(value); } }); return returnArr; } /** * Encode a array values to buffers and send out * * @param responseId ID of the response * @param arr Array to send out */ sendArray(responseId, arr) { for (let i = 0; i < this.activeMultiCalls.length; i++) { if (this.activeMultiCalls[i].responseIds.includes(responseId)) { this._handleMultiResponse(responseId, i, Resp.encodeArray(this.encodeRespArray(arr))); return; } } this.sendResponse(responseId, Resp.encodeArray(this.encodeRespArray(arr))); } /** * Handles a 'multi' command * */ _handleMulti() { this.activeMultiCalls.unshift({ responseIds: [], execCalled: false, responseCount: 0, responseMap: new Map(), }); } /** * Handles an 'exec' command * * @param responseId ID of the response */ _handleExec(responseId) { if (!this.activeMultiCalls[0]) { this.sendError(responseId, new Error('EXEC without MULTI')); return; } this.activeMultiCalls[0].execId = responseId; this.activeMultiCalls[0].execCalled = true; // maybe we have all fullfilled yet if (this.activeMultiCalls[0].responseCount === this.activeMultiCalls[0].responseIds.length) { const multiRespObj = this.activeMultiCalls.shift(); this._sendExecResponse(multiRespObj); } } /** * Builds up the exec response and sends it * * @param multiObj the multi object to send out */ _sendExecResponse(multiObj) { // collect all 'QUEUED' answers const queuedStrArr = new Array(multiObj.responseCount).fill(QUEUED_STR_BUF); this._sendQueued(multiObj.execId, Buffer.concat([OK_STR_BUF, ...queuedStrArr, Resp.encodeArray(Array.from(multiObj.responseMap.values()))])); } /** * Handles a multi response * * @param responseId ID of the response * @param index index of the multi call * @param buf buffer to include in response */ _handleMultiResponse(responseId, index, buf) { this.activeMultiCalls[index].responseMap.set(responseId, buf); this.activeMultiCalls[index].responseCount++; if (this.activeMultiCalls[index].execCalled && this.activeMultiCalls[index].responseCount === this.activeMultiCalls[index].responseIds.length) { const multiRespObj = this.activeMultiCalls.splice(index, 1)[0]; this._sendExecResponse(multiRespObj); } } } //# sourceMappingURL=redisHandler.js.map