← all posts · project-log · · 4 min read

What is our source of truth?

Deriving messages from a stream of events

1. The problem

How do I model half a message?

A simple Message in a traditional chat interface may look like this:

  struct Message {
    let content: String
  }

But when streaming messages incrementally we’re immediately hit with a question: how do I know when a message ends?

Given a stream of text: "Hell" -> "o, wo" -> "rld", should my data layer parse it like this for the UI to render?

  let eventsStream = ["Hell", "o, wo", "rld"]
  let messages = eventsStream.reduce([]) { /* Explained in future post */ }

  // prints
  [
    Message("Hell"),
    Message("o, wo"),
    Message("rld"),
  ]

"Hell" might be a message, but "o, wo" definitely isn’t.

Naively mapping our events to models is no problem, but I quickly ran into an issue: I could show these messages streaming in the UI, but I couldn’t know when to allow the user to interrupt or when to allow them to send another message.

This is where we need to think about what defines a message.

2. The solution

Defining boundaries

A message is a discrete unit of communication, where discrete1 here means having a clear beginning and end. So we need to take a continuous stream of events, and decide where to start building a message, and where to decide that message has finished.

The easiest way to do this is to add an Event type for our stream and simply model these boundaries:

  enum Event {
    case beginning
    case delta(String)
    case end
  }

  struct Message {
    let content: String
  }

  let eventsStream = [
    Event.beginning,
    Event.delta("Hell"),
    Event.delta("o, wo"),
    Event.delta("rld"),
    Event.end
  ]

  let messages = eventsStream.reduce([]) { /* Explained in future post */ }

  // prints
  [
    Message("Hello, world"),
  ]

This is a small change but it reveals something to us: once we model it this way, the events become the source of truth from which the messages are derived.

3. The result

What can we learn from this?

We may be tempted to consider an array of messages as the source of truth in our chat interface, particularly since as users they’re the part we see and interact with, but messages are actually derived state from our real source of truth: the events.

This is essentially how ledgers work in the real world. Banks don’t store your balance and your transactions; they store the transactions and use them to compute the balance. This means they don’t have to store two sources of truth, which may get out of sync, simplifying the state management.

Computed properties in Swift follow the same principle and are similarly useful for simplifying state.

4. The future

What’s next?

Now that we know what our source of truth is we can focus on ensuring our model for it is robust and deterministic. Since our messages will be built incrementally from events streaming in, most likely over an unreliable network connection, we want to make sure those events are always in the same order, and for errors to gracefully degrade functionality rather than break completely.

To this end, the next post will be on folding our continuous events into discrete state using reducers.

Footnotes

  1. This may sound like pointless semantics but the language we use plays a large part in how we mentally model our systems. Finding the right name for a type or a property lowers cognitive load without even touching architecture.