I was looking back to see if you were looking back to see me looking back at you

Let's start with the Observer pattern, because it is a good place to start.

Say you have some data. That data represents some sort of state, that is, it's “the number of active users” or “the health of the current player”; something that will, at some point in time, change.

This data is encapsulated in some object, like the connection manager or the player. Let's pick player and health as example.

    class Player {
        // ...
        get health() { ... }
        set health(h) { ... }
        // ...
    }
    let playerOne = new Player();
    playerOne.health; // 100

So now you need access to the player's health from a number of other places in the application. Not only you need access to it; these other parts want to monitor that health. They want to be constantly updating or doing things when the health changes.

    hud.setHealthIndicator(playerOne.health); // We need the hud to be correctly synchronized with the actual health value

You could “simply” have these components keep asking for the player's health every so often, but that is messy and quickly becomes a performance problem and most importantly it's wasteful. Wasteful as in most of the time they will be asking for the health and it won't have changed from the last time they asked. Ugly:

    setInterval(() => {
        hud.setHealthIndicator(playerOne.health);
    }, 50);
    setInterval(() => {
        something.else(playerOne.health);
    }, 50);

So instead you realise that the player knows exactly when the health changes. So you think of this: instead of other components monitoring the player, they will tell the player they are interested in knowing when the health changes. And then, when it does, the player will notify all those that manifested interest.

    class Player {
        Constructor(...) {
            this.notifyMePlease = function(callback) { ... }
        }
        // ...
        get health() { ... }
        set health(h) {
            // ...
            notifyInterestedParts(currentHealth);
        }
    }
 
    let playerOne = new Player();
 
    playerOne.notifyMePlease(function(currentHealth) {
        hud.setHealthIndicator(currentHealth);
    });
    playerOne.notifyMePlease(function(currentHealth) {
        something.else(currentHealth);
    });
 
    playerOne.health--;

This is the basic idea of the Observer pattern. Of course, that code is just for the example; you'd use better names and you'd make it more generic for reuse and all that. But the idea of the Observer pattern is just that. Interested parts subscribe to a certain entities' state changes and so are notified when such changes happen.


So now this idea can evolve in a number of ways, depending on what you focus on. For example, you could focus on the idea of the notifications and subscriptions, how they could be managed externally, not by the player itself. You could have an entity that centralises all messages for all state holders and all interested parties. Maybe with channels or different types of messages… And interested parties could subscribe and unsubscribe dynamically and state holders wouldn't even notice or care. This could then be a more general Pub/Sub system or message bus.


One other way it can evolve is focusing on how subsequent notifications of the health changing over time can be seen as forming a stream. That is, you subscribe to health and get 100. After a certain time you get notified of 95. Then you receive 70, later you get 42… You can see this as an ongoing sequence of values: 100, 95, 70, 42….

If you focus on that then you can change some of your vocabulary and approaches somewhat. You can see streams as “similar” to arrays ([100, 95, 70, 42…]), in the sense that they're collections of values. Yes, the difference is that new values are added over time, but essentially they're collections of values. So maybe you can use similar tools you use with arrays, like filtering or mapping, for example. So this turns into stream processing.

As a quick example, if you have that “health stream” as healthStream the healthIndicator in the hud can calculate how much of the width bar has to fill just by mapping health to width

    widthStream = healthStream.map((h) => h/100*totalWidth);

And now you have how the width of the health bar is defined in a very declarative way.

Streams are nice because they fit well with some paradigms like so called “functional reactive”. The thing is that they are very easily defined in a declarative way. Not only that, but if you think about it for a while you can start seeing that a “state change” is really an event i.e. “something happened”. If you see it this way, you can realise it's not so much about the value (100, 95…) but about the occurrence of some event: “Health changed”. So you think about it some more and realise that this is so similar to the user clicking a button (“clicked state changed to true”) that maybe you could treat them all in a similar manner. Events are just state changes. So you can create a clickStream which will be a sequence of the values of the state of the button ([false, true, false…]). And thus you could transform everything about “state” into streams of events and treat all state management as event stream processing.