UNPKG

@iobroker/db-base

Version:

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

437 lines (436 loc) 15 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var redisHandler_exports = {}; __export(redisHandler_exports, { RedisHandler: () => RedisHandler }); module.exports = __toCommonJS(redisHandler_exports); var import_respjs = __toESM(require("respjs"), 1); var import_node_events = require("node:events"); var import_constants = require("./constants.js"); class RedisHandler extends import_node_events.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 import_respjs.default(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}`)); } 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 = command.toString("utf-8"); if (Buffer.isBuffer(data[0])) { data[0] = data[0].toString("utf-8"); } 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) { this.log.warn(`${this.socketId} Conflicting multi call`); } this._handleMulti(); return; } if (this.activeMultiCalls.length && !this.activeMultiCalls[0].execCalled && command !== "exec") { this.activeMultiCalls[0].responseIds.push(responseId); this.activeMultiCalls[0].responseMap.set(responseId, null); } else { 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) { if (this.writeQueue[idx].id === responseId) { this.writeQueue[idx].data = data; if (idx > 0) { break; } } 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); if (this.writeQueue.length && this.writeQueue[idx].data === false) { break; } } else { 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) { 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 = import_respjs.default.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, import_respjs.default.encodeNull()); return; } } this.sendResponse(responseId, import_respjs.default.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, import_respjs.default.encodeNullArray()); return; } } this.sendResponse(responseId, import_respjs.default.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, import_respjs.default.encodeString(str)); return; } } this.sendResponse(responseId, import_respjs.default.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, import_respjs.default.encodeError(error)); return; } } this.sendResponse(responseId, import_respjs.default.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, import_respjs.default.encodeInteger(num)); return; } } this.sendResponse(responseId, import_respjs.default.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, import_respjs.default.encodeBulk(str)); return; } } this.sendResponse(responseId, import_respjs.default.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, import_respjs.default.encodeBufBulk(buf)); return; } } this.sendResponse(responseId, import_respjs.default.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] = import_respjs.default.encodeBufBulk(value); } else if (value === null) { returnArr[i] = import_respjs.default.encodeNull(); } else if (typeof value === "number") { returnArr[i] = import_respjs.default.encodeInteger(value); } else { returnArr[i] = import_respjs.default.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, import_respjs.default.encodeArray(this.encodeRespArray(arr))); return; } } this.sendResponse(responseId, import_respjs.default.encodeArray(this.encodeRespArray(arr))); } /** * Handles a 'multi' command * */ _handleMulti() { this.activeMultiCalls.unshift({ responseIds: [], execCalled: false, responseCount: 0, responseMap: /* @__PURE__ */ 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; 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) { const queuedStrArr = new Array(multiObj.responseCount).fill(import_constants.QUEUED_STR_BUF); this._sendQueued(multiObj.execId, Buffer.concat([import_constants.OK_STR_BUF, ...queuedStrArr, import_respjs.default.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); } } } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { RedisHandler }); //# sourceMappingURL=redisHandler.js.map