export type AppEvent = { id?: string; type: string; invoiceId?: string; status?: string | null; updatedAt?: string | null; createdAt?: string | null; }; type SubscribeAppEventsOptions = { accessToken: string; onEvent: (event: AppEvent) => void; onError?: (error: unknown) => void; }; function parseSseMessage(message: string): AppEvent | null { const dataLines = message .split(/\r?\n/) .filter((line) => line.startsWith("data:")) .map((line) => line.slice("data:".length).trimStart()); if (!dataLines.length) { return null; } try { return JSON.parse(dataLines.join("\n")) as AppEvent; } catch { return null; } } export function subscribeAppEvents(options: SubscribeAppEventsOptions) { const controller = new AbortController(); void (async () => { try { const response = await fetch("/api/events", { method: "GET", credentials: "include", headers: { authorization: `Bearer ${options.accessToken}`, accept: "text/event-stream", }, signal: controller.signal, }); if (!response.ok || !response.body) { throw new Error(`events_stream_unavailable:${response.status}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (!controller.signal.aborted) { const { done, value } = await reader.read(); if (done) { break; } buffer += decoder.decode(value, { stream: true }); const chunks = buffer.split(/\r?\n\r?\n/); buffer = chunks.pop() ?? ""; for (const chunk of chunks) { const event = parseSseMessage(chunk); if (event) { options.onEvent(event); } } } } catch (error) { if (!controller.signal.aborted) { options.onError?.(error); } } })(); return () => controller.abort(); }