/** * RevocationBus implementations over the FROZEN `relay:revocations` channel (INDEX §4.2 FIX 4). * P3 only PUBLISHES (P1 owns the subscriber that injects §4.1 CLOSE+RST / GOAWAY). Drain (T10) * and revoke (T13) use the SAME named channel. The KillSignal shape + channel name are imported * from relay-contracts and NEVER redefined. */ import { RELAY_REVOCATIONS_CHANNEL, KillSignalSchema, type KillSignal, type RevocationBus, } from 'relay-contracts' /** In-memory bus with a subscribe hook — used by tests and single-process wiring. */ export interface TestableRevocationBus extends RevocationBus { subscribe(handler: (signal: KillSignal) => void): () => void readonly channel: typeof RELAY_REVOCATIONS_CHANNEL } export function createInMemoryRevocationBus(): TestableRevocationBus { const handlers = new Set<(signal: KillSignal) => void>() return { channel: RELAY_REVOCATIONS_CHANNEL, async publish(signal) { // Validate at the boundary before it hits the wire (INV10: reason is metadata only). const parsed = KillSignalSchema.parse(signal) for (const h of handlers) h(parsed as KillSignal) }, subscribe(handler) { handlers.add(handler) return () => handlers.delete(handler) }, } } /** Minimal publisher surface of an ioredis client (integration seam; typed, not unit-tested). */ export interface RedisPublisher { publish(channel: string, message: string): Promise } /** Redis-backed publisher (production). P1 relay nodes SUBSCRIBE to the same channel. */ export function createRedisRevocationBus(redis: RedisPublisher): RevocationBus { return { async publish(signal) { const parsed = KillSignalSchema.parse(signal) await redis.publish(RELAY_REVOCATIONS_CHANNEL, JSON.stringify(parsed)) }, } }