defineEventStream, request options, and the stream handle.
SSE
Declare an event stream, call the operation with a client, iterate events, then close.
defineEventStream()
function defineEventStream<
const P extends string,
const M extends string = "GET",
TInput extends AnyStruct | undefined = undefined,
TEvents extends EventStructs = EventStructs,
TError extends AnyStruct | undefined = undefined,
>(
definition: Omit<EventStreamDefinitionBase<TEvents>, "method" | "path"> &
EventStreamErrorField<TError> & {
input?: TInput;
method?: M;
path: P;
} & BuilderField<
NoInfer<TInput>,
EventStreamRequestShape<NoInfer<P>, NoInfer<M>>
>,
): EventStreamEndpoint<EndpointArgs<TInput>, EventStreamData<TEvents>, TError>;- definition —
path,eventsmap, and the required positive-integer limitsmaxBufferSizeandmaxQueueSize; optionalmethod(default'GET'),operation,inputanderror. A declaredinputalways requires an explicitbuilder.inputonly defines the parameters; the builder alone decides where each value goes. Only definitions withoutinputmay omitbuilder. Thesedefinitionmembers are documented underEventStreamDefinition.erroris required only when an error struct type is given, andbuilderis required exactly wheninputis declared. - Returns an operation. The first argument is a
Client. Further arguments are the input only. Calling it starts the stream.
import { defineEventStream, struct } from "@defjs/core";
const ticks = defineEventStream({
maxBufferSize: 64 * 1024,
maxQueueSize: 100,
path: "/ticks",
events: { message: struct.object({ text: struct.string() }) },
});The call returns [null, stream, open] or [error, undefined, open?]. When error is declared, the fault is Fault of that struct; otherwise it is Fault<undefined>. Pass signal by deriving a client from RequestOptions. SSE reads only signal and ignores progress and WebSocket fields.
EventStreamExecuteOptions
type EventStreamExecuteOptions = {
signal?: AbortSignal;
};EventStreamExecuteOptions is { signal? }. There is no timeout or abort field and no progress callback. signal covers the whole stream, including after it opens. A deadline is AbortSignal.timeout(ms) and classifies as NET_TIMEOUT. A manual abort is NET_ABORTED. There is no startup-only deadline and no client-level default deadline. An application that wants a startup-only deadline creates its own AbortController, aborts it with new DOMException("...", "TimeoutError") from a timer, and clears the timer after the await returns.
EventStreamHandle
interface EventStreamHandle<TEvent = EventStreamMessage>
extends AsyncIterable<TEvent>, AsyncDisposable {
readonly open: EventStreamOpenInfo;
readonly closed: Promise<EventStreamCloseInfo>;
close(reason?: unknown): void;
[Symbol.asyncDispose](): PromiseLike<void>;
}The tuple's third slot is the startup snapshot of open; stream.open follows the latest connection after a reconnect. That third slot is the returned connection's first successful handshake snapshot only for a native Defjs handle or a wrapper forwarding the same live open object; custom handles and wrappers overriding open retain their current public open even after reconnect, so shared closed cannot bypass URL sanitization or header filtering, and Defjs does not reconstruct first-handshake metadata from arbitrary transformations. Prefer await using stream = handle for owned scope cleanup. The disposer stops Defjs reading and reconnect work and waits for its reader lock to be released; it does not promise that a provider-controlled, stuck ReadableStream.cancel() promise will finish. Manual close() / closed remains available.
async function consume(handle: EventStreamHandle<unknown>): Promise<void> {
await using stream = handle;
for await (const event of stream) console.log(event);
}Structural implementations of EventStreamHandle must now implement [Symbol.asyncDispose]() and return the same owned teardown path. This is a compile-time breaking change for custom handles; code that only receives handles returned by Defjs has no new runtime call requirement.
EventStreamOpenInfo
interface EventStreamOpenInfo {
response: HttpResponse<unknown>;
url: string;
}EventStreamCloseInfo
type EventStreamCloseInfo =
| (EventStreamCloseInfoBase & { kind: "eof" })
| (EventStreamCloseInfoBase & { kind: "aborted" })
| (EventStreamCloseInfoBase & { code: EventStreamFaultCode; kind: "error" });kind is eof, aborted, or error. Every variant may carry reason and cause. Only error has code, one of EventStreamFaultCode: 'CAP_BUFFER_EXCEEDED' | 'CAP_QUEUE_OVERFLOW' | 'EXT_HOOK_FAILED' | 'EXT_OBSERVER_FAILED' | 'NET_TIMEOUT' | 'NET_UNREACHABLE' | 'RES_DECODE_FAILED' | 'RES_MEDIA_TYPE_INVALID'.
EventStructs
type EventStructs = { [key: string]: AnyStruct };The key is the SSE event name; a message without an event: field is named message. When no key matches, the default struct is used. A string event struct keeps the raw data text. Every other kind is JSON text: JSON.parse, then the struct. A bare struct.literal therefore needs JSON text, not an unquoted token. Handshake error bodies use the same kind rule as HTTP: string is text, blob / file are blobs, arrayBuffer is bytes, and everything else is JSON.
Reconnect lives on the client (withSSEReconnect). SSE jitter is a 0–1 multiplicative factor, same as WebSocket.
The retry budget and backoff exponent reset after a successful handshake; they apply per outage. Invalid numeric options return REQ_OPTIONS_INVALID before opening a stream, and maxDelayMs caps the final delay after jitter. See withSSEReconnect for validation rules, defaults, and UTF-8 Last-Event-ID handling.
The parser follows WHATWG SSE line and field rules and removes only one leading UTF-8 BOM at the start of each physical stream. U+FEFF in data or ID values is preserved; a later BOM-prefixed field name is ignored as unknown. Multi-line data joins with \n, a blank line dispatches a data-bearing event, and an unfinished event at EOF is discarded.
See SSE guide and Consume an SSE stream.
EventStreamDefinition
path, events, required maxBufferSize and maxQueueSize, optional method / operation / input / error. The request shape matches HTTP, including body. GET and HEAD cannot return a body. A declared input always requires an explicit builder. input only defines the parameters; the builder alone decides where each value goes. Only definitions without input may omit builder.
EventStreamEndpoint
Returned by defineEventStream. The first argument is a client. Further arguments are the input only.
EventStreamRequestShape
Same fields as HttpRequestShape for the stream's method and path.
StreamAwaitResult
[null, stream, open] or [error, undefined, open?].
EventStreamData
Parsed event payload inferred from an EventStructs map.