@ddd-ts-kit/aspects
Logging, metrics, retries, timeouts, circuit breakers and caching are not business logic, so
they are never written inside it. Each one is an aspect: one implementation, put around an
adapter from outside, the way @ddd-ts-kit/tracing puts spans around it. The wrapped class
imports nothing, and domain/ and application/ never see this package.
npm i @ddd-ts-kit/aspectsWhere it sits in an application
Section titled “Where it sits in an application”A call leaves a use case through a port. What answers it is the adapter bound to that port, wrapped by the aspects the composition root put around it. The use case itself is wrapped too, but by the buses: their middlewares log and measure every command and query.
Instead of writing the policy into each adapter, write the adapter alone and put the policy around it:
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' });Three forms, one implementation
Section titled “Three forms, one implementation”The same policy, written three ways. All three run the same code.
1. At the composition root, on one binding (preferred)
Section titled “1. At the composition root, on one binding (preferred)”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.tsexport abstract class PaymentGateway { abstract charge(payment: Payment, signal?: AbortSignal): Promise<Receipt>;}
// contexts/billing/application/ports/payment-settings.port.tsexport abstract class PaymentSettings { abstract readonly baseUrl: string;
abstract readonly apiKey: string;}
// contexts/billing/infrastructure/external/stripe.gateway.tsexport 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.tsexport 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 })); },});The port is an abstract class and the class is what is bound. StripeGateway has no import
from this package and no knowledge of the policy. In the declared style, the default of a
generated project, there is no module file and the same policy is stated in the declaration
of the adapter: @Adapter(PaymentGateway, { kind: 'external', aspects: [retry({ attempts: 3 }), timeout(2000)] }). Without a container an aspect is a function:
retry({ attempts: 3 })(new StripeGateway(settings), { kind: 'external' }).
2. At the composition root, on every binding
Section titled “2. At the composition root, on every binding”import { Cache, Clock, Logger, Metrics } from '@ddd-ts-kit/kernel';import { createApplication, kernelModule, type ContextModule } from '@ddd-ts-kit/di';import { StdoutLogger, aspects, forKinds, installAspectRuntime, logged, retry, timed, timeout, when, type Aspect } from '@ddd-ts-kit/aspects';
declare const tracing: Aspect; // belowdeclare 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 is the hook of the tracing package written as an aspect:
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 }) });An application on kernelModule({ instrumentation: traceApplication() }), which is what the
CLI generates, does not write this tracing aspect: the kernel module traces the bindings
itself. Its span is inside whatever the decorate hook adds and outside the aspects of a
binding (see "Order"). A generated project does not use the decorate hook either: its
platform module puts the default policy around the adapters that leave the process with
builder.around(['external', 'gateway'], () => when(forKinds('external', 'gateway'), retry(...), timeout(...))),
and an adapter that states a policy of its own wraps it in ownPolicy(...) of
platform/resilience/, which the default policy then leaves alone.
3. As a decorator, on an adapter
Section titled “3. As a decorator, on an adapter”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, and
decoratorOf(adviceFactory) for an aspect of the project. Several on one method nest as they
read: the first written is outermost. The class takes no logger, metrics or cache in its
constructor: the decorators read them from the aspect runtime when a call is made.
Which form
Section titled “Which form”- Prefer the composition root (forms 1 and 2). The adapter stays free of any import, the policy of a whole application is read in one place, and it changes without touching a class.
- A decorator is acceptable on an adapter in
infrastructure/orplatform/whose policy is part of what the adapter is: a client of a provider that documents its own retry and timeout rules. Do not apply the same aspect in both forms: it would run twice. - Never in
domain/orapplication/, in any form:layer/domain-purityandlayer/application-inwardforbid the import. A use case gets its cross-cutting behaviour from a bus middleware, below.
Aspects
Section titled “Aspects”| Aspect | What it does | Default policy | Never records |
|---|---|---|---|
logged({ level?, fields?, logger?, clock? }) |
entries <Class>.<method> started (debug), then succeeded (level, default info) or failed |
a DomainError fails at warn, a fault at error; fields class, method, kind, context, durationMs, errorType, errorCode |
arguments, results, error messages |
timed({ name?, metrics?, clock? }) |
histogram <name>_duration_ms, counter <name>_total |
name is dependency_call; labels class, method, kind, context, outcome, error_code |
arguments, results, error messages, identifiers |
retry({ attempts, backoff?, retryOn?, sleep? }) |
calls again after a failure | never after a DomainError, an open circuit or a full bulkhead (isTransient); 100 ms doubling up to 30 s with full jitter |
nothing: it records nothing |
timeout(ms, { signal?, scheduler? }) |
rejects with TimeoutError (TIMEOUT) |
the underlying call keeps running unless signal: true |
the message names the class and method only |
circuitBreaker({ failureThreshold, resetAfterMs, halfOpenMax?, scope?, isFailure?, onStateChange?, clock? }) |
refuses calls with CircuitOpenError (CIRCUIT_OPEN) after consecutive failures, then lets trial calls through |
one circuit per instance and method; a DomainError is not a failure; one trial call |
onStateChange gets class, method, context and the two states |
cached({ key, ttlMs, cache?, allowKinds? }) |
answers a repeated read from the Cache port |
refuses kinds repository and use-case; a call whose key is undefined is not cached; failures are not cached; concurrent identical calls share one call; a failing cache is a miss |
the key is built by the project from the arguments and goes to the cache only |
bulkhead({ maxConcurrent, maxQueued?, scope? }) |
bounds the calls running at once, refuses beyond with BulkheadFullError (BULKHEAD_FULL) |
no queue; one compartment per instance and method | nothing |
Privacy. The rule is the one of tracing: an entry, a label or an error raised by this
package carries names from the code (class, method, context, kind), a duration, the class of
an error and its short code. Never an argument, a result or the message of a caught error:
they quote data. Errors are rethrown untouched.
timeout and cancellation. With signal: true the method is handed an AbortSignal as its
last declared parameter, aborted when the time is up. Declare signal?: AbortSignal last,
without a default value, and pass it to the underlying call. A signal the caller passed there is
kept: either one aborts. A method without parameter receives none.
retry and jitter. backoff: { baseMs, factor, maxMs, jitter }. jitter is a function
(delayMs, attempt) => delayMs: fullJitter() by default, fullJitter(seededRandom) or
noJitter for a deterministic schedule. sleep replaces the wait altogether. The wait after
the failed attempt n is baseMs x factor^(n - 1), capped at maxMs, then jittered:
delayBeforeRetry(n, backoff) computes it.
circuitBreaker and metrics. onStateChange: circuitStateGauge() publishes the state as
the gauge circuit_breaker_state: 0 closed, 1 half-open, 2 open.
A business refusal (a DomainError) proves that the dependency answers: it resets the count
like a success. A policy for a provider that may go away for a while, in the order the next
sections explain:
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 is for reads of gateways and external adapters. An aggregate served from a cache
behind a repository hides that it is stale, and the next command would decide on old state.
Wrapping a repository or a use case therefore takes an explicit allowKinds: ['repository'],
which is a decision to record.
A project writes its own aspect with defineAspect((joinPoint) => async ({ args, proceed }) => ...).
The outer function runs once per method of each wrapped instance and may answer undefined to
leave that method alone; the inner one runs at each call, and proceed() calls what is inside:
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 }); } } });}Selecting what is wrapped
Section titled “Selecting what is wrapped”when(selector, ...aspects) applies aspects to the methods a selector picks:
| Selector | Picks |
|---|---|
forKinds('repository', 'external') |
instances bound with .as(kind) |
forContext('billing') |
instances of a module |
forClasses('Http*') |
instances by class name |
forMethods('find*', 'get*') |
methods by name |
allOf(...), anyOf(...), not(...) |
combinations |
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) and stacked decorators all read the same way:
the first is outermost. Around one instance the application nests, outermost first: the
decorate hook, the aspects modules put around a kind with builder.around(kinds, ...) in
the order of the modules (the span of kernelModule({ instrumentation }) comes first, then
for a repository the hand-over of its domain events, then the ones of the platform module),
and last the aspects of the binding itself (.with(...), or aspects of a declaration).
Decorators on the methods of the class are inside all of them.
Order changes meaning:
retrythentimeout: each attempt has its own time limit. The other way round, one limit covers all the attempts and the waits between them.timedandloggedoutsideretry: one measurement per call as the caller lived it, retries included. Inside, one per attempt.cachedoutsideretryandcircuitBreaker: a hit costs nothing and is not refused by an open circuit.circuitBreakerinsideretry: each failed attempt counts, and an open circuit ends the retries at once, sinceretrydoes not retry aCircuitOpenError.
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 }));The order that fits most adapters, outermost first:
tracing, logged, timed, cached, retry, circuitBreaker, bulkhead, timeout, the adapterWith the instrumentation of the kernel module, the tracing span is where the kernel module
puts it: logged and timed given to the decorate hook are outside the span, the ones
given to builder.around or to a binding are inside it.
A repository has one more wrapper, put by kernelModule() between the decorate hook and the
aspects of its binding: the hand-over of the domain events of what it saves, which the unit of
work dispatches at its end, before the commit. So the aspects of a binding
(.with(retry(...))) are inside it, where a retried save announces its events once. Inside a
unit of work a handler of those events no longer runs during the save, so no retry of a save
ever covers it. Outside of any, the save dispatches before it resolves: a retry given to the
decorate hook would then run the save again after a handler failed, with no event left to
dispatch. Put the retry of a repository on its binding.
The aspect runtime
Section titled “The aspect runtime”logged, timed and cached need a logger, metrics and a cache. They take them from their
options, or from the runtime that the composition root installs once with
installAspectRuntime({ logger, metrics, cache, clock, scheduler }). The runtime is read when
a call is made, never when a class is decorated or an instance wrapped, so the order of the
statements in main.ts does not matter. A missing collaborator raises AspectRuntimeError,
which names it. The clock and the scheduler default to the system ones. Scheduler is the port
of this package for "run this later" (schedule(work, delayMs)), an abstract class like the
ports of the kernel: systemScheduler is its default, ManualScheduler of ./testing the one
of a spec.
Use cases: bus middlewares
Section titled “Use cases: bus middlewares”Handlers are not wrapped one by one; the buses are:
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));Same entries and series as logged and timed (prefix use_case), named after the message
class, never its content: command ChargeOrder started, then succeeded or failed, with the
fields message and messageKind, and the series use_case_duration_ms and use_case_total
labelled message and kind. There is no middleware opening an ExecutionScope: a command
does not carry a correlation id, and the scope must already be open when the route or the
consumer runs, before any bus. Open it at the boundary.
StdoutLogger
Section titled “StdoutLogger”A small adapter of the kernel Logger port writing JSON lines:
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 is read at each entry. This package depends neither on the tracing package nor on an
execution scope: the project chooses what each entry carries. When the same field comes from
several places, the later wins: base, then the fields of child(...), then fields(), then
the fields of the log call. None can overwrite time, level or message.
It implements the whole port, held by loggerContract like the pino and winston adapters:
- Errors. An
Errorpassed as a field (logger.error('payment failed', { error })) is written asdescribeErrorof the kernel describes it:type,codefor aDomainError,message,stack, thencausethe same way.scrubMessages: truewrites[message removed]for the message of every error that is not aDomainError: turn it on when the logs leave your hands.loggedandloggingMiddlewareare not concerned: they never pass an error, onlyerrorTypeanderrorCode. isLevelEnabled(level)answers the threshold (level, defaultinfo).loggedandloggingMiddlewareask it first: below the threshold no field is built and thefieldsfunction ofloggedis not called.flush()resolves once the standard output took every line written so far, and never rejects; the logger stays usable afterwards.app.stop()of the di package calls it last.- A log call never throws. Fields that cannot be read or serialized (a getter that throws,
a circular object), on an entry or on a child, give the entry without them, marked
logError: "unserializable fields".
It redacts no credential: @ddd-ts-kit/pino and @ddd-ts-kit/winston do.
What wrapping guarantees
Section titled “What wrapping guarantees”- The instance is wrapped in place: same object,
instanceof,thisandprivatemembers intact. A frozen instance is fronted by a proxy of the same type, and its methods run with that proxy asthis. - An aspect applied to an instance advises the calls that reach it from outside.
privateis erased by the compiler, and a name does not say whether a method is public (an underscore is a habit, not a rule), so the public API cannot be picked out reliably: every asynchronous method reachable by name, aprivate asyncmethod and a field holding an asynchronous function included, is wrapped. What tells the calls apart is where they come from. While an advised call is running on an instance, a call that instance makes on itself, to a public or aprivatemethod, is the inside of that call and is not advised again by the same aspect:retry({ attempts: 3 })around a public method that goes through a private step makes three attempts, not nine, andlogged()andtimed()record one call. The guard is kept per instance and per aspect, and follows the asynchronous flow of the call (AsyncLocalStorage): two calls made from outside at the same time are both advised, and so is whatever an advised call left behind (a timer, a listener) once that call is over. - What follows from it: a call that comes back to the instance from a collaborator while the
advised call is running is the inside of that call too;
cachedserves the callers from outside, not a method reading another throughthis;bulkheadandcircuitBreakercount one call. A method decorator is not concerned: it advises the one method it is put on, at every call, the ones made throughthisincluded. - Tracing differs on purpose:
traceDependencyof@ddd-ts-kit/tracingopens a child span for a call an instance makes on itself. A span describes a step, where an advice acts on it. - A frozen instance stays frozen: its fields,
privateones included, cannot be assigned. State it must change lives in an object it holds (aMap, an array). - Applying the same aspect twice to an instance is a no-op.
- Getters, symbols, generators, synchronous methods and lifecycle hooks (
LIFECYCLE_HOOKS) are left alone. - A method declared
asyncis advised from its first call. A method that merely returns a promise is advised from its second call: declare adapter methodsasync. A decorated method is always advised.
Testing
Section titled “Testing”Wrappers take their collaborators as options, so a spec passes fakes of the kernel. For
decorated classes and for aspects that read the runtime, @ddd-ts-kit/aspects/testing gives
createAspectFakes() (an in-memory logger, metrics and cache, a fixed clock and a
ManualScheduler on the same clock) and 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 holds for the work and everything it awaits, and concurrent tests do not
see each other's runtime. With createTestApplication of the di package the application is the
runtime: withAspectRuntime(app, () => ...), then assert app.logger.entries and app.metrics.
Decorator modes
Section titled “Decorator modes”The decorators of ./decorators work under both decorator modes of TypeScript, with the same
source line.
| Stack | tsconfig.json |
|---|---|
| class-validator, NestJS, TypeORM entities, any library with legacy decorators | "experimentalDecorators": true |
| anything else | leave experimentalDecorators unset: standard (TC39) decorators need no flag |
- How the mode is detected. By the call: the standard one is
(method, context)with a context object, the legacy one(prototype, name, descriptor).decoratorOfreads which it received, so one function serves both and an aspect has one implementation. - What differs. In the standard mode the method of each instance is wrapped when the
instance is constructed. The legacy mode has no initializer, so the method is replaced on
the prototype and the advice of each instance is built at its first call. State (a circuit,
a compartment, a single flight) is per instance and per method in both, several decorators
nest as they read in both, and neither needs
emitDecoratorMetadata. - Compilers. TypeScript 5, 6 and the native TypeScript 7 compile both modes. Node does not run decorator syntax, so the code is compiled either way.
- Vitest. It lowers legacy decorators by itself: a project with
experimentalDecoratorsneeds nothing. It does not lower standard ones (a spec importing a.tsfile that uses@Retryfails with a syntax error): adddecorators()of@ddd-ts-kit/testing/vitestto the Vitest config. Files wrapped at the composition root need nothing in either mode. - With a declaration.
@Adapter(...)on the class and@Retry(...)on its methods compose, with theaspectsof the declaration around the decorators of the methods:@Adapter(PaymentGateway, { kind: 'external' }). A static method, a method named by a symbol and a hard-private (#) method are refused.
The ./decorators entry is separate: nothing else in the package imports it.
Limits
Section titled “Limits”- State (circuits, compartments, single-flight) lives in the process, per wrapped instance. A transient binding gets a new state with each instance.
retryrepeats the call as it is: only for operations that are safe to repeat.timeoutwithoutsignaldoes not stop the underlying call.- A wrapped call always answers a promise: do not wrap an object whose methods return a chainable query builder.
whenselects methods of aspects made bydefineAspect,composeorwhen; a plain function used as an aspect filters by itself.