/** * Which provider a consumer actually resolved to (celilo#1072). * * `capabilities` is provider-side only, so celilo could answer "who could * provide this" and never "who does this module actually use". Callers that * needed the second question rebuilt the same approximation — the consumer's * `requires` + `optional` crossed with `capabilities` — which names every * provider a module MIGHT have bound to. `tango-nexus` declares four optional * capabilities and is deployed against one host, so that approximation carries * three edges that do not exist and nothing distinguishes them. * * The binding is the CALL, not the resolution. `loadCapabilityFunctions` * injects every capability that has a registered provider, deliberately, and * never reads the consumer's manifest — so what it resolves is a superset of * even the permissive set. A consumer's hook invoking a method is the one event * that separates the optional it uses from the three it does not. * * This runs BESIDE the permissive set, it does not replace it. * `planConsumerCleanup` must still notify every provider that might hold minted * state, including providers this table has no row for. */ import { desc, eq, sql } from 'drizzle-orm'; import type { DbClient } from '../db/client'; import { capabilityBindings } from '../db/schema'; export interface CapabilityBinding { capabilityName: string; providerModuleId: string; /** Last time the consumer called into this provider. */ boundAt: Date; } /** * Record that `consumer` called into `provider` for `capability`. * * Upsert on (consumer, capability): a redeploy re-asserts the row rather than * duplicating it, and a provider swap rewrites the provider in place. `boundAt` * carries last-seen semantics — it moves on every invocation, so a binding last * exercised forty deploys ago is a queryable fact rather than a silent lie. */ export function recordCapabilityBinding( db: DbClient, consumerModuleId: string, capabilityName: string, providerModuleId: string, ): void { db.insert(capabilityBindings) .values({ consumerModuleId, capabilityName, providerModuleId }) .onConflictDoUpdate({ target: [capabilityBindings.consumerModuleId, capabilityBindings.capabilityName], set: { providerModuleId, boundAt: sql`(unixepoch())` }, }) .run(); } /** Every provider this consumer has actually called into, most recent first. */ export function listCapabilityBindings( db: DbClient, consumerModuleId: string, ): CapabilityBinding[] { return db .select() .from(capabilityBindings) .where(eq(capabilityBindings.consumerModuleId, consumerModuleId)) .orderBy(desc(capabilityBindings.boundAt)) .all() .map((row) => ({ capabilityName: row.capabilityName, providerModuleId: row.providerModuleId, boundAt: row.boundAt, })); } /** * Wrap a capability interface so the first method call reports the binding. * * Modelled on `wrapWithLogging`: a new object over the same prototype, every * function replaced, everything else passed through. Unlike that wrapper this * one does NOT make the method async — it records and then returns the original * call untouched, so a synchronous capability method stays synchronous. * * `onUse` fires once per wrapped interface. A hook that calls a capability * twenty times writes one row. */ export function withBindingRecord(iface: T, onUse: () => void): T { let reported = false; const wrapped = Object.create(Object.getPrototypeOf(iface)); for (const key of Reflect.ownKeys(iface)) { const value = (iface as Record)[key]; if (typeof value !== 'function') { wrapped[key] = value; continue; } const original = value as (...args: unknown[]) => unknown; wrapped[key] = function (this: unknown, ...args: unknown[]): unknown { if (!reported) { reported = true; onUse(); } return original.apply(this, args); }; } return wrapped as T; }