UNPKG

minigame-std

Version:

Cross-platform standard library for WeChat minigame and web browsers with unified APIs for crypto, fs, fetch, storage, and more.

1 lines 41.9 kB
{"version":3,"file":"logger.mjs","names":[],"sources":["../src/std/logger/helpers.ts","../src/std/logger/logger_core.ts","../src/std/logger/plugins/file_logger.ts","../src/std/logger/plugins/mina_logger.ts"],"sourcesContent":["/**\n * 日志系统工具函数。\n *\n * @internal\n */\n\nimport type { LogLevel } from './defines.ts';\n\n// #region Internal Variables\n\nconst LOG_LEVEL_INDEX: Record<LogLevel, number> = { debug: 0, info: 1, warn: 2, error: 3 };\n\n// #endregion\n\n/**\n * 判断给定级别是否满足最低级别要求。\n *\n * @param entryLevel - 日志条目的级别。\n * @param minLevel - 最低允许的级别。\n * @returns `entryLevel >= minLevel` 时返回 `true`。\n */\nexport function shouldLog(entryLevel: LogLevel, minLevel: LogLevel): boolean {\n return LOG_LEVEL_INDEX[entryLevel] >= LOG_LEVEL_INDEX[minLevel];\n}\n\n/**\n * 将日志参数列表转为消息字符串。\n *\n * 单参数直接序列化,多参数用空格拼接,避免不必要的 map/join 开销。\n */\nexport function buildMessage(args: unknown[]): string {\n return args.length === 1\n ? stringifyArg(args[0])\n : args.map(stringifyArg).join(' ');\n}\n\n// #region Internal Functions\n\n/**\n * 将单个参数转为字符串。\n *\n * string 原样返回;bigint 转 `` `${value}n` ``;object 尝试 JSON 序列化\n *(含 BigInt replacer fallback);其余用 String()。\n */\nfunction stringifyArg(arg: unknown): string {\n if (typeof arg === 'bigint') {\n return `${arg}n`;\n }\n\n if (typeof arg === 'object' && arg !== null) {\n try {\n return JSON.stringify(arg, (_, v) =>\n typeof v === 'bigint' ? `${v}n` : v,\n );\n } catch {\n // 循环引用等,最终 fallback\n return String(arg);\n }\n }\n\n return String(arg);\n}\n\n// #endregion\n","/**\n * 日志系统核心逻辑:单例状态管理、日志流水线、插件调度。\n */\n\nimport { Lazy } from 'happy-rusty';\nimport type {\n LogFilter,\n LoggerConfig,\n LoggerPlugin,\n LogLevel,\n PluginContext,\n} from './defines.ts';\nimport { shouldLog } from './helpers.ts';\n\n// #region Internal Variables\n\n/**\n * Console 方法映射。\n *\n * `.bind(console)` 防止直接传递方法引用时 `this` 上下文丢失。\n */\nconst CONSOLE_FN: Record<LogLevel, (...args: unknown[]) => void> = {\n 'debug': console.debug.bind(console),\n 'info': console.info.bind(console),\n 'warn': console.warn.bind(console),\n 'error': console.error.bind(console),\n};\n\n/**\n * 全局单例状态(懒初始化)。\n */\nlet lazyState = /*#__PURE__*/ Lazy(() => createDefaultState());\n\n// #endregion\n\n/**\n * 初始化日志系统。\n *\n * @param config - 日志系统配置。\n * @since 2.6.0\n * @example\n * ```ts\n * const file = fileLog({ level: 'debug' });\n * logger.init({ plugins: [file, wxLog({ level: 'warn' })] });\n * ```\n */\nexport function init(config: LoggerConfig = {}): void {\n // 清理旧插件资源(不触发默认 state 创建)\n lazyState.get().inspect(state => {\n for (const plugin of state.plugins) {\n plugin.onDestroy?.();\n }\n });\n\n const level = config.level ?? 'info';\n const plugins = config.plugins ?? [];\n\n const ctx: PluginContext = { globalLevel: level, filter: config.filter };\n\n // 调用每个 plugin 的 onInit\n for (const plugin of plugins) {\n plugin.onInit?.(ctx);\n }\n\n const newState: LoggerState = {\n console: {\n enabled: config.console?.enabled ?? true,\n level: config.console?.level ?? level,\n filter: config.filter,\n },\n plugins,\n };\n\n lazyState = Lazy(() => newState);\n\n // 拦截全局 console(不提供 restore,如需 restore 请用独立 injectConsole 函数)\n if (config.injectConsole) {\n injectConsole();\n }\n}\n\n/**\n * 输出 debug 级别日志。\n * @since 2.6.0\n */\nexport function debug(...args: unknown[]): void {\n dispatchLog('debug', ...args);\n}\n\n/**\n * 输出 info 级别日志。\n * @since 2.6.0\n */\nexport function info(...args: unknown[]): void {\n dispatchLog('info', ...args);\n}\n\n/**\n * 输出 warn 级别日志。\n * @since 2.6.0\n */\nexport function warn(...args: unknown[]): void {\n dispatchLog('warn', ...args);\n}\n\n/**\n * 输出 error 级别日志。\n * @since 2.6.0\n */\nexport function error(...args: unknown[]): void {\n dispatchLog('error', ...args);\n}\n\n/**\n * 拦截全局 `console` 方法,将其重定向到 logger。\n *\n * 调用后,`console.debug`/`info`/`warn`/`error`(以及 `console.log`)会经过\n * logger 的 `dispatchLog`,触发插件 pipeline(如 `fileLog`)。\n *\n * logger 自身的 console 输出使用模块加载时捕获的原始方法(`CONSOLE_FN`),\n * 不会递归。\n *\n * @returns restore 函数,调用后恢复原始 `console` 方法。\n * @since 2.6.0\n * @example\n * ```ts\n * logger.init({ plugins: [fileLog()] });\n * const restore = injectConsole();\n * // 之后所有 console.info(...) 会走 logger pipeline\n * console.info('App started'); // → file 写入 + console 输出\n * restore(); // 需要时恢复\n * ```\n */\nexport function injectConsole(): () => void {\n const original = {\n log: console.log,\n debug: console.debug,\n info: console.info,\n warn: console.warn,\n error: console.error,\n };\n\n // console.log 映射到 info 级别(console.log ≈ console.info)\n console.log = (...args: unknown[]) => info(...args);\n console.debug = (...args: unknown[]) => debug(...args);\n console.info = (...args: unknown[]) => info(...args);\n console.warn = (...args: unknown[]) => warn(...args);\n console.error = (...args: unknown[]) => error(...args);\n\n return () => {\n console.log = original.log;\n console.debug = original.debug;\n console.info = original.info;\n console.warn = original.warn;\n console.error = original.error;\n };\n}\n\n// #region Internal Types\n\n/**\n * 日志系统运行时状态。\n *\n * 封装控制台输出配置、过滤器和插件列表。通过 init 创建,仅供模块内部使用。\n */\ninterface LoggerState {\n console: {\n enabled: boolean;\n level: LogLevel;\n filter?: LogFilter;\n };\n plugins: LoggerPlugin[];\n}\n\n// #endregion\n\n// #region Internal Functions\n\nfunction createDefaultState(): LoggerState {\n return {\n console: {\n enabled: true,\n level: 'info',\n },\n plugins: [],\n };\n}\n\n/**\n * 统一的日志分发入口。\n *\n * 控制台和插件均接收原始参数,由各消费端自行决定如何格式化或过滤。\n */\nfunction dispatchLog(level: LogLevel, ...args: unknown[]): void {\n const state = lazyState.force();\n\n // 1. 控制台输出\n if (state.console.enabled\n && shouldLog(level, state.console.level)\n && (!state.console.filter || state.console.filter(level, ...args))) {\n CONSOLE_FN[level](...args);\n }\n\n // 2. 插件回调\n for (const plugin of state.plugins) {\n plugin.onLog?.(level, ...args);\n }\n}\n\n// #endregion\n","/**\n * 文件日志插件:fileLog 工厂,提供缓冲写入、日志分割(period + size)、旧文件清理。\n */\n\nimport { gzipSync } from 'fflate/browser';\nimport type { AsyncIOResult } from 'happy-rusty';\nimport { encodeUtf8 } from '../../codec/mod.ts';\nimport { addHideListener } from '../../event/mod.ts';\nimport {\n appendFile,\n readDir,\n readFile,\n remove,\n stat,\n writeFile,\n} from '../../fs/mod.ts';\nimport type {\n LogEntry,\n LogFilter,\n LogFormatter,\n LoggerPlugin,\n LogLevel,\n PluginContext,\n} from '../defines.ts';\nimport { buildMessage, shouldLog } from '../helpers.ts';\nimport type { PluginConfigBase } from './defines.ts';\n\n// #region Internal Variables\n\n/**\n * 默认日志文件根目录。\n */\nconst DEFAULT_ROOT_DIR = '/.minigame-std-logs';\n\n/**\n * 日志级别标签。\n */\nconst LEVEL_LABELS: Record<LogLevel, string> = {\n debug: 'DEBUG',\n info: 'INFO ',\n warn: 'WARN ',\n error: 'ERROR',\n};\n\n// #endregion\n\n/**\n * 日志分割(split)配置。\n *\n * @since 2.6.0\n */\nexport interface FileSplitConfig {\n /**\n * 日志文件分割的时间粒度(毫秒)。\n *\n * 同一 period 内的日志写入同一个文件,到期自动切换。\n *\n * @defaultValue `3600000`(1 小时)\n */\n period?: number;\n /**\n * 单个日志文件最大大小(字节)。\n *\n * @defaultValue `10 * 1024 * 1024`(10MB)\n */\n maxSize?: number;\n /**\n * 最多保留的日志文件数。\n *\n * @defaultValue `24`(period 为 1 小时时即一天的日志量)\n */\n maxCount?: number;\n /**\n * 文件最大保留时间(毫秒),创建时间超过此值的文件将被删除。\n *\n * 与 `maxCount` 叠加:先按时间过期删除,剩余文件若仍超 `maxCount` 再按数量删最旧的。\n *\n * @defaultValue `undefined`(不按时间清理)\n */\n maxAge?: number;\n /**\n * 是否使用 UTF-8 字节数计算文件大小(`true`)而非字符数(`false`)。\n *\n * @defaultValue `false`\n */\n useByteSize?: boolean;\n /**\n * 是否在切分时压缩旧日志文件(`.log` → `.log.gz`)。\n *\n * 压缩后原始 `.log` 被删除,压缩是 fire-and-forget,不阻塞日志写入。\n * 注意:压缩后文件变为 `.log.gz`,读取/合并时需先解压。\n * `maxCount` 为 1 时压缩产物 `.log.gz` 会临时占用额外名额,建议 `maxCount >= 2`。\n *\n * @defaultValue `false`\n */\n compress?: boolean;\n}\n\n/**\n * 文件插件配置。\n *\n * @since 2.6.0\n */\nexport interface FilePluginConfig extends PluginConfigBase {\n /**\n * 日志格式化器。\n *\n * @defaultValue `defaultFormatter`(`[时间] [级别] 消息\\n`)\n */\n formatter?: LogFormatter;\n /**\n * 日志文件根目录。\n *\n * @defaultValue `'/.minigame-std-logs'`\n */\n rootDir?: string;\n /**\n * 日志分割配置。\n */\n split?: FileSplitConfig;\n /**\n * 缓冲区最大条目数,达到后触发 flush。\n *\n * @defaultValue `100`\n */\n maxBufferSize?: number;\n /**\n * 定时 flush 间隔(毫秒),`0` 表示仅靠缓冲区阈值触发。\n *\n * @defaultValue `5000`\n */\n flushInterval?: number;\n}\n\n/**\n * 日志文件查询条件。\n *\n * @since 2.6.0\n */\nexport interface LogFileQuery {\n /**\n * 起始时间戳(毫秒,含)。按文件创建时间(文件名时间戳)筛选。\n */\n from?: number;\n /**\n * 结束时间戳(毫秒,含)。按文件创建时间(文件名时间戳)筛选。\n */\n to?: number;\n}\n\n/**\n * 文件插件 API 接口。\n *\n * @since 2.6.0\n */\nexport interface FilePluginAPI extends LoggerPlugin {\n /**\n * 立即将缓冲区内容写入文件,并等待所有在途写入完成。\n *\n * 写入失败不会 reject,也不会返回错误(错误由内部统一处理)。\n */\n flush(): Promise<void>;\n /**\n * 获取日志文件列表,可按文件创建时间筛选。\n *\n * 返回完整路径(`rootDir/文件名.log`),结果按文件名排序。\n * 文件名无法解析时间戳的文件不受 `query` 过滤,始终包含在结果中。\n */\n getFiles(query?: LogFileQuery): AsyncIOResult<string[]>;\n /**\n * 获取日志文件根目录。\n */\n getRootDir(): string;\n}\n\n/**\n * 创建文件日志插件。\n *\n * 插件在创建时即完成初始化(恢复/创建活跃文件、启动定时 flush、注册切后台监听),\n * 因此即使不传给 `logger.init()` 也能独立工作。传给 `logger.init()` 后,\n * `onInit` 会用全局配置(`level`/`filter`)refinement 自身配置。\n *\n * **注意**:`onInit`/`onLog` 由 logger 核心调度,不应手动调用。\n *\n * @param config - 文件插件配置。\n * @returns 支持 LoggerPlugin 和文件管理 API 的插件实例。\n * @since 2.6.0\n * @example\n * ```ts\n * const file = fileLog({ level: 'debug' });\n * logger.init({ plugins: [file] });\n * await file.flush();\n * ```\n */\nexport function fileLog(config: FilePluginConfig = {}): FilePluginAPI {\n // ── 解析配置 ──\n const rootDir = config.rootDir ?? DEFAULT_ROOT_DIR;\n const split = {\n period: config.split?.period ?? 3600000,\n maxSize: config.split?.maxSize ?? 10 * 1024 * 1024,\n maxCount: config.split?.maxCount ?? 24,\n maxAge: config.split?.maxAge,\n useByteSize: config.split?.useByteSize ?? false,\n compress: config.split?.compress ?? false,\n };\n const maxBufferSize = config.maxBufferSize ?? 100;\n const flushInterval = config.flushInterval ?? 5000;\n\n // ── 运行时状态(创建即用 config 初始化,不依赖 onInit)──\n // buffer 存预格式化结果,避免 flush 时重复格式化\n const buffer: BufferedEntry[] = [];\n let level: LogLevel = config.level ?? 'info';\n let filter: LogFilter | null | undefined = config.filter;\n const formatter: LogFormatter = config.formatter ?? defaultFormatter;\n // 默认用当前时间戳做文件名,resumeOrCreate 可能改为续写\n let currentFile = newLogPath();\n // 同步累加:writeEntry 时即计入,appendFile 失败会导致 size 偏大但不回滚\n let currentFileSize = 0;\n let currentFileStartTime = Date.now();\n // 定时 flush 定时器,onDestroy 时清理\n let flushTimer: ReturnType<typeof setInterval> | null = null;\n // 切后台监听移除器,onDestroy 时清理\n let hideListenerRemover: (() => void) | null = null;\n\n // init 异步过程中暂存日志(带原始 timestamp),完成后回放\n const pending: [LogLevel, unknown[], number][] = [];\n let initPromise: Promise<void> | null = null;\n\n // fire-and-forget 的在途 append,flush() 等待它们完成\n const inFlight = new Set<Promise<void>>();\n // 当前 prune 链的 Promise,null 表示无 prune 在跑\n // 多次 schedulePrune 调用串行链接(chain),避免并发 prune 的重复 IO\n let prunePromise: Promise<void> | null = null;\n\n // 时间戳对应的 period 序号\n function periodIndex(ts: number): number {\n return Math.floor(ts / split.period);\n }\n\n // 时间戳是否在当前或未来的 period 内(>= 兼容时间回拨,保证最新日志写最新文件)\n function inCurrentPeriod(ts: number): boolean {\n return periodIndex(ts) >= periodIndex(Date.now());\n }\n\n // 当前活跃文件是否已离开 period\n function isFileExpired(): boolean {\n return !inCurrentPeriod(currentFileStartTime);\n }\n\n // 创建新日志文件路径(ISO 时间戳命名,永不碰撞)\n function newLogPath(): string {\n return `${rootDir}/${toLogName()}`;\n }\n\n // 同步切文件:重置文件路径、period 起点、size\n function newFile(): void {\n if (split.compress && currentFileSize > 0) {\n // currentFile 是 string,传参时值已复制,后续 reassign 不影响\n // Promise.all 调用时同步迭代 inFlight,调用后的新增 append 不影响\n void compressOldFile(currentFile, inFlight)\n .catch(() => {\n // 压缩失败——原始 .log 仍存在,下次 prune 清理\n });\n }\n currentFile = newLogPath();\n currentFileStartTime = Date.now();\n currentFileSize = 0;\n void schedulePrune();\n }\n\n // 触发当前 buffer 的 flush(fire-and-forget)\n // 同步 splice + 快照 targetFile,appendFile 并发安全\n function flushCurrent(): void {\n if (buffer.length === 0) return;\n\n const entries = buffer.splice(0);\n const content = entries.map(e => e.formatted).join('');\n\n const p = appendFile(currentFile, content)\n .then(() => {\n // 不关心返回值\n })\n .finally(() => {\n // 无论成功失败,都从 inFlight 移除自己\n inFlight.delete(p);\n })\n .catch(() => {\n // 吞掉意外 rejection,保证 flush() 的 Promise.all 不被 reject\n });\n\n inFlight.add(p);\n\n // 重置定时器,避免刚 flush 后短时间内又触发低效写入\n if (flushTimer != null) {\n clearInterval(flushTimer);\n flushTimer = setInterval(flushCurrent, flushInterval);\n }\n }\n\n // 调度 prune:串行链接(chain),避免并发 prune 的重复 IO\n // 每次调用链接到前一个 prune 之后,先等在途 append 完成再读目录\n function schedulePrune(): Promise<void> {\n const prev = prunePromise;\n const p = (prev ?? Promise.resolve())\n .then(() => Promise.all(inFlight))\n .then(tryReadLogFiles)\n .then(pruneOldFiles)\n .catch(() => {\n // 吞掉意外 rejection,防止 unhandled rejection\n })\n .finally(() => {\n // 只有最后一个链才清除,中间链不清除(已被新链覆盖)\n if (prunePromise === p) {\n prunePromise = null;\n }\n });\n prunePromise = p;\n return p;\n }\n\n // 读取目录下排序后的日志文件名列表(含 .log 和 .log.gz)\n async function readLogFiles(): AsyncIOResult<string[]> {\n const dirResult = await readDir(rootDir);\n return dirResult.map(files => files\n .filter(n => n.endsWith('.log') || n.endsWith('.log.gz'))\n .sort(),\n );\n }\n\n // readLogFiles 的便利包装:错误时返回空数组(调用方按\"无文件\"处理)\n async function tryReadLogFiles(): Promise<string[]> {\n return (await readLogFiles()).unwrapOr([]);\n }\n\n // 清理超量旧文件:按文件名排序,保留最新的 maxCount 个\n // best-effort:失败不处理,下次切文件时会重试\n // logFiles 由调用方通过 tryReadLogFiles 预先读取(避免隐藏 IO)\n // logFiles 已排序(oldest first),先按 maxAge 过期删除,剩余若超 maxCount 再删最旧的\n async function pruneOldFiles(logFiles: string[]): Promise<void> {\n if (logFiles.length === 0) return;\n\n const { maxAge, maxCount } = split;\n\n // currentFile 可能还没落盘(newFile 刚创建),不在 logFiles 中\n // 但它即将写入磁盘,需要为其预留一个 maxCount 名额\n const currentFileName = currentFile.substring(rootDir.length + 1);\n const reserveActive = !logFiles.includes(currentFileName);\n const effectiveMaxCount = reserveActive ? maxCount - 1 : maxCount;\n\n // 无 maxAge 且未超 effectiveMaxCount,直接返回\n if (maxAge == null && logFiles.length <= effectiveMaxCount) return;\n\n const now = Date.now();\n const toDelete: string[] = [];\n let remaining: string[];\n\n // 1. 按时间过期删除(maxAge)\n if (maxAge != null) {\n remaining = [];\n for (const name of logFiles) {\n const ts = parseTimestamp(name);\n if (ts != null && now - ts > maxAge) {\n toDelete.push(name);\n } else {\n remaining.push(name);\n }\n }\n } else {\n remaining = logFiles;\n }\n\n // 2. 剩余文件若仍超 effectiveMaxCount,删最旧的(remaining 保持有序)\n if (remaining.length > effectiveMaxCount) {\n const excess = remaining.slice(0, remaining.length - effectiveMaxCount);\n toDelete.push(...excess);\n }\n\n if (toDelete.length === 0) return;\n\n await Promise.all(toDelete.map(name => remove(`${rootDir}/${name}`)));\n }\n\n // 将过滤后的日志条目加入缓冲,同步决策切分 + 累加 size\n function writeEntry(level: LogLevel, args: unknown[], timestamp: number = Date.now()): void {\n const entry: LogEntry = { timestamp, level, message: buildMessage(args) };\n const formatted = formatter(entry);\n const entryLen = split.useByteSize ? encodeUtf8(formatted).length : formatted.length;\n\n // 写入前决策:period 过期或超限 → 先 flush 旧 buffer 到旧文件,再切新文件\n if (isFileExpired() || currentFileSize + entryLen > split.maxSize) {\n flushCurrent();\n newFile();\n }\n\n buffer.push({ entry, formatted });\n currentFileSize += entryLen;\n\n if (buffer.length >= maxBufferSize) {\n flushCurrent();\n }\n }\n\n // 启动定时 flush,flushInterval 为 0 时仅靠 buffer 阈值触发\n // timer 存储到 flushTimer,onDestroy 时 clearInterval\n function startAutoFlush(): void {\n if (flushInterval > 0) {\n flushTimer = setInterval(flushCurrent, flushInterval);\n }\n }\n\n // 查找当前 period 内的文件续写(检查最新文件的 mtime),没有则创建新文件\n // 同时清理超量旧文件(复用已读取的 logFiles,避免二次 readDir)\n // 用 mtime 而非文件名时间戳:防篡改;同一 period 内创建时间和 mtime 必定同 period\n async function resumeOrCreate(): Promise<void> {\n const logFiles = await tryReadLogFiles();\n // 只考虑未压缩的 .log 文件用于续写(不能 append 到 .log.gz)\n const resumable = logFiles.filter(n => n.endsWith('.log'));\n\n if (resumable.length === 0) {\n newFile();\n } else {\n // 检查最新文件的 mtime 是否在当前 period 内\n const latestPath = `${rootDir}/${resumable[resumable.length - 1]}`;\n const statResult = await stat(latestPath);\n if (statResult.isOkAnd(x => inCurrentPeriod(x.lastModifiedTime))) {\n const s = statResult.unwrap();\n currentFile = latestPath;\n currentFileStartTime = s.lastModifiedTime;\n currentFileSize = s.size;\n } else {\n newFile();\n }\n }\n\n // 清理超量文件(复用 logFiles 含 .log.gz,避免二次 readDir)\n await pruneOldFiles(logFiles);\n }\n\n // ── 启动初始化(创建即就绪,不依赖 onInit)──\n // 查找或创建活跃文件(同时清理超量旧文件)→ 启动定时器 → 回放 pending\n // 目录由首次 appendFile 自动创建(writeFile 内部会 mkdir parent)\n // .finally:无论 resumeOrCreate 成功失败都继续初始化(启动定时器、回放 pending)\n // .catch:吞掉意外 rejection,防止 unhandled rejection\n initPromise = resumeOrCreate()\n .finally(() => {\n startAutoFlush();\n hideListenerRemover = addHideListener(flushCurrent);\n // 回放 init 期间暂存的日志\n for (const [lvl, args, ts] of pending) {\n writeEntry(lvl, args, ts);\n }\n pending.length = 0;\n initPromise = null;\n })\n .catch(() => {\n // 吞掉意外 rejection,防止 unhandled rejection\n });\n\n // ── API ──\n\n return {\n name: 'fileLog',\n\n onInit(ctx: PluginContext): void {\n // 用全局配置 refinement 自身配置:自身 > 全局 > 默认\n // 创建时已用 config 初始化,这里仅补全 config 未指定时从全局继承\n level = config.level ?? ctx.globalLevel;\n filter = config.filter !== undefined ? config.filter : ctx.filter;\n },\n\n onLog(lvl: LogLevel, ...args: unknown[]): void {\n // 级别过滤 → 自定义过滤\n if (!shouldLog(lvl, level)) return;\n if (filter && !filter(lvl, ...args)) return;\n\n // init 尚未完成,暂存(带原始 timestamp)后回放\n if (initPromise != null) {\n pending.push([lvl, args, Date.now()]);\n return;\n }\n\n writeEntry(lvl, args);\n },\n\n onDestroy(): void {\n // 清理定时器和事件监听,防止 re-init 后资源泄漏\n if (flushTimer != null) {\n clearInterval(flushTimer);\n flushTimer = null;\n }\n hideListenerRemover?.();\n hideListenerRemover = null;\n },\n\n async flush(): Promise<void> {\n // 等 init 完成,确保 pending 已回放到 buffer\n if (initPromise != null) {\n await initPromise;\n }\n\n // 触发当前 buffer flush\n flushCurrent();\n // 等待在途 append 完成 + 触发 prune 并等待\n // schedulePrune 内部 Promise.all(inFlight) 等所有 append,\n // 链式确保串行 prune,flush 返回时文件数已不超过 maxCount\n await schedulePrune();\n },\n\n async getFiles(query?: LogFileQuery): AsyncIOResult<string[]> {\n // 确保缓冲区已落盘,使文件列表包含当前活跃文件\n flushCurrent();\n await Promise.all(inFlight);\n\n return (await readLogFiles()).map(logFiles => {\n const from = query?.from;\n const to = query?.to;\n const needFilter = from != null || to != null;\n\n const files: string[] = [];\n for (const name of logFiles) {\n // 仅在有时间范围过滤时才解析时间戳,避免无谓的正则匹配\n if (needFilter) {\n const fileTimestamp = parseTimestamp(name);\n if (fileTimestamp != null) {\n if (from != null && fileTimestamp < from) continue;\n if (to != null && fileTimestamp > to) continue;\n }\n }\n\n files.push(`${rootDir}/${name}`);\n }\n\n // readLogFiles 已排序,prepend 统一前缀不改变顺序\n return files;\n });\n },\n\n getRootDir(): string {\n return rootDir;\n },\n };\n}\n\n// #region Internal Types\n\n/**\n * 缓冲区条目:预格式化的 LogEntry。\n *\n * flush 时直接 join,避免重复格式化。\n */\ninterface BufferedEntry {\n entry: LogEntry;\n formatted: string;\n}\n\n// #endregion\n\n// #region Internal Functions\n\n/**\n * 默认日志格式化器。\n *\n * 输出格式:`[2026-06-18 14:30:00.123] [INFO ] 消息内容\\n`\n */\nfunction defaultFormatter(entry: LogEntry): string {\n const time = formatTimestamp(entry.timestamp, 'yyyy-MM-dd HH:mm:ss.SSS');\n return `[${time}] [${LEVEL_LABELS[entry.level]}] ${entry.message}\\n`;\n}\n\n/**\n * 格式化时间戳为指定模式。\n */\nfunction formatTimestamp(timestamp: number, pattern: string): string {\n const date = new Date(timestamp);\n\n const tokens: Record<string, string> = {\n 'yyyy': date.getFullYear().toString(),\n 'MM': pad2(date.getMonth() + 1),\n 'dd': pad2(date.getDate()),\n 'HH': pad2(date.getHours()),\n 'mm': pad2(date.getMinutes()),\n 'ss': pad2(date.getSeconds()),\n 'SSS': pad3(date.getMilliseconds()),\n };\n\n return pattern.replace(/yyyy|MM|dd|HH|mm|ss|SSS/g, matched => tokens[matched]);\n}\n\n/**\n * 用当前时间戳生成独一无二的日志文件名(本地时间)。\n *\n * 全部使用 `-` 分隔,避免 ISO 8601 的 `T` 误导(文件名是本地时间,无时区后缀)。\n * 排序友好:`localeCompare` 按年-月-日-时-分-秒.毫秒逐位递减排序。\n *\n * 跨时区场景下应以 `LogEntry.timestamp`(epoch 毫秒)为准。\n */\nfunction toLogName(): string {\n return `${formatTimestamp(Date.now(), 'yyyy-MM-dd-HH-mm-ss.SSS')}.log`;\n}\n\n/**\n * 从日志文件名解析时间戳(按本地时间)。\n *\n * 文件名由 {@link toLogName} 生成:`yyyy-MM-dd-HH-mm-ss.SSS.log`。\n * 用 `new Date(y, mo-1, d, h, mi, s, ms)` 显式按本地时间构造,避免\n * `new Date(string)` 在不同运行时对无时区字符串的解析差异。\n */\nfunction parseTimestamp(name: string): number | null {\n const match = name.match(/^(\\d{4})-(\\d{2})-(\\d{2})-(\\d{2})-(\\d{2})-(\\d{2})\\.(\\d{3})\\.log(?:\\.gz)?$/);\n if (!match) return null;\n\n const [, y, mo, d, h, mi, s, ms] = match.map(Number);\n return new Date(y, mo - 1, d, h, mi, s, ms).getTime();\n}\n\nfunction pad2(n: number): string {\n return pad(n, 2);\n}\n\nfunction pad3(n: number): string {\n return pad(n, 3);\n}\n\nfunction pad(n: number, maxLength: number): string {\n return n.toString().padStart(maxLength, '0');\n}\n\n/**\n * 压缩旧日志文件:等待 in-flight append 完成 → 读 → gzipSync → 写 .log.gz → 删 .log\n */\nasync function compressOldFile(filePath: string, pending: Set<Promise<void>>): Promise<void> {\n await Promise.all(pending);\n\n const readResult = await readFile(filePath);\n if (readResult.isErr()) return;\n\n const compressed = gzipSync(readResult.unwrap());\n\n const gzPath = `${filePath}.gz`;\n const writeResult = await writeFile(gzPath, compressed);\n if (writeResult.isErr()) return;\n\n // 压缩成功后才删原始文件,保证不丢数据\n await remove(filePath);\n}\n\n// #endregion\n","/**\n * wx.getLogManager 插件:声明式创建 wxLog。\n */\n\nimport { Lazy } from 'happy-rusty';\nimport type { LogFilter, LoggerPlugin, LogLevel, PluginContext } from '../defines.ts';\nimport { buildMessage, shouldLog } from '../helpers.ts';\nimport type { PluginConfigBase } from './defines.ts';\n\n// ── Plugin Types ─────────────────────────────────────────────────\n\n/**\n * wx.getLogManager 插件配置(仅小游戏生效)。\n *\n * @since 2.6.0\n */\nexport interface WxLogPluginConfig extends PluginConfigBase {\n /**\n * 透传给 `wx.getLogManager` 的参数。\n *\n * {@link WechatMinigame.GetLogManagerOption.level}\n *\n * @defaultValue `0`\n */\n rawLevel?: 0 | 1;\n}\n\n// ── Plugin API ───────────────────────────────────────────────────\n\n/**\n * 创建 wx.getLogManager 插件(仅小游戏生效)。\n *\n * @param config - 插件配置。\n * @returns LoggerPlugin 实例。\n * @since 2.6.0\n * @example\n * ```ts\n * logger.init({ plugins: [wxLog({ level: 'warn' })] });\n * ```\n */\nexport function wxLog(config: WxLogPluginConfig = {}): LoggerPlugin {\n const logManager = Lazy(() => wx.getLogManager({ level: config.rawLevel ?? 0 }));\n\n let level: LogLevel;\n let filter: LogFilter | null | undefined;\n\n return {\n name: 'wxLog',\n\n onInit(ctx: PluginContext): void {\n level = config.level ?? ctx.globalLevel;\n filter = config.filter !== undefined ? config.filter : ctx.filter;\n },\n\n onLog(lvl: LogLevel, ...args: unknown[]): void {\n if (!shouldLog(lvl, level)) return;\n if (filter && !filter(lvl, ...args)) return;\n\n const manager = logManager.force();\n // 微信小游戏会将参数逐一进行 JSON.stringify 序列化, 遇到 bigint 类型会报错 `TypeError: Do not know how to serialize a BigInt`\n // 预序列化参数为单条消息字符串(处理 BigInt 等平台无法序列化的类型)\n const message = buildMessage(args);\n\n switch (lvl) {\n case 'debug':\n manager.debug(message);\n break;\n case 'info':\n manager.info(message);\n break;\n case 'warn':\n manager.warn(message);\n break;\n case 'error':\n // wx.getLogManager 没有 error 方法,故降级为 warn\n manager.warn(`[ERROR] ${message}`);\n break;\n }\n },\n };\n}\n"],"mappings":"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"}