SSE Typed Event Stream Subscription

Subscribe to a typed server-sent event stream with EventSource listeners per event type, auto-reconnect awareness, and clean teardown.

TypescriptData Fetching

Code

snippet
type ProgressEvent = { jobId: string; percent: number };
type DoneEvent = { jobId: string; resultUrl: string };
type AlertEvent = { level: 'warn' | 'error'; text: string };

interface StreamHandlers {
  progress: (event: ProgressEvent) => void;
  done: (event: DoneEvent) => void;
  alert: (event: AlertEvent) => void;
}

function subscribeJobStream(jobId: string, handlers: StreamHandlers) {
  const source = new EventSource('/api/jobs/' + jobId + '/stream');

  const entries: [keyof StreamHandlers, (e: MessageEvent) => void][] = [
    ['progress', (e) => handlers.progress(JSON.parse(e.data) as ProgressEvent)],
    ['done', (e) => {
      handlers.done(JSON.parse(e.data) as DoneEvent);
      source.close();
    }],
    ['alert', (e) => handlers.alert(JSON.parse(e.data) as AlertEvent)],
  ];

  for (const [type, listener] of entries) {
    source.addEventListener(type, listener as EventListener);
  }

  source.onerror = () => {
    // EventSource 自动重连;done 后的断开无需处理
    if (source.readyState === EventSource.CLOSED) {
      console.log('stream closed by server');
    }
  };

  return () => {
    for (const [type, listener] of entries) {
      source.removeEventListener(type, listener as EventListener);
    }
    source.close();
  };
}

const teardown = subscribeJobStream('job-7', {
  progress: (e) => console.log('progress', e.percent),
  done: (e) => console.log('result at', e.resultUrl),
  alert: (e) => console.warn(e.level, e.text),
});
teardown();