/** * Local-bus event emitters for build-bus delivery outcomes. * * Phase 6 of [[openspec/changes/build-bus-poll-cd/proposal.md]] wants `celilo subscribers status` * to show last-delivery state + recent failures per subscriber. The * data source for that view is the local SQLite event bus — every * publisher-side fan-out call emits one `webhook.delivered` or * `webhook.failed` event here, and the status command queries them * back. * * Two event types instead of one (`webhook.delivery` with ok bool): * - `webhook.failed` filters cleanly for "show me what's broken" * - `webhook.delivered` filters cleanly for "show me success rate" * Either query is one indexed lookup against the events.type index * on the bus. */ import { defineEvents, openBus } from '@celilo/event-bus'; import type { DeliveryResult, PublishEvent } from '@celilo/event-bus/build-bus'; import { getEventBusPath } from '../../config/paths'; const NO_SCHEMAS = defineEvents({}); export const WEBHOOK_DELIVERED_EVENT = 'webhook.delivered'; export const WEBHOOK_FAILED_EVENT = 'webhook.failed'; /** * Payload shape for `webhook.delivered` / `webhook.failed` events. * Stored verbatim on the local bus; the status command queries with * `recentEvents({ type, limit })` and aggregates. */ export interface WebhookDeliveryPayload { /** Subscriber display label (or URL if no name was set). */ subscriberLabel: string; /** Subscriber webhook URL. Used as the aggregation key. */ subscriberUrl: string; /** UUID of the event we attempted to deliver. */ eventId: string; /** Package this event was about — convenience for status output. */ packageName: string; packageVersion: string; /** Dist-tag (e.g. 'latest', 'alpha'). */ tag: string; /** Attempts spent (1 + retries). */ attempts: number; /** Total ms spent including retries. */ durationMs: number; /** HTTP status, if a response came back. Absent on network errors. */ status?: number; /** Error message; only present on failed deliveries. */ error?: string; } function buildPayload(event: PublishEvent, result: DeliveryResult): WebhookDeliveryPayload { return { subscriberLabel: result.subscriber.name ?? result.subscriber.url, subscriberUrl: result.subscriber.url, eventId: event.eventId, packageName: event.package.name, packageVersion: event.package.version, tag: event.tag, attempts: result.attempts, durationMs: result.durationMs, status: result.status, error: result.error, }; } /** * Best-effort emit. Failures here (DB locked, disk full) get logged * to stderr but never propagate — the actual webhook ALREADY * succeeded (or failed) on the wire; recording the outcome is * housekeeping. */ function emitBest(type: string, payload: WebhookDeliveryPayload): void { let bus: ReturnType | undefined; try { bus = openBus({ dbPath: getEventBusPath(), events: NO_SCHEMAS }); bus.emitRaw(type, payload); } catch (err) { const msg = err instanceof Error ? err.message : String(err); console.warn(`[build-bus] failed to record ${type}: ${msg}`); } finally { bus?.close(); } } /** Record one delivery outcome on the local bus. */ export function recordDeliveryOutcome(event: PublishEvent, result: DeliveryResult): void { const type = result.ok ? WEBHOOK_DELIVERED_EVENT : WEBHOOK_FAILED_EVENT; emitBest(type, buildPayload(event, result)); }