(
url: string,
opts?: {
signal?: AbortSignal;
headers?: Record<string, string>;
maxReconnects?: number;
onProgress?: (progress: StreamProgress) => void;
}
)
| 171 | * Streams an async generator of StreamEvents with reconnection. |
| 172 | */ |
| 173 | export async function* fetchSSE( |
| 174 | url: string, |
| 175 | opts?: { |
| 176 | signal?: AbortSignal; |
| 177 | headers?: Record<string, string>; |
| 178 | maxReconnects?: number; |
| 179 | onProgress?: (progress: StreamProgress) => void; |
| 180 | } |
| 181 | ): AsyncGenerator<StreamEvent> { |
| 182 | const maxReconnects = opts?.maxReconnects ?? 5; |
| 183 | let attempt = 0; |
| 184 | |
| 185 | while (true) { |
| 186 | if (opts?.signal?.aborted) break; |
| 187 | |
| 188 | try { |
| 189 | const res = await fetch(url, { |
| 190 | headers: { |
| 191 | Accept: "text/event-stream", |
| 192 | "Cache-Control": "no-cache", |
| 193 | ...opts?.headers, |
| 194 | }, |
| 195 | signal: opts?.signal, |
| 196 | }); |
| 197 | |
| 198 | if (!res.ok) { |
| 199 | throw new ApiError(res.status, `SSE connection failed: ${res.status}`); |
| 200 | } |
| 201 | |
| 202 | // Reset attempt counter on successful connection |
| 203 | attempt = 0; |
| 204 | yield* parseStream(res, { signal: opts?.signal, onProgress: opts?.onProgress }); |
| 205 | |
| 206 | // Clean disconnect — don't reconnect |
| 207 | break; |
| 208 | } catch (err) { |
| 209 | if (err instanceof ApiError && err.type === "abort") break; |
| 210 | if (opts?.signal?.aborted) break; |
| 211 | if (attempt >= maxReconnects) throw err; |
| 212 | |
| 213 | const delay = Math.min(1_000 * Math.pow(2, attempt), 30_000); |
| 214 | attempt++; |
| 215 | await new Promise<void>((resolve, reject) => { |
| 216 | const timer = setTimeout(resolve, delay); |
| 217 | opts?.signal?.addEventListener( |
| 218 | "abort", |
| 219 | () => { |
| 220 | clearTimeout(timer); |
| 221 | reject(new ApiError(0, "Cancelled", "abort")); |
| 222 | }, |
| 223 | { once: true } |
| 224 | ); |
| 225 | }); |
| 226 | } |
| 227 | } |
| 228 | } |
nothing calls this directly
no test coverage detected