# @ddd-ts-kit/aspects 中文文档

规范地址：https://npmcn.com/package/%40ddd-ts-kit/aspects/

版本：0\.106\.0
npm 元数据更新时间：2026-10-31T21:41:50.565Z
中文译文更新时间：2026-11-01T00:20:35.618Z
软件包许可：MIT

本文由上游 README 机器翻译，可能存在偏差，请结合原文核对 API、命令与配置。版权与许可归原作者所有。

原始 npm 页面：https://www.npmjs.com/package/%40ddd-ts-kit/aspects
原文对照：https://npmcn.com/package/%40ddd-ts-kit/aspects/?lang=original
项目主页：https://gbourgeat.github.io/ddd-ts-kit/packages/aspects/
源码仓库：https://github.com/gbourgeat/ddd-ts-kit

## 关键信息（来自 npm 元数据）

- 包体积（解压后）：258 kB，352 个文件
- 模块格式：ES Module
- TypeScript 类型：包内自带
- Node.js 要求：\>=22\.12\.0
- 运行依赖：1 个
- 首次发布：2026-10-11T09:40:24.165Z
- 最新版本发布：2026-10-31T21:41:50.184Z
- 已发布版本数：63（近 12 个月 63 个）

## 中文简介

从外部应用的横切行为：日志记录、度量、重试、超时、断路器和缓存，作为组合根的可组合包装器，作为总线中间件，或作为适配器上的标准装饰器。域保持干净。

## 安装

```sh
npm install @ddd-ts-kit/aspects
```

也可以使用：`pnpm add @ddd-ts-kit/aspects`、`yarn add @ddd-ts-kit/aspects`、`bun add @ddd-ts-kit/aspects`

## README

