koordinates / xstate-tree

Build UIs with Actors using xstate and React
MIT License
82 stars 6 forks source link

broadcast 2.0 #19

Open UberMouse opened 1 year ago

UberMouse commented 1 year ago

In Thomas Weber's master theisis that inspired xstate-tree, all events were global and sent to all actors. I felt that was overkill for xstate-tree so only specific opt-in events are sent globally, via the broadcast method.

However now that we have been using that for a few years even just sending all events to all machines feels problematic, mostly because you need to start prefixing events since the names are global across all parts of the application. It feels like a better solution is to have mailboxes that have specific events attached to them that can be used in specific sections to better split up responsibilities. Then you can access this mailbox to send an event to all listeners of it, instead of funnelling all events through the same mailbox. It would also allow removing the somewhat strange GlobalEvent interface merging system for typing global events.

Could make sense to have a routing mailbox too :thinking:

The xstate receptionist RFC seems relevant to this https://github.com/statelyai/rfcs/pull/5

UberMouse commented 1 year ago

I've been thinking about this further this weekend and had some ideas. Building on the initial benefit of having separate "mailboxes" outlined in the OP, there are two other benefits of splitting this up.

  1. minor performance boost, currently a broadcast requires iterating through all machines in all active roots, which isn't expensive per say, but is rather ineffecient
  2. Inability to easily see all the locations a given event is broadcast

Both of those can be solved with a combination of more granular mailboxes that specific machines opt into listening to and generating functions to broadcast events so you can use "Find References" to locate all the broadcast sites.

Looking at the xstate receptionist RFC I don't think it's quite the fit for the broadcast system xstate-tree uses. It's based around registering actors and sending more scoped messages to specific actors, whereas the broadcast system is effectively "this thing happened, inform anyone who cares about it". It also does not deal with any concept of queued messages, which is something we have a few uses for at Koordinates so I would like to explore.

In light of that I am proposing the following

  1. Creating a mailbox

Creates a mailbox, defining the events as objects in Javascript instead of at the type level as we need the event types in Javascript to be able to generate the send* functions. Optional data property used to type the argument to the send functions

const mailbox = createMailbox(
  { type: "anEvent", data: {} as { aProperty: number},
  { type: "noDataEvent" }
);
  1. Sending an event to a mailbox

mailbox.sendAnEvent({ aProperty: 10 }); mailbox.sendNoDataEvent();

  1. Listening to events from a mailbox

Something to think about here is the ability to prefix the events with something to allow bringing in an event of the same name from two mailboxes, but that requires something in JS so can't be done here type Events = MailboxEvents<typeof mailbox, "anEvent">;

other xstate-tree boilerplate snipped

Pass the list of mailboxes the machine is interested in here so that the React view that manages the interpreter for the machine can subscribe to the mailboxes and send the relevant events to the interpreter

Since there is no enforcement of this if you are adding events from a mailbox to the machines events, some sort of checking here that any mailbox events have an attached mailbox that provides them would be useful. That would require having some way of differentiating mailbox events from machine events at the machine config level (ie event name) buildXstateTreeMachine(machine, {selectors: ..., actions: ... etc, mailboxes: [mailbox]}

  1. "queued" events

Some scenarios you need to pass data to a machine that is not yet listening for events, which is impossible with the current system because events are only delivered to actors listening at the time the event is delivered. Most events should not be queued as it they aren't needed in this scenario and it's perfectly fine for no consumers to be active for some events, don't want them queued up and then have something consume a stale event in the future randomly.

So I propose that this be opt in per mailpox, you can mark the mailbox as queued and then it will queue messages that have no active consumers until an actor is ready to consume a message of that type. Behaviour around queuing and multiple messages TBD.