Skip to content

Commit

Permalink
Merge pull request #7 from Flowduino/custom-event-filtering
Browse files Browse the repository at this point in the history
5.2.0 - Custom Event Filtering for `EventListener`
  • Loading branch information
LK-Simon authored Sep 3, 2022
2 parents 48cdd12 + 1784643 commit 951b5c8
Show file tree
Hide file tree
Showing 9 changed files with 104 additions and 10 deletions.
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,37 @@ In the above code example, `maximumAge` is a value defined in *nanoseconds*. Wit

This functionality is very useful when the context of an *Event*'s usage would have a known, fixed expiry.

## `EventListener` with *Custom Event Filtering* Interest
Version 5.2.0 of this library introduces the concept of *Custom Event Filtering* for *Listeners*.

Now, when registering a *Listener* for an `Eventable` type, you can specify a `customFilter` *Callback* which, ultimately, returns a `Bool` where `true` means that the *Listener* is interested in the *Event*, and `false` means that the *Listener* is **not** interested in the *Event*.

We have made it simple for you to configure a *Custom Filter* for your *Listener*. Taking the previous code example, we can simply modify it as follows:
```swift
class TemperatureRatingViewModel: ObservableObject {
@Published var temperatureInCelsius: Float
@Published var temperatureRating: TemperatureRating

var listenerHandle: EventListenerHandling?

internal func onTemperatureRatingEvent(_ event: TemperatureRatingEvent, _ priority: EventPriority, _ dispatchTime: DispatchTime) {
temperatureInCelsius = event.temperatureInCelsius
temperatureRating = event.temperatureRating
}

internal func onTemperatureRatingEventFilter(_ event: TemperatureRatingEvent, _ priority: EventPriority, _ dispatchTime: DispatchTime) -> Bool {
if event.temperatureInCelsius > 50 { return false } // If the Temperature is above 50 Degrees, this Listener is not interested in it!
return true // If the Temperature is NOT above 50 Degrees, the Listener IS interested in it!
}

init() {
// Let's register our Event Listener Callback!
listenerHandle = TemperatureRatingEvent.addListener(self, onTemperatureRatingEvent, interestedIn: .custom, customFilter: onTemperatureRatingEventFilter)
}
}
```
The above code will ensure that the `onTemperatureRatingEvent` method is only invoked for a `TemperatureRatingEvent` where its `temperatureInCelsius` is less than or equal to 50 Degrees Celsius. Any `TemperatureRatingEvent` with a `temperatureInCelsius` greater than 50 will simply be ignored by this *Listener*.

## `EventPool`
Version 4.0.0 introduces the extremely powerful `EventPool` solution, making it possible to create managed groups of `EventThread`s, where inbound *Events* will be directed to the best `EventThread` in the `EventPool` at any given moment.

Expand Down
4 changes: 2 additions & 2 deletions Sources/EventDrivenSwift/Central/EventCentral.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ final public class EventCentral: EventDispatcher, EventCentralable {
}
}

@discardableResult @inline(__always) public static func addListener<TEvent>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type, executeOn: ExecuteEventOn = .requesterThread, interestedIn: EventListenerInterest = .all, maximumAge: UInt64 = 0) -> EventListenerHandling where TEvent : Eventable {
return _shared.eventListener.addListener(requester, callback, forEventType: forEventType, executeOn: executeOn, interestedIn: interestedIn, maximumAge: maximumAge)
@discardableResult @inline(__always) public static func addListener<TEvent>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type, executeOn: ExecuteEventOn = .requesterThread, interestedIn: EventListenerInterest = .all, maximumAge: UInt64 = 0, customFilter: TypedEventFilterCallback<TEvent>? = nil) -> EventListenerHandling where TEvent : Eventable {
return _shared.eventListener.addListener(requester, callback, forEventType: forEventType, executeOn: executeOn, interestedIn: interestedIn, maximumAge: maximumAge, customFilter: customFilter)
}

