# @Trans 可重入改造（REQUIRED 传播）

> 状态：**已实现（2026-08-15）**
> 关联代码：`pylon-dao/src/trans.ts`、`pylon-dao/src/init.ts`（txStorage）
> 背景：ts-libs 会话「dd DDD 扩展讨论」；是 [aggregate.md](./aggregate.md)（聚合仓储）与 [domain-event.md](./domain-event.md)（领域事件）的**共同前置底座**

## 1. 技术结论

**`@Trans` 需要可重入：多层 trans 只有外层生效**（REQUIRED 传播语义）。

```
规则：
  外层已有事务（txStorage 有 trx）→ 直接复用，不开新的
  外层无事务                 → 才开新事务
```

## 2. 现状（未做）：每次 @Trans 都开新事务

`pylon-dao/src/trans.ts` 当前实现：

```ts
export function Trans() {
  return function (_target, _propertyKey, descriptor) {
    const original = descriptor.value;
    descriptor.value = async function (...args) {
      return knex.transaction(async (trx: Knex) => {   // ← 每次都开新事务！
        return txStorage.run(trx, () => original.apply(this, args));
      });
    };
  };
}
```

问题：service 方法 `@Trans()` 调 service 方法 `@Trans()` 时，内层**又开一个新事务**——MySQL 嵌套开事务行为不可控。

## 3. 需要的改动（一行判断）

```ts
import { txStorage, knex } from './init.js';

export function Trans() {
  return function (_target, _propertyKey, descriptor) {
    const original = descriptor.value;
    descriptor.value = async function (...args) {
      // 重入检查：当前上下文已在事务中 → 复用，不开新事务
      if (txStorage.getStore()) {
        return original.apply(this, args);            // 加入外层事务
      }
      return knex.transaction(async (trx: Knex) => {   // 只有外层才开事务
        return txStorage.run(trx, () => original.apply(this, args));
      });
    };
  };
}
```

`txStorage` 是 `AsyncLocalStorage`，`getStore()` 有值 = 已在事务中。**这一行 `if` 就是全部改动**。

## 4. 为什么必须做（是 DDD 扩展的前置条件）

```
CheckoutService.placeOrder（@Trans() 外层）        ← 开事务
  ├─ OrderRepo.save(order)（@Trans() 内层）        ← 复用外层，不开新的
  ├─ InventoryDao.deduct（@Trans() 内层）          ← 复用外层
  └─ OutboxDao.insert（无 @Trans()）               ← 直接在事务内执行

失败回滚：内层抛异常 → 整个外层事务回滚（含所有聚合的变更）
```

没有可重入的话：内层 `@Trans()` 自己 commit/rollback，外层感知不到，**多表一致性直接崩**。

| 依赖方 | 为什么需要可重入 |
|--------|----------------|
| **聚合/仓储（aggregate.md）** | `Repository.save()` 标 `@Trans()`（聚合组内多表原子），service 用例方法也标 `@Trans()`（用例整体）——两者必须可重入，否则嵌套事务爆炸 |
| **领域事件（domain-event.md）** | afterCommit 钩子要挂在"真正的外层事务"提交后触发；不可重入时内层先提交，钩子时机错乱 |
| **DAO 层** | 无 `@Trans()`，靠透明代理自动落 trx，不受影响 |

## 5. 落地结果（已完成 2026-08-15）

1. ✅ 修改 `pylon-dao/src/trans.ts`：加重入判断（`txStorage.getStore()` 复用，REQUIRED 传播）；
2. ✅ 补测试 `pylon-dao/test/trans.test.ts`（vitest + mock knex）：外层 @Trans 调内层 @Trans 只开一个事务、内层单独调用开一个事务、内层抛异常整体回滚——3 个测试全绿；
3. ✅ `pylon-dao` 新增 vitest devDependency + `test` script + `tsconfig.json` include 加 `test`（vitest 需读 experimentalDecorators），`tsconfig.build.json` 不受影响（独立 include src 仅产物）；
4. ✅ typecheck / build 通过。

后续：聚合级联保存 + 用例事务在可重入下正确协作（aggregate.md 落地时验证）。