Metadata and execution lifecycle
Decorated methods resolve a fresh exchange for each call while reusing a per-instance, per-method executor. Hooks run on the service instance, so avoid storing mutable per-request state on that instance when calls can overlap.
Choose beforeExecute to add per-call exchange attributes or select an extractor; choose afterExecute to inspect a validated response without consuming its body. Both are optional and have no default action. Call RequestExecutor or manipulate FunctionMetadata only when building a runtime extension; regular services need neither.
RequestExecutor lifecycle
new RequestExecutor(target: any, metadata: FunctionMetadata) and execute(args: any[]): Promise<any> implement this sequence:
- Resolve the Fetcher and merge arguments into request/attributes.
- Store the target and metadata under
DECORATOR_TARGET_ATTRIBUTE_KEY = '__decorator_target__'andDECORATOR_METADATA_ATTRIBUTE_KEY = '__decorator_metadata__'. - Call
fetcher.resolveExchangewith the selected result extractor. - Await optional
ExecuteLifeCycle.beforeExecute(exchange). - Await
fetcher.interceptors.exchange(exchange). - Await optional
ExecuteLifeCycle.afterExecute(exchange). - Return exchange for EXCHANGE mode, otherwise await
exchange.extractResult().
Both hooks return void | Promise<void>. Before runs before body/URL interceptors; after runs after response validation but before extraction. Neither is a finally hook. A before rejection prevents I/O; a pipeline rejection skips after; an after rejection prevents extraction. Hook and extractor failures occur outside the manager's error-interceptor catch. Add caller try/finally for unconditional cleanup.
EndpointReturnType.EXCHANGE = 'Exchange' returns the exchange without extraction. RESULT = 'Result' is the default. EndpointReturnTypeCapable.returnType? exposes this option; return mode does not infer from a method's return annotation.
FunctionMetadata
new FunctionMetadata(name, api, endpoint, parameters: Map<number, ParameterMetadata>) retains these public fields. Its methods expose the same resolution used by decorated calls:
| Member | Return / precedence |
|---|---|
fetcher | Required Fetcher resolved from endpoint, API, global registry. Missing registration throws. |
resolvePath(parameterPath?) | Combine endpoint/API basePath and parameter/endpoint path using truthy fallback. Absolute endpoint paths override the base. |
resolveTimeout() | Endpoint defined timeout, then API; client fallback is applied later. |
resolveResultExtractor() | Endpoint, API, JSON default. |
resolveAttributes() | New Map; API then endpoint entries. |
resolveEndpointReturnType() | Endpoint, API, RESULT default. |
resolveExchangeInit(args) | Required request and attributes fields only, with binding/request merge applied; does not send HTTP. |
Reflection and caching
API_METADATA_KEY stores class metadata on the constructor. ENDPOINT_METADATA_KEY and PARAMETER_METADATA_KEY store method and parameter metadata on prototype/property. These are exported Symbols, not stable string keys to recreate with Symbol(...).
buildRequestExecutor(target, defaultFunctionMetadata): RequestExecutor creates/uses target.requestExecutors: Map<string, RequestExecutor> and shallow-merges instance apiMetadata on first use. It is public for generators/extensions; normal users call decorated methods. api walks the prototype chain and binds the closest string-named function once per name, preserving inherited metadata lookup while installing executors on the decorated class. Avoid using requestExecutors as your own instance field.
The following example records resolved calls without a server by replacing the network interceptor on an isolated client. This is a test arrangement, not a production retry mechanism.
Complete example
import {
Fetcher,
FETCH_INTERCEPTOR_NAME,
FETCH_INTERCEPTOR_ORDER,
type FetchExchange,
} from '@ahoo-wang/fetcher';
import {
api,
get,
autoGeneratedError,
type ExecuteLifeCycle,
} from '@ahoo-wang/fetcher-decorator';
const client = new Fetcher({ baseURL: 'https://example.com' });
client.interceptors.request.eject(FETCH_INTERCEPTOR_NAME);
client.interceptors.request.use({
name: 'local-response',
order: FETCH_INTERCEPTOR_ORDER,
intercept(exchange) {
exchange.response = Response.json({ ok: true });
},
});
@api('/health', { fetcher: client })
class Health implements ExecuteLifeCycle {
beforeExecute(exchange: FetchExchange) {
exchange.attributes.set('started', true);
}
afterExecute(exchange: FetchExchange) {
console.assert(exchange.response?.status === 200);
}
@get()
check(): Promise<{ ok: boolean }> {
throw autoGeneratedError();
}
}
console.assert((await new Health().check()).ok);Public symbols and source
| Symbol | Implementation |
|---|---|
API_METADATA_KEY | apiDecorator.ts:90 |
buildRequestExecutor | apiDecorator.ts:164 |
ENDPOINT_METADATA_KEY | endpointDecorator.ts:31 |
EndpointReturnType | endpointReturnTypeCapable.ts:14 |
EndpointReturnTypeCapable | endpointReturnTypeCapable.ts:19 |
ExecuteLifeCycle | executeLifeCycle.ts:23 |
FunctionMetadata | functionMetadata.ts:100 |
DECORATOR_TARGET_ATTRIBUTE_KEY | requestExecutor.ts:17 |
DECORATOR_METADATA_ATTRIBUTE_KEY | requestExecutor.ts:18 |
RequestExecutor | requestExecutor.ts:61 |
