initial commit

This commit is contained in:
Pooya Parsa
2023-12-07 01:41:02 +01:00
commit 115e534a0b
28 changed files with 5635 additions and 0 deletions

15
.editorconfig Normal file
View File

@@ -0,0 +1,15 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
[*.js]
indent_style = space
indent_size = 2
[{package.json,*.yml,*.cjson}]
indent_style = space
indent_size = 2

4
.eslintignore Normal file
View File

@@ -0,0 +1,4 @@
node_modules
coverage
dist
types

7
.eslintrc Normal file
View File

@@ -0,0 +1,7 @@
{
"extends": ["eslint-config-unjs"],
"rules": {
"@typescript-eslint/no-unused-vars": 0,
"no-useless-constructor": 0
}
}

26
.github/workflows/autofix.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: autofix.ci # needed to securely identify the workflow
on:
pull_request:
push:
branches: ["main"]
permissions:
contents: read
jobs:
autofix:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: 18
cache: "pnpm"
- run: pnpm install
- name: Fix lint issues
run: pnpm run lint:fix
- uses: autofix-ci/action@bee19d72e71787c12ca0f29de72f2833e437e4c9
with:
commit-message: "chore: apply automated fixes"

26
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: ci
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: 18
cache: "pnpm"
- run: pnpm install
- run: pnpm lint
- run: pnpm test:types
- run: pnpm build
- run: pnpm vitest --coverage
- uses: codecov/codecov-action@v3

10
.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
node_modules
coverage
dist
types
.vscode
.DS_Store
.eslintcache
*.log*
*.conf*
*.env*

1
.prettierrc Normal file
View File

@@ -0,0 +1 @@
{}

46
LICENSE Normal file
View File

@@ -0,0 +1,46 @@
MIT License
Copyright (c) Pooya Parsa <pooya@pi0.io>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
Bundled with https://github.com/websockets/ws
Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
Copyright (c) 2013 Arnout Kazemier and contributors
Copyright (c) 2016 Luigi Pinca and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

187
README.md Normal file
View File

