@spfn/core
Version:
SPFN Framework Core - File-based routing, transactions, repository pattern
992 lines (982 loc) • 29.6 kB
JavaScript
import pino from 'pino';
import { existsSync, mkdirSync, accessSync, constants, writeFileSync, unlinkSync, createWriteStream, statSync, readdirSync, renameSync } from 'fs';
import { join } from 'path';
// src/logger/adapters/pino.ts
var PinoAdapter = class _PinoAdapter {
logger;
constructor(config) {
this.logger = pino({
level: config.level,
// 기본 필드
base: config.module ? { module: config.module } : void 0
});
}
child(module) {
const childLogger = new _PinoAdapter({ level: this.logger.level, module });
childLogger.logger = this.logger.child({ module });
return childLogger;
}
debug(message, context) {
this.logger.debug(context || {}, message);
}
info(message, context) {
this.logger.info(context || {}, message);
}
warn(message, errorOrContext, context) {
if (errorOrContext instanceof Error) {
this.logger.warn({ err: errorOrContext, ...context }, message);
} else {
this.logger.warn(errorOrContext || {}, message);
}
}
error(message, errorOrContext, context) {
if (errorOrContext instanceof Error) {
this.logger.error({ err: errorOrContext, ...context }, message);
} else {
this.logger.error(errorOrContext || {}, message);
}
}
fatal(message, errorOrContext, context) {
if (errorOrContext instanceof Error) {
this.logger.fatal({ err: errorOrContext, ...context }, message);
} else {
this.logger.fatal(errorOrContext || {}, message);
}
}
async close() {
}
};
// src/logger/types.ts
var LOG_LEVEL_PRIORITY = {
debug: 0,
info: 1,
warn: 2,
error: 3,
fatal: 4
};
// src/logger/formatters.ts
var SENSITIVE_KEYS = [
"password",
"passwd",
"pwd",
"secret",
"token",
"apikey",
"api_key",
"accesstoken",
"access_token",
"refreshtoken",
"refresh_token",
"authorization",
"auth",
"cookie",
"session",
"sessionid",
"session_id",
"privatekey",
"private_key",
"creditcard",
"credit_card",
"cardnumber",
"card_number",
"cvv",
"ssn",
"pin"
];
var MASKED_VALUE = "***MASKED***";
function isSensitiveKey(key) {
const lowerKey = key.toLowerCase();
return SENSITIVE_KEYS.some((sensitive) => lowerKey.includes(sensitive));
}
function maskSensitiveData(data) {
if (data === null || data === void 0) {
return data;
}
if (Array.isArray(data)) {
return data.map((item) => maskSensitiveData(item));
}
if (typeof data === "object") {
const masked = {};
for (const [key, value] of Object.entries(data)) {
if (isSensitiveKey(key)) {
masked[key] = MASKED_VALUE;
} else if (typeof value === "object" && value !== null) {
masked[key] = maskSensitiveData(value);
} else {
masked[key] = value;
}
}
return masked;
}
return data;
}
var COLORS = {
reset: "\x1B[0m",
bright: "\x1B[1m",
dim: "\x1B[2m",
// 로그 레벨 컬러
debug: "\x1B[36m",
// cyan
info: "\x1B[32m",
// green
warn: "\x1B[33m",
// yellow
error: "\x1B[31m",
// red
fatal: "\x1B[35m",
// magenta
// 추가 컬러
gray: "\x1B[90m"
};
function formatTimestamp(date) {
return date.toISOString();
}
function formatTimestampHuman(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
const seconds = String(date.getSeconds()).padStart(2, "0");
const ms = String(date.getMilliseconds()).padStart(3, "0");
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
}
function formatError(error) {
const lines = [];
lines.push(`${error.name}: ${error.message}`);
if (error.stack) {
const stackLines = error.stack.split("\n").slice(1);
lines.push(...stackLines);
}
return lines.join("\n");
}
function formatConsole(metadata, colorize = true) {
const parts = [];
const timestamp = formatTimestampHuman(metadata.timestamp);
if (colorize) {
parts.push(`${COLORS.gray}[${timestamp}]${COLORS.reset}`);
} else {
parts.push(`[${timestamp}]`);
}
if (metadata.module) {
if (colorize) {
parts.push(`${COLORS.dim}[module=${metadata.module}]${COLORS.reset}`);
} else {
parts.push(`[module=${metadata.module}]`);
}
}
if (metadata.context && Object.keys(metadata.context).length > 0) {
Object.entries(metadata.context).forEach(([key, value]) => {
const valueStr = typeof value === "string" ? value : String(value);
if (colorize) {
parts.push(`${COLORS.dim}[${key}=${valueStr}]${COLORS.reset}`);
} else {
parts.push(`[${key}=${valueStr}]`);
}
});
}
const levelStr = metadata.level.toUpperCase();
if (colorize) {
const color = COLORS[metadata.level];
parts.push(`${color}(${levelStr})${COLORS.reset}:`);
} else {
parts.push(`(${levelStr}):`);
}
if (colorize) {
parts.push(`${COLORS.bright}${metadata.message}${COLORS.reset}`);
} else {
parts.push(metadata.message);
}
let output = parts.join(" ");
if (metadata.error) {
output += "\n" + formatError(metadata.error);
}
return output;
}
function formatJSON(metadata) {
const obj = {
timestamp: formatTimestamp(metadata.timestamp),
level: metadata.level,
message: metadata.message
};
if (metadata.module) {
obj.module = metadata.module;
}
if (metadata.context) {
obj.context = metadata.context;
}
if (metadata.error) {
obj.error = {
name: metadata.error.name,
message: metadata.error.message,
stack: metadata.error.stack
};
}
return JSON.stringify(obj);
}
// src/logger/logger.ts
var Logger = class _Logger {
config;
module;
constructor(config) {
this.config = config;
this.module = config.module;
}
/**
* Get current log level
*/
get level() {
return this.config.level;
}
/**
* Create child logger (per module)
*/
child(module) {
return new _Logger({
...this.config,
module
});
}
/**
* Debug log
*/
debug(message, context) {
this.log("debug", message, void 0, context);
}
/**
* Info log
*/
info(message, context) {
this.log("info", message, void 0, context);
}
warn(message, errorOrContext, context) {
if (errorOrContext instanceof Error) {
this.log("warn", message, errorOrContext, context);
} else {
this.log("warn", message, void 0, errorOrContext);
}
}
error(message, errorOrContext, context) {
if (errorOrContext instanceof Error) {
this.log("error", message, errorOrContext, context);
} else {
this.log("error", message, void 0, errorOrContext);
}
}
fatal(message, errorOrContext, context) {
if (errorOrContext instanceof Error) {
this.log("fatal", message, errorOrContext, context);
} else {
this.log("fatal", message, void 0, errorOrContext);
}
}
/**
* Log processing (internal)
*/
log(level, message, error, context) {
if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[this.config.level]) {
return;
}
const metadata = {
timestamp: /* @__PURE__ */ new Date(),
level,
message,
module: this.module,
error,
// Mask sensitive information in context to prevent credential leaks
context: context ? maskSensitiveData(context) : void 0
};
this.processTransports(metadata);
}
/**
* Process Transports
*/
processTransports(metadata) {
const promises = this.config.transports.filter((transport) => transport.enabled).map((transport) => this.safeTransportLog(transport, metadata));
Promise.all(promises).catch((error) => {
const errorMessage = error instanceof Error ? error.message : String(error);
process.stderr.write(`[Logger] Transport error: ${errorMessage}
`);
});
}
/**
* Transport log (error-safe)
*/
async safeTransportLog(transport, metadata) {
try {
await transport.log(metadata);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
process.stderr.write(`[Logger] Transport "${transport.name}" failed: ${errorMessage}
`);
}
}
/**
* Close all Transports
*/
async close() {
const closePromises = this.config.transports.filter((transport) => transport.close).map((transport) => transport.close());
await Promise.all(closePromises);
}
};
// src/logger/transports/console.ts
var ConsoleTransport = class {
name = "console";
level;
enabled;
colorize;
constructor(config) {
this.level = config.level;
this.enabled = config.enabled;
this.colorize = config.colorize ?? true;
}
async log(metadata) {
if (!this.enabled) {
return;
}
if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
return;
}
const message = formatConsole(metadata, this.colorize);
if (metadata.level === "warn" || metadata.level === "error" || metadata.level === "fatal") {
console.error(message);
} else {
console.log(message);
}
}
};
var FileTransport = class {
name = "file";
level;
enabled;
logDir;
maxFileSize;
maxFiles;
currentStream = null;
currentFilename = null;
constructor(config) {
this.level = config.level;
this.enabled = config.enabled;
this.logDir = config.logDir;
this.maxFileSize = config.maxFileSize ?? 10 * 1024 * 1024;
this.maxFiles = config.maxFiles ?? 10;
if (!existsSync(this.logDir)) {
mkdirSync(this.logDir, { recursive: true });
}
}
async log(metadata) {
if (!this.enabled) {
return;
}
if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
return;
}
const message = formatJSON(metadata);
const filename = this.getLogFilename(metadata.timestamp);
if (this.currentFilename !== filename) {
await this.rotateStream(filename);
await this.cleanOldFiles();
} else if (this.currentFilename) {
await this.checkAndRotateBySize();
}
if (this.currentStream) {
return new Promise((resolve, reject) => {
this.currentStream.write(message + "\n", "utf-8", (error) => {
if (error) {
process.stderr.write(`[FileTransport] Failed to write log: ${error.message}
`);
reject(error);
} else {
resolve();
}
});
});
}
}
/**
* 스트림 교체 (날짜 변경 시)
*/
async rotateStream(filename) {
if (this.currentStream) {
await this.closeStream();
}
const filepath = join(this.logDir, filename);
this.currentStream = createWriteStream(filepath, {
flags: "a",
// append mode
encoding: "utf-8"
});
this.currentFilename = filename;
this.currentStream.on("error", (error) => {
process.stderr.write(`[FileTransport] Stream error: ${error.message}
`);
this.currentStream = null;
this.currentFilename = null;
});
}
/**
* 현재 스트림 닫기
*/
async closeStream() {
if (!this.currentStream) {
return;
}
return new Promise((resolve, reject) => {
this.currentStream.end((error) => {
if (error) {
reject(error);
} else {
this.currentStream = null;
this.currentFilename = null;
resolve();
}
});
});
}
/**
* 파일 크기 체크 및 크기 기반 로테이션
*/
async checkAndRotateBySize() {
if (!this.currentFilename) {
return;
}
const filepath = join(this.logDir, this.currentFilename);
if (!existsSync(filepath)) {
return;
}
try {
const stats = statSync(filepath);
if (stats.size >= this.maxFileSize) {
await this.rotateBySize();
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
process.stderr.write(`[FileTransport] Failed to check file size: ${errorMessage}
`);
}
}
/**
* 크기 기반 로테이션 수행
* 예: 2025-01-01.log -> 2025-01-01.1.log, 2025-01-01.1.log -> 2025-01-01.2.log
*/
async rotateBySize() {
if (!this.currentFilename) {
return;
}
await this.closeStream();
const baseName = this.currentFilename.replace(/\.log$/, "");
const files = readdirSync(this.logDir);
const relatedFiles = files.filter((file) => file.startsWith(baseName) && file.endsWith(".log")).sort().reverse();
for (const file of relatedFiles) {
const match = file.match(/\.(\d+)\.log$/);
if (match) {
const oldNum = parseInt(match[1], 10);
const newNum = oldNum + 1;
const oldPath = join(this.logDir, file);
const newPath2 = join(this.logDir, `${baseName}.${newNum}.log`);
try {
renameSync(oldPath, newPath2);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
process.stderr.write(`[FileTransport] Failed to rotate file: ${errorMessage}
`);
}
}
}
const currentPath = join(this.logDir, this.currentFilename);
const newPath = join(this.logDir, `${baseName}.1.log`);
try {
if (existsSync(currentPath)) {
renameSync(currentPath, newPath);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
process.stderr.write(`[FileTransport] Failed to rotate current file: ${errorMessage}
`);
}
await this.rotateStream(this.currentFilename);
}
/**
* 오래된 로그 파일 정리
* maxFiles 개수를 초과하는 로그 파일 삭제
*/
async cleanOldFiles() {
try {
if (!existsSync(this.logDir)) {
return;
}
const files = readdirSync(this.logDir);
const logFiles = files.filter((file) => file.endsWith(".log")).map((file) => {
const filepath = join(this.logDir, file);
const stats = statSync(filepath);
return { file, mtime: stats.mtime };
}).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
if (logFiles.length > this.maxFiles) {
const filesToDelete = logFiles.slice(this.maxFiles);
for (const { file } of filesToDelete) {
const filepath = join(this.logDir, file);
try {
unlinkSync(filepath);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
process.stderr.write(`[FileTransport] Failed to delete old file "${file}": ${errorMessage}
`);
}
}
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
process.stderr.write(`[FileTransport] Failed to clean old files: ${errorMessage}
`);
}
}
/**
* 날짜별 로그 파일명 생성
*/
getLogFilename(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}.log`;
}
async close() {
await this.closeStream();
}
};
function isFileLoggingEnabled() {
return process.env.LOGGER_FILE_ENABLED === "true";
}
function getDefaultLogLevel() {
const isProduction = process.env.NODE_ENV === "production";
const isDevelopment = process.env.NODE_ENV === "development";
if (isDevelopment) {
return "debug";
}
if (isProduction) {
return "info";
}
return "warn";
}
function getConsoleConfig() {
const isProduction = process.env.NODE_ENV === "production";
return {
level: "debug",
enabled: true,
colorize: !isProduction
// Dev: colored output, Production: plain text
};
}
function getFileConfig() {
const isProduction = process.env.NODE_ENV === "production";
return {
level: "info",
enabled: isProduction,
// File logging in production only
logDir: process.env.LOG_DIR || "./logs",
maxFileSize: 10 * 1024 * 1024,
// 10MB
maxFiles: 10
};
}
function validateDirectoryWritable(dirPath) {
if (!existsSync(dirPath)) {
try {
mkdirSync(dirPath, { recursive: true });
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to create log directory "${dirPath}": ${errorMessage}`);
}
}
try {
accessSync(dirPath, constants.W_OK);
} catch {
throw new Error(`Log directory "${dirPath}" is not writable. Please check permissions.`);
}
const testFile = join(dirPath, ".logger-write-test");
try {
writeFileSync(testFile, "test", "utf-8");
unlinkSync(testFile);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Cannot write to log directory "${dirPath}": ${errorMessage}`);
}
}
function validateFileConfig() {
if (!isFileLoggingEnabled()) {
return;
}
const logDir = process.env.LOG_DIR;
if (!logDir) {
throw new Error(
"LOG_DIR environment variable is required when LOGGER_FILE_ENABLED=true. Example: LOG_DIR=/var/log/myapp"
);
}
validateDirectoryWritable(logDir);
}
function validateSlackConfig() {
const webhookUrl = process.env.SLACK_WEBHOOK_URL;
if (!webhookUrl) {
return;
}
if (!webhookUrl.startsWith("https://hooks.slack.com/")) {
throw new Error(
`Invalid SLACK_WEBHOOK_URL: "${webhookUrl}". Slack webhook URLs must start with "https://hooks.slack.com/"`
);
}
}
function validateEmailConfig() {
const smtpHost = process.env.SMTP_HOST;
const smtpPort = process.env.SMTP_PORT;
const emailFrom = process.env.EMAIL_FROM;
const emailTo = process.env.EMAIL_TO;
const hasAnyEmailConfig = smtpHost || smtpPort || emailFrom || emailTo;
if (!hasAnyEmailConfig) {
return;
}
const missingFields = [];
if (!smtpHost) missingFields.push("SMTP_HOST");
if (!smtpPort) missingFields.push("SMTP_PORT");
if (!emailFrom) missingFields.push("EMAIL_FROM");
if (!emailTo) missingFields.push("EMAIL_TO");
if (missingFields.length > 0) {
throw new Error(
`Email transport configuration incomplete. Missing: ${missingFields.join(", ")}. Either set all required fields or remove all email configuration.`
);
}
const port = parseInt(smtpPort, 10);
if (isNaN(port) || port < 1 || port > 65535) {
throw new Error(
`Invalid SMTP_PORT: "${smtpPort}". Must be a number between 1 and 65535.`
);
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(emailFrom)) {
throw new Error(`Invalid EMAIL_FROM format: "${emailFrom}"`);
}
const recipients = emailTo.split(",").map((e) => e.trim());
for (const email of recipients) {
if (!emailRegex.test(email)) {
throw new Error(`Invalid email address in EMAIL_TO: "${email}"`);
}
}
}
function validateEnvironment() {
const nodeEnv = process.env.NODE_ENV;
if (!nodeEnv) {
process.stderr.write(
"[Logger] Warning: NODE_ENV is not set. Defaulting to test environment.\n"
);
}
}
function validateConfig() {
try {
validateEnvironment();
validateFileConfig();
validateSlackConfig();
validateEmailConfig();
} catch (error) {
if (error instanceof Error) {
throw new Error(`[Logger] Configuration validation failed: ${error.message}`);
}
throw error;
}
}
// src/logger/adapters/custom.ts
function initializeTransports() {
const transports = [];
const consoleConfig = getConsoleConfig();
transports.push(new ConsoleTransport(consoleConfig));
const fileConfig = getFileConfig();
if (fileConfig.enabled) {
transports.push(new FileTransport(fileConfig));
}
return transports;
}
var CustomAdapter = class _CustomAdapter {
logger;
constructor(config) {
this.logger = new Logger({
level: config.level,
module: config.module,
transports: initializeTransports()
});
}
child(module) {
const adapter = new _CustomAdapter({ level: this.logger.level, module });
adapter.logger = this.logger.child(module);
return adapter;
}
debug(message, context) {
this.logger.debug(message, context);
}
info(message, context) {
this.logger.info(message, context);
}
warn(message, errorOrContext, context) {
if (errorOrContext instanceof Error) {
this.logger.warn(message, errorOrContext, context);
} else {
this.logger.warn(message, errorOrContext);
}
}
error(message, errorOrContext, context) {
if (errorOrContext instanceof Error) {
this.logger.error(message, errorOrContext, context);
} else {
this.logger.error(message, errorOrContext);
}
}
fatal(message, errorOrContext, context) {
if (errorOrContext instanceof Error) {
this.logger.fatal(message, errorOrContext, context);
} else {
this.logger.fatal(message, errorOrContext);
}
}
async close() {
await this.logger.close();
}
};
// src/logger/adapter-factory.ts
function createAdapter(type) {
const level = getDefaultLogLevel();
switch (type) {
case "pino":
return new PinoAdapter({ level });
case "custom":
return new CustomAdapter({ level });
default:
return new PinoAdapter({ level });
}
}
function getAdapterType() {
const adapterEnv = process.env.LOGGER_ADAPTER;
if (adapterEnv === "custom" || adapterEnv === "pino") {
return adapterEnv;
}
return "pino";
}
function initializeLogger() {
validateConfig();
return createAdapter(getAdapterType());
}
var logger = initializeLogger();
// src/cache/cache-factory.ts
var cacheLogger = logger.child("cache");
function hasCacheConfig() {
return !!// Modern (Valkey/Cache)
(process.env.VALKEY_URL || process.env.CACHE_URL || process.env.VALKEY_WRITE_URL || process.env.VALKEY_READ_URL || process.env.CACHE_WRITE_URL || process.env.CACHE_READ_URL || process.env.VALKEY_SENTINEL_HOSTS || process.env.VALKEY_CLUSTER_NODES || // Legacy (Redis - backward compatibility)
process.env.REDIS_URL || process.env.REDIS_WRITE_URL || process.env.REDIS_READ_URL || process.env.REDIS_SENTINEL_HOSTS || process.env.REDIS_CLUSTER_NODES);
}
function getEnv(valkeyKey, cacheKey, redisKey) {
return process.env[valkeyKey] || process.env[cacheKey] || process.env[redisKey];
}
function createClient(RedisClient, url) {
const options = {};
if (url.startsWith("rediss://") || url.startsWith("valkeys://")) {
const rejectUnauthorized = getEnv(
"VALKEY_TLS_REJECT_UNAUTHORIZED",
"CACHE_TLS_REJECT_UNAUTHORIZED",
"REDIS_TLS_REJECT_UNAUTHORIZED"
);
options.tls = {
rejectUnauthorized: rejectUnauthorized !== "false"
};
}
return new RedisClient(url, options);
}
async function createCacheFromEnv() {
if (!hasCacheConfig()) {
cacheLogger.info("No cache configuration found - running without cache");
return { write: void 0, read: void 0 };
}
try {
const ioredis = await import('ioredis');
const RedisClient = ioredis.default;
const singleUrl = getEnv("VALKEY_URL", "CACHE_URL", "REDIS_URL");
const writeUrl = getEnv("VALKEY_WRITE_URL", "CACHE_WRITE_URL", "REDIS_WRITE_URL");
const readUrl = getEnv("VALKEY_READ_URL", "CACHE_READ_URL", "REDIS_READ_URL");
const clusterNodes = getEnv("VALKEY_CLUSTER_NODES", "CACHE_CLUSTER_NODES", "REDIS_CLUSTER_NODES");
const sentinelHosts = getEnv("VALKEY_SENTINEL_HOSTS", "CACHE_SENTINEL_HOSTS", "REDIS_SENTINEL_HOSTS");
const masterName = getEnv("VALKEY_MASTER_NAME", "CACHE_MASTER_NAME", "REDIS_MASTER_NAME");
const password = getEnv("VALKEY_PASSWORD", "CACHE_PASSWORD", "REDIS_PASSWORD");
if (singleUrl && !writeUrl && !readUrl && !clusterNodes) {
const client = createClient(RedisClient, singleUrl);
cacheLogger.debug("Created single cache instance", { url: singleUrl.replace(/:[^:@]+@/, ":***@") });
return { write: client, read: client };
}
if (writeUrl && readUrl) {
const write = createClient(RedisClient, writeUrl);
const read = createClient(RedisClient, readUrl);
cacheLogger.debug("Created master-replica cache instances");
return { write, read };
}
if (sentinelHosts && masterName) {
const sentinels = sentinelHosts.split(",").map((host) => {
const [hostname, port] = host.trim().split(":");
return { host: hostname, port: Number(port) || 26379 };
});
const options = {
sentinels,
name: masterName,
password
};
const client = new RedisClient(options);
cacheLogger.debug("Created sentinel cache instance", { masterName, sentinels: sentinels.length });
return { write: client, read: client };
}
if (clusterNodes) {
const nodes = clusterNodes.split(",").map((node) => {
const [host, port] = node.trim().split(":");
return { host, port: Number(port) || 6379 };
});
const clusterOptions = {
redisOptions: {
password
}
};
const cluster = new RedisClient.Cluster(nodes, clusterOptions);
cacheLogger.debug("Created cluster cache instance", { nodes: nodes.length });
return { write: cluster, read: cluster };
}
if (singleUrl) {
const client = createClient(RedisClient, singleUrl);
cacheLogger.debug("Created cache instance (fallback)", { url: singleUrl.replace(/:[^:@]+@/, ":***@") });
return { write: client, read: client };
}
cacheLogger.info("No valid cache configuration found - running without cache");
return { write: void 0, read: void 0 };
} catch (error) {
if (error instanceof Error) {
if (error.message.includes("Cannot find module")) {
cacheLogger.warn(
"Cache client library not installed",
error,
{
suggestion: "Install ioredis to enable cache: pnpm install ioredis",
mode: "disabled"
}
);
} else {
cacheLogger.warn(
"Failed to create cache client",
error,
{ mode: "disabled" }
);
}
} else {
cacheLogger.warn(
"Failed to create cache client",
{ error: String(error), mode: "disabled" }
);
}
return { write: void 0, read: void 0 };
}
}
async function createSingleCacheFromEnv() {
const { write } = await createCacheFromEnv();
return write;
}
// src/cache/cache-manager.ts
var cacheLogger2 = logger.child("cache");
var writeInstance;
var readInstance;
var isDisabled = false;
function getCache() {
return writeInstance;
}
function getCacheRead() {
return readInstance ?? writeInstance;
}
function isCacheDisabled() {
return isDisabled;
}
function setCache(write, read) {
writeInstance = write;
readInstance = read ?? write;
isDisabled = !write;
}
async function initCache() {
if (writeInstance) {
return { write: writeInstance, read: readInstance, disabled: isDisabled };
}
const { write, read } = await createCacheFromEnv();
if (write) {
try {
await write.ping();
if (read && read !== write) {
await read.ping();
}
writeInstance = write;
readInstance = read;
isDisabled = false;
const hasReplica = read && read !== write;
cacheLogger2.info(
hasReplica ? "Cache connected (Master-Replica)" : "Cache connected",
{ mode: "enabled" }
);
return { write: writeInstance, read: readInstance, disabled: false };
} catch (error) {
cacheLogger2.error(
"Cache connection failed - running in disabled mode",
error instanceof Error ? error : new Error(String(error)),
{ mode: "disabled" }
);
try {
await write.quit();
if (read && read !== write) {
await read.quit();
}
} catch {
}
isDisabled = true;
return { write: void 0, read: void 0, disabled: true };
}
}
isDisabled = true;
cacheLogger2.info("Cache disabled - no configuration or library not installed", { mode: "disabled" });
return { write: void 0, read: void 0, disabled: true };
}
async function closeCache() {
if (isDisabled) {
cacheLogger2.debug("Cache already disabled, nothing to close");
return;
}
const closePromises = [];
if (writeInstance) {
closePromises.push(
writeInstance.quit().catch((err) => {
cacheLogger2.error("Error closing cache write instance", err);
})
);
}
if (readInstance && readInstance !== writeInstance) {
closePromises.push(
readInstance.quit().catch((err) => {
cacheLogger2.error("Error closing cache read instance", err);
})
);
}
await Promise.all(closePromises);
writeInstance = void 0;
readInstance = void 0;
isDisabled = true;
cacheLogger2.info("Cache connections closed", { mode: "disabled" });
}
function getCacheInfo() {
return {
hasWrite: !!writeInstance,
hasRead: !!readInstance,
isReplica: !!(readInstance && readInstance !== writeInstance),
disabled: isDisabled
};
}
var getRedis = getCache;
var getRedisRead = getCacheRead;
var setRedis = setCache;
var initRedis = initCache;
var closeRedis = closeCache;
var getRedisInfo = getCacheInfo;
export { closeCache, closeRedis, createCacheFromEnv, createCacheFromEnv as createRedisFromEnv, createSingleCacheFromEnv, createSingleCacheFromEnv as createSingleRedisFromEnv, getCache, getCacheInfo, getCacheRead, getRedis, getRedisInfo, getRedisRead, initCache, initRedis, isCacheDisabled, setCache, setRedis };
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map