@inline(__always) public static func removeListener(_ token: UUID) {
Expand Down
2 changes: 1 addition & 1 deletion Sources/EventDrivenSwift/Central/EventCentralable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public protocol EventCentralable {
- forEventType: The `Eventable` Type for which to Register the Callback
- Returns: A `UUID` value representing the `token` associated with this Event Callback
*/
@discardableResult static func addListener<TEvent: Eventable>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type, executeOn: ExecuteEventOn, interestedIn: EventListenerInterest, maximumAge: UInt64) -> EventListenerHandling
@discardableResult static func addListener<TEvent: Eventable>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type, executeOn: ExecuteEventOn, interestedIn: EventListenerInterest, maximumAge: UInt64, customFilter: TypedEventFilterCallback<TEvent>?) -> EventListenerHandling

/**
Locates and removes the given Listener `token` (if it exists) from the Central Event Listener
Expand Down
6 changes: 3 additions & 3 deletions Sources/EventDrivenSwift/Event/Eventable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public protocol Eventable {
- callback: The code to invoke for the given `Eventable` Type
- Returns: A `UUID` value representing the `token` associated with this Event Callback
*/
@discardableResult static func addListener<TEvent: Eventable>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, executeOn: ExecuteEventOn, interestedIn: EventListenerInterest, maximumAge: UInt64) -> EventListenerHandling
@discardableResult static func addListener<TEvent: Eventable>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, executeOn: ExecuteEventOn, interestedIn: EventListenerInterest, maximumAge: UInt64, customFilter: TypedEventFilterCallback<TEvent>?) -> EventListenerHandling

/**
Locates and removes the given Listener `token` (if it exists) from the Central Event Listener
Expand Down Expand Up @@ -122,8 +122,8 @@ extension Eventable {
EventCentral.scheduleStack(self, at: at, priority: priority)
}

@discardableResult static public func addListener<TEvent: Eventable>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, executeOn: ExecuteEventOn = .requesterThread, interestedIn: EventListenerInterest = .all, maximumAge: UInt64 = 0) -> EventListenerHandling {
return EventCentral.addListener(requester, callback, forEventType: Self.self, executeOn: executeOn, interestedIn: interestedIn, maximumAge: maximumAge)
@discardableResult static public func addListener<TEvent: Eventable>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, executeOn: ExecuteEventOn = .requesterThread, interestedIn: EventListenerInterest = .all, maximumAge: UInt64 = 0, customFilter: TypedEventFilterCallback<TEvent>? = nil) -> EventListenerHandling {
return EventCentral.addListener(requester, callback, forEventType: Self.self, executeOn: executeOn, interestedIn: interestedIn, maximumAge: maximumAge, customFilter: customFilter)
}

public static func removeListener(_ token: UUID) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ open class EventDispatcher: EventHandler, EventDispatching {

_receivers.withLock { receivers in
var bucket = receivers[eventTypeName]
if bucket == nil { return } /// Can't remove a Receiver if there isn't even a Bucket for hte Event Type
if bucket == nil { return } /// Can't remove a Receiver if there isn't even a Bucket for the Event Type

/// Remove any Receivers from this Event-Type Bucket for the given `receiver` instance.
bucket!.removeAll { receiverContainer in
Expand Down
3 changes: 2 additions & 1 deletion Sources/EventDrivenSwift/EventListener/EventListenable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import Foundation
- Version: 3.0.0
*/
public typealias EventCallback = (_ event: any Eventable, _ priority: EventPriority, _ dispatchTime: DispatchTime) -> ()
public typealias EventFilterCallback = (_ event: any Eventable, _ priority: EventPriority, _ dispatchTime: DispatchTime) -> Bool

