Skip to content

Commit de16403

Browse files
authored
Adds Python version of examples in best-practices/websockets (#26793)
1 parent 6244713 commit de16403

File tree

1 file changed

+138
-9
lines changed

1 file changed

+138
-9
lines changed

src/content/docs/durable-objects/best-practices/websockets.mdx

Lines changed: 138 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ To use WebSockets with Durable Objects, you first need to proxy the `request` ob
5151

5252
If an event occurs for a hibernated Durable Object's corresponding handler method, it will return to memory. This will call the Durable Object's constructor, so it is best to minimize work in the constructor when using WebSocket hibernation.
5353

54-
<Tabs> <TabItem label="JavaScript" icon="seti:javascript">
54+
<Tabs syncKey="workersExamples">
55+
<TabItem label="JavaScript" icon="seti:javascript">
5556

5657
```js
5758
import { DurableObject } from "cloudflare:workers";
@@ -89,7 +90,8 @@ export class WebSocketHibernationServer extends DurableObject {
8990
}
9091
```
9192

92-
</TabItem> <TabItem label="TypeScript" icon="seti:typescript">
93+
</TabItem>
94+
<TabItem label="TypeScript" icon="seti:typescript">
9395

9496
```ts
9597
import { DurableObject } from "cloudflare:workers";
@@ -136,7 +138,46 @@ export class WebSocketHibernationServer extends DurableObject {
136138
}
137139
```
138140

139-
</TabItem> </Tabs>
141+
</TabItem>
142+
<TabItem label="Python" icon="seti:python">
143+
```python
144+
from workers import Response, DurableObject
145+
from js import WebSocketPair
146+
147+
# Durable Object
148+
class WebSocketHibernationServer(DurableObject):
149+
def __init__(self, state, env):
150+
super().__init__(state, env)
151+
self.ctx = state
152+
153+
async def fetch(self, request):
154+
# Creates two ends of a WebSocket connection.
155+
client, server = WebSocketPair.new().object_values()
156+
157+
# Calling `acceptWebSocket()` connects the WebSocket to the Durable Object, allowing the WebSocket to send and receive messages.
158+
# Unlike `ws.accept()`, `state.acceptWebSocket(ws)` allows the Durable Object to be hibernated
159+
# When the Durable Object receives a message during Hibernation, it will run the `__init__` to be re-initialized
160+
self.ctx.acceptWebSocket(server)
161+
162+
return Response(
163+
None,
164+
status=101,
165+
web_socket=client
166+
)
167+
168+
async def webSocketMessage(self, ws, message):
169+
# Upon receiving a message from the client, reply with the same message,
170+
# but will prefix the message with "[Durable Object]: " and return the number of connections.
171+
ws.send(
172+
f"[Durable Object] message: {message}, connections: {len(self.ctx.get_websockets())}"
173+
)
174+
175+
async def webSocketClose(self, ws, code, reason, was_clean):
176+
# If the client closes the connection, the runtime will invoke the webSocketClose() handler.
177+
ws.close(code, "Durable Object is closing WebSocket")
178+
```
179+
</TabItem>
180+
</Tabs>
140181

141182
Similar to the [WebSocket Standard API example](/durable-objects/examples/websocket-server/), to execute this code, 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 <GlossaryTooltip term="namespace">namespace</GlossaryTooltip> and class name chosen previously.
142183

@@ -192,7 +233,8 @@ Both Workers and Durable Objects are billed, in part, based on the number of req
192233

193234
:::
194235

195-
<Tabs> <TabItem label="JavaScript" icon="seti:javascript">
236+
<Tabs syncKey="workersExamples">
237+
<TabItem label="JavaScript" icon="seti:javascript">
196238

197239
```js
198240
// Worker
@@ -232,7 +274,8 @@ export default {
232274
};
233275
```
234276

235-
</TabItem> <TabItem label="TypeScript" icon="seti:typescript">
277+
</TabItem>
278+
<TabItem label="TypeScript" icon="seti:typescript">
236279

237280
```ts
238281
// Worker
@@ -272,11 +315,52 @@ export default {
272315
} satisfies ExportedHandler<Env>;
273316
```
274317

275-
</TabItem> </Tabs>
318+
</TabItem>
319+
<TabItem label="Python" icon="seti:python">
320+
```python
321+
from workers import Response, WorkerEntrypoint
322+
323+
# Worker
324+
class Default(WorkerEntrypoint):
325+
async def fetch(self, request):
326+
if request.method == "GET" and request.url.endswith("/websocket"):
327+
# Expect to receive a WebSocket Upgrade request.
328+
# If there is one, accept the request and return a WebSocket Response.
329+
upgrade_header = request.headers.get("Upgrade")
330+
if not upgrade_header or upgrade_header != "websocket":
331+
return Response(
332+
None,
333+
status=426,
334+
status_text="Durable Object expected Upgrade: websocket",
335+
headers={
336+
"Content-Type": "text/plain",
337+
},
338+
)
339+
340+
# This example will refer to a single Durable Object instance, since the name "foo" is
341+
# hardcoded
342+
stub = self.env.WEBSOCKET_SERVER.getByName("foo")
343+
344+
# The Durable Object's fetch handler will accept the server side connection and return
345+
# the client
346+
return await stub.fetch(request)
347+
348+
return Response(
349+
None,
350+
status=400,
351+
status_text="Bad Request",
352+
headers={
353+
"Content-Type": "text/plain",
354+
},
355+
)
356+
```
357+
</TabItem>
358+
</Tabs>
276359

277360
Each WebSocket server in this example is represented by a Durable Object. This WebSocket server creates a single WebSocket connection and responds to all messages over that connection with the total number of accepted WebSocket connections. In the Durable Object's fetch handler we create client and server connections and add event listeners for relevant event types.
278361

279-
<Tabs> <TabItem label="JavaScript" icon="seti:javascript">
362+
<Tabs syncKey="workersExamples">
363+
<TabItem label="JavaScript" icon="seti:javascript">
280364

281365
```js
282366
import { DurableObject } from "cloudflare:workers";
@@ -321,7 +405,8 @@ export class WebSocketServer extends DurableObject {
321405
}
322406
```
323407

324-
</TabItem> <TabItem label="TypeScript" icon="seti:typescript">
408+
</TabItem>
409+
<TabItem label="TypeScript" icon="seti:typescript">
325410

326411
```ts
327412
// Durable Object
@@ -364,7 +449,51 @@ export class WebSocketServer extends DurableObject {
364449
}
365450
```
366451

367-
</TabItem> </Tabs>
452+
</TabItem>
453+
<TabItem label="Python" icon="seti:python">
454+
```python
455+
from workers import Response, DurableObject
456+
from js import WebSocketPair
457+
from pyodide.ffi import create_proxy
458+
459+
# Durable Object
460+
class WebSocketServer(DurableObject):
461+
def __init__(self, ctx, env):
462+
super().__init__(ctx, env)
463+
self.currently_connected_websockets = 0
464+
465+
async def fetch(self, request):
466+
# Creates two ends of a WebSocket connection.
467+
client, server = WebSocketPair.new().object_values()
468+
469+
# Calling `accept()` connects the WebSocket to this Durable Object
470+
server.accept()
471+
self.currently_connected_websockets += 1
472+
473+
# Upon receiving a message from the client, the server replies with the same message,
474+
# and the total number of connections with the "[Durable Object]: " prefix
475+
def on_message(event):
476+
server.send(
477+
f"[Durable Object] currentlyConnectedWebSockets: {self.currently_connected_websockets}"
478+
)
479+
480+
server.addEventListener("message", create_proxy(on_message))
481+
482+
# If the client closes the connection, the runtime will close the connection too.
483+
def on_close(event):
484+
self.currently_connected_websockets -= 1
485+
server.close(event.code, "Durable Object is closing WebSocket")
486+
487+
server.addEventListener("close", create_proxy(on_close))
488+
489+
return Response(
490+
None,
491+
status=101,
492+
web_socket=client,
493+
)
494+
```
495+
</TabItem>
496+
</Tabs>
368497

369498
To execute this code, 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 <GlossaryTooltip term="namespace">namespace</GlossaryTooltip> and class name chosen previously.
370499

0 commit comments

Comments
 (0)