@ddd-ts-kit/aspects

中文文档
0.106.0发布于 2026/11/01MITTypeScriptES Module构建来源已验证

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

中文使用文档
文档目录16 节
  1. 它在应用程序中的位置
  2. 三种形式,一种实现
  3. 1. 在组合根,在一个绑定上(首选)
  4. 2. 在每个绑定的组合根
  5. 3. 作为适配器上的装饰器
  6. 形成
  7. 方面
  8. 选择包装的内容
  9. 订单
  10. 方面运行时
  11. 用例:总线中间件
  12. StdoutLogger
  13. 包装保证了什么
  14. 测试
  15. 装饰器模式
  16. 限制

英语 · 法语

@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

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

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. 在组合根,在一个绑定上(首选)

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. 在每个绑定的组合根

// 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 跟踪包的钩子是否写成了一个方面:

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. 作为适配器上的装饰器

// 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>_totalname 是 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_state0个关闭,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

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

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() 调用里面的东西:

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(...)组合
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

顺序变化含义:

  • 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

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 一个 规格的。

用例:总线中间件

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

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行:

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):

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 共发布 63 个版本,首次发布于 2026/10/11;过去 12 个月发布了 63 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.106.0latest2026/11/012 天前
0.105.02026/10/312 天前
0.104.02026/10/312 天前
0.103.02026/10/313 天前
0.102.02026/10/313 天前
0.101.02026/10/303 天前

关于 @ddd-ts-kit/aspects 的常见问题

@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 和提交记录判断。