Defjs

defineWebSocket, session, and request options.

WebSocket

Declare a socket, call the operation with a client, send and receive typed messages, then close.

defineWebSocket()

function defineWebSocket<
  const P extends string,
  TInput extends AnyStruct | undefined = undefined,
  TIncoming extends AnyStruct = AnyStruct,
  TOutgoing extends AnyStruct | undefined = undefined,
>(
  definition: Omit<WebSocketDefinitionBase<TIncoming, TOutgoing>, "path"> & {
    input?: TInput;
    path: P;
  } & BuilderField<NoInfer<TInput>, WebSocketRequestShape<NoInfer<P>>>,
): WebSocketEndpoint<
  EndpointArgs<TInput>,
  WebSocketIncomingData<TIncoming>,
  WebSocketOutgoingData<TOutgoing>
>;
  • definition — path, one incoming struct, and the required positive-integer maxIncomingQueueSize; optional outgoing and input structs, operation, static protocols, and a non-negative maxOutgoingQueueSize. 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 WebSocketDefinition. 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 opens the session.
import { defineWebSocket, struct } from "@defjs/core";

const chat = defineWebSocket({
  maxIncomingQueueSize: 100,
  path: "/chat",
  protocols: ["chat.v1"],
  incoming: struct.object({
    text: struct.string(),
    type: struct.literal("message"),
  }),
  outgoing: struct.object({
    text: struct.string(),
    type: struct.literal("message"),
  }),
});

The call returns [null, session, connection] or [error, undefined, connection?]. Pass per-call options by deriving a client from RequestOptions. WebSocket reads signal, beforeConnect, heartbeat, protocols, and reconnect, and ignores HTTP progress fields.

beforeConnect, heartbeat, protocols, and reconnect each replace the client value for that call as a whole. Protocol precedence is the derived client, then withWebSocketProtocols, then the definition's protocols. heartbeat.timeoutMs is the acknowledgement wait, not a request deadline.

WebSocketSession

interface WebSocketSession<
  TIncoming = unknown,
  TOutgoing = never,
> extends AsyncDisposable {
  readonly bufferedAmount: number;
  readonly connection: WebSocketConnectionInfo;
  readonly closed: Promise<WebSocketCloseInfo>;
  readonly receive: AsyncIterable<TIncoming>;
  readonly state: WebSocketState;
  close(code?: number, reason?: string): void;
  [Symbol.asyncDispose](): PromiseLike<void>;
  onRuntimeError(listener: (error: unknown) => void): () => void;
  onStateChange(listener: (state: WebSocketState) => void): () => void;
  send(message: TOutgoing): void;
}

Prefer await using ownedSession = session for owned scope cleanup. Manual session.close() and await session.closed remain available.

async function consume(
  session: WebSocketSession<unknown, never>,
): Promise<void> {
  await using ownedSession = session;
  for await (const message of ownedSession.receive) console.log(message);
}

closed reports the logical terminal state. The async disposer requests native close and waits for Defjs-owned teardown, but bounds a missing close event at one second and may reject with a DOMException named TimeoutError. It cannot prove that the physical TCP connection closed.

WebSocketState

'idle' | 'connecting' | 'open' | 'closing' | 'closed' | 'reconnecting' | 'aborted' | 'error'

WebSocketConnectionInfo

generation (increments on reconnect), and, once known, url, protocol and extensions.

WebSocketCloseInfo

type WebSocketCloseInfo =
  | { kind: "closed"; code?: number; reason?: string; wasClean?: boolean }
  | {
      kind: "aborted";
      cause?: unknown;
      code?: number;
      reason?: string;
      wasClean?: boolean;
    }
  | {
      kind: "error";
      cause: unknown;
      code?: number;
      reason?: string;
      wasClean?: boolean;
    };

kind is closed, aborted, or error. cause is absent on closed, optional on aborted, and required on error. code, reason, and wasClean may appear on every variant. A manual session.close() fills code and reason on { kind: "closed" } when the close event carries none.

WebSocketExecuteOptions

type WebSocketExecuteOptions<
  TIncoming = unknown,
  TOutgoing = unknown,
> = UseWebSocketConfig<TIncoming, TOutgoing>;

WebSocketExecuteOptions is UseWebSocketConfig: beforeConnect, heartbeat, protocols, reconnect, and signal. WebSocketHeartbeatConfig: intervalMs, optional message, isAck, timeoutMs. There is no timeout or abort field.

signal covers the whole session, 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. signal is the call's signal; combine signals with AbortSignal.any in application code. heartbeat is replaced whole; its timeoutMs waits for an acknowledgement and is not a request deadline.

When reconnect is set, attempts defaults to 3 per outage; 0 disables. Successful open resets the budget and backoff exponent, so each new outage starts at retry 1. Omitting attempts does not give unlimited retries within one outage. See Client for the defaults and validation rules.

Messages

incoming is one struct. Text, ArrayBuffer, typed-array, and Blob frames are read as UTF-8, JSON-parsed, then checked against that struct. A frame that fails JSON parse or the struct is reported to session.onRuntimeError and is not queued. There is no client invalid-frame option.

outgoing is one struct. session.send accepts StructInput of that struct, encodes aliases, then JSON.stringify. Values JSON cannot faithfully represent after encoding throw TypeError before sending or entering the reconnect queue, including non-finite numbers, functions, symbols, Map, Set, streams, and class instances without a JSON representation. Nested values use the same guard as HTTP JSON bodies. Heartbeat uses that guard too; a heartbeat serialization failure ends the session. Several message kinds are a struct.discriminatedUnion.

session.send({ type: "message", text: "Hello" });

See WebSocket guide and Open a WebSocket session.

WebSocketDefinition

path, incoming, required maxIncomingQueueSize, optional outgoing / input / operation / protocols / maxOutgoingQueueSize. The builder may return path and query only. 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.

WebSocketEndpoint

Returned by defineWebSocket. The first argument is a client. Further arguments are the input only.

WebSocketRequestShape

path and query only. headers and body are never.

UseWebSocketConfig

beforeConnect, heartbeat, protocols, reconnect, and signal. WebSocketExecuteOptions is this slice. There is no abort field.

SocketAwaitResult

[null, session, connection] or [error, undefined, connection?].

ManualSocketCloseReason

Reason recorded when the session is closed via session.close().

WebSocketHeartbeatConfig

intervalMs, optional message, isAck, timeoutMs.

WebSocketIncomingData

Decoded incoming frame: Infer of the incoming struct.

WebSocketOutgoingData

Value accepted by session.send: StructInput of the outgoing struct, or never when none is declared.

SocketLifecycleOutcome

Outcome of one connect attempt (closeInfo, connection, cause, opened), used by reconnect logic. It is not the final result of the session; see WebSocketCloseInfo for that.

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

On this page