Skip to content

Commit

Permalink
feat!: overhaul peer and message interface
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 committed Aug 15, 2024
1 parent b4071b7 commit 2c7c8c6
Show file tree
Hide file tree
Showing 16 changed files with 664 additions and 447 deletions.
70 changes: 54 additions & 16 deletions docs/1.guide/3.peer.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,42 @@ icon: mynaui:api

> Peer object allows easily interacting with connected clients.
Websocket [hooks](/guide/hooks) accept a peer instance as their first argument. You can use peer object to get information about each connected client or send a message to them.
When a new client connects to the server, crossws creates a peer instance that allows getting information from clients and sending messages to them.

> [!TIP]
> You can safely log a peer instance to the console using `console.log` it will be automatically stringified with useful information including the remote address and connection status!
## Instance properties

## Properties
### `peer.id`

### `peer.url`
Unique random [uuid v4](https://developer.mozilla.org/en-US/docs/Glossary/UUID) identifier for the message.

Request http url during upgrade. You can use it to do actions based on path and search params.
### `peer.request?`

### `peer.headers`
Access to the upgrade [request](<(https://developer.mozilla.org/en-US/docs/Web/API/Request)>) info. You can use it to do authentication and access users headers and cookies.

Request http headers during upgrade. Youb can use it to do authentication and access upgrade headers.
> [!NOTE]
> This property is compatible with web [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) interface, However interface is emulated for Node.js and sometimes unavailable. Refer to the [compatibility table](#compatibility) for more info.
### `peer.addr`
### `peer.remoteAddress?`

The IP address of the client.

### `peer.id`
> [!NOTE]
> Not all adapters provide this. Refer to the [compatibility table](#compatibility) for more info.
### `peer.webSocket`

A unique id assigned to the peer.
Direct access to the [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) instance (partial).

### `peer.readyState`
> [!NOTE]
> WebSocket available properties vary across runtimes. Refer to the [compatibility table](#compatibility) for more info.
Client connection status (might be `undefined`)
### `peer.extensions`

:read-more{to="https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState" title="readyState in MDN"}
Access to upgrade extensions using either native `webSocket.extensions` or `x` from `request.headers`

## Methods
## Instance methods

### `peer.send(message, compress)`
### `peer.send(message, { compress? })`

Send a message to the connected client.

Expand Down Expand Up @@ -79,3 +83,37 @@ To close the connection abruptly, use `peer.terminate()`.
Abruptly close the connection.

To gracefully close the connection, use `peer.close()`.

## Compatibility

| | [Bun][bun] | [Cloudflare][cfw] | [Cloudflare (durable)][cfd] | [Deno][deno] | [Node (ws)][nodews] | [Node (μWebSockets)][nodeuws] | [SSE][sse] |
| --------------------------- | ---------- | ----------------- | --------------------------- | ------------ | ------------------- | ----------------------------- | ---------- |
| `send()` ||||||||
| `publish()` / `subscribe()` |||[^4] |[^4] |[^4] ||[^4] |
| `close()` ||||||||
| `terminate()` ||[^5] |||||[^5] |
| `request` |||[^2] ||[^1] |[^1] ||
| `webSocket.binaryType` |[^3] ||||[^3] |||
| `webSocket.bufferedAmount` ||||||||
| `webSocket.extensions` ||||||||
| `webSocket.protocol` ||||||||
| `webSocket.readyState` ||||||||
| `webSocket.url` ||||||||

[bun]: /adapters/bun
[cfw]: /adapters/cloudflare
[cfd]: /adapters/cloudflare#durable-objects
[deno]: /adapters/deno
[nodews]: /adapters/node
[nodeuws]: /adapters/node#uwebsockets
[sse]: adapters/sse

[^1]: using a proxy for [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) compatible interface (`url`, `headers` only) wrapping Node.js requests.

[^2]: `request` is not always available (only in `open` hook).

[^3]: these runtimes support non standard binary types including `nodebuffer` and `uint8array`.

[^4]: pubsub is not natively handled by runtime. crossws tracks peers and sends message to them.

[^5]: `close()` will be used for compatibility.
70 changes: 61 additions & 9 deletions docs/1.guide/4.message.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,73 @@ icon: solar:letter-line-duotone

# Message

On `message` [hook](/guide/hooks), you receive a message object containing an incoming message from the client.
On `message` [hook](/guide/hooks), you receive a message object containing data from the client.

> [!TIP]
> You can safely log `message` object to the console using `console.log` it will be automatically stringified!
> [!NOTE]
> Message object is API-compatible with standard Websocket [`MessageEvent`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/message_event) with convenient superset of utils.
## API
## Instance properties

### `message.text()`
### `message.id`

Unique random [uuid v4](https://developer.mozilla.org/en-US/docs/Glossary/UUID) identifier for the message.

### `message.event`

Get stringified text version of the message
Access to the original [message event](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/message_event) if available.

### `message.peer`

Access to the [peer instance](/guide/peer) that emitted the message.

### `message.rawData`

Raw message data
Raw message data (can be of any type).

### `message.data`

Message data (value varies based on [`peer.binaryType`](/guide/peer#peerbinarytype)).

## Instance methods

### `message.text()`

Get stringified text version of the message.

If raw data is in any other format, it will be automatically converted or decoded.

### `message.json()`

Get parsed version of the message text with [`JSON.parse()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse).

### `message.uint8Array()`

Get data as [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) value.

If raw data is in any other format or string, it will be automatically converted or encoded.

### `message.arrayBuffer()`

Get data as [`ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) or [`SharedArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer) value.

If raw data is in any other format or string, it will be automatically converted or encoded.

### `message.blob()`

Get data as [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) value.

If raw data is in any other format or string, it will be automatically converted or encoded.

## Adapter support

### `message.isBinary`
| | [Bun][bun] | [Cloudflare][cfw] | [Cloudflare (durable)][cfd] | [Deno][deno] | [Node (ws)][nodews] | [Node (μWebSockets)][nodeuws] | [SSE][sse] |
| ------- | ---------- | ----------------- | --------------------------- | ------------ | ------------------- | ----------------------------- | ---------- |
| `event` ||||||||

Indicates if the message is binary (might be `undefined`)
[bun]: /adapters/bun
[cfw]: /adapters/cloudflare
[cfd]: /adapters/cloudflare#durable-objects
[deno]: /adapters/deno
[nodews]: /adapters/node
[nodeuws]: /adapters/node#uwebsockets
[sse]: adapters/sse
58 changes: 20 additions & 38 deletions src/adapters/bun.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { WebSocketHandler, ServerWebSocket, Server } from "bun";

import type { AdapterOptions, AdapterInstance } from "../adapter.ts";
import { toBufferLike } from "../utils.ts";
import { defineWebSocketAdapter, adapterUtils } from "../adapter.ts";
Expand All @@ -17,9 +16,8 @@ export interface BunAdapter extends AdapterInstance {
export interface BunOptions extends AdapterOptions {}

type ContextData = {
_peer?: BunPeer;
request?: Request;
requestUrl?: string;
peer?: BunPeer;
request: Request;
server?: Server;
};

Expand All @@ -41,7 +39,6 @@ export default defineWebSocketAdapter<BunAdapter, BunOptions>(
data: {
server,
request,
requestUrl: request.url,
} satisfies ContextData,
headers: res?.headers,
});
Expand All @@ -52,7 +49,7 @@ export default defineWebSocketAdapter<BunAdapter, BunOptions>(
websocket: {
message: (ws, message) => {
const peer = getPeer(ws, peers);
hooks.callHook("message", peer, new Message(message));
hooks.callHook("message", peer, new Message(message, peer));
},
open: (ws) => {
const peer = getPeer(ws, peers);
Expand Down Expand Up @@ -89,66 +86,51 @@ function getPeer(
ws: ServerWebSocket<ContextData>,
peers: Set<BunPeer>,
): BunPeer {
if (ws.data?._peer) {
return ws.data._peer;
if (ws.data?.peer) {
return ws.data.peer;
}
const peer = new BunPeer({ peers, bun: { ws } });
const peer = new BunPeer({ ws, request: ws.data.request, peers });
ws.data = {
...ws.data,
_peer: peer,
peer,
};
return peer;
}

class BunPeer extends Peer<{
ws: ServerWebSocket<ContextData>;
request: Request;
peers: Set<BunPeer>;
bun: { ws: ServerWebSocket<ContextData> };
}> {
get addr() {
let addr = this._internal.bun.ws.remoteAddress;
if (addr.includes(":")) {
addr = `[${addr}]`;
}
return addr;
}

get readyState() {
return this._internal.bun.ws.readyState as any;
}

get url() {
return this._internal.bun.ws.data.requestUrl || "/";
}

get headers() {
return this._internal.bun.ws.data.request?.headers;
get remoteAddress() {
return this._internal.ws.remoteAddress;
}

send(message: any, options?: { compress?: boolean }) {
return this._internal.bun.ws.send(toBufferLike(message), options?.compress);
send(data: unknown, options?: { compress?: boolean }) {
return this._internal.ws.send(toBufferLike(data), options?.compress);
}

publish(topic: string, message: any, options?: { compress?: boolean }) {
return this._internal.bun.ws.publish(
publish(topic: string, data: unknown, options?: { compress?: boolean }) {
return this._internal.ws.publish(
topic,
toBufferLike(message),
toBufferLike(data),
options?.compress,
);
}

subscribe(topic: string): void {
this._internal.bun.ws.subscribe(topic);
this._internal.ws.subscribe(topic);
}

unsubscribe(topic: string): void {
this._internal.bun.ws.unsubscribe(topic);
this._internal.ws.unsubscribe(topic);
}

close(code?: number, reason?: string) {
this._internal.bun.ws.close(code, reason);
this._internal.ws.close(code, reason);
}

terminate() {
this._internal.bun.ws.terminate();
this._internal.ws.terminate();
}
}
Loading

0 comments on commit 2c7c8c6

Please sign in to comment.