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

View File

@@ -1,13 +1,13 @@
/** /**
* Minimal protocol surfaces used by feature binders to interact with optional * Minimal protocol surfaces used by feature binders to interact with optional
* cross-cutting infrastructure (event bus, realtime broadcaster, realtime * cross-cutting infrastructure (event bus, realtime broadcaster, realtime
* handler registry). Lives in `core-shared` so `BindContext` can reference * handler registry, metrics). Lives in `core-shared` so `BindContext` can
* these unconditionally — features depend on `core-shared`, never on the * reference these unconditionally — features depend on `core-shared`, never
* optional packages directly. * on the optional packages directly.
* *
* The optional packages' full interfaces (`IEventBus`, `IRealtimeBroadcaster`, * The optional packages' full interfaces (`IEventBus`, `IRealtimeBroadcaster`,
* `IRealtimeHandlerRegistry`) `extends` these — typechecks fail if a refactor * `IRealtimeHandlerRegistry`, `IMetrics`) `extends` these — typechecks fail if
* narrows the protocol surface in a way the full interface would lose. * a refactor narrows the protocol surface in a way the full interface would lose.
*/ */
export type EventBusProtocol = { export type EventBusProtocol = {
@@ -31,3 +31,26 @@ export type RealtimeRegistryProtocol = {
registerChannel(descriptor: unknown): void; registerChannel(descriptor: unknown): void;
listChannels(): unknown[]; 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; export type MetricAttributeValue = string | number | boolean;
/** /**
* Vendor-neutral metrics signal interface. Mirrors the pattern of ITracer / ILogger. * Vendor-neutral metrics signal interface. Mirrors the pattern of ITracer / ILogger.
* Three impls: NoopMetrics (noop), OtelMetrics (OTel API), RecordingMetrics (core-testing). * 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. * gauge() limitation: uses UpDownCounter under the hood, which accumulates deltas.
* True "set to absolute value" semantics require ObservableGauge with a callback — * True "set to absolute value" semantics require ObservableGauge with a callback —
* deferred to a v2 interface when the first true-gauge use case lands. * 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). */ /** Monotonic counter. Use for event counts (signups, errors, requests). */
counter( counter(
name: string, name: string,