sourceafReflux::EventHub.fan

using afBeanUtils
using afIoc
using fwt

** (Service) - 
** An eventing strategy for Reflux Apps.
** 
** To receive events, classes must first register themselves with 'EventHub'. 
** Given 'MyEvents' is an event mixin: 
** 
**   syntax: fantom
**   class MyService : MyEvents {
**       new make(EventHub eventHub) {
**           eventHub.register(this)
**       }
** 
**       override Void onEvent() { ... }
**   } 
** 
** Note that instances of 'Panels', 'Views' and 'GlobalCommands' are already automatically added 
** to 'EventHub' by default. 
@Js
mixin EventHub {

    ** Registers an object to receive events. 
    ** The object must implement one or more contributed event types else an 'ArgErr' is thrown. 
    abstract Void register(Obj eventSink, Bool checked := true)

    abstract Void deregister(Obj eventSink)

    ** Fires an event. There should be very little need to call this directly. 
    ** Just '@Inject' the service and call the event method instead! 
    abstract Void fireEvent(Method method, Obj?[]? args := null)
    
    ** Fires an event after the given delay. 
    abstract Void fireEventIn(Duration delay, Method method, Obj?[]? args := null)

}

@Js
internal class EventHubImpl : EventHub {
    @Inject private EventTypes  eventTypes
    @Inject private Errors      errors
            private Obj[]       eventSinks  := [,]
    
    private new make(|This| in) {
        in(this)
    }

    // TODO: save into map of sinks, for optomidation
    override Void register(Obj eventSink, Bool checked := true) {
        if (!eventTypes.eventTypes.any { eventSink.typeof.fits(it) })
            if (checked) throw ArgNotFoundErr("EventSink ${eventSink.typeof} does not implement a contributed EventType", eventTypes.eventTypes); else return

        // it's important that Reflux is notified first, so it can set the activeView
        if (eventSink is RefluxImpl)
            eventSinks.insert(0, eventSink)
        else
            eventSinks.add(eventSink)
    }

    override Void deregister(Obj eventSink) {
        eventSinks.remove(eventSink)
    }

    override Void fireEvent(Method method, Obj?[]? args := null) {
        // TODO: queue up events to prevent infinite recursion
        check
            := eventTypes.eventTypes.find { it.fits(method.parent) }
            ?: throw ArgNotFoundErr("Method '${method.qname}' does not belong to an event type ", eventTypes.eventTypes)
        
        sinks := eventSinks.findAll { it.typeof.fits(method.parent) }

        sinks.each {
            try method.callOn(it, args)
            catch (Err err) {
                errors.add(err)
            }
        }
    }
    
    override Void fireEventIn(Duration delay, Method method, Obj?[]? args := null) {
        Desktop.callLater(delay) |->| { fireEvent(method, args) }
    }
}