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
JavaScript
// 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