Spoosh
Transports

SSE Transport

Server-Sent Events for real-time streaming

Installation

npm install @spoosh/transport-sse

Setup

import { Spoosh } from "@spoosh/core";
import { create } from "@spoosh/react";
import { sse } from "@spoosh/transport-sse";

const spoosh = new Spoosh<ApiSchema, Error>("/api").withTransports([sse()]);

export const { useSSE } = create(spoosh);

Define SSE Endpoints

SSE endpoints have an events field defining the event types:

type ApiSchema = {
  notifications: {
    GET: {
      query: { userId: string };
      events: {
        message: { data: { id: string; text: string } };
        alert: { data: { priority: "low" | "high"; text: string } };
      };
    };
  };
};

message is the default SSE event type. If your server sends data without an event: field, it will be received as message.

Basic Usage

function Notifications() {
  const { data, isConnected } = useSSE(
    (api) => api("notifications").GET({ query: { userId: "123" } })
  );

  return (
    <div>
      <p>{isConnected ? "Listening..." : "Not connected"}</p>
      {data?.message && <p>{data.message.text}</p>}
      {data?.alert && <p>Alert: {data.alert.text}</p>}
    </div>
  );
}

Parse Strategies

StrategyWhat it doesExample input → output
"auto"Smart detection (default)"123" → 123, "true" → true, '{"a":1}' → {a:1}
"json-done"Parse JSON, ignore [DONE]'{"text":"hi"}' → {text:"hi"}, "[DONE]" → ignored
"json"Always JSON'{"a":1}' → {a:1}
"text"Keep as string"hello" → "hello"
"number"Convert to number"42" → 42
"boolean"Convert to boolean"true" → true

Usage

// One strategy for all events
useSSE((api) => api("stream").GET(), { parse: "json" });

// Different strategy per event
useSSE((api) => api("stream").GET(), {
  parse: {
    chunk: "text", // Keep chunks as strings
    metadata: "json", // Parse metadata as JSON
  },
});

// Custom function
useSSE((api) => api("stream").GET(), {
  parse: (data) => data.toUpperCase(),
});

Accumulate Strategies

StrategyWhat it doesExample
"replace"New replaces old (default)"a" then "b" → "b"
"merge"Smart merge based on type"a" then "b" → "ab"

Merge Behavior

The "merge" strategy automatically handles different types:

prevnextresult
stringstringconcat
numbernumberreplace
stringnumberreplace
numberstringreplace
objectobjectshallow merge
arrayarrayconcat
objectarrayreplace
arrayobjectreplace

Usage

// One strategy for all events
useSSE((api) => api("stream").GET(), { accumulate: "merge" });

// Different strategy per event
useSSE((api) => api("stream").GET(), {
  accumulate: {
    chunk: "merge", // Build up text/objects
    status: "replace", // Only keep latest status
  },
});

// Field-specific config (merge only specific fields)
useSSE((api) => api("chat").POST(), {
  accumulate: {
    chunk: { text: "merge" }, // Concat text field, replace others
  },
});

// Example: Field-specific accumulation in action
// Schema: events: { chunk: { data: { id: string; text: string; tokens: number } } }
//
// Event 1: { id: "1", text: "Hello", tokens: 5 }
// Event 2: { id: "2", text: " World", tokens: 6 }
//
// With { chunk: "merge" }:           { id: "2", text: " World", tokens: 6 }  (shallow merge)
// With { chunk: { text: "merge" } }: { id: "2", text: "Hello World", tokens: 6 }  (concat text only)

// Same result using custom function:
useSSE((api) => api("chat").POST(), {
  accumulate: {
    chunk: (prev, curr) => ({
      ...curr,
      text: (prev?.text || "") + curr.text,
    }),
  },
});

Transport Options

sse({
  disconnectDelay: 100,
  throttle: true,
  openWhenHidden: true,
});
OptionTypeDefaultDescription
disconnectDelaynumber100Delay before closing idle connections
throttleboolean | numberfalseBatch updates
openWhenHiddenbooleantrueKeep connection in background tabs

Connection Options

useSSE(
  (api) =>
    api("notifications").GET({
      headers: { Authorization: "Bearer token" },
      credentials: "include",
    }),
  {
    maxRetries: 5,
    retryDelay: 2000,
  }
);
OptionTypeDefaultDescription
headersHeadersInit-Request headers
credentialsRequestCredentials-Include cookies ("include", "same-origin")
maxRetriesnumber3How many times to retry on failure
retryDelaynumber1000Ms between retries

Features

  • Connection Pooling - Multiple components share one connection to the same URL
  • Automatic Reconnection - Retries on failure with configurable maxRetries and retryDelay
  • React Strict Mode - Handles double-mount gracefully with disconnectDelay
  • Background Tab Support - Keeps connection alive when tab is hidden (configurable)

On this page