Skip to content

Commit 53c779a

Browse files
committed
add serializeAttachment API to Hibernation example
1 parent bc2a07f commit 53c779a

File tree

1 file changed

+202
-126
lines changed

1 file changed

+202
-126
lines changed

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

Lines changed: 202 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,16 @@ sidebar:
1010
order: 3
1111
description: Build a WebSocket server using WebSocket Hibernation on Durable
1212
Objects and Workers.
13-
1413
---
1514

16-
import { TabItem, Tabs } from "~/components"
15+
import { TabItem, Tabs } from "~/components";
1716

1817
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/reference/websockets/).
1918

2019
:::note
2120

22-
2321
WebSocket Hibernation is unavailable for outgoing WebSocket use cases. Hibernation is only supported when the Durable Object acts as a server. For use cases where outgoing WebSockets are required, refer to [Write a WebSocket client](/workers/examples/websockets/#write-a-websocket-client).
2422

25-
2623
:::
2724

2825
<Tabs> <TabItem label="JavaScript" icon="seti:javascript">
@@ -32,146 +29,225 @@ import { DurableObject } from "cloudflare:workers";
3229

3330
// Worker
3431
export default {
35-
async fetch(request, env, ctx) {
36-
if (request.url.endsWith("/websocket")) {
37-
// Expect to receive a WebSocket Upgrade request.
38-
// If there is one, accept the request and return a WebSocket Response.
39-
const upgradeHeader = request.headers.get('Upgrade');
40-
if (!upgradeHeader || upgradeHeader !== 'websocket') {
41-
return new Response('Durable Object expected Upgrade: websocket', { status: 426 });
42-
}
43-
44-
// This example will refer to the same Durable Object,
45-
// since the name "foo" is hardcoded.
46-
let id = env.WEBSOCKET_HIBERNATION_SERVER.idFromName("foo");
47-
let stub = env.WEBSOCKET_HIBERNATION_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-
}
32+
async fetch(request, env, ctx) {
33+
if (request.url.endsWith("/websocket")) {
34+
// Expect to receive a WebSocket Upgrade request.
35+
// 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", {
39+
status: 426,
40+
});
41+
}
42+
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");
46+
let stub = env.WEBSOCKET_HIBERNATION_SERVER.get(id);
47+
48+
return stub.fetch(request);
49+
}
50+
51+
return new Response(null, {
52+
status: 400,
53+
statusText: "Bad Request",
54+
headers: {
55+
"Content-Type": "text/plain",
56+
},
57+
});
58+
},
6059
};
6160

6261
// Durable Object
6362
export class WebSocketHibernationServer extends DurableObject {
64-
65-
async fetch(request) {
66-
// Creates two ends of a WebSocket connection.
67-
const webSocketPair = new WebSocketPair();
68-
const [client, server] = Object.values(webSocketPair);
69-
70-
// Calling `acceptWebSocket()` informs the runtime that this WebSocket is to begin terminating
71-
// request within the Durable Object. It has the effect of "accepting" the connection,
72-
// and allowing the WebSocket to send and receive messages.
73-
// Unlike `ws.accept()`, `state.acceptWebSocket(ws)` informs the Workers Runtime that the WebSocket
74-
// is "hibernatable", so the runtime does not need to pin this Durable Object to memory while
75-
// the connection is open. During periods of inactivity, the Durable Object can be evicted
76-
// from memory, but the WebSocket connection will remain open. If at some later point the
77-
// WebSocket receives a message, the runtime will recreate the Durable Object
78-
// (run the `constructor`) and deliver the message to the appropriate handler.
79-
this.ctx.acceptWebSocket(server);
80-
81-
return new Response(null, {
82-
status: 101,
83-
webSocket: client,
84-
});
85-
}
86-
87-
async webSocketMessage(ws, message) {
88-
// Upon receiving a message from the client, reply with the same message,
89-
// but will prefix the message with "[Durable Object]: " and return the
90-
// total number of connections.
91-
ws.send(`[Durable Object] message: ${message}, connections: ${this.ctx.getWebSockets().length}`);
92-
}
93-
94-
async webSocketClose(ws, code, reason, wasClean) {
95-
// If the client closes the connection, the runtime will invoke the webSocketClose() handler.
96-
ws.close(code, "Durable Object is closing WebSocket");
97-
}
63+
// Keep track of all WebSocket connections
64+
sessions = new Map();
65+
66+
async fetch(request) {
67+
// Creates two ends of a WebSocket connection.
68+
const webSocketPair = new WebSocketPair();
69+
const [client, server] = Object.values(webSocketPair);
70+
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+
});
116+
117+
// Send a message to all WebSocket connections except the sender, loop over all the connected WebSockets and filter out the sender.
118+
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+
);
123+
}
124+
});
125+
}
126+
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");
130+
}
98131
}
99-
100132
```
101133

102134
</TabItem> <TabItem label="TypeScript" icon="seti:typescript">
103135

104136
```ts
105137
import { DurableObject } from "cloudflare:workers";
106138

107-
export interface Env {
108-
WEBSOCKET_HIBERNATION_SERVER: DurableObjectNamespace<WebSocketHibernationServer>;
109-
}
139+
// Use npm run cf-typegen to generate the type definitions for the Durable Object
110140

