Skip to content

WebSocket Composables

Experimental

This package is in an experimental phase. The API may change without following semver until it reaches a stable release.

Composables for working with WebSocket connections, messages, rooms, and server state. Most composables follow the Wooks defineWook pattern — results are cached per context and resolved lazily; useWsServer() is a plain function.

useWsConnection

Returns connection-level information. Available in both connection handlers (onConnect, onDisconnect) and message handlers (onMessage).

ts
import { useWsConnection } from '@wooksjs/event-ws'

ws.onMessage('query', '/me', () => {
  const { id, send, close } = useWsConnection()
  return { connectionId: id }
})
PropertyTypeDescription
idstringUnique connection ID (crypto.randomUUID())
send(event, path, data?, params?) => voidSend a push message directly to this connection
close(code?, reason?) => voidClose the underlying WebSocket
contextEventContextThe connection-level event context

Direct push to a connection

ts
const { send } = useWsConnection()

// Send a push message to this specific connection
send('notification', '/alerts', { text: 'New message' })

// With route params (received by client in the push event)
send('update', '/users/42', { name: 'Alice' }, { id: '42' })

Push messages bypass room membership — they go directly to the connection regardless of which rooms it has joined.

currentConnection()

A lower-level escape hatch that returns the connection-level EventContext from either handler type — current() in onConnect/onDisconnect, or current().parent (the connection context) in onMessage. Most code should use useWsConnection() instead; reach for currentConnection() only when you need the raw context to read/write connection-scoped slots directly.

ts
import { currentConnection } from '@wooksjs/event-ws'

const connCtx = currentConnection() // connection EventContext, from any WS handler

useWsMessage

Returns the current message payload and metadata. Available only in message handlers (onMessage).

ts
import { useWsMessage } from '@wooksjs/event-ws'

ws.onMessage('message', '/chat/:room', () => {
  const { data, raw, id, path, event } = useWsMessage<{ text: string }>()

  console.log(event) // → 'message'
  console.log(path)  // → '/chat/general'
  console.log(data)  // → { text: 'hello' }

  return { received: true }
})
PropertyTypeDescription
dataTParsed message payload (generic type parameter)
rawBuffer | stringRaw message before parsing
idstring | number | undefinedCorrelation ID — present when client used call()
pathstringConcrete message path (e.g. /chat/general)
eventstringEvent type (e.g. message, join, subscribe)

Handler return values

What your handler returns determines the server's response:

SituationServer behavior
Client sent id, handler returns valueReply: { id, data: <value> }
Client sent id, handler returns undefinedReply: { id, data: null }
Client sent no id (fire-and-forget)Return value is ignored
Handler throws WsErrorReply: { id, error: { code, message } } (if id present)
Handler throws other ErrorReply: { id, error: { code: 500, message: "Internal Error" } }

useWsRooms

Room management and broadcasting. Available only in message handlers (onMessage). All methods default to the current message path as the room name.

ts
import { useWsRooms } from '@wooksjs/event-ws'

ws.onMessage('join', '/chat/:room', () => {
  const { join, broadcast, rooms } = useWsRooms()

  join()  // joins room = current path (/chat/general)
  broadcast('system', { text: 'Someone joined' })

  return { rooms: rooms() }
})
MethodSignatureDescription
join(room?: string) => voidJoin a room. Defaults to current message path.
leave(room?: string) => voidLeave a room. Defaults to current message path.
broadcast(event, data?, options?) => voidBroadcast to all connections in a room.
rooms() => string[]List all rooms this connection has joined.

See Rooms & Broadcasting for a detailed guide.

useWsServer

Server-wide state: all connections, global broadcast, room queries. Available anywhere — including outside event contexts — once a WooksWs adapter has been constructed; it throws [event-ws] No active WooksWs adapter otherwise. With multiple WooksWs instances in one process, it reads the most recently constructed one.

ts
import { useWsServer } from '@wooksjs/event-ws'

ws.onMessage('admin', '/broadcast', () => {
  const { broadcast, connections } = useWsServer()

  // Send to ALL connected clients
  broadcast('announcement', '/system', { text: 'Server restarting' })

  return { totalConnections: connections().size }
})
MethodSignatureDescription
connections() => Map<string, WsConnection>All active connections
broadcast(event, path, data?, params?) => voidSend to ALL connected clients
getConnection(id: string) => WsConnection | undefinedLook up a specific connection
roomConnections(room: string) => Set<WsConnection>All connections in a room

Difference from useWsRooms().broadcast

  • useWsServer().broadcast() sends to all connected clients regardless of room membership.
  • useWsRooms().broadcast() sends only to members of a specific room, excluding the sender by default.

Accessing connections directly

ts
const { getConnection, roomConnections } = useWsServer()

// Send to a specific connection by ID
const conn = getConnection('some-connection-id')
conn?.send('notification', '/private', { text: 'Just for you' })

// Iterate members of a room
for (const conn of roomConnections('/chat/general')) {
  console.log(conn.id, conn.rooms)
}

useRouteParams

