@frangoteam/fuxa
Version:
Web-based Process Visualization (SCADA/HMI/Dashboard) software
970 lines (867 loc) • 37.1 kB
JavaScript
/**
* Driver redis: client for Redis key/value and hash fields with polling and DAQ support
*/
;
let Redis; // resolved via require or via plugin manager if needed
const utils = require('../../utils');
const deviceUtils = require('../device-utils');
function tryLoadRedis(manager) {
let mod = null;
try { mod = require('redis'); } catch { /* not installed in root */ }
if (!mod && manager) {
try { mod = manager.require('redis'); } catch { /* not installed in _pkg */ }
}
return mod;
}
function RedisClient(_data, _logger, _events, _runtime) {
// ---- runtime and configuration data
let runtime = _runtime; // access to scripts, socketMutex, etc.
let data = JSON.parse(JSON.stringify(_data)); // deep copy of device config
const logger = _logger;
const events = _events;
// ---- connection and polling state
let client = null;
let lastStatus = ''; // 'connect-ok' | 'connect-off' | 'connect-error' | 'connect-busy'
let working = false; // avoid overload during polling/connection
let overloading = 0; // overload counter
let lastTimestampValue = null; // timestamp of last successful polling
let connected = false;
const withTimeout = async (p, ms, label) => {
const timeoutMs = ms || (data?.property?.redisTimeoutMs || 3000);
let to;
try {
return await Promise.race([
p,
new Promise((_, rej) => { to = setTimeout(() => rej(new Error(`Timeout ${label || ''}`)), timeoutMs); })
]);
} finally {
clearTimeout(to);
}
};
const hmgetCompat = async (cli, key, fields) => {
if (typeof cli.hMGet === 'function') {
try {
return await cli.hMGet(key, fields);
} catch (_) { /* fallthrough */ }
}
if (typeof cli.hmGet === 'function') {
try {
return await cli.hmGet(key, fields);
} catch (_) { /* fallthrough */ }
}
return await cli.sendCommand(['HMGET', key, ...fields.map(String)]);
};
const getDeviceOptions = () => {
const def = {
readFields: { value: 'Value', quality: 'Quality', timestamp: 'UtcTimeMs' },
maxKeysPerPoll: 500,
redisTimeoutMs: 3000,
};
const opt = data?.property?.options;
if (!opt) {
return def;
}
if (typeof opt === 'string') {
def.readFields.value = opt.trim() || def.readFields.value;
return def;
}
if (opt.readFields) {
def.readFields.value = opt.readFields.value || def.readFields.value;
def.readFields.quality = opt.readFields.quality || def.readFields.quality;
def.readFields.timestamp = opt.readFields.timestamp || def.readFields.timestamp;
}
if (typeof opt.maxKeysPerPoll === 'number') {
def.maxKeysPerPoll = opt.maxKeysPerPoll;
}
if (typeof opt.redisTimeoutMs === 'number') {
def.redisTimeoutMs = opt.redisTimeoutMs;
}
return def;
};
// ---- templating args per custom command
const expandArgs = (tplArgs, ctx) => {
const out = [];
for (const a of (tplArgs || [])) {
if (a === '{{key}}') {
out.push(ctx.key);
}
else if (a === '{{fields...}}') {
out.push(...ctx.fields);
}
else {
out.push(String(a));
}
}
return out;
};
// ---- normalize result (array|object -> array aligned to fields)
const normalizeFieldValues = (res, fields) => {
if (Array.isArray(res)) {
return res;
}
if (res && typeof res === 'object') {
return fields.map(f => res[f]);
}
if (res == null) {
return fields.map(() => undefined);
}
return [String(res)];
};
// ---- utility chunk
const chunk = (arr, n) => Array.from({ length: Math.ceil(arr.length / n) }, (_, i) => arr.slice(i * n, (i + 1) * n));
// ---- tags cache and mapping
// varsValue: map { [tagId]: { id, value, type } }
let varsValue = {};
// keyMap: map tagId -> { kind: 'key'|'hash', key: string, field?: string, type, format, name }
let keyMap = {};
this.init = function () {
// nothing to do for Redis
};
/**
* Connect to Redis server
*/
this.connect = function () {
return new Promise(async (resolve, reject) => {
try {
if (!_checkWorking(true)) {
return reject();
}
logger.info(`'${data.name}' try to connect ${data?.property?.address || ''}`, true);
if (!Redis) {
_emitStatus('connect-error');
_clearVarsValue();
_checkWorking(false);
return reject(new Error('redis module not found'));
}
client = await _buildRedisClient.call(this);
await client.connect();
connected = true;
_emitStatus('connect-ok');
logger.info(`'${data.name}' connected!`, true);
_checkWorking(false);
resolve();
} catch (err) {
connected = false;
_emitStatus('connect-error');
_clearVarsValue();
_checkWorking(false);
logger.error(`'${data.name}' connect failed! ${err}`);
if (client) {
try { await client.quit(); } catch { }
client = null;
}
reject(err);
}
});
};
/**
* Disconnect from Redis server
*/
this.disconnect = function () {
return new Promise(async (resolve) => {
try {
_checkWorking(false);
if (client) {
try { await client.quit(); } catch { }
}
} catch (e) {
logger.error(`'${data.name}' disconnect failure! ${e}`);
} finally {
client = null;
connected = false;
_emitStatus('connect-off');
_clearVarsValue();
resolve(true);
}
});
};
/**
* Polling: read all configured keys in batch (MGET / HMGET)
*/
this.polling = async function () {
if (!_checkWorking(true)) {
_emitStatus('connect-busy');
return;
}
try {
if (!client || !connected) {
_checkWorking(false);
return;
}
const readMode = (data?.property?.connectionOption || 'simple').toLowerCase();
const batchResults = [];
const timeoutMs = data?.property?.redisTimeoutMs || 3000;
const deviceFields = getDeviceOptions().readFields; // { value, quality, timestamp }
if (readMode === 'hash') {
// Group tags by key and deduplicate the fields required for that key.
const itemsByKey = new Map(); // key -> [{ tagId, field }]
for (const tagId in keyMap) {
const km = keyMap[tagId];
if (km.kind !== 'hash') {
continue;
}
if (!itemsByKey.has(km.key)) {
itemsByKey.set(km.key, []);
}
itemsByKey.get(km.key).push({ tagId, field: km.field });
}
const keys = Array.from(itemsByKey.keys());
const parts = chunk(keys, data?.property?.maxKeysPerPoll || 500);
for (const part of parts) {
const results = await Promise.all(part.map(async (key) => {
const items = itemsByKey.get(key) || [];
const fields = Array.from(new Set([
...items.map(x => x.field),
deviceFields?.timestamp,
deviceFields?.quality
].filter(Boolean)));
const valsRaw = await withTimeout(
hmgetCompat(client, key, fields),
timeoutMs,
`HMGET ${key}`
).catch(err => {
logger.warn(`HMGET ${key} failed: ${err?.message || err}`);
return new Array(fields.length);
});
const vals = normalizeFieldValues(valsRaw, fields);
return { key, fields, vals, items };
}));
for (const { fields, vals, items } of results) {
const byName = {};
fields.forEach((f, i) => { byName[f] = vals[i]; });
const metaTs = deviceFields?.timestamp ? byName[deviceFields.timestamp] : undefined;
const metaQ = deviceFields?.quality ? byName[deviceFields.quality] : undefined;
for (const { tagId, field } of items) {
batchResults.push({ tagId, raw: byName[field], metaTs, metaQ });
}
}
}
} else {
const keyReads = [];
const hashGroups = new Map();
for (const tagId in keyMap) {
const km = keyMap[tagId];
if (km.kind === 'hash') {
if (!hashGroups.has(km.key)) {
hashGroups.set(km.key, []);
}
hashGroups.get(km.key).push({ tagId, field: km.field });
} else {
keyReads.push({ tagId: tagId, key: km.key });
}
}
if (keyReads.length) {
const ids = keyReads.map(x => x.tagId);
const keys = keyReads.map(x => x.key);
const parts = chunk(keys, data?.property?.maxKeysPerPoll || 500);
let ofs = 0;
for (const part of parts) {
const vals = await withTimeout(client.mGet(part), timeoutMs, 'MGET');
for (let i = 0; i < part.length; i++, ofs++) {
batchResults.push({ tagId: ids[ofs], raw: vals[i] });
}
}
}
for (const [hashKey, items] of hashGroups.entries()) {
const fields = Array.from(new Set([
...items.map(x => x.field),
deviceFields?.timestamp,
deviceFields?.quality
].filter(Boolean)));
const valsRaw = await withTimeout(
hmgetCompat(client, hashKey, fields),
timeoutMs,
`HMGET ${hashKey}`
);
const vals = normalizeFieldValues(valsRaw, fields);
const byName = {};
fields.forEach((f, idx) => { byName[f] = vals[idx]; });
const metaTs = deviceFields?.timestamp ? byName[deviceFields.timestamp] : undefined;
const metaQ = deviceFields?.quality ? byName[deviceFields.quality] : undefined;
for (const { tagId, field } of items) {
batchResults.push({ tagId, raw: byName[field], metaTs, metaQ });
}
}
}
const changed = await _updateVarsValue(batchResults);
lastTimestampValue = Date.now();
_emitValues(varsValue);
if (this.addDaq && !utils.isEmptyObject(changed)) {
this.addDaq(changed, data.name, data.id);
}
if (lastStatus !== 'connect-ok') {
_emitStatus('connect-ok');
}
} catch (err) {
logger.error(`'${data.name}' polling error: ${err?.message || err}`);
} finally {
_checkWorking(false);
}
};
/**
* Load: map tags into key/hash definition
* - tag.address = redis key
*/
this.load = function (_data) {
varsValue = {};
data = JSON.parse(JSON.stringify(_data));
keyMap = {};
try {
const readMode = (data?.property?.connectionOption || 'simple').toLowerCase();
const isHashLike = (readMode === 'hash');
const deviceValueField = getDeviceOptions().readFields.value;
const tags = data?.tags || {};
const count = Object.keys(tags).length;
for (const id in tags) {
const tag = tags[id];
// In 'simple' read string keys (MGET); in 'hash' and 'custom' read hash fields
const kind = isHashLike ? 'hash' : 'key';
// Field to read for hash tags:
// - if the tag has a non-empty options string -> and the name of the field
// - otherwise, the default device is used (readFields.value)
const tagField = isHashLike
? ((typeof tag?.options === 'string' && tag.options.trim())
? tag.options.trim()
: deviceValueField)
: undefined;
const entry = {
kind,
key: String(tag.address || tag.name || id),
field: (kind === 'hash') ? String(tagField) : undefined,
type: tag.type,
format: tag.format,
name: tag.name
};
keyMap[id] = entry;
}
logger.info(`'${data.name}' data loaded (${count})`, true);
} catch (err) {
logger.error(`'${data.name}' load error! ${err}`);
}
};
/**
* getValues: return the full cache
*/
this.getValues = function () {
return varsValue;
};
/**
* getValue: return { id, value, ts } for one tag
*/
this.getValue = function (id) {
if (varsValue[id]) {
const t = varsValue[id];
return { id, value: t.value, ts: t.timestamp ?? lastTimestampValue };
}
return null;
};
/**
* getStatus: return current connection status
*/
this.getStatus = function () {
return lastStatus;
};
/**
* Return tag property for frontend
*/
this.getTagProperty = function (tagId) {
if (data.tags[tagId]) {
const t = data.tags[tagId];
return { id: tagId, name: t.name, type: t.type, format: t.format };
}
return null;
};
/**
* setValue: write SET / HSET
* Write modes:
* - Default: HSET/SET (sample-compat)
* - Command mode: write.name set (e.g., DAINSY.HSET)
* * pairs mode: args as {name,value} or ["Field","{{token}}", ...]
* * full argv: args already include {{key}}, {{history}}, {{field}}, {{value}}, ...
*/
this.setValue = async function (tagId, value) {
if (!client || !connected) {
return false;
}
try {
// 1) Resolve mapping/tags
const km = keyMap[tagId];
if (!km || !km.key) {
throw new Error(`unknown-tag ${tagId}`);
}
const tag = data?.tags?.[tagId];
// 2) Device options
const timeoutMs = data?.property?.redisTimeoutMs || 3000;
const ttlSeconds = data?.property?.ttlSeconds ?? 0;
const readMode = (data?.property?.connectionOption || 'simple').toLowerCase();
const isHash = readMode === 'hash';
// 3) Normalise payload (supports primitive, JSON string, object)
let payload = { value };
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
payload = parsed;
}
} catch { /* result { value: string } */ }
} else if (value && typeof value === 'object' && !Buffer.isBuffer(value)) {
payload = value;
}
// 4) Pick helper
const pick = (obj, names) => {
for (const n of names) {
if (obj[n] !== undefined) {
return obj[n];
}
}
return undefined;
};
// 5) Main field (hash only) and input value
const deviceValueField = getDeviceOptions().readFields?.value || 'Value';
const mainValueInput = pick(payload, ['value', 'Value', 'val', 'Val']) ?? payload;
const fieldOverride = pick(payload, ['field', 'Field']);
const mainField = isHash ? String(fieldOverride || km.field || deviceValueField) : undefined;
// 6) Calculate RAW with FUXA logic and normalise by tag type
let raw = await deviceUtils.tagRawCalculator(mainValueInput, tag, runtime);
if (tag?.type === 'boolean') {
raw = raw ? '1' : '0';
} else if (tag?.type === 'number') {
const str = String(raw).replace(',', '.');
const num = Number(str);
if (!Number.isFinite(num)) {
logger.warn(`'${tag?.name || tagId}' setValue rejected: NaN`);
return false;
}
raw = String(num);
} else {
raw = String(raw ?? '');
}
const mainValue = raw;
// 7) Optional targets from payload (only these three, no extras)
const quality = pick(payload, ['quality', 'Quality', 'qual', 'Qual']);
const timestamp = pick(payload, ['timestamp', 'Timestamp', 'utcTimeMs', 'UtcTimeMs']);
const provider = pick(payload, ['provider', 'Provider']);
const history = pick(payload, ['history', 'History', 'hist', 'Hist']);
// 8) Dynamic writing configuration (frontend)
const opts = data?.property?.options || {};
const write = opts?.customCommand?.write || {};
const cmdName = (write?.name || '').trim(); // es. "DAINSY.HSET" or ""
const writeArgsRaw = Array.isArray(write?.args) ? write.args : [];
if (Array.isArray(writeArgsRaw) && writeArgsRaw.some(x => x == null)) {
logger.warn(`'${data.name}' write.args contains null/undefined entries — ignored`);
}
const writeArgs = normalizeWriteArgs(writeArgsRaw);
// 9) Placeholder context (known tokens only)
const tokenCtx = {
key: km.key,
field: mainField,
value: mainValue,
quality,
timestamp,
provider,
history,
};
// 10) COMMAND MODE: if write.name
if (cmdName) {
// used:
// A) argv complete in args (already with key/history/field/value)
// B) pairs (field, value), including ‘history’:<n> which becomes a positional argument
const expanded = expandTokens(writeArgs, tokenCtx);
// complete argv if the first argument resembles the key
const looksLikeFullArgv =
expanded.length > 0 && (expanded[0] === km.key || expanded[0].includes(':'));
if (looksLikeFullArgv) {
const argv = [cmdName, ...expanded];
await withTimeout(client.sendCommand(argv), timeoutMs, `${cmdName} ...`);
return true;
}
// Pairs mode: extract history, the rest remains HSET pairs
const histSize = Number(history) || 100; // default 100
const pairs = [];
for (let i = 0; i < expanded.length; i += 2) {
const k = expanded[i];
const v = expanded[i + 1];
if (k == null) {
continue;
}
if (String(k).toLowerCase() === 'history') {
continue;
}
pairs.push(String(k), v == null ? '' : String(v));
}
const argv = [cmdName, km.key, String(histSize), mainField, String(mainValue), ...pairs];
await withTimeout(client.sendCommand(argv), timeoutMs, `${cmdName} ...`);
return true;
}
// 11) DEFAULT (not write.name): compatible with the sample
if (isHash) {
const argv = ['HSET', km.key, mainField, String(mainValue)];
// If you have configured pairs in write.args (without name), append them as they are.
if (writeArgs.length > 0) {
const expanded = expandTokens(writeArgs, tokenCtx);
for (let i = 0; i < expanded.length; i += 2) {
const k = expanded[i];
const v = expanded[i + 1];
if (k == null) {
continue;
}
argv.push(String(k), v == null ? '' : String(v));
}
} else {
// No configuration: only use meta tags present in the payload (as in the sample)
if (quality !== undefined) {
argv.push('Quality', String(quality));
}
if (timestamp !== undefined) {
argv.push('UtcTimeMs', String(timestamp));
}
if (provider !== undefined) {
argv.push('Provider', String(provider));
}
}
await withTimeout(client.sendCommand(argv), timeoutMs, `HSET ${km.key}`);
if (ttlSeconds > 0) {
await withTimeout(
client.sendCommand(['EXPIRE', km.key, String(ttlSeconds)]),
timeoutMs,
`EXPIRE ${km.key}`,
);
}
} else {
// SIMPLE: SET (like sample), with EX optional
if (ttlSeconds > 0) {
await withTimeout(
client.sendCommand(['SET', km.key, String(mainValue), 'EX', String(ttlSeconds)]),
timeoutMs,
`SET ${km.key} EX ${ttlSeconds}`,
);
} else {
await withTimeout(
client.sendCommand(['SET', km.key, String(mainValue)]),
timeoutMs,
`SET ${km.key}`,
);
}
}
return true;
} catch (err) {
logger.error(`'${tag?.name || tagId}' setValue error: ${err?.message || err}`);
return false;
}
};
var expandTokens = (arr, ctx) => {
return (arr || []).map((x) => {
if (typeof x !== 'string') return x == null ? '' : String(x);
return x.replace(/\{\{([\w.\-]+)\}\}/g, (_, kRaw) => {
const k = String(kRaw);
let v = ctx[k];
// Automatic fallbacks for known tokens
if (v == null) {
switch (k.toLowerCase()) {
case 'timestamp':
case 'utctimems':
case 'now':
case 'nowms':
v = Date.now();
break;
case 'provider':
v = 'app-fuxa';
break;
case 'quality':
v = 0;
break;
// 'history' Not fallback
default:
v = '';
}
}
return String(v);
});
});
}
var normalizeWriteArgs = (args) => {
const flat = [];
for (const it of args || []) {
if (typeof it === 'string') {
flat.push(it);
} else if (it && typeof it === 'object') {
// supports {name,value} or common aliases
const n = it.name ?? it.field ?? it.key ?? it.n;
const v = it.value ?? it.val ?? it.v;
if (n !== undefined) {
flat.push(String(n), v == null ? '' : String(v));
}
}
}
return flat;
}
this.isConnected = function () {
return !!connected;
};
// DAQ binder
this.bindAddDaq = function (fnc) {
this.addDaq = fnc;
};
this.addDaq = null;
// last read timestamp
this.lastReadTimestamp = () => lastTimestampValue;
// security binder
this.bindGetProperty = function (fnc) {
this.getProperty = fnc;
};
this.getProperty = null;
// optional: DAQ settings
this.getTagDaqSettings = (tagId) => data.tags?.[tagId]?.daq || null;
this.setTagDaqSettings = (tagId, settings) => {
if (data.tags?.[tagId]) {
utils.mergeObjectsValues(data.tags[tagId].daq, settings);
}
};
/**
* One-shot full scan, max (default 100k)
* node = {
* match?: string, // es. 'dainsy:*' or '*' (default)
* count?: number, // hint for SCAN (default 2000)
* max?: number, // max element to return (default 100000)
* includeType?: boolean // include TYPE for every key (default true)
* }
*/
this.browse = function (node) {
return new Promise(async function (resolve, reject) {
try {
if (!client || !connected || client.isOpen === false) {
return resolve({ items: [], total: 0 });
}
const opts = node || {};
const match = (typeof opts.match === 'string' && opts.match.length) ? opts.match : '*';
const count = Number.isFinite(opts.count) ? opts.count : 2000;
const max = Number.isFinite(opts.max) ? opts.max : 100000;
const includeType = opts.includeType !== false;
/** Temporary storage of keys (always strings) */
const keysBuffer = [];
let produced = 0;
// 1) SCAN → accumulates only single strings
for await (const key of client.scanIterator({ MATCH: match, COUNT: count })) {
// defence: if an array results, splat it; otherwise, use the string
if (Array.isArray(key)) {
for (const k of key) {
keysBuffer.push(String(k));
produced++;
if (produced >= max) {
break;
}
}
} else {
keysBuffer.push(String(key));
produced++;
if (produced >= max) {
break;
}
}
if (produced >= max) {
break;
}
}
if (keysBuffer.length === 0) {
return resolve({ items: [], total: 0 });
}
// 2) Build items with/without TYPE
const items = [];
if (!includeType) {
for (const k of keysBuffer) {
items.push({ address: k, type: 'unknown' });
}
return resolve({ items, total: items.length });
}
// with TYPE: pipeline in batch
const batchSize = 500;
for (let start = 0; start < keysBuffer.length; start += batchSize) {
const chunk = keysBuffer.slice(start, start + batchSize);
const multi = client.multi();
for (const k of chunk) {
multi.type(k);
}
const replies = await multi.exec(); // ex. ['string','hash',...]
for (let i = 0; i < chunk.length; i++) {
const t = (typeof replies?.[i] === 'string') ? replies[i] : 'unknown';
items.push({ address: chunk[i], type: t || 'unknown' });
}
}
resolve({ items, total: items.length });
} catch (err) {
if (err) {
logger.error(`'${data.name}' scan failure! ${err}`);
}
reject();
}
});
}
var _clearVarsValue = function () {
for (let id in varsValue) {
varsValue[id].value = null;
}
_emitValues(varsValue);
}
/**
* Update the Tags values (compose + DAQ) following the standard FUXA pattern.
* @param {Array<{tagId:string, raw:any}>} batch
* @returns {Object|null} DAQ map { [id]: tagObj } or null if nothing relevant
*/
var _updateVarsValue = async (batch) => {
let hasAny = false;
const tempTags = {};
// 1) build tempTags with rawValue and changed flag
for (const item of batch) {
const id = item.tagId;
const tag = data.tags[id];
if (!tag) {
continue;
}
const prevRaw = varsValue[id]?.rawValue;
const rawValue = item.raw;
const changed = prevRaw !== rawValue;
tempTags[id] = {
id,
rawValue,
type: tag.type,
daq: tag.daq,
changed,
tagref: tag,
metaTs: item.metaTs,
metaQ: item.metaQ
};
hasAny = true;
}
if (!hasAny) {
return null;
}
// 2) compose value (scale/scripts/deadband/format) + DAQ decision
const timestamp = Date.now();
const result = {};
for (const id in tempTags) {
const t = tempTags[id];
if (!utils.isNullOrUndefined(t.rawValue)) {
// parse raw -> typed
const parsed = deviceUtils.parseValue(t.rawValue, t.tagref?.type);
// compose: script/scale/deadband/format
t.value = await deviceUtils.tagValueCompose(
parsed,
varsValue[id] ? varsValue[id].value : null,
t.tagref,
runtime
);
const fromRedis = Number(t.metaTs);
const ts = Number.isFinite(fromRedis) ? fromRedis : Date.now();
t.timestamp = ts;
if (t.metaQ !== undefined) {
const q = Number(t.metaQ);
if (Number.isFinite(q)) t.quality = q;
}
// DAQ decision
if (this.addDaq && deviceUtils.tagDaqToSave(t, t.timestamp)) {
result[id] = t;
}
}
// cache (keep raw + reset changed)
varsValue[id] = t;
varsValue[id].changed = false;
}
return Object.keys(result).length ? result : null;
}
var _emitValues = function (values) {
// send all current values to frontend
events.emit('device-value:changed', { id: data.id, values: values });
}
var _emitStatus = function (status) {
lastStatus = status;
events.emit('device-status:changed', { id: data.id, status });
}
var _checkWorking = function (flag) {
if (flag) {
if (working) {
if (++overloading > 3) {
_emitStatus('connect-busy');
overloading = 0;
}
logger.warn(`'${data.name}' working (connection || polling) overload! ${overloading}`);
return false;
}
working = true;
return true;
} else {
working = false;
return true;
}
}
/**
* Build a redis client using node-redis v4
* - data.property.address: host or URL
* - data.property.port: optional
* - security options: via bindGetProperty({query:'security',name:data.id})
*/
var _buildRedisClient = async () => {
const address = data?.property?.address || '127.0.0.1';
const port = data?.property?.port ? `:${data.property.port}` : '';
let url;
if (address.startsWith('redis://') || address.startsWith('rediss://')) {
url = address;
} else {
url = `redis://${address}${port}`;
}
// optional: security property with uid/pwd/tls
try {
if (this.getProperty) {
const sec = await this.getProperty({ query: 'security', name: data.id });
if (sec && sec.value && sec.value !== 'null') {
const prop = JSON.parse(sec.value);
if (prop.pwd || prop.uid) {
const u = encodeURIComponent(prop.uid || '');
const p = encodeURIComponent(prop.pwd || '');
const proto = (url.startsWith('rediss://')) ? 'rediss' : 'redis';
const hostPart = url.replace(/^redis[s]?:\/\//, '');
const auth = prop.uid ? `${u}:${p}@` : `:${p}@`;
url = `${proto}://${auth}${hostPart}`;
}
if (prop.tls === true && !url.startsWith('rediss://')) {
url = url.replace(/^redis:\/\//, 'rediss://');
}
}
}
} catch (e) {
logger.warn(`'${data.name}' security property parse warning: ${e}`);
}
const cli = Redis.createClient({
url,
autoPipelining: true,
socket: {
reconnectStrategy: (retries) => Math.min(100 + retries * 200, 3000)
}
});
cli.on('ready', () => {
connected = true;
_emitStatus('connect-ok');
logger.info(`'${data.name}' redis ready`, true);
});
cli.on('end', () => {
connected = false;
_emitStatus('connect-off');
logger.warn(`'${data.name}' redis connection closed`);
});
cli.on('error', (err) => {
logger.error(`'${data.name}' redis error: ${err}`);
});
return cli;
}
}
module.exports = {
init: function () { },
create: function (data, logger, events, manager, runtime) {
if (!Redis) {
Redis = tryLoadRedis(manager);
}
if (!Redis) {
return null;
}
return new RedisClient(data, logger, events, runtime);
}
};