Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 37 additions & 40 deletions db-server/src/routes/db-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,53 +92,50 @@ export function createDbRoutes(depsIn: Partial<DbRoutesDeps>) {
}

// 交互式事务会话 — 供 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<string, unknown>, 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<string, unknown>;
const sessionOps: Array<{
path: string;
requireTxId?: boolean;
requireStep?: boolean;
run: (args: { txId: string; step?: TxStep | undefined }) => Promise<SessionReply> | 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<SessionReply>,
},
{
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<string, unknown>, 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<string, unknown>, 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<string, unknown>, result.ok ? 200 : 400);
const result = await op.run(step !== undefined ? { txId, step } : { txId });
json(res, result as Record<string, unknown>, result.ok ? 200 : 400);
} catch (e) {
jsonV2Fail(res, (e as Error).message, 500);
}
Expand Down
14 changes: 9 additions & 5 deletions db-server/src/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
33 changes: 8 additions & 25 deletions db-server/src/services/builtin-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 注册内置插件,返回已注册插件数 */
Expand All @@ -51,30 +37,27 @@ export function registerEnabledBuiltinServices(
return n;
}

/** 热启用:只注册单个内置插件(若尚未注册) */
/** 热启用:只注册单个内置插件(若尚未注册) — 以 registry.moduleId 为权威,勿硬编码 service 名(DRY) */
export function registerBuiltinPluginForModule(
registry: ServiceRegistry,
deps: BuiltinServiceDeps,
moduleId: string
): 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;
}
8 changes: 4 additions & 4 deletions modules/sdk/@sfmc-sdk/src/module-loader/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
19 changes: 16 additions & 3 deletions modules/sdk/@sfmc-sdk/src/sapi/config/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 7 additions & 1 deletion modules/sdk/@sfmc-sdk/src/sapi/db/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 7 additions & 1 deletion modules/sdk/@sfmc-sdk/src/sapi/service/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading