Skip to content

Commit

Permalink
Merge pull request #6 from Flowduino/event_younger_than
Browse files Browse the repository at this point in the history
5.1.0 - Event "Younger Than" Age added
  • Loading branch information
LK-Simon authored Aug 30, 2022
2 parents 90fe210 + 29b97b6 commit 9c0924c
Show file tree
Hide file tree
Showing 13 changed files with 73 additions and 12 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version: 5.7
// swift-tools-version: 5.6
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,32 @@ class TemperatureRatingViewModel: ObservableObject {
```
By including the `interestedIn` optional parameter when invoking `addListener` against any `Eventable` type, and passing for this parameter a value of `.latestOnly`, we define that this *Listener* is only interested in the *Latest* `TemperatureRatingEvent` to be *Dispatched*. Should a number of `TemperatureRatingEvent`s build up in the Queue/Stack, the above-defined *Listener* will simply discard any older Events, and only invoke for the newest.

## `EventListener` with *Maximum Age* Interest
Version 5.1.0 of this library introduces the concent of *Maximum Age Listeners*. A *Maximum Age Listener* is a *Listener* that will only be invoked for *Events* of its registered *Event Type* that are younger than a defined *Maximum Age*. Any *Event* older than the defined *Maximum Age* will be skipped over, while any *Event* younger will invoke your *Listener*.

We have made it simple for you to configure your *Listener* to define a *Maximum Age* interest. 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
}

init() {
// Let's register our Event Listener Callback!
listenerHandle = TemperatureRatingEvent.addListener(self, onTemperatureRatingEvent, interestedIn: .youngerThan, maximumAge: 1 * 1_000_000_000)
}
}
```
In the above code example, `maximumAge` is a value defined in *nanoseconds*. With that in mind, `1 * 1_000_000_000` would be 1 second. This means that, any `TemperatureRatingEvent` older than 1 second would be ignored by the *Listener*, while any `TemperatureRatingEvent` *younger* than 1 second would invoke the `onTemperatureRatingEvent` method.

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

## `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) -> EventListenerHandling where TEvent : Eventable {
return _shared.eventListener.addListener(requester, callback, forEventType: forEventType, executeOn: executeOn, interestedIn: interestedIn)
@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)
}

@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) -> EventListenerHandling
@discardableResult static func addListener<TEvent: Eventable>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type, executeOn: ExecuteEventOn, interestedIn: EventListenerInterest, maximumAge: UInt64) -> 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) -> EventListenerHandling
@discardableResult static func addListener<TEvent: Eventable>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, executeOn: ExecuteEventOn, interestedIn: EventListenerInterest, maximumAge: UInt64) -> 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) -> EventListenerHandling {
return EventCentral.addListener(requester, callback, forEventType: Self.self, executeOn: executeOn, interestedIn: interestedIn)
@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)
}

