Last active
October 1, 2025 08:09
-
-
Save yusukebe/ec1747487bc22f06eb14e348abb7370c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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