**英语** · [法语](https://npmcn.com/package/%40ddd-ts-kit/aspects/README.fr.md)

# @ddd-ts-kit /方面

日志记录、度量、重试、超时、断路器和缓存都不是业务逻辑，所以 它们从来没有写在里面。每一个都是一个方面：一个实现，围绕一个 适配器从外部，方式 `@ddd-ts-kit/tracing` 在它周围放上跨度。包装类 没有进口，并且 `domain/` 和 `application/` 永远不要看到这个包裹。

```
npm i @ddd-ts-kit/aspects
```

## 它在应用程序中的位置

调用通过端口离开用例。回答这个问题的是绑定到该端口的适配器， 由方面包裹的组成根放在它周围。用例本身也被包装起来了， 但是通过总线：它们的中间件记录和测量每个命令和查询。

![A route calls the command bus, whose logging and metrics middlewares run around the handler; the handler calls the PaymentGateway port; the call goes through logged, timed, retry and timeout before it reaches the StripeGateway adapter, which calls the Stripe API; the composition root put the aspects and the middlewares in place, and neither domain nor application imports the package](https://raw.githubusercontent.com/gbourgeat/ddd-ts-kit/main/docs/assets/diagrams/aspects-place-in-an-application.svg)

与其将策略写入每个适配器，不如单独编写适配器并放置策略 :

```ts
import { compose, retry, timeout } from '@ddd-ts-kit/aspects';

interface Quote {
  readonly pair: string;
  readonly rate: number;
}

declare function fetchQuote(pair: string): Promise<Quote>;

export abstract class ExchangeRates {
  abstract quoteOf(pair: string): Promise<Quote>;
}

// Instead of: the retry loop, its waits and its limit written inside the adapter,
// once more in every adapter, and invisible from the composition root.
export class TangledRates implements ExchangeRates {
  async quoteOf(pair: string): Promise<Quote> {
    for (let attempt = 1; ; attempt += 1) {
      try {
        return await fetchQuote(pair);
      } catch (error) {
        if (attempt === 3) {
          throw error;
        }

        await new Promise((resolve) => setTimeout(resolve, 100 * attempt));
      }
    }
  }
}

// Write: the adapter does its one job...
export class HttpRates implements ExchangeRates {
  async quoteOf(pair: string): Promise<Quote> {
    return fetchQuote(pair);
  }
}

// ...and the composition root puts the policy around it: the same object, still an HttpRates.
export const rates: ExchangeRates = compose(retry({ attempts: 3 }), timeout(2000))(new HttpRates(), { kind: 'external' });
```

## 三种形式，一种实现

同样的政策，有三种写法。它们都运行相同的代码。

### 1\. 在组合根，在一个绑定上（首选）

```ts
import { retry, timeout } from '@ddd-ts-kit/aspects';
import { defineContextModule } from '@ddd-ts-kit/di';

interface Payment {
  readonly orderId: string;
  readonly amountCents: number;
}

interface Receipt {
  readonly chargeId: string;
}

declare function post(url: string, body: Payment, options: { apiKey: string; signal?: AbortSignal | undefined }): Promise<Receipt>;
declare const config: { readonly stripe: { readonly url: string; readonly key: string } };

// contexts/billing/application/ports/payment-gateway.port.ts
export abstract class PaymentGateway {
  abstract charge(payment: Payment, signal?: AbortSignal): Promise<Receipt>;
}

// contexts/billing/application/ports/payment-settings.port.ts
export abstract class PaymentSettings {
  abstract readonly baseUrl: string;

  abstract readonly apiKey: string;
}

// contexts/billing/infrastructure/external/stripe.gateway.ts
export class StripeGateway implements PaymentGateway {
  constructor(private readonly settings: PaymentSettings) {}

  async charge(payment: Payment, signal?: AbortSignal): Promise<Receipt> {
    return post(`${this.settings.baseUrl}/charges`, payment, { apiKey: this.settings.apiKey, signal });
  }
}

// contexts/billing/billing.module.ts
export const billingModule = defineContextModule({
  name: 'billing',
  register(builder) {
    builder.bind(PaymentSettings).toValue({ baseUrl: config.stripe.url, apiKey: config.stripe.key });
    builder
      .bind(PaymentGateway)
      .toClass(StripeGateway, [PaymentSettings])
      .as('external')
      .with(retry({ attempts: 3 }), timeout(2000, { signal: true }));
  },
});
```

端口是一个抽象类，类是被绑定的对象。 `StripeGateway` 没有意义 从这个包里，不知道的政策。在声明的样式中， 生成的项目中，没有模块文件，并且在声明中声明了相同的策略 适配器的： `@Adapter(PaymentGateway, { kind: 'external', aspects: [retry({ attempts: 3 }), timeout(2000)] })`． 没有容器，方面就是一个函数： `retry({ attempts: 3 })(new StripeGateway(settings), { kind: 'external' })`.

### 2\. 在每个绑定的组合根

```ts
// main.ts
import { Cache, Clock, Logger, Metrics } from '@ddd-ts-kit/kernel';
import { kernelModule, type ContextModule } from '@ddd-ts-kit/di';
import { createApplication } from '@ddd-ts-kit/di/tsyringe';
import { StdoutLogger, aspects, forKinds, installAspectRuntime, logged, retry, timed, timeout, when, type Aspect } from '@ddd-ts-kit/aspects';

declare const tracing: Aspect; // below
declare const platformModule: ContextModule;
declare const billingModule: ContextModule;

const app = createApplication({
  modules: [kernelModule({ logger: new StdoutLogger() }), platformModule, billingModule],
  decorate: aspects(
    tracing,
    when(forKinds('repository', 'gateway', 'external'), logged(), timed()),
    when(forKinds('external'), retry({ attempts: 3 }), timeout(2000)),
  ),
});

installAspectRuntime({
  logger: app.resolve(Logger),
  metrics: app.resolve(Metrics),
  cache: app.resolve(Cache),
  clock: app.resolve(Clock),
});
```

`tracing` 跟踪包的钩子是否写成了一个方面：

```ts
import type { Aspect } from '@ddd-ts-kit/aspects';
import { traceDependency } from '@ddd-ts-kit/tracing';

export const tracing: Aspect = (instance, info) =>
  info?.kind === undefined ? instance : traceDependency(instance, { kind: info.kind, ...(info.context === undefined ? {} : { context: info.context }) });
```

申请网址： `kernelModule({ instrumentation: traceApplication() })`，这就是 CLI生成，而不是写这个 `tracing` 方面：内核模块跟踪绑定 本身。它张成的空间在 `decorate` Hook添加和添加了a的各个方面 具有约束力（见“命令”）。生成的项目不使用 `decorate` 勾起任何一个： 平台模块将默认策略放置在离开流程的适配器周围 `builder.around(['external', 'gateway'], () => when(forKinds('external', 'gateway'), retry(...), timeout(...)))`， 并且一个适配器声明了它自己的策略 `ownPolicy(...)` 的 `platform/resilience/`，然后默认策略将其保留。

### 3\. 作为适配器上的装饰器

```ts
// contexts/billing/infrastructure/external/stripe.gateway.ts
import { Retry, Timeout } from '@ddd-ts-kit/aspects/decorators';

interface Payment {
  readonly orderId: string;
  readonly amountCents: number;
}

interface Receipt {
  readonly chargeId: string;
}

declare function post(url: string, body: Payment, options: { apiKey: string; signal?: AbortSignal | undefined }): Promise<Receipt>;

abstract class PaymentGateway {
  abstract charge(payment: Payment, signal?: AbortSignal): Promise<Receipt>;
}

abstract class PaymentSettings {
  abstract readonly baseUrl: string;

  abstract readonly apiKey: string;
}

export class StripeGateway implements PaymentGateway {
  constructor(private readonly settings: PaymentSettings) {}

  @Retry({ attempts: 3 })
  @Timeout(2000, { signal: true })
  async charge(payment: Payment, signal?: AbortSignal): Promise<Receipt> {
    return post(`${this.settings.baseUrl}/charges`, payment, { apiKey: this.settings.apiKey, signal });
  }
}
```

`@Retry`， `@Timeout`， `@Cached`， `@Logged`， `@Timed`， `@CircuitBreaker`， `@Bulkhead`, `decoratorOf(adviceFactory)` 项目的一个方面。一个方法上有几个嵌套 读：第一个写在最外面。类中不带日志记录器、指标或缓存 构造函数：当调用时，装饰器从方面运行时读取它们。

### 形成

-   **选择合成根** （表格1及2）。适配器不受任何导入的影响 整个应用程序的策略都是在一个地方读取的，它在不触及类的情况下改变。
-   **一个装饰师是可以接受的** 在适配器上 `infrastructure/` 或者 `platform/` 的政策 是适配器的一部分：记录其自己的重试和 超时规则。不要在两种形式中应用相同的方面：它会运行两次。
-   **从来没有在 `domain/` 或者 `application/`**，以任何形式； `layer/domain-purity` 和 `layer/application-inward` 禁止进口。一个用例得到它的横切行为 来自总线中间件，如下所示。

## 方面

| 方面 | 它是做什么的 | 默认策略 | 从来没有记录 |
| --- | --- | --- | --- |
| `logged({ level?, fields?, logger?, clock? })` | 条目 `<Class>.<method> started` (调试) `succeeded` （`level`，默认为info)或 `failed` | 一个 `DomainError` 失败在 `warn`错误的；错误的 `error`;字段 `class`， `method`， `kind`， `context`， `durationMs`， `errorType`， `errorCode` | 参数、结果、错误消息 |
| `timed({ name?, metrics?, clock? })` | 柱状图 `<name>_duration_ms`,反 `<name>_total` | `name` 是 `dependency_call`;标签 `class`， `method`， `kind`， `context`， `outcome`， `error_code` | 参数、结果、错误消息、标识符 |
| `retry({ attempts, backoff?, retryOn?, sleep? })` | 失败后再次调用 | 从来没有。 `DomainError`、开路或全舱壁(`isTransient`）; 100毫秒加倍到30秒与全抖动 | 没有：它没有记录 |
| `timeout(ms, { signal?, scheduler? })` | 拒绝与 `TimeoutError` （`TIMEOUT`） | 底层调用将继续运行，除非 `signal: true` | 该消息仅命名类和方法 |
| `circuitBreaker({ failureThreshold, resetAfterMs, halfOpenMax?, scope?, isFailure?, onStateChange?, clock? })` | 拒绝与 `CircuitOpenError` （`CIRCUIT_OPEN`)，然后让尝试电话接通 | 每个实例和方法一个电路；一个 `DomainError` 不是失败；一次试用电话 | `onStateChange` 获取类、方法、上下文和两个状态 |
| `cached({ key, ttlMs, cache?, allowKinds? })` | 答案是重复阅读 `Cache` 港口 | 拒绝类 `repository` 和 `use-case`;键为的呼叫 `undefined` 未被缓存；失败不会被缓存；并发的相同调用共享一个调用；缓存失败是失败 | 该键由项目根据参数构建，并仅进入缓存 |
| `bulkhead({ maxConcurrent, maxQueued?, scope? })` | 边界召唤奔跑，拒绝超越 `BulkheadFullError` （`BULKHEAD_FULL`） | 没有队列;每个实例和方法一个隔间 | 没有什么 |

**隐私。** 该规则是跟踪规则之一：条目、标签或由此引发的错误 包包含来自代码（类、方法、上下文、类型）、持续时间、类的名称 一个简短的错误 `code`． 绝不是参数、结果或捕获错误的消息： 他们引用数据。错误被原封不动地重新抛出。

**`timeout` 和取消。** 与 `signal: true` 这种方法是现成的 `AbortSignal` 作为它的 最后声明的参数，当时间到时终止。声明 `signal?: AbortSignal` 最后的， 没有默认值，并将其传递给底层调用。呼叫者经过的信号是 保留：任何一方中止。不带参数的方法不接收任何参数。

**`retry` 和抖动。** `backoff: { baseMs, factor, maxMs, jitter }`. `jitter` 是一个函数 `(delayMs, attempt) => delayMs`： `fullJitter()` 默认情况下, `fullJitter(seededRandom)` 或者 `noJitter` 对于一个确定的时间表。 `sleep` 完全取代了等待。之后的等待 失败的尝试 `n` 是 `baseMs x factor^(n - 1)`，上限为 `maxMs`然后颤抖起来。 `delayBeforeRetry(n, backoff)` 计算它。

**`circuitBreaker` 和指标。** `onStateChange: circuitStateGauge()` 将状态发布为 该指数 `circuit_breaker_state`0个关闭，1个半开，2个开。

![The three states of a circuit: closed lets every call through and counts consecutive failures; after failureThreshold of them it opens and refuses every call with CircuitOpenError; after resetAfterMs it is half-open and lets halfOpenMax trial calls through; a trial that answers closes it, a trial that fails opens it again](https://raw.githubusercontent.com/gbourgeat/ddd-ts-kit/main/docs/assets/diagrams/aspects-circuit-breaker-states.svg)

生意上的拒绝 `DomainError`)证明依赖项回答：它重置计数 就像一个成功者。一个提供者的策略，它可能会消失一段时间，按照下一个的顺序 部分解释:

```ts
import { circuitBreaker, circuitStateGauge, compose, retry, timeout } from '@ddd-ts-kit/aspects';

export const paymentProviderPolicy = compose(
  // Outermost: an open circuit ends the retries at once (retry never retries CIRCUIT_OPEN).
  retry({ attempts: 3, backoff: { baseMs: 200 } }),
  // Five consecutive failures open it for 30 s; the gauge circuit_breaker_state follows it.
  circuitBreaker({ failureThreshold: 5, resetAfterMs: 30_000, onStateChange: circuitStateGauge() }),
  // Innermost: each attempt has its own 2 s.
  timeout(2000),
);
```

**`cached` 用于读取网关和外部适配器。** 从缓存中提供的聚合 在存储库的后面隐藏着它是陈旧的，下一个命令将决定旧的状态。 因此，包装存储库或用例需要显式的 `allowKinds: ['repository']`， 这是一个记录的决定。

项目编写自己的方面 `defineAspect((joinPoint) => async ({ args, proceed }) => ...)`. 外部函数对每个包装实例的每个方法运行一次，并可能应答 `undefined` 来 别管那个方法；内部的在每次调用时运行，并且 `proceed()` 调用里面的东西：

```ts
import { defineAspect, type Aspect } from '@ddd-ts-kit/aspects';
import type { Clock, Logger } from '@ddd-ts-kit/kernel';

/** Warns when a call lasts longer than the threshold: names from the code and a duration, never the arguments. */
export function warnWhenSlow(thresholdMs: number, logger: Logger, clock: Clock): Aspect {
  return defineAspect((joinPoint) => async ({ proceed }) => {
    const startedAt = clock.now().getTime();

    try {
      return await proceed();
    } finally {
      const durationMs = clock.now().getTime() - startedAt;

      if (durationMs > thresholdMs) {
        logger.warn(`${joinPoint.className}.${joinPoint.method} slow`, { durationMs, thresholdMs });
      }
    }
  });
}
```

## 选择包装的内容

`when(selector, ...aspects)` 将方面应用于选择器选择的方法：

| 选择器 | 选择 |
| --- | --- |
| `forKinds('repository', 'external')` | 绑定的实例 `.as(kind)` |
| `forContext('billing')` | 模块的实例 |
| `forClasses('Http*')` | 按类名的实例 |
| `forMethods('find*', 'get*')` | 按名称命名的方法 |
| `allOf(...)`， `anyOf(...)`， `not(...)` | 组合 |

```ts
import { allOf, cached, forKinds, forMethods, when } from '@ddd-ts-kit/aspects';

// The one place arguments are read: build the key from an identifier, never from personal data.
// `undefined` leaves the call uncached. The cache key also holds the class and the method.
const key = (_method: string, [id]: readonly unknown[]): string | undefined => (typeof id === 'string' ? id : undefined);

export const cacheReads = when(allOf(forKinds('gateway', 'external'), forMethods('find*')), cached({ key, ttlMs: 60_000 }));
```

## 订单

`compose(a, b)`， `.with(a, b)`， `aspects(a, b)` 堆叠的装饰器都是一样的： **第一个是最外层的**． 应用程序在一个实例周围嵌套，最外层首先是 `decorate` 钩子，将各个方面模块放在一起 `builder.around(kinds, ...)` 在 模块的顺序（张成的空间） `kernelModule({ instrumentation })` 那就先来吧 对于存储库，首先是域事件的移交，然后是平台模块事件的移交； 最后是绑定本身的方面(`.with(...)`，或者 `aspects` 声明)。 类方法上的装饰器在所有方法中。

![Six nested layers around the method of one instance (an adapter or a repository), outermost first: the decorate hook of createApplication; the span of kernelModule instrumentation; for a repository, the hand-over of its domain events; the aspects of the platform module put with builder.around; the aspects of the binding, .with or the aspects of a declaration; the method decorators of the class; then the method itself](https://raw.githubusercontent.com/gbourgeat/ddd-ts-kit/main/docs/assets/diagrams/aspects-order-of-wrappers.svg)

顺序变化含义：

-   `retry` 然后 `timeout`每次尝试都有自己的时间限制。反过来，一个极限 涵盖了所有的尝试和等待。
-   `timed` 和 `logged` 外 `retry`：每一次通话都有一个测量值， 包括重试。里面，一次一次。
-   `cached` 外 `retry` 和 `circuitBreaker`一击不花一分钱，也不会被对方拒绝 开路。
-   `circuitBreaker` 内部 `retry`：每次失败的尝试计数，开路结束 重试一次，因为 `retry` 不重试 `CircuitOpenError`.

![Two timelines. With retry outside timeout, each of the three attempts has its own 2 s limit, and the call can last three times 2 s plus the waits. With timeout outside retry, one 2 s limit covers the attempts and the waits together, and the call rejects with TimeoutError at 2 s whatever the attempt in progress](https://raw.githubusercontent.com/gbourgeat/ddd-ts-kit/main/docs/assets/diagrams/aspects-retry-and-timeout-order.svg)

```ts
import { compose, retry, timeout } from '@ddd-ts-kit/aspects';

// Each attempt has its own 2 s: the call may last 3 x 2 s, plus the waits between attempts.
export const limitPerAttempt = compose(retry({ attempts: 3 }), timeout(2000));

// One 2 s limit for the whole call: attempts and waits together.
export const limitForTheCall = compose(timeout(2000), retry({ attempts: 3 }));
```

适合大多数适配器的顺序，最外层优先：

```
tracing, logged, timed, cached, retry, circuitBreaker, bulkhead, timeout, the adapter
```

对于内核模块的插装，跟踪范围是内核模块所在的位置 所说: `logged` 和 `timed` 给予 `decorate` 钩子在跨度之外 给 `builder.around` 或者绑定在里面。

存储库还有一个包装器 `kernelModule()` 之间的 `decorate` 虎克和鱼 它的绑定方面：它保存的域事件的移交，其中的单元 在提交之前，在其结束时进行工作分派。绑定的各个方面 （`.with(retry(...))`)都在里面，重试保存一次会宣布它的事件。在一个 这些事件的处理程序的工作单元在保存期间不再运行，因此不会重试保存 永远覆盖它。在any之外，save在解析之前进行调度 `retry` 给予 `decorate` 在处理程序失败后，Hook将再次运行保存，而不会留下任何事件 调度。将存储库的重试放在其绑定上。

## 方面运行时

`logged`， `timed` 和 `cached` 需要一个日志，指标和缓存。他们从他们的 选项，或者从组合根安装一次的运行时中获取 `installAspectRuntime({ logger, metrics, cache, clock, scheduler })`． 时读取运行时 调用永远不会在类被装饰或实例被包装时进行，因此 语句 `main.ts` 没关系。失踪的合作者加薪 `AspectRuntimeError`， 这就是它的名字。时钟和调度器默认为系统时钟。 `Scheduler` 是港口吗？ 为“稍后运行此包”(`schedule(work, delayMs)`类的抽象类 内核端口： `systemScheduler` 是默认值， `ManualScheduler` 的 `./testing` 一个 规格的。

## 用例：总线中间件

处理程序不会一个一个地包装；公共汽车是：

```ts
import { loggingMiddleware, metricsMiddleware } from '@ddd-ts-kit/aspects';
import { InMemoryCommandBus, InMemoryQueryBus, type Logger, type Metrics } from '@ddd-ts-kit/kernel';

declare const logger: Logger;
declare const metrics: Metrics;

export const commands = new InMemoryCommandBus().use(loggingMiddleware(logger)).use(metricsMiddleware(metrics));
export const queries = new InMemoryQueryBus().use(loggingMiddleware(logger)).use(metricsMiddleware(metrics));
```

相同的项和级数 `logged` 和 `timed` (前缀 `use_case`)，以消息命名 类，而不是它的内容； `command ChargeOrder started`,然后 `succeeded` 或者 `failed`，与 字段 `message` 和 `messageKind`，以及系列 `use_case_duration_ms` 和 `use_case_total` 贴上标签 `message` 和 `kind`． 没有中间件打开一个 `ExecutionScope`：命令 不携带关联id，并且当路由或 消费者运行，在任何总线之前。在边界处打开。

## StdoutLogger

内核的一个小适配器 `Logger` 端口写入JSON行：

```ts
import { StdoutLogger } from '@ddd-ts-kit/aspects';
import { logFieldsOf, type ExecutionScope } from '@ddd-ts-kit/kernel';
import { traceLogFields } from '@ddd-ts-kit/tracing';

declare const scope: ExecutionScope;

// Without tracing: the correlation id, actor and tenant of the current execution context.
export const logger = new StdoutLogger({
  level: 'info',
  base: { service: 'shop-api' },
  fields: () => {
    const context = scope.current();

    return context === undefined ? {} : logFieldsOf(context);
  },
});

// With @ddd-ts-kit/tracing: the same fields, plus trace_id and span_id of the active span.
export const tracedLogger = new StdoutLogger({ base: { service: 'shop-api' }, fields: () => traceLogFields(scope) });

logger.info('payment accepted', { orderId: 'o-42' });
// {"time":"2026-03-01T10:00:00.000Z","level":"info","message":"payment accepted","service":"shop-api","correlationId":"...","origin":"http","actorId":"...","orderId":"o-42"}
```

`fields` 在每个条目处读取。此包既不依赖于跟踪包也不依赖于 执行范围：项目选择每个条目所携带的内容。当同样的场来自 几个地方，后者获胜： `base`，然后是田野 `child(...)`,然后 `fields()`,然后 日志调用的字段。没有可以覆盖 `time`， `level` 或者 `message`.

它实现了整个端口，由 `loggerContract` 像pino和Winston适配器：

-   **错误。** 一个 `Error` 作为字段传递(`logger.error('payment failed', { error })`)是 写成 `describeError` 的内核描述了它： `type`， `code` 对于一个 `DomainError`， `message`， `stack`,然后 `cause` 同样的道理。 `scrubMessages: true` 写 `[message removed]` 对于消息的每一个错误，都不是一个错误 `DomainError`打开它 当原木离开你的手。 `logged` 和 `loggingMiddleware` 不担心：他们 永远不要传递错误 `errorType` 和 `errorCode`.
-   **`isLevelEnabled(level)`** 回答阈值(`level`、默认 `info`）。 `logged` 和 `loggingMiddleware` 先问它：门槛以下没有场是建的 `fields` 的函数 `logged` 未被调用。
-   **`flush()`** 解析一次标准输出，占用到目前为止编写的每一行，永远不会 拒绝;之后记录器仍然可用。 `app.stop()` 的函数最后调用它。
-   **`stream`** 将行发送到标准输出以外的另一个流，并且 `flush()` 等待 为了那条小溪。一个 `write` 项目的每一条线反而收到并赢得 `stream`.
-   **日志调用永远不会抛出。** 无法读取或序列化的字段(getter会抛出， （圆形物体），在条目或子条目上，给出没有它们的条目，标记 `logError: "unserializable fields"`.

它将凭证隐藏为 `@ddd-ts-kit/pino` 和 `@ddd-ts-kit/winston` 做：每一个的价值 关键的 `CREDENTIAL_KEYS` 内核的，不管它是什么情况(`isCredentialKey`)，是写的 `[redacted]` 在入口的顶部和下面一层(`headers.authorization`)，在田野里 系的，系子的，系的 `base` 和 `fields`． 没有更深的，也不是在列表中。的对象 传递的对象永远不会写入。

## 包装保证了什么

-   实例被包装在：同一个对象， `instanceof`， `this` 和 `private` 成员 完好无损。被冻结的实例由相同类型的代理作为前端，其方法运行于 这个代理是 `this`.
-   **应用于实例的方面通知从外部到达实例的调用。** `private` 被编译器擦除，并且名称没有说明方法是否为公共(下划线 （这是一种习惯，而不是规则），所以公共API不能被可靠地挑选出来：每个异步 方法可通过名称访问 `private async` 方法和包含异步对象的字段 包含的函数被封装。区分这些叫声的是它们的来源。而一个 建议调用在实例上运行，该实例对其自身进行调用，对公共或 一个 `private` 方法，是该调用的内部，并且不会被同一方面再次建议： `retry({ attempts: 3 })` 围绕一个公共方法，经过一个私有步骤得到三个 尝试，而不是九次 `logged()` 和 `timed()` 录一个电话。守卫是按人看守的 实例和每个方面，并遵循调用的异步流 （`AsyncLocalStorage`)：同时从外部发出的两个呼叫都是建议的，以此类推 是被建议调用结束后留下的任何东西（计时器、侦听器）。
-   它的后续内容是：从协作者返回到实例的调用，而 建议调用正在运行也是调用的内部； `cached` 为来自的来电者服务 在外面，不是一个方法读另一个通过 `this`； `bulkhead` 和 `circuitBreaker` 数 一个电话。方法装饰器不关心：它建议它所使用的唯一方法at 每一个电话，打进来的 `this` 包括在内。
-   跟踪的目的不同： `traceDependency` 的 `@ddd-ts-kit/tracing` 为的子跨度打开 实例对自身的调用。span描述了一个步骤，在这个步骤上有一个通知。
-   冻结的实例保持冻结：它的字段， `private` 包括在内的，不能分配。状态 它必须改变它持有的对象中的生命值(a) `Map`（数组）。
-   对一个实例应用相同的方面两次是无操作的。
-   getter、符号、生成器、同步方法和生命周期钩子(`LIFECYCLE_HOOKS`) 独处。
-   声明的方法 `async` 从其第一次调用开始被建议。一个只返回A的方法 Promise从它的第二个调用中被告知：declare adapter methods `async`． 装饰方法 总是被建议。

## 测试

包装器将其协作器作为选项，因此规范传递内核的副本。为 装饰类和读取运行时的方面， `@ddd-ts-kit/aspects/testing` 给了 `createAspectFakes()` (内存记录器，度量和缓存，固定时钟和 `ManualScheduler` 在同一时钟)和 `withAspectRuntime(runtime, work)`：

```ts
import { expect, it } from 'vitest';
import { Logged, Timeout } from '@ddd-ts-kit/aspects/decorators';
import { createAspectFakes, withAspectRuntime } from '@ddd-ts-kit/aspects/testing';

const fetchRate = (_pair: string): Promise<number> => new Promise(() => undefined); // a provider that never answers

class SlowRates {
  @Logged()
  @Timeout(2000)
  async rateOf(pair: string): Promise<number> {
    return fetchRate(pair);
  }
}

it('gives up after two seconds, and logs the failure without the pair', async () => {
  const fakes = createAspectFakes(); // logger, metrics, cache, clock, scheduler
  const rates = new SlowRates();

  const pending = withAspectRuntime(fakes, () => rates.rateOf('EUR-USD'));
  fakes.scheduler.advance(2000); // time moves when the test says so

  await expect(pending).rejects.toMatchObject({ code: 'TIMEOUT' });
  expect(fakes.logger.entries.map((entry) => entry.message)).toEqual(['SlowRates.rateOf started', 'SlowRates.rateOf failed']);
});
```

`withAspectRuntime` 保留工作及其等待的所有内容，而并发测试则不保留 查看彼此的运行时间。与 `createTestApplication` 在di包中，应用程序是 运行时: `withAspectRuntime(app, () => ...)`，然后断言 `app.logger.entries` 和 `app.metrics`.

## 装饰器模式

的装饰者 `./decorators` 在TypeScript的两种装饰器模式下都可以工作，使用相同的 源线。

| 堆栈 | `tsconfig.json` |
| --- | --- |
| class-validator, NestJS， TypeORM实体，任何带有遗留装饰符的库 | `"experimentalDecorators": true` |
| 别的 | 离开 `experimentalDecorators` unset：标准（TC39）装饰者不需要标志 |

-   **如何检测模式。** 通过呼叫：标准的是 `(method, context)` 与一个 Context对象，遗留的那个 `(prototype, name, descriptor)`. `decoratorOf` 读起来 已接收，因此一个函数同时服务于两个方面，而一个方面有一个实现。
-   **有什么不同。** 在标准模式中，每个实例的方法在 实例被构造。遗留模式没有初始化项，因此该方法被替换为 每个实例的原型和通知都是在第一次调用时构建的。状态（电路） 一个隔间，一个飞行)是每个实例和每个方法，几个装饰器 当他们在两者中阅读时，他们都不需要 `emitDecoratorMetadata`.
-   **编译器。** TypeScript 5、6和原生的TypeScript 7都可以编译这两种模式。Node没有 运行decorator语法，以两种方式编译代码。
-   **轻快地。** 它自己降低了遗留装饰器：一个带有 `experimentalDecorators` 需要什么。它不降低标准（规格进口） `.ts` 使用的文件 `@Retry` 失败，有语法错误):add `decorators()` 的 `@ddd-ts-kit/testing/vitest` 来 Vitest配置。在组合根目录下包装的文件在这两种模式下都不需要。
-   **用声明。** `@Adapter(...)` 在课堂上 `@Retry(...)` 关于其方法 撰写，与 `aspects` 方法的装饰器周围的声明： `@Adapter(PaymentGateway, { kind: 'external' })`． 一个静态方法，A 由符号和硬私有(`#`)方法被拒绝。

这个 `./decorators` 条目是单独的：包中的其他内容都不会导入它。

## 限制

-   状态（电路、隔间、单次飞行）存在于每个封装实例的进程中。一个 瞬态绑定在每个实例中获得一个新状态。
-   `retry` 按原样重复调用：仅对可以安全重复的操作重复。
-   `timeout` 没有 `signal` 不会停止基础调用。
-   包装调用总是回应一个承诺：不要包装方法返回A的对象 可链查询生成器。
-   `when` 选择的方面的方法 `defineAspect`， `compose` 或者 `when`;一个普通 函数本身用作方面过滤器。

## 常见问题

### @ddd-ts-kit/aspects 是什么？

从外部应用的横切行为：日志记录、度量、重试、超时、断路器和缓存，作为组合根的可组合包装器，作为总线中间件，或作为适配器上的标准装饰器。域保持干净。

### 如何安装 @ddd-ts-kit/aspects？

在项目目录运行 npm install @ddd-ts-kit/aspects，或使用 pnpm add / yarn add / bun add。

### @ddd-ts-kit/aspects 支持 TypeScript 吗？

支持。包内自带 TypeScript 类型声明（package\.json 声明了 types 入口），安装后即可获得类型提示，无需再安装 @types 包。

### @ddd-ts-kit/aspects 支持 ESM 还是 CommonJS？

以 ES Module 形式发布，推荐使用 import 引入。在 CommonJS 项目中，Node\.js 20\.19\+ / 22\.12\+ 可以直接 require\(\) 不含顶层 await 的 ES Module，更早的版本需要改用动态 import\(\)。

### @ddd-ts-kit/aspects 需要什么版本的 Node\.js？

package\.json 的 engines 字段要求 Node\.js \>=22\.12\.0。版本不符时 npm 默认只给出警告，开启 engine-strict 后会拒绝安装。

### @ddd-ts-kit/aspects 可以商用吗？

@ddd-ts-kit/aspects 使用 MIT 许可。宽松许可：允许商业使用、修改和再分发，通常只需在分发时保留原版权与许可声明。以上为概括，不构成法律意见，请以许可原文为准。

### @ddd-ts-kit/aspects 还在维护吗？

最新版本 0\.106\.0 发布于 2026/11/01（2 天前）；至今共发布 63 个版本，其中过去 12 个月发布了 63 个；首次发布于 2026/10/11。发布频率只是参考信号：功能稳定的工具可能很少需要更新，建议结合源码仓库的 issue 和提交记录判断。

