reactpy
reactpy copied to clipboard
messenger pattern
By submitting this pull request you agree that all contributions to this project are made under the MIT license.
Issues
Currently, while the client and server have the ability to specify different message types, the server has no way of broadcasting messages other than layout events. Doing so is necessary for a number of different use cases...
- https://github.com/reactive-python/reactpy/discussions/1083#discussioncomment-6338943
- https://github.com/reactive-python/reactpy-router/issues/19
- https://github.com/reactive-python/reactpy/issues/894
- Fix #975
Solution
Implement a generic Messenger class that allows for users to send/receive arbitrary messages to/from the client. Users will gain access to a Messenger via the use_messenger hook. Usage could look like one of the following...
Using start_consumer and start_producer:
@component
def demo_producer_consumer():
msgr = use_messenger()
use_effect(lambda: msgr.start_consumer("my-message-type", my_message_consumer))
use_effect(lambda: msgr.start_producer(my_message_producer))
async def my_message_consumer(msg):
...
async def my_message_producer():
while True:
yield ...
Using send and receive:
@component
def demo_send_receive():
msgr = use_messenger()
@use_effect
async def start_consumer():
async for msg in msgr.consume("my-message-type"):
...
@use_effect
async def start_producer():
while True:
await msgr.send("my-message-type", ...)
Ultimately, the start_consumer and start_producer methods are convenience methods that call send and receive under the hood.
Checklist
- [ ] Tests have been included for all bug fixes or added functionality.
- [ ] The
changelog.rsthas been updated with any significant changes.
Unfortunately, async for msg in msgr.consume("my-message-type"): is fundamentally flawed since it relies on our async use_effect.
We would need a stateful async paradigm, similar to Django Channels consumers.
At some point we need to figure out async effects. I don't think that should be a blocker here.
Edit: see https://github.com/reactive-python/reactpy/pull/1090
After today's tag up, two theoretical interfaces could look like this
Notes
- Use messenger will never restart a consumer that is already running
- We should timeout the consumer if it takes too long to teardown
- Async function interface needs a teardown parameter in the hook
Async Function Interface
from typing import Coroutine
import asyncio
from reactpy import use_state, component, use_effect
def use_messenger(consumer: Coroutine, timeout: int = 10, teardown: Coroutine | None = None):
...
@component
def example():
state, set_state = use_state(0)
async def consumer_teardown(send, receive):
...
@use_messenger(timeout=20, teardown=consumer_teardown)
async def consumer(receive, send, cancel: asyncio.Event):
while True:
msg = await receive("my-message-type")
await send("my-message-type", ...)
print(state)
set_state(msg + 1)
if cancel.is_set():
break
Class Based Interface
from typing import Coroutine
import asyncio
from reactpy import use_state, component, use_effect
def use_messenger(consumer: Coroutine, timeout: int = 10):
...
@component
def example():
state, set_state = use_state(0)
@use_messenger(timeout=20)
class Consumer:
async def __call__(self, receive, send, cancel: asyncio.Event):
while True:
msg = await receive("my-message-type")
await send("my-message-type", ...)
print(state)
set_state(msg + 1)
if cancel.is_set():
break
async def teardown(self):
...
Note: We may want to make some considerations around Django Channels Layers while developing this interface.
It's an existing ASGI messaging paradigm, so taking inspiration from their interface might not be a bad idea.