@@ -11,6 +11,7 @@ import { jsxTemplate } from "preact/jsx-runtime";
1111import { SpanStatusCode } from "@opentelemetry/api" ;
1212import type { ResolvedFreshConfig } from "./config.ts" ;
1313import type { BuildCache } from "./build_cache.ts" ;
14+ import { HttpError } from "./error.ts" ;
1415import type { LayoutConfig } from "./types.ts" ;
1516import {
1617 FreshScripts ,
@@ -31,6 +32,43 @@ import { renderToString } from "preact-render-to-string";
3132
3233const ENCODER = new TextEncoder ( ) ;
3334
35+ /**
36+ * Event handlers for a WebSocket connection upgraded via
37+ * {@linkcode Context.upgrade}.
38+ */
39+ export interface WebSocketHandlers {
40+ /** Called when the WebSocket connection is established. */
41+ open ?( socket : WebSocket ) : void ;
42+ /** Called when a message is received. */
43+ message ?( socket : WebSocket , event : MessageEvent ) : void ;
44+ /** Called when the connection is closed. */
45+ close ?( socket : WebSocket , code : number , reason : string ) : void ;
46+ /** Called when an error occurs. */
47+ error ?( socket : WebSocket , event : Event | ErrorEvent ) : void ;
48+ }
49+
50+ /**
51+ * Options forwarded to `Deno.upgradeWebSocket()`.
52+ */
53+ export interface WebSocketUpgradeOptions {
54+ /** Automatically close the connection if no ping is received
55+ * within this many seconds. Default: 120. */
56+ idleTimeout ?: number ;
57+ /** The WebSocket sub-protocol to negotiate. */
58+ protocol ?: string ;
59+ }
60+
61+ function isWebSocketHandlers (
62+ value : unknown ,
63+ ) : value is WebSocketHandlers {
64+ if ( typeof value !== "object" || value === null ) return false ;
65+ const v = value as Record < string , unknown > ;
66+ return typeof v . open === "function" ||
67+ typeof v . message === "function" ||
68+ typeof v . close === "function" ||
69+ typeof v . error === "function" ;
70+ }
71+
3472export interface Island {
3573 file : string ;
3674 name : string ;
@@ -489,6 +527,85 @@ export class Context<State> {
489527
490528 return new Response ( body , init ) ;
491529 }
530+
531+ /**
532+ * Upgrade the request to a WebSocket connection.
533+ *
534+ * **Bare mode** — returns the socket and the upgrade response.
535+ * Wire events yourself and return `response` from your handler:
536+ *
537+ * ```ts
538+ * app.get("/ws", (ctx) => {
539+ * const { socket, response } = ctx.upgrade();
540+ * socket.onmessage = (e) => socket.send(e.data);
541+ * return response;
542+ * });
543+ * ```
544+ *
545+ * **Managed mode** — pass handlers and receive the response directly:
546+ *
547+ * ```ts
548+ * app.get("/ws", (ctx) =>
549+ * ctx.upgrade({
550+ * message(socket, event) {
551+ * socket.send(event.data);
552+ * },
553+ * })
554+ * );
555+ * ```
556+ */
557+ upgrade (
558+ options ?: WebSocketUpgradeOptions ,
559+ ) : { socket : WebSocket ; response : Response } ;
560+ upgrade (
561+ handlers : WebSocketHandlers ,
562+ options ?: WebSocketUpgradeOptions ,
563+ ) : Response ;
564+ upgrade (
565+ handlersOrOptions ?: WebSocketHandlers | WebSocketUpgradeOptions ,
566+ maybeOptions ?: WebSocketUpgradeOptions ,
567+ ) : { socket : WebSocket ; response : Response } | Response {
568+ let handlers : WebSocketHandlers | undefined ;
569+ let options : WebSocketUpgradeOptions | undefined ;
570+
571+ if ( isWebSocketHandlers ( handlersOrOptions ) ) {
572+ handlers = handlersOrOptions ;
573+ options = maybeOptions ;
574+ } else {
575+ options = handlersOrOptions ;
576+ }
577+
578+ if ( this . req . headers . get ( "upgrade" ) !== "websocket" ) {
579+ throw new HttpError ( 400 , "Expected a WebSocket upgrade request" ) ;
580+ }
581+
582+ const { socket, response } = Deno . upgradeWebSocket ( this . req , options ) ;
583+
584+ if ( handlers === undefined ) {
585+ return { socket, response } ;
586+ }
587+
588+ if ( handlers . open ) {
589+ socket . addEventListener ( "open" , ( ) => handlers . open ! ( socket ) ) ;
590+ }
591+ if ( handlers . message ) {
592+ socket . addEventListener (
593+ "message" ,
594+ ( ev ) => handlers . message ! ( socket , ev ) ,
595+ ) ;
596+ }
597+ if ( handlers . close ) {
598+ socket . addEventListener (
599+ "close" ,
600+ ( ev ) => handlers . close ! ( socket , ev . code , ev . reason ) ,
601+ ) ;
602+ }
603+ if ( handlers . error ) {
604+ socket . addEventListener ( "error" , ( ev ) => handlers . error ! ( socket , ev ) ) ;
605+ }
606+
607+ return response ;
608+ }
492609}
493610
494611function getHeadersFromInit ( init ?: ResponseInit ) {
0 commit comments