reactpy icon indicating copy to clipboard operation
reactpy copied to clipboard

messenger pattern

Open rmorshea opened this issue 2 years ago • 4 comments
trafficstars

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.rst has been updated with any significant changes.

rmorshea avatar Jul 03 '23 08:07 rmorshea

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.

Archmonger avatar Jul 04 '23 21:07 Archmonger

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

rmorshea avatar Jul 04 '23 22:07 rmorshea

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):
            ...

Archmonger avatar Jul 07 '23 05:07 Archmonger

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.

Archmonger avatar Sep 16 '23 10:09 Archmonger