Spaces:
Runtime error
Runtime error
| <script lang="ts"> | |
| import { browser, dev } from "$app/environment"; | |
| import { invalidate } from "$app/navigation"; | |
| import { | |
| type BackgroundGeneration, | |
| backgroundGenerationEntries, | |
| removeBackgroundGeneration, | |
| } from "$lib/stores/backgroundGenerations"; | |
| import { handleResponse, useAPIClient } from "$lib/APIClient"; | |
| import { UrlDependency } from "$lib/types/UrlDependency"; | |
| import { MessageUpdateStatus, MessageUpdateType } from "$lib/types/MessageUpdate"; | |
| import type { Message } from "$lib/types/Message"; | |
| const POLL_INTERVAL_MS = 1000; | |
| const MAX_POLL_DURATION_MS = 3 * 60_000; | |
| const client = useAPIClient(); | |
| const pollers = new Map<string, () => void>(); | |
| const inflight = new Set<string>(); | |
| const assistantSnapshots = new Map<string, string>(); | |
| const failureCounts = new Map<string, number>(); | |
| $effect.root(() => { | |
| if (!browser) { | |
| pollers.clear(); | |
| return; | |
| } | |
| let destroyed = false; | |
| const log = (...args: unknown[]) => { | |
| if (dev) { | |
| console.log("background generation", ...args); | |
| } | |
| }; | |
| const stopPoller = (id: string, reason?: string) => { | |
| const stop = pollers.get(id); | |
| if (!stop) return; | |
| stop(); | |
| pollers.delete(id); | |
| inflight.delete(id); | |
| assistantSnapshots.delete(id); | |
| failureCounts.delete(id); | |
| log("stop", id, reason); | |
| }; | |
| const pollOnce = async (id: string) => { | |
| if (destroyed || inflight.has(id)) return; | |
| const entry = backgroundGenerationEntries.find((candidate) => candidate.id === id); | |
| if (entry && Date.now() - entry.startedAt > MAX_POLL_DURATION_MS) { | |
| removeBackgroundGeneration(id); | |
| stopPoller(id, "timed out"); | |
| log("timeout", id); | |
| await invalidate(UrlDependency.ConversationList); | |
| await invalidate(UrlDependency.Conversation); | |
| return; | |
| } | |
| inflight.add(id); | |
| log("poll", id); | |
| try { | |
| const response = await client.conversations({ id }).get(); | |
| const conversation = handleResponse(response); | |
| const messages = conversation?.messages ?? []; | |
| const lastAssistant = [...messages] | |
| .reverse() | |
| .find((message: Message) => message.from === "assistant"); | |
| const hasFinalAnswer = | |
| lastAssistant?.updates?.some((update) => update.type === MessageUpdateType.FinalAnswer) ?? | |
| false; | |
| const hasError = | |
| lastAssistant?.updates?.some( | |
| (update) => | |
| update.type === MessageUpdateType.Status && | |
| update.status === MessageUpdateStatus.Error | |
| ) ?? false; | |
| const snapshot = lastAssistant | |
| ? JSON.stringify({ | |
| id: lastAssistant.id, | |
| updatedAt: lastAssistant.updatedAt, | |
| contentLength: lastAssistant.content?.length ?? 0, | |
| updatesLength: lastAssistant.updates?.length ?? 0, | |
| }) | |
| : "__none__"; | |
| const previousSnapshot = assistantSnapshots.get(id); | |
| let shouldInvalidateConversation = false; | |
| if (lastAssistant) { | |
| assistantSnapshots.set(id, snapshot); | |
| if (snapshot !== previousSnapshot) { | |
| shouldInvalidateConversation = true; | |
| } | |
| } else if (assistantSnapshots.has(id)) { | |
| assistantSnapshots.delete(id); | |
| shouldInvalidateConversation = true; | |
| } | |
| if (lastAssistant && (hasFinalAnswer || hasError)) { | |
| removeBackgroundGeneration(id); | |
| assistantSnapshots.delete(id); | |
| failureCounts.delete(id); | |
| shouldInvalidateConversation = true; | |
| log("complete", id, hasFinalAnswer ? "final" : "error"); | |
| await invalidate(UrlDependency.ConversationList); | |
| } | |
| if (shouldInvalidateConversation) { | |
| await invalidate(UrlDependency.Conversation); | |
| } | |
| failureCounts.delete(id); | |
| } catch (err) { | |
| console.error("Background generation poll failed", id, err); | |
| const failures = (failureCounts.get(id) ?? 0) + 1; | |
| failureCounts.set(id, failures); | |
| if (failures >= 3) { | |
| removeBackgroundGeneration(id); | |
| assistantSnapshots.delete(id); | |
| failureCounts.delete(id); | |
| log("failures", id, failures); | |
| await invalidate(UrlDependency.ConversationList); | |
| } | |
| } finally { | |
| inflight.delete(id); | |
| } | |
| }; | |
| const startPoller = (entry: BackgroundGeneration) => { | |
| if (pollers.has(entry.id)) return; | |
| const intervalId = setInterval(() => { | |
| void pollOnce(entry.id); | |
| }, POLL_INTERVAL_MS); | |
| pollers.set(entry.id, () => clearInterval(intervalId)); | |
| void pollOnce(entry.id); | |
| log("start", entry.id); | |
| }; | |
| $effect(() => { | |
| const entries = backgroundGenerationEntries; | |
| if (destroyed) return; | |
| const activeIds = new Set(entries.map((entry) => entry.id)); | |
| for (const id of pollers.keys()) { | |
| if (!activeIds.has(id)) { | |
| stopPoller(id); | |
| } | |
| } | |
| for (const entry of entries) { | |
| startPoller(entry); | |
| } | |
| }); | |
| return () => { | |
| destroyed = true; | |
| for (const stop of pollers.values()) stop(); | |
| pollers.clear(); | |
| inflight.clear(); | |
| assistantSnapshots.clear(); | |
| failureCounts.clear(); | |
| }; | |
| }); | |
| </script> | |