Source: RouterRunner/redis.js

// @ts-check
/**
 * Minimaler Redis-KV-Client für TTL-Keys (Runner-Registrierungscodes, …).
 *
 * Bewusst getrennt vom Pub/Sub-Client (utils/events.js): Diese Funktionen
 * brauchen nur SET mit EX / GET / DEL / EXISTS. Verbindung wird lazy nach dem
 * Secret-Load aufgebaut (mergedSecrets aus http-server.js). Fällt Redis aus,
 * degradieren die Funktionen auf `false`/`null` — der Aufrufer entscheidet,
 * ob er hart oder weich fehlschlagen will.
 */

import redis from 'redis';
import { errorLogger } from '../utils/requestLogger.js';

/** @type {import('redis').RedisClientType | null} */
let client = null;
let initialized = false;

const getConfig = async () => {
    const { mergedSecrets } = await import('../http-server.js');
    const secrets = mergedSecrets || {};
    const server = secrets.redisServer || process.env.REDIS_SERVER;
    const port = secrets.redisPort || process.env.REDIS_PORT || 6379;
    const password = secrets.redisPassword || process.env.REDIS_PASSWORD || undefined;
    return { server, port, password };
};

/** @type {Promise<boolean>} */
let connecting = null;

const connect = async () => {
    if (initialized) return !!client;
    if (connecting) return connecting;
    connecting = (async () => {
        try {
            const { server, port, password } = await getConfig();
            if (!server) return false;
            client = redis.createClient({
                url: `redis://${server}:${port}`,
                password,
                legacyMode: false,
                socket: {
                    connectTimeout: 10000,
                    reconnectStrategy: (retries) => (retries > 5
                        ? new Error('Max redis retries reached')
                        : Math.min(retries * 1000, 5000)),
                },
            });
            client.on('error', (err) => console.warn('[runnerRedis] client error', err?.message));
            await client.connect();
            initialized = true;
            return true;
        } catch (err) {
            console.warn('[runnerRedis] connect failed — transient keys (registration codes) unavailable', err?.message);
            errorLogger(err);
            return false;
        } finally {
            connecting = null;
        }
    })();
    return connecting;
};

/**
 * Key mit TTL setzen. @returns {Promise<boolean>} true bei Erfolg.
 */
export const kvSetEx = async (key, seconds, value) => {
    try {
        if (!(await connect())) return false;
        await client.set(key, value, { EX: seconds });
        return true;
    } catch (err) {
        console.warn('[runnerRedis] set failed', err?.message);
        return false;
    }
};

/** @returns {Promise<string|null>} */
export const kvGet = async (key) => {
    try {
        if (!(await connect())) return null;
        return await client.get(key);
    } catch (err) {
        console.warn('[runnerRedis] get failed', err?.message);
        return null;
    }
};

/** @returns {Promise<boolean>} true wenn der Key (noch) existiert. */
export const kvExists = async (key) => {
    try {
        if (!(await connect())) return false;
        return (await client.exists(key)) === 1;
    } catch (err) {
        console.warn('[runnerRedis] exists failed', err?.message);
        return false;
    }
};

/** @returns {Promise<boolean>} */
export const kvDel = async (key) => {
    try {
        if (!(await connect())) return false;
        await client.del(key);
        return true;
    } catch (err) {
        console.warn('[runnerRedis] del failed', err?.message);
        return false;
    }
};