public static func removeListener(_ token: UUID) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,11 @@ open class EventDispatcher: EventHandler, EventDispatching {
if receiver.receiver == nil { /// If the Recevier is `nil`...
continue
}

if receiver.receiver!.interestedIn == .latestOnly && event.dispatchTime < latestEventDispatchTime[event.event.getEventTypeName()]! { continue } // If this Receiver is only interested in the Latest Event dispatched for this Event Type, and this Event is NOT the Latest... skip it!

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

// so, we have a receiver... let's deal with it!
switch dispatchMethod {
case .stack:
Expand Down
6 changes: 4 additions & 2 deletions Sources/EventDrivenSwift/EventListener/EventListenable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,17 @@ public protocol EventListenable: AnyObject, EventReceiving {
/**
Registers an Event Callback for the given `Eventable` Type
- Author: Simon J. Stuart
- Version: 3.0.0
- Version: 5.1.0
- Parameters:
- requester: The Object owning the Callback Method
- callback: The code to invoke for the given `Eventable` Type
- forEventType: The `Eventable` Type for which to Register the Callback
- executeOn: Tells the `EventListenable` whether to execute the Callback on the `requester`'s Thread, or the Listener's.
- interestedIn: Defines the conditions under which the Listener is interested in an Event (anything outside of the given condition will be ignored by this Listener)
- 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) -> EventListenerHandling
@discardableResult func addListener<TEvent: Eventable>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, forEventType: Eventable.Type, executeOn: ExecuteEventOn, interestedIn: EventListenerInterest, maximumAge: UInt64) -> EventListenerHandling

/**
Locates and removes the given Listener `token` (if it exists)
Expand Down
9 changes: 7 additions & 2 deletions Sources/EventDrivenSwift/EventListener/EventListener.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import Observable
- Note: Inherit from this to implement a discrete unit of code designed specifically to operate upon specific `Eventable` types containing information useful to its operation(s)
*/
open class EventListener: EventHandler, EventListenable {
public var maximumEventAge: UInt64 = 0

public var interestedIn: EventListenerInterest = .all

/**
Expand All @@ -31,6 +33,7 @@ open class EventListener: EventHandler, EventListenable {
var dispatchQueue: DispatchQueue?
var executeOn: ExecuteEventOn = .requesterThread
var interestedIn: EventListenerInterest = .all
var maximumEventAge: UInt64 = 0
}

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

if listener.interestedIn == .latestOnly && event.dispatchTime < latestEventDispatchTime[event.event.getEventTypeName()]! { continue } // If this Listener is only interested in the Latest Event dispatched for this Event Type, and this Event is NOT the Latest... skip it!

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!

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 @@ -86,12 +91,12 @@ 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) -> 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) -> 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)
let eventListenerContainer = EventListenerContainer(requester: requester, callback: method, dispatchQueue: OperationQueue.current?.underlyingQueue, executeOn: executeOn, interestedIn: interestedIn, maximumEventAge: maximumAge)
_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
14 changes: 14 additions & 0 deletions Sources/EventDrivenSwift/EventListener/EventListenerInterest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@
import Foundation

public enum EventListenerInterest: CaseIterable {
/**
Receivers will receive all Events, regardless of age or whether they are the newest.
*/
case all

/**
Receivers will ignore any Events older than the last one dispatched of the given `Eventable` type.
*/
case latestOnly

/**
Receivers will ignore any Event that is older than a defined Delta (Maximum Age).
- Author: Simon J. Stuart
- Version: 5.0.0
*/
case youngerThan
}
2 changes: 2 additions & 0 deletions Sources/EventDrivenSwift/EventPool/EventPool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import ThreadSafeSwift
- Note: Event Pools own and manage all instances of the given `TEventThread` type
*/
open class EventPool<TEventThread: EventThreadable>: EventHandler, EventPooling {
public var maximumEventAge: UInt64 = 0

public var interestedIn: EventListenerInterest = .all

@ThreadSafeSemaphore public var balancer: EventPoolBalancing
Expand Down
2 changes: 2 additions & 0 deletions Sources/EventDrivenSwift/EventReceiver/EventReceiver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,7 @@ import Observable
- Note: `EventThread` inherits from this
*/
open class EventReceiver: EventHandler, EventReceiving {
public var maximumEventAge: UInt64 = 0

public var interestedIn: EventListenerInterest = .all
}
7 changes: 7 additions & 0 deletions Sources/EventDrivenSwift/EventReceiver/EventReceiving.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,11 @@ public protocol EventReceiving: AnyObject, EventHandling {
- Version: 4.3.0
*/
var interestedIn: EventListenerInterest { get set }

/**
Declares the maximum age of an `Eventable` before it will be ignored if `interestedIn` == `.youngerThan`
- Author: Simon J. Stuart
- Version: 5.1.0
*/
var maximumEventAge: UInt64 { get set }
}
2 changes: 1 addition & 1 deletion Tests/EventDrivenSwiftTests/BasicEventListenerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import ThreadSafeSwift
@testable import EventDrivenSwift

final class BasicEventListenerTests: XCTestCase, EventListening {
struct TestEventTypeOne: Eventable {
struct TestEventTypeOne: Eventable {
var foo: Int
}

Expand Down

0 comments on commit 9c0924c

Please sign in to comment.