import type { VextInternalHooks } from "../../types/hooks.js"; /** * service-reloader.ts — 选择性 Service 实例重载(Phase 2B) * * Soft Reload 时只重新实例化 invalidation set 中包含的 service, * 其他 service 实例保持不变。正确处理嵌套目录结构。 * * 核心流程: * * 1. 扫描 outDir/services/ 下所有 .js 文件 * 2. 筛选出在 invalidation set 中的文件(需要重载的) * 3. 保存受影响 service 的旧引用(用于回退) * 4. 逐个调用旧实例的 dispose()(如果存在) * 5. require() 新编译产物,实例化并挂载到 app.services * 6. 如果任何步骤失败,回滚受影响的 service 到旧实例 * * 安全保证: * * | 场景 | 行为 | * |----------------------------|------------------------------------------------| * | service 文件不在失效集合中 | 完全不触碰,实例保持不变 | * | dispose() 抛出异常 | 打印警告,继续重载(不中断流程) | * | require() 或 new 失败 | 回滚所有受影响 service 到旧实例,向上抛出错误 | * | 嵌套目录(payment/stripe) | 正确映射为 app.services.payment.stripe | * * @module lib/dev/service-reloader * @see 11b-soft-reload.md §4(服务实例重载) * @see 11e-edge-cases.md §1(Reload 失败回退) * @see 11e-edge-cases.md §6(Service 副作用安全) * @see IMPLEMENTATION-PLAN.md 任务 2.2b */ /** * 最小化的 VextApp 接口(仅包含 service-reloader 需要的字段) * * 使用局部接口避免对完整 VextApp 类型的直接依赖, * 便于单元测试中构造 mock 对象。 */ export interface ServiceReloaderApp { services: Record; logger: { info(...args: unknown[]): void; warn(...args: unknown[]): void; debug(...args: unknown[]): void; error(...args: unknown[]): void; }; hooks?: VextInternalHooks; } /** * Service 重载结果 */ export interface ServiceReloadResult { /** 受影响(重载)的 service 数量 */ reloaded: number; /** 未受影响(保持不变)的 service 数量 */ unchanged: number; /** 重载的 service key 列表(点分格式,如 "payment.stripe") */ reloadedKeys: string[]; } /** * filePathToServiceKeys — 将 service 文件路径转为嵌套 key 数组 * * 复用 service-loader 的路径转换逻辑(详见 02-services.md): * - 取相对路径(相对于 servicesDir) * - 去掉扩展名 * - 按 / 拆分为段 * - 每段做 kebab-case → camelCase 转换 * * 示例: * "user.js" → ["user"] * "user-profile.js" → ["userProfile"] * "payment/stripe.js" → ["payment", "stripe"] * "payment/ali-pay.js" → ["payment", "aliPay"] * * v2.2 修复:v2.1 使用 path.basename() 只取文件名, * 无法处理嵌套目录结构,导致 payment/stripe.js 被错误地 * 映射为 app.services["stripe"] 而非 app.services.payment.stripe。 * * @param relativePath 相对于 servicesDir 的文件路径(如 "payment/stripe.js") * @returns 嵌套 key 数组(如 ["payment", "stripe"]) */ export declare function filePathToServiceKeys(relativePath: string): string[]; /** * getNestedValue — 从嵌套对象中读取值 * * getNestedValue(obj, ["payment", "stripe"]) → obj.payment.stripe * * @param obj 根对象 * @param keys 嵌套 key 数组 * @returns 目标值,路径不存在时返回 undefined */ export declare function getNestedValue(obj: Record, keys: string[]): unknown; /** * setNestedValue — 向嵌套对象中设置值 * * setNestedValue(obj, ["payment", "stripe"], value) * → obj.payment.stripe = value * * 自动创建中间层级对象(如果不存在)。 * * @param obj 根对象 * @param keys 嵌套 key 数组 * @param value 要设置的值 */ export declare function setNestedValue(obj: Record, keys: string[], value: unknown): void; /** * scanServiceDirectory — 递归扫描 services/ 目录下的所有 .js 文件 * * 只扫描编译产物(.js),忽略 .map / .d.ts 等辅助文件。 * 跳过以 _ 或 . 开头的文件/目录。 * * @param dir 当前扫描的目录路径 * @returns 所有 service .js 文件的绝对路径数组 */ export declare function scanServiceDirectory(dir: string): Promise; /** * reloadServices — 选择性重载 service(v2.2) * * 只重新实例化变更的 service,其他 service 实例保持不变。 * 正确处理嵌套目录结构(如 payment/stripe → app.services.payment.stripe)。 * * 流程: * 1. 扫描 outDir/services/ 下所有 .js 文件 * 2. 筛选出在 invalidation set 中的文件 * 3. 保存受影响 service 的旧引用 * 4. 逐个 dispose + 重新 require + 实例化 * 5. 失败时回滚所有受影响 service * * @param app VextApp 实例(需要 app.services 和 app.logger) * @param outDir 编译产物目录(.vext/dev/ 的绝对路径) * @param invalidated require.cache 失效集合(绝对路径集合) * @returns 重载结果(重载数 / 未变更数 / 重载的 key 列表) * @throws 重载失败时抛出错误(已回滚受影响 service) */ export declare function reloadServices(app: ServiceReloaderApp, outDir: string, invalidated: Set): Promise;