Skip to content

Commit 9e52e04

Browse files
committed
feat(event): replace dynamic method dispatch with receive_event for channel events
1 parent 55b3450 commit 9e52e04

File tree

3 files changed

+50
-39
lines changed

3 files changed

+50
-39
lines changed

chanx/generic/websocket.py

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,16 @@
2525
- M (optional): Model subclass for object-level permissions
2626
2727
Developers should subclass AsyncJsonWebsocketConsumer with appropriate generic parameters
28-
and implement the receive_message method to handle incoming messages. The consumer
29-
automatically handles connection lifecycle, authentication, message validation, and
30-
group messaging.
28+
and implement the receive_message method to handle incoming messages (and optionally
29+
the receive_event method to handle channel events). The consumer automatically handles
30+
connection lifecycle, authentication, message validation, and group messaging.
3131
"""
3232

3333
import asyncio
3434
import sys
3535
import uuid
3636
from abc import ABC, abstractmethod
37-
from collections.abc import Awaitable, Callable, Iterable, Sequence
37+
from collections.abc import Iterable, Sequence
3838
from types import ModuleType
3939
from typing import (
4040
Annotated,
@@ -123,6 +123,9 @@ class AsyncJsonWebsocketConsumer(
123123
124124
For typed channel events, subclasses can define a union type of channel events
125125
and use the Event generic parameter to enable type-safe channel event handling.
126+
Override the receive_event() method to process events sent via send_channel_event()
127+
or asend_channel_event(). Events are automatically validated against the Event
128+
type before being passed to your handler method.
126129
127130
For group messaging functionality, subclasses should also define the outgoing group
128131
message type using the OG generic parameter to enable proper validation and handling
@@ -705,12 +708,8 @@ async def handle_channel_event(self, event_payload: EventPayload) -> None:
705708
Internal dispatcher for typed channel events with completion signal.
706709
707710
This method is called by the channel layer when an event is sent to a group
708-
this consumer belongs to. It extracts the event data, validates it against
709-
the Event generic parameter, finds the appropriate handler method, and calls
710-
it with proper error handling.
711-
712-
The handler method name is determined by the event's 'handler' field, and it
713-
must be an async method on the consumer class that accepts the event as a parameter.
711+
this consumer belongs to. It validates the event data and forwards it to
712+
the receive_event method.
714713
715714
Args:
716715
event_payload: The message from the channel layer containing event data
@@ -722,20 +721,7 @@ async def handle_channel_event(self, event_payload: EventPayload) -> None:
722721

723722
assert event_data is not None
724723

725-
handler_name = event_data.handler
726-
727-
# Find and call the handler method
728-
handler_method: Callable[[Event], Awaitable[None]] | None = getattr(
729-
self, handler_name, None
730-
)
731-
if not callable(handler_method):
732-
await logger.aerror(
733-
f"Handler method '{handler_name}' is not available for sending event"
734-
)
735-
return
736-
737-
# Handler is async, await it
738-
await handler_method(event_data)
724+
await self.receive_event(event_data)
739725

740726
except Exception:
741727
await logger.aexception("Failed to process channel event")
@@ -745,6 +731,27 @@ async def handle_channel_event(self, event_payload: EventPayload) -> None:
745731
if self.send_completion:
746732
await self.send_message(CompleteMessage())
747733

734+
async def receive_event(self, event: Event) -> None:
735+
"""
736+
Process typed channel events received through the channel layer.
737+
738+
This method is called when a channel event is sent to a group this consumer
739+
belongs to. Override this method to handle events sent via send_channel_event()
740+
or asend_channel_event().
741+
742+
Channel events provide a way to send typed messages to consumers from outside
743+
the WebSocket connection (e.g., from Django views, tasks, or other consumers).
744+
Use pattern matching to handle different event types based on your Event
745+
generic parameter.
746+
747+
Args:
748+
event: The validated event object (typed based on Event generic parameter)
749+
750+
Note:
751+
This method is only called if your consumer defines the Event generic
752+
parameter. If Event is None, channel events are not supported.
753+
"""
754+
748755
# Helper methods
749756

750757
async def _handle_receive_json_and_signal_complete(

sandbox/discussion/consumers.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,13 @@ async def receive_message(
4949
),
5050
)
5151

52-
async def notify_people(self, event: NotifyEvent) -> None:
53-
notify_message = f"ATTENTION: {event.payload.content}"
54-
55-
await self.send_message(
56-
DiscussionMemberMessage(
57-
payload=DiscussionMessagePayload(content=notify_message)
58-
)
59-
)
52+
async def receive_event(self, event: DiscussionEvent) -> None:
53+
match event:
54+
case NotifyEvent():
55+
notify_message = f"ATTENTION: {event.payload.content}"
56+
57+
await self.send_message(
58+
DiscussionMemberMessage(
59+
payload=DiscussionMessagePayload(content=notify_message)
60+
)
61+
)

sandbox/test_chanx/generic/test_websocket_events.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,14 @@ async def receive_message(self, message: IncomingMessage, **kwargs: Any) -> None
5555
case PingMessage():
5656
await self.send_message(PongMessage())
5757

58-
async def notify(self, event: NotifyEvent) -> None:
59-
notify_message = f"ATTENTION: {event.payload.content}"
58+
async def receive_event(self, event: MyEvent) -> None:
59+
match event:
60+
case NotifyEvent():
61+
notify_message = f"ATTENTION: {event.payload.content}"
6062

61-
await self.send_message(ReplyMessage(payload=notify_message))
63+
await self.send_message(ReplyMessage(payload=notify_message))
64+
case _:
65+
raise ValueError("Unhandled event")
6266

6367

6468
class MyEventsConsumerTestCase(WebsocketTestCase):
@@ -108,10 +112,8 @@ async def test_sent_unhandled_event(self) -> None:
108112
)
109113
await asyncio.sleep(0.1) # wait for processing
110114
err_log = logs[0]
111-
assert (
112-
err_log["event"]
113-
== "Handler method 'unhandled' is not available for sending event"
114-
)
115+
assert err_log["event"] == "Failed to process channel event"
116+
assert "Unhandled event" in str(err_log["exc_info"])
115117
assert err_log["log_level"] == "error"
116118

117119
@override_chanx_settings(

0 commit comments

Comments
 (0)