0Z4C0
// navigation
// @ozaco/std

WebSocket

An effect-native WebSocket client — open a socket, stream incoming frames, and send codec-encoded values.

import from@ozaco/std/ws

std:ws is the socket counterpart to std:fetch: a WebSocket CLIENT built on effects. connect() opens a socket and resolves once it is OPEN (or fails with 'ws/connect' if the handshake errors), handing back a Connection that exposes incoming frames as an effect Stream, a send operation, a close, and a closed Future.

Message framing mirrors the server gateway and, like std:fetch, runs through the registered std:codec: strings and binary go over the wire as-is, while every other value is codec-encoded on send and codec-decoded on receive. A codec (e.g. JsonCodec) must be installed in scope. Incoming frames are buffered in a queue and decoded lazily as they are pulled, so nothing is lost between arrival and consumption.

The underlying constructor is injectable through the wsImpl context — it defaults to the platform global WebSocket and can be swapped for a loopback socket or custom transport in tests. connect forces binaryType = 'arraybuffer' so binary frames arrive consistently as ArrayBuffer across Bun, Node and the browser.

// on this page
01

Client

Open a connection.

fn

connect

Open a WebSocket and resolve once it is OPEN, yielding a `Connection`.

signature
const connect: (url: string | URL, options?: WsDef.Options) => Future<WsDef.Connection>

Fails with 'ws/unsupported' if no WebSocket implementation is available, or 'ws/connect' if the socket errors during the handshake. Headers (options.headers) use the Bun/Node options-object constructor form; the browser WebSocket cannot set handshake headers, so authenticate with a ?token= query param there. A codec must be installed for non-string/binary payloads.

// parameters
url: string | URL
The socket URL (`ws:`/`wss:`).
options?: WsDef.Options
Sub-protocols and handshake headers.
// returns A `Future<WsDef.Connection>` that resolves once the socket is OPEN.
example
import { streamForEach } from 'std:effect'
import { connect } from '@ozaco/std/ws'
import { JsonCodec } from 'std:codec/impl/json'

yield* install(JsonCodec)

const socket = yield* connect('wss://example.com/chat', {
  headers: { authorization: 'Bearer …' },
})

yield* socket.send({ event: 'join', room: 'general' })

// consume decoded frames until the socket closes
yield* streamForEach(socket.messages, function* (msg) {
  console.log('frame', msg)
})

const { code, reason } = yield* socket.closed
02

Context

Inject the underlying WebSocket constructor.

const

wsImpl

Effect context holding the `WebSocket` constructor `connect()` dispatches through.

signature
const wsImpl: Context<WsDef.Ctor>

Defaults to the platform global WebSocket. Override it for tests, a loopback socket, or a custom transport with wsImpl.set(Ctor) or wsImpl.with(Ctor, op) in the running scope; connect reads it via wsImpl.get() so the default applies when unset.

example
import { wsImpl } from '@ozaco/std/ws'

yield* wsImpl.with(FakeSocket, function* () {
  const socket = yield* connect('ws://test')
  // ...
})
03

Types

The `WsDef` namespace: the connection surface and its supporting shapes.

iface

WsDef.Connection

A live socket: its message `Stream`, `send`, `close`, `closed` Future and the native escape hatch.

signature
interface Connection {
  readonly native: SocketLike
  readonly url: string
  readonly readyState: number
  send(data: unknown): Operation<void>
  readonly messages: Stream<unknown, StreamClose>
  close(code?: number, reason?: string): Operation<void>
  readonly closed: Future<CloseInfo>
}

send encodes non-string/binary values through the registered codec; messages yields codec-decoded frames and closes true on a clean close or with a failure on error; closed resolves with the close code/reason.

iface

WsDef.Options

Handshake options for `connect`: sub-protocols and headers.

signature
interface Options {
  protocols?: string | string[]
  headers?: Record<string, string>
}

headers are honored by Bun’s and Node’s global WebSocket; the browser WebSocket cannot set handshake headers.

iface

WsDef.CloseInfo

Why/how the socket closed.

signature
interface CloseInfo {
  code: number
  reason: string
}
iface

WsDef.SocketLike

The WHATWG `WebSocket` subset the browser, Bun and Node globals all satisfy.

signature
interface SocketLike {
  send(data: string | ArrayBufferLike | ArrayBufferView): void
  close(code?: number, reason?: string): void
  readonly readyState: number
  binaryType: 'blob' | 'arraybuffer'
  onopen: ((event: AnyType) => void) | null
  onmessage: ((event: { data: AnyType }) => void) | null
  onerror: ((event: AnyType) => void) | null
  onclose: ((event: { code?: number; reason?: string }) => void) | null
}
type

WsDef.Ctor

The injectable `WebSocket` constructor type (standard or Bun/Node options-object form).

signature
type Ctor = new (
  url: string | URL,
  options?:
    | string
    | string[]
    | { protocols?: string | string[]; headers?: Record<string, string> },
) => SocketLike
type

WsDef.StreamClose

The value the message stream settles with: `true` on a clean close, or a failure on error.

signature
type StreamClose = true | Result.Failure<unknown>
type

WsDef.Error

Errors surfaced by `connect`. Thrown faults pass through as `unknown`; the only tags it raises are `'ws/unsupported'` and `'ws/connect'`.

signature
type Error = unknown