[BETA] Fun With Combine

I'm an old fart, that's not in any way debatable. But being an old fart, I have done old things, like implementing a databus-like system in a program before. So when I saw Combine, I thought I'd have fun with re-implementing a databus with it.

First things first

Why would I need a databus?

If you've done some complex mobile programming, you will probably have passed notifications around to signal stuff from one leaf of your logic tree to another, something like a network event that went in the background and signalled its task was done, even though the view controller that spawned it has gone dead a long time ago.

Databuses solve that problem, in a way. You have a stream of "stuff", with multiple listeners on it that want to react to, say, the network went down or up, the user changed a crucial global setting, etc. And you also have multiple publishers that generate those events.

That's why we used to use notifications. Once fired, every observer would receive it, independantly of their place in the logic (or visual) tree.

The goal

I wanted to have a databus that could do two things:

  • allow someone to subscribe to certain events or all of them
  • allow to replay the events (for debug, log, or recovery purposes)

I also decided I wanted to have operators that reminded me of C++ for some reason.

The base

Of course, for replay purposes, you need a buffer, and for a buffer, you need a type (this is Swift after all)

public protocol Event {
    
}

public final class EventBus {
    fileprivate var eventBuffer : [Event] = []
    fileprivate var eventStream = PassthroughSubject<Event,Never>()

PassthroughSubject allows me to avoid implementing my own Publisher, and does what it says on the tin. It passes Event objects around, and fails Never.

Now, because I want to replay but not remember everything (old fart, remember), I decided to impose a maximum length to the replay buffer.

    public var bufferLength = 5 {
        didSet {
            truncate()
        }
    }
    
    fileprivate func truncate() {
        while eventBuffer.count > bufferLength {
            eventBuffer.remove(at: 0)
        }
    }

It's a standard FIFO, oldest in front latest at the back. I will just pile them in, and truncate when necessary.

Replaying is fairly easy: you just pick the last x elements of a certain type of Event and process them again. The only issue is reversing twice: once for the cutoff, and once of the replay itself. But since it's going to be seldom used, I figured it was not a big deal.

    public func replay<T>(count: UInt = UInt.max, handler: @escaping (T) -> Void) {
        var b = [T]()
        for e in eventBuffer.reversed() {
            if b.count >= count { break }
            if let e = e as? T {
                b.append(e)
            }
        }
        for e in b.reversed() {
            handler(e)
        }
    }

Yes I could have done it more efficiently, but part of the audience is new to swift (or any other kind) of programming, and, again, it's for demonstration purposes.

Sending an event

That's the easy part: you just make a new event and send it. It has to conform to the Event protocol , so there's that. Oh and I added the << operator.

    public func send(_ event: Event) {
        eventBuffer.append(event)
        truncate()
        eventStream.send(event)
    }
    static public func << (_ bus: EventBus, _ event: Event) {
        bus.send(event)
    }

From now on, I can do bus << myEvent and the event is propagated.

Receiving an event

I wanted to be able to filter at subscription time, so I used the stream transformator compactMap that works exactly like its Array counterpart: if the transformation result is nil, it's not included in the output. Oh and I added the >> operator.

    public func subscribe<T:Event>(_ handler: @escaping (T) -> Void) {
        eventStream.compactMap { $0 as? T }.sink(receiveValue: handler)
    }
    static public func >><T:Event> (_ bus: EventBus, handler: @escaping (T) -> Void) {
        bus.subscribe(handler)
    }

The idea is that you define what kind of event you want from the block's input, and Swift should (hopefully) infer what to do.

I can now write something like

bus >> { (e : EventSubType) in
    print("We haz receifed \(e)")
}

EventSubType implements the Event protocol, and the generic type is correctly inferred.

The End (?)

It was actually super simple to write and test (with very high volumes too), but I'm guessing there would be memory retention issues as I can't figure out a way to properly unsubscribe from the bus, especially if you have self references in the block.

Then again it's a beta and this is a toy sample. I will need to dig deeper in the memory management stuff, but at first glance, it looks like the lifetime of the blocks is exactly the lifetime of the bus, which makes it impractical in real cases. Fun stuff, though.