/**
Convienience `typealias` used for Typed Event Callbacks
Expand Down Expand Up @@ -63,7 +64,7 @@ public protocol EventListenable: AnyObject, EventReceiving {
- maximumAge: If `interestedIn` == `.youngerThan`, this is the number of nanoseconds between the time of dispatch and the moment of processing where the Listener will be interested in the Event. Any Event older will be ignored
- Returns: A `UUID` value representing the `token` associated with this Event Callback
*/
@discardableResult func addListener<TEvent: Eventable>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type, executeOn: ExecuteEventOn, interestedIn: EventListenerInterest, maximumAge: UInt64) -> EventListenerHandling
@discardableResult func addListener<TEvent: Eventable>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type, executeOn: ExecuteEventOn, interestedIn: EventListenerInterest, maximumAge: UInt64, customFilter: TypedEventFilterCallback<TEvent>?) -> EventListenerHandling

/**
Locates and removes the given Listener `token` (if it exists)
Expand Down
31 changes: 29 additions & 2 deletions Sources/EventDrivenSwift/EventListener/EventListener.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ open class EventListener: EventHandler, EventListenable {
var executeOn: ExecuteEventOn = .requesterThread
var interestedIn: EventListenerInterest = .all
var maximumEventAge: UInt64 = 0
var customFilter: EventFilterCallback?
}

/**
Expand Down Expand Up @@ -73,6 +74,8 @@ open class EventListener: EventHandler, EventListenable {

if listener.interestedIn == .youngerThan && listener.maximumEventAge != 0 && (DispatchTime.now().uptimeNanoseconds - event.dispatchTime.uptimeNanoseconds) > listener.maximumEventAge { continue } // If this Receiver has a maximum age of interest, and this Event is older than that... skip it!

if listener.interestedIn == .custom && (listener.customFilter == nil || !listener.customFilter!(event.event, priority, event.dispatchTime)) { continue }

switch listener.executeOn {
case .requesterThread:
Task { // We raise a Task because we don't want the entire Listener blocked in the event the dispatchQueue is busy or blocked!
Expand All @@ -91,12 +94,18 @@ open class EventListener: EventHandler, EventListenable {
}
}

@discardableResult public func addListener<TEvent: Eventable>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type, executeOn: ExecuteEventOn = .requesterThread, interestedIn: EventListenerInterest = .all, maximumAge: UInt64 = 0) -> EventListenerHandling {
@discardableResult public func addListener<TEvent: Eventable>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type, executeOn: ExecuteEventOn = .requesterThread, interestedIn: EventListenerInterest = .all, maximumAge: UInt64 = 0, customFilter: TypedEventFilterCallback<TEvent>? = nil) -> EventListenerHandling {
let eventTypeName = forEventType.getEventTypeName()
let method: EventCallback = { event, priority, dispatchTime in
self.callTypedEventCallback(callback, forEvent: event, priority: priority, dispatchTime: dispatchTime)
}
let eventListenerContainer = EventListenerContainer(requester: requester, callback: method, dispatchQueue: OperationQueue.current?.underlyingQueue, executeOn: executeOn, interestedIn: interestedIn, maximumEventAge: maximumAge)
var filterMethod: EventFilterCallback? = nil
if customFilter != nil {
filterMethod = { event, priority, dispatchTime in
self.callTypedEventFilterCallback(customFilter!, forEvent: event, priority: priority, dispatchTime: dispatchTime)
}
}
let eventListenerContainer = EventListenerContainer(requester: requester, callback: method, dispatchQueue: OperationQueue.current?.underlyingQueue, executeOn: executeOn, interestedIn: interestedIn, maximumEventAge: maximumAge, customFilter: filterMethod)
_eventListeners.withLock { eventCallbacks in
var bucket = eventCallbacks[eventTypeName]
if bucket == nil { bucket = [EventListenerContainer]() } // Create a new bucket if there isn't already one!
Expand Down Expand Up @@ -161,10 +170,28 @@ open class EventListener: EventHandler, EventListenable {
- callback: The code (Closure or Callback Method) to execute for the given `forEvent`, typed generically using `TEvent`
- forEvent: The instance of the `Eventable` type to be processed
- priority: The `EventPriority` with which the `forEvent` was dispatched
- dispatchTime: The `DispatchTime` at which `forEvent` was Dispatched
*/
internal func callTypedEventCallback<TEvent: Eventable>(_ callback: @escaping TypedEventCallback<TEvent>, forEvent: Eventable, priority: EventPriority, dispatchTime: DispatchTime) {
if let typedEvent = forEvent as? TEvent {
callback(typedEvent, priority, dispatchTime)
}
}

