Understanding Streams By Implementing Your Own Pt 2
Tags: JavaScript, Programming, Functional Programming, Monads, Observables

Now we can get into adding some more functionality to our Signals. Let’s first do something fairly easy, and define merge . merge takes a variadic number of Signal arguments and produces a Signal that will emit any time any of the inner Signals emit.

Signal.merge = function(...sigs) {

return Signal(function(subscriber) {

const disps = []

sigs.forEach(sig => {

disps.push(sig.subscribe(subscriber))

})

return Unsubscriber(() => disps.forEach(d => d.unsubscribe()))

}, null)

}

Here, we return a Signal whose subscriber is subscribed to each of the merged Signals. So that we can properly cleanup our Signals, we push each produced Unsubscriber to the disps array. Then when our main Unsubscriber calls its unsubscribe function, it will call each of the inner Unsubscriber s that we accumulated. It’s very useful to be able to merge our Signals, I’ll give a trivial example:

div1 = document.body.appendChild(document.createElement('div')) const enters_and_exits = Signal.merge(

Signal.for('mouseenter', div1),

Signal.for('mouseexit, div1)

)

Anytime either mouseenter or mouseexit occurs on the element div1 , the enters_and_exits Signal will emit. I’ll include more detailed examples in the pen at the end of this article.

Now onto zip . Here’s the definition I came up with:

Signal.zip = function(...sigs) {

return Signal(function(subscriber) {

const disps = []

const latest = []

latest.length = sigs.length

sigs.forEach((sig, i) => {

disps.push(sig.subscribe(x => {

latest[i] = x

subscriber(latest)

}))

})

return Unsubscriber(() => disps.forEach(d => d.unsubscribe()))

}, null)

}

It looks lot like merge because it is a lot like merge — but with some additions. latest is the result tuple that we’ll be filling in with the latest values from the signal. Notice that we set latest.length to be the same as the sigs.length parameter that we supplied to the function. That allows us to utilize the index parameter of our forEach call on sigs to set the latest value in the corresponding inner Signal to it’s expected index within the tuple. Then we call our subscriber with the latest tuple as an argument.

Now I want to focus on is making our Signal type capable of scheduling. When I say scheduling, I mean creating a stream that emits at a provided regular interval. It’s going to get a little complicated, but we will break it down. I’ll walk through the steps I took to make this a possibility:

We will define a new function on Signal called ofMany that takes an array of values, and optionally an interval parameter for the scheduling portion. We will define another function on Signal called defer . defer takes an interval and returns a Signal that emits after the provided interval. Next we will add a branch to ofMany for the scheduling portion (when an interval is provided). We will define a mini-iterator to produce values from the provided array on demand. We will use the defer function to create inner Signals that the emission of each next value in the provided set will be bound by. We will make sure to properly cleanup by unsubscribing from each of our inner Signals when that Signal is done emitting its next value.

So first let’s write the ofMany branch that expects no interval:

Signal.ofMany = function(xs, interval) {

return Signal(function(subscriber) {

if(!interval) {

xs.forEach(item => subscriber(item))

return Unsubscriber()

} // else { ... }

}

This is the simple part — just return a signal that has an array inner value, and apply the subscriber in a forEach loop (and of course don’t forget to return the Unsubscriber ).

defer is near equally simple:

Signal.defer = function(interval) {

return Signal(function(subscriber) {

const timeout = setTimeout(subscriber, interval)

return Unsubscriber(() => clearTimeout(timeout))

}

}

We’re hijacking setTimeout to create a Signal that emits only after the provided interval . Then we cleanup after ourselves by providing our returned Unsubscriber a cleanup function that clears the associated timeout ID.

Before we can actually write scheduled ofMany Signals, we still need that mini-iterator I briefly discussed above in my process. But first, a brief discussion about iterators. An iterator in JavaScript is actually just an interface that offers values from a collection on demand via a next function. The return value of that function is an object that takes the shape:

{ value: any, done: Boolean }

So let’s make the simplest iterator we can to walk through our values for us:

const mini_iter = xs => {

let i = 0

return xs === Infinity

? () => ({value: i++, done: false})

: () => xs[i]

? ({value: xs[i++], done: false}) //until xs[i] === null

: ({value: null, done: true}) //xs[i] was null

}

This is a function that takes either an array or the Infinity special JS value, and from that it decides what iterator function to produce. If we supply that function with Infinity , we’ll get an iterator function that will never produce an object with a done value of true, meaning that you will be able to call the next function forever. If we pass it an array type object, it will produce an iterator function that returns a value until xs.length + 1 , at which point it will emit the complete object with null value and done of true .

Now we can use our defer Signal in combination with our iterator to schedule ofMany parameters to emit at regular intervals.

Signal.ofMany = function(xs, interval) {

return Signal(function(subscriber) {

if(!interval) {

xs.forEach(item => subscriber(item))

return Unsubscriber()

} else {

let busy = false,

disposer



let next = mini_iter(xs)



const scheduler = Signal.defer(interval)

const initializer = scheduler.subscribe(function step() {

if(busy) return

busy = true

const { value, done } = next()

if(!done) {

subscriber(value)

disposer.unsubscribe()

disposer = scheduler.subscribe(step)

}

busy = false

})

disposer = disposer ? disposer : initializer



return Unsubscriber(() =>

disposer ? disposer.unsubscribe() : noop())

}

})

}

Phew, that’s a lot to process. Let’s isolate just the else branch, and focus on that:

//...

} else {

let busy = false, //(1)

disposer //(2)



let next = mini_iter(xs) //(3) const scheduler = Signal.defer(interval) //(4)

// (5) (6)

const initializer = scheduler.subscribe(function step() {

if(busy) return //(7)

busy = true

const { value, done } = next() //(8)

if(!done) { //(9)

subscriber(value)

disposer.unsubscribe()

disposer = scheduler.subscribe(step)

}

busy = false

})

disposer = disposer ? disposer : initializer //(10)

//(11)

return Unsubscriber(() =>

disposer ? disposer.unsubscribe() : noop())

}

})

}

