WebSocket and SSE event streams#
Litestar Vite includes browser helpers for JSON event streams. They own connection lifecycle, reconnect backoff, heartbeat filtering, duplicate suppression, and sequence-gap warnings. Your application still owns authorization and rendering.
Choose the narrowest helper for the server route:
createEventStream()for an application-defined WebSocket or SSE route.createChannelsStream()for WebSocket routes generated by Litestar’sChannelsPlugin.createQueueEventStream()for routes generated by Litestar Queues, or a custom route that intentionally uses the same queue event envelope.
Generic streams#
createEventStream() is neutral: it parses JSON when possible but does not
assume queue event names, heartbeat shapes, identifiers, or sequence fields.
import { createEventStream } from "litestar-vite-plugin/helpers"
const stream = createEventStream({
url: "/events",
onEvent: (event) => console.log(event),
onGap: (gap) => console.warn("Missing events", gap),
})
stream.connect()
// Later:
stream.dispose()
Use buildUrl instead of url when a signed endpoint or token can change.
It runs immediately before every connection attempt:
const stream = createEventStream({
buildUrl: () => `/events?ticket=${encodeURIComponent(currentTicket)}`,
onEvent: handleEvent,
})
Regular Litestar Channels#
When ChannelsPlugin generates /<channel> WebSocket routes, let
createChannelsStream() encode the channel and build the route:
import { createChannelsStream } from "litestar-vite-plugin/helpers"
const stream = createChannelsStream({
basePath: "/channels",
channel: "notifications",
onEvent: showNotification,
})
stream.connect()
channel may be a function and transformUrl may add a current signed
token before each reconnect.
Litestar Queues#
createQueueEventStream() understands QueuePlugin’s generated task, queue,
worker, global, and custom routes. It also owns the queue event names and the
standard heartbeat, deduplication, and sequence selectors:
import { createQueueEventStream } from "litestar-vite-plugin/helpers"
const stream = createQueueEventStream({
scope: "task",
taskId: () => selectedTaskId,
transport: "sse",
onEvent: updateTask,
onGap: warnAboutMissingEvents,
})
stream.connect()
A custom endpoint can opt into the same queue envelope explicitly by supplying
url or buildUrl instead of a generated-route target.
Declarative stream element#
Call defineStreamElement() once, then bind the connection to ordinary DOM
lifecycle:
import { defineStreamElement } from "litestar-vite-plugin/helpers"
defineStreamElement()
<litestar-stream url="/events" transport="sse" swap="json">
<template ls-for="item in $data.items">
<p>${item}</p>
</template>
</litestar-stream>
The element uses light DOM and dispatches bubbling
litestar:stream-event, litestar:stream-health,
litestar:stream-gap, litestar:stream-open, and
litestar:stream-close events. Adding preset="queues" delegates to
createQueueEventStream(); generic behavior remains the default.
The element replaces htmx-ext-ws and htmx-ext-sse for JSON streams.
Do not layer both extensions on the same endpoint: two reconnect policies
would compete for one connection.
React, Vue, and Svelte#
Framework adapters are optional subpaths. All return the same state fields:
healthy, lastEvent, lastGap, and a bounded events buffer.
import { useQueueEventStream } from "litestar-vite-plugin/react"
const stream = useQueueEventStream({
key: taskId,
scope: "task",
taskId,
onEvent: handleTaskEvent,
})
import { useEventStream } from "litestar-vite-plugin/vue"
const stream = useEventStream({
key: () => projectId.value,
url: "/events",
onEvent: handleEvent,
})
import { createEventStreamStore } from "litestar-vite-plugin/svelte"
const stream = createEventStreamStore({
url: "/events",
onEvent: handleEvent,
})
React and Vue reconnect only when their explicit key or transport changes,
not when an inline callback gets a new identity. A Svelte store connects for
its first subscriber and disposes after its last subscriber leaves.
Authentication#
Authentication stays on the Litestar route:
Same-origin session cookies and JWT cookies are sent automatically by native
WebSocketandEventSourceclients. Validate them with Litestar auth middleware and guards.For a rotating signed URL, use
buildUrlortransformUrl. Keep the credential in a property or closure instead of serializing it into markup.Native browser WebSocket and EventSource APIs cannot attach an arbitrary
Authorizationheader. Prefer an HttpOnly cookie. If bearer authentication must remain, exchange it over authenticated HTTP for a short-lived, audience-restricted stream ticket instead of putting a general access token in the query string.Cross-origin SSE credentials require a credentialed EventSource client plus matching CORS policy. The default Litestar Vite design keeps streams same-origin through the Litestar port.
For WebSockets, reserve a close code such as 4001 for expired
authentication and a different code such as 4003 for a real authorization
denial. shouldReconnect(code) can trigger application-owned refresh logic
and suppress retries for terminal denials.
Transport choice#
SSE is unidirectional and has native reconnect behavior, but named server
events must be enumerated because EventSource.onmessage receives only
unnamed events. WebSocket is bidirectional and needs no event-name list, but
browser clients cannot add authorization headers during the handshake.
Replay and gaps#
The helper warns about a sequence gap; it never guesses at missing data or silently refetches it. Silent recovery across reconnects requires storage on both sides of a QueuePlugin stream:
configure Litestar Queues with
replay_limit > 0(the default is0);configure the Channels backend with
history=(for example,MemoryChannelsBackend(history=200)).
Without both settings, an outage can produce a gap that the browser correctly reports but cannot recover. See the stream example for ordinary Channels plus queue-shaped WebSocket and SSE routes.