> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ooneex.com/llms.txt
> Use this file to discover all available pages before exploring further.

# WebSockets

> Build real-time features end to end with the @ooneex/socket server controllers and the @ooneex/socket-client typed client.

Ooneex models WebSockets as ordinary routes. On the server, `@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.channel` exposes `subscribe`, `unsubscribe`, `publish`, `send`, and `close` with the response type baked in, so broadcasts and direct replies are checked at compile time.
* **Built-in pub/sub.** `subscribe()` joins a channel and `publish()` 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.** `ISocketMiddleware` runs the same pipeline as HTTP middleware against the socket context for auth and guards before the controller's `index` runs.

## How it works

A socket route is registered just like an HTTP route, but flagged as a socket and pinned to `GET`. 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.                   |

Unlike an HTTP controller, a socket `index` returns nothing — it performs side effects on the channel. The `IController` contract is:

```typescript theme={null}
interface IController<T extends ContextConfigType = ContextConfigType> {
  index: (context: ContextType<T>) => Promise<void> | void;
}
```

See [Routing](/basics/routing) for the decorator family and [Controllers](/basics/controller) for how controllers are resolved.

## 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`.

```typescript theme={null}
import { Route } from "@ooneex/routing";
import type { ContextType, IController } from "@ooneex/socket";

@Route.socket("/ws/chat", {
  name: "api.chat.connect",
  description: "Connect to the chat socket",
})
export class ChatController implements IController {
  public async index(context: ContextType): Promise<void> {
    // Join the channel so this connection receives published messages
    await context.channel.subscribe();

    // Greet the connecting client directly
    await context.channel.send(
      context.response.json({ event: "welcome", message: "Connected to chat" }),
    );
  }
}
```

### 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. |

The full context type extends the HTTP controller context with this `channel`:

```typescript theme={null}
type ContextType<T extends ContextConfigType = ContextConfigType> =
  ControllerContextType<T> & {
    channel: {
      ws: ServerWebSocket<T["response"]>;
      send: (response: IResponse<T["response"]>) => Promise<void>;
      close(code?: number, reason?: string): void;
      subscribe: () => Promise<void>;
      isSubscribed(): boolean;
      unsubscribe: () => Promise<void>;
      publish: (response: IResponse<T["response"]>) => Promise<void>;
    };
  };
```

Because it extends the controller context, you also get `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.

```typescript theme={null}
import { Route } from "@ooneex/routing";
import { type } from "@ooneex/validation";
import type { ContextConfigType, ContextType, IController } from "@ooneex/socket";

interface ChatRoomConfig extends ContextConfigType {
  params: { roomId: string };
  payload: { message: string };
  response: { event: string; roomId: string; message?: string; userId?: string };
}

@Route.socket("/ws/rooms/:roomId", {
  name: "api.rooms.join",
  description: "Join a chat room and broadcast messages",
  params: { roomId: type("string") },
  payload: type({ message: "string" }),
})
export class ChatRoomController implements IController<ChatRoomConfig> {
  public async index(context: ContextType<ChatRoomConfig>): Promise<void> {
    const { roomId } = context.params;
    const { message } = context.payload;

    // Join the room channel
    await context.channel.subscribe();

    // Confirm the join to this client only
    await context.channel.send(
      context.response.json({ event: "joined", roomId }),
    );

    // Broadcast the message to everyone subscribed to the room
    await context.channel.publish(
      context.response.json({
        event: "message",
        roomId,
        message,
        userId: context.user?.id,
      }),
    );
  }
}
```

To leave a room or end the connection, unsubscribe and optionally close with a [WebSocket close code](https://developer.mozilla.org/docs/Web/API/CloseEvent/code):

```typescript theme={null}
if (context.channel.isSubscribed()) {
  await context.channel.unsubscribe();
}

context.channel.close(1000, "Left the room");
```

### Socket middleware

Connection-time interception uses `ISocketMiddleware`, 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.

```typescript theme={null}
import type { ContextType } from "@ooneex/socket";
import { decorator, type ISocketMiddleware } from "@ooneex/middleware";

@decorator.middleware()
export class SocketAuthMiddleware implements ISocketMiddleware {
  public async handler(context: ContextType): Promise<ContextType> {
    return context;
  }
}
```

See [Middleware](/basics/middleware) for the full pipeline, ordering, and short-circuit rules.

## 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`.

```typescript theme={null}
import { Socket } from "@ooneex/socket-client";

// http:// -> ws://, https:// -> wss://, bare host -> wss://
const socket = new Socket("https://api.example.com/ws/chat");

socket.onOpen(() => console.log("connected"));
socket.onMessage((response) => console.log("received", response));
socket.onClose((event) => console.log("closed", event.code, event.reason));
socket.onError((event, response) => console.error("error", event, 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.                        |

A few behaviors are worth knowing:

* **Message queuing.** Calling `send` before the socket is open pushes the message onto an internal queue; the queue is flushed in order as soon as `onopen` fires, so you never have to wait for the connection by hand.
* **JSON in and out.** `send` runs `JSON.stringify` on your data, and incoming frames are `JSON.parse`d into `ResponseDataType<Response>` before reaching `onMessage`.
* **Success and done flags.** A parsed message with `success` true is delivered to `onMessage`; an unsuccessful one is routed to the `onError` handler with the parsed body. If a message carries `done`, the client closes the connection.
* **Locale support.** `RequestDataType` includes an optional `lang` field, 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 expected `payload`, and the message you receive matches the controller's `response`.

```typescript theme={null}
import { Socket, type RequestDataType } from "@ooneex/socket-client";

interface ChatRequest extends RequestDataType {
  payload: { message: string };
  queries?: { roomId: string };
}

interface ChatResponse {
  event: string;
  roomId: string;
  message?: string;
  userId?: string;
}

const socket = new Socket<ChatRequest, ChatResponse>(
  "https://api.example.com/ws/rooms/general",
);

socket.onMessage((response) => {
  // response is typed as ResponseDataType<ChatResponse>
  if (response.data.event === "message") {
    console.log(`${response.data.userId}: ${response.data.message}`);
  }
});

// Safe to call immediately — this is queued until the socket opens
socket.send({
  payload: { message: "Hello, room!" },
  queries: { roomId: "general" },
  lang: { locale: "en" },
});

// Later, close cleanly
socket.close(1000, "User left");
```

## Best practices

* **Path first.** Call `@Route.socket(path, config)` with the path as the first argument and the config object second — the config never contains `path`.
* **Subscribe before you publish.** Join the channel with `subscribe()` so the connection receives the broadcasts it triggers; check `isSubscribed()` before unsubscribing.
* **`send` vs `publish`.** Use `send` to reply to the connected client only; use `publish` to fan a message out to every subscriber of the channel.
* **Type both ends.** Define a `ContextConfigType` on the server and matching `SendData`/`Response` generics on the client so payloads and messages stay checked across the wire.
* **Guard at the edge.** Put auth and rate limits in `ISocketMiddleware` so unauthorized connections are rejected before `index` runs.
* **Close with intent.** Pass a standard WebSocket close code (e.g. `1000` for normal closure, `1008` for policy violations) and a human-readable reason so clients can react.
* **Let the client queue.** Call `send` as soon as you have data — the client flushes queued messages on open, so there is no need to wait for `onOpen` to start sending.
