96 lines
2.4 KiB
TypeScript
96 lines
2.4 KiB
TypeScript
import Redis, { type RedisOptions } from 'ioredis';
|
|
|
|
const ERR_MISSING_REDIS_URL =
|
|
'Missing required Valkey/Redis connection URL. Set VALKEY_URL or REDIS_URL.';
|
|
|
|
export interface RedisHealthCheck {
|
|
readonly checkedAt: number;
|
|
readonly latencyMs: number;
|
|
readonly ok: boolean;
|
|
readonly response?: string;
|
|
readonly error?: string;
|
|
}
|
|
|
|
export interface RedisPingClient {
|
|
ping(): Promise<string>;
|
|
}
|
|
|
|
export type RedisClientConstructor<TClient> = new (
|
|
url: string,
|
|
options?: RedisOptions,
|
|
) => TClient;
|
|
|
|
export interface CreateRedisClientOptions<TClient> {
|
|
readonly env?: NodeJS.ProcessEnv;
|
|
readonly redisConstructor?: RedisClientConstructor<TClient>;
|
|
readonly redisOptions?: RedisOptions;
|
|
}
|
|
|
|
export function resolveRedisUrl(env: NodeJS.ProcessEnv = process.env): string {
|
|
const resolvedUrl = env.VALKEY_URL ?? env.REDIS_URL;
|
|
|
|
if (typeof resolvedUrl !== 'string' || resolvedUrl.trim().length === 0) {
|
|
throw new Error(ERR_MISSING_REDIS_URL);
|
|
}
|
|
|
|
return resolvedUrl;
|
|
}
|
|
|
|
export function createRedisClient<TClient = Redis>(
|
|
options: CreateRedisClientOptions<TClient> = {},
|
|
): TClient {
|
|
const redisUrl = resolveRedisUrl(options.env);
|
|
|
|
const RedisCtor =
|
|
options.redisConstructor ??
|
|
(Redis as unknown as RedisClientConstructor<TClient>);
|
|
|
|
return new RedisCtor(redisUrl, {
|
|
maxRetriesPerRequest: null,
|
|
...options.redisOptions,
|
|
});
|
|
}
|
|
|
|
export async function runRedisHealthCheck(
|
|
client: RedisPingClient,
|
|
): Promise<RedisHealthCheck> {
|
|
const startedAt = process.hrtime.bigint();
|
|
|
|
try {
|
|
const response = await client.ping();
|
|
const elapsedMs = Number((process.hrtime.bigint() - startedAt) / 1_000_000n);
|
|
|
|
return {
|
|
checkedAt: Date.now(),
|
|
latencyMs: elapsedMs,
|
|
ok: true,
|
|
response,
|
|
};
|
|
} catch (error) {
|
|
const elapsedMs = Number((process.hrtime.bigint() - startedAt) / 1_000_000n);
|
|
const message =
|
|
error instanceof Error ? error.message : 'Unknown redis health check error';
|
|
|
|
return {
|
|
checkedAt: Date.now(),
|
|
latencyMs: elapsedMs,
|
|
ok: false,
|
|
error: message,
|
|
};
|
|
}
|
|
}
|
|
|
|
export async function assertRedisHealthy(
|
|
client: RedisPingClient,
|
|
): Promise<RedisHealthCheck> {
|
|
const health = await runRedisHealthCheck(client);
|
|
|
|
if (!health.ok) {
|
|
throw new Error(
|
|
`Redis health check failed after ${health.latencyMs}ms: ${health.error ?? 'unknown error'}`,
|
|
);
|
|
}
|
|
|
|
return health;
|
|
}
|