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.
104 lines • 4.14 kB
JavaScript
/**
* A replayable, multi-consumer stream of events: buffer every event, hand out live async
* iterators, and replay history from an offset (tail=N replay).
*
* Absorbed from `@gemstack/ai-autopilot` when that package was deleted (A2). Its element type
* used to default to the supervisor's event; the only caller here passes `FrameworkEvent`, so
* the parameter is now required.
*/
export class EventStream {
buffer = [];
waiters = [];
closed = false;
/** Append an event. Wire this in as an `onEvent` sink. Ignored once closed. */
push = (event) => {
if (this.closed)
return;
this.buffer.push(event);
for (const wake of this.waiters.splice(0))
wake();
};
/** Alias for {@link push}, reads well at the `onEvent:` call site. */
get sink() {
return this.push;
}
/** Events buffered so far, from `fromOffset` (default 0) — Flue-style tail replay. */
history(fromOffset = 0) {
return this.buffer.slice(fromOffset);
}
/** Number of events buffered. */
get length() {
return this.buffer.length;
}
/** True once {@link close} has run. */
get isClosed() {
return this.closed;
}
/** End the stream: live iterators drain their backlog, then finish. Idempotent. */
close() {
if (this.closed)
return;
this.closed = true;
for (const wake of this.waiters.splice(0))
wake();
}
/**
* A fresh async iterator that replays every buffered event, then yields new
* ones as they arrive, and finishes once the stream is closed and drained.
* Independent iterators each keep their own cursor, so late consumers still
* see the full history.
*/
[Symbol.asyncIterator]() {
let index = 0;
let done = false;
let pending;
const stream = this;
return {
[Symbol.asyncIterator]() {
return this;
},
next() {
if (done)
return Promise.resolve({ value: undefined, done: true });
if (index < stream.buffer.length) {
return Promise.resolve({ value: stream.buffer[index++], done: false });
}
if (stream.closed)
return Promise.resolve({ value: undefined, done: true });
return new Promise(resolve => {
const wake = () => {
pending = undefined;
if (done)
return resolve({ value: undefined, done: true });
if (index < stream.buffer.length)
return resolve({ value: stream.buffer[index++], done: false });
if (stream.closed)
return resolve({ value: undefined, done: true });
// Woken with nothing left for us (a concurrent next() on this iterator took the
// event): the stream is still open, so wait again rather than resolve a false done.
pending = wake;
stream.waiters.push(wake);
};
pending = wake;
stream.waiters.push(wake);
});
},
// Cancel this iterator (e.g. an SSE client disconnected): drop its waiter so
// it does not linger in `waiters` until the next push, and settle any pending
// `next()`. Without this, a disconnected consumer leaks until the stream ends.
return() {
done = true;
if (pending) {
const i = stream.waiters.indexOf(pending);
if (i >= 0)
stream.waiters.splice(i, 1);
const wake = pending;
pending = undefined;
wake();
}
return Promise.resolve({ value: undefined, done: true });
},
};
}
}
//# sourceMappingURL=event-stream.js.map