@@ -0,0 +1,187 @@
# ⛨ CrossWS
[![npm version][npm-version-src]][npm-version-href]
[![bundle][bundle-src]][bundle-href]
<!-- [![npm downloads][npm-downloads-src]][npm-downloads-href] -->
<!-- [![Codecov][codecov-src]][codecov-href] -->
> [!WIP]
> This project and API is under heavy development and opened to test integrations. Don't rely on it for production yet. Feedbacks welcome about API design!
Cross-platform WebSocket server adapters:
- Elegant, typed and simple interface to define WebSocket handlers
- Performant per-server handlers instead of per-connection events api ([why](https://bun.sh/docs/api/websockets#lcYFjkFYJC-summary))
- Zero dependencies with bundled [ws](https://github.com/websockets/ws) types and runtime for [Node.js](https://nodejs.org/) support
- Native integration with [Bun](https://bun.sh/) and [Deno](https://deno.com/) WebSocket API
- Super lightweight tree-shakable packaging
- Developer-Friendly logging
## Install
```sh
# npm
npm install crossws
# yarn
yarn add crossws
# pnpm
pnpm install crossws
# bun
bun install crossws
```
## Integration
CrossWS allows integrating your WebSocket handlers with different runtimes and platforms using built-in adapters. Each runtime has specific method of integrating WebSocket. Once integrated, your custom handlers (such as `onMessage`) will work consitent even if you change the runtime!
### Integration with **Node.js**
In order to integrate crosws with your Node.js HTTP server, you need to connect `upgrade` event to `handleUpgrade` method returned from adapter. Behind the scenes CrossWS uses an embdeded version of [ws](https://github.com/websockets/ws).
```ts
// Initialize Server
import { createServer } from "node:http";
const server = createServer((req, res) => {
res.end(
`<script>new WebSocket("ws://localhost:3000").addEventListener('open', (e) => e.target.send("Hello from client!"));</script>`,
);
}).listen(3000);
// Initialize WebSocket Handler
import nodeWSAdapter from "crossws/adapters/node";
const { handleUpgrade } = nodeWSAdapter({ onMessage: console.log });
server.on("upgrade", handleUpgrade);
```
See [playground/node.ts](./playground/node.ts) for demo and [src/adapters/node.ts](./src/adapters/node.ts) for implementation.
## Integration with **Bun**
In order to integrate crosws with your Bun server, you need to check for `server.upgrade` and also pass `websocket` object returned from adapter to server options. CrossWS leverages native Bun WebSocket API.
```ts
import bunAdapter from "./dist/adapters/bun";
const { websocket } = bunAdapter({ onMessage: console.log });
Bun.serve({
port: 3000,
fetch(req, server) {
if (server.upgrade(req)) {
return;
}
return new Response(
`<script>new WebSocket("ws://localhost:3000").addEventListener('open', (e) => e.target.send("Hello from client!"));</script>`,
{ headers: { "content-type": "text/html" } },
);
},
websocket,
});
```
See [playground/bun.ts](./playground/bun.ts) for demo and [src/adapters/bun.ts](./src/adapters/bun.ts) for implementation.
## Integration with **Deno**
In order to integrate crosws with your Deno server, you need to check for `upgrade` header than then call `handleUpgrade` method from adapter passing the incoming request object. Returned value is server upgrade response.
```ts
import denoAdapter from "crossws/adapters/deno";
const { handleUpgrade } = denoAdapter({ onMessage: console.log });
Deno.serve({ port: 3000 }, (req) => {
if (req.headers.get("upgrade") === "websocket") {
return handleUpgrade(req);
}
return new Response(
`<script>new WebSocket("ws://localhost:3000").addEventListener("open", (e) => e.target.send("Hello from client!"));</script>`,
{ headers: { "content-type": "text/html" } },
);
});
```
See [playground/deno.ts](./playground/deno.ts) for demo and [src/adapters/deno.ts](./src/adapters/deno.ts) for implementation.
## Integration with other runtimes
You can define your custom adapters using `defineWebSocketAdapter` wrapper.
See other adapter implementations in [./src/adapters](./src/adapters/) to get and idea how adapters can be implemented and feel free to directly make a Pull Request to support your environment in CrossWS!
## Handler API
Previously you saw in the adapter examples that we pass `onMessage` option.
First object passed to adapters is a list of global handlers that will get called during lifecycle of a WebSocket connection. You can use `defineWebSocketHandler` utility to make a typed websocket handler object and pass it to the actual adapter when needed.
**Note: API is subject to change! Feedbacks Welcome!**
```ts
import { defineWebSocketHandler } from "crossws";
const websocketHandler = defineWebSocketHandler({
onMessage: (peer, message) => {
console.log("message", peer, message);
if (message.text().includes("ping")) {
peer.send("pong");
}
},
onError: (peer, error) => {
console.log("error", peer, error);
},
onOpen: (peer) => {
console.log("open", peer);
},
onClose: (peer, code, reason) => {
console.log("close", peer, code, reason);
},
onEvent: (event, ...args) => {
console.log("event", event);
},
});
```
### `WebSocketPeer`
Websocket handler methods accept a peer instance as first argument. peer is a wrapper over platform natives WebSocket connection instance and alows to send message.
**Tip:** You can safely log a peer instance to console using `console.log` it will be automatically stringified with useful information including remote address and connection status!
### `WebSocketMessage`
Second argument to `onMessage` event handler is a message object. You can access raw data using `message.rawData` or stringified message using `message.text()`.
**Tip:** You can safely log `message` object to console using `console.log` it will be automatically stringified!
## Development
- Clone this repository
- Install latest LTS version of [Node.js](https://nodejs.org/en/)
- Enable [Corepack](https://github.com/nodejs/corepack) using `corepack enable`
- Install dependencies using `pnpm install`
- Run interactive tests using `pnpm dev`
## License
Made with 💛
Published under [MIT License](./LICENSE).
<!-- Badges -->
[npm-version-src]: https://img.shields.io/npm/v/crossws?style=flat&colorA=18181B&colorB=F0DB4F
[npm-version-href]: https://npmjs.com/package/crossws
[npm-downloads-src]: https://img.shields.io/npm/dm/crossws?style=flat&colorA=18181B&colorB=F0DB4F
[npm-downloads-href]: https://npmjs.com/package/crossws
[codecov-src]: https://img.shields.io/codecov/c/gh/unjs/crossws/main?style=flat&colorA=18181B&colorB=F0DB4F
[codecov-href]: https://codecov.io/gh/unjs/crossws
[bundle-src]: https://img.shields.io/bundlephobia/minzip/crossws?style=flat&colorA=18181B&colorB=F0DB4F
[bundle-href]: https://bundlephobia.com/result?p=crossws

67
package.json Normal file
View File

@@ -0,0 +1,67 @@
{
"name": "crossws",
"version": "0.0.0",
"description": "",
"repository": "unjs/crossws",
"license": "MIT",
"sideEffects": false,
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./adapters/bun": {
"types": "./dist/adapters/bun.d.ts",
"import": "./dist/adapters/bun.mjs",
"require": "./dist/adapters/bun.cjs"
},
"./adapters/deno": {
"types": "./dist/adapters/deno.d.ts",
"import": "./dist/adapters/deno.mjs",
"require": "./dist/adapters/deno.cjs"
},
"./adapters/node": {
"types": "./dist/adapters/node.d.ts",
"import": "./dist/adapters/node.mjs",
"require": "./dist/adapters/node.cjs"
}
},
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "unbuild",
"dev": "vitest dev",
"play:node": "jiti playground/node.ts",
"play:bun": "bun playground/bun.ts",
"play:deno": "deno run -A playground/deno.ts",
"lint": "eslint --cache --ext .ts,.js,.mjs,.cjs . && prettier -c src test",
"lint:fix": "eslint --cache --ext .ts,.js,.mjs,.cjs . --fix && prettier -c src test -w",
"prepack": "pnpm run build",
"release": "pnpm test && changelogen --release && npm publish && git push --follow-tags",
"test": "pnpm lint && pnpm test:types && vitest run --coverage",
"test:types": "tsc --noEmit --skipLibCheck"
},
"devDependencies": {
"@types/node": "^20.10.3",
"@types/web": "^0.0.125",
"@types/ws": "^8.5.10",
"@vitest/coverage-v8": "^1.0.1",
"changelogen": "^0.5.5",
"eslint": "^8.55.0",
"eslint-config-unjs": "^0.2.1",
"jiti": "^1.21.0",
"prettier": "^3.1.0",
"typescript": "^5.3.2",
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.33.0",
"unbuild": "^2.0.0",
"vitest": "^1.0.1",
"ws": "^8.14.2"
},
"packageManager": "pnpm@8.11.0"
}

38
playground/_common.ts Normal file
View File

@@ -0,0 +1,38 @@
import {
defineWebSocketHandler,
type WebSocketAdapter,
type WebSocketHandler,
} from "../src";
export const indexHTMLURL = new URL("_index.html", import.meta.url);
export const log = (arg0: string, ...args) =>
console.log(`[ws] [${arg0}]`, ...args);
const websocketHandler = defineWebSocketHandler({
onMessage: (peer, message) => {
log("message", peer, message);
if (message.text().includes("ping")) {
peer.send("pong");
}
},
onError: (peer, error) => {
log("error", peer, error);
},
onOpen: (peer) => {
log("open", peer);
},
onClose: (peer, code, reason) => {
log("close", peer, code, reason);
},
onEvent: (event, ...args) => {
log("event", event);
},
});
export function createDemo<T extends WebSocketAdapter>(
adapter: T,
opts?: Parameters<T>[1],
): ReturnType<T> {
return adapter(websocketHandler, opts);
}

28
playground/_index.html Normal file
View File

@@ -0,0 +1,28 @@
<!doctype html>
<head>
<title>CrossWS Test Page</title>
</head>
<body>
<div id="logs"></div>
<script type="module">
const url = `ws://${location.host}`;
const logsEl = document.querySelector("#logs");
const log = (...args) => {
console.log("[ws]", ...args);
logsEl.innerHTML += `<p>[${new Date().toJSON()}] ${args.join(" ")}</p>`;
};
log(`Connecting to "${url}""...`);
const ws = new WebSocket(url);
ws.addEventListener("message", (event) => {
log("Message from server:", event.data);
});
log("Waiting for connection...");
await new Promise((resolve) => ws.addEventListener("open", resolve));
log("Sending ping...");
ws.send("ping from client");
</script>
</body>

19
playground/bun.ts Normal file
View File

@@ -0,0 +1,19 @@
// You can run this demo using `bun --bun ./bun.ts` or `npm run play:bun` in repo
import bunAdapter from "../src/adapters/bun";
import { createDemo, indexHTMLURL } from "./_common";
const adapter = createDemo(bunAdapter);
Bun.serve({
port: 3001,
fetch(req, server) {
if (server.upgrade(req)) {
return;
}
return new Response(Bun.file(indexHTMLURL), {
headers: { "Content-Type": "text/html" },
});
},
websocket: adapter.websocket,
});

20
playground/deno.ts Normal file
View File

@@ -0,0 +1,20 @@
// You can run this demo using `deno run -A ./deno.ts` or `npm run play:deno` in repo
import denoAdapter from "../dist/adapters/deno.mjs";
// @ts-ignore
import type * as _Deno from "../types/lib.deno.d.ts";
import { createDemo, indexHTMLURL } from "./_common.ts";
const adapter = createDemo(denoAdapter);
Deno.serve({ port: 3001 }, (req) => {
if (req.headers.get("upgrade") === "websocket") {
return adapter.handleUpgrade(req);
}
return new Response(Deno.readFileSync(indexHTMLURL), {
headers: { "Content-Type": "text/html" },
});
});

22
playground/node.ts Normal file
View File

@@ -0,0 +1,22 @@
// You can run this demo using `npm run play:node` in repo
import { createServer } from "node:http";
import { readFileSync } from "node:fs";
import nodeAdapter from "../src/adapters/node";
import { createDemo, indexHTMLURL } from "./_common";
const adapter = createDemo(nodeAdapter);
const server = createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/html" });
const indexHTML = readFileSync(indexHTMLURL, "utf8");
res.end(indexHTML);
});
server.on("upgrade", adapter.handleUpgrade);
const port = process.env.PORT || 3001;
server.listen(3001, () => {
console.log(`Server running at http://localhost:${port}/`);
});

4727
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

3
renovate.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": ["github>unjs/renovate-config"]
}

12
src/adapter.ts Normal file
View File

@@ -0,0 +1,12 @@
import type { WebSocketHandler } from "./handler";
export type WebSocketAdapter<RT = any, OT = any> = (
handler: WebSocketHandler,
opts: OT,
) => RT;
export function defineWebSocketAdapter<RT, OT>(
factory: WebSocketAdapter<RT, OT>,
) {
return factory;
}

66
src/adapters/bun.ts Normal file
View File

@@ -0,0 +1,66 @@
// https://bun.sh/docs/api/websockets
import type { BunWSOptions, BunServerWebSocket } from "../../types/bun";
import { WebSocketMessage } from "../message";
import { WebSocketError } from "../error";
import { WebSocketPeer } from "../peer";
import { defineWebSocketAdapter } from "../adapter";
export const WebSocket = globalThis.WebSocket;
export interface AdapterOptions {}
export interface Adapter {
websocket: BunWSOptions;
}
export default defineWebSocketAdapter<Adapter, AdapterOptions>(
(handler, opts = {}) => {
return {
websocket: {
message: (ws, message) => {
handler.onEvent?.("bun:message", ws, message);
const peer = new BunWebSocketPeer(ws);
handler.onMessage?.(peer, new WebSocketMessage(message));
},
open: (ws) => {
handler.onEvent?.("bun:open", ws);
const peer = new BunWebSocketPeer(ws);
handler.onOpen?.(peer);
},
close: (ws) => {
handler.onEvent?.("bun:close", ws);
const peer = new BunWebSocketPeer(ws);
handler.onClose?.(peer, 0, "");
},
error: (ws, error) => {
handler.onEvent?.("bun:error", ws, error);
const peer = new BunWebSocketPeer(ws);
handler.onError?.(peer, new WebSocketError(error));
},
drain: (ws) => {
handler.onEvent?.("bun:drain", ws);
},
},
};
},
);
class BunWebSocketPeer extends WebSocketPeer {
constructor(private _ws: BunServerWebSocket) {
super();
}
get id() {
return this._ws.remoteAddress;
}
get readyState() {
return this._ws.readyState as any;
}
send(message: string | ArrayBuffer) {
this._ws.send(message);
return 0;
}
}

71
src/adapters/deno.ts Normal file
View File

@@ -0,0 +1,71 @@
// https://deno.land/api?s=WebSocket
// https://deno.land/api?s=Deno.upgradeWebSocket
// https://examples.deno.land/http-server-websocket
// @ts-nocheck
import type * as _DENO_TYPES_ from "../../types/lib.deno.d.ts";
import { WebSocketMessage } from "../message";
import { WebSocketError } from "../error";
import { WebSocketPeer } from "../peer";
import { defineWebSocketAdapter } from "../adapter.js";
export const WebSocket = globalThis.WebSocket;
export interface AdapterOptions {}
export interface Adapter {
handleUpgrade(req: Deno.Request): Response;
}
export default defineWebSocketAdapter<Adapter, AdapterOptions>(
(handler, opts = {}) => {
const handleUpgrade = (req: Request) => {
const upgrade = Deno.upgradeWebSocket(req);
upgrade.socket.addEventListener("open", () => {
handler.onEvent?.("deno:open", upgrade.socket);
const peer = new DenoWebSocketPeer(upgrade.socket);
handler.onOpen?.(peer);
});
upgrade.socket.addEventListener("message", (event) => {
handler.onEvent?.("deno:message", upgrade.socket, event);
const peer = new DenoWebSocketPeer(upgrade.socket);
handler.onMessage?.(peer, new WebSocketMessage(event.data));
});
upgrade.socket.addEventListener("close", () => {
handler.onEvent?.("deno:close", upgrade.socket);
const peer = new DenoWebSocketPeer(upgrade.socket);
handler.onClose?.(peer, 0, "");
});
upgrade.socket.addEventListener("error", (error) => {
handler.onEvent?.("deno:error", upgrade.socket, error);
const peer = new DenoWebSocketPeer(upgrade.socket);
handler.onError?.(peer, new WebSocketError(error));
});
return upgrade.response;
};
return {
handleUpgrade,
};
},
);
class DenoWebSocketPeer extends WebSocketPeer {
constructor(private _ws: DenoWebSocketPeer) {
super();
}
get id() {
return this._ws.remoteAddress;
}
get readyState() {
return this._ws.readyState as any;
}
send(message: string | ArrayBuffer) {
this._ws.send(message);
return 0;
}
}

135
src/adapters/node.ts Normal file
View File

@@ -0,0 +1,135 @@
// https://github.com/websockets/ws
// https://github.com/websockets/ws/blob/master/doc/ws.md
import type { ClientRequest, IncomingMessage } from "node:http";
import type { Duplex } from "node:stream";
import {
WebSocketServer as _WebSocketServer,
WebSocket as _WebSocket,
} from "ws";
import type {
ServerOptions,
RawData,
WebSocketServer,
WebSocket as WebSocketT,
} from "../../types/ws";
import { WebSocketPeer } from "../peer";
import { WebSocketMessage } from "../message";
import { WebSocketError } from "../error";
import { defineWebSocketAdapter } from "../adapter";
export const WebSocket = _WebSocket as unknown as WebSocketT;
export interface AdapterOptions {
wss?: WebSocketServer;
serverOptions?: ServerOptions;
}
export interface Adapter {
handleUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer): void;
}
export default defineWebSocketAdapter<Adapter, AdapterOptions>(
(handler, opts = {}) => {
const wss: WebSocketServer =
opts.wss ||
(new _WebSocketServer({
noServer: true,
...(opts.serverOptions as any),
}) as WebSocketServer);
// Unmanaged server-level events
wss.on("error", (error) => {
handler.onEvent?.("node:server-error", error);
});
wss.on("headers", (headers, request) => {
handler.onEvent?.("node:server-headers", headers, request);
});
wss.on("listening", () => {
handler.onEvent?.("node:server-listening");
});
wss.on("close", () => {
handler.onEvent?.("node:server-close");
});
wss.on("connection", (ws, req) => {
const peer = new NodeWebSocketPeer(ws, req);
// Managed socket-level events
ws.on("message", (data: RawData, isBinary: boolean) => {
handler.onEvent?.("node:message", ws, data, isBinary);
if (Array.isArray(data)) {
data = Buffer.concat(data);
}
handler.onMessage?.(peer, new WebSocketMessage(data, isBinary));
});
ws.on("error", (error: Error) => {
handler.onEvent?.("node:error", ws, error);
handler.onError?.(peer, new WebSocketError(error));
});
ws.on("close", (code: number, reason: Buffer) => {
handler.onEvent?.("node:close", ws, code, reason);
handler.onClose?.(peer, code, reason?.toString());
});
ws.on("open", () => {
handler.onEvent?.("node:open", ws);
handler.onOpen?.(peer);
});
// Unmanaged socket-level events
ws.on("ping", (data: Buffer) => {
handler.onEvent?.("node:ping", ws, data);
});
ws.on("pong", (data: Buffer) => {
handler.onEvent?.("node:pong", ws, data);
});
ws.on(
"unexpected-response",
(request: ClientRequest, response: IncomingMessage) => {
handler.onEvent?.("node:unexpected-response", ws, request, response);
},
);
ws.on("upgrade", (request: IncomingMessage) => {
handler.onEvent?.("node:upgrade", ws, request);
});
});
return {
handleUpgrade: (req, socket, head) => {
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit("connection", ws, req);
});
},
};
},
);
class NodeWebSocketPeer extends WebSocketPeer {
constructor(
private _ws: WebSocketT,
private _req: IncomingMessage,
) {
super();
}
get id() {
const socket = this._req?.socket;
if (!socket) {
return undefined;
}
const addr =
socket.remoteFamily === "IPv6"
? `[${socket.remoteAddress}]`
: socket.remoteAddress;
return `${addr}:${socket.remotePort}`;
}
get readyState() {
return this._ws.readyState;
}
send(message: string, compress?: boolean) {
this._ws.send(message, { compress });
return 0;
}
}

