-
-
Notifications
You must be signed in to change notification settings - Fork 318
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
messenger pattern #1084
base: main
Are you sure you want to change the base?
messenger pattern #1084
Conversation
def start_producer(self, producer: Callable[[], AsyncIterator[Message]]) -> None: | ||
"""Add a message producer""" | ||
|
||
async def _producer() -> None: | ||
async for message in producer(): | ||
await self.send(message) | ||
|
||
self._task_group.start_soon(_producer) | ||
|
||
def start_consumer( | ||
self, message_type: str, consumer: Callable[[Message], Awaitable[None]] | ||
) -> None: | ||
"""Add a message consumer""" | ||
|
||
async def _consumer() -> None: | ||
async for message in self.receive(message_type): | ||
self._task_group.start_soon(consumer, message) | ||
|
||
self._task_group.start_soon(_consumer) |
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.
These need to return callbacks to stop the consumer/producer - this will work well with use_effect
.
Unfortunately, We would need a stateful async paradigm, similar to |
At some point we need to figure out async effects. I don't think that should be a blocker here. Edit: see #1090 |
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.
I dislike the start_consumer
and start_producer
from an "ease of learning" perspective. Following the ReactJS philsophy of diluting the core API to nothing but the most basic interface, we should simplify everything to just send
and receive
.
The "easiest to learn" way to advertise this functionality would be as such:
@component
def demo_send_receive():
@use_messenger
async def consumer(send, receive):
while True:
msg = await receive("my-message-type")
await send("my-message-type", ...)
Just the bare concept of async cancellation, and lack of reliability for other hooks to work in a persistent way makes the user experience really awkward though.
For example, if we force the user to enable shield_cancel
, we should consider whether state
and set_state
will always work as expected in the following scenario:
@component
def demo_send_receive():
state, set_state = use_state(0)
@use_messenger
async def consumer(send, receive):
while True:
msg = await msgr.receive("my-message-type")
print(state)
set_state(msg + 1)
Or alternatively, if we do not enable shield_cancel
is there potentially a gap in time where the previous use_effect
has been de-registered, but the new one is not registered yet?
"""Receive messages of a given type""" | ||
send, recv = create_memory_object_stream() | ||
self._streams.setdefault(message_type, []).append((send, recv)) | ||
async with recv: |
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.
Would async with send, receive:
not work here?
MessengerContext( | ||
ConnectionContext( |
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.
We can consider merging everything into a ReactPyContext
.
async def renders(self) -> AsyncIterator[_Render]: | ||
"""Render a series of updates to a view""" | ||
|
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.
Why not have AsyncIterator
be an accepted type on render
, and then handle detection of this within layout.py
?
Less APIs is generally more intuitive than more.
After today's tag up, two theoretical interfaces could look like this Notes
Async Function Interfacefrom 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 Interfacefrom 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. |
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...
use_script
hook #894Solution
Implement a generic
Messenger
class that allows for users to send/receive arbitrary messages to/from the client. Users will gain access to aMessenger
via theuse_messenger
hook. Usage could look like one of the following...Using
start_consumer
andstart_producer
:Using
send
andreceive
:Ultimately, the
start_consumer
andstart_producer
methods are convenience methods that callsend
andreceive
under the hood.Checklist
changelog.rst
has been updated with any significant changes.