111141
// Worker
112142
export default {
113-
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
114-
if (request.url.endsWith("/websocket")) {
115-
// Expect to receive a WebSocket Upgrade request.
116-
// If there is one, accept the request and return a WebSocket Response.
117-
const upgradeHeader = request.headers.get('Upgrade');
118-
if (!upgradeHeader || upgradeHeader !== 'websocket') {
119-
return new Response('Durable Object expected Upgrade: websocket', { status: 426 });
120-
}
121-
122-
// This example will refer to the same Durable Object,
123-
// since the name "foo" is hardcoded.
124-
let id = env.WEBSOCKET_HIBERNATION_SERVER.idFromName("foo");
125-
let stub = env.WEBSOCKET_HIBERNATION_SERVER.get(id);
126-
127-
return stub.fetch(request);
128-
}
129-
130-
return new Response(null, {
131-
status: 400,
132-
statusText: 'Bad Request',
133-
headers: {
134-
'Content-Type': 'text/plain',
135-
},
136-
});
137-
}
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+
},
138174
};
139175

140176
// Durable Object
141177
export class WebSocketHibernationServer extends DurableObject {
142-
143-
async fetch(request: Request): Promise<Response> {
144-
// Creates two ends of a WebSocket connection.
145-
const webSocketPair = new WebSocketPair();
146-
const [client, server] = Object.values(webSocketPair);
147-
148-
// Calling `acceptWebSocket()` informs the runtime that this WebSocket is to begin terminating
149-
// request within the Durable Object. It has the effect of "accepting" the connection,
150-
// and allowing the WebSocket to send and receive messages.
151-
// Unlike `ws.accept()`, `state.acceptWebSocket(ws)` informs the Workers Runtime that the WebSocket
152-
// is "hibernatable", so the runtime does not need to pin this Durable Object to memory while
153-
// the connection is open. During periods of inactivity, the Durable Object can be evicted
154-
// from memory, but the WebSocket connection will remain open. If at some later point the
155-
// WebSocket receives a message, the runtime will recreate the Durable Object
156-
// (run the `constructor`) and deliver the message to the appropriate handler.
157-
this.ctx.acceptWebSocket(server);
158-
159-
return new Response(null, {
160-
status: 101,
161-
webSocket: client,
162-
});
163-
}
164-
165-
async webSocketMessage(ws: WebSocket, message: ArrayBuffer | string) {
166-
// Upon receiving a message from the client, the server replies with the same message,
167-
// and the total number of connections with the "[Durable Object]: " prefix
168-
ws.send(`[Durable Object] message: ${message}, connections: ${this.ctx.getWebSockets().length}`);
169-
}
170-
171-
async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) {
172-
// If the client closes the connection, the runtime will invoke the webSocketClose() handler.
173-
ws.close(code, "Durable Object is closing WebSocket");
174-
}
178+
// Keep track of all WebSocket connections
179+
sessions = new Map()<WebSocket, any>;
180+
181+
async fetch(request: Request): Promise<Response> {
182+
// Creates two ends of a WebSocket connection.
183+
const webSocketPair = new WebSocketPair();
184+
const [client, server] = Object.values(webSocketPair);
185+
186+
// Calling `acceptWebSocket()` informs the runtime that this WebSocket is to begin terminating
187+
// request within the Durable Object. It has the effect of "accepting" the connection,
188+
// and allowing the WebSocket to send and receive messages.
189+
// Unlike `ws.accept()`, `state.acceptWebSocket(ws)` informs the Workers Runtime that the WebSocket
190+
// is "hibernatable", so the runtime does not need to pin this Durable Object to memory while
191+
// the connection is open. During periods of inactivity, the Durable Object can be evicted
192+
// from memory, but the WebSocket connection will remain open. If at some later point the
193+
// WebSocket receives a message, the runtime will recreate the Durable Object
194+
// (run the `constructor`) and deliver the message to the appropriate handler.
195+
this.ctx.acceptWebSocket(server);
196+
197+
// Keep a copy of value in memory to survive hibernation.
198+
this.sessions.set(server, {});
199+
200+
return new Response(null, {
201+
status: 101,
202+
webSocket: client,
203+
});
204+
}
205+
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+
}
218+
219+
// Upon receiving a message from the client, the server replies with the same message,
220+
// 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+
);
224+
225+
// 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+
);
230+
});
231+
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+
);
238+
}
239+
});
240+
}
241+
242+
async webSocketClose(
243+
ws: WebSocket,
244+
code: number,
245+
reason: string,
246+
wasClean: boolean,
247+
) {
248+
// If the client closes the connection, the runtime will invoke the webSocketClose() handler.
249+
ws.close(code, "Durable Object is closing WebSocket");
250+
}
175251
}
176252
```
177253

@@ -193,4 +269,4 @@ new_classes = ["WebSocketHibernationServer"]
193269

194270
### Related resources
195271

196-
* [Durable Objects: Edge Chat Demo with Hibernation](https://github.com/cloudflare/workers-chat-demo/).
272+
- [Durable Objects: Edge Chat Demo with Hibernation](https://github.com/cloudflare/workers-chat-demo/).

0 commit comments

Comments
 (0)