fix(web): tolerant polling, objectURL cleanup, throttled pointermove
- home/detail video pollers no longer clearInterval on a single transient error (give up only after 10 consecutive failures), matching the agent page - agent page creates preview objectURLs inside useEffect so each has a matching revoke under strict-mode double-invocation - login pointermove throttled via rAF and skipped on coarse pointers - source-analysis.html: changelog entry for this hardening pass Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import {
|
||||
ArrowDownToLine,
|
||||
CheckCircle2,
|
||||
@@ -73,8 +73,14 @@ export default function AgentPage() {
|
||||
const [error, setError] = useState("")
|
||||
const terminalRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const previews = useMemo(() => files.map((file) => ({ file, url: URL.createObjectURL(file) })), [files])
|
||||
useEffect(() => () => previews.forEach((item) => URL.revokeObjectURL(item.url)), [previews])
|
||||
// create object URLs inside the effect (not during render) so every URL has a
|
||||
// matching revoke even under React strict-mode double-invocation
|
||||
const [previews, setPreviews] = useState<{ file: File; url: string }[]>([])
|
||||
useEffect(() => {
|
||||
const next = files.map((file) => ({ file, url: URL.createObjectURL(file) }))
|
||||
setPreviews(next)
|
||||
return () => next.forEach((item) => URL.revokeObjectURL(item.url))
|
||||
}, [files])
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_BASE}/agent-runs?limit=8`, { cache: "no-store" })
|
||||
|
||||
@@ -116,11 +116,16 @@ export default function DetailPage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!job || !runningVideo) return
|
||||
let failures = 0
|
||||
const timer = window.setInterval(async () => {
|
||||
try {
|
||||
setJob(await getJob(job.id))
|
||||
failures = 0
|
||||
} catch {
|
||||
window.clearInterval(timer)
|
||||
// one transient 5xx / network blip must not freeze progress forever;
|
||||
// only give up after sustained failures
|
||||
failures += 1
|
||||
if (failures >= 10) window.clearInterval(timer)
|
||||
}
|
||||
}, 2600)
|
||||
return () => window.clearInterval(timer)
|
||||
|
||||
@@ -61,15 +61,26 @@ export default function LoginPage() {
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
// skip touch / coarse pointers — the eye-follow effect is pointless there and
|
||||
// would thrash state (and battery) on scroll-driven pointer events
|
||||
if (typeof window !== "undefined" && window.matchMedia?.("(pointer: coarse)").matches) return
|
||||
let frame = 0
|
||||
const onPointerMove = (event: PointerEvent) => {
|
||||
const centerX = window.innerWidth / 2
|
||||
const centerY = window.innerHeight / 2
|
||||
const nextX = Math.max(-1, Math.min(1, (event.clientX - centerX) / centerX))
|
||||
const nextY = Math.max(-1, Math.min(1, (event.clientY - centerY) / centerY))
|
||||
setEyeOffset({ x: nextX * 8, y: nextY * 5.5 })
|
||||
if (frame) return // coalesce to at most one state update per animation frame
|
||||
frame = window.requestAnimationFrame(() => {
|
||||
frame = 0
|
||||
const centerX = window.innerWidth / 2
|
||||
const centerY = window.innerHeight / 2
|
||||
const nextX = Math.max(-1, Math.min(1, (event.clientX - centerX) / centerX))
|
||||
const nextY = Math.max(-1, Math.min(1, (event.clientY - centerY) / centerY))
|
||||
setEyeOffset({ x: nextX * 8, y: nextY * 5.5 })
|
||||
})
|
||||
}
|
||||
window.addEventListener("pointermove", onPointerMove)
|
||||
return () => window.removeEventListener("pointermove", onPointerMove)
|
||||
return () => {
|
||||
window.removeEventListener("pointermove", onPointerMove)
|
||||
if (frame) window.cancelAnimationFrame(frame)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const disabled = status === "loading" || status === "success"
|
||||
|
||||
@@ -191,11 +191,16 @@ export default function Home() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!job || !runningVideo) return
|
||||
let failures = 0
|
||||
const timer = window.setInterval(async () => {
|
||||
try {
|
||||
setJob(await getJob(job.id))
|
||||
failures = 0
|
||||
} catch {
|
||||
window.clearInterval(timer)
|
||||
// one transient 5xx / network blip must not freeze progress forever;
|
||||
// only give up after sustained failures
|
||||
failures += 1
|
||||
if (failures >= 10) window.clearInterval(timer)
|
||||
}
|
||||
}, 2600)
|
||||
return () => window.clearInterval(timer)
|
||||
|
||||
Reference in New Issue
Block a user