├── bun.lockb ├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── .husky └── commit-msg ├── .huskyrc.json ├── tsconfig.build.json ├── tsconfig.json ├── .vscode └── settings.json ├── commitlint.config.js ├── examples ├── chat │ ├── README.md │ └── server.ts ├── polka.ts ├── basic.ts └── express.ts ├── src └── index.ts ├── LICENSE ├── biome.json ├── package.json ├── .gitignore ├── logo.svg ├── README.md └── tests └── index.test.ts /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinyhttp/tinyws/HEAD/bun.lockb -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: v1rtl 2 | liberapay: v1rtl 3 | issuehunt: talentlessguy 4 | github: [talentlessguy] 5 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | pnpm commitlint --config commitlint.config.js --edit "$1" -------------------------------------------------------------------------------- /.huskyrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "pnpm format && pnpm lint && pnpm test", 4 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "Node", 6 | "declaration": true, 7 | "rootDir": "src", 8 | "isolatedModules": true, 9 | "outDir": "dist", 10 | "preserveSymlinks": true 11 | }, 12 | "include": ["src/**/*"] 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "Node", 6 | "declaration": true, 7 | "isolatedModules": true, 8 | "outDir": "dist", 9 | "preserveSymlinks": true 10 | }, 11 | "include": ["src/**/*", "bun-types", "tests/**/*", "examples"] 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "npm.packageManager": "pnpm", 3 | "editor.formatOnSave": true, 4 | "biome.enabled": true, 5 | "eslint.enable": false, 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll": "explicit", 8 | "source.organizeImports.biome": "explicit" 9 | }, 10 | "typescript.tsdk": "node_modules/typescript/lib" 11 | } 12 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | import commitlint from '@commitlint/config-conventional' 2 | 3 | export default { 4 | extends: ['@commitlint/config-conventional'], 5 | ...commitlint.rules, 6 | 'type-enum': [ 7 | 2, 8 | 'always', 9 | ['fix', 'test', 'tooling', 'refactor', 'revert', 'example', 'docs', 'format', 'feat', 'chore', 'ci'] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /examples/chat/README.md: -------------------------------------------------------------------------------- 1 | # Chat example 2 | 3 | Simple chat server built with [tinyhttp](https://github.com/talentlessguy/tinyhttp) and tinyws. 4 | 5 | ## Run 6 | 7 | ```sh 8 | esno server.ts 9 | ``` 10 | 11 | now open a few terminal windows and send messages. 12 | 13 | ```sh 14 | $ wscat -c http://localhost:3000/chat 15 | # > hello 16 | # < hello 17 | # < someone else sent this 18 | # > 19 | ``` 20 | -------------------------------------------------------------------------------- /examples/polka.ts: -------------------------------------------------------------------------------- 1 | import * as polka from 'polka' 2 | 3 | import { type TinyWSRequest, tinyws } from '../src/index' 4 | 5 | const app = polka() 6 | 7 | app.use(tinyws()) 8 | 9 | app.use('/hmr', async (req, res) => { 10 | if (req.ws) { 11 | const ws = await req.ws() 12 | 13 | return ws.send('hello from polka@1.0') 14 | } 15 | res.end('Hello from HTTP!') 16 | }) 17 | 18 | app.listen(3000) 19 | -------------------------------------------------------------------------------- /examples/basic.ts: -------------------------------------------------------------------------------- 1 | import { App, type Request } from '@tinyhttp/app' 2 | 3 | import { type TinyWSRequest, tinyws } from '../src/index' 4 | 5 | const app = new App() 6 | 7 | app.use(tinyws()) 8 | 9 | app.use('/hmr', async (req, res) => { 10 | if (req.ws) { 11 | const ws = await req.ws() 12 | 13 | return ws.send('hello there') 14 | } 15 | res.send('Hello from HTTP!') 16 | }) 17 | 18 | app.listen(3000) 19 | -------------------------------------------------------------------------------- /examples/express.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express' 2 | import type { WebSocket } from 'ws' 3 | import { tinyws } from '../src/index' 4 | 5 | declare global { 6 | namespace Express { 7 | export interface Request { 8 | ws: () => Promise 9 | } 10 | } 11 | } 12 | 13 | const app = express() 14 | 15 | app.use('/hmr', tinyws(), async (req, res) => { 16 | if (req.ws) { 17 | const ws = await req.ws() 18 | 19 | return ws.send('hello from express@4') 20 | } 21 | res.send('Hello from HTTP!') 22 | }) 23 | 24 | app.listen(3000) 25 | -------------------------------------------------------------------------------- /examples/chat/server.ts: -------------------------------------------------------------------------------- 1 | import { App, type Request } from '@tinyhttp/app' 2 | import type { WebSocket } from 'ws' 3 | import { type TinyWSRequest, tinyws } from '../../src/index' 4 | 5 | const app = new App() 6 | 7 | app.use(tinyws()) 8 | 9 | let connections: WebSocket[] = [] 10 | 11 | app.use('/chat', async (req) => { 12 | if (req.ws) { 13 | const ws = await req.ws() 14 | 15 | connections.push(ws) 16 | 17 | ws.on('message', (message) => { 18 | console.log('Received message:', message) 19 | 20 | // broadcast 21 | // biome-ignore lint/complexity/noForEach: 22 | connections.forEach((socket) => socket.send(message)) 23 | }) 24 | 25 | ws.on('close', () => (connections = connections.filter((conn) => conn !== ws))) 26 | } 27 | }) 28 | 29 | app.listen(3000) 30 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type * as http from 'node:http' 2 | import type { ServerOptions, WebSocket } from 'ws' 3 | import { WebSocketServer as Server } from 'ws' 4 | 5 | export interface TinyWSRequest extends http.IncomingMessage { 6 | ws: () => Promise 7 | } 8 | 9 | /** 10 | * tinyws - adds `req.ws` method that resolves when websocket request appears 11 | * @param wsOptions 12 | */ 13 | export const tinyws = 14 | (wsOptions?: ServerOptions, wss: Server = new Server({ ...wsOptions, noServer: true })) => 15 | async (req: TinyWSRequest, _: unknown, next: () => void | Promise) => { 16 | const upgradeHeader = (req.headers.upgrade || '').split(',').map((s) => s.trim()) 17 | 18 | // When upgrade header contains "websocket" it's index is 0 19 | if (upgradeHeader.indexOf('websocket') === 0) { 20 | req.ws = () => 21 | new Promise((resolve) => { 22 | wss.handleUpgrade(req, req.socket, Buffer.alloc(0), (ws) => { 23 | wss.emit('connection', ws, req) 24 | resolve(ws) 25 | }) 26 | }) 27 | } 28 | 29 | await next() 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 v 1 r t l 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [master] 10 | pull_request: 11 | branches: [master] 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | # This workflow contains a single job called "test" 16 | test: 17 | # The type of runner that the job will run on 18 | runs-on: ubuntu-latest 19 | 20 | # Steps represent a sequence of tasks that will be executed as part of the job 21 | steps: 22 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 23 | - uses: actions/checkout@v4 24 | - uses: oven-sh/setup-bun@v2 25 | with: 26 | bun-version: latest 27 | - run: bun i 28 | - run: bun test --coverage 29 | - name: Coveralls 30 | uses: coverallsapp/github-action@master 31 | with: 32 | github-token: ${{ secrets.GITHUB_TOKEN }} 33 | path-to-lcov: ./coverage.lcov 34 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.8.2/schema.json", 3 | "files": { 4 | "ignore": [ 5 | "node_modules", 6 | "dist", 7 | "coverage", 8 | ".pnpm-store" 9 | ] 10 | }, 11 | "formatter": { 12 | "enabled": true, 13 | "formatWithErrors": false, 14 | "indentStyle": "space", 15 | "indentWidth": 2, 16 | "lineEnding": "lf", 17 | "lineWidth": 120, 18 | "attributePosition": "auto" 19 | }, 20 | "organizeImports": { "enabled": true }, 21 | "linter": { 22 | "enabled": true, 23 | "rules": { 24 | "recommended": true, 25 | "correctness": { 26 | "noVoidTypeReturn": "off" 27 | }, 28 | "style": { 29 | "noParameterAssign": "off" 30 | }, 31 | "suspicious": { 32 | "noAssignInExpressions": "off", 33 | "noExplicitAny": "off" 34 | } 35 | } 36 | }, 37 | "javascript": { 38 | "formatter": { 39 | "jsxQuoteStyle": "double", 40 | "quoteProperties": "asNeeded", 41 | "trailingCommas": "none", 42 | "semicolons": "asNeeded", 43 | "arrowParentheses": "always", 44 | "bracketSpacing": true, 45 | "bracketSameLine": false, 46 | "quoteStyle": "single", 47 | "attributePosition": "auto" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tinyws", 3 | "version": "0.1.0", 4 | "description": "Tiny WebSocket middleware for Node.js based on ws.", 5 | "files": [ 6 | "dist" 7 | ], 8 | "engines": { 9 | "node": ">=12.4" 10 | }, 11 | "type": "module", 12 | "exports": "./dist/index.js", 13 | "types": "dist/index.d.ts", 14 | "scripts": { 15 | "build": "tsc -p tsconfig.build.json", 16 | "test": "uvu -r tsm tests", 17 | "test:coverage": "c8 --include=src pnpm test", 18 | "test:report": "c8 report --reporter=text-lcov > coverage.lcov", 19 | "lint": "eslint \"./**/*.ts\"", 20 | "format": "prettier --write \"./**/*.ts\"", 21 | "prepublishOnly": "npm run test && npm run lint && npm run build" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/talentlessguy/tinyws.git" 26 | }, 27 | "keywords": [ 28 | "ws", 29 | "express", 30 | "tinyhttp", 31 | "websocket", 32 | "middleware", 33 | "polka", 34 | "http", 35 | "server" 36 | ], 37 | "author": "v1rtl (https://v1rtl.site)", 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/talentlessguy/tinyws/issues" 41 | }, 42 | "homepage": "https://github.com/talentlessguy/tinyws#readme", 43 | "devDependencies": { 44 | "@biomejs/biome": "^1.8.2", 45 | "@commitlint/cli": "^17.6.5", 46 | "@commitlint/config-conventional": "^17.6.5", 47 | "@tinyhttp/app": "^2.1.0", 48 | "@types/bun": "^1.1.5", 49 | "@types/express": "^4.17.17", 50 | "@types/node": "^18.16.18", 51 | "@types/ws": "^8.5.5", 52 | "c8": "7.12.0", 53 | "express": "^4.18.2", 54 | "husky": "^8.0.3", 55 | "polka": "^1.0.0-next.25", 56 | "typescript": "^4.9.5", 57 | "ws": "^8.13.0" 58 | }, 59 | "peerDependencies": { 60 | "ws": ">=8" 61 | }, 62 | "packageManager": "pnpm@9.3.0+sha256.e1f9e8d1a16607a46dd3c158b5f7a7dc7945501d1c6222d454d63d033d1d918f" 63 | } 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | tinyws 3 |

🚡 tiny WebSocket middleware for Node.js

4 |
5 | 6 | [![Version][v-badge-url]][npm-url] [![Downloads][dl-badge-url]][npm-url] [![GitHub Workflow Status][gh-actions-img]][github-actions] [![Codecov][cov-badge-url]][cov-url] 7 | 8 |
9 | 10 | _**tinyws**_ is a WebSocket middleware for Node.js based on [ws](https://github.com/websockets/ws), inspired by [koa-easy-ws](https://github.com/b3nsn0w/koa-easy-ws). 11 | 12 | Check the [chat example](examples/chat) out to get familiar with tinyws. 13 | 14 | ## Features 15 | 16 | - Small size (**498B**) 17 | - Easy to use (only `req.ws` and nothing else) 18 | - Framework-agnostic (works with tinyhttp, express etc) 19 | - Written in TypeScript 20 | - Pure ESM 21 | 22 | ## Why not [express-ws](https://github.com/HenningM/express-ws)? 23 | 24 | because express-ws is... 25 | 26 | - [Abandoned](https://github.com/HenningM/express-ws/issues/135) since 2018 💀 27 | - Doesn't come with types out of the box (have to install `@types/express-ws`) 28 | - Not compatible with tinyhttp and polka 29 | - Buggy as hell 30 | - Doesn't have tests 31 | 32 | ## Install 33 | 34 | ```sh 35 | pnpm i ws tinyws 36 | ``` 37 | 38 | ## Example 39 | 40 | ```ts 41 | import { App, Request } from '@tinyhttp/app' 42 | import { tinyws, TinyWSRequest } from 'tinyws' 43 | 44 | const app = new App() 45 | 46 | app.use(tinyws()) 47 | 48 | app.use('/ws', async (req, res) => { 49 | if (req.ws) { 50 | const ws = await req.ws() 51 | 52 | return ws.send('hello there') 53 | } else { 54 | res.send('Hello from HTTP!') 55 | } 56 | }) 57 | 58 | app.listen(3000) 59 | ``` 60 | 61 | See [examples](examples) for express and polka integration. 62 | 63 | [v-badge-url]: https://img.shields.io/npm/v/tinyws.svg?style=for-the-badge&color=F55A5A&label=&logo=npm 64 | [npm-url]: https://www.npmjs.com/package/tinyws 65 | [cov-badge-url]: https://img.shields.io/coveralls/github/tinyhttp/tinyws?style=for-the-badge&color=F55A5A 66 | [cov-url]: https://coveralls.io/github/tinyhttp/tinyws 67 | [dl-badge-url]: https://img.shields.io/npm/dt/tinyws?style=for-the-badge&color=F55A5A 68 | [github-actions]: https://github.com/tinyhttp/tinyws/actions 69 | [gh-actions-img]: https://img.shields.io/github/actions/workflow/status/tinyhttp/tinyws/main.yml?branch=master&style=for-the-badge&color=F55A5A&label=&logo=github 70 | -------------------------------------------------------------------------------- /tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { it } from 'bun:test' 2 | import * as assert from 'node:assert' 3 | import { once } from 'node:events' 4 | import { App, type Request } from '@tinyhttp/app' 5 | import { type Server, type ServerOptions, WebSocketServer } from 'ws' 6 | import { type TinyWSRequest, tinyws } from '../src/index' 7 | 8 | const s = (handler: (req: TinyWSRequest) => void, opts?: ServerOptions, inst?: Server) => { 9 | const app = new App() 10 | 11 | app.use(tinyws(opts, inst)) 12 | app.use('/ws', async (req) => { 13 | if (typeof req.ws !== 'undefined') { 14 | handler(req) 15 | } 16 | }) 17 | 18 | return app 19 | } 20 | 21 | it('should respond with a message', async () => { 22 | const app = s(async (req) => { 23 | const ws = await req?.ws() 24 | 25 | return ws.send('hello there') 26 | }) 27 | 28 | const server = app.listen(4443, undefined, 'localhost') 29 | 30 | const ws = new WebSocket('ws://localhost:4443/ws') 31 | 32 | const [data] = await once(ws, 'message') 33 | 34 | assert.equal(data.toString(), 'hello there') 35 | ws.close() 36 | server.close() 37 | }) 38 | 39 | it('should resolve a `.ws` property', async () => { 40 | const app = s(async (req) => { 41 | const ws = await req.ws() 42 | 43 | assert.ok(ws instanceof WebSocket) 44 | 45 | return ws.send('hello there') 46 | }) 47 | 48 | const server = app.listen(4444, undefined, 'localhost') 49 | 50 | const ws = new WebSocket('ws://localhost:4444/ws') 51 | 52 | ws.on('message', () => { 53 | server.close() 54 | ws.close() 55 | }) 56 | }) 57 | 58 | it('should pass ws options', async () => { 59 | const app = s( 60 | async (req) => { 61 | const ws = await req.ws() 62 | 63 | assert.ok(ws instanceof WebSocket) 64 | 65 | ws.on('error', (err) => { 66 | assert.match(err.message, /Max payload size exceeded/) 67 | }) 68 | 69 | return ws.send('hello there') 70 | }, 71 | { 72 | maxPayload: 2 73 | } 74 | ) 75 | 76 | const server = app.listen(4445, undefined, 'localhost') 77 | 78 | const ws = new WebSocket('ws://localhost:4445/ws') 79 | 80 | await once(ws, 'message') 81 | 82 | ws.send('some lenghty message') 83 | 84 | server.close() 85 | ws.close() 86 | }) 87 | 88 | it('should accept messages', async () => { 89 | const app = s(async (req) => { 90 | const ws = await req.ws() 91 | 92 | assert.ok(ws instanceof WebSocket) 93 | 94 | return ws.on('message', (msg) => ws.send(`You sent: ${msg}`)) 95 | }) 96 | 97 | const server = app.listen(4446, undefined, 'localhost') 98 | 99 | const ws = new WebSocket('ws://localhost:4446/ws') 100 | 101 | await once(ws, 'open') 102 | 103 | ws.send('42') 104 | 105 | const [data] = await once(ws, 'message') 106 | 107 | assert.equal(data.toString(), 'You sent: 42') 108 | 109 | server.close() 110 | ws.close() 111 | }) 112 | 113 | it('supports passing a server instance', async () => { 114 | const wss = new WebSocketServer({ noServer: true }) 115 | 116 | wss.on('connection', (socket) => { 117 | assert.ok(socket instanceof WebSocket) 118 | }) 119 | 120 | const app = s( 121 | async (req) => { 122 | const ws = await req.ws() 123 | 124 | assert.ok(ws instanceof WebSocket) 125 | 126 | return ws.send('hello there') 127 | }, 128 | {}, 129 | wss 130 | ) 131 | 132 | const server = app.listen(4447, undefined, 'localhost') 133 | 134 | const ws = new WebSocket('ws://localhost:4447/ws') 135 | 136 | await once(ws, 'message') 137 | 138 | server.close() 139 | ws.close() 140 | }) 141 | 142 | it('returns a middleware function', () => { 143 | assert.ok(typeof tinyws() === 'function') 144 | }) 145 | --------------------------------------------------------------------------------