-
-
Couldn't load subscription status.
- Fork 331
messenger pattern #1084
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
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():
breakClass 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_scripthook #894Solution
Implement a generic
Messengerclass that allows for users to send/receive arbitrary messages to/from the client. Users will gain access to aMessengervia theuse_messengerhook. Usage could look like one of the following...Using
start_consumerandstart_producer:Using
sendandreceive:Ultimately, the
start_consumerandstart_producermethods are convenience methods that callsendandreceiveunder the hood.Checklist
changelog.rsthas been updated with any significant changes.