@@ -24,88 +24,134 @@ WebSocket Hibernation is unavailable for outgoing WebSocket use cases. Hibernati
24
24
25
25
<TypeScriptExample >
26
26
``` 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' ;
32
28
33
29
// Worker
34
30
export 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
+ },
66
67
};
67
68
68
69
// Durable Object
69
70
export 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
+ }
109
155
}
110
156
```
111
157
</TypeScriptExample >
0 commit comments