Route parameters work the same as in HTTP. Available in message handlers when the path pattern contains parameters.

ts
import { useRouteParams } from '@wooksjs/event-ws'

ws.onMessage('message', '/chat/:room', () => {
  const { get } = useRouteParams<{ room: string }>()
  const room = get('room') // → 'general'
})

This is re-exported from @wooksjs/event-core for convenience.

HTTP Composables in WebSocket

When a connection starts as an HTTP upgrade request, the original request data is accessible through the parent context chain. HTTP composables resolve transparently:

Integrated mode only

HTTP composables are available only when the connection went through an UPGRADE route that calls ws.upgrade() (integrated mode with @wooksjs/event-http). In standalone mode (ws.listen()), there is no HTTP upgrade context and these composables throw.

ts
import { useHeaders, useCookies, useAuthorization } from '@wooksjs/event-http'

ws.onConnect(() => {
  // All of these read from the original upgrade request
  const { headers } = useHeaders()
  const { getCookie } = useCookies()
  const { is, basicCredentials } = useAuthorization()

  if (!is('bearer')) {
    throw new WsError(401, 'Authentication required')
  }
})

ws.onMessage('query', '/me', () => {
  // Also works in message handlers — resolved through parent chain
  const { headers } = useHeaders()
  return { userAgent: headers['user-agent'] }
})
HTTP ComposableWorks in WS (integrated mode)?Notes
useRequest()YesReturns the upgrade IncomingMessage
useHeaders()YesUpgrade request headers
useCookies()YesCookies from the upgrade request
useAuthorization()YesAuth header from the upgrade request
useUrlParams()YesQuery string from the upgrade URL
useAccept()YesAccept header from the upgrade request
useResponse()NoNo HTTP response exists in WebSocket context
useBody()NoUpgrade requests have no body

WsError

Throw WsError to send structured error responses to the client.

ts
import { WsError } from '@wooksjs/event-ws'

ws.onMessage('join', '/chat/:room', () => {
  const { data } = useWsMessage<{ name: string }>()
  if (!data?.name) {
    throw new WsError(400, 'Name is required')
  }
  if (isNameTaken(data.name)) {
    throw new WsError(409, 'Name already taken')
  }
  // ...
})
ContextBehavior
onMessage with idSends { id, error: { code, message } }
onMessage without idNothing sent; WsError is silently swallowed (only non-WsError exceptions are logged)
onConnectRejects the connection (close code 1008 for 401/403, 1011 for others)

Connection Lifecycle

onConnect

Runs when a new WebSocket connection is established. Use it for authentication, session setup, or logging.

ts
ws.onConnect(() => {
  const { id } = useWsConnection()
  const { getCookie } = useCookies()
  console.log(`New connection: ${id}`)

  // Reject unauthenticated connections
  if (!getCookie('session')) {
    throw new WsError(401, 'Not authenticated')
  }
})

onDisconnect

Runs when a connection closes. Use it for cleanup. Room membership is cleaned up automatically — you don't need to call leave() here.

ts
ws.onDisconnect(() => {
  const { id } = useWsConnection()
  console.log(`Disconnected: ${id}`)
  // Custom cleanup: remove from state maps, notify other users, etc.
})

WARNING

useWsRooms() is not available in onDisconnect — it requires a message context. If you need to notify rooms about a disconnection, use useWsServer().getConnection(id) to read the connection's rooms set and send messages manually.

Heartbeat

In standalone mode, the server sends periodic ping frames to detect dead connections. A connection that has not answered the previous ping with a pong by the next tick is closed with WS code 1001 (Heartbeat timeout) — the pong window equals heartbeatInterval.

ts
const ws = createWsApp({
  heartbeatInterval: 30000, // ms between pings (default: 30000)
})
await ws.listen(3000)

Set heartbeatInterval: 0 to disable heartbeat.

WARNING

Heartbeat runs only in standalone mode (ws.listen()). When @wooksjs/event-ws is integrated with an HTTP server via createWsApp(http) + ws.upgrade(), no heartbeat pings are sent — implement your own liveness checks if you need dead-connection reaping in integrated mode.

Server Options

All options accepted by createWsApp() — pass them as the only argument in standalone mode, or as the second argument after the HTTP app in integrated mode:

OptionDefaultDescription
heartbeatInterval30000Ms between heartbeat pings (standalone mode only). 0 disables.
messageParserJSON.parseCustom deserializer for incoming messages. See Custom Serialization.
messageSerializerJSON.stringifyCustom serializer for outgoing messages. See Custom Serialization.
maxMessageSize1 MBIncoming messages exceeding this size are silently dropped.
wsServerAdapterwraps wsPlug-in point for alternative WS engines (e.g. uWebSockets.js, Bun). An object implementing WsServerAdapter (create(): WsServerInstance); the default wraps the ws package in noServer mode.
broadcastTransportlocal onlyCross-instance broadcast transport. See Multi-Instance Broadcasting.
loggerbuilt-inLogger instance. See Logging.

Released under the MIT License.