@inso_web/els-mcp
Version:
MCP-сервер поверх INSO Error Logs Service. Read-only tools (search, analytics, fingerprinting, correlations) для подключения Claude Desktop/Code и ChatGPT к логам ошибок. Streamable HTTP transport + stdio для npx-запуска.
134 lines • 4.28 kB
JavaScript
import IORedisModule from 'ioredis';
// ioredis с verbatimModuleSyntax=false и NodeNext: класс находится в default экспорте.
// `IORedisModule` в зависимости от bundler может быть либо классом, либо { default: class }.
const IORedis = IORedisModule.default ?? IORedisModule;
export class RedisService {
client;
log;
connected = false;
disabled;
constructor(opts) {
this.log = opts.log;
this.disabled = opts.disabled === true;
if (this.disabled) {
this.client = null;
this.log?.info?.('Redis cache disabled via config');
return;
}
if (opts.client) {
this.client = opts.client;
this.connected = true;
this.attachListeners();
return;
}
const ioOpts = {
lazyConnect: true,
maxRetriesPerRequest: 2,
enableOfflineQueue: false,
retryStrategy: (times) => Math.min(times * 200, 10_000),
};
const client = new IORedis(opts.url, ioOpts);
this.client = client;
this.attachListeners();
// Lazy connect — стартуем коннект в фоне, не валим startup
client.connect().catch((err) => {
this.log?.warn?.({ err: err?.message }, 'Redis initial connect failed; cache will be no-op until reconnected');
});
}
attachListeners() {
if (!this.client)
return;
this.client.on('ready', () => {
this.connected = true;
this.log?.info?.('Redis connected');
});
this.client.on('end', () => {
this.connected = false;
this.log?.warn?.('Redis connection ended');
});
this.client.on('error', (err) => {
this.connected = false;
this.log?.warn?.({ err: err.message }, 'Redis error');
});
}
/** True если Redis недоступен или disabled. */
get unavailable() {
return this.disabled || !this.client || !this.connected;
}
get raw() {
return this.client;
}
/**
* Безопасный GET, возвращает null при любых ошибках/недоступности.
*/
async get(key) {
if (this.unavailable || !this.client)
return null;
try {
return await this.client.get(key);
}
catch (err) {
this.log?.warn?.({ err: err.message, key }, 'Redis GET failed');
return null;
}
}
/**
* Безопасный SETEX. Возвращает true если записали, false при ошибке.
*/
async setex(key, ttlSec, value) {
if (this.unavailable || !this.client)
return false;
try {
await this.client.setex(key, ttlSec, value);
return true;
}
catch (err) {
this.log?.warn?.({ err: err.message, key }, 'Redis SETEX failed');
return false;
}
}
/** Health-check (ping → PONG). Возвращает true если ответил. */
async ping() {
if (this.disabled || !this.client)
return false;
try {
const res = await this.client.ping();
return res === 'PONG';
}
catch {
return false;
}
}
async close() {
if (!this.client)
return;
try {
await this.client.quit();
}
catch {
this.client.disconnect();
}
}
}
let singleton = null;
/**
* Получить (или создать) singleton экземпляр.
*
* В тестах используем `setRedisServiceForTests(...)`.
*/
export function getRedisService(opts) {
if (!singleton) {
if (!opts) {
throw new Error('getRedisService(): первый вызов требует opts');
}
singleton = new RedisService(opts);
}
return singleton;
}
export function setRedisServiceForTests(service) {
singleton = service;
}
export function resetRedisSingleton() {
singleton = null;
}
//# sourceMappingURL=redis.js.map