mirror of
https://github.com/LukeHagar/crossws.git
synced 2025-12-06 04:19:26 +00:00
feat!: overhaul peer and message interface (#70)
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,4 +9,5 @@ dist
|
||||
.wrangler
|
||||
|
||||
/adapters
|
||||
/websocket
|
||||
websocket.d.ts
|
||||
|
||||
@@ -6,38 +6,38 @@ 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!
|
||||
|
||||
## Properties
|
||||
|
||||
### `peer.url`
|
||||
|
||||
Request http url during upgrade. You can use it to do actions based on path and search params.
|
||||
|
||||
### `peer.headers`
|
||||
|
||||
Request http headers during upgrade. Youb can use it to do authentication and access upgrade headers.
|
||||
|
||||
### `peer.addr`
|
||||
|
||||
The IP address of the client.
|
||||
## Instance properties
|
||||
|
||||
### `peer.id`
|
||||
|
||||
A unique id assigned to the peer.
|
||||
Unique random identifier ([uuid v4](https://developer.mozilla.org/en-US/docs/Glossary/UUID)) for the peer.
|
||||
|
||||
### `peer.readyState`
|
||||
### `peer.request?`
|
||||
|
||||
Client connection status (might be `undefined`)
|
||||
Access to the upgrade request info. You can use it to do authentication and access users headers and cookies.
|
||||
|
||||
:read-more{to="https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState" title="readyState in MDN"}
|
||||
> [!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.
|
||||
|
||||
## Methods
|
||||
### `peer.remoteAddress?`
|
||||
|
||||
### `peer.send(message, compress)`
|
||||
The IP address of the client.
|
||||
|
||||
> [!NOTE]
|
||||
> Not all adapters provide this. Refer to the [compatibility table](#compatibility) for more info.
|
||||
|
||||
### `peer.websocket`
|
||||
|
||||
Direct access to the [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) instance.
|
||||
|
||||
> [!NOTE]
|
||||
> WebSocket properties vary across runtimes. When accessing `peer.websocket`, a lightweight proxy increases stablity. Refer to the [compatibility table](#compatibility) for more info.
|
||||
|
||||
## Instance methods
|
||||
|
||||
### `peer.send(message, { compress? })`
|
||||
|
||||
Send a message to the connected client.
|
||||
|
||||
@@ -79,3 +79,44 @@ 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()` | ✓ | ⨉ | ✓ [^1] | ✓ [^1] | ✓ [^1] | ✓ | ✓ [^1] |
|
||||
| `close()` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| `terminate()` | ✓ | ✓ [^2] | ✓ | ✓ | ✓ | ✓ | ✓ [^2] |
|
||||
| `request` | ✓ | ✓ | ✓ [^3] | ✓ | ✓ [^3] | ✓ [^3] | ✓ |
|
||||
| `remoteAddress` | ✓ | ⨉ | ⨉ | ✓ | ✓ | ✓ | ⨉ |
|
||||
| `websocket.url` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| `websocket.extensions` | ✓ [^4] | ⨉ | ⨉ | ✓ [^4] | ✓ [^4] | ✓ [^4] | ⨉ |
|
||||
| `websocket.protocol` | ✓ [^5] | ✓ [^5] | ✓ [^5] | [^5] ✓ | ✓ [^5] | ✓ [^5] | ⨉ |
|
||||
| `websocket.readyState` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ [^6] | ✓ [^6] |
|
||||
| `websocket.binaryType` | ✓ [^7] | ⨉ | ⨉ | ✓ | ✓ [^7] | ✓ | ⨉ |
|
||||
| `websocket.bufferedAmount` | ⨉ | ⨉ | ⨉ | ✓ | ✓ | ✓ | ⨉ |
|
||||
|
||||
[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]: pubsub is not natively handled by runtime. peers are internally tracked.
|
||||
|
||||
[^2]: `close()` will be used for compatibility.
|
||||
|
||||
[^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.
|
||||
|
||||
[^3]: `request` is not always available (only in `open` hook).
|
||||
|
||||
[^4]: [`websocket.extensions`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/extensions) is polyfilled using [`sec-websocket-extensions`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism#websocket-specific_headers) request header.
|
||||
|
||||
[^5]: [`websocket.protocol`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/protocol) is polyfilled using [`sec-websocket-protocol`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism#websocket-specific_headers) request header.
|
||||
|
||||
[^6]: [`websocket.readyState`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState) is polyfilled by tracking open/close events.
|
||||
|
||||
[^7]: Some runtimes have non standard values including `"nodebuffer"` and `"uint8array"`. crossws auto converts them for [`message.data`](/guide/message#messagedata).
|
||||
|
||||
@@ -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`
|
||||
|
||||
Get stringified text version of the message
|
||||
Unique random identifier ([uuid v4](https://developer.mozilla.org/en-US/docs/Glossary/UUID)) for the message.
|
||||
|
||||
### `message.event`
|
||||
|
||||
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.isBinary`
|
||||
### `message.data`
|
||||
|
||||
Indicates if the message is binary (might be `undefined`)
|
||||
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
|
||||
|
||||
| | [Bun][bun] | [Cloudflare][cfw] | [Cloudflare (durable)][cfd] | [Deno][deno] | [Node (ws)][nodews] | [Node (μWebSockets)][nodeuws] | [SSE][sse] |
|
||||
| ------- | ---------- | ----------------- | --------------------------- | ------------ | ------------------- | ----------------------------- | ---------- |
|
||||
| `event` | ⨉ | ✓ | ⨉ | ✓ | ⨉ | ⨉ | ⨉ |
|
||||
|
||||
[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,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";
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -41,7 +39,6 @@ export default defineWebSocketAdapter<BunAdapter, BunOptions>(
|
||||
data: {
|
||||
server,
|
||||
request,
|
||||
requestUrl: request.url,
|
||||
} satisfies ContextData,
|
||||
headers: res?.headers,
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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 remoteAddress() {
|
||||
return this._internal.ws.remoteAddress;
|
||||
}
|
||||
|
||||
get readyState() {
|
||||
return this._internal.bun.ws.readyState as any;
|
||||
send(data: unknown, options?: { compress?: boolean }) {
|
||||
return this._internal.ws.send(toBufferLike(data), options?.compress);
|
||||
}
|
||||
|
||||
get url() {
|
||||
return this._internal.bun.ws.data.requestUrl || "/";
|
||||
}
|
||||
|
||||
get headers() {
|
||||
return this._internal.bun.ws.data.request?.headers;
|
||||
}
|
||||
|
||||
send(message: any, options?: { compress?: boolean }) {
|
||||
return this._internal.bun.ws.send(toBufferLike(message), 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AdapterOptions, AdapterInstance } from "../adapter.ts";
|
||||
import type * as web from "../../types/web.ts";
|
||||
import { toBufferLike } from "../utils.ts";
|
||||
import { defineWebSocketAdapter, adapterUtils } from "../adapter.ts";
|
||||
import { AdapterHookable } from "../hooks.ts";
|
||||
@@ -38,13 +39,13 @@ export interface CloudflareDurableAdapter extends AdapterInstance {
|
||||
|
||||
handleDurableMessage(
|
||||
obj: DurableObject,
|
||||
ws: WebSocket | CF.WebSocket,
|
||||
ws: WebSocket | CF.WebSocket | web.WebSocket,
|
||||
message: ArrayBuffer | string,
|
||||
): Promise<void>;
|
||||
|
||||
handleDurableClose(
|
||||
obj: DurableObject,
|
||||
ws: WebSocket | CF.WebSocket,
|
||||
ws: WebSocket | CF.WebSocket | web.WebSocket,
|
||||
code: number,
|
||||
reason: string,
|
||||
wasClean: boolean,
|
||||
@@ -102,7 +103,7 @@ export default defineWebSocketAdapter<
|
||||
handleDurableMessage: async (obj, ws, message) => {
|
||||
const peer = peerFromDurableEvent(obj, ws as CF.WebSocket);
|
||||
hooks.callAdapterHook("cloudflare:message", peer, message);
|
||||
hooks.callHook("message", peer, new Message(message));
|
||||
hooks.callHook("message", peer, new Message(message, peer));
|
||||
},
|
||||
handleDurableClose: async (obj, ws, code, reason, wasClean) => {
|
||||
const peer = peerFromDurableEvent(obj, ws as CF.WebSocket);
|
||||
@@ -124,12 +125,10 @@ function peerFromDurableEvent(
|
||||
return peer;
|
||||
}
|
||||
peer = ws._crosswsPeer = new CloudflareDurablePeer({
|
||||
cloudflare: {
|
||||
ws: ws as CF.WebSocket,
|
||||
request,
|
||||
env: (obj as DurableObjectPub).env,
|
||||
context: (obj as DurableObjectPub).ctx,
|
||||
},
|
||||
ws: ws as CF.WebSocket,
|
||||
request: request as Request,
|
||||
cfEnv: (obj as DurableObjectPub).env,
|
||||
cfCtx: (obj as DurableObjectPub).ctx,
|
||||
});
|
||||
return peer;
|
||||
}
|
||||
@@ -137,60 +136,24 @@ function peerFromDurableEvent(
|
||||
// --- peer ---
|
||||
|
||||
class CloudflareDurablePeer extends Peer<{
|
||||
ws: AugmentedWebSocket;
|
||||
request?: Request;
|
||||
peers?: never;
|
||||
cloudflare: {
|
||||
ws: AugmentedWebSocket;
|
||||
request?: Request | CF.Request;
|
||||
env: unknown;
|
||||
context: DurableObject["ctx"];
|
||||
};
|
||||
cfEnv: unknown;
|
||||
cfCtx: DurableObject["ctx"];
|
||||
}> {
|
||||
get url() {
|
||||
return (
|
||||
this._internal.cloudflare.request?.url ||
|
||||
this._internal.cloudflare.ws.url ||
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
get headers() {
|
||||
return this._internal.cloudflare.request?.headers as Headers;
|
||||
}
|
||||
|
||||
get readyState() {
|
||||
return this._internal.cloudflare.ws.readyState as -1 | 0 | 1 | 2 | 3;
|
||||
}
|
||||
|
||||
send(message: any) {
|
||||
this._internal.cloudflare.ws.send(toBufferLike(message));
|
||||
return 0;
|
||||
}
|
||||
|
||||
subscribe(topic: string): void {
|
||||
super.subscribe(topic);
|
||||
const state: CrosswsState = {
|
||||
// Max limit: 2,048 bytes
|
||||
...(this._internal.cloudflare.ws.deserializeAttachment() as CrosswsState),
|
||||
topics: this._topics,
|
||||
};
|
||||
this._internal.cloudflare.ws._crosswsState = state;
|
||||
this._internal.cloudflare.ws.serializeAttachment(state);
|
||||
}
|
||||
|
||||
get peers() {
|
||||
const clients =
|
||||
this._internal.cloudflare.context.getWebSockets() as unknown as (typeof this._internal.cloudflare.ws)[];
|
||||
this._internal.cfCtx.getWebSockets() as unknown as (typeof this._internal.ws)[];
|
||||
return new Set(
|
||||
clients.map((client) => {
|
||||
let peer = client._crosswsPeer;
|
||||
if (!peer) {
|
||||
peer = client._crosswsPeer = new CloudflareDurablePeer({
|
||||
cloudflare: {
|
||||
ws: client,
|
||||
request: undefined,
|
||||
env: this._internal.cloudflare.env,
|
||||
context: this._internal.cloudflare.context,
|
||||
},
|
||||
ws: client,
|
||||
request: undefined,
|
||||
cfEnv: this._internal.cfEnv,
|
||||
cfCtx: this._internal.cfCtx,
|
||||
});
|
||||
}
|
||||
return peer;
|
||||
@@ -198,14 +161,29 @@ class CloudflareDurablePeer extends Peer<{
|
||||
);
|
||||
}
|
||||
|
||||
publish(topic: string, message: any): void {
|
||||
send(data: unknown) {
|
||||
return this._internal.ws.send(toBufferLike(data));
|
||||
}
|
||||
|
||||
subscribe(topic: string): void {
|
||||
super.subscribe(topic);
|
||||
const state: CrosswsState = {
|
||||
// Max limit: 2,048 bytes
|
||||
...(this._internal.ws.deserializeAttachment() as CrosswsState),
|
||||
topics: this._topics,
|
||||
};
|
||||
this._internal.ws._crosswsState = state;
|
||||
this._internal.ws.serializeAttachment(state);
|
||||
}
|
||||
|
||||
publish(topic: string, data: unknown): void {
|
||||
const clients = (
|
||||
this._internal.cloudflare.context.getWebSockets() as unknown as (typeof this._internal.cloudflare.ws)[]
|
||||
).filter((c) => c !== this._internal.cloudflare.ws);
|
||||
this._internal.cfCtx.getWebSockets() as unknown as (typeof this._internal.ws)[]
|
||||
).filter((c) => c !== this._internal.ws);
|
||||
if (clients.length === 0) {
|
||||
return;
|
||||
}
|
||||
const data = toBufferLike(message);
|
||||
const dataBuff = toBufferLike(data);
|
||||
for (const client of clients) {
|
||||
let state = client._crosswsState;
|
||||
if (!state) {
|
||||
@@ -213,16 +191,12 @@ class CloudflareDurablePeer extends Peer<{
|
||||
client.deserializeAttachment() as CrosswsState;
|
||||
}
|
||||
if (state.topics?.has(topic)) {
|
||||
client.send(data);
|
||||
client.send(dataBuff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
close(code?: number, reason?: string) {
|
||||
this._internal.cloudflare.ws.close(code, reason);
|
||||
}
|
||||
|
||||
terminate(): void {
|
||||
this.close();
|
||||
this._internal.ws.close(code, reason);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +44,12 @@ export default defineWebSocketAdapter<CloudflareAdapter, CloudflareOptions>(
|
||||
const client = pair[0];
|
||||
const server = pair[1];
|
||||
const peer = new CloudflarePeer({
|
||||
ws: client,
|
||||
peers,
|
||||
cloudflare: { client, server, request, env, context },
|
||||
wsServer: server,
|
||||
request: request as unknown as Request,
|
||||
cfEnv: env,
|
||||
cfCtx: context,
|
||||
});
|
||||
peers.add(peer);
|
||||
server.accept();
|
||||
@@ -53,7 +57,11 @@ export default defineWebSocketAdapter<CloudflareAdapter, CloudflareOptions>(
|
||||
hooks.callHook("open", peer);
|
||||
server.addEventListener("message", (event) => {
|
||||
hooks.callAdapterHook("cloudflare:message", peer, event);
|
||||
hooks.callHook("message", peer, new Message(event.data));
|
||||
hooks.callHook(
|
||||
"message",
|
||||
peer,
|
||||
new Message(event.data, peer, event as MessageEvent),
|
||||
);
|
||||
});
|
||||
server.addEventListener("error", (event) => {
|
||||
peers.delete(peer);
|
||||
@@ -79,51 +87,23 @@ export default defineWebSocketAdapter<CloudflareAdapter, CloudflareOptions>(
|
||||
// --- peer ---
|
||||
|
||||
class CloudflarePeer extends Peer<{
|
||||
ws: _cf.WebSocket;
|
||||
request: Request;
|
||||
peers: Set<CloudflarePeer>;
|
||||
cloudflare: {
|
||||
client: _cf.WebSocket;
|
||||
server: _cf.WebSocket;
|
||||
request: _cf.Request;
|
||||
env: unknown;
|
||||
context: _cf.ExecutionContext;
|
||||
};
|
||||
wsServer: _cf.WebSocket;
|
||||
cfEnv: unknown;
|
||||
cfCtx: _cf.ExecutionContext;
|
||||
}> {
|
||||
get addr() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get url() {
|
||||
return this._internal.cloudflare.request.url;
|
||||
}
|
||||
|
||||
get headers() {
|
||||
return this._internal.cloudflare.request.headers as unknown as Headers;
|
||||
}
|
||||
|
||||
get readyState() {
|
||||
return this._internal.cloudflare.client.readyState as -1 | 0 | 1 | 2 | 3;
|
||||
}
|
||||
|
||||
send(message: any) {
|
||||
this._internal.cloudflare.server.send(toBufferLike(message));
|
||||
send(data: unknown) {
|
||||
this._internal.wsServer.send(toBufferLike(data));
|
||||
return 0;
|
||||
}
|
||||
|
||||
publish(_topic: string, _message: any): void {
|
||||
// Not supported
|
||||
// Throws: A hanging Promise was canceled
|
||||
// for (const peer of this._internal.peers) {
|
||||
// if (peer !== this && peer._topics.has(_topic)) {
|
||||
// peer.publish(_topic, _message);
|
||||
// }
|
||||
// }
|
||||
// not supported
|
||||
}
|
||||
|
||||
close(code?: number, reason?: string) {
|
||||
this._internal.cloudflare.client.close(code, reason);
|
||||
}
|
||||
|
||||
terminate(): void {
|
||||
this.close();
|
||||
this._internal.ws.close(code, reason);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,9 @@ declare global {
|
||||
}
|
||||
|
||||
type WebSocketUpgrade = import("@deno/types").Deno.WebSocketUpgrade;
|
||||
type ServeHandlerInfo = unknown; // TODO
|
||||
type ServeHandlerInfo = {
|
||||
remoteAddr?: { transport: string; hostname: string; port: number };
|
||||
};
|
||||
|
||||
// --- adapter ---
|
||||
|
||||
@@ -42,8 +44,10 @@ export default defineWebSocketAdapter<DenoAdapter, DenoOptions>(
|
||||
headers: res?.headers,
|
||||
});
|
||||
const peer = new DenoPeer({
|
||||
ws: upgrade.socket,
|
||||
request,
|
||||
peers,
|
||||
deno: { ws: upgrade.socket, request, info },
|
||||
denoInfo: info,
|
||||
});
|
||||
peers.add(peer);
|
||||
upgrade.socket.addEventListener("open", () => {
|
||||
@@ -52,7 +56,7 @@ export default defineWebSocketAdapter<DenoAdapter, DenoOptions>(
|
||||
});
|
||||
upgrade.socket.addEventListener("message", (event) => {
|
||||
hooks.callAdapterHook("deno:message", peer, event);
|
||||
hooks.callHook("message", peer, new Message(event.data));
|
||||
hooks.callHook("message", peer, new Message(event.data, peer, event));
|
||||
});
|
||||
upgrade.socket.addEventListener("close", () => {
|
||||
peers.delete(peer);
|
||||
@@ -73,50 +77,33 @@ export default defineWebSocketAdapter<DenoAdapter, DenoOptions>(
|
||||
// --- peer ---
|
||||
|
||||
class DenoPeer extends Peer<{
|
||||
ws: WebSocketUpgrade["socket"];
|
||||
request: Request;
|
||||
peers: Set<DenoPeer>;
|
||||
deno: {
|
||||
ws: WebSocketUpgrade["socket"];
|
||||
request: Request;
|
||||
info: ServeHandlerInfo;
|
||||
};
|
||||
denoInfo: ServeHandlerInfo;
|
||||
}> {
|
||||
get addr() {
|
||||
// @ts-expect-error types missing
|
||||
return this._internal.deno.ws.remoteAddress;
|
||||
get remoteAddress() {
|
||||
return this._internal.denoInfo.remoteAddr?.hostname;
|
||||
}
|
||||
|
||||
get readyState() {
|
||||
return this._internal.deno.ws.readyState as -1 | 0 | 1 | 2 | 3;
|
||||
send(data: unknown) {
|
||||
return this._internal.ws.send(toBufferLike(data));
|
||||
}
|
||||
|
||||
get url() {
|
||||
return this._internal.deno.request.url;
|
||||
}
|
||||
|
||||
get headers() {
|
||||
return this._internal.deno.request.headers || new Headers();
|
||||
}
|
||||
|
||||
send(message: any) {
|
||||
this._internal.deno.ws.send(toBufferLike(message));
|
||||
return 0;
|
||||
}
|
||||
|
||||
publish(topic: string, message: any) {
|
||||
const data = toBufferLike(message);
|
||||
publish(topic: string, data: unknown) {
|
||||
const dataBuff = toBufferLike(data);
|
||||
for (const peer of this._internal.peers) {
|
||||
if (peer !== this && peer._topics.has(topic)) {
|
||||
peer._internal.deno.ws.send(data);
|
||||
peer._internal.ws.send(dataBuff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
close(code?: number, reason?: string) {
|
||||
this._internal.deno.ws.close(code, reason);
|
||||
this._internal.ws.close(code, reason);
|
||||
}
|
||||
|
||||
terminate(): void {
|
||||
// @ts-ignore (terminate is Deno-only api)
|
||||
this._internal.deno.ws.terminate();
|
||||
(this._internal.ws as any).terminate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AdapterOptions, AdapterInstance } from "../adapter.ts";
|
||||
import type { WebSocket } from "../../types/web.ts";
|
||||
import { toBufferLike } from "../utils.ts";
|
||||
import { defineWebSocketAdapter, adapterUtils } from "../adapter.ts";
|
||||
import { AdapterHookable } from "../hooks.ts";
|
||||
@@ -11,14 +12,16 @@ import type { Duplex } from "node:stream";
|
||||
import { WebSocketServer as _WebSocketServer } from "ws";
|
||||
import type {
|
||||
ServerOptions,
|
||||
RawData,
|
||||
WebSocketServer,
|
||||
WebSocket as WebSocketT,
|
||||
} from "../../types/ws";
|
||||
|
||||
// --- types ---
|
||||
|
||||
type AugmentedReq = IncomingMessage & { _upgradeHeaders?: HeadersInit };
|
||||
type AugmentedReq = IncomingMessage & {
|
||||
_request: NodeReqProxy;
|
||||
_upgradeHeaders?: HeadersInit;
|
||||
};
|
||||
|
||||
export interface NodeAdapter extends AdapterInstance {
|
||||
handleUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer): void;
|
||||
@@ -46,18 +49,19 @@ export default defineWebSocketAdapter<NodeAdapter, NodeOptions>(
|
||||
...(options.serverOptions as any),
|
||||
}) as WebSocketServer);
|
||||
|
||||
wss.on("connection", (ws, req) => {
|
||||
const peer = new NodePeer({ peers, node: { ws, req, server: wss } });
|
||||
wss.on("connection", (ws, nodeReq) => {
|
||||
const request = new NodeReqProxy(nodeReq);
|
||||
const peer = new NodePeer({ ws, request, peers, nodeReq });
|
||||
peers.add(peer);
|
||||
hooks.callHook("open", peer);
|
||||
|
||||
// Managed socket-level events
|
||||
ws.on("message", (data: RawData, isBinary: boolean) => {
|
||||
ws.on("message", (data: unknown, isBinary: boolean) => {
|
||||
hooks.callAdapterHook("node:message", peer, data, isBinary);
|
||||
if (Array.isArray(data)) {
|
||||
data = Buffer.concat(data);
|
||||
}
|
||||
hooks.callHook("message", peer, new Message(data, isBinary));
|
||||
hooks.callHook("message", peer, new Message(data, peer));
|
||||
});
|
||||
ws.on("error", (error: Error) => {
|
||||
peers.delete(peer);
|
||||
@@ -94,7 +98,7 @@ export default defineWebSocketAdapter<NodeAdapter, NodeOptions>(
|
||||
});
|
||||
});
|
||||
|
||||
wss.on("headers", function (outgoingHeaders, req) {
|
||||
wss.on("headers", (outgoingHeaders, req) => {
|
||||
const upgradeHeaders = (req as AugmentedReq)._upgradeHeaders;
|
||||
if (upgradeHeaders) {
|
||||
for (const [key, value] of new Headers(upgradeHeaders)) {
|
||||
@@ -105,14 +109,16 @@ export default defineWebSocketAdapter<NodeAdapter, NodeOptions>(
|
||||
|
||||
return {
|
||||
...adapterUtils(peers),
|
||||
handleUpgrade: async (req, socket, head) => {
|
||||
const res = await hooks.callHook("upgrade", new NodeReqProxy(req));
|
||||
handleUpgrade: async (nodeReq, socket, head) => {
|
||||
const request = new NodeReqProxy(nodeReq);
|
||||
const res = await hooks.callHook("upgrade", request);
|
||||
if (res instanceof Response) {
|
||||
return sendResponse(socket, res);
|
||||
}
|
||||
(req as AugmentedReq)._upgradeHeaders = res?.headers;
|
||||
wss.handleUpgrade(req, socket, head, (ws) => {
|
||||
wss.emit("connection", ws, req);
|
||||
(nodeReq as AugmentedReq)._request = request;
|
||||
(nodeReq as AugmentedReq)._upgradeHeaders = res?.headers;
|
||||
wss.handleUpgrade(nodeReq, socket, head, (ws) => {
|
||||
wss.emit("connection", ws, nodeReq);
|
||||
});
|
||||
},
|
||||
closeAll: (code, data) => {
|
||||
@@ -128,49 +134,18 @@ export default defineWebSocketAdapter<NodeAdapter, NodeOptions>(
|
||||
|
||||
class NodePeer extends Peer<{
|
||||
peers: Set<NodePeer>;
|
||||
node: {
|
||||
server: WebSocketServer;
|
||||
req: IncomingMessage;
|
||||
ws: WebSocketT & { _peer?: NodePeer };
|
||||
};
|
||||
request: NodeReqProxy;
|
||||
nodeReq: IncomingMessage;
|
||||
ws: WebSocketT & { _peer?: NodePeer };
|
||||
}> {
|
||||
_req: NodeReqProxy;
|
||||
constructor(ctx: NodePeer["_internal"]) {
|
||||
super(ctx);
|
||||
this._req = new NodeReqProxy(ctx.node.req);
|
||||
ctx.node.ws._peer = this;
|
||||
get remoteAddress() {
|
||||
return this._internal.nodeReq.socket?.remoteAddress;
|
||||
}
|
||||
|
||||
get addr() {
|
||||
const socket = this._internal.node.req.socket;
|
||||
if (!socket) {
|
||||
return undefined;
|
||||
}
|
||||
const headers = this._internal.node.req.headers;
|
||||
let addr = headers["x-forwarded-for"] || socket.remoteAddress || "??";
|
||||
if (addr.includes(":")) {
|
||||
addr = `[${addr}]`;
|
||||
}
|
||||
const port = headers["x-forwarded-port"] || socket.remotePort || "??";
|
||||
return `${addr}:${port}`;
|
||||
}
|
||||
|
||||
get url() {
|
||||
return this._req.url;
|
||||
}
|
||||
|
||||
get headers() {
|
||||
return this._req.headers;
|
||||
}
|
||||
|
||||
get readyState() {
|
||||
return this._internal.node.ws.readyState;
|
||||
}
|
||||
|
||||
send(message: any, options?: { compress?: boolean }) {
|
||||
const data = toBufferLike(message);
|
||||
send(data: unknown, options?: { compress?: boolean }) {
|
||||
const dataBuff = toBufferLike(data);
|
||||
const isBinary = typeof data !== "string";
|
||||
this._internal.node.ws.send(data, {
|
||||
this._internal.ws.send(dataBuff, {
|
||||
compress: options?.compress,
|
||||
binary: isBinary,
|
||||
...options,
|
||||
@@ -178,8 +153,12 @@ class NodePeer extends Peer<{
|
||||
return 0;
|
||||
}
|
||||
|
||||
publish(topic: string, message: any, options?: { compress?: boolean }): void {
|
||||
const data = toBufferLike(message);
|
||||
publish(
|
||||
topic: string,
|
||||
data: unknown,
|
||||
options?: { compress?: boolean },
|
||||
): void {
|
||||
const dataBuff = toBufferLike(data);
|
||||
const isBinary = typeof data !== "string";
|
||||
const sendOptions = {
|
||||
compress: options?.compress,
|
||||
@@ -188,17 +167,17 @@ class NodePeer extends Peer<{
|
||||
};
|
||||
for (const peer of this._internal.peers) {
|
||||
if (peer !== this && peer._topics.has(topic)) {
|
||||
peer._internal.node.ws.send(data, sendOptions);
|
||||
peer._internal.ws.send(dataBuff, sendOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
close(code?: number, data?: string | Buffer) {
|
||||
this._internal.node.ws.close(code, data);
|
||||
this._internal.ws.close(code, data);
|
||||
}
|
||||
|
||||
terminate() {
|
||||
this._internal.node.ws.terminate();
|
||||
this._internal.ws.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AdapterOptions, AdapterInstance } from "../adapter.ts";
|
||||
import type * as web from "../../types/web.ts";
|
||||
import { toString } from "../utils.ts";
|
||||
import { defineWebSocketAdapter, adapterUtils } from "../adapter.ts";
|
||||
import { AdapterHookable } from "../hooks.ts";
|
||||
@@ -43,7 +44,7 @@ export default defineWebSocketAdapter<SSEAdapter, SSEOptions>((opts = {}) => {
|
||||
const stream = request.body.pipeThrough(new TextDecoderStream());
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
hooks.callHook("message", peer, new Message(chunk));
|
||||
hooks.callHook("message", peer, new Message(chunk, peer));
|
||||
}
|
||||
} catch {
|
||||
await stream.cancel().catch(() => {});
|
||||
@@ -52,18 +53,13 @@ export default defineWebSocketAdapter<SSEAdapter, SSEOptions>((opts = {}) => {
|
||||
return new Response(null, {});
|
||||
} else {
|
||||
// Add a new peer
|
||||
const ws = new SSEWebSocketStub();
|
||||
peer = new SSEPeer({
|
||||
peers,
|
||||
sse: {
|
||||
request,
|
||||
hooks,
|
||||
onClose: () => {
|
||||
peers.delete(peer);
|
||||
if (opts.bidir) {
|
||||
peersMap!.delete(peer.id);
|
||||
}
|
||||
},
|
||||
},
|
||||
peersMap,
|
||||
request,
|
||||
hooks,
|
||||
ws,
|
||||
});
|
||||
peers.add(peer);
|
||||
if (opts.bidir) {
|
||||
@@ -96,37 +92,36 @@ export default defineWebSocketAdapter<SSEAdapter, SSEOptions>((opts = {}) => {
|
||||
|
||||
class SSEPeer extends Peer<{
|
||||
peers: Set<SSEPeer>;
|
||||
sse: {
|
||||
request: Request;
|
||||
hooks: AdapterHookable;
|
||||
onClose: (peer: SSEPeer) => void;
|
||||
};
|
||||
peersMap?: Map<string, SSEPeer>;
|
||||
request: Request;
|
||||
ws: SSEWebSocketStub;
|
||||
hooks: AdapterHookable;
|
||||
}> {
|
||||
_sseStream: ReadableStream;
|
||||
_sseStream: ReadableStream; // server -> client
|
||||
_sseStreamController?: ReadableStreamDefaultController;
|
||||
|
||||
constructor(internal: SSEPeer["_internal"]) {
|
||||
super(internal);
|
||||
constructor(_internal: SSEPeer["_internal"]) {
|
||||
super(_internal);
|
||||
_internal.ws.readyState = 0 /* CONNECTING */;
|
||||
this._sseStream = new ReadableStream({
|
||||
start: (controller) => {
|
||||
_internal.ws.readyState = 1 /* OPEN */;
|
||||
this._sseStreamController = controller;
|
||||
this._internal.sse.hooks.callHook("open", this);
|
||||
_internal.hooks.callHook("open", this);
|
||||
},
|
||||
cancel: () => {
|
||||
this._internal.sse.onClose(this);
|
||||
this._internal.sse.hooks.callHook("close", this);
|
||||
_internal.ws.readyState = 2 /* CLOSING */;
|
||||
_internal.peers.delete(this);
|
||||
_internal.peersMap?.delete(this.id);
|
||||
Promise.resolve(this._internal.hooks.callHook("close", this)).finally(
|
||||
() => {
|
||||
_internal.ws.readyState = 3 /* CLOSED */;
|
||||
},
|
||||
);
|
||||
},
|
||||
}).pipeThrough(new TextEncoderStream());
|
||||
}
|
||||
|
||||
get url() {
|
||||
return this._internal.sse.request.url;
|
||||
}
|
||||
|
||||
get headers() {
|
||||
return this._internal.sse.request.headers;
|
||||
}
|
||||
|
||||
_sendEvent(event: string, data: string) {
|
||||
const lines = data.split("\n");
|
||||
this._sseStreamController?.enqueue(
|
||||
@@ -134,16 +129,16 @@ class SSEPeer extends Peer<{
|
||||
);
|
||||
}
|
||||
|
||||
send(message: any) {
|
||||
this._sendEvent("message", toString(message));
|
||||
send(data: unknown) {
|
||||
this._sendEvent("message", toString(data));
|
||||
return 0;
|
||||
}
|
||||
|
||||
publish(topic: string, message: any) {
|
||||
const data = toString(message);
|
||||
publish(topic: string, data: unknown) {
|
||||
const dataBuff = toString(data);
|
||||
for (const peer of this._internal.peers) {
|
||||
if (peer !== this && peer._topics.has(topic)) {
|
||||
peer._sendEvent("message", data);
|
||||
peer._sendEvent("message", dataBuff);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,8 +146,10 @@ class SSEPeer extends Peer<{
|
||||
close() {
|
||||
this._sseStreamController?.close();
|
||||
}
|
||||
|
||||
terminate() {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
// --- web compat ---
|
||||
|
||||
class SSEWebSocketStub implements Partial<web.WebSocket> {
|
||||
readyState?: number | undefined;
|
||||
}
|
||||
|
||||
@@ -1,28 +1,23 @@
|
||||
import type { AdapterOptions, AdapterInstance } from "../adapter.ts";
|
||||
import type { WebSocket } from "../../types/web.ts";
|
||||
import type uws from "uWebSockets.js";
|
||||
import { toBufferLike } from "../utils.ts";
|
||||
import { defineWebSocketAdapter, adapterUtils } from "../adapter.ts";
|
||||
import { AdapterHookable } from "../hooks.ts";
|
||||
import { Message } from "../message.ts";
|
||||
import { Peer } from "../peer.ts";
|
||||
|
||||
import type {
|
||||
WebSocketBehavior,
|
||||
WebSocket,
|
||||
HttpRequest,
|
||||
HttpResponse,
|
||||
RecognizedString,
|
||||
} from "uWebSockets.js";
|
||||
|
||||
// --- types ---
|
||||
|
||||
type UserData = {
|
||||
_peer?: any;
|
||||
req: HttpRequest;
|
||||
res: HttpResponse;
|
||||
context: any;
|
||||
peer?: UWSPeer;
|
||||
req: uws.HttpRequest;
|
||||
res: uws.HttpResponse;
|
||||
protocol: string;
|
||||
extensions: string;
|
||||
};
|
||||
|
||||
type WebSocketHandler = WebSocketBehavior<UserData>;
|
||||
type WebSocketHandler = uws.WebSocketBehavior<UserData>;
|
||||
|
||||
export interface UWSAdapter extends AdapterInstance {
|
||||
websocket: WebSocketHandler;
|
||||
@@ -30,7 +25,7 @@ export interface UWSAdapter extends AdapterInstance {
|
||||
|
||||
export interface UWSOptions extends AdapterOptions {
|
||||
uws?: Exclude<
|
||||
WebSocketBehavior<any>,
|
||||
uws.WebSocketBehavior<any>,
|
||||
| "close"
|
||||
| "drain"
|
||||
| "message"
|
||||
@@ -56,12 +51,16 @@ export default defineWebSocketAdapter<UWSAdapter, UWSOptions>(
|
||||
...options.uws,
|
||||
close(ws, code, message) {
|
||||
const peer = getPeer(ws, peers);
|
||||
((peer as any)._internal.ws as UwsWebSocketProxy).readyState =
|
||||
2 /* CLOSING */;
|
||||
peers.delete(peer);
|
||||
hooks.callAdapterHook("uws:close", peer, ws, code, message);
|
||||
hooks.callHook("close", peer, {
|
||||
code,
|
||||
reason: message?.toString(),
|
||||
});
|
||||
((peer as any)._internal.ws as UwsWebSocketProxy).readyState =
|
||||
3 /* CLOSED */;
|
||||
},
|
||||
drain(ws) {
|
||||
const peer = getPeer(ws, peers);
|
||||
@@ -70,8 +69,7 @@ export default defineWebSocketAdapter<UWSAdapter, UWSOptions>(
|
||||
message(ws, message, isBinary) {
|
||||
const peer = getPeer(ws, peers);
|
||||
hooks.callAdapterHook("uws:message", peer, ws, message, isBinary);
|
||||
const msg = new Message(message, isBinary);
|
||||
hooks.callHook("message", peer, msg);
|
||||
hooks.callHook("message", peer, new Message(message, peer));
|
||||
},
|
||||
open(ws) {
|
||||
const peer = getPeer(ws, peers);
|
||||
@@ -131,16 +129,21 @@ export default defineWebSocketAdapter<UWSAdapter, UWSOptions>(
|
||||
res.writeHeader(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
res.cork(() => {
|
||||
const key = req.getHeader("sec-websocket-key");
|
||||
const protocol = req.getHeader("sec-websocket-protocol");
|
||||
const extensions = req.getHeader("sec-websocket-extensions");
|
||||
res.upgrade(
|
||||
{
|
||||
req,
|
||||
res,
|
||||
context,
|
||||
protocol,
|
||||
extensions,
|
||||
},
|
||||
req.getHeader("sec-websocket-key"),
|
||||
req.getHeader("sec-websocket-protocol"),
|
||||
req.getHeader("sec-websocket-extensions"),
|
||||
key,
|
||||
protocol,
|
||||
extensions,
|
||||
context,
|
||||
);
|
||||
});
|
||||
@@ -152,73 +155,63 @@ export default defineWebSocketAdapter<UWSAdapter, UWSOptions>(
|
||||
|
||||
// --- peer ---
|
||||
|
||||
function getPeer(ws: WebSocket<UserData>, peers: Set<UWSPeer>): UWSPeer {
|
||||
const userData = ws.getUserData();
|
||||
if (userData._peer) {
|
||||
return userData._peer as UWSPeer;
|
||||
function getPeer(uws: uws.WebSocket<UserData>, peers: Set<UWSPeer>): UWSPeer {
|
||||
const uwsData = uws.getUserData();
|
||||
if (uwsData.peer) {
|
||||
return uwsData.peer;
|
||||
}
|
||||
const peer = new UWSPeer({ peers, uws: { ws, userData } });
|
||||
userData._peer = peer;
|
||||
const peer = new UWSPeer({
|
||||
peers,
|
||||
uws,
|
||||
ws: new UwsWebSocketProxy(uws),
|
||||
request: new UWSReqProxy(uwsData.req),
|
||||
uwsData,
|
||||
});
|
||||
uwsData.peer = peer;
|
||||
return peer;
|
||||
}
|
||||
|
||||
class UWSPeer extends Peer<{
|
||||
peers: Set<UWSPeer>;
|
||||
uws: {
|
||||
ws: WebSocket<UserData>;
|
||||
userData: UserData;
|
||||
};
|
||||
request: UWSReqProxy;
|
||||
uws: uws.WebSocket<UserData>;
|
||||
ws: UwsWebSocketProxy;
|
||||
uwsData: UserData;
|
||||
}> {
|
||||
_decoder = new TextDecoder();
|
||||
_req: UWSReqProxy;
|
||||
|
||||
constructor(ctx: UWSPeer["_internal"]) {
|
||||
super(ctx);
|
||||
this._req = new UWSReqProxy(ctx.uws.userData.req);
|
||||
}
|
||||
|
||||
get addr() {
|
||||
get remoteAddress() {
|
||||
try {
|
||||
const addr = this._decoder.decode(
|
||||
this._internal.uws.ws?.getRemoteAddressAsText(),
|
||||
const addr = new TextDecoder().decode(
|
||||
this._internal.uws.getRemoteAddressAsText(),
|
||||
);
|
||||
return addr.replace(/(0000:)+/, "");
|
||||
return addr;
|
||||
} catch {
|
||||
// Error: Invalid access of closed uWS.WebSocket/SSLWebSocket.
|
||||
}
|
||||
}
|
||||
|
||||
get url() {
|
||||
return this._req.url;
|
||||
}
|
||||
|
||||
get headers() {
|
||||
return this._req.headers;
|
||||
}
|
||||
|
||||
send(message: any, options?: { compress?: boolean }) {
|
||||
const data = toBufferLike(message);
|
||||
send(data: unknown, options?: { compress?: boolean }) {
|
||||
const dataBuff = toBufferLike(data);
|
||||
const isBinary = typeof data !== "string";
|
||||
return this._internal.uws.ws.send(data, isBinary, options?.compress);
|
||||
return this._internal.uws.send(dataBuff, isBinary, options?.compress);
|
||||
}
|
||||
|
||||
subscribe(topic: string): void {
|
||||
this._internal.uws.ws.subscribe(topic);
|
||||
this._internal.uws.subscribe(topic);
|
||||
}
|
||||
|
||||
publish(topic: string, message: string, options?: { compress?: boolean }) {
|
||||
const data = toBufferLike(message);
|
||||
const isBinary = typeof data !== "string";
|
||||
this._internal.uws.ws.publish(topic, data, isBinary, options?.compress);
|
||||
this._internal.uws.publish(topic, data, isBinary, options?.compress);
|
||||
return 0;
|
||||
}
|
||||
|
||||
close(code?: number, reason?: RecognizedString) {
|
||||
this._internal.uws.ws.end(code, reason);
|
||||
close(code?: number, reason?: uws.RecognizedString) {
|
||||
this._internal.uws.end(code, reason);
|
||||
}
|
||||
|
||||
terminate(): void {
|
||||
this._internal.uws.ws.close();
|
||||
this._internal.uws.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,11 +220,10 @@ class UWSPeer extends Peer<{
|
||||
class UWSReqProxy {
|
||||
private _headers?: Headers;
|
||||
private _rawHeaders: [string, string][] = [];
|
||||
|
||||
url: string;
|
||||
|
||||
constructor(_req: HttpRequest) {
|
||||
// We need to precompute values since uws doesn't provide them after handler.
|
||||
|
||||
constructor(_req: uws.HttpRequest) {
|
||||
// Headers
|
||||
let host = "localhost";
|
||||
let proto = "http";
|
||||
@@ -244,7 +236,6 @@ class UWSReqProxy {
|
||||
}
|
||||
this._rawHeaders.push([key, value]);
|
||||
});
|
||||
|
||||
// URL
|
||||
const query = _req.getQuery();
|
||||
const pathname = _req.getUrl();
|
||||
@@ -258,3 +249,21 @@ class UWSReqProxy {
|
||||
return this._headers;
|
||||
}
|
||||
}
|
||||
|
||||
class UwsWebSocketProxy implements Partial<WebSocket> {
|
||||
readyState?: number = 1 /* OPEN */;
|
||||
|
||||
constructor(private _uws: uws.WebSocket<UserData>) {}
|
||||
|
||||
get bufferedAmount() {
|
||||
return this._uws?.getBufferedAmount();
|
||||
}
|
||||
|
||||
get protocol() {
|
||||
return this._uws?.getUserData().protocol;
|
||||
}
|
||||
|
||||
get extensions() {
|
||||
return this._uws?.getUserData().extensions;
|
||||
}
|
||||
}
|
||||
|
||||
207
src/message.ts
207
src/message.ts
@@ -1,27 +1,202 @@
|
||||
import { toBufferLike } from "./utils.ts";
|
||||
import type { Peer } from "./peer.ts";
|
||||
import { randomUUID } from "uncrypto";
|
||||
|
||||
export class Message {
|
||||
constructor(
|
||||
public readonly rawData: any,
|
||||
public readonly isBinary?: boolean,
|
||||
) {}
|
||||
export class Message implements Partial<MessageEvent> {
|
||||
/** Access to the original [message event](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/message_event) if available. */
|
||||
readonly event?: MessageEvent;
|
||||
|
||||
text(): string {
|
||||
if (typeof this.rawData === "string") {
|
||||
return this.rawData;
|
||||
}
|
||||
const buff = toBufferLike(this.rawData);
|
||||
if (typeof buff === "string") {
|
||||
return buff;
|
||||
}
|
||||
return new TextDecoder().decode(buff);
|
||||
/** Access to the Peer that emitted the message. */
|
||||
readonly peer?: Peer;
|
||||
|
||||
/** Raw message data (can be of any type). */
|
||||
readonly rawData: unknown;
|
||||
|
||||
#id?: string;
|
||||
#uint8Array?: Uint8Array;
|
||||
#arrayBuffer?: ArrayBuffer | SharedArrayBuffer;
|
||||
#blob?: Blob;
|
||||
#text?: string;
|
||||
#json?: unknown;
|
||||
|
||||
constructor(rawData: unknown, peer: Peer, event?: MessageEvent) {
|
||||
this.rawData = rawData || "";
|
||||
this.peer = peer;
|
||||
this.event = event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unique random [uuid v4](https://developer.mozilla.org/en-US/docs/Glossary/UUID) identifier for the message.
|
||||
*/
|
||||
get id(): string {
|
||||
if (!this.#id) {
|
||||
this.#id = randomUUID();
|
||||
}
|
||||
return this.#id;
|
||||
}
|
||||
|
||||
// --- data views ---
|
||||
|
||||
/**
|
||||
* 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 and encoded.
|
||||
*/
|
||||
uint8Array() {
|
||||
// Cached
|
||||
const _uint8Array = this.#uint8Array;
|
||||
if (_uint8Array) {
|
||||
return _uint8Array;
|
||||
}
|
||||
const rawData = this.rawData;
|
||||
// Uint8Array
|
||||
if (rawData instanceof Uint8Array) {
|
||||
return (this.#uint8Array = rawData);
|
||||
}
|
||||
// ArrayBuffer
|
||||
if (
|
||||
rawData instanceof ArrayBuffer ||
|
||||
rawData instanceof SharedArrayBuffer
|
||||
) {
|
||||
this.#arrayBuffer = rawData;
|
||||
return (this.#uint8Array = new Uint8Array(rawData));
|
||||
}
|
||||
// String
|
||||
if (typeof rawData === "string") {
|
||||
this.#text = rawData;
|
||||
return (this.#uint8Array = new TextEncoder().encode(this.#text));
|
||||
}
|
||||
// Iterable and ArrayLike
|
||||
if (Symbol.iterator in (rawData as Iterable<number>)) {
|
||||
return (this.#uint8Array = new Uint8Array(rawData as Iterable<number>));
|
||||
}
|
||||
if (typeof (rawData as ArrayLike<number>)?.length === "number") {
|
||||
return (this.#uint8Array = new Uint8Array(rawData as ArrayLike<number>));
|
||||
}
|
||||
// DataView
|
||||
if (rawData instanceof DataView) {
|
||||
return (this.#uint8Array = new Uint8Array(
|
||||
rawData.buffer,
|
||||
rawData.byteOffset,
|
||||
rawData.byteLength,
|
||||
));
|
||||
}
|
||||
throw new TypeError(
|
||||
`Unsupported message type: ${Object.prototype.toString.call(rawData)}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 and encoded.
|
||||
*/
|
||||
arrayBuffer(): ArrayBuffer | SharedArrayBuffer {
|
||||
// Cached
|
||||
const _arrayBuffer = this.#arrayBuffer;
|
||||
if (_arrayBuffer) {
|
||||
return _arrayBuffer;
|
||||
}
|
||||
const rawData = this.rawData;
|
||||
// Use as-is
|
||||
if (
|
||||
rawData instanceof ArrayBuffer ||
|
||||
rawData instanceof SharedArrayBuffer
|
||||
) {
|
||||
return (this.#arrayBuffer = rawData);
|
||||
}
|
||||
// Fallback to UInt8Array
|
||||
return (this.#arrayBuffer = this.uint8Array().buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 and encoded. */
|
||||
blob(): Blob {
|
||||
// Cached
|
||||
const _blob = this.#blob;
|
||||
if (_blob) {
|
||||
return _blob;
|
||||
}
|
||||
const rawData = this.rawData;
|
||||
// Use as-is
|
||||
if (rawData instanceof Blob) {
|
||||
return (this.#blob = rawData);
|
||||
}
|
||||
// Fallback to UInt8Array
|
||||
return (this.#blob = new Blob([this.uint8Array()]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stringified text version of the message.
|
||||
*
|
||||
* If raw data is in any other format, it will be automatically converted and decoded.
|
||||
*/
|
||||
text(): string {
|
||||
// Cached
|
||||
const _text = this.#text;
|
||||
if (_text) {
|
||||
return _text;
|
||||
}
|
||||
const rawData = this.rawData;
|
||||
// Use as-is
|
||||
if (typeof rawData === "string") {
|
||||
return (this.#text = rawData);
|
||||
}
|
||||
// Fallback to UInt8Array
|
||||
return (this.#text = new TextDecoder().decode(this.uint8Array()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parsed version of the message text with [`JSON.parse()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse).
|
||||
*/
|
||||
json<T = unknown>(): T {
|
||||
const _json = this.#json;
|
||||
if (_json) {
|
||||
return _json as T;
|
||||
}
|
||||
return (this.#json = JSON.parse(this.text()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Message data (value varies based on `peer.websocket.binaryType`).
|
||||
*/
|
||||
get data() {
|
||||
switch (this.peer?.websocket?.binaryType as string) {
|
||||
case "arraybuffer": {
|
||||
return this.arrayBuffer();
|
||||
}
|
||||
case "blob": {
|
||||
return this.blob();
|
||||
}
|
||||
case "nodebuffer": {
|
||||
return globalThis.Buffer
|
||||
? Buffer.from(this.uint8Array())
|
||||
: this.uint8Array();
|
||||
}
|
||||
case "uint8array": {
|
||||
return this.uint8Array();
|
||||
}
|
||||
case "text": {
|
||||
return this.text();
|
||||
}
|
||||
default: {
|
||||
return this.rawData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- inspect ---
|
||||
|
||||
toString() {
|
||||
return this.text();
|
||||
}
|
||||
|
||||
[Symbol.for("nodejs.util.inspect.custom")]() {
|
||||
[Symbol.toPrimitive]() {
|
||||
return this.text();
|
||||
}
|
||||
|
||||
[Symbol.for("nodejs.util.inspect.custom")]() {
|
||||
return { data: this.rawData };
|
||||
}
|
||||
}
|
||||
|
||||
157
src/peer.ts
157
src/peer.ts
@@ -1,111 +1,142 @@
|
||||
import type * as web from "../types/web.ts";
|
||||
import { randomUUID } from "uncrypto";
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState
|
||||
type ReadyState = 0 | 1 | 2 | 3;
|
||||
const ReadyStateMap = {
|
||||
"-1": "unknown",
|
||||
0: "connecting",
|
||||
1: "open",
|
||||
2: "closing",
|
||||
3: "closed",
|
||||
} as const;
|
||||
|
||||
export interface AdapterInternal {
|
||||
ws: unknown;
|
||||
request?: Request | Partial<Request>;
|
||||
peers?: Set<Peer>;
|
||||
}
|
||||
|
||||
export abstract class Peer<Internal extends AdapterInternal = AdapterInternal> {
|
||||
protected _internal: Internal;
|
||||
protected _topics: Set<string>;
|
||||
|
||||
private _id?: string;
|
||||
#id?: string;
|
||||
#ws?: Partial<web.WebSocket>;
|
||||
|
||||
constructor(internal: Internal) {
|
||||
this._topics = new Set();
|
||||
this._internal = internal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unique random [uuid v4](https://developer.mozilla.org/en-US/docs/Glossary/UUID) identifier for the peer.
|
||||
*/
|
||||
get id(): string {
|
||||
if (!this._id) {
|
||||
this._id = randomUUID();
|
||||
if (!this.#id) {
|
||||
this.#id = randomUUID();
|
||||
}
|
||||
return this._id;
|
||||
return this.#id;
|
||||
}
|
||||
|
||||
get addr(): string | undefined {
|
||||
/** IP address of the peer */
|
||||
get remoteAddress(): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get url(): string {
|
||||
return "";
|
||||
/** upgrade request */
|
||||
get request(): Request | Partial<Request> | undefined {
|
||||
return this._internal.request;
|
||||
}
|
||||
|
||||
get headers(): Headers | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get readyState(): ReadyState | -1 {
|
||||
return -1;
|
||||
/**
|
||||
* Get the [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) instance.
|
||||
*
|
||||
* **Note:** crossws adds polyfill for the following properties if native values are not available:
|
||||
* - `protocol`: Extracted from the `sec-websocket-protocol` header.
|
||||
* - `extensions`: Extracted from the `sec-websocket-extensions` header.
|
||||
* - `url`: Extracted from the request URL (http -> ws).
|
||||
* */
|
||||
get websocket(): Partial<web.WebSocket> {
|
||||
if (!this.#ws) {
|
||||
const _ws = this._internal.ws as Partial<web.WebSocket>;
|
||||
const _request = this._internal.request;
|
||||
this.#ws = _request ? createWsProxy(_ws, _request) : _ws;
|
||||
}
|
||||
return this.#ws;
|
||||
}
|
||||
|
||||
/** All connected peers to the server */
|
||||
get peers(): Set<Peer> {
|
||||
return this._internal.peers || new Set();
|
||||
}
|
||||
|
||||
abstract send(message: any, options?: { compress?: boolean }): number;
|
||||
abstract close(code?: number, reason?: string): void;
|
||||
|
||||
abstract publish(
|
||||
topic: string,
|
||||
message: any,
|
||||
options?: { compress?: boolean },
|
||||
): void;
|
||||
/** Abruptly close the connection */
|
||||
terminate() {
|
||||
this.close();
|
||||
}
|
||||
|
||||
/** Subscribe to a topic */
|
||||
subscribe(topic: string) {
|
||||
this._topics.add(topic);
|
||||
}
|
||||
|
||||
/** Unsubscribe from a topic */
|
||||
unsubscribe(topic: string) {
|
||||
this._topics.delete(topic);
|
||||
}
|
||||
|
||||
/** Send a message to the peer. */
|
||||
abstract send(
|
||||
data: unknown,
|
||||
options?: { compress?: boolean },
|
||||
): number | void | undefined;
|
||||
|
||||
/** Send message to subscribes of topic */
|
||||
abstract publish(
|
||||
topic: string,
|
||||
data: unknown,
|
||||
options?: { compress?: boolean },
|
||||
): void;
|
||||
|
||||
// --- inspect ---
|
||||
|
||||
toString() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
[Symbol.for("nodejs.util.inspect.custom")]() {
|
||||
const _id = this.toString();
|
||||
const _addr = this.addr ? ` (${this.addr})` : "";
|
||||
const _state =
|
||||
this.readyState === 1 || this.readyState === -1
|
||||
? ""
|
||||
: ` [${ReadyStateMap[this.readyState]}]`;
|
||||
|
||||
return `${_id}${_addr}${_state}`;
|
||||
[Symbol.toPrimitive]() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the connection.
|
||||
*
|
||||
* Here is a list of close codes:
|
||||
*
|
||||
* - `1000` means "normal closure" (default)
|
||||
* - `1009` means a message was too big and was rejected
|
||||
* - `1011` means the server encountered an error
|
||||
* - `1012` means the server is restarting
|
||||
* - `1013` means the server is too busy or the client is rate-limited
|
||||
* - `4000` through `4999` are reserved for applications (you can use it!)
|
||||
*
|
||||
* To close the connection abruptly, use `terminate()`.
|
||||
*
|
||||
* @param code The close code to send
|
||||
* @param reason The close reason to send
|
||||
*/
|
||||
abstract close(code?: number, reason?: string): void;
|
||||
[Symbol.toStringTag]() {
|
||||
return "WebSocket";
|
||||
}
|
||||
|
||||
/**
|
||||
* Abruptly close the connection.
|
||||
*
|
||||
* To gracefully close the connection, use `close()`.
|
||||
*/
|
||||
abstract terminate(): void;
|
||||
[Symbol.for("nodejs.util.inspect.custom")]() {
|
||||
return Object.fromEntries(
|
||||
[
|
||||
["id", this.id],
|
||||
["remoteAddress", this.remoteAddress],
|
||||
["peers", this.peers],
|
||||
["webSocket", this.websocket],
|
||||
].filter((p) => p[1]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function createWsProxy(
|
||||
ws: Partial<web.WebSocket>,
|
||||
request: Partial<Request>,
|
||||
): Partial<web.WebSocket> {
|
||||
return new Proxy(ws, {
|
||||
get: (target, prop) => {
|
||||
const value = Reflect.get(target, prop);
|
||||
if (!value) {
|
||||
switch (prop) {
|
||||
case "protocol": {
|
||||
return request?.headers?.get("sec-websocket-protocol") || "";
|
||||
}
|
||||
case "extensions": {
|
||||
return request?.headers?.get("sec-websocket-extensions") || "";
|
||||
}
|
||||
case "url": {
|
||||
return request?.url?.replace(/^http/, "ws") || undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ export function wsConnect(
|
||||
headers: opts?.headers,
|
||||
dispatcher: inspector,
|
||||
});
|
||||
ws.binaryType = "arraybuffer";
|
||||
|
||||
websockets.add(ws);
|
||||
|
||||
const send = async (data: any): Promise<any> => {
|
||||
@@ -51,13 +53,22 @@ export function wsConnect(
|
||||
nextIndex += count;
|
||||
};
|
||||
|
||||
ws.addEventListener("message", (event) => {
|
||||
const str =
|
||||
typeof event.data === "string"
|
||||
? event.data
|
||||
: new TextDecoder().decode(event.data);
|
||||
const payload = str[0] === "{" ? JSON.parse(str) : str;
|
||||
ws.addEventListener("message", async (event) => {
|
||||
let text: string;
|
||||
if (typeof event.data === "string") {
|
||||
text = event.data;
|
||||
} else {
|
||||
let rawData = event.data;
|
||||
if (rawData instanceof Blob) {
|
||||
rawData = await event.data.arrayBuffer();
|
||||
} else if (rawData instanceof Uint8Array) {
|
||||
rawData = rawData.buffer;
|
||||
}
|
||||
text = new TextDecoder().decode(rawData);
|
||||
}
|
||||
const payload = text[0] === "{" ? JSON.parse(text) : text;
|
||||
messages.push(payload);
|
||||
|
||||
const index = messages.length - 1;
|
||||
if (waitCallbacks[index]) {
|
||||
waitCallbacks[index](payload);
|
||||
|
||||
@@ -27,9 +27,19 @@ export function createDemo<T extends Adapter<any, any>>(
|
||||
case "debug": {
|
||||
peer.send({
|
||||
id: peer.id,
|
||||
ip: peer.addr,
|
||||
url: peer.url,
|
||||
headers: Object.fromEntries(peer.headers || []),
|
||||
remoteAddress: peer.remoteAddress,
|
||||
request: {
|
||||
url: peer.request?.url,
|
||||
headers: Object.fromEntries(peer.request?.headers || []),
|
||||
},
|
||||
websocket: {
|
||||
readyState: peer.websocket.readyState,
|
||||
protocol: peer.websocket.protocol,
|
||||
extensions: peer.websocket.extensions,
|
||||
url: peer.websocket.url,
|
||||
binaryType: peer.websocket.binaryType,
|
||||
bufferedAmount: peer.websocket.bufferedAmount,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// You can run this demo using `deno run -A ./deno.ts` or `npm run play:deno` in repo
|
||||
// You can run this demo using `npm run play:deno` in repo
|
||||
|
||||
import denoAdapter from "../../src/adapters/deno.ts";
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { wsConnect } from "./_utils";
|
||||
|
||||
export function wsTests(
|
||||
getURL: () => string,
|
||||
opts: { adapter: string; pubsub?: boolean; resHeaders?: boolean },
|
||||
) {
|
||||
export interface WSTestOpts {
|
||||
adapter: string;
|
||||
pubsub?: boolean;
|
||||
resHeaders?: boolean;
|
||||
}
|
||||
|
||||
export function wsTests(getURL: () => string, opts: WSTestOpts) {
|
||||
test("http works", async () => {
|
||||
const response = await fetch(getURL().replace("ws", "http"));
|
||||
expect(response.status).toBe(200);
|
||||
@@ -60,28 +63,50 @@ export function wsTests(
|
||||
},
|
||||
);
|
||||
|
||||
test("upgrade request headers", async () => {
|
||||
const ws = await wsConnect(getURL(), {
|
||||
test("peer.request (headers, url, remoteAddress)", async () => {
|
||||
const ws = await wsConnect(getURL() + "?foo=bar", {
|
||||
skip: 1,
|
||||
headers: { "x-test": "1" },
|
||||
});
|
||||
await ws.send("debug");
|
||||
const { headers } = await ws.next();
|
||||
const { request, remoteAddress } = await ws.next();
|
||||
|
||||
// Headers
|
||||
if (opts.adapter === "sse") {
|
||||
expect(headers["connection"]).toBe("keep-alive");
|
||||
expect(request.headers["connection"]).toBe("keep-alive");
|
||||
} else {
|
||||
expect(headers["connection"]).toMatch(/^upgrade$/i);
|
||||
expect(headers["x-test"]).toBe("1");
|
||||
expect(request.headers["connection"]).toMatch(/^upgrade$/i);
|
||||
expect(request.headers["x-test"]).toBe("1");
|
||||
}
|
||||
|
||||
// URL
|
||||
expect(request.url).toMatch(/^http:\/\/localhost:\d+\/\?foo=bar$/);
|
||||
const url = new URL(request.url);
|
||||
expect(url.search).toBe("?foo=bar");
|
||||
|
||||
// Remote address
|
||||
if (!/sse|cloudflare/.test(opts.adapter)) {
|
||||
expect(remoteAddress).toMatch(/:{2}1|(?:0{4}:){7}0{3}1|127\.0\.\0\.1/);
|
||||
}
|
||||
});
|
||||
|
||||
test("upgrade request url", async () => {
|
||||
const ws = await wsConnect(getURL() + "?foo=bar", { skip: 1 });
|
||||
test("peer.websocket", async () => {
|
||||
const ws = await wsConnect(getURL() + "?foo=bar", {
|
||||
skip: 1,
|
||||
headers: {
|
||||
"Sec-WebSocket-Protocol": "crossws",
|
||||
},
|
||||
});
|
||||
await ws.send("debug");
|
||||
const info = await ws.next();
|
||||
expect(info.url).toMatch(/^http:\/\/localhost:\d+\/\?foo=bar$/);
|
||||
const url = new URL(info.url);
|
||||
expect(url.search).toBe("?foo=bar");
|
||||
const { websocket } = await ws.next();
|
||||
expect(websocket).toMatchObject({
|
||||
readyState: 1,
|
||||
protocol: /ss/.test(opts.adapter) ? "" : "crossws",
|
||||
extensions: /sse|cloudflare/.test(opts.adapter)
|
||||
? ""
|
||||
: "permessage-deflate; client_max_window_bits",
|
||||
url: getURL() + "?foo=bar",
|
||||
});
|
||||
});
|
||||
|
||||
test.skipIf(opts.adapter === "sse")("upgrade fail response", async () => {
|
||||
|
||||
@@ -255,7 +255,7 @@ export interface WebSocket extends EventTarget {
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/binaryType)
|
||||
*/
|
||||
binaryType: BinaryType;
|
||||
binaryType: BinaryType | (string & {});
|
||||
/**
|
||||
* Returns the number of bytes of application data (UTF-8 text and binary data) that have been queued using send() but not yet been transmitted to the network.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user