Skip to content

Commit 06073ff

Browse files
committed
update code example
1 parent bd817b7 commit 06073ff

File tree

1 file changed

+72
-167
lines changed

1 file changed

+72
-167
lines changed

src/content/docs/durable-objects/examples/websocket-hibernation-server.mdx

Lines changed: 72 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -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

1717
This 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
3130
export 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
6270
export 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

256161
Finally, 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

Comments
 (0)