@ooneex/socket lets you declare a socket endpoint with the @Route.socket(path, config) decorator and implement a controller whose index(context) runs on connect, with a typed context.channel for subscribing, publishing, and sending. On the client, @ooneex/socket-client ships a typed Socket<SendData, Response> class that auto-detects the protocol, serializes JSON, queues messages until the connection opens, and dispatches typed events. The two ends share the same request/response shapes, so a real-time feature stays type-safe across the wire.
Why WebSockets in Ooneex
- Routes, not a separate world. A socket endpoint is declared with the same decorator family as HTTP routes (
@Route.socket) and resolves a container-managed controller — the same routing, validation, roles, and middleware you already use. - Typed channel API.
context.channelexposessubscribe,unsubscribe,publish,send, andclosewith the response type baked in, so broadcasts and direct replies are checked at compile time. - Built-in pub/sub.
subscribe()joins a channel andpublish()fans a message out to every subscriber — room broadcasting without a separate message bus. - A matching client.
Socket<SendData, Response>mirrors the server’s request and response types, so the payload you send and the message you receive are the same shapes on both ends. - Connection-time interception.
ISocketMiddlewareruns the same pipeline as HTTP middleware against the socket context for auth and guards before the controller’sindexruns.
How it works
A socket route is registered just like an HTTP route, but flagged as a socket and pinned toGET. When a client opens the connection, the matched controller’s index(context) runs once. From there, everything happens through context.channel.
| Stage | What happens |
|---|---|
| Declaration | @Route.socket(path, config) registers the controller as a socket route (isSocket: true, method GET). |
| Connection | A client connects; socket middleware runs, then the controller’s index(context) executes once. |
| Subscribe | context.channel.subscribe() joins the channel so this connection receives published messages. |
| Broadcast | context.channel.publish(response) sends a message to every subscriber of the channel. |
| Direct reply | context.channel.send(response) sends a message back to the connected client only. |
| Close | context.channel.close(code?, reason?) ends the connection with a WebSocket close code. |
index returns nothing — it performs side effects on the channel. The IController contract is:
Server: defining a socket controller
@Route.socket takes the path as its first argument and a config object as its second — the same signature as @Route.get, @Route.post, and the rest. Implement IController from @ooneex/socket and do your work in index using context.channel.
The channel API
context.channel is the typed surface for everything a socket controller does. Every payload is built with context.response (the same response builder as HTTP controllers), so the response type flows through publish and send.
| Method | Returns | Description |
|---|---|---|
subscribe() | Promise<void> | Join the channel so this connection receives published messages. |
isSubscribed() | boolean | Whether this connection is currently subscribed. |
unsubscribe() | Promise<void> | Leave the channel; published messages no longer reach this connection. |
publish(response) | Promise<void> | Broadcast a response to every subscriber of the channel. |
send(response) | Promise<void> | Send a response to the connected client only. |
close(code?, reason?) | void | Close the connection with an optional WebSocket close code and reason. |
channel:
context.params, context.payload, context.queries, context.user, and context.response — the same members you use in HTTP controllers.
A chat room with pub/sub
A realistic room controller uses a path parameter for the room, subscribes the connection, replies directly to the joiner, and publishes a join event to everyone else in the room. Type the context for end-to-end safety.Socket middleware
Connection-time interception usesISocketMiddleware, which mirrors the HTTP IMiddleware shape against the socket context — same decorator, same handler(context) contract, same short-circuiting. Use it for auth and guards before index runs.
Client: connecting with @ooneex/socket-client
Socket<SendData, Response> is a thin, typed wrapper over the browser WebSocket. The constructor takes a single URL and auto-detects the protocol: http:// becomes ws://, https:// becomes wss://, and a bare host (no scheme) is upgraded to wss://. Outgoing data is JSON-serialized for you, and incoming messages are parsed back into the typed Response.
Client methods
The two generic parameters tie the client to the server:SendData (extending RequestDataType) is what you send, and Response is the message shape you receive (wrapped as ResponseDataType<Response>).
| Method | Returns | Description |
|---|---|---|
send(data) | void | JSON-serialize and send data; queued if the connection is not yet open. |
onMessage(handler) | void | Register a handler for incoming, successfully-parsed messages. |
onOpen(handler) | void | Register a handler that runs when the connection opens. |
onClose(handler) | void | Register a handler for connection close, receiving the CloseEvent. |
onError(handler) | void | Register a handler for errors; also called for messages flagged unsuccessful. |
close(code?, reason?) | void | Close the connection with an optional code and reason. |
- Message queuing. Calling
sendbefore the socket is open pushes the message onto an internal queue; the queue is flushed in order as soon asonopenfires, so you never have to wait for the connection by hand. - JSON in and out.
sendrunsJSON.stringifyon your data, and incoming frames areJSON.parsed intoResponseDataType<Response>before reachingonMessage. - Success and done flags. A parsed message with
successtrue is delivered toonMessage; an unsuccessful one is routed to theonErrorhandler with the parsed body. If a message carriesdone, the client closes the connection. - Locale support.
RequestDataTypeincludes an optionallangfield, so you can attach locale information to any message you send.
Typed client usage
Mirror the server’s request and response shapes for a fully typed exchange. The payload you send matches the controller’s expectedpayload, and the message you receive matches the controller’s response.
Best practices
- Path first. Call
@Route.socket(path, config)with the path as the first argument and the config object second — the config never containspath. - Subscribe before you publish. Join the channel with
subscribe()so the connection receives the broadcasts it triggers; checkisSubscribed()before unsubscribing. sendvspublish. Usesendto reply to the connected client only; usepublishto fan a message out to every subscriber of the channel.- Type both ends. Define a
ContextConfigTypeon the server and matchingSendData/Responsegenerics on the client so payloads and messages stay checked across the wire. - Guard at the edge. Put auth and rate limits in
ISocketMiddlewareso unauthorized connections are rejected beforeindexruns. - Close with intent. Pass a standard WebSocket close code (e.g.
1000for normal closure,1008for policy violations) and a human-readable reason so clients can react. - Let the client queue. Call
sendas soon as you have data — the client flushes queued messages on open, so there is no need to wait foronOpento start sending.