Reactive streams are a great tool. They allow us to write performant and asynchronous code with ease. They help us to focus on what to do with data instead of how to do it. And they are easy to understand. Well, at first.

Most of us start by imagining streams as a series of values emitted over time. This simple model is enough to subscribe to streams and apply basic operators like map to it. But as soon as we need to add more complex operators or if something doesn’t work as expected the idea of value streams is no longer helpful.

So let me propose another mental model, one that focuses more on how streams work: Streams are just functions.

A small note: I use JavaScript in my examples and try to keep it as simple as possible, so that it‘s easy to understand even for developers without JavaScript skills.

The source

When you apply an operator to an observable you create a new observable with the original one as parent. A source is an observable that has no parent. It only emits values.

function source(subscriber) {

subscriber(1);

subscriber(2);

}

The source function expects a callback (the subscriber) and emits values by invoking the subscriber. Instead of „manually“ emitting values we could also loop over an array. Let‘s subscribe to our „observable“.

function subscriber(value) {

console.log(value);

} source(subscriber); //Prints 1 and 2 in the console

You might object that this doesn‘t allow for asynchronous streams. Here we go:

function source(subscriber) {

setTimeout(function() {

subscriber(1);

}, 1000); setTimeout(function() {

subscriber(2);

}, 3000);

} source(subscriber); //Prints 1 after 1 second and 2 after 3 seconds

The setTimeout function calls the subscriber after the given period of time. If you try this you will notice that the values are emitted after some seconds.

The operator

An operator is a simple function that takes a value and invokes a subscriber.

function operator(value, subscriber) {

let newValue = "Value: " + value;

subscriber(newValue);

}

The secret weapon of the operator is that it does not return a new value, but calls the subscriber instead. That makes it extremely powerful. We can decide when a value is emitted (e.g. debounce), how many values are emitted (e.g. flatMap) or if any value is emitted at all (e.g. filter).

The glue

Of course, we don‘t want to call the operator directly but apply it to another observable. So let‘s create a pipe function:

function pipe(source, operator) { function newSource(subscriber) {

function operatorSubscriber(value) {

operator(value, subscriber);

} return source(operatorSubscriber);

} return newSource;

}

That looks a bit more complex because of those nested functions. Let‘s go through them. We start with the innermost operatorSubscriber. A source only accepts subscribers, therefore we wrap the operator into another function. The source can call the operatorSubscriber, that invokes the operator, that in turn can call the actual subscriber.

When we apply an operator to a stream then we create a new stream. So the pipe function needs to return one. I called it operatorSource (although it‘s not actually a source). When someone „subscribes“ to the new source that request is passed on to the original source by invoking the source function.

Our workflow runs like this:

The subscriber calls the operatorSource (that wraps the operator)

The operatorSource calls the source

The source calls the operatorSubscriber repeatedly

The operatorSubscriber calls subscriber repeatedly

It‘s basically a request / response pattern

The subscription

We have already successfully subscribed to the stream, but we also want to cancel the subscription. Let‘s add an unsubscribe function:

function source(subscriber) {

subscriber(1); let id1 = setTimeout(function() {

subscriber(2);

}, 1000); let id2 = setTimeout(function() {

subscriber(3);

}, 3000); function unsubscribe() {

clearTimeout(id1);

clearTimeout(id2);

} return unsubscribe;

} let unsubscribe = source(subscriber);

unsubscribe();

The clearTimeout cancels the setTimeout. We call unsubscribe immediately after the subscription, hence we don‘t get the asynchronous values. But we do get the first value, because it is emitted before we can unsubscribe.

Conclusion

A full-fledged implementation needs a bit more than we have covered, but we got some basic building blocks for streams. And we have learned that a stream is basically just a series of function calls. Well, two series actually: From the subscriber to the source, when we subscribe, and from the source to the subscriber when values are emitted.

And now we also know why we need to actively subscribe to a stream: We need to call the source function.

The next time a stream does not behave as expected, try to figure out which function have been called and not called, and why.