diff --git a/packages/core-shared/src/di/bind-context.ts b/packages/core-shared/src/di/bind-context.ts index 01958ef..bb4aa96 100644 --- a/packages/core-shared/src/di/bind-context.ts +++ b/packages/core-shared/src/di/bind-context.ts @@ -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 & { + Metrics extends MetricsProtocol = MetricsProtocol, +> = BindContext & { config: SanitizedConfig; }; diff --git a/packages/core-shared/src/di/bind-protocols.ts b/packages/core-shared/src/di/bind-protocols.ts index 8d37379..50fca96 100644 --- a/packages/core-shared/src/di/bind-protocols.ts +++ b/packages/core-shared/src/di/bind-protocols.ts @@ -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, + ): void; + histogram( + name: string, + value: number, + attributes?: Record, + ): void; + gauge( + name: string, + value: number, + attributes?: Record, + ): void; +}; diff --git a/packages/core-shared/src/instrumentation/metrics.interface.ts b/packages/core-shared/src/instrumentation/metrics.interface.ts index c21c250..d48caa9 100644 --- a/packages/core-shared/src/instrumentation/metrics.interface.ts +++ b/packages/core-shared/src/instrumentation/metrics.interface.ts @@ -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,