├── .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 |

3 |
4 |
5 | # deno websocket
6 |
7 | [](https://doc.deno.land/https/deno.land/x/denon/mod.ts)
8 | 
9 | [](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 | 
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 |
--------------------------------------------------------------------------------