/**
Performs a Transparent Type Test, Type Cast, and Method Call to the Custom Filter via a `callback` Closure.
- Author: Simon J. Stuart
- Version: 5.2.0
- Parameters:
- callback: The code (Closure or Callback Method) to execute for the given `forEvent`, typed generically using `TEvent`... returns `true` if the Listener is interested in `forEvent`, `false` if the Listener wants to ignore it
- forEvent: The instance of the `Eventable` type to be processed
- priority: The `EventPriority` with which the `forEvent` was dispatched
- dispatchTime: The `DispatchTime` at which `forEvent` was Dispatched
*/
internal func callTypedEventFilterCallback<TEvent: Eventable>(_ callback: @escaping TypedEventFilterCallback<TEvent>, forEvent: Eventable, priority: EventPriority, dispatchTime: DispatchTime) -> Bool {
if let typedEvent = forEvent as? TEvent {
return callback(typedEvent, priority, dispatchTime)
}
return false /// We will simply return `false` if the Event is of the wrong Type
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,13 @@ public enum EventListenerInterest: CaseIterable {
- Version: 5.0.0
*/
case youngerThan

/**
Receivers will ignore any Event where the Filter Callback returns `false`, and accept any Event where the Filter Callback returns `true`
- Author: Simon J. Stuart
- Version: 5.2.0
*/
case custom
}

public typealias TypedEventFilterCallback<EventType: Eventable> = (_ event: EventType, _ priority: EventPriority, _ dispatchTime: DispatchTime) -> Bool
26 changes: 26 additions & 0 deletions Tests/EventDrivenSwiftTests/BasicEventListenerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ final class BasicEventListenerTests: XCTestCase, EventListening {
var myFoo = 0
var listenerHandler: EventListenerHandling? = nil
let testOne = TestEventTypeOne(foo: 1000) // Create the Event
let testZero = TestEventTypeOne(foo: 2000) // Create the Event
var awaiter = DispatchSemaphore(value: 0)

func testEventListenerOnListenerThread() throws {
Expand Down Expand Up @@ -76,4 +77,29 @@ final class BasicEventListenerTests: XCTestCase, EventListening {

listenerHandler?.remove()
}

func testEventListenerCustomFilter() throws {
XCTAssertEqual(myFoo, 0, "Expect initial value of eventThread.foo to be 0, but it's \(myFoo)")

listenerHandler = TestEventTypeOne.addListener(self, { (event: TestEventTypeOne, priority, dispatchTime) in
self.myFoo = event.foo
self.awaiter.signal()
}, executeOn: .taskThread, interestedIn: .custom, customFilter: { (event: TestEventTypeOne, priority, dispatchTime) in
if event.foo == 1000 {
print("Accepting Event because foo = 1000")
return true
}
print("Ignoring Event where foo = \(event.foo)")
return false
})

testOne.queue()
testZero.queue()

let result = awaiter.wait(timeout: DispatchTime.now().advanced(by: DispatchTimeInterval.seconds(10)))
XCTAssertEqual(result, .success, "The Event Handler was not invoked in time!")
XCTAssertEqual(self.myFoo, testOne.foo, "Expect new value of eventThread.foo to be \(testOne.foo), but it's \(self.myFoo)")

listenerHandler?.remove()
}
}

0 comments on commit 951b5c8

Please sign in to comment.