-
Notifications
You must be signed in to change notification settings - Fork 10.4k
[DO] Update WebSocket Hibernation example #17670
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
5c47060
6b89f8a
52f50db
434f2f1
f56ab06
9929e5c
92653af
02c5345
c515635
7581e5e
80eebe9
c168325
b53bdbc
7be8bef
de02459
a50504c
2233120
62aa3bc
f258209
9fe36f4
64e82c2
6257297
313f97f
f2741f7
43e500c
a3ee4d9
5b590c1
5cf63d9
5697425
f66bedd
9a75621
24ac9ba
04573a0
98d4377
1b4c0a2
82fdeef
51f64fb
4314f9e
9377db1
e4cc852
82c8175
f39203f
0b81e1a
6e5c249
781d838
1fd66fd
ad0478d
7d0c6f6
77dca3d
e93a832
3441384
3faf293
91f83f8
82904ea
671657f
17db83b
1f5d1f9
6f60406
d79bda8
3791833
b4b2d3d
9f55f2e
f588ade
a65fbe1
cb5644f
4fc9579
8c9b3fd
c731b6d
b013c8c
e575e1e
57a1bb2
ab16ad0
7bd219b
17c16f1
77839c9
9535480
b6f6fef
2df9a89
b748c3f
92ee379
de23824
66556b5
f6982fc
4d5a7e1
1054045
ca22b1d
12287c9
347e4a3
5e7a99f
dd92a5f
76f4c39
d281158
e3a6f02
2a4d9dd
30e40c2
f31435e
f56d2d9
f4f59a3
26f5b1d
5f85359
106db71
918222f
7cd80c7
53663f0
1f07180
15307a4
4222241
8f4a682
f742068
cf500af
6a49959
34ca5e0
a1e3d06
73194da
63762ee
9b8fa5a
e7731fd
543fe38
87ea1d7
91d14ed
241fc4d
3527d2a
eee650f
9eb7fce
05ca6fc
7b0bec6
ade662f
b2dbf8e
2774f91
9169427
9f08abb
88618e8
1fa8799
05dd01b
55912cf
eb59dfc
d97e868
c9c5e7d
3d62f00
af0e312
bb336f9
48e9ce6
33da5b9
29938ff
9842349
166383a
94b2cc7
dbc1071
a97f108
d452e09
4ec319e
342f2ca
0091245
fb61c3d
f37a992
324384b
0722cd5
f3c4007
0537687
56d61fb
7dec6af
b587130
416af5b
a29a750
4a7f468
46bbd57
3e9d948
80d414d
c99de12
003b103
b89aaa3
3f9ee44
05071b5
78ea89a
72ed69b
0ffb4ee
935df31
4b20424
cd5d699
344a52d
a027957
4cf4143
a13ed52
5c9fe29
c1194eb
2b8946d
f3e470c
558d377
ca92793
c69fa0b
0a170c4
40dcc04
97493b8
6de55d0
76b9c77
8288886
93bda50
8944c6e
3873cc5
74d7c66
3b0c325
945bcde
7b3a863
3448a26
31b89cd
d60b7bf
3affa04
c1079fb
0e3f7b5
69595fd
1753fce
09498fc
8c4691a
fac59d1
8a7810c
ded54b0
e6e8a0d
f16062d
1043cc8
4ce9ada
158927d
8b0e821
6ee7150
7fcf1b6
8c010d1
7865c5f
0ef6ac9
f263d21
89a0104
600cc8d
7537e41
9f68971
c6d5020
bc72924
07c8d3b
d74df08
b5c8e0d
527098c
b4bb839
46b7277
09350bb
d5f53ed
d1bbd24
58904d4
e2ec09f
01d7500
da4efa5
6c1e69a
bd817b7
06073ff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,19 +10,16 @@ sidebar: | |
| order: 3 | ||
| description: Build a WebSocket server using WebSocket Hibernation on Durable | ||
| Objects and Workers. | ||
|
|
||
| --- | ||
|
|
||
| import { TabItem, Tabs } from "~/components" | ||
| import { TabItem, Tabs } from "~/components"; | ||
|
|
||
| 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/). | ||
|
|
||
| :::note | ||
|
|
||
|
|
||
| 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). | ||
|
|
||
|
|
||
| ::: | ||
|
|
||
| <Tabs> <TabItem label="JavaScript" icon="seti:javascript"> | ||
|
|
@@ -32,146 +29,225 @@ import { DurableObject } from "cloudflare:workers"; | |
|
|
||
| // Worker | ||
| export default { | ||
| async fetch(request, env, ctx) { | ||
| if (request.url.endsWith("/websocket")) { | ||
| // Expect to receive a WebSocket Upgrade request. | ||
| // If there is one, accept the request and return a WebSocket Response. | ||
| const upgradeHeader = request.headers.get('Upgrade'); | ||
| if (!upgradeHeader || upgradeHeader !== 'websocket') { | ||
| return new Response('Durable Object expected Upgrade: websocket', { status: 426 }); | ||
| } | ||
|
|
||
| // This example will refer to the same Durable Object, | ||
| // since the name "foo" is hardcoded. | ||
| let id = env.WEBSOCKET_HIBERNATION_SERVER.idFromName("foo"); | ||
| let stub = env.WEBSOCKET_HIBERNATION_SERVER.get(id); | ||
|
|
||
| return stub.fetch(request); | ||
| } | ||
|
|
||
| return new Response(null, { | ||
| status: 400, | ||
| statusText: 'Bad Request', | ||
| headers: { | ||
| 'Content-Type': 'text/plain', | ||
| }, | ||
| }); | ||
| } | ||
| async fetch(request, env, ctx) { | ||
| if (request.url.endsWith("/websocket")) { | ||
| // Expect to receive a WebSocket Upgrade request. | ||
| // If there is one, accept the request and return a WebSocket Response. | ||
| const upgradeHeader = request.headers.get("Upgrade"); | ||
| if (!upgradeHeader || upgradeHeader !== "websocket") { | ||
| return new Response("Durable Object expected Upgrade: websocket", { | ||
| status: 426, | ||
| }); | ||
| } | ||
|
|
||
| // This example will refer to the same Durable Object, | ||
| // since the name "foo" is hardcoded. | ||
| let id = env.WEBSOCKET_HIBERNATION_SERVER.idFromName("foo"); | ||
| let stub = env.WEBSOCKET_HIBERNATION_SERVER.get(id); | ||
|
|
||
| return stub.fetch(request); | ||
| } | ||
|
|
||
| return new Response(null, { | ||
| status: 400, | ||
| statusText: "Bad Request", | ||
| headers: { | ||
| "Content-Type": "text/plain", | ||
| }, | ||
| }); | ||
| }, | ||
| }; | ||
|
|
||
| // Durable Object | ||
| export class WebSocketHibernationServer extends DurableObject { | ||
|
|
||
| async fetch(request) { | ||
| // Creates two ends of a WebSocket connection. | ||
| const webSocketPair = new WebSocketPair(); | ||
| const [client, server] = Object.values(webSocketPair); | ||
|
|
||
| // Calling `acceptWebSocket()` informs the runtime that this WebSocket is to begin terminating | ||
| // request within the Durable Object. It has the effect of "accepting" the connection, | ||
| // and allowing the WebSocket to send and receive messages. | ||
| // Unlike `ws.accept()`, `state.acceptWebSocket(ws)` informs the Workers Runtime that the WebSocket | ||
| // is "hibernatable", so the runtime does not need to pin this Durable Object to memory while | ||
| // the connection is open. During periods of inactivity, the Durable Object can be evicted | ||
| // from memory, but the WebSocket connection will remain open. If at some later point the | ||
| // WebSocket receives a message, the runtime will recreate the Durable Object | ||
| // (run the `constructor`) and deliver the message to the appropriate handler. | ||
| this.ctx.acceptWebSocket(server); | ||
|
|
||
| return new Response(null, { | ||
| status: 101, | ||
| webSocket: client, | ||
| }); | ||
| } | ||
|
|
||
| async webSocketMessage(ws, message) { | ||
| // Upon receiving a message from the client, reply with the same message, | ||
| // but will prefix the message with "[Durable Object]: " and return the | ||
| // total number of connections. | ||
| ws.send(`[Durable Object] message: ${message}, connections: ${this.ctx.getWebSockets().length}`); | ||
| } | ||
|
|
||
| async webSocketClose(ws, code, reason, wasClean) { | ||
| // If the client closes the connection, the runtime will invoke the webSocketClose() handler. | ||
| ws.close(code, "Durable Object is closing WebSocket"); | ||
| } | ||
| // Keep track of all WebSocket connections | ||
| sessions = new Map(); | ||
|
|
||
| async fetch(request) { | ||
| // Creates two ends of a WebSocket connection. | ||
| const webSocketPair = new WebSocketPair(); | ||
| const [client, server] = Object.values(webSocketPair); | ||
|
|
||
| // Calling `acceptWebSocket()` informs the runtime that this WebSocket is to begin terminating | ||
| // request within the Durable Object. It has the effect of "accepting" the connection, | ||
| // and allowing the WebSocket to send and receive messages. | ||
| // Unlike `ws.accept()`, `state.acceptWebSocket(ws)` informs the Workers Runtime that the WebSocket | ||
| // is "hibernatable", so the runtime does not need to pin this Durable Object to memory while | ||
| // the connection is open. During periods of inactivity, the Durable Object can be evicted | ||
| // from memory, but the WebSocket connection will remain open. If at some later point the | ||
| // WebSocket receives a message, the runtime will recreate the Durable Object | ||
| // (run the `constructor`) and deliver the message to the appropriate handler. | ||
| this.ctx.acceptWebSocket(server); | ||
|
|
||
| // Keep a copy of value in memory to survive hibernation. | ||
|
||
| this.sessions.set(server, {}); | ||
|
|
||
| return new Response(null, { | ||
| status: 101, | ||
| webSocket: client, | ||
| }); | ||
| } | ||
|
|
||
| async webSocketMessage(sender, message) { | ||
| // Upon receiving a message, get the session associated with the WebSocket connection. | ||
| const session = this.sessions.get(sender); | ||
|
||
|
|
||
| // If it is a new connection, generate a new ID for the session. | ||
|
||
| if (!session.id) { | ||
| session.id = crypto.randomUUID(); | ||
| sender.serializeAttachment({ | ||
|
||
| ...sender.deserializeAttachment(), | ||
| id: session.id, | ||
| }); | ||
| } | ||
|
|
||
| // Upon receiving a message from the client, the server replies with the same message, | ||
| // and the total number of connections with the "[Durable Object]: " prefix | ||
| sender.send( | ||
| `[Durable Object] message: ${message}, from: ${session.id}. Total connections: ${this.ctx.getWebSockets().length}`, | ||
| ); | ||
|
|
||
| // Send a message to all WebSocket connections, loop over all the connected WebSockets. | ||
| this.ctx.getWebSockets().forEach((ws) => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just FYI calling This means that if you call I don't think we actually explain this well in the docs, but yeah, just something you might wanna know! |
||
| ws.send( | ||
| `[Durable Object] message: ${message}, from: ${session.id}. Total connections: ${this.ctx.getWebSockets().length}`, | ||
| ); | ||
| }); | ||
|
|
||
| // Send a message to all WebSocket connections except the sender, loop over all the connected WebSockets and filter out the sender. | ||
| this.ctx.getWebSockets().forEach((ws) => { | ||
| if (ws !== sender) { | ||
| ws.send( | ||
| `[Durable Object] message: ${message}, from: ${session.id}. Total connections: ${this.ctx.getWebSockets().length}`, | ||
| ); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| async webSocketClose(ws, code, reason, wasClean) { | ||
| // If the client closes the connection, the runtime will invoke the webSocketClose() handler. | ||
| ws.close(code, "Durable Object is closing WebSocket"); | ||
| } | ||
| } | ||
|
|
||
| ``` | ||
|
|
||
| </TabItem> <TabItem label="TypeScript" icon="seti:typescript"> | ||
|
|
||
| ```ts | ||
| import { DurableObject } from "cloudflare:workers"; | ||
|
|
||
| export interface Env { | ||
| WEBSOCKET_HIBERNATION_SERVER: DurableObjectNamespace<WebSocketHibernationServer>; | ||
| } | ||
| // Use npm run cf-typegen to generate the type definitions for the Durable Object | ||
|
|
||
| // Worker | ||
| export default { | ||
| async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> { | ||
| if (request.url.endsWith("/websocket")) { | ||
| // Expect to receive a WebSocket Upgrade request. | ||
| // If there is one, accept the request and return a WebSocket Response. | ||
| const upgradeHeader = request.headers.get('Upgrade'); | ||
| if (!upgradeHeader || upgradeHeader !== 'websocket') { | ||
| return new Response('Durable Object expected Upgrade: websocket', { status: 426 }); | ||
| } | ||
|
|
||
| // This example will refer to the same Durable Object, | ||
| // since the name "foo" is hardcoded. | ||
| let id = env.WEBSOCKET_HIBERNATION_SERVER.idFromName("foo"); | ||
| let stub = env.WEBSOCKET_HIBERNATION_SERVER.get(id); | ||
|
|
||
| return stub.fetch(request); | ||
| } | ||
|
|
||
| return new Response(null, { | ||
| status: 400, | ||
| statusText: 'Bad Request', | ||
| headers: { | ||
| 'Content-Type': 'text/plain', | ||
| }, | ||
| }); | ||
| } | ||
| async fetch( | ||
| request: Request, | ||
| env: Env, | ||
| ctx: ExecutionContext, | ||
| ): Promise<Response> { | ||
| if (request.url.endsWith("/websocket")) { | ||
| // Expect to receive a WebSocket Upgrade request. | ||
| // If there is one, accept the request and return a WebSocket Response. | ||
| const upgradeHeader = request.headers.get("Upgrade"); | ||
| if (!upgradeHeader || upgradeHeader !== "websocket") { | ||
| return new Response("Durable Object expected Upgrade: websocket", { | ||
| status: 426, | ||
| }); | ||
| } | ||
|
|
||
| // This example will refer to the same Durable Object, | ||
| // since the name "foo" is hardcoded. | ||
| let id = env.WEBSOCKET_HIBERNATION_SERVER.idFromName("foo"); | ||
| let stub = env.WEBSOCKET_HIBERNATION_SERVER.get(id); | ||
|
|
||
| return stub.fetch(request); | ||
| } | ||
|
|
||
| return new Response(null, { | ||
| status: 400, | ||
| statusText: "Bad Request", | ||
| headers: { | ||
| "Content-Type": "text/plain", | ||
| }, | ||
| }); | ||
| }, | ||
| }; | ||
|
|
||
| // Durable Object | ||
| export class WebSocketHibernationServer extends DurableObject { | ||
|
|
||
| async fetch(request: Request): Promise<Response> { | ||
| // Creates two ends of a WebSocket connection. | ||
| const webSocketPair = new WebSocketPair(); | ||
| const [client, server] = Object.values(webSocketPair); | ||
|
|
||
| // Calling `acceptWebSocket()` informs the runtime that this WebSocket is to begin terminating | ||
| // request within the Durable Object. It has the effect of "accepting" the connection, | ||
| // and allowing the WebSocket to send and receive messages. | ||
| // Unlike `ws.accept()`, `state.acceptWebSocket(ws)` informs the Workers Runtime that the WebSocket | ||
| // is "hibernatable", so the runtime does not need to pin this Durable Object to memory while | ||
| // the connection is open. During periods of inactivity, the Durable Object can be evicted | ||
| // from memory, but the WebSocket connection will remain open. If at some later point the | ||
| // WebSocket receives a message, the runtime will recreate the Durable Object | ||
| // (run the `constructor`) and deliver the message to the appropriate handler. | ||
| this.ctx.acceptWebSocket(server); | ||
|
|
||
| return new Response(null, { | ||
| status: 101, | ||
| webSocket: client, | ||
| }); | ||
| } | ||
|
|
||
| async webSocketMessage(ws: WebSocket, message: ArrayBuffer | string) { | ||
| // Upon receiving a message from the client, the server replies with the same message, | ||
| // and the total number of connections with the "[Durable Object]: " prefix | ||
| ws.send(`[Durable Object] message: ${message}, connections: ${this.ctx.getWebSockets().length}`); | ||
| } | ||
|
|
||
| async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) { | ||
| // If the client closes the connection, the runtime will invoke the webSocketClose() handler. | ||
| ws.close(code, "Durable Object is closing WebSocket"); | ||
| } | ||
| // Keep track of all WebSocket connections | ||
| sessions = new Map()<WebSocket, any>; | ||
|
|
||
| async fetch(request: Request): Promise<Response> { | ||
| // Creates two ends of a WebSocket connection. | ||
| const webSocketPair = new WebSocketPair(); | ||
| const [client, server] = Object.values(webSocketPair); | ||
|
|
||
| // Calling `acceptWebSocket()` informs the runtime that this WebSocket is to begin terminating | ||
| // request within the Durable Object. It has the effect of "accepting" the connection, | ||
| // and allowing the WebSocket to send and receive messages. | ||
| // Unlike `ws.accept()`, `state.acceptWebSocket(ws)` informs the Workers Runtime that the WebSocket | ||
| // is "hibernatable", so the runtime does not need to pin this Durable Object to memory while | ||
| // the connection is open. During periods of inactivity, the Durable Object can be evicted | ||
| // from memory, but the WebSocket connection will remain open. If at some later point the | ||
| // WebSocket receives a message, the runtime will recreate the Durable Object | ||
| // (run the `constructor`) and deliver the message to the appropriate handler. | ||
| this.ctx.acceptWebSocket(server); | ||
|
|
||
| // Keep a copy of value in memory to survive hibernation. | ||
| this.sessions.set(server, {}); | ||
|
|
||
| return new Response(null, { | ||
| status: 101, | ||
| webSocket: client, | ||
| }); | ||
| } | ||
|
|
||
| async webSocketMessage(sender: WebSocket, message: ArrayBuffer | string) { | ||
| // Upon receiving a message, get the session associated with the WebSocket connection. | ||
| const session = this.sessions.get(sender); | ||
|
|
||
| // If it is a new connection, generate a new ID for the session. | ||
| if (!session.id) { | ||
| session.id = crypto.randomUUID(); | ||
| sender.serializeAttachment({ | ||
| ...sender.deserializeAttachment(), | ||
| id: session.id, | ||
| }); | ||
| } | ||
|
|
||
| // Upon receiving a message from the client, the server replies with the same message, | ||
| // and the total number of connections with the "[Durable Object]: " prefix | ||
| sender.send( | ||
| `[Durable Object] message: ${message}, from: ${session.id}. Total connections: ${this.ctx.getWebSockets().length}`, | ||
| ); | ||
|
|
||
| // Send a message to all WebSocket connections, loop over all the connected WebSockets. | ||
| this.ctx.getWebSockets().forEach((ws) => { | ||
| ws.send( | ||
| `[Durable Object] message: ${message}, from: ${session.id}. Total connections: ${this.ctx.getWebSockets().length}`, | ||
| ); | ||
| }); | ||
|
|
||
| // Send a message to all WebSocket connections except the sender, loop over all the connected WebSockets and filter out the sender. | ||
| this.ctx.getWebSockets().forEach((ws) => { | ||
| if (ws !== sender) { | ||
| ws.send( | ||
| `[Durable Object] message: ${message}, from: ${session.id}. Total connections: ${this.ctx.getWebSockets().length}`, | ||
| ); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| async webSocketClose( | ||
| ws: WebSocket, | ||
| code: number, | ||
| reason: string, | ||
| wasClean: boolean, | ||
| ) { | ||
| // If the client closes the connection, the runtime will invoke the webSocketClose() handler. | ||
| ws.close(code, "Durable Object is closing WebSocket"); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
|
|
@@ -193,4 +269,4 @@ new_classes = ["WebSocketHibernationServer"] | |
|
|
||
| ### Related resources | ||
|
|
||
| * [Durable Objects: Edge Chat Demo with Hibernation](https://github.com/cloudflare/workers-chat-demo/). | ||
| - [Durable Objects: Edge Chat Demo with Hibernation](https://github.com/cloudflare/workers-chat-demo/). | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is lost every time we hibernate FYI. You would probably want to reconstruct it whenever the
constructor()runs.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What Milan said, but you probably want something like
You'll also have to
ws.serializeAttachmentevery time that you update sessions.