Skip to content

Tweak use_channel_layer to allow custom group channel names #225

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/examples/python/use-channel-layer-group.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

@component
def my_sender_component():
sender = use_channel_layer("my-channel-name", group=True)
sender = use_channel_layer(group_name="my-group-name")

async def submit_event(event):
if event["key"] == "Enter":
Expand All @@ -23,7 +23,7 @@ def my_receiver_component_1():
async def receive_event(message):
set_message(message["text"])

use_channel_layer("my-channel-name", receiver=receive_event, group=True)
use_channel_layer(group_name="my-group-name", receiver=receive_event)

return html.div(f"Message Receiver 1: {message}")

Expand All @@ -35,6 +35,6 @@ def my_receiver_component_2():
async def receive_event(message):
set_message(message["text"])

use_channel_layer("my-channel-name", receiver=receive_event, group=True)
use_channel_layer(group_name="my-group-name", receiver=receive_event)

return html.div(f"Message Receiver 2: {message}")
5 changes: 2 additions & 3 deletions docs/examples/python/use-channel-layer-signal-sender.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
from django.dispatch import receiver


class ExampleModel(Model):
...
class ExampleModel(Model): ...


@receiver(pre_save, sender=ExampleModel)
Expand All @@ -17,4 +16,4 @@ def my_sender_signal(sender, instance, **kwargs):
async_to_sync(layer.send)("my-channel-name", {"text": "Hello World!"})

# Example of sending a message to a group channel
async_to_sync(layer.group_send)("my-channel-name", {"text": "Hello World!"})
async_to_sync(layer.group_send)("my-group-name", {"text": "Hello World!"})
31 changes: 18 additions & 13 deletions docs/src/reference/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,16 +340,18 @@ This is often used to create chat systems, synchronize data between components,

| Name | Type | Description | Default |
| --- | --- | --- | --- |
| `#!python name` | `#!python str` | The name of the channel to subscribe to. | N/A |
| `#!python receiver` | `#!python AsyncMessageReceiver | None` | An async function that receives a `#!python message: dict` from the channel layer. If more than one receiver waits on the same channel, a random one will get the result (unless `#!python group=True` is defined). | `#!python None` |
| `#!python group` | `#!python bool` | If `#!python True`, a "group channel" will be used. Messages sent within a group are broadcasted to all receivers on that channel. | `#!python False` |
| `#!python layer` | `#!python str` | The channel layer to use. These layers must be defined in `#!python settings.py:CHANNEL_LAYERS`. | `#!python 'default'` |
| `#!python name` | `#!python str | None` | The name of the channel to subscribe to. If you define a `#!python group_name`, you can keep `#!python name` undefined to auto-generate a unique name. | `#!python None` |
| `#!python group_name` | `#!python str | None` | If configured, any messages sent within this hook will be broadcasted to all channels in this group. | `#!python None` |
| `#!python group_add` | `#!python bool` | If `#!python True`, the channel will automatically be added to the group when the component mounts. | `#!python True` |
| `#!python group_discard` | `#!python bool` | If `#!python True`, the channel will automatically be removed from the group when the component dismounts. | `#!python True` |
| `#!python receiver` | `#!python AsyncMessageReceiver | None` | An async function that receives a `#!python message: dict` from a channel. If more than one receiver waits on the same channel name, a random receiver will get the result. | `#!python None` |
| `#!python layer` | `#!python str` | The channel layer to use. This layer must be defined in `#!python settings.py:CHANNEL_LAYERS`. | `#!python 'default'` |

<font size="4">**Returns**</font>

| Type | Description |
| --- | --- |
| `#!python AsyncMessageSender` | An async callable that can send a `#!python message: dict` |
| `#!python AsyncMessageSender` | An async callable that can send a `#!python message: dict`. |

??? warning "Extra Django configuration required"

Expand All @@ -359,13 +361,15 @@ This is often used to create chat systems, synchronize data between components,

In summary, you will need to:

