# WebSockets Integration

WebSockets keep a persistent connection open so your Aurelia components can react to server-side events instantly—no polling necessary. The Vite-powered Aurelia 2 starter already ships with everything you need to consume WebSockets from the browser; you only need a small client service and (optionally) a Node-based development server to broadcast messages while you iterate.

***

## 1. Optional: spin up a tiny WebSocket server

If you don’t already have a backend, you can scaffold a development server with the popular [`ws`](https://github.com/websockets/ws) package:

```bash
npm install ws --save-dev
```

```js
// scripts/dev-websocket-server.mjs
import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', (socket) => {
  console.log('Client connected');
  socket.send(JSON.stringify({ type: 'server-ready', timestamp: Date.now() }));

  socket.on('message', (data) => {
    // echo message to everyone (simple chat)
    wss.clients.forEach(client => {
      if (client.readyState === 1 /* WebSocket.OPEN */) {
        client.send(JSON.stringify({
          type: 'message',
          text: data.toString(),
          timestamp: Date.now(),
        }));
      }
    });
  });
});

console.log('WebSocket server listening on ws://localhost:8080');
```

Run it alongside `npm run dev` so both the Vite dev server and the WebSocket server stay active.

***

## 2. Configure an environment variable for the WebSocket URL

Keep the WebSocket endpoint in `.env.local` so you can point the frontend at different servers per environment:

```dotenv
# .env.local
VITE_WS_URL=ws://localhost:8080
```

Vite exposes variables prefixed with `VITE_` via `import.meta.env`, letting you inject the URL without hard-coding it in source control.

***

## 3. Create a WebSocket client service

```ts
// src/services/websocket-client.ts
import { DI, Registration } from '@aurelia/kernel';
import { IDisposable } from '@aurelia/kernel';

export interface ChatMessage {
  type: string;
  text?: string;
  timestamp: number;
}

export const IWebSocketClient = DI.createInterface<IWebSocketClient>('IWebSocketClient');

export interface IWebSocketClient extends IDisposable {
  connect(url: string, listener: (msg: ChatMessage) => void): void;
  send(payload: Record<string, unknown>): void;
  readyState(): number;
}

export class WebSocketClient implements IWebSocketClient {
  private socket: WebSocket | null = null;
  private listener: ((msg: ChatMessage) => void) | null = null;

  connect(url: string, listener: (msg: ChatMessage) => void): void {
    this.dispose();
    this.listener = listener;

    const socket = new WebSocket(url);
    socket.onopen = () => console.info('WebSocket connected');
    socket.onclose = (event) => {
      console.info('WebSocket closed', event.reason);
      this.socket = null;
    };
    socket.onerror = (err) => console.error('WebSocket error', err);
    socket.onmessage = (event: MessageEvent<string>) => {
      try {
        const data = JSON.parse(event.data) as ChatMessage;
        this.listener?.(data);
      } catch (error) {
        console.warn('Unparseable message', error, event.data);
      }
    };

    this.socket = socket;
  }

  send(payload: Record<string, unknown>): void {
    if (this.socket?.readyState === WebSocket.OPEN) {
      this.socket.send(JSON.stringify(payload));
    } else {
      console.warn('WebSocket not connected yet');
    }
  }

  readyState(): number {
    return this.socket?.readyState ?? WebSocket.CLOSED;
  }

  dispose(): void {
    this.socket?.close();
    this.socket = null;
    this.listener = null;
  }
}

export const WebSocketClientRegistration = Registration.singleton(IWebSocketClient, WebSocketClient);
```

This service encapsulates connection lifecycle, JSON parsing, and basic logging. Because it’s registered as a singleton, every component that resolves `IWebSocketClient` shares the same connection.

***

## 4. Register the service when bootstrapping Aurelia

```ts
// src/main.ts
import { Aurelia, StandardConfiguration } from '@aurelia/runtime-html';
import { WebSocketClientRegistration } from './services/websocket-client';
import { MyApp } from './my-app';

await new Aurelia()
  .register(StandardConfiguration, WebSocketClientRegistration)
  .app({ host: document.querySelector('my-app')!, component: MyApp })
  .start();
```

***

## 5. Build a chat component on top of the service

```ts
// src/components/chat.ts
import { customElement } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';
import { IWebSocketClient, ChatMessage } from '../services/websocket-client';

@customElement({
  name: 'chat',
  template: `
    <div class="chat">
      <ul>
        <li repeat.for="message of messages">
          <strong>${message.type}:</strong> ${message.text}
          <span class="timestamp">\${new Date(message.timestamp).toLocaleTimeString()}</span>
        </li>
      </ul>

      <form submit.trigger="send()">
        <input value.bind="draft" autocomplete="off" placeholder="Type a message…" />
        <button type="submit" disabled.bind="!draft.trim()">Send</button>
      </form>
    </div>
  `,
})
export class Chat {
  draft = '';
  messages: ChatMessage[] = [];
  private readonly ws = resolve(IWebSocketClient);
  private readonly url = import.meta.env.VITE_WS_URL as string;

  binding() {
    this.ws.connect(this.url, (message) => {
      this.messages = [...this.messages, message];
    });
  }

  detaching() {
    this.ws.dispose(); // or keep alive if other components rely on it
  }

  send() {
    if (!this.draft.trim()) return;
    this.ws.send({ type: 'message', text: this.draft.trim(), timestamp: Date.now() });
    this.draft = '';
  }
}
```

The single `chat` element now reflects server updates in real time and can safely send messages through the shared connection.

***

## 6. Tips & extensions

* **Reconnect logic:** wrap the `WebSocket` instantiation in a helper that backs off and retries on `close` events when `event.wasClean` is `false`.
* **Binary data:** switch to `socket.binaryType = 'arraybuffer'` and send typed arrays when transferring files or images.
* **Authentication:** include JWTs or session tokens in the initial connection URL (e.g., `ws://host?token=...`) or upgrade request headers depending on your backend.
* **State integration:** dispatch incoming messages into `@aurelia/state` or another store so multiple components can react without each subscribing to the socket.

With the connection wrapped in a DI-friendly service and the endpoint stored in environment variables, swapping between local, staging, and production WebSocket servers becomes trivial—and you avoid the legacy webpack-specific loader configuration entirely.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.aurelia.io/recipes/websockets.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
