| 42 | let cancelled = false; |
| 43 | |
| 44 | const open = () => { |
| 45 | if (cancelled) return; |
| 46 | source = new EventSource(url); |
| 47 | |
| 48 | source.onopen = () => { |
| 49 | retryIndex = 0; |
| 50 | setConnected(true); |
| 51 | }; |
| 52 | |
| 53 | source.onmessage = (msg) => { |
| 54 | if (!msg.data) return; |
| 55 | try { |
| 56 | const parsed = JSON.parse(msg.data) as StreamEvent; |
| 57 | handlerRef.current(parsed); |
| 58 | } catch { |
| 59 | // malformed payload — ignore |
| 60 | } |
| 61 | }; |
| 62 | |
| 63 | source.onerror = () => { |
| 64 | setConnected(false); |
| 65 | source?.close(); |
| 66 | source = null; |
| 67 | if (cancelled) return; |
| 68 | const wait = BACKOFFS_MS[Math.min(retryIndex, BACKOFFS_MS.length - 1)]; |
| 69 | retryIndex += 1; |
| 70 | reconnectTimer = window.setTimeout(open, wait); |
| 71 | }; |
| 72 | }; |
| 73 | |
| 74 | open(); |
| 75 | |