/**
 * Aggregates session and schedule state across all online daemons.
 * Pure state machine — no I/O. The dashboard process feeds it events from
 * each daemon's SSE stream (via subscribeDaemon below) and from initial
 * hydration calls (via GET /api/sessions /api/schedules).
 */
export class Aggregator {
    sessions = new Map();
    schedules = new Map();
    listeners = new Set();
    applyEvent(larkAppId, ev) {
        switch (ev.type) {
            case 'session.spawned': {
                const r = ev.body.session;
                this.sessions.set(r.sessionId, { ...r, larkAppId });
                break;
            }
            case 'session.update': {
                const cur = this.sessions.get(ev.body.sessionId);
                if (cur)
                    this.sessions.set(ev.body.sessionId, { ...cur, ...ev.body.patch });
                break;
            }
            case 'session.exited': {
                const cur = this.sessions.get(ev.body.sessionId);
                if (cur)
                    this.sessions.set(ev.body.sessionId, { ...cur, status: 'closed' });
                break;
            }
            case 'schedule.created':
                this.schedules.set(ev.body.schedule.id, ev.body.schedule);
                break;
            case 'schedule.updated': {
                const cur = this.schedules.get(ev.body.id);
                if (cur)
                    this.schedules.set(ev.body.id, { ...cur, ...ev.body.patch });
                break;
            }
            case 'schedule.deleted':
                this.schedules.delete(ev.body.id);
                break;
            // schedule.fired and heartbeat are pass-through (no cache mutation)
        }
        for (const fn of this.listeners) {
            try {
                fn({ ...ev, larkAppId });
            }
            catch { /* swallow */ }
        }
    }
    /** Bulk-load on dashboard start before SSE catches up. Idempotent. */
    hydrateSessions(larkAppId, rows) {
        for (const r of rows)
            this.sessions.set(r.sessionId, { ...r, larkAppId });
    }
    hydrateSchedules(rows) {
        for (const r of rows)
            this.schedules.set(r.id, r);
    }
    getSessions() { return [...this.sessions.values()]; }
    getSchedules() { return [...this.schedules.values()]; }
    /** sessionId → owning daemon's larkAppId (used for write routing). */
    ownerOf(sessionId) {
        return this.sessions.get(sessionId)?.larkAppId;
    }
    scheduleOwnerOf(id) {
        return this.schedules.get(id)?.larkAppId;
    }
    on(fn) {
        this.listeners.add(fn);
        return () => this.listeners.delete(fn);
    }
}
/**
 * Subscribe to one daemon's SSE stream and feed events into the aggregator.
 * Auto-reconnects on error with 1s backoff. Returns an abort function.
 */
export function subscribeDaemon(d, agg, onError) {
    const ctrl = new AbortController();
    const url = `http://127.0.0.1:${d.ipcPort}/api/events`;
    (async () => {
        while (!ctrl.signal.aborted) {
            try {
                const res = await fetch(url, { signal: ctrl.signal });
                if (!res.ok || !res.body)
                    throw new Error(`bad status ${res.status}`);
                const reader = res.body.getReader();
                const dec = new TextDecoder();
                let buf = '';
                let evt = '';
                for (;;) {
                    const { value, done } = await reader.read();
                    if (done)
                        break;
                    buf += dec.decode(value, { stream: true });
                    let nl;
                    while ((nl = buf.indexOf('\n')) >= 0) {
                        const line = buf.slice(0, nl);
                        buf = buf.slice(nl + 1);
                        if (line.startsWith('event:'))
                            evt = line.slice(6).trim();
                        else if (line.startsWith('data:') && evt) {
                            const data = line.slice(5).trim();
                            try {
                                const body = JSON.parse(data);
                                agg.applyEvent(d.larkAppId, { type: evt, body });
                            }
                            catch {
                                // Skip malformed frame
                            }
                            evt = '';
                        }
                    }
                }
            }
            catch (e) {
                if (!ctrl.signal.aborted)
                    onError(e);
                await new Promise(r => setTimeout(r, 1_000));
            }
        }
    })();
    return () => ctrl.abort();
}
//# sourceMappingURL=aggregator.js.map