feat(core-shared): MetricsProtocol + BindContext.metrics? field

This commit is contained in:
2026-05-11 12:02:06 +02:00
parent 0850711986
commit d5f0f51b75
3 changed files with 48 additions and 12 deletions

View File

@@ -5,6 +5,7 @@ import type {
EventBusProtocol,
RealtimeBroadcasterProtocol,
RealtimeRegistryProtocol,
MetricsProtocol,
} from "./bind-protocols";
/** Always-present fields. Feature binders rely on these unconditionally. */
@@ -15,21 +16,26 @@ type BindContextBase = {
/**
* Optional cross-cutting deps. Generics let the app aggregator narrow the
* shape to full interfaces (`IEventBus`, `IRealtimeBroadcaster`, etc.); feature
* binders see only the protocol surface, which is enough for the methods they
* call. When an optional core package is absent the corresponding generic
* defaults to its protocol type, and `ctx.bus` / `ctx.realtime` are undefined
* at runtime.
* shape to full interfaces (`IEventBus`, `IRealtimeBroadcaster`, `IMetrics`,
* etc.); feature binders see only the protocol surface, which is enough for
* the methods they call. When an optional core package is absent the
* corresponding generic defaults to its protocol type, and `ctx.bus` /
* `ctx.realtime` / `ctx.metrics` are undefined at runtime.
*
* The 4th generic `Metrics` defaults to `MetricsProtocol` so existing call
* sites that pass 3 explicit args remain backward-compatible.
*/
export type BindContext<
Bus extends EventBusProtocol = EventBusProtocol,
Realtime extends RealtimeBroadcasterProtocol = RealtimeBroadcasterProtocol,
RealtimeReg extends RealtimeRegistryProtocol = RealtimeRegistryProtocol,
Metrics extends MetricsProtocol = MetricsProtocol,
> = BindContextBase & {
bus?: Bus;
queue?: IJobQueue;
realtime?: Realtime;
realtimeRegistry?: RealtimeReg;
metrics?: Metrics;
};
/** Production binders also receive the resolved Payload config. */
@@ -37,6 +43,7 @@ export type BindProductionContext<
Bus extends EventBusProtocol = EventBusProtocol,
Realtime extends RealtimeBroadcasterProtocol = RealtimeBroadcasterProtocol,
RealtimeReg extends RealtimeRegistryProtocol = RealtimeRegistryProtocol,
> = BindContext<Bus, Realtime, RealtimeReg> & {
Metrics extends MetricsProtocol = MetricsProtocol,
> = BindContext<Bus, Realtime, RealtimeReg, Metrics> & {
config: SanitizedConfig;
};

View File

@@ -1,13 +1,13 @@
/**
* Minimal protocol surfaces used by feature binders to interact with optional
* cross-cutting infrastructure (event bus, realtime broadcaster, realtime
* handler registry). Lives in `core-shared` so `BindContext` can reference
* these unconditionally — features depend on `core-shared`, never on the
* optional packages directly.
* handler registry, metrics). Lives in `core-shared` so `BindContext` can
* reference these unconditionally — features depend on `core-shared`, never
* on the optional packages directly.
*
* The optional packages' full interfaces (`IEventBus`, `IRealtimeBroadcaster`,
* `IRealtimeHandlerRegistry`) `extends` these — typechecks fail if a refactor
* narrows the protocol surface in a way the full interface would lose.
* `IRealtimeHandlerRegistry`, `IMetrics`) `extends` these — typechecks fail if
* a refactor narrows the protocol surface in a way the full interface would lose.
*/
export type EventBusProtocol = {
@@ -31,3 +31,26 @@ export type RealtimeRegistryProtocol = {
registerChannel(descriptor: unknown): void;
listChannels(): unknown[];
};
/**
* Minimal metrics protocol surface. `IMetrics` in `core-shared/instrumentation`
* extends this — typechecks fail if `IMetrics` is narrowed below this surface.
* Feature binders that receive `ctx.metrics` see only this protocol type.
*/
export type MetricsProtocol = {
counter(
name: string,
value?: number,
attributes?: Record<string, string | number | boolean>,
): void;
histogram(
name: string,
value: number,
attributes?: Record<string, string | number | boolean>,
): void;
gauge(
name: string,
value: number,
attributes?: Record<string, string | number | boolean>,
): void;
};

View File

@@ -1,14 +1,20 @@
import type { MetricsProtocol } from "../di/bind-protocols";
export type MetricAttributeValue = string | number | boolean;
/**
* Vendor-neutral metrics signal interface. Mirrors the pattern of ITracer / ILogger.
* Three impls: NoopMetrics (noop), OtelMetrics (OTel API), RecordingMetrics (core-testing).
*
* Extends MetricsProtocol from `core-shared/di/bind-protocols` so the type
* system enforces structural compatibility — narrowing IMetrics below the
* protocol surface causes a typecheck error.
*
* gauge() limitation: uses UpDownCounter under the hood, which accumulates deltas.
* True "set to absolute value" semantics require ObservableGauge with a callback —
* deferred to a v2 interface when the first true-gauge use case lands.
*/
export interface IMetrics {
export interface IMetrics extends MetricsProtocol {
/** Monotonic counter. Use for event counts (signups, errors, requests). */
counter(
name: string,