diff --git a/db-server/src/routes/db-routes.ts b/db-server/src/routes/db-routes.ts index dd0b0d62..eff9c735 100644 --- a/db-server/src/routes/db-routes.ts +++ b/db-server/src/routes/db-routes.ts @@ -92,53 +92,50 @@ export function createDbRoutes(depsIn: Partial) { } // 交互式事务会话 — 供 SDK 在回调内读回 query/get/call 结果 - if (path === "/api/sfmc/db/tx/begin") { - try { - const result = deps.txRunner!.beginSession(moduleId); - json(res, result as unknown as Record, result.ok ? 200 : 400); - } catch (e) { - jsonV2Fail(res, (e as Error).message, 500); - } - return true; - } - if (path === "/api/sfmc/db/tx/step") { + // OCP/DRY:新会话动词只往表里加一项,勿再复制 try/catch 四份 + type SessionReply = { ok: boolean } & Record; + const sessionOps: Array<{ + path: string; + requireTxId?: boolean; + requireStep?: boolean; + run: (args: { txId: string; step?: TxStep | undefined }) => Promise | SessionReply; + }> = [ + { + path: "/api/sfmc/db/tx/begin", + run: () => deps.txRunner!.beginSession(moduleId) as SessionReply, + }, + { + path: "/api/sfmc/db/tx/step", + requireTxId: true, + requireStep: true, + run: ({ txId, step }) => deps.txRunner!.stepSession(txId, moduleId, step!) as Promise, + }, + { + path: "/api/sfmc/db/tx/commit", + requireTxId: true, + run: ({ txId }) => deps.txRunner!.commitSession(txId, moduleId) as SessionReply, + }, + { + path: "/api/sfmc/db/tx/rollback", + requireTxId: true, + run: ({ txId }) => deps.txRunner!.rollbackSession(txId, moduleId) as SessionReply, + }, + ]; + for (const op of sessionOps) { + if (path !== op.path) continue; try { const txId = String(body.txId || ""); const step = body.step as TxStep | undefined; - if (!txId || !step || typeof step !== "object" || !("op" in step)) { - jsonV2Fail(res, "tx/step 需要 { txId, step }", 400); + if (op.requireTxId && !txId) { + jsonV2Fail(res, `${op.path.split("/").pop()} 需要 { txId }`, 400); return true; } - const result = await deps.txRunner!.stepSession(txId, moduleId, step); - json(res, result as unknown as Record, result.ok ? 200 : 400); - } catch (e) { - jsonV2Fail(res, (e as Error).message, 500); - } - return true; - } - if (path === "/api/sfmc/db/tx/commit") { - try { - const txId = String(body.txId || ""); - if (!txId) { - jsonV2Fail(res, "tx/commit 需要 { txId }", 400); - return true; - } - const result = deps.txRunner!.commitSession(txId, moduleId); - json(res, result as unknown as Record, result.ok ? 200 : 400); - } catch (e) { - jsonV2Fail(res, (e as Error).message, 500); - } - return true; - } - if (path === "/api/sfmc/db/tx/rollback") { - try { - const txId = String(body.txId || ""); - if (!txId) { - jsonV2Fail(res, "tx/rollback 需要 { txId }", 400); + if (op.requireStep && (!step || typeof step !== "object" || !("op" in step))) { + jsonV2Fail(res, "tx/step 需要 { txId, step }", 400); return true; } - const result = deps.txRunner!.rollbackSession(txId, moduleId); - json(res, result as unknown as Record, result.ok ? 200 : 400); + const result = await op.run(step !== undefined ? { txId, step } : { txId }); + json(res, result as Record, result.ok ? 200 : 400); } catch (e) { jsonV2Fail(res, (e as Error).message, 500); } diff --git a/db-server/src/runtime.test.ts b/db-server/src/runtime.test.ts index 6e9fa3d9..01bf4d5a 100644 --- a/db-server/src/runtime.test.ts +++ b/db-server/src/runtime.test.ts @@ -207,19 +207,23 @@ test("module-auth: ensureModuleToken / revoke 复用 secret(DRY)", async () => { equal(revokeModuleToken(auth, "feature-afk"), false); }); -test("builtin-handlers: 热禁用按 serviceNames 卸载(OCP)", async () => { - const { BUILTIN_SERVICE_PLUGINS, unregisterBuiltinPluginForModule } = await import( +test("builtin-handlers: 热禁用按 moduleId 卸载(DRY/OCP)", async () => { + const { unregisterBuiltinPluginForModule, registerBuiltinPluginForModule } = await import( "./services/builtin-handlers.js" ); - const economy = BUILTIN_SERVICE_PLUGINS.find((p) => p.moduleId === "feature-economy"); - equal(!!economy?.serviceNames?.includes("economy.account.get"), true); const reg = new ServiceRegistry(); reg.registerHandler("feature-economy", "economy.account.get", async () => ({})); reg.registerHandler("feature-economy", "economy.account.debit", async () => ({})); + reg.registerHandler("other-mod", "other.ping", async () => ({})); equal(unregisterBuiltinPluginForModule(reg, "feature-economy"), 2); - equal(reg.list().length, 0); + equal(reg.list().length, 1); + equal(reg.list()[0]?.moduleId, "other-mod"); equal(unregisterBuiltinPluginForModule(reg, "feature-unknown"), 0); + + // 已有同 moduleId handler 时热启用跳过 + equal(registerBuiltinPluginForModule(reg, { query: (() => []) as never, db: {} as never }, "feature-economy"), true); + equal(registerBuiltinPluginForModule(reg, { query: (() => []) as never, db: {} as never }, "feature-economy"), false); }); test("TxRunner 交互会话: step 中途读回 insert/get(PR #31 leftover)", async () => { diff --git a/db-server/src/services/builtin-handlers.ts b/db-server/src/services/builtin-handlers.ts index 260de84d..84f284a8 100644 --- a/db-server/src/services/builtin-handlers.ts +++ b/db-server/src/services/builtin-handlers.ts @@ -15,25 +15,11 @@ export type BuiltinServiceDeps = { query: QueryFn; db: DatabaseSync }; export type BuiltinServicePlugin = { moduleId: string; register: (registry: ServiceRegistry, deps: BuiltinServiceDeps) => void; - /** 与 register 写入的 handler 名对齐,供热禁用 unregister */ - serviceNames?: string[]; }; /** 内置 service 插件清单 — 唯一扩展点 */ export const BUILTIN_SERVICE_PLUGINS: BuiltinServicePlugin[] = [ - { - moduleId: "feature-economy", - register: registerEconomyHandlers, - serviceNames: [ - "economy.account.get", - "economy.account.debit", - "economy.account.credit", - "economy.account.transfer", - "economy.dailyTasks.list", - "economy.dailyTasks.submit", - "economy.stats.monthly", - ], - }, + { moduleId: "feature-economy", register: registerEconomyHandlers }, ]; /** 按 enabledSet 注册内置插件,返回已注册插件数 */ @@ -51,7 +37,7 @@ export function registerEnabledBuiltinServices( return n; } -/** 热启用:只注册单个内置插件(若尚未注册) */ +/** 热启用:只注册单个内置插件(若尚未注册) — 以 registry.moduleId 为权威,勿硬编码 service 名(DRY) */ export function registerBuiltinPluginForModule( registry: ServiceRegistry, deps: BuiltinServiceDeps, @@ -59,22 +45,19 @@ export function registerBuiltinPluginForModule( ): boolean { const plugin = BUILTIN_SERVICE_PLUGINS.find((p) => p.moduleId === moduleId); if (!plugin) return false; - const already = (plugin.serviceNames ?? []).some((n) => registry.list().some((h) => h.name === n)); + const already = registry.list().some((h) => h.moduleId === moduleId); if (already) return false; plugin.register(registry, deps); return true; } -/** 热禁用:卸掉该模块的内置 handler */ +/** 热禁用:按 moduleId 卸掉该模块全部 handler(勿维护 serviceNames 副本 — DRY/OCP) */ export function unregisterBuiltinPluginForModule(registry: ServiceRegistry, moduleId: string): number { - const plugin = BUILTIN_SERVICE_PLUGINS.find((p) => p.moduleId === moduleId); - if (!plugin?.serviceNames) return 0; let n = 0; - for (const name of plugin.serviceNames) { - if (registry.list().some((h) => h.name === name)) { - registry.unregisterHandler(name); - n += 1; - } + for (const h of registry.list()) { + if (h.moduleId !== moduleId) continue; + registry.unregisterHandler(h.name); + n += 1; } return n; } diff --git a/modules/sdk/@sfmc-sdk/src/module-loader/runtime.ts b/modules/sdk/@sfmc-sdk/src/module-loader/runtime.ts index 696775ae..217f283b 100644 --- a/modules/sdk/@sfmc-sdk/src/module-loader/runtime.ts +++ b/modules/sdk/@sfmc-sdk/src/module-loader/runtime.ts @@ -228,10 +228,10 @@ export class ModuleRegistry { cleanups.set(id, []); } booted.delete(id); - // 清身份避免禁用后仍带旧 token 调用(Demeter:只清本模块上下文) - clearDbModuleContext(); - clearConfigModuleContext(); - clearServiceModuleContext(); + // 清身份避免禁用后仍带旧 token 调用(Demeter:只清本模块上下文,勿误清其他模块桶) + clearDbModuleContext(id); + clearConfigModuleContext(id); + clearServiceModuleContext(id); } static teardown(): void { diff --git a/modules/sdk/@sfmc-sdk/src/sapi/config/client.ts b/modules/sdk/@sfmc-sdk/src/sapi/config/client.ts index 4c281ba5..1f0ad6a4 100644 --- a/modules/sdk/@sfmc-sdk/src/sapi/config/client.ts +++ b/modules/sdk/@sfmc-sdk/src/sapi/config/client.ts @@ -44,9 +44,22 @@ export function setConfigModuleContext(moduleId: string, configKey: string, toke _activeConfigKey = configKey; } -export function clearConfigModuleContext(): void { - _buckets.clear(); - _activeConfigKey = ""; +/** + * 清理配置上下文。 + * - 传入 moduleId:只删该模块的桶(Demeter/OCP:禁用 A 不误清 B 的缓存) + * - 省略 moduleId:全清(进程级 teardown) + */ +export function clearConfigModuleContext(moduleId?: string): void { + if (!moduleId) { + _buckets.clear(); + _activeConfigKey = ""; + return; + } + for (const [key, bucket] of [..._buckets.entries()]) { + if (bucket.moduleId !== moduleId) continue; + _buckets.delete(key); + if (_activeConfigKey === key) _activeConfigKey = ""; + } } function activeBucket(): ConfigBucket { diff --git a/modules/sdk/@sfmc-sdk/src/sapi/db/client.ts b/modules/sdk/@sfmc-sdk/src/sapi/db/client.ts index fc24d2e1..4400220b 100644 --- a/modules/sdk/@sfmc-sdk/src/sapi/db/client.ts +++ b/modules/sdk/@sfmc-sdk/src/sapi/db/client.ts @@ -33,7 +33,13 @@ export function setDbModuleContext(moduleId: string, token: string): void { _authToken = token; } -export function clearDbModuleContext(): void { +/** + * 清理 db 模块身份。 + * - 传入 moduleId:仅当当前身份匹配时清空(Demeter:禁用 A 不误清 B) + * - 省略 moduleId:强制清空 + */ +export function clearDbModuleContext(moduleId?: string): void { + if (moduleId && _moduleId !== moduleId) return; _moduleId = ""; _authToken = ""; _currentTxId = null; diff --git a/modules/sdk/@sfmc-sdk/src/sapi/service/client.ts b/modules/sdk/@sfmc-sdk/src/sapi/service/client.ts index a51a98a6..83a79abe 100644 --- a/modules/sdk/@sfmc-sdk/src/sapi/service/client.ts +++ b/modules/sdk/@sfmc-sdk/src/sapi/service/client.ts @@ -23,7 +23,13 @@ export function setServiceModuleContext(moduleId: string, token: string, inTx: ( _isInTx = inTx; } -export function clearServiceModuleContext(): void { +/** + * 清理 service 模块身份。 + * - 传入 moduleId:仅当当前身份匹配时清空(Demeter:禁用 A 不误清 B) + * - 省略 moduleId:强制清空 + */ +export function clearServiceModuleContext(moduleId?: string): void { + if (moduleId && _moduleId !== moduleId) return; _moduleId = ""; _authToken = ""; _isInTx = () => false;