UNPKG

framework

Version:

The (AI) Framework: turnkey, zero-config AI orchestration that wraps a coding-agent CLI (Claude Code) as a black box and takes you from an idea to a running app. Vite for AI.

84 lines 3.69 kB
// The daemon-side half of notifications (#627): a background poll of a projection over the // registered projects that fires even when no dashboard is open — that is what a Discord // message buys over the browser notification. One engine, two callers: the "needs you" queue // (#627) and the "New activity" feed. They differ only in what they project and how an item // is identified, so those are parameters rather than a second copy of the poll. /** * Tracks which items have been announced, so only new ones notify. Identity is the caller's * (`keyOf`), since what makes two items "the same" is a property of what is being watched. * Testable without timers. * * The baseline is kept per project (`scopeOf`), not once for the whole poll (#1623). A poll that * reached three projects out of four knows what already existed on those three and knows nothing * about the fourth, and announcing is a per-project decision anyway. Held globally, one project * that can never be read — a registered repo with no remote is an ordinary case — would either * silence every project's notifications or hand the whole set a baseline it had not earned. */ export class SeenTracker { keyOf; scopeOf; seen = new Set(); warmedUp = new Set(); constructor(keyOf, scopeOf) { this.keyOf = keyOf; this.scopeOf = scopeOf; } /** * Fold a poll's items into the baseline and return the ones worth announcing: the items not seen * before, from projects this watcher has a real baseline for. `whole` names the projects the poll * read completely, and only those earn a baseline — so whatever already existed at start-up is * never announced, and a project that could not be read stays quiet until it can be. * * Items keep being folded in either way: a partial read can only under-report, never invent, so * anything it did see is still something the user should not later hear about as new. */ observe(items, whole) { const fresh = items.filter(item => this.warmedUp.has(this.scopeOf(item)) && !this.seen.has(this.keyOf(item))); for (const item of items) this.seen.add(this.keyOf(item)); for (const project of whole) this.warmedUp.add(project); return fresh; } } /** * Watch a projection and hand each poll's new items to `onNew`. A project's first *whole* read only * seeds that project's baseline. Forgiving — a failed project scan or projection just yields no new * items that cycle, and earns no baseline: the baseline must come from a real read, or a first poll * that could not reach GitHub would make the next good one announce everything pre-existing as new. * * Owns no timer (E4): the daemon's one clock calls {@link KeyedWatcher.poll}, so the cadence is * declared where every other background job's is. */ export function startKeyedWatcher(opts) { const tracker = new SeenTracker(opts.keyOf, opts.scopeOf); let stopped = false; let running = false; const poll = async () => { if (stopped || running) return; running = true; try { let read; try { read = await opts.build(await opts.projects()); } catch { return; } const fresh = tracker.observe(read.items, read.whole); if (fresh.length > 0 && !stopped) await opts.onNew(fresh); } finally { running = false; } }; return { stop: () => { stopped = true; }, poll, }; } //# sourceMappingURL=keyed-watcher.js.map