1. Run the following command to install `channels-redis` in your Python environment.
1. Install [`redis`](https://redis.io/download/) on your machine.

2. Run the following command to install `channels-redis` in your Python environment.

```bash linenums="0"
pip install channels-redis
```

2. Configure your `settings.py` to use `RedisChannelLayer` as your layer backend.
3. Configure your `settings.py` to use `RedisChannelLayer` as your layer backend.

```python linenums="0"
CHANNEL_LAYERS = {
Expand All @@ -380,9 +384,9 @@ This is often used to create chat systems, synchronize data between components,

??? question "How do I broadcast a message to multiple components?"

By default, if more than one receiver waits on the same channel, a random one will get the result.
If more than one receiver waits on the same channel, a random one will get the result.

However, by defining `#!python group=True` you can configure a "group channel", which will broadcast messages to all receivers.
To get around this, you can define a `#!python group_name` to broadcast messages to all channels within a specific group. If you do not define a channel `#!python name` while using groups, ReactPy will automatically generate a unique channel name for you.

In the example below, all messages sent by the `#!python sender` component will be received by all `#!python receiver` components that exist (across every active client browser).

Expand All @@ -400,15 +404,16 @@ This is often used to create chat systems, synchronize data between components,

In the example below, the sender will send a signal every time `#!python ExampleModel` is saved. Then, when the receiver component gets this signal, it explicitly calls `#!python set_message(...)` to trigger a re-render.

=== "components.py"
=== "signals.py"

```python
{% include "../../examples/python/use-channel-layer-signal-receiver.py" %}
{% include "../../examples/python/use-channel-layer-signal-sender.py" %}
```
=== "signals.py"

=== "components.py"

```python
{% include "../../examples/python/use-channel-layer-signal-sender.py" %}
{% include "../../examples/python/use-channel-layer-signal-receiver.py" %}
```

---
Expand Down
72 changes: 41 additions & 31 deletions src/reactpy_django/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@


_logger = logging.getLogger(__name__)
_REFETCH_CALLBACKS: DefaultDict[
Callable[..., Any], set[Callable[[], None]]
] = DefaultDict(set)
_REFETCH_CALLBACKS: DefaultDict[Callable[..., Any], set[Callable[[], None]]] = (
DefaultDict(set)
)


# TODO: Deprecate this once the equivalent hook gets moved to reactpy.hooks.*
Expand Down Expand Up @@ -109,17 +109,15 @@ def use_query(
query: Callable[FuncParams, Awaitable[Inferred]] | Callable[FuncParams, Inferred],
*args: FuncParams.args,
**kwargs: FuncParams.kwargs,
) -> Query[Inferred]:
...
) -> Query[Inferred]: ...


@overload
def use_query(
query: Callable[FuncParams, Awaitable[Inferred]] | Callable[FuncParams, Inferred],
*args: FuncParams.args,
**kwargs: FuncParams.kwargs,
) -> Query[Inferred]:
...
) -> Query[Inferred]: ...


def use_query(*args, **kwargs) -> Query[Inferred]:
Expand Down Expand Up @@ -221,20 +219,20 @@ def register_refetch_callback() -> Callable[[], None]:
@overload
def use_mutation(
options: MutationOptions,
mutation: Callable[FuncParams, bool | None]
| Callable[FuncParams, Awaitable[bool | None]],
mutation: (
Callable[FuncParams, bool | None] | Callable[FuncParams, Awaitable[bool | None]]
),
refetch: Callable[..., Any] | Sequence[Callable[..., Any]] | None = None,
) -> Mutation[FuncParams]:
...
) -> Mutation[FuncParams]: ...


@overload
def use_mutation(
mutation: Callable[FuncParams, bool | None]
| Callable[FuncParams, Awaitable[bool | None]],
mutation: (
Callable[FuncParams, bool | None] | Callable[FuncParams, Awaitable[bool | None]]
),
refetch: Callable[..., Any] | Sequence[Callable[..., Any]] | None = None,
) -> Mutation[FuncParams]:
...
) -> Mutation[FuncParams]: ...


