# @amatiasq/socket

WebSockets in three layers, on both ends of the wire. Pick the one you need and
ignore the rest.

Install with `npm i --save @amatiasq/socket`.

|                                 | client (browser)  | server           | one connection      |
| ------------------------------- | ----------------- | ---------------- | ------------------- |
| transport, reconnection, queue  | `ResilientSocket` | —                | —                   |
| typed JSON                      | `JsonSocket`      | `JsonServer`     | `JsonConnection`    |
| typed messages, session identity| `SessionSocket`   | `SessionServer`  | `SessionConnection` |

Client-side names come from `@amatiasq/socket`, server-side from
`@amatiasq/socket/server`. The split is deliberate: importing the client surface
never pulls the server code into your bundle.

**This package has one dependency (`@amatiasq/emitter`) and no WebSocket
implementation.** The client uses the platform's global `WebSocket`; the server
takes sockets you already have. That is what lets it run on Node, Bun, Deno and
Cloudflare Workers without a per-runtime build.

## `ResilientSocket` — reconnection and a send queue

```js
import { ResilientSocket } from '@amatiasq/socket';

const socket = new ResilientSocket('wss://sockethost.com');

socket.onOpen(() => console.log('Socket open'));
socket.onClose(() => console.log('Socket closed'));
socket.onMessage(event => console.log(event.data));

// Fires when it gives up, after maxReconnectAttempts
socket.onError(() => console.log('Reconnection failed'));

socket.onReconnect(event =>
  console.log(`Disconnected for ${Date.now() - +event.disconnectedTime}ms`),
);

// Sent now if connected, queued and flushed on reconnect if not
socket.send('hello');
```

## `JsonSocket` — typed JSON

```ts
import { JsonSocket } from '@amatiasq/socket';

interface Incoming {
  foo: number;
}
interface Outgoing {
  bar: string;
}

const socket = new JsonSocket<Incoming, Outgoing>('wss://sockethost.com');

socket.onMessage(message => console.log(message.foo));
socket.send({ bar: 'test' }); // a wrong shape is a compile error
```

A frame that is not valid JSON is dropped with a `console.warn` — a peer sending
garbage does not take the listener down.

## `JsonServer` — the same, server-side

```ts
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { JsonServer, fromEventEmitter } from '@amatiasq/socket/server';

const server = new JsonServer<Incoming, Outgoing>();

server.onConnection(client => {
  client.onClose(() => logout(client));
  client.onMessage(data => console.log(data.foo));
  client.send({ bar: 'hello' });
});

// You own the listening socket. `ws` here, but see "Runtimes" below.
const wss = new WebSocketServer({ server: createServer() });
wss.on('connection', ws => server.accept(fromEventEmitter(ws)));
```

## The session layer

`SessionSocket` / `SessionServer` add an identity that outlives the socket. When
a client reconnects it re-announces its `ClientId`, and the server rebinds it to
the same `SessionConnection` — every listener registered on it stays attached,
and `onConnection` does **not** fire again.

```ts
import { SessionServer, fromEventEmitter } from '@amatiasq/socket/server';

type ServerMessage = { type: 'tick'; data: number };
type ClientMessage = { type: 'move'; data: string };

const server = new SessionServer<ServerMessage, ClientMessage>();

server.onConnection(client => {
  client.onMessageType('move', where => console.log(client.id, where));
  client.send('tick', Date.now());
});

wss.on('connection', ws => server.accept(fromEventEmitter(ws)));
```

### Session ids are bearer tokens

A `ClientId` is a v4 UUID from the platform CSPRNG — 122 random bits — issued by
the server and never taken from the wire. A reconnect naming an id the server does
not know gets a fresh session rather than adopting that id.

Treat it as you would a session cookie: **whoever presents the id is that client.**
There is nothing else distinguishing a reconnecting client from someone who
obtained its id, so do not log it, do not put it in a URL, and do not persist it
anywhere a third party can read.

(Before 2.0.0 these were `1`, `2`, `3`… and any client naming another's id was
handed its session. If you are on 1.x, upgrade.)

## Runtimes

The servers never create a socket, so they work anywhere. Two adapters cover the
two socket shapes that exist:

```ts
import { fromEventTarget, fromEventEmitter } from '@amatiasq/socket/server';

fromEventEmitter(ws); //  `ws` on Node — .on() / .off()
fromEventTarget(socket); //  Deno, Cloudflare Workers, browsers — addEventListener
```

Bun's `ServerWebSocket` declares its handlers on the server rather than per
socket, so build the transport by hand — it is four methods:

```ts
Bun.serve({
  websocket: {
    open(ws) {
      server.accept({
        send: data => ws.send(data),
        close: () => ws.close(),
        onMessage: listener => ((ws.data.onMessage = listener), () => {}),
        onClose: listener => ((ws.data.onClose = listener), () => {}),
      });
    },
    message: (ws, data) => ws.data.onMessage?.(String(data)),
    close: ws => ws.data.onClose?.(),
  },
});
```

## Reconnection, in detail

A dropped socket is retried after `reconnectionDelay` (100ms by default), then
200ms, then 400ms… After `maxReconnectAttempts` (14) it stops and fires
`onError`. Both are per-instance options:

```js
new ResilientSocket(uri, { reconnectionDelay: 500, maxReconnectAttempts: 3 });
```

Anything sent while disconnected is queued and flushed, in order, once the
connection is back.

## History

This package is the merge of four: `@amatiasq/socket`, `@amatiasq/json-socket`,
`@amatiasq/nice-socket` and `@amatiasq/resilient-socket`. The three absorbed
names are deprecated on npm and will not get further versions.

Renames, if you are coming from one of them:

| before                          | now                 |
| ------------------------------- | ------------------- |
| `ClientSocket`                  | `SessionSocket`     |
| `WebSocketServer`               | `SessionServer`     |
| `ServerSocket`                  | `SessionConnection` |
| `NiceSocketServer`              | `JsonServer`        |
| `NiceSocket`                    | `JsonConnection`    |
| `sendJson()` / `onJsonMessage()`| `send()` / `onMessage()` |

`JsonSocket` used to carry its own copy of the reconnection logic and no send
queue; it is now a layer over `ResilientSocket`, so it gained the queue. Its
`onMessage`/`onOpen`/`onReconnect` are subscribe functions now, matching the rest
of the package — call them, do not reach for `.subscribe`.

The server classes no longer take an `http.Server`. See "Runtimes".
