diff --git a/Package.swift b/Package.swift index e11078a..d1e2414 100644 --- a/Package.swift +++ b/Package.swift @@ -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 diff --git a/README.md b/README.md index 04bb661..e35f4e6 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/Sources/EventDrivenSwift/Central/EventCentral.swift b/Sources/EventDrivenSwift/Central/EventCentral.swift index 294eefc..36cddd5 100644 --- a/Sources/EventDrivenSwift/Central/EventCentral.swift +++ b/Sources/EventDrivenSwift/Central/EventCentral.swift @@ -75,8 +75,8 @@ final public class EventCentral: EventDispatcher, EventCentralable { } } - @discardableResult @inline(__always) public static func addListener(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback, 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(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback, 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) { diff --git a/Sources/EventDrivenSwift/Central/EventCentralable.swift b/Sources/EventDrivenSwift/Central/EventCentralable.swift index 8d21e83..6bbe0ca 100644 --- a/Sources/EventDrivenSwift/Central/EventCentralable.swift +++ b/Sources/EventDrivenSwift/Central/EventCentralable.swift @@ -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(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback, forEventType: Eventable.Type, executeOn: ExecuteEventOn, interestedIn: EventListenerInterest) -> EventListenerHandling + @discardableResult static func addListener(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback, 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 diff --git a/Sources/EventDrivenSwift/Event/Eventable.swift b/Sources/EventDrivenSwift/Event/Eventable.swift index c013eeb..c89d17d 100644 --- a/Sources/EventDrivenSwift/Event/Eventable.swift +++ b/Sources/EventDrivenSwift/Event/Eventable.swift @@ -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(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback, executeOn: ExecuteEventOn, interestedIn: EventListenerInterest) -> EventListenerHandling + @discardableResult static func addListener(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback, executeOn: ExecuteEventOn, interestedIn: EventListenerInterest, maximumAge: UInt64) -> EventListenerHandling /** Locates and removes the given Listener `token` (if it exists) from the Central Event Listener @@ -122,8 +122,8 @@ extension Eventable { EventCentral.scheduleStack(self, at: at, priority: priority) } - @discardableResult static public func addListener(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback, executeOn: ExecuteEventOn = .requesterThread, interestedIn: EventListenerInterest = .all) -> EventListenerHandling { - return EventCentral.addListener(requester, callback, forEventType: Self.self, executeOn: executeOn, interestedIn: interestedIn) + @discardableResult static public func addListener(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback, 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) { diff --git a/Sources/EventDrivenSwift/EventDispatcher/EventDispatcher.swift b/Sources/EventDrivenSwift/EventDispatcher/EventDispatcher.swift index 8efe0ba..e466e45 100644 --- a/Sources/EventDrivenSwift/EventDispatcher/EventDispatcher.swift +++ b/Sources/EventDrivenSwift/EventDispatcher/EventDispatcher.swift @@ -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: diff --git a/Sources/EventDrivenSwift/EventListener/EventListenable.swift b/Sources/EventDrivenSwift/EventListener/EventListenable.swift index 7afd223..dbcfea6 100644 --- a/Sources/EventDrivenSwift/EventListener/EventListenable.swift +++ b/Sources/EventDrivenSwift/EventListener/EventListenable.swift @@ -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(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback, forEventType: Eventable.Type, executeOn: ExecuteEventOn, interestedIn: EventListenerInterest) -> EventListenerHandling + @discardableResult func addListener(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback, forEventType: Eventable.Type, executeOn: ExecuteEventOn, interestedIn: EventListenerInterest, maximumAge: UInt64) -> EventListenerHandling /** Locates and removes the given Listener `token` (if it exists) diff --git a/Sources/EventDrivenSwift/EventListener/EventListener.swift b/Sources/EventDrivenSwift/EventListener/EventListener.swift index 77f2d0d..df179a7 100644 --- a/Sources/EventDrivenSwift/EventListener/EventListener.swift +++ b/Sources/EventDrivenSwift/EventListener/EventListener.swift @@ -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 /** @@ -31,6 +33,7 @@ open class EventListener: EventHandler, EventListenable { var dispatchQueue: DispatchQueue? var executeOn: ExecuteEventOn = .requesterThread var interestedIn: EventListenerInterest = .all + var maximumEventAge: UInt64 = 0 } /** @@ -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! @@ -86,12 +91,12 @@ open class EventListener: EventHandler, EventListenable { } } - @discardableResult public func addListener(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback, forEventType: Eventable.Type, executeOn: ExecuteEventOn = .requesterThread, interestedIn: EventListenerInterest = .all) -> EventListenerHandling { + @discardableResult public func addListener(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback, 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! diff --git a/Sources/EventDrivenSwift/EventListener/EventListenerInterest.swift b/Sources/EventDrivenSwift/EventListener/EventListenerInterest.swift index 1122634..cf9b034 100644 --- a/Sources/EventDrivenSwift/EventListener/EventListenerInterest.swift +++ b/Sources/EventDrivenSwift/EventListener/EventListenerInterest.swift @@ -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 } diff --git a/Sources/EventDrivenSwift/EventPool/EventPool.swift b/Sources/EventDrivenSwift/EventPool/EventPool.swift index 7df1ee5..ba49da1 100644 --- a/Sources/EventDrivenSwift/EventPool/EventPool.swift +++ b/Sources/EventDrivenSwift/EventPool/EventPool.swift @@ -18,6 +18,8 @@ import ThreadSafeSwift - Note: Event Pools own and manage all instances of the given `TEventThread` type */ open class EventPool: EventHandler, EventPooling { + public var maximumEventAge: UInt64 = 0 + public var interestedIn: EventListenerInterest = .all @ThreadSafeSemaphore public var balancer: EventPoolBalancing diff --git a/Sources/EventDrivenSwift/EventReceiver/EventReceiver.swift b/Sources/EventDrivenSwift/EventReceiver/EventReceiver.swift index 524a2ef..ea0d938 100644 --- a/Sources/EventDrivenSwift/EventReceiver/EventReceiver.swift +++ b/Sources/EventDrivenSwift/EventReceiver/EventReceiver.swift @@ -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 } diff --git a/Sources/EventDrivenSwift/EventReceiver/EventReceiving.swift b/Sources/EventDrivenSwift/EventReceiver/EventReceiving.swift index 3d5c730..36037c7 100644 --- a/Sources/EventDrivenSwift/EventReceiver/EventReceiving.swift +++ b/Sources/EventDrivenSwift/EventReceiver/EventReceiving.swift @@ -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 } } diff --git a/Tests/EventDrivenSwiftTests/BasicEventListenerTests.swift b/Tests/EventDrivenSwiftTests/BasicEventListenerTests.swift index 7f7909f..8acde01 100644 --- a/Tests/EventDrivenSwiftTests/BasicEventListenerTests.swift +++ b/Tests/EventDrivenSwiftTests/BasicEventListenerTests.swift @@ -10,7 +10,7 @@ import ThreadSafeSwift @testable import EventDrivenSwift final class BasicEventListenerTests: XCTestCase, EventListening { - struct TestEventTypeOne: Eventable { + struct TestEventTypeOne: Eventable { var foo: Int }