pluralchat / src /lib /components /BackgroundGenerationPoller.svelte
victor's picture
victor HF Staff
fix: enhance logging and timeout handling in BackgroundGenerationPoller component
808bcfb
<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>