@iobroker/db-base
Version:
This Library contains base classes that are used by the database classes for ioBroker
437 lines (436 loc) • 15 kB
JavaScript
"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