-
Notifications
You must be signed in to change notification settings - Fork 9.9k
Update WebSocket examples #18535
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
Update WebSocket examples #18535
Changes from 6 commits
f2b809e
d6cf904
82adbcd
92049d3
572bdb9
a19f1b7
a1f4e74
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 | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,7 +12,7 @@ description: Build a WebSocket server using WebSocket Hibernation on Durable | |||||||
| Objects and Workers. | ||||||||
| --- | ||||||||
|
|
||||||||
| import { TabItem, Tabs, WranglerConfig } from "~/components"; | ||||||||
| import { TypeScriptExample, WranglerConfig } 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/best-practices/websockets/). | ||||||||
|
|
||||||||
|
|
@@ -22,171 +22,141 @@ WebSocket Hibernation is unavailable for outgoing WebSocket use cases. Hibernati | |||||||
|
|
||||||||
| ::: | ||||||||
|
|
||||||||
| <Tabs> <TabItem label="JavaScript" icon="seti:javascript"> | ||||||||
|
|
||||||||
| ```js | ||||||||
| 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", | ||||||||
| }, | ||||||||
| }); | ||||||||
| }, | ||||||||
| }; | ||||||||
|
|
||||||||
| // 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"); | ||||||||
| } | ||||||||
| } | ||||||||
| ``` | ||||||||
|
|
||||||||
| </TabItem> <TabItem label="TypeScript" icon="seti:typescript"> | ||||||||
|
|
||||||||
| <TypeScriptExample> | ||||||||
| ```ts | ||||||||
| import { DurableObject } from "cloudflare:workers"; | ||||||||
|
|
||||||||
| export interface Env { | ||||||||
| WEBSOCKET_HIBERNATION_SERVER: DurableObjectNamespace<WebSocketHibernationServer>; | ||||||||
| } | ||||||||
| import { DurableObject } from 'cloudflare:workers'; | ||||||||
|
|
||||||||
| // 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('Worker expected Upgrade: websocket', { | ||||||||
| status: 426, | ||||||||
| }); | ||||||||
| } | ||||||||
|
|
||||||||
| if (request.method !== 'GET') { | ||||||||
| return new Response('Worker expected GET method', { | ||||||||
| status: 400, | ||||||||
| }); | ||||||||
| } | ||||||||
|
|
||||||||
| // Since we are hard coding the Durable Object ID by providing the constant name 'foo', | ||||||||
| // all requests to this Worker will be sent to the same Durable Object instance. | ||||||||
| let id = env.WEBSOCKET_HIBERNATION_SERVER.idFromName('foo'); | ||||||||
| let stub = env.WEBSOCKET_HIBERNATION_SERVER.get(id); | ||||||||
|
|
||||||||
| return stub.fetch(request); | ||||||||
| } | ||||||||
|
|
||||||||
| return new Response( | ||||||||
| `Supported endpoints: | ||||||||
| /websocket: Expects a WebSocket upgrade request`, | ||||||||
| { | ||||||||
| status: 200, | ||||||||
| 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"); | ||||||||
| } | ||||||||
| // Keeps track of all WebSocket connections | ||||||||
| // When the DO hibernates, gets reconstructed in the constructor | ||||||||
| sessions: Map<WebSocket, { [key: string]: string }>; | ||||||||
|
|
||||||||
| constructor(ctx: DurableObjectState, env: Env) { | ||||||||
| super(ctx, env); | ||||||||
| this.sessions = new Map(); | ||||||||
|
|
||||||||
| // As part of constructing the Durable Object, | ||||||||
| // we wake up any hibernating WebSockets and | ||||||||
| // place them back in the `sessions` map. | ||||||||
|
|
||||||||
| // Get all WebSocket connections from the DO | ||||||||
| this.ctx.getWebSockets().forEach((ws) => { | ||||||||
| let attachment = ws.deserializeAttachment(); | ||||||||
| if (ws.deserializeAttachment()) { | ||||||||
| // If we previously attached state to our WebSocket, | ||||||||
| // let's add it to `sessions` map to restore the state of the connection. | ||||||||
| const { ...session } = attachment; | ||||||||
| this.sessions.set(ws, { ...session }); | ||||||||
|
Comment on lines
+89
to
+90
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. Why the spreading and then recollecting? Just pass
Suggested change
|
||||||||
| } | ||||||||
| }); | ||||||||
|
|
||||||||
| // Sets an application level auto response that does not wake hibernated WebSockets. | ||||||||
| this.ctx.setWebSocketAutoResponse(new WebSocketRequestResponsePair('ping', 'pong')); | ||||||||
| } | ||||||||
|
|
||||||||
| 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()`, `this.ctx.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); | ||||||||
|
|
||||||||
| // Generate a random UUID for the session. | ||||||||
| const id = crypto.randomUUID(); | ||||||||
|
|
||||||||
| // Attach the session ID to the WebSocket connection and serialize it. | ||||||||
| // This is necessary to restore the state of the connection when the Durable Object wakes up. | ||||||||
| server.serializeAttachment({ id }); | ||||||||
|
|
||||||||
| // Add the WebSocket connection to the map of active sessions. | ||||||||
| this.sessions.set(server, { id }); | ||||||||
|
|
||||||||
| return new Response(null, { | ||||||||
| status: 101, | ||||||||
| webSocket: client, | ||||||||
| }); | ||||||||
| } | ||||||||
|
|
||||||||
| async webSocketMessage(ws: WebSocket, message: ArrayBuffer | string) { | ||||||||
| // Get the session associated with the WebSocket connection. | ||||||||
| const session = this.sessions.get(ws)!; | ||||||||
|
|
||||||||
| // Upon receiving a message from the client, the server replies with the same message, the session ID of the connection, | ||||||||
| // and the total number of connections with the "[Durable Object]: " prefix | ||||||||
| ws.send(`[Durable Object] message: ${message}, from: ${session.id}. Total connections: ${this.sessions.size}`); | ||||||||
|
|
||||||||
| // Send a message to all WebSocket connections, loop over all the connected WebSockets. | ||||||||
| this.sessions.forEach((attachment, session) => { | ||||||||
| session.send(`[Durable Object] message: ${message}, from: ${attachment.id}. Total connections: ${this.sessions.size}`); | ||||||||
| }); | ||||||||
|
|
||||||||
| // Send a message to all WebSocket connections except the connection (ws), | ||||||||
| // loop over all the connected WebSockets and filter out the connection (ws). | ||||||||
| this.sessions.forEach((attachment, session) => { | ||||||||
| if (session !== ws) { | ||||||||
| session.send(`[Durable Object] message: ${message}, from: ${attachment.id}. Total connections: ${this.sessions.size}`); | ||||||||
| } | ||||||||
| }); | ||||||||
|
Comment on lines
+139
to
+149
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. Combine these two loops?
Contributor
Author
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. The loops are kept separate to make it easy for users to simply copy-paste based on their use-case. |
||||||||
| } | ||||||||
|
|
||||||||
| 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'); | ||||||||
| } | ||||||||
| } | ||||||||
| ``` | ||||||||
|
|
||||||||
| </TabItem> </Tabs> | ||||||||
| ``` | ||||||||
| </TypeScriptExample> | ||||||||
|
|
||||||||
| Finally, configure your Wrangler file to include a Durable Object [binding](/durable-objects/get-started/#4-configure-durable-object-bindings) and [migration](/durable-objects/reference/durable-objects-migrations/) based on the namespace and class name chosen previously. | ||||||||
|
|
||||||||
|
|
||||||||
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 seems wrong?