@@ -24,88 +24,134 @@ WebSocket Hibernation is unavailable for outgoing WebSocket use cases. Hibernati
2424
2525<TypeScriptExample >
2626``` ts
27- import { DurableObject } from " cloudflare:workers" ;
28-
29- export interface Env {
30- WEBSOCKET_HIBERNATION_SERVER: DurableObjectNamespace <WebSocketHibernationServer >;
31- }
27+ import { DurableObject } from ' cloudflare:workers' ;
3228
3329// Worker
3430export default {
35- async fetch(
36- request : Request ,
37- env : Env ,
38- ctx : ExecutionContext ,
39- ): Promise <Response > {
40- if (request .url .endsWith (" /websocket" )) {
41- // Expect to receive a WebSocket Upgrade request.
42- // If there is one, accept the request and return a WebSocket Response.
43- const upgradeHeader = request .headers .get (" Upgrade" );
44- if (! upgradeHeader || upgradeHeader !== " websocket" ) {
45- return new Response (" Durable Object expected Upgrade: websocket" , {
46- status: 426 ,
47- });
48- }
49-
50- // This example will refer to the same Durable Object,
51- // since the name "foo" is hardcoded.
52- let id = env .WEBSOCKET_HIBERNATION_SERVER .idFromName (" foo" );
53- let stub = env .WEBSOCKET_HIBERNATION_SERVER .get (id );
54-
55- return stub .fetch (request );
56- }
57-
58- return new Response (null , {
59- status: 400 ,
60- statusText: " Bad Request" ,
61- headers: {
62- " Content-Type" : " text/plain" ,
63- },
64- });
65- },
31+ async fetch(request : Request , env : Env , ctx : ExecutionContext ): Promise <Response > {
32+ if (request .url .endsWith (' /websocket' )) {
33+ // Expect to receive a WebSocket Upgrade request.
34+ // If there is one, accept the request and return a WebSocket Response.
35+ const upgradeHeader = request .headers .get (' Upgrade' );
36+ if (! upgradeHeader || upgradeHeader !== ' websocket' ) {
37+ return new Response (' Worker expected Upgrade: websocket' , {
38+ status: 426 ,
39+ });
40+ }
41+
42+ if (request .method !== ' GET' ) {
43+ return new Response (' Worker expected GET method' , {
44+ status: 400 ,
45+ });
46+ }
47+
48+ // Since we are hard coding the Durable Object ID by providing the constant name 'foo',
49+ // all requests to this Worker will be sent to the same Durable Object instance.
50+ let id = env .WEBSOCKET_HIBERNATION_SERVER .idFromName (' foo' );
51+ let stub = env .WEBSOCKET_HIBERNATION_SERVER .get (id );
52+
53+ return stub .fetch (request );
54+ }
55+
56+ return new Response (
57+ ` Supported endpoints:
58+ /websocket: Expects a WebSocket upgrade request ` ,
59+ {
60+ status: 200 ,
61+ headers: {
62+ ' Content-Type' : ' text/plain' ,
63+ },
64+ }
65+ );
66+ },
6667};
6768
6869// Durable Object
6970export class WebSocketHibernationServer extends DurableObject {
70- async fetch(request : Request ): Promise <Response > {
71- // Creates two ends of a WebSocket connection.
72- const webSocketPair = new WebSocketPair ();
73- const [client, server] = Object .values (webSocketPair );
74-
75- // Calling `acceptWebSocket()` informs the runtime that this WebSocket is to begin terminating
76- // request within the Durable Object. It has the effect of "accepting" the connection,
77- // and allowing the WebSocket to send and receive messages.
78- // Unlike `ws.accept()`, `state.acceptWebSocket(ws)` informs the Workers Runtime that the WebSocket
79- // is "hibernatable", so the runtime does not need to pin this Durable Object to memory while
80- // the connection is open. During periods of inactivity, the Durable Object can be evicted
81- // from memory, but the WebSocket connection will remain open. If at some later point the
82- // WebSocket receives a message, the runtime will recreate the Durable Object
83- // (run the `constructor`) and deliver the message to the appropriate handler.
84- this .ctx .acceptWebSocket (server );
85-
86- return new Response (null , {
87- status: 101 ,
88- webSocket: client ,
89- });
90- }
91-
92- async webSocketMessage(ws : WebSocket , message : ArrayBuffer | string ) {
93- // Upon receiving a message from the client, the server replies with the same message,
94- // and the total number of connections with the "[Durable Object]: " prefix
95- ws .send (
96- ` [Durable Object] message: ${message }, connections: ${this .ctx .getWebSockets ().length } ` ,
97- );
98- }
99-
100- async webSocketClose(
101- ws : WebSocket ,
102- code : number ,
103- reason : string ,
104- wasClean : boolean ,
105- ) {
106- // If the client closes the connection, the runtime will invoke the webSocketClose() handler.
107- ws .close (code , " Durable Object is closing WebSocket" );
108- }
71+ // Keeps track of all WebSocket connections
72+ // When the DO hibernates, gets reconstructed in the constructor
73+ sessions: Map <WebSocket , { [key : string ]: string }>;
74+
75+ constructor (ctx : DurableObjectState , env : Env ) {
76+ super (ctx , env );
77+ this .sessions = new Map ();
78+
79+ // As part of constructing the Durable Object,
80+ // we wake up any hibernating WebSockets and
81+ // place them back in the `sessions` map.
82+
83+ // Get all WebSocket connections from the DO
84+ this .ctx .getWebSockets ().forEach ((ws ) => {
85+ let attachment = ws .deserializeAttachment ();
86+ if (attachment ) {
87+ // If we previously attached state to our WebSocket,
88+ // let's add it to `sessions` map to restore the state of the connection.
89+ this .sessions .set (ws , { ... attachment });
90+ }
91+ });
92+
93+ // Sets an application level auto response that does not wake hibernated WebSockets.
94+ this .ctx .setWebSocketAutoResponse (new WebSocketRequestResponsePair (' ping' , ' pong' ));
95+ }
96+
97+ async fetch(request : Request ): Promise <Response > {
98+ // Creates two ends of a WebSocket connection.
99+ const webSocketPair = new WebSocketPair ();
100+ const [client, server] = Object .values (webSocketPair );
101+
102+ // Calling `acceptWebSocket()` informs the runtime that this WebSocket is to begin terminating
103+ // request within the Durable Object. It has the effect of "accepting" the connection,
104+ // and allowing the WebSocket to send and receive messages.
105+ // Unlike `ws.accept()`, `this.ctx.acceptWebSocket(ws)` informs the Workers Runtime that the WebSocket
106+ // is "hibernatable", so the runtime does not need to pin this Durable Object to memory while
107+ // the connection is open. During periods of inactivity, the Durable Object can be evicted
108+ // from memory, but the WebSocket connection will remain open. If at some later point the
109+ // WebSocket receives a message, the runtime will recreate the Durable Object
110+ // (run the `constructor`) and deliver the message to the appropriate handler.
111+ this .ctx .acceptWebSocket (server );
112+
113+ // Generate a random UUID for the session.
114+ const id = crypto .randomUUID ();
115+
116+ // Attach the session ID to the WebSocket connection and serialize it.
117+ // This is necessary to restore the state of the connection when the Durable Object wakes up.
118+ server .serializeAttachment ({ id });
119+
120+ // Add the WebSocket connection to the map of active sessions.
121+ this .sessions .set (server , { id });
122+
123+ return new Response (null , {
124+ status: 101 ,
125+ webSocket: client ,
126+ });
127+ }
128+
129+ async webSocketMessage(ws : WebSocket , message : ArrayBuffer | string ) {
130+ // Get the session associated with the WebSocket connection.
131+ const session = this .sessions .get (ws )! ;
132+
133+ // Upon receiving a message from the client, the server replies with the same message, the session ID of the connection,
134+ // and the total number of connections with the "[Durable Object]: " prefix
135+ ws .send (` [Durable Object] message: ${message }, from: ${session .id }. Total connections: ${this .sessions .size } ` );
136+
137+ // Send a message to all WebSocket connections, loop over all the connected WebSockets.
138+ this .sessions .forEach ((attachment , session ) => {
139+ session .send (` [Durable Object] message: ${message }, from: ${attachment .id }. Total connections: ${this .sessions .size } ` );
140+ });
141+
142+ // Send a message to all WebSocket connections except the connection (ws),
143+ // loop over all the connected WebSockets and filter out the connection (ws).
144+ this .sessions .forEach ((attachment , session ) => {
145+ if (session !== ws ) {
146+ session .send (` [Durable Object] message: ${message }, from: ${attachment .id }. Total connections: ${this .sessions .size } ` );
147+ }
148+ });
149+ }
150+
151+ async webSocketClose(ws : WebSocket , code : number , reason : string , wasClean : boolean ) {
152+ // If the client closes the connection, the runtime will invoke the webSocketClose() handler.
153+ ws .close (code , ' Durable Object is closing WebSocket' );
154+ }
109155}
110156```
111157</TypeScriptExample >
0 commit comments