feat: Introduce WebSocket helper for Node.js | `@hono/node-ws` (#503)
* feat: Introduce WebSocket helper for Node.js | `@hono/node-ws` * feat(node-ws): add ci and build command * chore: update yarn.lock * update `yarn.lock` * fixed the ci config * add `@hono/node-server` to devDependecies * fixed format and specify node version * fix(node-ws/docs): fix example * feat: `upgradeWebSocket` be named function --------- Co-authored-by: Yusuke Wada <yusuke@kamawada.com>pull/508/head
parent
650e20fab5
commit
d11c3a565f
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@hono/node-ws': major
|
||||
---
|
||||
|
||||
Inited @hono/node-ws
|
|
@ -0,0 +1,25 @@
|
|||
name: ci-node-ws
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'packages/node-ws/**'
|
||||
pull_request:
|
||||
branches: ['*']
|
||||
paths:
|
||||
- 'packages/node-ws/**'
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./packages/node-ws
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn build
|
||||
- run: yarn test
|
|
@ -30,6 +30,7 @@
|
|||
"build:bun-transpiler": "yarn workspace @hono/bun-transpiler build",
|
||||
"build:prometheus": "yarn workspace @hono/prometheus build",
|
||||
"build:oidc-auth": "yarn workspace @hono/oidc-auth build",
|
||||
"build:node-ws": "yarn workspace @hono/node-ws build",
|
||||
"build": "run-p 'build:*'",
|
||||
"lint": "eslint 'packages/**/*.{ts,tsx}'",
|
||||
"lint:fix": "eslint --fix 'packages/**/*.{ts,tsx}'",
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
# WebSocket helper for Node.js
|
||||
|
||||
A WebSocket helper for Node.js
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { createNodeWebSocket } from '@hono/node-ws'
|
||||
import { Hono } from 'hono'
|
||||
import { serve } from '@hono/node-server'
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app })
|
||||
|
||||
app.get('/ws', upgradeWebSocket((c) => ({
|
||||
// https://hono.dev/helpers/websocket
|
||||
})))
|
||||
|
||||
const server = serve(app)
|
||||
injectWebSocket(server)
|
||||
```
|
||||
|
||||
## Author
|
||||
|
||||
Shotaro Nakamura <https://github.com/nakasyou>
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"name": "@hono/node-ws",
|
||||
"version": "0.1.0",
|
||||
"description": "WebSocket helper for Node.js",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.mjs",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "vitest --run",
|
||||
"build": "tsup ./src/index.ts --format esm,cjs --dts",
|
||||
"publint": "publint",
|
||||
"release": "yarn build && yarn test && yarn publint && yarn publish"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
"registry": "https://registry.npmjs.org",
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/honojs/middleware.git"
|
||||
},
|
||||
"homepage": "https://github.com/honojs/middleware",
|
||||
"devDependencies": {
|
||||
"@hono/node-server": "^1.11.1",
|
||||
"@types/ws": "^8",
|
||||
"hono": "^4.2.9",
|
||||
"tsup": "^8.0.1",
|
||||
"vitest": "^1.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"ws": "^8.17.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@hono/node-server": "^1.11.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.14.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import { serve } from '@hono/node-server'
|
||||
import type { ServerType } from '@hono/node-server/dist/types'
|
||||
import { Hono } from 'hono'
|
||||
import { WebSocket } from 'ws'
|
||||
import { createNodeWebSocket } from '.'
|
||||
|
||||
describe('WebSocket helper', () => {
|
||||
const app = new Hono()
|
||||
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app })
|
||||
|
||||
const mainPromise = new Promise((resolve) =>
|
||||
app.get(
|
||||
'/',
|
||||
upgradeWebSocket(() => ({
|
||||
onOpen() {
|
||||
resolve(true)
|
||||
},
|
||||
}))
|
||||
)
|
||||
)
|
||||
|
||||
it('Should be able to connect', async () => {
|
||||
const server = await new Promise<ServerType>((resolve) => {
|
||||
const server = serve(
|
||||
{
|
||||
fetch: app.fetch,
|
||||
port: 3030,
|
||||
},
|
||||
() => {
|
||||
resolve(server)
|
||||
}
|
||||
)
|
||||
})
|
||||
injectWebSocket(server)
|
||||
const ws = new WebSocket('ws://localhost:3030/')
|
||||
|
||||
expect(await mainPromise).toBe(true)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,115 @@
|
|||
import { Buffer } from 'buffer'
|
||||
import type { Server } from 'node:http'
|
||||
import type { Http2SecureServer, Http2Server } from 'node:http2'
|
||||
import type { Hono } from 'hono'
|
||||
import { createMiddleware } from 'hono/factory'
|
||||
import type { UpgradeWebSocket, WSContext } from 'hono/ws'
|
||||
import { WebSocketServer } from 'ws'
|
||||
|
||||
export interface NodeWebSocket {
|
||||
upgradeWebSocket: UpgradeWebSocket
|
||||
injectWebSocket(server: Server | Http2Server | Http2SecureServer): void
|
||||
}
|
||||
export interface NodeWebSocketInit {
|
||||
app: Hono
|
||||
baseUrl?: string | URL
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended for telling WebSocket
|
||||
* @internal
|
||||
*/
|
||||
class WSResponse extends Response {
|
||||
readonly wss: WebSocketServer
|
||||
constructor(wss: WebSocketServer) {
|
||||
super()
|
||||
this.wss = wss
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create WebSockets for Node.js
|
||||
* @param init Options
|
||||
* @returns NodeWebSocket
|
||||
*/
|
||||
export const createNodeWebSocket = (init: NodeWebSocketInit): NodeWebSocket => {
|
||||
return {
|
||||
injectWebSocket(server) {
|
||||
;(server as Server).on('upgrade', async (request, socket, head) => {
|
||||
const url = new URL(request.url ?? '/', init.baseUrl ?? 'http://localhost')
|
||||
const headers = new Headers()
|
||||
for (const key in request.headers) {
|
||||
const value = request.headers[key]
|
||||
if (!value) {
|
||||
continue
|
||||
}
|
||||
headers.append(key, Array.isArray(value) ? value[0] : value)
|
||||
}
|
||||
const res = (await init.app.request(url, {
|
||||
headers: headers,
|
||||
})) as Response | WSResponse
|
||||
if (!(res instanceof WSResponse)) {
|
||||
socket.destroy()
|
||||
return
|
||||
}
|
||||
res.wss.handleUpgrade(request, socket, head, (ws) => {
|
||||
res.wss.emit('connection', ws, request)
|
||||
})
|
||||
})
|
||||
},
|
||||
upgradeWebSocket: (createEvents) =>
|
||||
async function upgradeWebSocket(c, next) {
|
||||
if (c.req.header('upgrade') !== 'websocket') {
|
||||
// Not websocket
|
||||
await next()
|
||||
return
|
||||
}
|
||||
const wss = new WebSocketServer({ noServer: true })
|
||||
const events = await createEvents(c)
|
||||
wss.on('connection', (ws) => {
|
||||
const ctx: WSContext = {
|
||||
binaryType: 'arraybuffer',
|
||||
close(code, reason) {
|
||||
ws.close(code, reason)
|
||||
},
|
||||
protocol: ws.protocol,
|
||||
raw: ws,
|
||||
get readyState() {
|
||||
return ws.readyState
|
||||
},
|
||||
send(source, opts) {
|
||||
ws.send(source, {
|
||||
compress: opts?.compress,
|
||||
})
|
||||
},
|
||||
url: new URL(c.req.url),
|
||||
}
|
||||
events.onOpen?.(new Event('open'), ctx)
|
||||
ws.on('message', (data, isBinary) => {
|
||||
const datas = Array.isArray(data) ? data : [data]
|
||||
for (const data of datas) {
|
||||
const buff: Buffer = Buffer.from(data)
|
||||
events.onMessage?.(
|
||||
new MessageEvent('message', {
|
||||
data: isBinary ? buff.buffer : buff.toString('utf-8'),
|
||||
}),
|
||||
ctx
|
||||
)
|
||||
}
|
||||
})
|
||||
ws.on('close', () => {
|
||||
events.onClose?.(new CloseEvent('close'), ctx)
|
||||
})
|
||||
ws.on('error', (error) => {
|
||||
events.onError?.(
|
||||
new ErrorEvent('error', {
|
||||
error: error,
|
||||
}),
|
||||
ctx
|
||||
)
|
||||
})
|
||||
})
|
||||
return new WSResponse(wss)
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/// <reference types="vitest" />
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
},
|
||||
})
|
53
yarn.lock
53
yarn.lock
|
@ -1937,6 +1937,28 @@ __metadata:
|
|||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@hono/node-server@npm:^1.11.1":
|
||||
version: 1.11.1
|
||||
resolution: "@hono/node-server@npm:1.11.1"
|
||||
checksum: 87ad10b0e79c5434935cd83172d1923eb04c7ac1853e79b60303cae983876fc3c736e1826b824b34adae7a3e7289c99d764ecefe13b650f81b1f9d5baea3ed4a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@hono/node-ws@workspace:packages/node-ws":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@hono/node-ws@workspace:packages/node-ws"
|
||||
dependencies:
|
||||
"@hono/node-server": "npm:^1.11.1"
|
||||
"@types/ws": "npm:^8"
|
||||
hono: "npm:^4.2.9"
|
||||
tsup: "npm:^8.0.1"
|
||||
vitest: "npm:^1.0.4"
|
||||
ws: "npm:^8.17.0"
|
||||
peerDependencies:
|
||||
"@hono/node-server": ^1.11.1
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@hono/oauth-providers@workspace:packages/oauth-providers":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@hono/oauth-providers@workspace:packages/oauth-providers"
|
||||
|
@ -4170,6 +4192,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/ws@npm:^8":
|
||||
version: 8.5.10
|
||||
resolution: "@types/ws@npm:8.5.10"
|
||||
dependencies:
|
||||
"@types/node": "npm:*"
|
||||
checksum: e9af279b984c4a04ab53295a40aa95c3e9685f04888df5c6920860d1dd073fcc57c7bd33578a04b285b2c655a0b52258d34bee0a20569dca8defb8393e1e5d29
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/yargs-parser@npm:*":
|
||||
version: 21.0.3
|
||||
resolution: "@types/yargs-parser@npm:21.0.3"
|
||||
|
@ -9602,6 +9633,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hono@npm:^4.2.9":
|
||||
version: 4.3.3
|
||||
resolution: "hono@npm:4.3.3"
|
||||
checksum: 2e02a563ab8461a56a97b59b1c31fd002179999a0323b3a44cbf8b69b92ad35cc8f38ba26a88b64caa71e2c1c39a1454d84473ed0c69f4e9573e7b3b064e0f58
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hosted-git-info@npm:^2.1.4":
|
||||
version: 2.8.9
|
||||
resolution: "hosted-git-info@npm:2.8.9"
|
||||
|
@ -19024,6 +19062,21 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ws@npm:^8.17.0":
|
||||
version: 8.17.0
|
||||
resolution: "ws@npm:8.17.0"
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: ">=5.0.2"
|
||||
peerDependenciesMeta:
|
||||
bufferutil:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
checksum: 55241ec93a66fdfc4bf4f8bc66c8eb038fda2c7a4ee8f6f157f2ca7dc7aa76aea0c0da0bf3adb2af390074a70a0e45456a2eaf80e581e630b75df10a64b0a990
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"xdg-basedir@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "xdg-basedir@npm:4.0.0"
|
||||
|
|
Loading…
Reference in New Issue