Defjs

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, events map, and the required positive-integer limits maxBufferSize and maxQueueSize; optional method (default 'GET'), operation, input and error. 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. These definition members are documented under EventStreamDefinition. error is required only when an error struct type is given, and builder is required exactly when input is 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.

Edit this page on GitHub
Released under the MIT License. · Copyright © 2026 MunMunMiao

On this page