6
src/error.ts Normal file
View File

@@ -0,0 +1,6 @@
export class WebSocketError extends Error {
constructor(...args: any[]) {
super(...args);
this.name = "WebSocketError";
}
}

25
src/handler.ts Normal file
View File

@@ -0,0 +1,25 @@
import { WebSocketError } from "./error";
import type { WebSocketMessage } from "./message";
import type { WebSocketPeer } from "./peer";
export interface WebSocketHandler {
onEvent?(event: string, ...args: any[]): void;
/** A message is received */
onMessage?(peer: WebSocketPeer, message: WebSocketMessage): void;
/** A socket is opened */
onOpen?(peer: WebSocketPeer): void;
/** A socket is closed */
onClose?(peer: WebSocketPeer, code: number, reason: string): void;
/** An error occurs */
onError?(peer: WebSocketPeer, error: WebSocketError): void;
}
export function defineWebSocketHandler(
handler: WebSocketHandler,
): WebSocketHandler {
return handler;
}

5
src/index.ts Normal file
View File

@@ -0,0 +1,5 @@
export * from "./adapter";
export * from "./error";
export * from "./message";
export * from "./peer";
export * from "./handler";

18
src/message.ts Normal file
View File

@@ -0,0 +1,18 @@
export class WebSocketMessage {
constructor(
public readonly rawData: string | ArrayBuffer | Uint8Array,
public readonly isBinary?: boolean,
) {}
text(): string {
return this.rawData.toString();
}
toString() {
return `[WebSocketMessage] ${this.text()}`;
}
[Symbol.for("nodejs.util.inspect.custom")]() {
return this.toString();
}
}

33
src/peer.ts Normal file
View File

@@ -0,0 +1,33 @@
// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState
type ReadyState = 0 | 1 | 2 | 3;
const ReadyStateMap = {
"-1": "unkown",
0: "connecting",
1: "open",
2: "closing",
3: "closed",
} as const;
export abstract class WebSocketPeer {
get id(): string | undefined {
return undefined;
}
get readyState(): ReadyState | -1 {
return -1;
}
abstract send(
message: string | ArrayBuffer | Uint8Array,
compress?: boolean,
): number;
toString() {
const readyState = ReadyStateMap[this.readyState];
return `[WebSocketPeer] ${this.id || "-"} (${readyState})`;
}
[Symbol.for("nodejs.util.inspect.custom")]() {
return this.toString();
}
}

8
test/index.test.ts Normal file
View File

@@ -0,0 +1,8 @@
import { expect, it, describe } from "vitest";
import {} from "../src";
describe("crossws", () => {
it.todo("pass", () => {
expect(true).toBe(true);
});
});

10
tsconfig.json Normal file
View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Node",
"esModuleInterop": true,
"strict": true
},
"include": ["src"]
}