reactpy icon indicating copy to clipboard operation
reactpy copied to clipboard

Allow server-side components to send messages to client-side components

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

Current Situation

Presently, custom client-side components are able to send events back to the server. However, it is not possible to send events from server-side components to client-side ones.

Proposed Actions

Now that custom JS components have the ability to register callbacks with the client this should be technologically feasible. With that said, while users can listen in on particular message types, there is no concept of a message "target". We could achieve this by having message types of the form server-event:<the-target>, but this seems like a bit of a hack. Perhaps we can allow (require?) a nullable target field for this purpose.

Before diving into all those details though, we need to work out exactly what this interface should look like.

I can imagine having an interface similar to:

@component
def example():
    channel = use_channel()

    @use_effect
    async def delayed_message():
        import asyncio
        await asyncio.sleep(5)
        await channel.send({"my": "message"})
        response = await channel.receive()

    return custom_js_component({"channel_id": channel.id})

where custom_js_component would then subscribe to messages of the type channel-message and target channel.id.

rmorshea avatar May 11 '23 06:05 rmorshea

Tangentially related

  • https://github.com/reactive-python/reactpy/issues/647

Archmonger avatar May 11 '23 07:05 Archmonger

The name use_channel kind of implies that multiple channels and/or channel groups can be defined per component. Here's some other potential names

  • use_data_message
  • use_message
  • use_messenger
  • use_message_listener
  • use_message_signal
  • use_message_subscriber
  • use_signal_listener
  • use_consumer
  • use_data_channel
  • use_data_messenger
  • use_data_transport
  • use_data_dispatcher
  • use_communicator

The design below would allow for an event-driven architecture when possible, inspired by channels.

With any design we make, we're going to need to be careful about our asyncio cancellation policy for messengers. This will extend to us thinking deeper about async use_effect.

from dataclasses import dataclass

from reactpy import component, hooks


@dataclass
class DataMessenger:
    def __init__(self, *args, **kwargs):
        self.args = args or ()
        self.kwargs = kwargs or {}

    async def send(self, message):
        """This method typically isn't overridden."""
        ...

    async def recieve(self, message):
        """This method typically gets overriden with custom user behavior."""
        ...

    async def register(self, ... ):
        """This method typically isn't overridden. Used to register a message handler to a component."""
        ...


def use_messenger(messenger: DataMessenger) -> DataMessenger:
    """This is a hook that allows users to communicate to client-side components.
    The given `messenger` will be registered to the current component.
    """
    MESSENGER_CONTEXT: list = hooks.use_context(...)
    
    # A given `Messenger` should persist throughout the entire lifecycle of a component
    if messenger not in MESSENGER_CONTEXT:  # Not a usable implementation, but you get the point
        messenger.register(...)

    # Update `args`/`kwargs` on each component re-render.
    else:
        messenger.args = MESSENGER_CONTEXT[0].args
        messenger.kwargs = MESSENGER_CONTEXT[0].kwargs

    # Give the user access to the registered messenger, if he wants to do some shenanigans within `use_effect`.
    return messenger


@component
def example():
    # If the user only wants to be event driven, then he won't need to save a `messenger = ...` variable
    messenger = use_messenger(DataMessenger(example_value=1))

    @hooks.use_effect
    async def send_message():
        # Using `messenger` methods here would rely on our half-baked async `use_effect`
        # I think this might be an argument to fleshing out async effects
        await messenger.send("Hello, World!")

Archmonger avatar May 11 '23 09:05 Archmonger

The name use_channel kind of implies that multiple channels and/or channel groups can be defined per component

I think this is true. One could imagine a server-side component that constructs several channels in order to communicate with different client-side components. use_messenger would be fine too though.

rmorshea avatar May 11 '23 17:05 rmorshea

If we're allowing support for multiple communicators, imo that narrows it down to the following:

  • use_channel
  • use_data_channel
  • use_messenger
  • use_data_messenger
  • use_communicator

I updated the example above to better outline how persistence of DataMessenger can be achieved.

Archmonger avatar May 11 '23 21:05 Archmonger