Skip to content

Instantly share code, notes, and snippets.

@yusukebe
Last active October 1, 2025 08:09
Show Gist options
  • Select an option

  • Save yusukebe/ec1747487bc22f06eb14e348abb7370c to your computer and use it in GitHub Desktop.

Select an option

Save yusukebe/ec1747487bc22f06eb14e348abb7370c to your computer and use it in GitHub Desktop.
import { Hono } from 'hono'
import { upgradeWebSocket, websocket } from 'hono/bun'
import { MyApiServer } from './my-api-server'
import { newHonoRpcResponse } from './capnweb-server-hono.ts'
const app = new Hono()
app.all('/api', (c) => {
return newHonoRpcResponse(c, new MyApiServer(), {
upgradeWebSocket
})
})
export default {
fetch: app.fetch,
port: 8787,
websocket
}
// Created with AI. We should improve it.
import { newHttpBatchRpcResponse, newWebSocketRpcSession, newWorkersWebSocketRpcResponse } from 'capnweb'
import type { Context } from 'hono'
import type { UpgradeWebSocket } from 'hono/ws'
// Adapter to wrap Bun's ServerWebSocket to standard WebSocket API
class BunWebSocketAdapter {
private ws: any
private handlers: Map<string, Function[]> = new Map()
public readonly readyState: number
public readonly CONNECTING = 0
public readonly OPEN = 1
public readonly CLOSING = 2
public readonly CLOSED = 3
constructor(ws: any) {
this.ws = ws
this.readyState = ws.readyState || 1 // Bun's ServerWebSocket usually starts as OPEN
// Keep reference to adapter
;(ws as any).__adapter = this
}
addEventListener(event: string, handler: Function) {
if (!this.handlers.has(event)) {
this.handlers.set(event, [])
}
this.handlers.get(event)!.push(handler)
}
removeEventListener(event: string, handler: Function) {
const handlers = this.handlers.get(event)
if (handlers) {
const index = handlers.indexOf(handler)
if (index > -1) {
handlers.splice(index, 1)
}
}
}
handleMessage(data: any) {
this.triggerEvent('message', { data })
}
handleClose(code: number, reason: string) {
this.triggerEvent('close', { code, reason })
}
handleError(error: any) {
this.triggerEvent('error', { error })
}
private triggerEvent(event: string, data: any) {
const handlers = this.handlers.get(event)
if (handlers) {
handlers.forEach((handler) => handler(data))
}
}
send(data: any) {
this.ws.send(data)
}
close(code?: number, reason?: string) {
this.ws.close(code, reason)
}
// WebSocket compatible properties
get url() {
return ''
}
get protocol() {
return ''
}
get extensions() {
return ''
}
get bufferedAmount() {
return 0
}
get binaryType() {
return 'blob'
}
// Event handler properties
onopen: ((event: Event) => any) | null = null
onclose: ((event: CloseEvent) => any) | null = null
onmessage: ((event: MessageEvent) => any) | null = null
onerror: ((event: Event) => any) | null = null
dispatchEvent(_event: Event): boolean {
return true
}
}
// Check WebSocket compatibility and return appropriate adapter
function createWebSocketAdapter(ws: any): any {
// Check if standard WebSocket API addEventListener exists
if (typeof ws.addEventListener === 'function') {
// Deno/Node/Browser: can be used directly
return ws
} else if (ws.message !== undefined || ws.send !== undefined) {
// Bun: ServerWebSocket needs adapter wrapper
return new BunWebSocketAdapter(ws)
} else {
throw new Error('Unsupported WebSocket implementation')
}
}
// Unified helper to handle both WebSocket and HTTP based on environment
export function newHonoRpcResponse(
c: Context,
localMain: any,
options?: { upgradeWebSocket?: UpgradeWebSocket<any> }
): Response | Promise<Response> {
const isUpgradeRequest = c.req.header('upgrade')?.toLowerCase() === 'websocket'
if (isUpgradeRequest) {
if (options?.upgradeWebSocket) {
// upgradeWebSocket provided (Bun/Deno/Node environments)
let adapter: any
return options.upgradeWebSocket(() => {
return {
onOpen(_event, ws) {
// Auto-detect ws.raw and apply adapter
const webSocket = createWebSocketAdapter(ws.raw || ws)
adapter = (ws.raw as any)?.__adapter
newWebSocketRpcSession(webSocket, localMain)
},
onMessage(event) {
// For Bun, handle events via adapter
if (adapter?.handleMessage) {
adapter.handleMessage(event.data)
}
},
onClose(event) {
if (adapter?.handleClose) {
adapter.handleClose(event.code, event.reason)
}
},
onError(event) {
if (adapter?.handleError) {
adapter.handleError(event)
}
}
}
})(c, () => Promise.resolve()) as Promise<Response>
} else if (typeof WebSocketPair !== 'undefined') {
// Cloudflare Workers environment: use WebSocketPair
return newWorkersWebSocketRpcResponse(c.req.raw, localMain)
} else {
// Environment doesn't support WebSocket
return new Response('WebSocket not supported in this environment', { status: 400 })
}
}
// Regular HTTP POST request
return newHttpBatchRpcResponse(c.req.raw, localMain)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment