Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline (Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/* packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
104 lines
3.3 KiB
TypeScript
104 lines
3.3 KiB
TypeScript
import { COMPUTER_UPDATE_STAGES, type ComputerUpdate } from "@rakazo/contracts";
|
|
|
|
/** Shared polling and local presentation state; the server owns operation lifetime. */
|
|
export function createComputerUpdates(client: {
|
|
list: () => Promise<ComputerUpdate[]>;
|
|
start: (botId: string, action: "update" | "recover") => Promise<ComputerUpdate>;
|
|
dismiss: (id: string) => Promise<unknown>;
|
|
releaseInterrupted: (id: string) => Promise<unknown>;
|
|
}) {
|
|
let state: { updates: ComputerUpdate[]; openId: string | null } = { updates: [], openId: null };
|
|
const listeners = new Set<() => void>();
|
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
let generation = 0;
|
|
let revision = 0;
|
|
const emit = () => {
|
|
for (const listener of listeners) listener();
|
|
};
|
|
const poll = async (epoch: number) => {
|
|
const stamp = revision;
|
|
try {
|
|
const updates = await client.list();
|
|
if (epoch !== generation || stamp !== revision) return;
|
|
if (JSON.stringify(state.updates) === JSON.stringify(updates)) return;
|
|
state = {
|
|
updates,
|
|
openId: updates.some((item) => item.id === state.openId) ? state.openId : null,
|
|
};
|
|
emit();
|
|
} catch {
|
|
/* Keep the last known operation through transient connection failures. */
|
|
} finally {
|
|
if (epoch === generation) {
|
|
clearTimeout(timer);
|
|
timer = setTimeout(() => void poll(epoch), state.updates.length ? 1500 : 15000);
|
|
}
|
|
}
|
|
};
|
|
return {
|
|
getSnapshot: () => state,
|
|
subscribe(listener: () => void) {
|
|
listeners.add(listener);
|
|
return () => {
|
|
listeners.delete(listener);
|
|
};
|
|
},
|
|
watch() {
|
|
const epoch = ++generation;
|
|
void poll(epoch);
|
|
return () => {
|
|
generation++;
|
|
clearTimeout(timer);
|
|
state = { updates: [], openId: null };
|
|
};
|
|
},
|
|
open(id: string | null) {
|
|
state = { ...state, openId: id };
|
|
emit();
|
|
},
|
|
async start(botId: string, action: "update" | "recover" = "update") {
|
|
const epoch = generation;
|
|
const update = await client.start(botId, action);
|
|
if (epoch !== generation) return;
|
|
revision++;
|
|
state = {
|
|
updates: [update, ...state.updates.filter((item) => item.id !== update.id)],
|
|
openId: update.id,
|
|
};
|
|
emit();
|
|
clearTimeout(timer);
|
|
timer = setTimeout(() => void poll(epoch), 1500);
|
|
},
|
|
async releaseInterrupted(id: string) {
|
|
const epoch = generation;
|
|
await client.releaseInterrupted(id);
|
|
if (epoch !== generation) return;
|
|
revision++;
|
|
await poll(epoch);
|
|
},
|
|
async dismiss(id: string) {
|
|
const epoch = generation;
|
|
await client.dismiss(id);
|
|
if (epoch !== generation) return;
|
|
revision++;
|
|
state = {
|
|
updates: state.updates.filter((item) => item.id !== id),
|
|
openId: state.openId === id ? null : state.openId,
|
|
};
|
|
emit();
|
|
},
|
|
};
|
|
}
|
|
|
|
export function computerUpdateStages(
|
|
action: ComputerUpdate["action"],
|
|
): readonly ComputerUpdate["stage"][] {
|
|
return action === "recover"
|
|
? COMPUTER_UPDATE_STAGES.filter((stage) => stage !== "saving")
|
|
: COMPUTER_UPDATE_STAGES;
|
|
}
|
|
|
|
export function computerUpdateNeedsAttention(update: ComputerUpdate): boolean {
|
|
return update.status === "failed" || update.status === "interrupted";
|
|
}
|