@geektr/vue-fetch-event-source
v0.1.0
Published
useEventSource from @vueuse/core, powered by @microsoft/fetch-event-source (POST / custom headers / custom fetch)
Maintainers
Readme
@geektr/vue-fetch-event-source
Vue composable for Server-Sent Events with a useEventSource-compatible API, powered by @microsoft/fetch-event-source.
Enables POST requests, custom headers, and custom fetch implementations — features not available with the native EventSource API.
Install
pnpm add @geektr/vue-fetch-event-sourceUsage
import { useFetchEventSource } from '@geektr/vue-fetch-event-source'
const { data, event, status, error, close, open } = useFetchEventSource(
'/api/sse',
['chat', 'notification'], // named events to listen for
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: 'hello' }),
},
)With custom fetch
const { data, status } = useFetchEventSource('/api/sse', [], {
fetch: myCustomFetch,
credentials: 'include',
})Manual connect
const { data, open } = useFetchEventSource('/api/sse', [], {
immediate: false,
})
// Connect later
open()onEvent — streaming without lost events
The data ref holds the latest value. If a single SSE chunk contains multiple events, intermediate values are silently overwritten. For streaming consumers, use the onEvent callback — it fires synchronously for every event, in order:
const logs = ref<string[]>([])
useFetchEventSource('/api/logs', [], {
onEvent(msg) {
// msg = { event: string | null, data: T, id: string | null }
logs.value.push(msg.data)
},
})onEvent receives the same deserialized data as the data ref (serializer is not called twice). Named events are only delivered if they match the events whitelist, same as data. Errors thrown inside onEvent are caught and logged — they do not break the connection.
Dynamic request parameters
body and headers accept MaybeRefOrGetter, so they can be reactive refs or getters. Values are resolved with toValue() each time a connection (or reconnection) is established:
const token = ref('initial-token')
const { open } = useFetchEventSource('/api/sse', [], {
method: 'POST',
headers: () => ({ Authorization: `Bearer ${token.value}` }),
body: () => JSON.stringify({ timestamp: Date.now() }),
})You can also pass one-time overrides to open() — useful for form submissions with immediate: false:
const { open } = useFetchEventSource('/api/sse', [], {
method: 'POST',
immediate: false,
})
// Submit form
open({
body: JSON.stringify(formState),
headers: { 'X-Idempotency-Key': uuid() },
})Overrides persist across automatic reconnects until the next open() call.
Reconnect & non-idempotent requests
autoReconnect default depends on the HTTP method:
| Method | Default autoReconnect |
|---|---|
| GET, HEAD | true (infinite, 1s delay) |
| POST, PUT, PATCH, DELETE | false |
This prevents accidental duplicate side effects (e.g. re-triggering a certificate issuance). To opt in:
useFetchEventSource('/api/trigger', [], {
method: 'POST',
autoReconnect: { retries: 3, delay: 2000 },
})A dev-mode warning is emitted if autoReconnect: true is set on non-idempotent methods.
Error handling — SseResponseError
Non-2xx responses and wrong content-types throw SseResponseError, which is exported:
import { useFetchEventSource, SseResponseError } from '@geektr/vue-fetch-event-source'
const { error } = useFetchEventSource('/api/sse')
watchEffect(() => {
if (error.value instanceof SseResponseError) {
console.log(error.value.status) // 404
console.log(error.value.body) // parsed JSON or raw text
console.log(error.value.message) // "not found" (from body.error / body.message / "HTTP {status}")
}
})- 4xx errors (400–499) are never retried, even with
autoReconnectenabled — the throw terminates the connection immediately. - 5xx errors follow the normal
autoReconnectstrategy. - The default
onopenreads the response body as text, attemptingJSON.parsefor structured error payloads.
Differences from @vueuse/core useEventSource
| Feature | useEventSource | useFetchEventSource |
|---|---|---|
| Underlying API | EventSource | fetch |
| POST / custom headers | No | Yes |
| Custom fetch | No | Yes (fetch option) |
| Connection instance | eventSource: ShallowRef<EventSource> | controller: ShallowRef<AbortController> |
| Error type | ShallowRef<Event> | ShallowRef<unknown> (SseResponseError for HTTP errors) |
| autoReconnect default | false | true for GET, false for non-GET |
| openWhenHidden default | N/A | true (keeps connection alive when tab hidden) |
| withCredentials | Supported | Use credentials: 'include' (standard RequestInit) |
| Streaming events | data ref only (may lose events) | onEvent callback (no loss) + data ref |
| Reactive params | N/A | body/headers accept MaybeRefOrGetter |
Options
All options from UseEventSourceOptions (except autoReconnect and withCredentials) and FetchEventSourceInit (except signal, onmessage, onclose, onerror, body, headers) are merged into a flat object.
autoReconnect
// Default: true for GET/HEAD (infinite retries, 1s delay), false for other methods
autoReconnect?: boolean | {
retries?: number | (() => boolean) // default: -1 (infinite)
delay?: number // default: 1000
onFailed?: () => void
}body / headers
Accept MaybeRefOrGetter. Resolved with toValue() on each connection/reconnection.
onEvent
onEvent?: (msg: SseMessage<Data>) => void
interface SseMessage<Data = unknown> {
event: string | null // null for unnamed / "message" events
data: Data
id: string | null
}open(overrides?)
open: (overrides?: { body?: BodyInit | null; headers?: Record<string, string> }) => voidopenWhenHidden
Default: true. Set to false to abort connection when the page becomes hidden (like EventSource default behavior in some browsers).
Return
Same as useEventSource except:
controller(ShallowRef<AbortController | null>) replaceseventSourceerrorisShallowRef<unknown>instead ofShallowRef<Event>openaccepts optional{ body?, headers? }overrides
License
MIT