First we setup some variables we will need. busy is a switch to lock the inner Signal. When busy is true it will prevent any other inner Signals from being subscribed to. disposer is the variable we will use to accumulate the Unsubscriber s. We use our mini_iter to generate an appropriate iterator based on xs . We assign that iterator to next so we can call it easily as next() . Then we create our defer Signal and call it scheduler . It’s important to note that the interval will not begin counting down until a .subscribe is called on the scheduler we’ve instantiated. The initializer is just the first inner defer to which we subscribe, starting a chain reaction of deferred subscriptions until our iterator returns an object with done: true . We pass a named function to the initializer. We name it so that we can call it recursively, or from within itself. The first thing we do in the step function is check to see if we’re locked out by busy , and return from the function if we are. If not, proceed, and lock the inner Signal by setting busy to true. Now that we’re finally in the looping portion of the function, we can get our value and done variables out of our next() call. If done is not true, then we should call our original subscriber function (from way up in the second line of the ofMany function, where we return a new Signal). We can discard our previous Unsubscriber by calling .unsubscribe on it. The we reassign disposer to the next subcribe using our named step function as the subscriber. In the case that we only have one value in our scheduled Signal, disposer will never be set. If that’s the case, we can assign disposer to refer to the initializer Signal. Finally, we can return an Unsubscriber with its cleanup logic defined to call unsubscribe on the inner Signal’s last subscription.

Now we can finally schedule our streams to emit at regular intervals!

Signal.ofMany([1,2,3,4,5], 1000)

//O------1------2------3------4------5-/

// [1sec] [2sec] [3sec] [4sec] [5sec]

Here’s an example pen:

That’s all for now folks, hope you enjoyed it and FP on!