def use_mutation(*args: Any, **kwargs: Any) -> Mutation[FuncParams]:
Expand Down Expand Up @@ -327,8 +325,9 @@ def use_user() -> AbstractUser:


def use_user_data(
default_data: None
| dict[str, Callable[[], Any] | Callable[[], Awaitable[Any]] | Any] = None,
default_data: (
None | dict[str, Callable[[], Any] | Callable[[], Awaitable[Any]] | Any]
) = None,
save_default_data: bool = False,
) -> UserData:
"""Get or set user data stored within the REACTPY_DATABASE.
Expand Down Expand Up @@ -368,27 +367,37 @@ async def _set_user_data(data: dict):


def use_channel_layer(
name: str,
name: str | None = None,
*,
group_name: str | None = None,
group_add: bool = True,
group_discard: bool = True,
receiver: AsyncMessageReceiver | None = None,
group: bool = False,
layer: str = DEFAULT_CHANNEL_LAYER,
) -> AsyncMessageSender:
"""
Subscribe to a Django Channels layer to send/receive messages.

Args:
name: The name of the channel to subscribe to.
receiver: An async function that receives a `message: dict` from the channel layer. \
If more than one receiver waits on the same channel, a random one \
will get the result (unless `group=True` is defined).
group: If `True`, a "group channel" will be used. Messages sent within a \
group are broadcasted to all receivers on that channel.
layer: The channel layer to use. These layers must be defined in \
name: The name of the channel to subscribe to. If you define a `group_name`, you \
can keep `name` undefined to auto-generate a unique name.
group_name: If configured, any messages sent within this hook will be broadcasted \
to all channels in this group.
group_add: If `True`, the channel will automatically be added to the group \
when the component mounts.
group_discard: If `True`, the channel will automatically be removed from the \
group when the component dismounts.
receiver: An async function that receives a `message: dict` from a channel. \
If more than one receiver waits on the same channel name, a random receiver \
will get the result.
layer: The channel layer to use. This layer must be defined in \
`settings.py:CHANNEL_LAYERS`.
"""
channel_layer: InMemoryChannelLayer | RedisChannelLayer = get_channel_layer(layer)
channel_name = use_memo(lambda: str(uuid4() if group else name))
group_name = name if group else ""
channel_name = use_memo(lambda: str(name or uuid4()))

if not name and not group_name:
raise ValueError("You must define a `name` or `group_name` for the channel.")

if not channel_layer:
raise ValueError(
Expand All @@ -399,17 +408,18 @@ def use_channel_layer(
# Add/remove a group's channel during component mount/dismount respectively.
@use_effect(dependencies=[])
async def group_manager():
if group:
if group_name and group_add:
await channel_layer.group_add(group_name, channel_name)

if group_name and group_discard:
return lambda: asyncio.run(
channel_layer.group_discard(group_name, channel_name)
)

# Listen for messages on the channel using the provided `receiver` function.
@use_effect
async def message_receiver():
if not receiver or not channel_name:
if not receiver:
return

while True:
Expand All @@ -418,7 +428,7 @@ async def message_receiver():

# User interface for sending messages to the channel
async def message_sender(message: dict):
if group:
if group_name:
await channel_layer.group_send(group_name, message)
else:
await channel_layer.send(channel_name, message)
Expand Down
4 changes: 2 additions & 2 deletions tests/test_app/channel_layers/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def group_receiver(id: int):
async def receiver(message):
set_state(message["text"])

use_channel_layer("group-messenger", receiver=receiver, group=True)
use_channel_layer(receiver=receiver, group_name="group-messenger")

return html.div(
{"id": f"group-receiver-{id}", "data-message": state},
Expand All @@ -50,7 +50,7 @@ async def receiver(message):

@component
def group_sender():
sender = use_channel_layer("group-messenger", group=True)
sender = use_channel_layer(group_name="group-messenger")

async def submit_event(event):
if event["key"] == "Enter":
Expand Down