UNPKG

@iobroker/db-states-jsonl

Version:

The Library contains the JSONL Database classes for File based states database client and server.

444 lines (443 loc) 18.6 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 statesInMemServerRedis_exports = {}; __export(statesInMemServerRedis_exports, { StatesInMemoryServer: () => StatesInMemoryServer }); module.exports = __toCommonJS(statesInMemServerRedis_exports); var import_node_net = __toESM(require("node:net"), 1); var import_node_util = require("node:util"); var import_db_base = require("@iobroker/db-base"); var import_statesInMemJsonlDB = require("./statesInMemJsonlDB.js"); var import_tools = require("@iobroker/js-controller-common-db/tools"); var import_js_controller_common_db = require("@iobroker/js-controller-common-db"); class StatesInMemoryServer extends import_statesInMemJsonlDB.StatesInMemoryJsonlDB { /** * Constructor * * @param settings State and InMem-DB settings */ constructor(settings) { super(settings); this.serverConnections = {}; this.namespaceStates = `${this.settings.redisNamespace || "io"}.`; this.namespaceMsg = `${this.settings.namespaceMsg || "messagebox"}.`; this.namespaceLog = `${this.settings.namespaceLog || "log"}.`; this.namespaceSession = `${this.settings.namespaceSession || "session"}.`; this.namespaceMsgLen = this.namespaceMsg.length; this.namespaceLogLen = this.namespaceLog.length; this.metaNamespace = `${this.settings.metaNamespace || "meta"}.`; this.metaNamespaceLen = this.metaNamespace.length; this.open().then(() => { return this._initRedisServer(this.settings.connection); }).then(() => { this.log.debug(`${this.namespace} ${settings.secure ? "Secure " : ""} Redis inMem-states listening on port ${this.settings.port || 9e3}`); if (typeof this.settings.connected === "function") { setImmediate(() => this.settings.connected()); } }).catch((e) => { this.log.error(`${this.namespace} Cannot start inMem-states on port ${this.settings.port || 9e3}: ${e.message}`); process.exit(import_js_controller_common_db.EXIT_CODES.NO_CONNECTION_TO_STATES_DB); }); } /** * Separate Namespace from ID and return both * * @param idWithNamespace ID or Array of IDs containing a redis namespace and the real ID * @returns Object with namespace and the * ID/Array of IDs without the namespace */ _normalizeId(idWithNamespace) { let ns = this.namespaceStates; let id; if (Array.isArray(idWithNamespace)) { const ids = []; idWithNamespace.forEach((el) => { const { id: id2, namespace } = this._normalizeId(el); ids.push(id2); ns = namespace; }); id = ids; } else { id = idWithNamespace; const pointIdx = idWithNamespace.indexOf("."); if (pointIdx !== -1) { ns = idWithNamespace.substr(0, pointIdx + 1); if (ns === this.namespaceStates || ns === this.metaNamespace) { id = idWithNamespace.substr(pointIdx + 1); } } } return { id, namespace: ns }; } /** * Publish a subscribed value to one of the redis connections in redis format * * @param client Instance of RedisHandler * @param type Type of subscribed key * @param id Subscribed ID * @param obj Object to publish * @returns Publish counter 0 or 1 depending if send out or not */ publishToClients(client, type, id, obj) { if (!client._subscribe || !client._subscribe[type]) { return 0; } const s = client._subscribe[type]; const found = s.find((sub) => sub.regex.test(id)); if (found) { if (type === "meta") { this.log.silly(`${this.namespace} Redis Publish Meta ${id}=${obj}`); const sendPattern = this.metaNamespace + found.pattern; const sendId = this.metaNamespace + id; client.sendArray(null, ["pmessage", sendPattern, sendId, obj]); } else { let objString; try { objString = JSON.stringify(obj); } catch (e) { this.log.error(`${this.namespace} Error on publishing state: ${id}=${(0, import_node_util.inspect)(obj)}: ${e.message}`); return 0; } this.log.silly(`${this.namespace} Redis Publish State ${id}=${objString}`); const sendPattern = (type === "state" ? "" : this.namespaceStates) + found.pattern; const sendId = (type === "state" ? "" : this.namespaceStates) + id; client.sendArray(null, ["pmessage", sendPattern, sendId, objString]); } return 1; } return 0; } /** * Register all event listeners for Handler and implement the relevant logic * * @param handler RedisHandler instance */ _socketEvents(handler) { let connectionName = null; let namespaceLog = this.namespace; handler.on("info", (_data, responseId) => { let infoString = "# Server\r\n"; infoString += "redis_version:3.0.0-iobroker\r\n"; infoString += "# Clients\r\n"; infoString += "# Memory\r\n"; infoString += "# Persistence\r\n"; infoString += "# Stats\r\n"; infoString += "# Replication\r\n"; infoString += "# CPU\r\n"; infoString += "# Cluster\r\n"; infoString += "# Keyspace\r\n"; infoString += `db0:keys=${Object.keys(this.dataset).length},expires=${Object.keys(this.stateExpires).length + Object.keys(this.sessionExpires).length},avg_ttl=98633637897`; handler.sendBulk(responseId, infoString); }); handler.on("quit", (_data, responseId) => { this.log.silly(`${namespaceLog} Redis QUIT received, close connection`); handler.sendString(responseId, "OK"); handler.close(); }); handler.on("publish", (data, responseId) => { const { id, namespace } = this._normalizeId(data[0]); if (namespace === this.namespaceStates || namespace === this.metaNamespace) { return void handler.sendInteger(responseId, 0); } const publishCount = this.publishAll(namespace.substr(0, namespace.length - 1), id, JSON.parse(data[1])); handler.sendInteger(responseId, publishCount); }); handler.on("mget", (data, responseId) => { if (!data || !data[0]) { return void handler.sendArray(responseId, []); } const { id, namespace } = this._normalizeId(data); if (namespace === this.namespaceStates) { try { const states = this._getStates(id); const result = states.map((el) => el ? JSON.stringify(el) : null); handler.sendArray(responseId, result); } catch (err) { handler.sendError(responseId, new Error(`ERROR _getStates: ${err.message}`)); } } else { handler.sendError(responseId, new Error(`MGET-UNSUPPORTED for namespace ${namespace}: Data=${JSON.stringify(data)}`)); } }); handler.on("get", (data, responseId) => { const { id, namespace } = this._normalizeId(data[0]); if (namespace === this.namespaceStates) { const result = this._getState(id); if (!result) { handler.sendNull(responseId); } else { if (Buffer.isBuffer(result)) { handler.sendBufBulk(responseId, result); } else { handler.sendBulk(responseId, JSON.stringify(result)); } } } else if (namespace === this.namespaceSession) { const result = this._getSession(id); if (result === void 0 || result === null) { handler.sendNull(responseId); } else { handler.sendBulk(responseId, JSON.stringify(result)); } } else if (namespace === this.metaNamespace) { const result = this.getMeta(id); if (result === void 0 || result === null) { handler.sendNull(responseId); } else { handler.sendBulk(responseId, result); } } else { handler.sendError(responseId, new Error(`GET-UNSUPPORTED for namespace ${namespace}: Data=${JSON.stringify(data)}`)); } }); handler.on("set", (data, responseId) => { const { id, namespace } = this._normalizeId(data[0]); if (namespace === this.namespaceStates) { try { const state = JSON.parse(data[1].toString("utf-8")); this._setStateDirect(id, state); handler.sendString(responseId, "OK"); } catch (err) { handler.sendError(responseId, new Error(`ERROR setState id=${id}: ${err.message}`)); } } else if (namespace === this.metaNamespace) { this.setMeta(id, data[1].toString("utf-8")); handler.sendString(responseId, "OK"); } else { handler.sendError(responseId, new Error(`SET-UNSUPPORTED for namespace ${namespace}: Data=${JSON.stringify(data)}`)); } }); handler.on("setex", (data, responseId) => { const { id, namespace } = this._normalizeId(data[0]); if (namespace === this.namespaceStates) { try { const state = JSON.parse(data[2].toString("utf-8")); const expire = parseInt(data[1].toString("utf-8"), 10); if (isNaN(expire)) { return void handler.sendError(responseId, new Error(`ERROR parsing expire value ${data[1].toString("utf-8")}`)); } this._setStateDirect(id, state, expire); handler.sendString(responseId, "OK"); } catch (err) { handler.sendError(responseId, new Error(`ERROR setStateEx id=${id}: ${err.message}`)); } } else if (namespace === this.namespaceSession) { try { const state = JSON.parse(data[2].toString("utf-8")); const expire = parseInt(data[1].toString("utf-8"), 10); if (isNaN(expire)) { return void handler.sendError(responseId, new Error(`ERROR parsing expire value ${data[1].toString("utf-8")}`)); } this._setSession(id, expire, state); handler.sendString(responseId, "OK"); } catch (err) { handler.sendError(responseId, new Error(`ERROR _setSession ${id}: ${err.message}`)); } } else { handler.sendError(responseId, new Error(`SETEX-UNSUPPORTED for namespace ${namespace}: Data=${JSON.stringify(data)}`)); } }); handler.on("del", (data, responseId) => { const { id, namespace } = this._normalizeId(data[0]); if (namespace === this.namespaceStates) { this._delState(id); handler.sendInteger(responseId, 1); } else if (namespace === this.namespaceSession) { this._destroySession(id); handler.sendInteger(responseId, 1); } else { handler.sendError(responseId, new Error(`DEL-UNSUPPORTED for namespace ${namespace}: Data=${JSON.stringify(data)}`)); } }); handler.on("keys", (data, responseId) => { if (!data || !data.length) { return void handler.sendArray(responseId, []); } const { id, namespace } = this._normalizeId(data[0]); if (namespace === this.namespaceStates) { let pattern = id; if (pattern.substring(0, 3) === this.namespaceStates) { pattern = pattern.substring(this.namespaceStates.length); } const keys = this._getKeys(pattern); const result = keys.map((id2) => this.namespaceStates + id2); handler.sendArray(responseId, result); } else { handler.sendError(responseId, new Error(`KEYS-UNSUPPORTED for namespace ${namespace}: Data=${JSON.stringify(data)}`)); } }); handler.on("psubscribe", (data, responseId) => { const { id, namespace } = this._normalizeId(data[0]); if (namespace === this.namespaceMsg) { this._subscribeMessageForClient(handler, id.substr(this.namespaceMsgLen)); handler.sendArray(responseId, ["psubscribe", data[0], 1]); } else if (namespace === this.namespaceLog) { this._subscribeLogForClient(handler, id.substr(this.namespaceLogLen)); handler.sendArray(responseId, ["psubscribe", data[0], 1]); } else if (namespace === this.namespaceStates) { try { this._subscribeForClient(handler, id); handler.sendArray(responseId, ["psubscribe", data[0], 1]); } catch (e) { handler.sendError(responseId, e); } } else if (namespace === this.metaNamespace) { this._subscribeMeta(handler, id); handler.sendArray(responseId, ["psubscribe", data[0], 1]); } else { handler.sendError(responseId, new Error(`PSUBSCRIBE-UNSUPPORTED for namespace ${namespace}: Data=${JSON.stringify(data)}`)); } }); handler.on("punsubscribe", (data, responseId) => { const { id, namespace } = this._normalizeId(data[0]); if (namespace === this.namespaceMsg) { this._unsubscribeMessageForClient(handler, id.substr(this.namespaceMsgLen)); handler.sendArray(responseId, ["punsubscribe", data[0], 1]); } else if (namespace === this.namespaceLog) { this._unsubscribeLogForClient(handler, id.substr(this.namespaceLogLen)); handler.sendArray(responseId, ["punsubscribe", data[0], 1]); } else if (namespace === this.namespaceStates) { this._unsubscribeForClient(handler, id); handler.sendArray(responseId, ["punsubscribe", data[0], 1]); } else { handler.sendError(responseId, new Error(`PUNSUBSCRIBE-UNSUPPORTED for namespace ${namespace}: Data=${JSON.stringify(data)}`)); } }); handler.on("subscribe", (data, responseId) => { if (data[0].startsWith("__keyevent@")) { handler.sendArray(responseId, ["subscribe", data[0], 1]); } else { handler.sendError(responseId, new Error(`SUBSCRIBE-UNSUPPORTED for ${data[0]}`)); } }); handler.on("config", (data, responseId) => { if (typeof data[0] === "string" && data[0].toLowerCase() === "set" && data[1] === "notify-keyspace-events") { handler.sendString(responseId, "OK"); } else { handler.sendError(responseId, new Error(`CONFIG-UNSUPPORTED for ${JSON.stringify(data)}`)); } }); handler.on("client", (data, responseId) => { if (data[0] === "setname" && typeof data[1] === "string") { connectionName = data[1]; namespaceLog = connectionName; handler.sendString(responseId, "OK"); } else if (data[0] === "getname") { if (connectionName && typeof connectionName === "string") { handler.sendString(responseId, connectionName); } else { handler.sendNull(responseId); } } else { handler.sendError(responseId, new Error(`CLIENT-UNSUPPORTED for ${JSON.stringify(data)}`)); } }); handler.on("error", (err) => this.log.warn(`${namespaceLog} Redis states: ${err}`)); } /** * Return connected RedisHandlers/Connections * * @returns */ getClients() { return this.serverConnections; } /** * Destructor of the class. Called by shutting down. */ async destroy() { if (this.server) { for (const s of Object.keys(this.serverConnections)) { this.serverConnections[s].close(); delete this.serverConnections[s]; } await new Promise((resolve) => { if (!this.server) { return void resolve(); } try { this.server.close(() => resolve()); } catch (e) { console.log(e.message); resolve(); } }); } await super.destroy(); } /** * Initialize RedisHandler for a new network connection * * @param socket Network socket */ _initSocket(socket) { this.settings.connection.enhancedLogging && this.log.silly(`${this.namespace} Handling new Redis States connection`); const options = { log: this.log, logScope: `${this.namespace} States`, handleAsBuffers: true, enhancedLogging: this.settings.connection.enhancedLogging }; const handler = new import_db_base.RedisHandler(socket, options); this._socketEvents(handler); this.serverConnections[`${socket.remoteAddress}:${socket.remotePort}`] = handler; socket.on("close", () => { if (this.serverConnections[`${socket.remoteAddress}:${socket.remotePort}`]) { delete this.serverConnections[`${socket.remoteAddress}:${socket.remotePort}`]; } }); } /** * Initialize Redis Server * * @param settings Settings object * @returns */ _initRedisServer(settings) { return new Promise((resolve, reject) => { if (settings.secure) { reject(new Error("Secure Redis unsupported for File-DB")); } try { this.server = import_node_net.default.createServer(); this.server.on("error", (err) => this.log.info(`${this.namespace} ${settings.secure ? "Secure " : ""} Error inMem-objects listening on port ${settings.port || 9001}: ${err}`)); this.server.on("connection", (socket) => this._initSocket(socket)); this.server.listen(settings.port || 9e3, settings.host === "localhost" ? (0, import_tools.getLocalAddress)() : settings.host ? settings.host : void 0, () => resolve()); } catch (err) { reject(err); } }); } } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { StatesInMemoryServer }); //# sourceMappingURL=statesInMemServerRedis.js.map