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:<id>` or `owner:<id>`). 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<Env> {
private version = 1;
private silencedAt: number | null = null;
async ring(): Promise<void> {
this.version++;
this.silencedAt = null;
}
async check(): Promise<Doorbell> {
const stale = this.silencedAt === null || Date.now() - this.silencedAt > RECHECK_MS;
return { ringing: stale, version: this.version };
}
async silence(version: number): Promise<void> {
if (version === this.version) this.silencedAt = Date.now();
}
}