@iobroker/db-base 7.2.3 → 8.0.0-alpha.1-20260921-e5941ca8a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cjs/index.d.ts +1 -1
- package/build/cjs/index.js +3 -3
- package/build/cjs/index.js.map +2 -2
- package/build/cjs/lib/inMemFileDB.d.ts +155 -44
- package/build/cjs/lib/inMemFileDB.js +108 -46
- package/build/cjs/lib/inMemFileDB.js.map +2 -2
- package/build/cjs/lib/redisHandler.d.ts +1 -1
- package/build/cjs/lib/redisHandler.js +11 -11
- package/build/cjs/lib/redisHandler.js.map +2 -2
- package/build/esm/index.d.ts +1 -1
- package/build/esm/index.d.ts.map +1 -1
- package/build/esm/index.js +1 -1
- package/build/esm/index.js.map +1 -1
- package/build/esm/lib/inMemFileDB.d.ts +155 -44
- package/build/esm/lib/inMemFileDB.d.ts.map +1 -1
- package/build/esm/lib/inMemFileDB.js +107 -49
- package/build/esm/lib/inMemFileDB.js.map +1 -1
- package/build/esm/lib/redisHandler.d.ts +1 -1
- package/build/esm/lib/redisHandler.d.ts.map +1 -1
- package/build/esm/lib/redisHandler.js +7 -11
- package/build/esm/lib/redisHandler.js.map +1 -1
- package/build/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +6 -6
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/inMemFileDB.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * States DB in memory - Server\n *\n * Copyright 2013-2024 bluefox <dogafox@gmail.com>\n *\n * MIT License\n *\n */\n\nimport fs from 'fs-extra';\nimport path from 'node:path';\nimport { tools } from '@iobroker/js-controller-common-db';\nimport type { InternalLogger } from '@iobroker/js-controller-common-db/tools';\nimport { createGzip } from 'node:zlib';\n\n// settings = {\n// change: function (id, state) {},\n// connected: function (nameOfServer) {},\n// logger: {\n// silly: function (msg) {},\n// debug: function (msg) {},\n// info: function (msg) {},\n// warn: function (msg) {},\n// error: function (msg) {}\n// },\n// connection: {\n// dataDir: 'relative path'\n// },\n// auth: null, //unused\n// secure: true/false,\n// certificates: as required by createServer\n// port: 9000,\n// host: localhost\n// };\n//\n\nexport interface ConnectionOptions {\n pass?: string;\n sentinelName?: string;\n /** array on sentinel */\n host: string | string[];\n /** array on sentinel */\n port: number | number[];\n options: Record<string, any>;\n enhancedLogging?: boolean;\n backup?: BackupOptions;\n /** relative path to the data dir */\n dataDir?: string;\n}\n\ntype ChangeFunction = (id: string, state: any) => void;\n\nexport interface DbStatus {\n type: string;\n server: boolean;\n}\n\ninterface BackupOptions {\n /** deactivates backup if true */\n disabled: boolean;\n /** minimum number of files */\n files: number;\n hours: number;\n /** minutes */\n period: number;\n path: string;\n}\n\nexport interface DbOptions {\n backupDirName: string;\n fileName: string;\n}\n\ninterface FileDbSettings {\n fileDB: DbOptions;\n jsonlDB: DbOptions;\n backup: BackupOptions;\n change?: ChangeFunction;\n connected: (nameOfServer: string) => void;\n logger: InternalLogger;\n connection: ConnectionOptions;\n /** unused */\n auth?: null;\n secure: boolean;\n /** as required by createServer TODO: if createServer is typed, add type */\n certificates: any;\n port: number;\n host: string;\n /** logging namespace */\n namespace?: string;\n}\n\ninterface Subscription {\n pattern: string;\n regex: RegExp;\n options: any;\n}\n\ninterface SubscriptionClient {\n _subscribe?: Record<string, Subscription[]>;\n}\n\n/**\n * The parent of the class structure, which provides basic JSON storage\n * and general subscription and publish functionality\n */\nexport class InMemoryFileDB {\n private settings: FileDbSettings;\n private readonly change: ChangeFunction | undefined;\n protected dataset: Record<string, any>;\n private readonly namespace: string;\n private lastSave: null | number;\n private stateTimer: NodeJS.Timeout | null;\n private callbackSubscriptionClient: SubscriptionClient;\n private readonly dataDir: string;\n private readonly datasetName: string;\n private log: InternalLogger;\n private readonly backupDir: string;\n\n constructor(settings: FileDbSettings) {\n this.settings = settings || {};\n\n this.change = this.settings.change;\n\n this.dataset = {};\n\n this.namespace = this.settings.namespace || '';\n this.lastSave = null;\n this.callbackSubscriptionClient = {};\n\n this.settings.backup = this.settings.connection.backup || {\n disabled: false, // deactivates\n files: 24, // minimum number of files\n hours: 48, // hours\n period: 120, // minutes\n path: '', // use default path\n };\n\n this.dataDir = this.settings.connection.dataDir || tools.getDefaultDataDir();\n if (!path.isAbsolute(this.dataDir)) {\n this.dataDir = path.normalize(path.join(tools.getControllerDir(), this.dataDir));\n }\n this.dataDir = this.dataDir.replace(/\\\\/g, '/');\n\n const fileName = this.settings.jsonlDB ? this.settings.jsonlDB.fileName : this.settings.fileDB.fileName;\n this.datasetName = path.join(this.dataDir, fileName);\n const parts = path.dirname(this.datasetName);\n fs.ensureDirSync(parts);\n\n this.stateTimer = null;\n\n this.backupDir = this.settings.backup.path || path.join(this.dataDir, this.settings.fileDB.backupDirName);\n\n this.log = tools.getLogger(this.settings.logger);\n\n if (!this.settings.backup.disabled) {\n try {\n this.initBackupDir();\n } catch (e) {\n this.log.error(\n `Database backups are disabled, because backup directory could not be initialized: ${e.message}`,\n );\n this.log.error(\n 'This leads to an increased risk of data loss, please check that the configured backup directory is available and restart the controller',\n );\n this.settings.backup.disabled = true;\n }\n }\n\n this.log.debug(`${this.namespace} Data File: ${this.datasetName}`);\n }\n\n async open(): Promise<void> {\n // load values from file\n this.dataset = await this.loadDataset(this.datasetName);\n }\n\n /**\n * Loads a dataset file\n *\n * @param datasetName Filename of the file to load\n * @returns obj read data, normally as object\n */\n async loadDatasetFile(datasetName: string): Promise<Record<string, any>> {\n if (!(await fs.pathExists(datasetName))) {\n throw new Error(`Database file ${datasetName} does not exists.`);\n }\n return fs.readJSON(datasetName);\n }\n\n /**\n * Loads the dataset including pot. Fallback handling\n *\n * @param datasetName Filename of the file to load\n * @returns obj dataset read as object\n */\n async loadDataset(datasetName: string): Promise<Record<string, any>> {\n let ret = {};\n try {\n ret = await this.loadDatasetFile(datasetName);\n\n // loading worked, make sure that \"bak\" File is not broken\n try {\n await fs.readJSON(`${datasetName}.bak`);\n } catch (e) {\n this.log.info(\n `${this.namespace} Rewrite bak file, because error on verify ${datasetName}.bak: ${e.message}`,\n );\n try {\n const jsonString = JSON.stringify(ret);\n await fs.writeFile(`${datasetName}.bak`, jsonString);\n } catch (e) {\n this.log.error(`${this.namespace} Cannot save ${datasetName}.bak: ${e.message}`);\n }\n }\n } catch (err) {\n this.log.error(`${this.namespace} Cannot load ${datasetName}: ${err.message}. We try last Backup!`);\n\n try {\n ret = await this.loadDatasetFile(`${datasetName}.bak`);\n\n // it worked, lets overwrite old file and store the broken one for pot. forensic check\n try {\n if (await fs.pathExists(datasetName)) {\n try {\n await fs.move(datasetName, `${datasetName}.broken`, { overwrite: true });\n } catch (e) {\n this.log.error(\n `${this.namespace} Cannot copy the broken file ${datasetName} to ${datasetName}.broken ${e.message}`,\n );\n }\n try {\n await fs.writeFile(datasetName, JSON.stringify(ret));\n } catch (e) {\n this.log.error(\n `${this.namespace} Cannot restore backup file as new main ${datasetName}: ${e.message}`,\n );\n }\n }\n } catch {\n // ignore, file does not exist\n }\n } catch (err) {\n this.log.error(\n `${this.namespace} Cannot load ${datasetName}.bak: ${err.message}. Continue with empty dataset!`,\n );\n this.log.error(\n `${this.namespace} If this is no Migration or initial start please restore the last backup from ${this.backupDir}`,\n );\n }\n }\n return ret;\n }\n\n initBackupDir(): void {\n // Interval in minutes => to milliseconds\n this.settings.backup.period =\n this.settings.backup.period === undefined ? 120 : parseInt(String(this.settings.backup.period));\n if (isNaN(this.settings.backup.period)) {\n this.settings.backup.period = 120;\n }\n // Node.js timeouts overflow after roughly 24 days, defaulting to 1 millisecond, which causes chaos.\n // If a user configured the backup this way, we use our default of 120 minutes instead.\n const maxTimeoutMinutes = Math.floor((2 ** 31 - 1) / 60000);\n if (this.settings.backup.period > maxTimeoutMinutes) {\n this.log.warn(\n `${this.namespace} Configured backup period ${this.settings.backup.period} is larger than the supported maximum of ${maxTimeoutMinutes} minutes. Defaulting to 120 minutes.`,\n );\n this.settings.backup.period = 120;\n }\n this.settings.backup.period *= 60_000;\n\n this.settings.backup.files =\n this.settings.backup.files === undefined ? 24 : parseInt(String(this.settings.backup.files));\n if (isNaN(this.settings.backup.files)) {\n this.settings.backup.files = 24;\n }\n\n this.settings.backup.hours =\n this.settings.backup.hours === undefined ? 48 : parseInt(String(this.settings.backup.hours));\n if (isNaN(this.settings.backup.hours)) {\n this.settings.backup.hours = 48;\n }\n // Create backup directory\n fs.ensureDirSync(this.backupDir);\n }\n\n handleSubscribe(client: SubscriptionClient, type: string, pattern: string | string[], cb?: () => void): void;\n handleSubscribe(\n client: SubscriptionClient,\n type: string,\n pattern: string | string[],\n options: any,\n cb?: () => void,\n ): void;\n\n handleSubscribe(\n client: SubscriptionClient,\n type: string,\n pattern: string | string[],\n options: any,\n cb?: () => void,\n ): void {\n if (typeof options === 'function') {\n cb = options;\n options = undefined;\n }\n client._subscribe = client._subscribe || {};\n client._subscribe[type] = client._subscribe[type] || [];\n\n const s = client._subscribe[type];\n\n if (pattern instanceof Array) {\n pattern.forEach(pattern => {\n if (s.find(sub => sub.pattern === pattern)) {\n return;\n }\n\n s.push({ pattern, regex: new RegExp(tools.pattern2RegEx(pattern)), options });\n });\n } else {\n if (!s.find(sub => sub.pattern === pattern)) {\n s.push({ pattern, regex: new RegExp(tools.pattern2RegEx(pattern)), options });\n }\n }\n\n typeof cb === 'function' && cb();\n }\n\n handleUnsubscribe(\n client: SubscriptionClient,\n type: string,\n pattern: string | string[],\n cb?: () => void,\n ): void | Promise<void> {\n const s = client?._subscribe?.[type];\n if (s) {\n const removeEntry = (p: string): void => {\n const index = s.findIndex(sub => sub.pattern === p);\n if (index > -1) {\n s.splice(index, 1);\n }\n };\n\n if (pattern instanceof Array) {\n pattern.forEach(p => {\n removeEntry(p);\n });\n } else {\n removeEntry(pattern);\n }\n }\n\n return tools.maybeCallback(cb);\n }\n\n publishToClients(_client: SubscriptionClient, _type: string, _id: string, _obj: any): number {\n throw new Error('no communication handling implemented');\n }\n\n deleteOldBackupFiles(baseFilename: string): void {\n // delete files only if settings.backupNumber is not 0\n let files = fs.readdirSync(this.backupDir);\n files.sort();\n const limit = Date.now() - this.settings.backup.hours * 3600000;\n\n files = files.filter(f => f.endsWith(`${baseFilename}.gz`));\n\n while (files.length > this.settings.backup.files) {\n const file = files.shift();\n if (!file) {\n continue;\n }\n // extract time\n const ms = new Date(`${file.substring(0, 10)} ${file.substring(11, 16).replace('-', ':')}:00`).getTime();\n if (limit > ms) {\n try {\n fs.unlinkSync(path.join(this.backupDir, file));\n } catch (e) {\n this.log.error(\n `${this.namespace} Cannot delete file \"${path.join(this.backupDir, file)}: ${e.message}`,\n );\n }\n }\n }\n }\n\n getTimeStr(date: number): string {\n const dateObj = new Date(date);\n\n let text = `${dateObj.getFullYear().toString()}-`;\n let v = dateObj.getMonth() + 1;\n if (v < 10) {\n text += '0';\n }\n text += `${v.toString()}-`;\n\n v = dateObj.getDate();\n if (v < 10) {\n text += '0';\n }\n text += `${v.toString()}_`;\n\n v = dateObj.getHours();\n if (v < 10) {\n text += '0';\n }\n text += `${v.toString()}-`;\n\n v = dateObj.getMinutes();\n if (v < 10) {\n text += '0';\n }\n text += v.toString();\n\n return text;\n }\n\n /**\n * Handle saving the dataset incl. backups\n */\n async saveState(): Promise<void> {\n try {\n const jsonString = await this.saveDataset();\n\n if (!this.settings.backup.disabled && jsonString) {\n this.saveBackup(jsonString);\n }\n } finally {\n if (this.stateTimer) {\n clearTimeout(this.stateTimer);\n this.stateTimer = null;\n }\n }\n }\n\n /**\n * Saves the dataset into File incl. handling of a fallback backup file\n *\n * @returns dataset JSON string of the complete dataset to also be stored into a compressed backup file\n */\n async saveDataset(): Promise<string> {\n const jsonString = JSON.stringify(this.dataset);\n\n try {\n await fs.writeFile(`${this.datasetName}.new`, jsonString);\n } catch (e) {\n this.log.error(`${this.namespace} Cannot save Dataset to ${this.datasetName}.new: ${e.message}`);\n return jsonString;\n }\n\n let bakOk = true;\n try {\n if (await fs.pathExists(this.datasetName)) {\n try {\n await fs.move(this.datasetName, `${this.datasetName}.bak`, { overwrite: true });\n } catch (e) {\n bakOk = false;\n this.log.error(`${this.namespace} Cannot backup file ${this.datasetName}.bak: ${e.message}`);\n }\n } else {\n bakOk = false;\n }\n } catch {\n bakOk = false;\n // ignore, file does not exist\n }\n\n try {\n await fs.move(`${this.datasetName}.new`, this.datasetName, { overwrite: true });\n } catch (e) {\n this.log.error(\n `${this.namespace} Cannot move ${this.datasetName}.new to ${this.datasetName}: ${e.message}. Try direct write as fallback`,\n );\n try {\n await fs.writeFile(this.datasetName, jsonString);\n } catch (e) {\n this.log.error(`${this.namespace} Cannot directly write Dataset to ${this.datasetName}: ${e.message}`);\n return jsonString;\n }\n }\n\n if (!bakOk) {\n // it seems the bak File is not successfully there, write current content again\n try {\n await fs.writeFile(`${this.datasetName}.bak`, jsonString);\n } catch (e) {\n this.log.error(`${this.namespace} Cannot save ${this.datasetName}.bak: ${e.message}`);\n }\n }\n\n return jsonString;\n }\n\n /**\n * Stores a compressed backup of the DB in definable intervals\n *\n * @param jsonString JSON string of the complete dataset to also be stored into a compressed backup file\n */\n saveBackup(jsonString: string): void {\n // save files for the last x hours\n const now = Date.now();\n\n // makes backups only if settings.backupInterval is not 0\n if (this.settings.backup.period && (!this.lastSave || now - this.lastSave > this.settings.backup.period)) {\n this.lastSave = now;\n const backFileName = path.join(\n this.backupDir,\n `${this.getTimeStr(now)}_${this.settings.fileDB.fileName}.gz`,\n );\n\n try {\n if (!fs.existsSync(backFileName)) {\n const output = fs.createWriteStream(backFileName);\n output.on('error', err => {\n this.log.error(`${this.namespace} Cannot save ${this.datasetName}: ${err.stack}`);\n });\n\n const compress = createGzip();\n /* The following line will pipe everything written into compress to the file stream */\n compress.pipe(output);\n /* Since we're piped through the file stream, the following line will do:\n 'Hello World!'->gzip compression->file which is the desired effect */\n compress.write(jsonString);\n compress.end();\n\n // analyse older files\n this.deleteOldBackupFiles(this.settings.fileDB.fileName);\n }\n } catch (e) {\n this.log.error(`${this.namespace} Cannot save backup ${backFileName}: ${e.message}`);\n }\n }\n }\n\n getStatus(): DbStatus {\n return { type: 'file', server: true };\n }\n\n getClients(): Record<string, any> {\n return {};\n }\n\n publishAll(type: string, id: string, obj: any): number {\n if (id === undefined) {\n this.log.error(`${this.namespace} Can not publish empty ID`);\n return 0;\n }\n\n const clients = this.getClients();\n let publishCount = 0;\n\n if (clients && typeof clients === 'object') {\n for (const i of Object.keys(clients)) {\n publishCount += this.publishToClients(clients[i], type, id, obj);\n }\n }\n\n // local subscriptions\n if (\n this.change &&\n this.callbackSubscriptionClient._subscribe &&\n this.callbackSubscriptionClient._subscribe[type]\n ) {\n for (const entry of this.callbackSubscriptionClient._subscribe[type]) {\n if (entry.regex.test(id)) {\n // @ts-expect-error we have checked 3 lines above\n setImmediate(() => this.change(id, obj));\n break;\n }\n }\n }\n\n return publishCount;\n }\n\n // Destructor of the class. Called by shutting down.\n async destroy(): Promise<void> {\n if (this.stateTimer) {\n clearTimeout(this.stateTimer);\n await this.saveState();\n }\n }\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["/**\n * States DB in memory - Server\n *\n * Copyright 2013-2026 bluefox <dogafox@gmail.com>\n *\n * MIT License\n *\n */\n/// <reference types=\"@iobroker/types-dev\" />\n\nimport fs from 'fs-extra';\nimport path from 'node:path';\nimport { tools } from '@iobroker/js-controller-common-db';\nimport type { InternalLogger } from '@iobroker/js-controller-common-db/tools';\nimport { createGzip } from 'node:zlib';\n\n// settings = {\n// change: function (id, state) {},\n// connected: function (nameOfServer) {},\n// logger: {\n// silly: function (msg) {},\n// debug: function (msg) {},\n// info: function (msg) {},\n// warn: function (msg) {},\n// error: function (msg) {}\n// },\n// connection: {\n// dataDir: 'relative path'\n// },\n// auth: null, //unused\n// secure: true/false,\n// certificates: as required by createServer\n// port: 9000,\n// host: localhost\n// };\n//\ninterface BackupOptions {\n /** deactivates backup if true */\n disabled: boolean;\n /** minimum number of files */\n files: number;\n hours: number;\n /** minutes */\n period: number;\n path: string;\n}\n\nexport type MetaObject = Record<string, string>;\nexport type FileSize = number;\n\n/** Options describing how to connect to the database server */\nexport interface ConnectionOptions {\n /** Password for authentication, if required */\n pass?: string;\n /** Name of the sentinel master to connect to */\n sentinelName?: string;\n /** array on sentinel */\n host: string | string[];\n /** array on sentinel */\n port: number | number[];\n /** Additional connection options passed to the database driver */\n options: ioBroker.ObjectsDatabaseOptions['options'];\n /** Options forwarded to the JSONL database backend */\n jsonlOptions?: ioBroker.ObjectsDatabaseOptions['jsonlOptions'];\n /** Redis key prefix used by the in-memory server */\n redisNamespace?: string;\n /** Enable more verbose connection logging */\n enhancedLogging?: boolean;\n /** Disable the in-memory file cache and always read files from disk */\n noFileCache?: boolean;\n /** Whether the connection should be secured via TLS */\n secure?: boolean;\n /** Backup configuration */\n backup?: BackupOptions;\n /** relative path to the data dir */\n dataDir?: string;\n /** Interval in milliseconds in which the in-memory states are persisted to disk */\n writeFileInterval?: number;\n}\n\ntype ChangeFunction<TObject> = (id: string, obj: TObject) => void;\n\n/** Status information about the database */\nexport interface DbStatus {\n /** Type of the database backend (e.g. file, jsonl, redis) */\n type: 'file' | 'redis' | 'jsonl';\n /** Whether this process runs the database server */\n server: boolean;\n}\n\n/** Options describing where the database stores its files */\nexport interface DbOptions {\n /** Name of the directory used for backups */\n backupDirName: string;\n /** Name of the database file */\n fileName: string;\n}\n\n/** Settings accepted by the in-memory database classes and their subclasses */\nexport interface FileDbSettings<TObject> {\n /** File DB storage options (filled in by the subclass constructor before `super()`) */\n fileDB?: DbOptions;\n /** JSONL DB storage options (only set by the JSONL backend; only the file name is used) */\n jsonlDB?: Pick<DbOptions, 'fileName'>;\n /** Backup options (derived from `connection.backup` by the base constructor) */\n backup?: BackupOptions;\n /** Callback invoked when a value in the database changes */\n change?: ChangeFunction<TObject>;\n /** Callback invoked once the database server is ready */\n connected: (nameOfServer?: string) => void;\n /** Logger instance (or logger configuration) used for all database logging */\n logger?: InternalLogger;\n /** Options describing how to connect to the database server */\n connection: ConnectionOptions;\n /** unused */\n auth?: null;\n /** Whether the server connection should be secured via TLS */\n secure?: boolean;\n /** Port the in-memory server listens on */\n port?: number;\n /** Host the in-memory server binds to */\n host?: string;\n /** logging namespace */\n namespace?: string;\n /** Host name used as a fallback for the logging namespace */\n hostname?: string;\n /** Default ACL applied to newly created objects */\n defaultNewAcl?: {\n object: number;\n state: number;\n file: number;\n owner: ioBroker.ObjectIDs.User;\n ownerGroup: ioBroker.ObjectIDs.Group;\n };\n /** Redis key prefix used by the in-memory server (defaults to \"io\") */\n redisNamespace?: string;\n /** Namespace used for the message box */\n namespaceMsg?: string;\n /** Namespace used for log messages */\n namespaceLog?: string;\n /** Namespace used for sessions */\n namespaceSession?: string;\n /** Namespace used for meta information */\n metaNamespace?: string;\n /** Redis key prefix used for object meta information (defaults to \"meta\") */\n namespaceMeta?: string;\n}\n\ninterface Subscription {\n pattern: string;\n regex: RegExp;\n}\n\ninterface SubscriptionClient {\n _subscribe?: Record<string, Subscription[]>;\n}\n\n/**\n * The parent of the class structure, which provides basic JSON storage\n * and general subscription and publish functionality\n */\nexport class InMemoryFileDB<TObject, THandler extends SubscriptionClient = SubscriptionClient> {\n protected settings: FileDbSettings<TObject>;\n protected change: ChangeFunction<TObject> | undefined;\n protected dataset: Record<string, TObject | MetaObject> = {};\n protected namespace: string;\n private lastSave: null | number = null;\n protected stateTimer: NodeJS.Timeout | null = null;\n private callbackSubscriptionClient: SubscriptionClient = {};\n protected readonly dataDir: string;\n private readonly datasetName: string;\n protected log: InternalLogger;\n protected readonly backupDir: string;\n\n /**\n * @param settings Settings for the database, the connection and backups\n */\n constructor(settings: FileDbSettings<TObject>) {\n this.settings = settings;\n\n this.change = this.settings.change;\n\n this.namespace = this.settings.namespace || '';\n\n this.settings.backup = this.settings.connection.backup || {\n disabled: false, // deactivates\n files: 24, // minimum number of files\n hours: 48, // hours\n period: 120, // minutes\n path: '', // use default path\n };\n const backup: BackupOptions = this.settings.backup;\n\n this.dataDir = this.settings.connection.dataDir || tools.getDefaultDataDir();\n if (!path.isAbsolute(this.dataDir)) {\n this.dataDir = path.normalize(path.join(tools.getControllerDir(), this.dataDir));\n }\n this.dataDir = this.dataDir.replace(/\\\\/g, '/');\n\n const fileDbOptions = this.settings.fileDB;\n if (!fileDbOptions) {\n throw new Error(`${this.namespace} No fileDB storage options were provided`);\n }\n const fileName = this.settings.jsonlDB ? this.settings.jsonlDB.fileName : fileDbOptions.fileName;\n this.datasetName = path.join(this.dataDir, fileName);\n const parts = path.dirname(this.datasetName);\n fs.ensureDirSync(parts);\n\n this.backupDir = backup.path || path.join(this.dataDir, fileDbOptions.backupDirName);\n\n this.log = tools.getLogger(this.settings.logger);\n\n if (!backup.disabled) {\n try {\n this.initBackupDir();\n } catch (e) {\n this.log.error(\n `Database backups are disabled, because backup directory could not be initialized: ${e.message}`,\n );\n this.log.error(\n 'This leads to an increased risk of data loss, please check that the configured backup directory is available and restart the controller',\n );\n backup.disabled = true;\n }\n }\n\n this.log.debug(`${this.namespace} Data File: ${this.datasetName}`);\n }\n\n /**\n * Open the database by loading the dataset from disk\n */\n async open(): Promise<void> {\n // load values from file\n this.dataset = await this.loadDataset(this.datasetName);\n }\n\n /**\n * Loads a dataset file\n *\n * @param datasetName Filename of the file to load\n * @returns obj read data, normally as object\n */\n async loadDatasetFile(datasetName: string): Promise<Record<string, TObject>> {\n if (!(await fs.pathExists(datasetName))) {\n throw new Error(`Database file ${datasetName} does not exists.`);\n }\n return fs.readJSON(datasetName);\n }\n\n /**\n * Loads the dataset including pot. Fallback handling\n *\n * @param datasetName Filename of the file to load\n * @returns obj dataset read as object\n */\n async loadDataset(datasetName: string): Promise<Record<string, TObject>> {\n let ret = {};\n try {\n ret = await this.loadDatasetFile(datasetName);\n\n // loading worked, make sure that \"bak\" File is not broken\n try {\n await fs.readJSON(`${datasetName}.bak`);\n } catch (e) {\n this.log.info(\n `${this.namespace} Rewrite bak file, because error on verify ${datasetName}.bak: ${e.message}`,\n );\n try {\n const jsonString = JSON.stringify(ret);\n await fs.writeFile(`${datasetName}.bak`, jsonString);\n } catch (e) {\n this.log.error(`${this.namespace} Cannot save ${datasetName}.bak: ${e.message}`);\n }\n }\n } catch (err) {\n this.log.error(`${this.namespace} Cannot load ${datasetName}: ${err.message}. We try last Backup!`);\n\n try {\n ret = await this.loadDatasetFile(`${datasetName}.bak`);\n\n // it worked, lets overwrite old file and store the broken one for pot. forensic check\n try {\n if (await fs.pathExists(datasetName)) {\n try {\n await fs.move(datasetName, `${datasetName}.broken`, { overwrite: true });\n } catch (e) {\n this.log.error(\n `${this.namespace} Cannot copy the broken file ${datasetName} to ${datasetName}.broken ${e.message}`,\n );\n }\n try {\n await fs.writeFile(datasetName, JSON.stringify(ret));\n } catch (e) {\n this.log.error(\n `${this.namespace} Cannot restore backup file as new main ${datasetName}: ${e.message}`,\n );\n }\n }\n } catch {\n // ignore, file does not exist\n }\n } catch (err) {\n this.log.error(\n `${this.namespace} Cannot load ${datasetName}.bak: ${err.message}. Continue with empty dataset!`,\n );\n this.log.error(\n `${this.namespace} If this is no Migration or initial start please restore the last backup from ${this.backupDir}`,\n );\n }\n }\n return ret;\n }\n\n /**\n * Normalize the backup settings and create the backup directory\n */\n initBackupDir(): void {\n // Create backup directory\n fs.ensureDirSync(this.backupDir);\n\n const backup = this.settings.backup;\n if (!backup) {\n return;\n }\n // Interval in minutes => to milliseconds\n backup.period = backup.period === undefined ? 120 : parseInt(String(backup.period));\n if (isNaN(backup.period)) {\n backup.period = 120;\n }\n // Node.js timeouts overflow after roughly 24 days, defaulting to 1 millisecond, which causes chaos.\n // If a user configured the backup this way, we use our default of 120 minutes instead.\n const maxTimeoutMinutes = Math.floor((2 ** 31 - 1) / 60000);\n if (backup.period > maxTimeoutMinutes) {\n this.log.warn(\n `${this.namespace} Configured backup period ${backup.period} is larger than the supported maximum of ${maxTimeoutMinutes} minutes. Defaulting to 120 minutes.`,\n );\n backup.period = 120;\n }\n backup.period *= 60_000;\n\n backup.files = backup.files === undefined ? 24 : parseInt(String(backup.files));\n if (isNaN(backup.files)) {\n backup.files = 24;\n }\n\n backup.hours = backup.hours === undefined ? 48 : parseInt(String(backup.hours));\n if (isNaN(backup.hours)) {\n backup.hours = 48;\n }\n }\n\n /**\n * Subscribe a client to changes matching the given pattern\n *\n * @param client The client to register the subscription for\n * @param type The subscription type (e.g. objects or states)\n * @param pattern One or more patterns to subscribe to\n */\n handleSubscribe(client: SubscriptionClient, type: string, pattern: string | string[]): void {\n client._subscribe ||= {};\n client._subscribe[type] ||= [];\n\n const s = client._subscribe[type];\n\n if (pattern instanceof Array) {\n pattern.forEach(pattern => {\n if (s.find(sub => sub.pattern === pattern)) {\n return;\n }\n\n s.push({ pattern, regex: new RegExp(tools.pattern2RegEx(pattern)) });\n });\n } else {\n if (!s.find(sub => sub.pattern === pattern)) {\n s.push({ pattern, regex: new RegExp(tools.pattern2RegEx(pattern)) });\n }\n }\n }\n\n /**\n * Remove a client's subscription for the given pattern\n *\n * @param client The client whose subscription should be removed\n * @param type The subscription type (e.g. objects or states)\n * @param pattern One or more patterns to unsubscribe from\n * @param cb Called once the subscription has been removed\n */\n handleUnsubscribe(\n client: SubscriptionClient,\n type: string,\n pattern: string | string[],\n cb?: () => void,\n ): void | Promise<void> {\n const s = client?._subscribe?.[type];\n if (s) {\n const removeEntry = (p: string): void => {\n const index = s.findIndex(sub => sub.pattern === p);\n if (index > -1) {\n s.splice(index, 1);\n }\n };\n\n if (pattern instanceof Array) {\n pattern.forEach(p => {\n removeEntry(p);\n });\n } else {\n removeEntry(pattern);\n }\n }\n\n return tools.maybeCallback(cb);\n }\n\n /**\n * Publish a change to a single client. Must be implemented by a subclass that handles client communication.\n *\n * @param _client The client to publish to\n * @param _type The change type (e.g. objects or states)\n * @param _id The ID of the changed object or state\n * @param _obj The new value of the object or state\n */\n publishToClients(\n _client: SubscriptionClient,\n _type: string,\n _id: string,\n _obj: TObject | string | null | FileSize | MetaObject,\n ): number {\n throw new Error('no communication handling implemented');\n }\n\n /**\n * Delete outdated backup files according to the configured retention settings\n *\n * @param baseFilename Base file name of the backups that should be pruned\n */\n deleteOldBackupFiles(baseFilename: string): void {\n // delete files only if settings.backupNumber is not 0\n let files = fs.readdirSync(this.backupDir);\n files.sort();\n const limit = Date.now() - (this.settings.backup?.hours ?? 48) * 3600000;\n\n files = files.filter(f => f.endsWith(`${baseFilename}.gz`));\n\n while (files.length > (this.settings.backup?.files ?? 24)) {\n const file = files.shift();\n if (!file) {\n continue;\n }\n // extract time\n const ms = new Date(`${file.substring(0, 10)} ${file.substring(11, 16).replace('-', ':')}:00`).getTime();\n if (limit > ms) {\n try {\n fs.unlinkSync(path.join(this.backupDir, file));\n } catch (e) {\n this.log.error(\n `${this.namespace} Cannot delete file \"${path.join(this.backupDir, file)}: ${e.message}`,\n );\n }\n }\n }\n }\n\n /**\n * Format a timestamp into a \"YYYY-MM-DD_HH-MM-SS\" string used for backup file names\n *\n * @param date Timestamp in milliseconds\n */\n getTimeStr(date: number): string {\n const dateObj = new Date(date);\n\n let text = `${dateObj.getFullYear().toString()}-`;\n let v = dateObj.getMonth() + 1;\n if (v < 10) {\n text += '0';\n }\n text += `${v.toString()}-`;\n\n v = dateObj.getDate();\n if (v < 10) {\n text += '0';\n }\n text += `${v.toString()}_`;\n\n v = dateObj.getHours();\n if (v < 10) {\n text += '0';\n }\n text += `${v.toString()}-`;\n\n v = dateObj.getMinutes();\n if (v < 10) {\n text += '0';\n }\n text += v.toString();\n\n return text;\n }\n\n /**\n * Handle saving the dataset incl. backups\n */\n async saveState(): Promise<void> {\n try {\n const jsonString = await this.saveDataset();\n\n if (!this.settings.backup?.disabled && jsonString) {\n this.saveBackup(jsonString);\n }\n } finally {\n if (this.stateTimer) {\n clearTimeout(this.stateTimer);\n this.stateTimer = null;\n }\n }\n }\n\n /**\n * Saves the dataset into File incl. handling of a fallback backup file\n *\n * @returns dataset JSON string of the complete dataset to also be stored into a compressed backup file\n */\n async saveDataset(): Promise<string> {\n const jsonString = JSON.stringify(this.dataset);\n\n try {\n await fs.writeFile(`${this.datasetName}.new`, jsonString);\n } catch (e) {\n this.log.error(`${this.namespace} Cannot save Dataset to ${this.datasetName}.new: ${e.message}`);\n return jsonString;\n }\n\n let bakOk = true;\n try {\n if (await fs.pathExists(this.datasetName)) {\n try {\n await fs.move(this.datasetName, `${this.datasetName}.bak`, { overwrite: true });\n } catch (e) {\n bakOk = false;\n this.log.error(`${this.namespace} Cannot backup file ${this.datasetName}.bak: ${e.message}`);\n }\n } else {\n bakOk = false;\n }\n } catch {\n bakOk = false;\n // ignore, file does not exist\n }\n\n try {\n await fs.move(`${this.datasetName}.new`, this.datasetName, { overwrite: true });\n } catch (e) {\n this.log.error(\n `${this.namespace} Cannot move ${this.datasetName}.new to ${this.datasetName}: ${e.message}. Try direct write as fallback`,\n );\n try {\n await fs.writeFile(this.datasetName, jsonString);\n } catch (e) {\n this.log.error(`${this.namespace} Cannot directly write Dataset to ${this.datasetName}: ${e.message}`);\n return jsonString;\n }\n }\n\n if (!bakOk) {\n // it seems the bak File is not successfully there, write current content again\n try {\n await fs.writeFile(`${this.datasetName}.bak`, jsonString);\n } catch (e) {\n this.log.error(`${this.namespace} Cannot save ${this.datasetName}.bak: ${e.message}`);\n }\n }\n\n return jsonString;\n }\n\n /**\n * Stores a compressed backup of the DB in definable intervals\n *\n * @param jsonString JSON string of the complete dataset to also be stored into a compressed backup file\n */\n saveBackup(jsonString: string): void {\n // save files for the last x hours\n const now = Date.now();\n\n // makes backups only if settings.backupInterval is not 0\n const fileName = this.settings.fileDB?.fileName;\n if (\n fileName &&\n this.settings.backup?.period &&\n (!this.lastSave || now - this.lastSave > this.settings.backup.period)\n ) {\n this.lastSave = now;\n const backFileName = path.join(this.backupDir, `${this.getTimeStr(now)}_${fileName}.gz`);\n\n try {\n if (!fs.existsSync(backFileName)) {\n const output = fs.createWriteStream(backFileName);\n output.on('error', err => {\n this.log.error(`${this.namespace} Cannot save ${this.datasetName}: ${err.stack}`);\n });\n\n const compress = createGzip();\n /* The following line will pipe everything written into compress to the file stream */\n compress.pipe(output);\n /* Since we're piped through the file stream, the following line will do:\n 'Hello World!'->gzip compression->file which is the desired effect */\n compress.write(jsonString);\n compress.end();\n\n // analyse older files\n this.deleteOldBackupFiles(fileName);\n }\n } catch (e) {\n this.log.error(`${this.namespace} Cannot save backup ${backFileName}: ${e.message}`);\n }\n }\n }\n\n /**\n * Get the current status of the database\n */\n getStatus(): DbStatus {\n return { type: 'file', server: true };\n }\n\n /**\n * Get the currently connected clients. Subclasses override this to return the real clients.\n */\n getClients(): Record<string, THandler> {\n return {};\n }\n\n /**\n * Publish a change to all connected clients and local subscribers\n *\n * @param type The change type (e.g. objects or states)\n * @param id The ID of the changed object or state\n * @param obj The new value of the object or state\n */\n publishAll(type: string, id: string, obj: TObject | string | null | FileSize | MetaObject): number {\n if (id === undefined) {\n this.log.error(`${this.namespace} Can not publish empty ID`);\n return 0;\n }\n\n const clients = this.getClients();\n let publishCount = 0;\n\n if (clients && typeof clients === 'object') {\n for (const i of Object.keys(clients)) {\n publishCount += this.publishToClients(clients[i], type, id, obj);\n }\n }\n\n // local subscriptions\n if (\n this.change &&\n this.callbackSubscriptionClient._subscribe &&\n this.callbackSubscriptionClient._subscribe[type]\n ) {\n for (const entry of this.callbackSubscriptionClient._subscribe[type]) {\n if (entry.regex.test(id)) {\n // @ts-expect-error we have checked 3 lines above\n setImmediate(() => this.change(id, obj));\n break;\n }\n }\n }\n\n return publishCount;\n }\n\n /**\n * Destructor of the class. Called when shutting down to persist state and clear timers.\n */\n async destroy(): Promise<void> {\n if (this.stateTimer) {\n clearTimeout(this.stateTimer);\n await this.saveState();\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["path", "fs", "e", "err", "pattern"]
|
|
7
7
|
}
|
|
@@ -50,7 +50,7 @@ export declare class RedisHandler extends EventEmitter {
|
|
|
50
50
|
*/
|
|
51
51
|
_handleCommand(data: any[]): void;
|
|
52
52
|
/**
|
|
53
|
-
* Check if the response to a certain command can be
|
|
53
|
+
* Check if the response to a certain command can be sent out directly or
|
|
54
54
|
* if it needs to wait till earlier responses are ready
|
|
55
55
|
*
|
|
56
56
|
* @param responseId ID of the response
|
|
@@ -5,6 +5,7 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
8
9
|
var __export = (target, all) => {
|
|
9
10
|
for (var name in all)
|
|
10
11
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -35,16 +36,19 @@ var import_respjs = __toESM(require("respjs"), 1);
|
|
|
35
36
|
var import_node_events = require("node:events");
|
|
36
37
|
var import_constants = require("./constants.js");
|
|
37
38
|
class RedisHandler extends import_node_events.EventEmitter {
|
|
39
|
+
static {
|
|
40
|
+
__name(this, "RedisHandler");
|
|
41
|
+
}
|
|
38
42
|
socket;
|
|
39
43
|
logScope;
|
|
40
44
|
handleBuffers;
|
|
41
45
|
options;
|
|
42
46
|
log;
|
|
43
47
|
socketId;
|
|
44
|
-
initialized;
|
|
45
|
-
stop;
|
|
46
|
-
activeMultiCalls;
|
|
47
|
-
writeQueue;
|
|
48
|
+
initialized = false;
|
|
49
|
+
stop = false;
|
|
50
|
+
activeMultiCalls = [];
|
|
51
|
+
writeQueue = [];
|
|
48
52
|
responseId = 0;
|
|
49
53
|
resp;
|
|
50
54
|
/**
|
|
@@ -55,7 +59,7 @@ class RedisHandler extends import_node_events.EventEmitter {
|
|
|
55
59
|
*/
|
|
56
60
|
constructor(socket, options) {
|
|
57
61
|
super();
|
|
58
|
-
options
|
|
62
|
+
options ||= {};
|
|
59
63
|
this.options = options;
|
|
60
64
|
this.log = options.log || console;
|
|
61
65
|
this.logScope = options.logScope || "";
|
|
@@ -64,10 +68,6 @@ class RedisHandler extends import_node_events.EventEmitter {
|
|
|
64
68
|
}
|
|
65
69
|
this.socket = socket;
|
|
66
70
|
this.socketId = `${this.logScope + socket.remoteAddress}:${socket.remotePort}`;
|
|
67
|
-
this.initialized = false;
|
|
68
|
-
this.stop = false;
|
|
69
|
-
this.activeMultiCalls = [];
|
|
70
|
-
this.writeQueue = [];
|
|
71
71
|
this.handleBuffers = false;
|
|
72
72
|
const respOptions = {};
|
|
73
73
|
if (options.handleAsBuffers) {
|
|
@@ -76,7 +76,7 @@ class RedisHandler extends import_node_events.EventEmitter {
|
|
|
76
76
|
}
|
|
77
77
|
this.resp = new import_respjs.default(respOptions);
|
|
78
78
|
this.resp.on("error", (err) => {
|
|
79
|
-
this.log.error(`${this.socketId} (Init=${this.initialized}) Redis error
|
|
79
|
+
this.log.error(`${this.socketId} (Init=${this.initialized}) Redis error: ${err}`);
|
|
80
80
|
if (this.initialized) {
|
|
81
81
|
this.sendError(null, new Error(`PARSER ERROR ${err}`));
|
|
82
82
|
} else {
|
|
@@ -153,7 +153,7 @@ class RedisHandler extends import_node_events.EventEmitter {
|
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
/**
|
|
156
|
-
* Check if the response to a certain command can be
|
|
156
|
+
* Check if the response to a certain command can be sent out directly or
|
|
157
157
|
* if it needs to wait till earlier responses are ready
|
|
158
158
|
*
|
|
159
159
|
* @param responseId ID of the response
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/redisHandler.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Socket } from 'node:net';\n// @ts-expect-error no types\nimport Resp from 'respjs';\nimport { EventEmitter } from 'node:events';\nimport { QUEUED_STR_BUF, OK_STR_BUF } from './constants.js';\nimport type { InternalLogger } from '@iobroker/js-controller-common-db/tools';\n\ntype NestedArray<T> = T[] | NestedArray<T>[];\n\ninterface RedisHandlerOptions {\n // Logger object\n log: InternalLogger;\n // log prefix\n logScope?: string;\n // if data should be handled as buffer\n handleAsBuffers: boolean;\n // additional debug information\n enhancedLogging?: boolean;\n}\n\ninterface WriteQueueElement {\n // response id\n id: ResponseId;\n // response data, temporary false if not ready to sent yet\n data: false | Buffer;\n}\n\ninterface FullWriteQueueElement extends WriteQueueElement {\n // response data\n data: Buffer;\n}\n\ninterface MultiCallElement {\n // all response ids of the multi call\n responseIds: ResponseId[];\n // indicator if exec already called\n execCalled: boolean;\n // number of responses which are already ready\n responseCount: number;\n // all responses as a map in correct order, key is responseId, value is null if no response there yet\n responseMap: Map<ResponseId, Buffer | null>;\n // id of the exec response\n execId?: ResponseId;\n}\n\ninterface FullMultiCallElement extends MultiCallElement {\n // id of the exec response\n execId: ResponseId;\n}\n\n// null if send without id\ntype ResponseId = number | null;\n\n/**\n * Class to handle a redis connection and provide events to react on for\n * all incoming Redis commands\n */\nexport class RedisHandler extends EventEmitter {\n private readonly socket: Socket;\n private readonly logScope: string;\n private readonly handleBuffers: boolean;\n private readonly options: RedisHandlerOptions;\n private readonly log: InternalLogger;\n private readonly socketId: string;\n private initialized: boolean;\n private stop: boolean;\n private readonly activeMultiCalls: MultiCallElement[];\n private readonly writeQueue: WriteQueueElement[];\n private responseId: number = 0;\n private readonly resp: any;\n\n /**\n * Initialize and register all data handlers to send out events on new commands\n *\n * @param socket Network Socket/Connection\n * @param options options objects, currently mainly for logger\n */\n constructor(socket: Socket, options: RedisHandlerOptions) {\n super();\n\n options = options || {};\n this.options = options;\n this.log = options.log || console;\n this.logScope = options.logScope || '';\n if (this.logScope.length) {\n this.logScope += ' ';\n }\n this.socket = socket;\n\n this.socketId = `${this.logScope + socket.remoteAddress}:${socket.remotePort}`;\n this.initialized = false;\n this.stop = false;\n\n this.activeMultiCalls = [];\n this.writeQueue = [];\n\n this.handleBuffers = false;\n const respOptions: Record<string, any> = {};\n if (options.handleAsBuffers) {\n this.handleBuffers = true;\n respOptions.bufBulk = true;\n }\n this.resp = new Resp(respOptions);\n\n this.resp.on('error', (err: any) => {\n this.log.error(`${this.socketId} (Init=${this.initialized}) Redis error:${err}`);\n if (this.initialized) {\n this.sendError(null, new Error(`PARSER ERROR ${err}`)); // TODO\n } else {\n this.close();\n }\n });\n\n this.resp.on('data', (data: any) => this._handleCommand(data));\n\n socket.on('data', data => {\n if (this.options.enhancedLogging) {\n this.log.silly(\n `${this.socketId} New Redis request: ${\n data.length > 1024\n ? `${data\n .toString()\n .replace(/[\\r\\n]+/g, '')\n .substring(0, 100)} -- ${data.length} bytes`\n : data.toString().replace(/[\\r\\n]+/g, '')\n }`,\n );\n }\n this.resp.write(data);\n });\n\n socket.on('error', err => {\n if (!this.stop) {\n this.log.debug(`${this.socketId} Redis Socket error: ${err.stack}`);\n }\n if (this.socket) {\n this.socket.destroy();\n }\n });\n }\n\n /**\n * Handle one incoming command, assign responseId and emit event to be handled\n *\n * @param data Array RESP data\n */\n _handleCommand(data: any[]): void {\n let command = data.splice(0, 1)[0];\n if (this.handleBuffers) {\n // Command and pot. first parameter should always be Buffer\n command = command.toString('utf-8');\n if (Buffer.isBuffer(data[0])) {\n data[0] = data[0].toString('utf-8');\n }\n // Binary data only relevant for GET and SET in our case\n if (command !== 'set' && data.length > 1) {\n for (let i = 1; i < data.length; i++) {\n if (Buffer.isBuffer(data[i])) {\n data[i] = data[i].toString('utf-8');\n }\n }\n }\n }\n\n if (this.responseId === Number.MAX_VALUE) {\n this.responseId = 0;\n }\n const responseId = ++this.responseId;\n\n if (this.options.enhancedLogging) {\n this.log.silly(\n `${this.socketId} Parser result: id=${responseId}, command=${command}, data=${\n JSON.stringify(data).length > 1024\n ? `${JSON.stringify(data).substring(0, 100)} -- ${JSON.stringify(data).length} bytes`\n : JSON.stringify(data)\n }`,\n );\n }\n\n if (command === 'multi') {\n if (this.activeMultiCalls.length && !this.activeMultiCalls[0].execCalled) {\n // should never happen\n this.log.warn(`${this.socketId} Conflicting multi call`);\n }\n this._handleMulti();\n return;\n }\n\n // multi active and exec not called yet\n if (this.activeMultiCalls.length && !this.activeMultiCalls[0].execCalled && command !== 'exec') {\n // store all response ids so we know which need to be in the multi call\n this.activeMultiCalls[0].responseIds.push(responseId);\n // add it for the correct order will be overwritten with correct response\n this.activeMultiCalls[0].responseMap.set(responseId, null);\n } else {\n // multi response ids should not be pushed - we will answer combined\n this.writeQueue.push({ id: responseId, data: false });\n }\n\n if (command === 'exec') {\n this._handleExec(responseId);\n return;\n }\n\n if (command === 'info') {\n this.initialized = true;\n }\n\n if (this.listenerCount(command) !== 0) {\n setImmediate(() => this.emit(command, data, responseId));\n } else {\n this.sendError(responseId, new Error(`${command} NOT SUPPORTED`));\n }\n }\n\n /**\n * Check if the response to a certain command can be send out directly or\n * if it needs to wait till earlier responses are ready\n *\n * @param responseId ID of the response\n * @param data Buffer to send out\n */\n _sendQueued(responseId: ResponseId, data: Buffer): void {\n let idx = 0;\n\n while (this.writeQueue.length && idx < this.writeQueue.length) {\n // we found the queue entry that matches with the responseId, so store the data so be sent out\n if (this.writeQueue[idx].id === responseId) {\n this.writeQueue[idx].data = data;\n // if we found it not on first index we are done because we can not send it out,\n // need to wait for index 0 to have a complete answer\n if (idx > 0) {\n break;\n }\n }\n // when data for queue entry 0 are preset (!== false) we can send it, remove the first entry\n // and check the other entries if they have completed responses too\n if (idx === 0 && this.writeQueue[idx].data !== false) {\n const response = this.writeQueue.shift() as FullWriteQueueElement;\n if (this.options.enhancedLogging) {\n this.log.silly(\n `${this.socketId} Redis response (${response.id}): ${\n response.data.length > 1024\n ? `${data.length} bytes`\n : response.data.toString().replace(/[\\r\\n]+/g, '')\n }`,\n );\n }\n\n this._write(response.data);\n // We sended out first queue entry but no further response is ready\n // and we do not need to check the whole queue, so we are done here\n if (this.writeQueue.length && this.writeQueue[idx].data === false) {\n break;\n }\n } else {\n // we have not found the response on the current index, try next one\n idx++;\n }\n }\n\n if (idx > 0) {\n if (this.options.enhancedLogging) {\n this.log.silly(`${this.socketId} Redis response (${responseId}): Response queued`);\n }\n }\n }\n\n /**\n * Really write out a response to the network connection\n *\n * @param data Buffer to send out\n */\n _write(data: Buffer): void {\n this.socket.write(data);\n }\n\n /**\n * Guard to make sure a response is valid to be sent out\n *\n * @param responseId ID of the response\n * @param data Buffer to send out\n */\n sendResponse(responseId: ResponseId, data: Buffer): void {\n // handle responses without a specific request like publishing data, so send directly\n if (responseId === null) {\n if (this.options.enhancedLogging) {\n this.log.silly(`${this.socketId} Redis response DIRECT: ${data.toString().replace(/[\\r\\n]+/g, '')}`);\n }\n return this._write(data);\n }\n if (!responseId) {\n throw new Error('Invalid implementation: no responseId provided!');\n }\n if (!data) {\n this.log.warn(`${this.socketId} Not able to write ${JSON.stringify(data)}`);\n data = Resp.encodeError(new Error(`INVALID RESPONSE: ${JSON.stringify(data)}`));\n }\n\n setImmediate(() => this._sendQueued(responseId, data));\n }\n\n /**\n * Close network connection\n */\n close(): void {\n this.log.silly(`${this.socketId} close Redis connection`);\n this.stop = true;\n this.socket.end();\n }\n\n /**\n * Return if socket/handler active or closed\n *\n * @returns is Handler/Connection active (not closed)\n */\n isActive(): boolean {\n return !this.stop;\n }\n\n /**\n * Encode RESP's Null value to RESP buffer and send out\n *\n * @param responseId ID of the response\n */\n sendNull(responseId: ResponseId): void {\n for (let i = 0; i < this.activeMultiCalls.length; i++) {\n if (this.activeMultiCalls[i].responseIds.includes(responseId)) {\n this._handleMultiResponse(responseId, i, Resp.encodeNull());\n return;\n }\n }\n\n this.sendResponse(responseId, Resp.encodeNull());\n }\n\n /**\n * Encode RESP's Null Array value to RESP buffer and send out\n *\n * @param responseId ID of the response\n */\n sendNullArray(responseId: ResponseId): void {\n for (let i = 0; i < this.activeMultiCalls.length; i++) {\n if (this.activeMultiCalls[i].responseIds.includes(responseId)) {\n this._handleMultiResponse(responseId, i, Resp.encodeNullArray());\n return;\n }\n }\n\n this.sendResponse(responseId, Resp.encodeNullArray());\n }\n\n /**\n * Encode string to RESP buffer and send out\n *\n * @param responseId ID od the response\n * @param str String to encode\n */\n sendString(responseId: ResponseId, str: string): void {\n for (let i = 0; i < this.activeMultiCalls.length; i++) {\n if (this.activeMultiCalls[i].responseIds.includes(responseId)) {\n this._handleMultiResponse(responseId, i, Resp.encodeString(str));\n return;\n }\n }\n\n this.sendResponse(responseId, Resp.encodeString(str));\n }\n\n /**\n * Encode error object to RESP buffer and send out\n *\n * @param responseId ID of the response\n * @param error Error object with error details to send out\n */\n sendError(responseId: ResponseId, error: Error): void {\n this.log.warn(`${this.socketId} Error from InMemDB: ${error.stack}`);\n\n for (let i = 0; i < this.activeMultiCalls.length; i++) {\n if (this.activeMultiCalls[i].responseIds.includes(responseId)) {\n this._handleMultiResponse(responseId, i, Resp.encodeError(error));\n return;\n }\n }\n\n this.sendResponse(responseId, Resp.encodeError(error));\n }\n\n /**\n * Encode integer to RESP buffer and send out\n *\n * @param responseId ID of the response\n * @param num Integer to send out\n */\n sendInteger(responseId: ResponseId, num: number): void {\n for (let i = 0; i < this.activeMultiCalls.length; i++) {\n if (this.activeMultiCalls[i].responseIds.includes(responseId)) {\n this._handleMultiResponse(responseId, i, Resp.encodeInteger(num));\n return;\n }\n }\n\n this.sendResponse(responseId, Resp.encodeInteger(num));\n }\n\n /**\n * Encode RESP's bulk string to RESP buffer and send out\n *\n * @param responseId ID of the response\n * @param str String to send out\n */\n sendBulk(responseId: ResponseId, str: string): void {\n for (let i = 0; i < this.activeMultiCalls.length; i++) {\n if (this.activeMultiCalls[i].responseIds.includes(responseId)) {\n this._handleMultiResponse(responseId, i, Resp.encodeBulk(str));\n return;\n }\n }\n\n this.sendResponse(responseId, Resp.encodeBulk(str));\n }\n\n /**\n * Encode RESP's bulk buffer to RESP buffer.\n *\n * @param responseId ID of the response\n * @param buf Buffer to send out\n */\n sendBufBulk(responseId: ResponseId, buf: Buffer): void {\n for (let i = 0; i < this.activeMultiCalls.length; i++) {\n if (this.activeMultiCalls[i].responseIds.includes(responseId)) {\n this._handleMultiResponse(responseId, i, Resp.encodeBufBulk(buf));\n return;\n }\n }\n\n this.sendResponse(responseId, Resp.encodeBufBulk(buf));\n }\n\n /**\n * Encode an Array depending on the type of the elements\n *\n * @param arr Array to encode\n * @returns Array with Buffers with encoded values\n */\n encodeRespArray(arr: unknown[]): NestedArray<Buffer> {\n const returnArr: NestedArray<Buffer> = new Array(arr.length);\n arr.forEach((value, i) => {\n if (Array.isArray(value)) {\n returnArr[i] = this.encodeRespArray(value);\n } else if (Buffer.isBuffer(value)) {\n returnArr[i] = Resp.encodeBufBulk(value);\n } else if (value === null) {\n returnArr[i] = Resp.encodeNull();\n } else if (typeof value === 'number') {\n returnArr[i] = Resp.encodeInteger(value);\n } else {\n returnArr[i] = Resp.encodeBulk(value);\n }\n });\n\n return returnArr;\n }\n\n /**\n * Encode a array values to buffers and send out\n *\n * @param responseId ID of the response\n * @param arr Array to send out\n */\n sendArray(responseId: ResponseId, arr: any[]): void {\n for (let i = 0; i < this.activeMultiCalls.length; i++) {\n if (this.activeMultiCalls[i].responseIds.includes(responseId)) {\n this._handleMultiResponse(responseId, i, Resp.encodeArray(this.encodeRespArray(arr)));\n return;\n }\n }\n\n this.sendResponse(responseId, Resp.encodeArray(this.encodeRespArray(arr)));\n }\n\n /**\n * Handles a 'multi' command\n *\n */\n _handleMulti(): void {\n this.activeMultiCalls.unshift({\n responseIds: [],\n execCalled: false,\n responseCount: 0,\n responseMap: new Map(),\n });\n }\n\n /**\n * Handles an 'exec' command\n *\n * @param responseId ID of the response\n */\n _handleExec(responseId: ResponseId): void {\n if (!this.activeMultiCalls[0]) {\n this.sendError(responseId, new Error('EXEC without MULTI'));\n return;\n }\n\n this.activeMultiCalls[0].execId = responseId;\n this.activeMultiCalls[0].execCalled = true;\n\n // maybe we have all fullfilled yet\n if (this.activeMultiCalls[0].responseCount === this.activeMultiCalls[0].responseIds.length) {\n const multiRespObj = this.activeMultiCalls.shift() as FullMultiCallElement;\n this._sendExecResponse(multiRespObj);\n }\n }\n\n /**\n * Builds up the exec response and sends it\n *\n * @param multiObj the multi object to send out\n */\n _sendExecResponse(multiObj: FullMultiCallElement): void {\n // collect all 'QUEUED' answers\n const queuedStrArr = new Array(multiObj.responseCount).fill(QUEUED_STR_BUF);\n\n this._sendQueued(\n multiObj.execId,\n Buffer.concat([OK_STR_BUF, ...queuedStrArr, Resp.encodeArray(Array.from(multiObj.responseMap.values()))]),\n );\n }\n\n /**\n * Handles a multi response\n *\n * @param responseId ID of the response\n * @param index index of the multi call\n * @param buf buffer to include in response\n */\n _handleMultiResponse(responseId: ResponseId, index: number, buf: Buffer): void {\n this.activeMultiCalls[index].responseMap.set(responseId, buf);\n this.activeMultiCalls[index].responseCount++;\n if (\n this.activeMultiCalls[index].execCalled &&\n this.activeMultiCalls[index].responseCount === this.activeMultiCalls[index].responseIds.length\n ) {\n const multiRespObj = this.activeMultiCalls.splice(index, 1)[0] as FullMultiCallElement;\n this._sendExecResponse(multiRespObj);\n }\n }\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type { Socket } from 'node:net';\n// @ts-expect-error no types\nimport Resp from 'respjs';\nimport { EventEmitter } from 'node:events';\nimport { QUEUED_STR_BUF, OK_STR_BUF } from './constants.js';\nimport type { InternalLogger } from '@iobroker/js-controller-common-db/tools';\n\ntype NestedArray<T> = T[] | NestedArray<T>[];\n\ninterface RedisHandlerOptions {\n // Logger object\n log: InternalLogger;\n // log prefix\n logScope?: string;\n // if data should be handled as buffer\n handleAsBuffers: boolean;\n // additional debug information\n enhancedLogging?: boolean;\n}\n\ninterface WriteQueueElement {\n // response id\n id: ResponseId;\n // response data, temporary false if not ready to sent yet\n data: false | Buffer;\n}\n\ninterface FullWriteQueueElement extends WriteQueueElement {\n // response data\n data: Buffer;\n}\n\ninterface MultiCallElement {\n // all response ids of the multi call\n responseIds: ResponseId[];\n // indicator if exec already called\n execCalled: boolean;\n // number of responses which are already ready\n responseCount: number;\n // all responses as a map in correct order, key is responseId, value is null if no response there yet\n responseMap: Map<ResponseId, Buffer | null>;\n // id of the exec response\n execId?: ResponseId;\n}\n\ninterface FullMultiCallElement extends MultiCallElement {\n // id of the exec response\n execId: ResponseId;\n}\n\n// null if send without id\ntype ResponseId = number | null;\n\n/**\n * Class to handle a redis connection and provide events to react on for\n * all incoming Redis commands\n */\nexport class RedisHandler extends EventEmitter {\n private readonly socket: Socket;\n private readonly logScope: string;\n private readonly handleBuffers;\n private readonly options: RedisHandlerOptions;\n private readonly log: InternalLogger;\n private readonly socketId: string;\n private initialized = false;\n private stop = false;\n private readonly activeMultiCalls: MultiCallElement[] = [];\n private readonly writeQueue: WriteQueueElement[] = [];\n private responseId: number = 0;\n private readonly resp: any;\n\n /**\n * Initialize and register all data handlers to send out events on new commands\n *\n * @param socket Network Socket/Connection\n * @param options options objects, currently mainly for logger\n */\n constructor(socket: Socket, options: RedisHandlerOptions) {\n super();\n\n options ||= {} as RedisHandlerOptions;\n this.options = options;\n this.log = options.log || console;\n this.logScope = options.logScope || '';\n if (this.logScope.length) {\n this.logScope += ' ';\n }\n this.socket = socket;\n\n this.socketId = `${this.logScope + socket.remoteAddress}:${socket.remotePort}`;\n\n this.handleBuffers = false;\n const respOptions: { bufBulk?: boolean } = {};\n if (options.handleAsBuffers) {\n this.handleBuffers = true;\n respOptions.bufBulk = true;\n }\n this.resp = new Resp(respOptions);\n\n this.resp.on('error', (err: Error) => {\n this.log.error(`${this.socketId} (Init=${this.initialized}) Redis error: ${err}`);\n if (this.initialized) {\n this.sendError(null, new Error(`PARSER ERROR ${err}`)); // TODO\n } else {\n this.close();\n }\n });\n\n this.resp.on('data', (data: any) => this._handleCommand(data));\n\n socket.on('data', data => {\n if (this.options.enhancedLogging) {\n this.log.silly(\n `${this.socketId} New Redis request: ${\n data.length > 1024\n ? `${data\n .toString()\n .replace(/[\\r\\n]+/g, '')\n .substring(0, 100)} -- ${data.length} bytes`\n : data.toString().replace(/[\\r\\n]+/g, '')\n }`,\n );\n }\n this.resp.write(data);\n });\n\n socket.on('error', err => {\n if (!this.stop) {\n this.log.debug(`${this.socketId} Redis Socket error: ${err.stack}`);\n }\n if (this.socket) {\n this.socket.destroy();\n }\n });\n }\n\n /**\n * Handle one incoming command, assign responseId and emit event to be handled\n *\n * @param data Array RESP data\n */\n _handleCommand(data: any[]): void {\n let command = data.splice(0, 1)[0];\n if (this.handleBuffers) {\n // Command and pot. first parameter should always be Buffer\n command = command.toString('utf-8');\n if (Buffer.isBuffer(data[0])) {\n data[0] = data[0].toString('utf-8');\n }\n // Binary data only relevant for GET and SET in our case\n if (command !== 'set' && data.length > 1) {\n for (let i = 1; i < data.length; i++) {\n if (Buffer.isBuffer(data[i])) {\n data[i] = data[i].toString('utf-8');\n }\n }\n }\n }\n\n if (this.responseId === Number.MAX_VALUE) {\n this.responseId = 0;\n }\n const responseId = ++this.responseId;\n\n if (this.options.enhancedLogging) {\n this.log.silly(\n `${this.socketId} Parser result: id=${responseId}, command=${command}, data=${\n JSON.stringify(data).length > 1024\n ? `${JSON.stringify(data).substring(0, 100)} -- ${JSON.stringify(data).length} bytes`\n : JSON.stringify(data)\n }`,\n );\n }\n\n if (command === 'multi') {\n if (this.activeMultiCalls.length && !this.activeMultiCalls[0].execCalled) {\n // should never happen\n this.log.warn(`${this.socketId} Conflicting multi call`);\n }\n this._handleMulti();\n return;\n }\n\n // multi active and exec not called yet\n if (this.activeMultiCalls.length && !this.activeMultiCalls[0].execCalled && command !== 'exec') {\n // store all response ids so we know which need to be in the multi call\n this.activeMultiCalls[0].responseIds.push(responseId);\n // add it for the correct order will be overwritten with correct response\n this.activeMultiCalls[0].responseMap.set(responseId, null);\n } else {\n // multi response ids should not be pushed - we will answer combined\n this.writeQueue.push({ id: responseId, data: false });\n }\n\n if (command === 'exec') {\n this._handleExec(responseId);\n return;\n }\n\n if (command === 'info') {\n this.initialized = true;\n }\n\n if (this.listenerCount(command) !== 0) {\n setImmediate(() => this.emit(command, data, responseId));\n } else {\n this.sendError(responseId, new Error(`${command} NOT SUPPORTED`));\n }\n }\n\n /**\n * Check if the response to a certain command can be sent out directly or\n * if it needs to wait till earlier responses are ready\n *\n * @param responseId ID of the response\n * @param data Buffer to send out\n */\n _sendQueued(responseId: ResponseId, data: Buffer): void {\n let idx = 0;\n\n while (this.writeQueue.length && idx < this.writeQueue.length) {\n // we found the queue entry that matches with the responseId, so store the data so be sent out\n if (this.writeQueue[idx].id === responseId) {\n this.writeQueue[idx].data = data;\n // if we found it not on first index we are done because we can not send it out,\n // need to wait for index 0 to have a complete answer\n if (idx > 0) {\n break;\n }\n }\n // when data for queue entry 0 are preset (!== false) we can send it, remove the first entry\n // and check the other entries if they have completed responses too\n if (idx === 0 && this.writeQueue[idx].data !== false) {\n const response = this.writeQueue.shift() as FullWriteQueueElement;\n if (this.options.enhancedLogging) {\n this.log.silly(\n `${this.socketId} Redis response (${response.id}): ${\n response.data.length > 1024\n ? `${data.length} bytes`\n : response.data.toString().replace(/[\\r\\n]+/g, '')\n }`,\n );\n }\n\n this._write(response.data);\n // We sended out first queue entry but no further response is ready\n // and we do not need to check the whole queue, so we are done here\n if (this.writeQueue.length && this.writeQueue[idx].data === false) {\n break;\n }\n } else {\n // we have not found the response on the current index, try next one\n idx++;\n }\n }\n\n if (idx > 0) {\n if (this.options.enhancedLogging) {\n this.log.silly(`${this.socketId} Redis response (${responseId}): Response queued`);\n }\n }\n }\n\n /**\n * Really write out a response to the network connection\n *\n * @param data Buffer to send out\n */\n _write(data: Buffer): void {\n this.socket.write(data);\n }\n\n /**\n * Guard to make sure a response is valid to be sent out\n *\n * @param responseId ID of the response\n * @param data Buffer to send out\n */\n sendResponse(responseId: ResponseId, data: Buffer): void {\n // handle responses without a specific request like publishing data, so send directly\n if (responseId === null) {\n if (this.options.enhancedLogging) {\n this.log.silly(`${this.socketId} Redis response DIRECT: ${data.toString().replace(/[\\r\\n]+/g, '')}`);\n }\n return this._write(data);\n }\n if (!responseId) {\n throw new Error('Invalid implementation: no responseId provided!');\n }\n if (!data) {\n this.log.warn(`${this.socketId} Not able to write ${JSON.stringify(data)}`);\n data = Resp.encodeError(new Error(`INVALID RESPONSE: ${JSON.stringify(data)}`));\n }\n\n setImmediate(() => this._sendQueued(responseId, data));\n }\n\n /**\n * Close network connection\n */\n close(): void {\n this.log.silly(`${this.socketId} close Redis connection`);\n this.stop = true;\n this.socket.end();\n }\n\n /**\n * Return if socket/handler active or closed\n *\n * @returns is Handler/Connection active (not closed)\n */\n isActive(): boolean {\n return !this.stop;\n }\n\n /**\n * Encode RESP's Null value to RESP buffer and send out\n *\n * @param responseId ID of the response\n */\n sendNull(responseId: ResponseId): void {\n for (let i = 0; i < this.activeMultiCalls.length; i++) {\n if (this.activeMultiCalls[i].responseIds.includes(responseId)) {\n this._handleMultiResponse(responseId, i, Resp.encodeNull());\n return;\n }\n }\n\n this.sendResponse(responseId, Resp.encodeNull());\n }\n\n /**\n * Encode RESP's Null Array value to RESP buffer and send out\n *\n * @param responseId ID of the response\n */\n sendNullArray(responseId: ResponseId): void {\n for (let i = 0; i < this.activeMultiCalls.length; i++) {\n if (this.activeMultiCalls[i].responseIds.includes(responseId)) {\n this._handleMultiResponse(responseId, i, Resp.encodeNullArray());\n return;\n }\n }\n\n this.sendResponse(responseId, Resp.encodeNullArray());\n }\n\n /**\n * Encode string to RESP buffer and send out\n *\n * @param responseId ID od the response\n * @param str String to encode\n */\n sendString(responseId: ResponseId, str: string): void {\n for (let i = 0; i < this.activeMultiCalls.length; i++) {\n if (this.activeMultiCalls[i].responseIds.includes(responseId)) {\n this._handleMultiResponse(responseId, i, Resp.encodeString(str));\n return;\n }\n }\n\n this.sendResponse(responseId, Resp.encodeString(str));\n }\n\n /**\n * Encode error object to RESP buffer and send out\n *\n * @param responseId ID of the response\n * @param error Error object with error details to send out\n */\n sendError(responseId: ResponseId, error: Error): void {\n this.log.warn(`${this.socketId} Error from InMemDB: ${error.stack}`);\n\n for (let i = 0; i < this.activeMultiCalls.length; i++) {\n if (this.activeMultiCalls[i].responseIds.includes(responseId)) {\n this._handleMultiResponse(responseId, i, Resp.encodeError(error));\n return;\n }\n }\n\n this.sendResponse(responseId, Resp.encodeError(error));\n }\n\n /**\n * Encode integer to RESP buffer and send out\n *\n * @param responseId ID of the response\n * @param num Integer to send out\n */\n sendInteger(responseId: ResponseId, num: number): void {\n for (let i = 0; i < this.activeMultiCalls.length; i++) {\n if (this.activeMultiCalls[i].responseIds.includes(responseId)) {\n this._handleMultiResponse(responseId, i, Resp.encodeInteger(num));\n return;\n }\n }\n\n this.sendResponse(responseId, Resp.encodeInteger(num));\n }\n\n /**\n * Encode RESP's bulk string to RESP buffer and send out\n *\n * @param responseId ID of the response\n * @param str String to send out\n */\n sendBulk(responseId: ResponseId, str: string): void {\n for (let i = 0; i < this.activeMultiCalls.length; i++) {\n if (this.activeMultiCalls[i].responseIds.includes(responseId)) {\n this._handleMultiResponse(responseId, i, Resp.encodeBulk(str));\n return;\n }\n }\n\n this.sendResponse(responseId, Resp.encodeBulk(str));\n }\n\n /**\n * Encode RESP's bulk buffer to RESP buffer.\n *\n * @param responseId ID of the response\n * @param buf Buffer to send out\n */\n sendBufBulk(responseId: ResponseId, buf: Buffer): void {\n for (let i = 0; i < this.activeMultiCalls.length; i++) {\n if (this.activeMultiCalls[i].responseIds.includes(responseId)) {\n this._handleMultiResponse(responseId, i, Resp.encodeBufBulk(buf));\n return;\n }\n }\n\n this.sendResponse(responseId, Resp.encodeBufBulk(buf));\n }\n\n /**\n * Encode an Array depending on the type of the elements\n *\n * @param arr Array to encode\n * @returns Array with Buffers with encoded values\n */\n encodeRespArray(arr: unknown[]): NestedArray<Buffer> {\n const returnArr: NestedArray<Buffer> = new Array(arr.length);\n arr.forEach((value, i) => {\n if (Array.isArray(value)) {\n returnArr[i] = this.encodeRespArray(value);\n } else if (Buffer.isBuffer(value)) {\n returnArr[i] = Resp.encodeBufBulk(value);\n } else if (value === null) {\n returnArr[i] = Resp.encodeNull();\n } else if (typeof value === 'number') {\n returnArr[i] = Resp.encodeInteger(value);\n } else {\n returnArr[i] = Resp.encodeBulk(value);\n }\n });\n\n return returnArr;\n }\n\n /**\n * Encode a array values to buffers and send out\n *\n * @param responseId ID of the response\n * @param arr Array to send out\n */\n sendArray(responseId: ResponseId, arr: any[]): void {\n for (let i = 0; i < this.activeMultiCalls.length; i++) {\n if (this.activeMultiCalls[i].responseIds.includes(responseId)) {\n this._handleMultiResponse(responseId, i, Resp.encodeArray(this.encodeRespArray(arr)));\n return;\n }\n }\n\n this.sendResponse(responseId, Resp.encodeArray(this.encodeRespArray(arr)));\n }\n\n /**\n * Handles a 'multi' command\n *\n */\n _handleMulti(): void {\n this.activeMultiCalls.unshift({\n responseIds: [],\n execCalled: false,\n responseCount: 0,\n responseMap: new Map(),\n });\n }\n\n /**\n * Handles an 'exec' command\n *\n * @param responseId ID of the response\n */\n _handleExec(responseId: ResponseId): void {\n if (!this.activeMultiCalls[0]) {\n this.sendError(responseId, new Error('EXEC without MULTI'));\n return;\n }\n\n this.activeMultiCalls[0].execId = responseId;\n this.activeMultiCalls[0].execCalled = true;\n\n // maybe we have all fullfilled yet\n if (this.activeMultiCalls[0].responseCount === this.activeMultiCalls[0].responseIds.length) {\n const multiRespObj = this.activeMultiCalls.shift() as FullMultiCallElement;\n this._sendExecResponse(multiRespObj);\n }\n }\n\n /**\n * Builds up the exec response and sends it\n *\n * @param multiObj the multi object to send out\n */\n _sendExecResponse(multiObj: FullMultiCallElement): void {\n // collect all 'QUEUED' answers\n const queuedStrArr = new Array(multiObj.responseCount).fill(QUEUED_STR_BUF);\n\n this._sendQueued(\n multiObj.execId,\n Buffer.concat([OK_STR_BUF, ...queuedStrArr, Resp.encodeArray(Array.from(multiObj.responseMap.values()))]),\n );\n }\n\n /**\n * Handles a multi response\n *\n * @param responseId ID of the response\n * @param index index of the multi call\n * @param buf buffer to include in response\n */\n _handleMultiResponse(responseId: ResponseId, index: number, buf: Buffer): void {\n this.activeMultiCalls[index].responseMap.set(responseId, buf);\n this.activeMultiCalls[index].responseCount++;\n if (\n this.activeMultiCalls[index].execCalled &&\n this.activeMultiCalls[index].responseCount === this.activeMultiCalls[index].responseIds.length\n ) {\n const multiRespObj = this.activeMultiCalls.splice(index, 1)[0] as FullMultiCallElement;\n this._sendExecResponse(multiRespObj);\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["Resp"]
|
|
7
7
|
}
|
package/build/esm/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { InMemoryFileDB } from './lib/inMemFileDB.js';
|
|
1
|
+
export { InMemoryFileDB, type ConnectionOptions, type DbStatus, type DbOptions, type FileDbSettings, type MetaObject, } from './lib/inMemFileDB.js';
|
|
2
2
|
export { RedisHandler } from './lib/redisHandler.js';
|
|
3
3
|
export { tools } from '@iobroker/js-controller-common-db';
|
|
4
4
|
//# sourceMappingURL=index.d.ts.map
|
package/build/esm/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,cAAc,EACd,KAAK,iBAAiB,EACtB,KAAK,QAAQ,EACb,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,UAAU,GAClB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,KAAK,EAAE,MAAM,mCAAmC,CAAC"}
|
package/build/esm/index.js
CHANGED
package/build/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,cAAc,GAMjB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,KAAK,EAAE,MAAM,mCAAmC,CAAC"}
|