EventEmitter Implementation With Tinkerbell Signal

I thought I should share this for anyone who wish to use an EventEmitter that’s tied close to the Javascript equivalent. This has been sufficient for my use case, you can extend it further if you wish.

import tink.core.Callback;
import tink.core.Callback.CallbackLink;
import tink.core.Pair.MPair;
import tink.core.Signal;
import tink.core.Signal.SignalTrigger;
using StringTools;

class EventEmitter {
    final signals:haxe.ds.Map<String, Array<MPair<Bool, SignalTrigger<haxe.Rest<Any>>>>>;
    final __listeners:haxe.ds.Map<String, Array<Callback<haxe.Rest<Any>>>>;
    public function new() {
        signals = new Map();
        __listeners = new Map();
    }

    public function on(name:String, callback:(...v:Any)->Void):CallbackLink {
        final trigger = Signal.trigger();
        final signal = trigger.asSignal();
        var funcs = new Array<MPair<Bool, SignalTrigger<haxe.Rest<Any>>>>();

        if(signals.exists(name)){
            funcs = signals.get(name);
        }

        funcs.push(new MPair(false, trigger));
        signals.set(name, funcs);

        return signal.handle(callback); 
    }

    public function once(name:String, callback:(...v:Any)->Void):CallbackLink {
        final trigger = Signal.trigger();
        final signal = trigger.asSignal();
        var funcs = new Array<MPair<Bool, SignalTrigger<haxe.Rest<Any>>>>();

        if(signals.exists(name)){
            funcs = signals.get(name);
        }

        funcs.push(new MPair(true, trigger));
        signals.set(name, funcs);

        return signal.handle(callback); 
    }

    public function emit(name:String, ...v:Any) {
        var funcs = new Array<MPair<Bool, SignalTrigger<haxe.Rest<Any>>>>();
        if(signals.exists(name)){
            funcs = signals.get(name);
            for(f in funcs){
                f.b.trigger(...v);
                if(f.a){
                    f.b.dispose();
                }
            }
        }
    }

    public function addListener(name:String, callback:(...v:Any)->Void) {
        var funcs = new Array<MPair<Bool, SignalTrigger<haxe.Rest<Any>>>>();
        if(signals.exists(name)){
            funcs = signals.get(name);
            var _listeners = __listeners.exists(name) ? __listeners.get(name) : [];
            for(f in funcs){
                f.b.listen(callback);
                _listeners.push(callback); 
                if(_listeners.length == 0){
                    __listeners.set(name, _listeners);
                }
            }
        }
    }

    public function removeListener(name:String, callback:(...v:Any)->Void) {
        var listeners = this.listeners(name);
        if(listeners.contains(callback)){
            listeners.remove(callback);
        }
    }

    public function listeners(name:String) {
        return __listeners.get(name);
    }
}

Usage

final eventEmitter = new EventEmitter();
eventEmitter.on('start', (values) -> {
			final start = values[0];
			final end = values[1];
			trace("started from " + start + " to "+end);
});
eventEmitter.once('start', (values) -> {
			final start = values[0];
			final end = values[1];
			trace("started from " + start + " to "+end);
});
eventEmitter.emit('start', 1, 100);
eventEmitter.emit('start', 2, 100);
1 Like

Seems like an antipattern because Signal is designed to be type safe… and the use of Any subverts that…

You are right. I didn’t put much thought into it beyond having an API close enough to the Javascript API for my use case.

I am also rewriting to support concurrency, cos I noticed the higher the number of listeners the slower the even triggers.

There’s that. What’s more is anyone with a reference to the emitter can freely emit events - a design defect that that tink’s singals were made to avoid.

In any case, if it’s useful to you, why not. But the implementation seems rather complex, has quite a few useless allocations and is somewhat leaky (e.g. if you call cancel on the returned CallbackLink the subscription to the trigger is canceled, but the trigger remains stored in signals) and buggy (you have _listeners.push(callback); if(_listeners.length == 0){ - which means that condition can never be met).

Here’s a version that remedies those problems while keeping the API: Try Haxe!

It’s not tested though, so only use it with appropriate care :wink:

1 Like

@back2dos I tried your implementation on a much more complex set of test cases, and it seems like it’s calling the listeners in the wrong order, or it disposed the fresh signals and kept the older ones.

So from further investigation, an once listener is still being called on a separate occasion. it wasn’t disposed

Hmm, you’ll probably want to swap the two calls in callAndRemove (first remove, then call) although I can really only speculate ^^

1 Like

That did it!!