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 once and merge arguments into request/attributes. Steps 3 and 5 mirror
Fetcher.exchange, with the hooks in between. - 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 (a request path selects the URL and is not carried); 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 builds a fresh executor for each call, adds no field to the instance, and shallow-merges the instance's apiMetadata over the class metadata, so replacing or mutating target.apiMetadata takes effect on the next call. Nothing is cached, so an overriding endpoint and the parent endpoint it reaches through super.method() each send their own request. It is public for generators/extensions; normal users call decorated methods. api walks the prototype chain and binds the closest function once per name (string or symbol), preserving inherited metadata lookup while installing executors on the decorated class. A method the decorated class overrides without its own endpoint decorator keeps its implementation. Calling a bound method without its instance (const { list } = service; list()) throws a TypeError that names the class and method; bind it or call it on the service.
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:91 |
buildRequestExecutor | apiDecorator.ts:188 |
ENDPOINT_METADATA_KEY | endpointDecorator.ts:32 |
EndpointReturnType | endpointReturnTypeCapable.ts:14 |
EndpointReturnTypeCapable | endpointReturnTypeCapable.ts:19 |
ExecuteLifeCycle | executeLifeCycle.ts:23 |
FunctionMetadata | functionMetadata.ts:108 |
DECORATOR_TARGET_ATTRIBUTE_KEY | requestExecutor.ts:17 |
DECORATOR_METADATA_ATTRIBUTE_KEY | requestExecutor.ts:18 |
RequestExecutor | requestExecutor.ts:61 |
