@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
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 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