@@ -22,87 +22,113 @@ WebSocket connections pin your Durable Object to memory, and so duration charges
22
22
23
23
<TypeScriptExample >
24
24
``` ts
25
- import { DurableObject } from " cloudflare:workers" ;
26
-
27
- export interface Env {
28
- WEBSOCKET_SERVER: DurableObjectNamespace <WebSocketServer >;
29
- }
25
+ import { DurableObject } from ' cloudflare:workers' ;
30
26
31
27
// Worker
32
28
export default {
33
- async fetch(request , env , ctx ): Promise <Response > {
34
- if (request .url .endsWith (" /websocket" )) {
35
- // Expect to receive a WebSocket Upgrade request.
36
- // If there is one, accept the request and return a WebSocket Response.
37
- const upgradeHeader = request .headers .get (" Upgrade" );
38
- if (! upgradeHeader || upgradeHeader !== " websocket" ) {
39
- return new Response (" Durable Object expected Upgrade: websocket" , {
40
- status: 426 ,
41
- });
42
- }
43
-
44
- // This example will refer to the same Durable Object,
45
- // since the name "foo" is hardcoded.
46
- let id = env .WEBSOCKET_SERVER .idFromName (" foo" );
47
- let stub = env .WEBSOCKET_SERVER .get (id );
48
-
49
- return stub .fetch (request );
50
- }
51
-
52
- return new Response (null , {
53
- status: 400 ,
54
- statusText: " Bad Request" ,
55
- headers: {
56
- " Content-Type" : " text/plain" ,
57
- },
58
- });
59
- },
60
- } satisfies ExportedHandler <Env >;
29
+ async fetch(request : Request , env : Env , ctx : ExecutionContext ): Promise <Response > {
30
+ if (request .url .endsWith (' /websocket' )) {
31
+ // Expect to receive a WebSocket Upgrade request.
32
+ // If there is one, accept the request and return a WebSocket Response.
33
+ const upgradeHeader = request .headers .get (' Upgrade' );
34
+ if (! upgradeHeader || upgradeHeader !== ' websocket' ) {
35
+ return new Response (' Worker expected Upgrade: websocket' , {
36
+ status: 426 ,
37
+ });
38
+ }
39
+
40
+ if (request .method !== ' GET' ) {
41
+ return new Response (' Worker expected GET method' , {
42
+ status: 400 ,
43
+ });
44
+ }
45
+
46
+ // Since we are hard coding the Durable Object ID by providing the constant name 'foo',
47
+ // all requests to this Worker will be sent to the same Durable Object instance.
48
+ let id = env .WEBSOCKET_SERVER .idFromName (' foo' );
49
+ let stub = env .WEBSOCKET_SERVER .get (id );
50
+
51
+ return stub .fetch (request );
52
+ }
53
+
54
+ return new Response (
55
+ ` Supported endpoints:
56
+ /websocket: Expects a WebSocket upgrade request ` ,
57
+ {
58
+ status: 200 ,
59
+ headers: {
60
+ ' Content-Type' : ' text/plain' ,
61
+ },
62
+ }
63
+ );
64
+ },
65
+ };
61
66
62
67
// Durable Object
63
68
export class WebSocketServer extends DurableObject {
64
- currentlyConnectedWebSockets: number ;
65
-
66
- constructor (ctx : DurableObjectState , env : Env ) {
67
- // This is reset whenever the constructor runs because
68
- // regular WebSockets do not survive Durable Object resets.
69
- //
70
- // WebSockets accepted via the Hibernation API can survive
71
- // a certain type of eviction, but we will not cover that here.
72
- super (ctx , env );
73
- this .currentlyConnectedWebSockets = 0 ;
74
- }
75
-
76
- async fetch(request : Request ): Promise <Response > {
77
- // Creates two ends of a WebSocket connection.
78
- const webSocketPair = new WebSocketPair ();
79
- const [client, server] = Object .values (webSocketPair );
80
-
81
- // Calling `accept()` tells the runtime that this WebSocket is to begin terminating
82
- // request within the Durable Object. It has the effect of "accepting" the connection,
83
- // and allowing the WebSocket to send and receive messages.
84
- server .accept ();
85
- this .currentlyConnectedWebSockets += 1 ;
86
-
87
- // Upon receiving a message from the client, the server replies with the same message,
88
- // and the total number of connections with the "[Durable Object]: " prefix
89
- server .addEventListener (" message" , (event : MessageEvent ) => {
90
- server .send (
91
- ` [Durable Object] currentlyConnectedWebSockets: ${this .currentlyConnectedWebSockets } ` ,
92
- );
93
- });
94
-
95
- // If the client closes the connection, the runtime will close the connection too.
96
- server .addEventListener (" close" , (cls : CloseEvent ) => {
97
- this .currentlyConnectedWebSockets -= 1 ;
98
- server .close (cls .code , " Durable Object is closing WebSocket" );
99
- });
100
-
101
- return new Response (null , {
102
- status: 101 ,
103
- webSocket: client ,
104
- });
105
- }
69
+ // Keeps track of all WebSocket connections
70
+ sessions: Map <WebSocket , { [key : string ]: string }>;
71
+
72
+ constructor (ctx : DurableObjectState , env : Env ) {
73
+ super (ctx , env );
74
+ this .sessions = new Map ();
75
+ }
76
+
77
+ async fetch(request : Request ): Promise <Response > {
78
+ // Creates two ends of a WebSocket connection.
79
+ const webSocketPair = new WebSocketPair ();
80
+ const [client, server] = Object .values (webSocketPair );
81
+
82
+ // Calling `accept()` tells the runtime that this WebSocket is to begin terminating
83
+ // request within the Durable Object. It has the effect of "accepting" the connection,
84
+ // and allowing the WebSocket to send and receive messages.
85
+ server .accept ();
86
+
87
+ // Generate a random UUID for the session.
88
+ const id = crypto .randomUUID ();
89
+ // Add the WebSocket connection to the map of active sessions.
90
+ this .sessions .set (server , { id });
91
+
92
+ server .addEventListener (' message' , (event ) => {
93
+ this .handleWebSocketMessage (server , event .data );
94
+ });
95
+
96
+ // If the client closes the connection, the runtime will close the connection too.
97
+ server .addEventListener (' close' , () => {
98
+ this .handleConnectionClose (server );
99
+ });
100
+
101
+ return new Response (null , {
102
+ status: 101 ,
103
+ webSocket: client ,
104
+ });
105
+ }
106
+
107
+ async handleWebSocketMessage(ws : WebSocket , message : string | ArrayBuffer ) {
108
+ const connection = this .sessions .get (ws )! ;
109
+
110
+ // Reply back with the same message to the connection
111
+ ws .send (` [Durable Object] message: ${message }, from: ${connection .id } ` );
112
+
113
+ // Broadcast the message to all the connections,
114
+ // except the one that sent the message.
115
+ this .sessions .forEach ((k , session ) => {
116
+ if (session !== ws ) {
117
+ session .send (` [Durable Object] message: ${message }, from: ${connection .id } ` );
118
+ }
119
+ });
120
+
121
+ // Broadcast the message to all the connections,
122
+ // including the one that sent the message.
123
+ this .sessions .forEach ((k , session ) => {
124
+ session .send (` [Durable Object] message: ${message }, from: ${connection .id } ` );
125
+ });
126
+ }
127
+
128
+ async handleConnectionClose(ws : WebSocket ) {
129
+ this .sessions .delete (ws );
130
+ ws .close (1000 , ' Durable Object is closing WebSocket' );
131
+ }
106
132
}
107
133
```
108
134
</TypeScriptExample >
0 commit comments