@@ -12,7 +12,7 @@ description: Build a WebSocket server using WebSocket Hibernation on Durable
1212 Objects and Workers.
1313---
1414
15- import { TabItem , Tabs } from " ~/components" ;
15+ import { TabItem , Tabs , TypeScriptExample } from " ~/components" ;
1616
1717This example is similar to the [ Build a WebSocket server] ( /durable-objects/examples/websocket-server/ ) example, but uses the WebSocket Hibernation API. The WebSocket Hibernation API should be preferred for WebSocket server applications built on Durable Objects, since it significantly decreases duration charge, and provides additional features that pair well with WebSocket applications. For more information, refer to [ Use Durable Objects with WebSockets] ( /durable-objects/best-practices/websockets/ ) .
1818
@@ -22,161 +22,78 @@ WebSocket Hibernation is unavailable for outgoing WebSocket use cases. Hibernati
2222
2323:::
2424
25- <Tabs > <TabItem label = " JavaScript" icon = " seti:javascript" >
26-
27- ``` js
28- import { DurableObject } from " cloudflare:workers" ;
25+ <TypeScriptExample >
26+ ``` ts
27+ import { DurableObject } from ' cloudflare:workers' ;
2928
3029// Worker
3130export default {
32- async fetch (request , env , ctx ) {
33- if (request .url .endsWith (" /websocket" )) {
31+ async fetch(request : Request , env : Env , ctx : ExecutionContext ) : Promise < Response > {
32+ if (request .url .endsWith (' /websocket' )) {
3433 // Expect to receive a WebSocket Upgrade request.
3534 // If there is one, accept the request and return a WebSocket Response.
36- const upgradeHeader = request .headers .get (" Upgrade" );
37- if (! upgradeHeader || upgradeHeader !== " websocket" ) {
38- return new Response (" Durable Object expected Upgrade: websocket" , {
35+ const upgradeHeader = request .headers .get (' Upgrade' );
36+ if (! upgradeHeader || upgradeHeader !== ' websocket' ) {
37+ return new Response (' Worker expected Upgrade: websocket' , {
3938 status: 426 ,
4039 });
4140 }
4241
43- // This example will refer to the same Durable Object,
44- // since the name "foo" is hardcoded.
45- let id = env .WEBSOCKET_HIBERNATION_SERVER .idFromName (" foo" );
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' );
4651 let stub = env .WEBSOCKET_HIBERNATION_SERVER .get (id );
4752
4853 return stub .fetch (request );
4954 }
5055
51- return new Response (null , {
52- status: 400 ,
53- statusText: " Bad Request" ,
54- headers: {
55- " Content-Type" : " text/plain" ,
56- },
57- });
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+ );
5866 },
5967};
6068
6169// Durable Object
6270export class WebSocketHibernationServer extends DurableObject {
63- // Keep track of all WebSocket connections
64- sessions = new Map ();
71+ // Keeps track of all WebSocket connections
72+ // When the DO hibernates, gets reconstructed in the constructor
73+ sessions: Map <WebSocket , { [key : string ]: string }>;
6574
66- async fetch (request ) {
67- // Creates two ends of a WebSocket connection.
68- const webSocketPair = new WebSocketPair ();
69- const [client , server ] = Object .values (webSocketPair);
75+ constructor (ctx : DurableObjectState , env : Env ) {
76+ super (ctx , env );
77+ this .sessions = new Map ();
7078
71- // Calling `acceptWebSocket()` informs the runtime that this WebSocket is to begin terminating
72- // request within the Durable Object. It has the effect of "accepting" the connection,
73- // and allowing the WebSocket to send and receive messages.
74- // Unlike `ws.accept()`, `state.acceptWebSocket(ws)` informs the Workers Runtime that the WebSocket
75- // is "hibernatable", so the runtime does not need to pin this Durable Object to memory while
76- // the connection is open. During periods of inactivity, the Durable Object can be evicted
77- // from memory, but the WebSocket connection will remain open. If at some later point the
78- // WebSocket receives a message, the runtime will recreate the Durable Object
79- // (run the `constructor`) and deliver the message to the appropriate handler.
80- this .ctx .acceptWebSocket (server);
81-
82- // Keep a copy of value in memory to survive hibernation.
83- this .sessions .set (server, {});
84-
85- return new Response (null , {
86- status: 101 ,
87- webSocket: client,
88- });
89- }
90-
91- async webSocketMessage (sender , message ) {
92- // Upon receiving a message, get the session associated with the WebSocket connection.
93- const session = this .sessions .get (sender);
94-
95- // If it is a new connection, generate a new ID for the session.
96- if (! session .id ) {
97- session .id = crypto .randomUUID ();
98- sender .serializeAttachment ({
99- ... sender .deserializeAttachment (),
100- id: session .id ,
101- });
102- }
103-
104- // Upon receiving a message from the client, the server replies with the same message,
105- // and the total number of connections with the "[Durable Object]: " prefix
106- sender .send (
107- ` [Durable Object] message: ${ message} , from: ${ session .id } . Total connections: ${ this .ctx .getWebSockets ().length } ` ,
108- );
109-
110- // Send a message to all WebSocket connections, loop over all the connected WebSockets.
111- this .ctx .getWebSockets ().forEach ((ws ) => {
112- ws .send (
113- ` [Durable Object] message: ${ message} , from: ${ session .id } . Total connections: ${ this .ctx .getWebSockets ().length } ` ,
114- );
115- });
79+ // As part of constructing the Durable Object,
80+ // we wake up any hibernating WebSockets and
81+ // place them back in the `sessions` map.
11682
117- // Send a message to all WebSocket connections except the sender, loop over all the connected WebSockets and filter out the sender.
83+ // Get all WebSocket connections from the DO
11884 this .ctx .getWebSockets ().forEach ((ws ) => {
119- if (ws !== sender) {
120- ws .send (
121- ` [Durable Object] message: ${ message} , from: ${ session .id } . Total connections: ${ this .ctx .getWebSockets ().length } ` ,
122- );
85+ let attachment = ws .deserializeAttachment ();
86+ if (ws .deserializeAttachment ()) {
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+ const { ... session } = attachment ;
90+ this .sessions .set (ws , { ... session });
12391 }
12492 });
125- }
12693
127- async webSocketClose (ws , code , reason , wasClean ) {
128- // If the client closes the connection, the runtime will invoke the webSocketClose() handler.
129- ws .close (code, " Durable Object is closing WebSocket" );
94+ // Sets an application level auto response that does not wake hibernated WebSockets.
95+ this .ctx .setWebSocketAutoResponse (new WebSocketRequestResponsePair (' ping' , ' pong' ));
13096 }
131- }
132- ```
133-
134- </TabItem > <TabItem label = " TypeScript" icon = " seti:typescript" >
135-
136- ``` ts
137- import { DurableObject } from " cloudflare:workers" ;
138-
139- // Use npm run cf-typegen to generate the type definitions for the Durable Object
140-
141- // Worker
142- export default {
143- async fetch(
144- request : Request ,
145- env : Env ,
146- ctx : ExecutionContext ,
147- ): Promise <Response > {
148- if (request .url .endsWith (" /websocket" )) {
149- // Expect to receive a WebSocket Upgrade request.
150- // If there is one, accept the request and return a WebSocket Response.
151- const upgradeHeader = request .headers .get (" Upgrade" );
152- if (! upgradeHeader || upgradeHeader !== " websocket" ) {
153- return new Response (" Durable Object expected Upgrade: websocket" , {
154- status: 426 ,
155- });
156- }
157-
158- // This example will refer to the same Durable Object,
159- // since the name "foo" is hardcoded.
160- let id = env .WEBSOCKET_HIBERNATION_SERVER .idFromName (" foo" );
161- let stub = env .WEBSOCKET_HIBERNATION_SERVER .get (id );
162-
163- return stub .fetch (request );
164- }
165-
166- return new Response (null , {
167- status: 400 ,
168- statusText: " Bad Request" ,
169- headers: {
170- " Content-Type" : " text/plain" ,
171- },
172- });
173- },
174- };
175-
176- // Durable Object
177- export class WebSocketHibernationServer extends DurableObject {
178- // Keep track of all WebSocket connections
179- sessions = new Map ()< WebSocket , any > ;
18097
18198 async fetch(request : Request ): Promise <Response > {
18299 // Creates two ends of a WebSocket connection.
@@ -186,72 +103,60 @@ export class WebSocketHibernationServer extends DurableObject {
186103 // Calling `acceptWebSocket()` informs the runtime that this WebSocket is to begin terminating
187104 // request within the Durable Object. It has the effect of "accepting" the connection,
188105 // and allowing the WebSocket to send and receive messages.
189- // Unlike `ws.accept()`, `state .acceptWebSocket(ws)` informs the Workers Runtime that the WebSocket
106+ // Unlike `ws.accept()`, `this.ctx .acceptWebSocket(ws)` informs the Workers Runtime that the WebSocket
190107 // is "hibernatable", so the runtime does not need to pin this Durable Object to memory while
191108 // the connection is open. During periods of inactivity, the Durable Object can be evicted
192109 // from memory, but the WebSocket connection will remain open. If at some later point the
193110 // WebSocket receives a message, the runtime will recreate the Durable Object
194111 // (run the `constructor`) and deliver the message to the appropriate handler.
195112 this .ctx .acceptWebSocket (server );
196113
197- // Keep a copy of value in memory to survive hibernation.
198- this .sessions .set (server , {});
114+ // Generate a random UUID for the session.
115+ const id = crypto .randomUUID ();
116+
117+ // Attach the session ID to the WebSocket connection and serialize it.
118+ // This is necessary to restore the state of the connection when the Durable Object wakes up.
119+ server .serializeAttachment ({ id });
120+
121+ // Add the WebSocket connection to the map of active sessions.
122+ this .sessions .set (server , { id });
199123
200124 return new Response (null , {
201125 status: 101 ,
202126 webSocket: client ,
203127 });
204128 }
205129
206- async webSocketMessage(sender : WebSocket , message : ArrayBuffer | string ) {
207- // Upon receiving a message, get the session associated with the WebSocket connection.
208- const session = this .sessions .get (sender );
209-
210- // If it is a new connection, generate a new ID for the session.
211- if (! session .id ) {
212- session .id = crypto .randomUUID ();
213- sender .serializeAttachment ({
214- ... sender .deserializeAttachment (),
215- id: session .id ,
216- });
217- }
130+ async webSocketMessage(ws : WebSocket , message : ArrayBuffer | string ) {
131+ // Get the session associated with the WebSocket connection.
132+ const session = this .sessions .get (ws )! ;
218133
219- // Upon receiving a message from the client, the server replies with the same message,
134+ // Upon receiving a message from the client, the server replies with the same message, the session ID of the connection,
220135 // and the total number of connections with the "[Durable Object]: " prefix
221- sender .send (
222- ` [Durable Object] message: ${message }, from: ${session .id }. Total connections: ${this .ctx .getWebSockets ().length } ` ,
223- );
136+ ws .send (` [Durable Object] message: ${message }, from: ${session .id }. Total connections: ${this .sessions .size } ` );
224137
225138 // Send a message to all WebSocket connections, loop over all the connected WebSockets.
226- this .ctx .getWebSockets ().forEach ((ws ) => {
227- ws .send (
228- ` [Durable Object] message: ${message }, from: ${session .id }. Total connections: ${this .ctx .getWebSockets ().length } ` ,
229- );
139+ this .sessions .forEach ((attachment , session ) => {
140+ session .send (` [Durable Object] message: ${message }, from: ${attachment .id }. Total connections: ${this .sessions .size } ` );
230141 });
231142
232- // Send a message to all WebSocket connections except the sender, loop over all the connected WebSockets and filter out the sender.
233- this .ctx .getWebSockets ().forEach ((ws ) => {
234- if (ws !== sender ) {
235- ws .send (
236- ` [Durable Object] message: ${message }, from: ${session .id }. Total connections: ${this .ctx .getWebSockets ().length } ` ,
237- );
143+ // Send a message to all WebSocket connections except the connection (ws),
144+ // loop over all the connected WebSockets and filter out the connection (ws).
145+ this .sessions .forEach ((attachment , session ) => {
146+ if (session !== ws ) {
147+ session .send (` [Durable Object] message: ${message }, from: ${attachment .id }. Total connections: ${this .sessions .size } ` );
238148 }
239149 });
240150 }
241151
242- async webSocketClose(
243- ws : WebSocket ,
244- code : number ,
245- reason : string ,
246- wasClean : boolean ,
247- ) {
152+ async webSocketClose(ws : WebSocket , code : number , reason : string , wasClean : boolean ) {
248153 // If the client closes the connection, the runtime will invoke the webSocketClose() handler.
249- ws .close (code , " Durable Object is closing WebSocket" );
154+ ws .close (code , ' Durable Object is closing WebSocket' );
250155 }
251156}
252- ```
253157
254- </TabItem > </Tabs >
158+ ```
159+ </TypeScriptExample >
255160
256161Finally, configure your ` wrangler.toml ` file to include a Durable Object [ binding] ( /durable-objects/get-started/#5-configure-durable-object-bindings ) and [ migration] ( /durable-objects/reference/durable-objects-migrations/ ) based on the namespace and class name chosen previously.
257162
0 commit comments