import { env } from "cloudflare:workers"; import type { Doorbell } from "./job-doorbell-do"; type Scoped = { repo_id: string | null; owner_id: string | null }; function bell(key: string) { return env.JOB_DOORBELL.get(env.JOB_DOORBELL.idFromName(key)); } function scopeKeys(runner: Scoped): string[] { return [ ...(runner.repo_id ? [`repo:${runner.repo_id}`] : []), ...(runner.owner_id ? [`owner:${runner.owner_id}`] : []), ]; } /** After a job becomes claimable: wakes runners registered to its repo and to its owner. */ export async function ringJobDoorbells(repoId: string, ownerId: string): Promise { await Promise.all( [`repo:${repoId}`, `owner:${ownerId}`].map(async (key) => { try { await bell(key).ring(); } catch (error) { console.error("Job doorbell ring failed", key, error); } }), ); } export type RunnerBells = { ringing: boolean; silence(): Promise }; /** Whether this runner's scopes may have queued work; failures count as ringing so jobs are never stranded. */ export async function checkRunnerBells(runner: Scoped): Promise { const keys = scopeKeys(runner); let states: Array<{ key: string; state: Doorbell }>; try { states = await Promise.all(keys.map(async (key) => ({ key, state: await bell(key).check() }))); } catch (error) { console.error("Job doorbell check failed", error); return { ringing: true, silence: async () => {} }; } return { ringing: keys.length === 0 || states.some((entry) => entry.state.ringing), async silence() { await Promise.all( states.map(({ key, state }) => bell(key) .silence(state.version) .catch(() => undefined), ), ); }, }; }