├── .assets └── logo.png ├── .gitignore ├── LICENSE ├── README.md ├── deps.ts ├── egg.yml ├── example ├── client.ts ├── deps.ts └── server.ts ├── lib ├── errors.ts └── websocket.ts ├── mod.ts └── test.ts /.assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryo-ma/deno-websocket/022791e4e9c015cb9038b69f493e21b778a6697c/.assets/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | .vim 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 ryo-ma 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | logo 3 |
4 | 5 | # deno websocket 6 | 7 | [![deno doc](https://img.shields.io/badge/deno-doc-informational?logo=deno)](https://doc.deno.land/https/deno.land/x/denon/mod.ts) 8 | ![GitHub](https://img.shields.io/github/license/ryo-ma/deno-websocket) 9 | [![nest badge](https://nest.land/badge.svg)](https://nest.land/package/deno-websocket) 10 | 11 | 🦕 A simple WebSocket library like [ws of node.js library](https://github.com/websockets/ws) for deno 12 | 13 | This library is wrapping the [ws standard library](https://github.com/denoland/deno_std/tree/main/ws) as a server-side and the [native WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) as a client-side. 14 | You can receive callbacks at the EventEmitter and can use the same object format on both the server-side and the client-side. 15 | 16 | 17 | * Deno >= 1.8.3 18 | 19 | 20 | # Quick Start 21 | 22 | ## Example Demo 23 | 24 | ![demo](https://user-images.githubusercontent.com/6661165/84665958-6df6d880-af5b-11ea-91b8-24c5122ddf9a.gif) 25 | 26 | Server side 27 | 28 | ```bash 29 | $ deno run --allow-net https://deno.land/x/websocket@v0.1.4/example/server.ts 30 | ``` 31 | 32 | Client side 33 | 34 | ```bash 35 | $ deno run --allow-net https://deno.land/x/websocket@v0.1.4/example/client.ts 36 | ws connected! (type 'close' to quit) 37 | > something 38 | ``` 39 | 40 | ## Usage 41 | 42 | Server side 43 | 44 | ```typescript 45 | import { WebSocketClient, WebSocketServer } from "https://deno.land/x/websocket@v0.1.4/mod.ts"; 46 | 47 | const wss = new WebSocketServer(8080); 48 | wss.on("connection", function (ws: WebSocketClient) { 49 | ws.on("message", function (message: string) { 50 | console.log(message); 51 | ws.send(message); 52 | }); 53 | }); 54 | 55 | ``` 56 | 57 | Client side 58 | 59 | ```typescript 60 | import { WebSocketClient, StandardWebSocketClient } from "https://deno.land/x/websocket@v0.1.4/mod.ts"; 61 | const endpoint = "ws://127.0.0.1:8080"; 62 | const ws: WebSocketClient = new StandardWebSocketClient(endpoint); 63 | ws.on("open", function() { 64 | console.log("ws connected!"); 65 | ws.send("something"); 66 | }); 67 | ws.on("message", function (message: string) { 68 | console.log(message); 69 | }); 70 | ``` 71 | 72 | # Documentation 73 | 74 | ## WebSocketServer 75 | 76 | ### Event 77 | 78 | | event | detail| 79 | | --- | --- | 80 | | connection | Emitted when the handshake is complete | 81 | | error | Emitted when an error occurs | 82 | 83 | ### Field 84 | 85 | | field | detail | type | 86 | | --- | --- | --- | 87 | | server.clients | A set that stores all connected clients | Set\ | 88 | 89 | ### Method 90 | 91 | | method | detail | 92 | | --- | --- | 93 | | close() | Close the server | 94 | 95 | ## WebSocketClient 96 | 97 | ### Event 98 | 99 | | event | detail| 100 | | --- | --- | 101 | | open | Emitted when the connection is established | 102 | | close | Emitted when the connection is closed | 103 | | message | Emitted when a message is received from the server | 104 | | ping | Emitted when a ping is received from the server | 105 | | pong | Emitted when a pong is received from the server | 106 | | error | Emitted when an error occurs | 107 | 108 | ### Field 109 | 110 | | field | detail | type | 111 | | --- | --- | --- | 112 | | websocket.isClosed | Get the close flag | Boolean \| undefined | 113 | 114 | ### Method 115 | 116 | | method | detail | 117 | | --- | --- | 118 | | send(message:string \| Unit8Array) | Send a message | 119 | | ping(message:string \| Unit8Array) | Send the ping | 120 | | close([code:int[, reason:string]]) | Close the connection with the server | 121 | | forceClose() | Forcibly close the connection with the server | 122 | 123 | 124 | # LICENSE 125 | [MIT LICENSE](./LICENSE) 126 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | export { EventEmitter, on } from "https://deno.land/std@0.92.0/node/events.ts"; 2 | export { serve, Server, ServerRequest } from "https://deno.land/std@0.92.0/http/server.ts"; 3 | export { 4 | acceptWebSocket, 5 | isWebSocketCloseEvent, 6 | isWebSocketPingEvent, 7 | isWebSocketPongEvent, 8 | } from "https://deno.land/std@0.92.0/ws/mod.ts"; 9 | 10 | export type { WebSocket } from "https://deno.land/std@0.92.0/ws/mod.ts"; 11 | 12 | export { 13 | assertEquals, 14 | assertNotEquals, 15 | assertThrowsAsync, 16 | } from "https://deno.land/std@0.92.0/testing/asserts.ts"; 17 | -------------------------------------------------------------------------------- /egg.yml: -------------------------------------------------------------------------------- 1 | name: deno-websocket 2 | description: A simple WebSocket library like ws of node.js library for Deno 3 | stable: true 4 | version: v0.1.1 5 | repository: https://github.com/ryo-ma/deno-websocket 6 | files: 7 | ./mod.ts 8 | ./deps.ts 9 | ./test.ts 10 | ./example/* 11 | ./libs/**/* 12 | ./README.md 13 | -------------------------------------------------------------------------------- /example/client.ts: -------------------------------------------------------------------------------- 1 | import { 2 | encode, 3 | BufReader, 4 | TextProtoReader, 5 | green, red 6 | } from "./deps.ts"; 7 | import { WebSocketClient, StandardWebSocketClient } from "../lib/websocket.ts"; 8 | 9 | const endpoint = Deno.args[0] || "ws://127.0.0.1:8080"; 10 | 11 | const ws: WebSocketClient = new StandardWebSocketClient(endpoint); 12 | ws.on("open", function() { 13 | Deno.stdout.write(encode(green("ws connected! (type 'close' to quit)\n"))); 14 | Deno.stdout.write(encode("> ")); 15 | }); 16 | ws.on("message", function (message: string) { 17 | Deno.stdout.write(encode(`${message}\n`)); 18 | Deno.stdout.write(encode("> ")); 19 | }); 20 | 21 | /** simple websocket cli */ 22 | try { 23 | const cli = async (): Promise => { 24 | const tpr = new TextProtoReader(new BufReader(Deno.stdin)); 25 | while (true) { 26 | const line = await tpr.readLine(); 27 | if (line === null || line === "close") { 28 | break; 29 | } else if (line === "ping") { 30 | await ws.ping(); 31 | } else { 32 | await ws.send(line); 33 | } 34 | } 35 | }; 36 | await cli().catch(console.error); 37 | if (!ws.isClosed) { 38 | await ws.close(1000).catch(console.error); 39 | } 40 | } catch (err) { 41 | Deno.stderr.write(encode(red(`Could not connect to WebSocket: '${err}'`))); 42 | } 43 | Deno.exit(0); 44 | -------------------------------------------------------------------------------- /example/deps.ts: -------------------------------------------------------------------------------- 1 | export { encode } from "https://deno.land/std@0.65.0/encoding/utf8.ts"; 2 | export { BufReader } from "https://deno.land/std@0.65.0/io/bufio.ts"; 3 | export { TextProtoReader } from "https://deno.land/std@0.65.0/textproto/mod.ts"; 4 | export { blue, green, red, yellow } from "https://deno.land/std@0.65.0/fmt/colors.ts"; 5 | -------------------------------------------------------------------------------- /example/server.ts: -------------------------------------------------------------------------------- 1 | import { WebSocketClient, WebSocketServer } from "../lib/websocket.ts"; 2 | 3 | const wss = new WebSocketServer(); 4 | wss.on("connection", function (ws: WebSocketClient) { 5 | console.log("socket connected!"); 6 | ws.on("message", function (message: string) { 7 | console.log(message); 8 | ws.send(message) 9 | }); 10 | }); -------------------------------------------------------------------------------- /lib/errors.ts: -------------------------------------------------------------------------------- 1 | 2 | export class WebSocketError extends Error { 3 | constructor(e?: string){ 4 | super(e); 5 | Object.setPrototypeOf(this, WebSocketError.prototype); 6 | } 7 | } -------------------------------------------------------------------------------- /lib/websocket.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "./../deps.ts"; 2 | import { serve, Server, ServerRequest } from "./../deps.ts"; 3 | import { 4 | acceptWebSocket, 5 | isWebSocketCloseEvent, 6 | isWebSocketPingEvent, 7 | isWebSocketPongEvent, 8 | WebSocket as DenoWebSocketType, 9 | } from "./../deps.ts"; 10 | 11 | import { WebSocketError } from "./errors.ts"; 12 | 13 | export enum WebSocketState { 14 | CONNECTING = 0, 15 | OPEN = 1, 16 | CLOSING = 2, 17 | CLOSED = 3, 18 | } 19 | 20 | export type EventTypesMap = { [key: string]: (...params: any[]) => void }; 21 | export type DefaultServerEventTypes = { 22 | connection: (ws: WebSocketClient, url: ServerRequest["url"]) => void; 23 | error: (err: Error | unknown) => void; // unknown is an "any" error in catch case - maybe worth wrapping? 24 | }; 25 | 26 | export class GenericEventEmitter extends EventEmitter { 27 | on (eventType: K, listener: EventTypes[K]): this; 28 | /** @deprecated unsafe fallback to EventEmitter.on (no typeguards) */ 29 | on (...params: Parameters): this; 30 | on (...params: Parameters): this { return super.on(...params) }; 31 | 32 | emit (eventType: K, ...params: Parameters): boolean; 33 | /** @deprecated unsafe fallback to EventEmitter.emit (no typeguards) */ 34 | emit (...params: Parameters): boolean; 35 | emit (...params: Parameters): boolean { return super.emit(...params) } 36 | } 37 | 38 | export class WebSocketServer extends GenericEventEmitter { 39 | clients: Set = new Set(); 40 | server?: Server = undefined; 41 | constructor( 42 | private port: Number = 8080, 43 | private realIpHeader: string | null = null, 44 | ) { 45 | super(); 46 | this.connect(); 47 | } 48 | async connect() { 49 | this.server = serve(`:${this.port}`); 50 | for await (const req of this.server) { 51 | const { conn, r: bufReader, w: bufWriter, headers } = req; 52 | try { 53 | const sock = await acceptWebSocket({ 54 | conn, 55 | bufReader, 56 | bufWriter, 57 | headers, 58 | }); 59 | if (this.realIpHeader && "hostname" in sock.conn.remoteAddr) { 60 | if (!req.headers.has(this.realIpHeader)) { 61 | this.emit( 62 | "error", 63 | new Error("specified real ip header does not exist"), 64 | ); 65 | } else { 66 | sock.conn.remoteAddr.hostname = 67 | req.headers.get(this.realIpHeader) || 68 | sock.conn.remoteAddr.hostname; 69 | } 70 | } 71 | const ws: WebSocketAcceptedClient = new WebSocketAcceptedClient(sock); 72 | this.clients.add(ws); 73 | this.emit("connection", ws, req.url); 74 | } catch (err) { 75 | this.emit("error", err); 76 | await req.respond({ status: 400 }); 77 | } 78 | } 79 | } 80 | async close() { 81 | this.server?.close(); 82 | this.clients.clear(); 83 | } 84 | } 85 | 86 | export type DefaultClientEventTypes = { 87 | open: () => void; 88 | message: (data: AllowedMessageEventContent) => void; 89 | ping: (data: Uint8Array) => void; 90 | pong: (data: Uint8Array) => void; 91 | close: (code?: number | WebSocketError | unknown) => void; // unknown is an "any" error in catch - maybe worth wrapping? 92 | error: () => void; 93 | }; 94 | 95 | export interface WebSocketClient extends EventEmitter { 96 | send(message: string | Uint8Array): void; 97 | ping(message?: string | Uint8Array): void; 98 | close(code: number, reason?: string): Promise; 99 | closeForce(): void; 100 | isClosed: boolean | undefined; 101 | } 102 | 103 | type WebSocketAcceptedClientAllowedMessageEventContent = string | Uint8Array; 104 | type DefaultAcceptedClientEventTypes = DefaultClientEventTypes; 105 | export class WebSocketAcceptedClient extends GenericEventEmitter 106 | implements WebSocketClient { 107 | state: WebSocketState = WebSocketState.CONNECTING; 108 | webSocket: DenoWebSocketType; 109 | constructor(sock: DenoWebSocketType) { 110 | super(); 111 | this.webSocket = sock; 112 | this.open(); 113 | } 114 | async open() { 115 | this.state = WebSocketState.OPEN; 116 | this.emit("open"); 117 | try { 118 | for await (const ev of this.webSocket) { 119 | if (typeof ev === "string") { 120 | // text message 121 | this.emit("message", ev); 122 | } else if (ev instanceof Uint8Array) { 123 | // binary message 124 | this.emit("message", ev); 125 | } else if (isWebSocketPingEvent(ev)) { 126 | const [, body] = ev; 127 | // ping 128 | this.emit("ping", body); 129 | } else if (isWebSocketPongEvent(ev)) { 130 | const [, body] = ev; 131 | // pong 132 | this.emit("pong", body); 133 | } else if (isWebSocketCloseEvent(ev)) { 134 | // close 135 | const { code, reason } = ev; 136 | this.state = WebSocketState.CLOSED; 137 | this.emit("close", code); 138 | } 139 | } 140 | } catch (err) { 141 | this.emit("close", err); 142 | if (!this.webSocket.isClosed) { 143 | await this.webSocket.close(1000).catch((e) => { 144 | // This fixes issue #12 where if sent a null payload, the server would crash. 145 | if ( 146 | this.state === WebSocketState.CLOSING && this.webSocket.isClosed 147 | ) { 148 | this.state = WebSocketState.CLOSED; 149 | return; 150 | } 151 | throw new WebSocketError(e); 152 | }); 153 | } 154 | } 155 | } 156 | async ping(message?: string | Uint8Array) { 157 | if (this.state === WebSocketState.CONNECTING) { 158 | throw new WebSocketError( 159 | "WebSocket is not open: state 0 (CONNECTING)", 160 | ); 161 | } 162 | return this.webSocket!.ping(message); 163 | } 164 | async send(message: string | Uint8Array) { 165 | try { 166 | if (this.state === WebSocketState.CONNECTING) { 167 | throw new WebSocketError( 168 | "WebSocket is not open: state 0 (CONNECTING)", 169 | ); 170 | } 171 | return this.webSocket!.send(message); 172 | } catch (error) { 173 | this.state = WebSocketState.CLOSED; 174 | this.emit("close", error.message); 175 | } 176 | } 177 | async close(code = 1000, reason?: string): Promise { 178 | if ( 179 | this.state === WebSocketState.CLOSING || 180 | this.state === WebSocketState.CLOSED 181 | ) { 182 | return; 183 | } 184 | this.state = WebSocketState.CLOSING; 185 | return this.webSocket!.close(code, reason!); 186 | } 187 | async closeForce() { 188 | if ( 189 | this.state === WebSocketState.CLOSING || 190 | this.state === WebSocketState.CLOSED 191 | ) { 192 | return; 193 | } 194 | this.state = WebSocketState.CLOSING; 195 | return this.webSocket!.closeForce(); 196 | } 197 | get isClosed(): boolean | undefined { 198 | return this.webSocket!.isClosed; 199 | } 200 | } 201 | 202 | export class StandardWebSocketClient extends GenericEventEmitter> 203 | implements WebSocketClient { 204 | webSocket?: WebSocket; 205 | constructor(private endpoint?: string) { 206 | super(); 207 | if (this.endpoint !== undefined) { 208 | this.webSocket = new WebSocket(endpoint!); 209 | this.webSocket.onopen = () => this.emit("open"); 210 | this.webSocket.onmessage = (message) => this.emit("message", message); 211 | this.webSocket.onclose = () => this.emit("close"); 212 | this.webSocket.onerror = () => this.emit("error"); 213 | } 214 | } 215 | async ping(message?: string | Uint8Array) { 216 | if (this.webSocket?.readyState === WebSocketState.CONNECTING) { 217 | throw new WebSocketError( 218 | "WebSocket is not open: state 0 (CONNECTING)", 219 | ); 220 | } 221 | return this.webSocket!.send("ping"); 222 | } 223 | async send(message: string | Uint8Array) { 224 | if (this.webSocket?.readyState === WebSocketState.CONNECTING) { 225 | throw new WebSocketError( 226 | "WebSocket is not open: state 0 (CONNECTING)", 227 | ); 228 | } 229 | return this.webSocket!.send(message); 230 | } 231 | async close(code = 1000, reason?: string): Promise { 232 | if ( 233 | this.webSocket!.readyState === WebSocketState.CLOSING || 234 | this.webSocket!.readyState === WebSocketState.CLOSED 235 | ) { 236 | return; 237 | } 238 | return this.webSocket!.close(code, reason!); 239 | } 240 | closeForce(): void { 241 | throw new Error("Method not implemented."); 242 | } 243 | get isClosed(): boolean | undefined { 244 | return this.webSocket!.readyState === WebSocketState.CLOSING || 245 | this.webSocket!.readyState === WebSocketState.CLOSED 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | export { 2 | StandardWebSocketClient, 3 | WebSocketAcceptedClient, 4 | WebSocketServer, 5 | WebSocketState, 6 | } from "./lib/websocket.ts"; 7 | export type { WebSocketClient } from "./lib/websocket.ts"; 8 | export { WebSocketError } from "./lib/errors.ts"; 9 | -------------------------------------------------------------------------------- /test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertEquals, 3 | assertNotEquals, 4 | assertThrowsAsync, 5 | } from "./deps.ts"; 6 | import { StandardWebSocketClient, WebSocketServer, WebSocketError } from "./mod.ts"; 7 | import { on } from "./deps.ts"; 8 | 9 | const endpoint = "ws://127.0.0.1:8080"; 10 | 11 | Deno.test( 12 | { 13 | name: "Connect to the server", 14 | async fn(): Promise { 15 | const wss = new WebSocketServer(8080); 16 | const connection = on(wss, "connection"); 17 | 18 | const ws = new StandardWebSocketClient(endpoint); 19 | assertEquals(ws.webSocket?.readyState, 0) 20 | const open = on(ws, "open"); 21 | const event = await connection.next(); 22 | assertNotEquals(event, undefined); 23 | 24 | await open.next(); 25 | assertEquals(ws.webSocket?.readyState, 1) 26 | await ws.close(); 27 | assertEquals(ws.webSocket?.readyState, 2) 28 | assertEquals(ws.isClosed, true) 29 | 30 | await wss.close(); 31 | }, 32 | }, 33 | ); 34 | 35 | Deno.test( 36 | { 37 | name: "Connect to the server from the two clients", 38 | async fn(): Promise { 39 | const wss = new WebSocketServer(8080); 40 | const connection = on(wss, "connection"); 41 | 42 | const ws1 = new StandardWebSocketClient(endpoint); 43 | const ws2 = new StandardWebSocketClient(endpoint); 44 | const open1 = on(ws1, "open"); 45 | const open2 = on(ws2, "open"); 46 | 47 | let event = await connection.next(); 48 | assertNotEquals(event, undefined); 49 | event = await connection.next(); 50 | assertNotEquals(event, undefined); 51 | 52 | await open1.next(); 53 | await ws1.close(); 54 | assertEquals(ws1.isClosed, true); 55 | 56 | await open2.next(); 57 | await ws2.close(); 58 | assertEquals(ws2.isClosed, true); 59 | 60 | await wss.close(); 61 | }, 62 | }, 63 | ); 64 | Deno.test( 65 | { 66 | name: "Fails connection to the server", 67 | async fn(): Promise { 68 | const wss = new WebSocketServer(8080); 69 | const ws = new StandardWebSocketClient(endpoint); 70 | const connection = on(wss, "connection"); 71 | const open = on(ws, "open"); 72 | await assertThrowsAsync(async (): Promise => { 73 | await ws.send("message"); 74 | }, WebSocketError, "WebSocket is not open: state 0 (CONNECTING)"); 75 | 76 | await open.next(); 77 | await connection.next(); 78 | await ws.close(); 79 | await wss.close(); 80 | }, 81 | }, 82 | ); 83 | --------------------------------------------------------------------------------