import { DurableObject } from "cloudflare:workers"; /** Even with no rings, a scope re-checks D1 this often, bounding any missed-ring delay. */ const RECHECK_MS = 30_000; export type Doorbell = { ringing: boolean; version: number }; /** * One per runner scope (`repo:` or `owner:`). Idle self-hosted runners long-poll every couple of * seconds; without this every poll scans queued jobs in D1. Queuing a job rings the scope's bell, and * pollers only query D1 while it rings. A poll that finds no candidate jobs silences it, unless a ring * arrived meanwhile (the version check). State is memory-only: a fresh or evicted object starts ringing, * so losing it only costs one extra D1 query. */ export class JobDoorbell extends DurableObject { private version = 1; private silencedAt: number | null = null; async ring(): Promise { this.version++; this.silencedAt = null; } async check(): Promise { const stale = this.silencedAt === null || Date.now() - this.silencedAt > RECHECK_MS; return { ringing: stale, version: this.version }; } async silence(version: number): Promise { if (version === this.version) this.silencedAt = Date.now(); } }