/** * Triggers — declarative reactive glue. * * A trigger matches a message by `type` and named params, and emits messages * in response. It is declared as data, keyed by `role+type+id` like other * defs, and collision-checked the same way. `match` binds named params from * the payload (like a route's candidates); `emit` uses the command-id-as-key * convention. Multiple triggers can match the same message — both fire. */ import type { Message } from './message.js'; export interface Trigger { /** The message type this trigger matches. */ type: string; /** Optional identity, for runtime enable/disable and collision checks. */ id?: string; /** Named params that must equal the corresponding fields in `msg.data`. */ match?: Record; /** Messages to emit when the trigger matches. */ emit: Message[]; } /** A trigger matches when its `type` and every `match` param line up. */ export function triggerMatches(t: Trigger, msg: Message): boolean { if (t.type !== msg.type) return false; if (!t.match) return true; const data = msg.data as Record | undefined; if (!data) return false; return Object.entries(t.match).every(([k, v]) => data[k] === v); } /** * A registry of triggers, keyed by `type#id`. `register` collision-checks the * key; `enable`/`disable` toggle a trigger at runtime (an orchestrator's * "no more placements this turn" control). `match` returns every enabled * trigger that matches a message. */ export class TriggerRegistry { private triggers = new Map(); private enabled = new Set(); register(t: Trigger): void { const key = triggerKey(t); if (this.triggers.has(key)) { throw new Error(`Duplicate trigger: ${key}`); } this.triggers.set(key, t); this.enabled.add(key); } unregister(t: Trigger): void { const key = triggerKey(t); this.triggers.delete(key); this.enabled.delete(key); } enable(type: string, id?: string): void { this.enabled.add(triggerKey({ type, id })); } disable(type: string, id?: string): void { this.enabled.delete(triggerKey({ type, id })); } /** Every enabled trigger matching `msg`. */ match(msg: Message): Trigger[] { const out: Trigger[] = []; for (const [key, t] of this.triggers) { if (this.enabled.has(key) && triggerMatches(t, msg)) out.push(t); } return out; } } function triggerKey(t: Pick): string { return t.id ? `${t.type}#${t.id}` : t.type; }