Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 34 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ let package = Package(
dependencies: [
.package(
url: "https://github.com/Flowduino/EventDrivenSwift.git",
.upToNextMajor(from: "4.2.0")
.upToNextMajor(from: "5.0.0")
),
],
//...
Expand Down Expand Up @@ -217,7 +217,7 @@ class TemperatureProcessor: EventThread {
}

/// Define our Callback Function to process received TemperatureEvent Events
func onTemperatureEvent(_ event: TemperatureEvent, _ priority: EventPriority) {
func onTemperatureEvent(_ event: TemperatureEvent, _ priority: EventPriority, _ dispatchTime: DispatchTime) {

}
}
Expand All @@ -230,6 +230,8 @@ The function `registerEventListeners` will be called automatically when an insta

Our *Callback* (or *Handler* or *Listener Event*) is called `onTemperatureEvent`, which is where we will implement whatever *Operation* is to be performed against a `TemperatureEvent`.

Version 5.0.0 introduces the new parameter, `dispatchTime`, which will always provide the `DispatchTime` reference at which the *Event* was *Dispatched*. You can use this to determine *Delta* (how much time has passed since the *Event* was *Dispatched*), which is particularly useful if you are performing interpolation and/or extrapolation.

Now, let's actually do something with our `TemperatureEvent` in the `onTemperatureEvent` method.
```swift
/// An Enum to map a Temperature value onto a Rating
Expand Down Expand Up @@ -262,7 +264,7 @@ Now, let's actually do something with our `TemperatureEvent` in the `onTemperatu
@ThreadSafeSemaphore public var temperatureInCelsius: Float = Float.zero
@ThreadSafeSemaphore public var temperatureRating: TemperatureRating = .freezing

func onTemperatureEvent(_ event: TemperatureEvent, _ priority: EventPriority) {
func onTemperatureEvent(_ event: TemperatureEvent, _ priority: EventPriority, _ dispatchTime: DispatchTime) {
temperatureInCelsius = event.temperatureInCelsius
temperatureRating = TemperatureRating.fromTemperature(event.temperatureInCelsius)
}
Expand Down Expand Up @@ -315,7 +317,7 @@ protocol TemperatureProcessorObserver: AnyObject {
```
Now let's modify the `onTemperatureEvent` method we implemented in the previous example:
```swift
func onTemperatureEvent(_ event: TemperatureEvent, _ priority: EventPriority) {
func onTemperatureEvent(_ event: TemperatureEvent, _ priority: EventPriority, _ dispatchTime: DispatchTime) {
temperatureInCelsius = event.temperatureInCelsius
temperatureRating = TemperatureRating.fromTemperature(event.temperatureInCelsius)

Expand Down Expand Up @@ -343,7 +345,7 @@ enum TemperatureRatingEvent: Eventable {
```
With the *Event* type defined, we can now once more expand our `onTemperatureEvent` to *Dispatch* our reciprocal `TemperatureRatingEvent`:
```swift
func onTemperatureEvent(_ event: TemperatureEvent, _ priority: EventPriority) {
func onTemperatureEvent(_ event: TemperatureEvent, _ priority: EventPriority, _ dispatchTime: DispatchTime) {
temperatureInCelsius = event.temperatureInCelsius
temperatureRating = TemperatureRating.fromTemperature(event.temperatureInCelsius)

Expand Down Expand Up @@ -385,7 +387,7 @@ class TemperatureRatingViewModel: ObservableObject {

var listenerHandle: EventListenerHandling?

internal func onTemperatureRatingEvent(_ event: TemperatureRatingEvent, _ priority: EventPriority) {
internal func onTemperatureRatingEvent(_ event: TemperatureRatingEvent, _ priority: EventPriority, _ dispatchTime: DispatchTime) {
temperatureInCelsius = event.temperatureInCelsius
temperatureRating = event.temperatureRating
}
Expand Down Expand Up @@ -427,6 +429,30 @@ This will remove your *Listener Callback*, meaning it will no longer be invoked

`EventListener`s are an extremely versatile and very powerful addition to `EventDrivenSwift`.

## `EventListener` with *Latest-Only* Interest
Version 4.3.0 of this library introduces the concept of *Latest-Only Listeners*. A *Latest-Only Listener* is a *Listener* that will only be invoked for the very latest *Event* of its requested *Event Type*. If there are a number of older *Events* of this type pending in a Queue/Stack, they will simply be skipped over... and only the very *Latest* will invoke your *Listener*.

We have made it incredibly simple for you to configure your *Listener* to be a *Latest-Only 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
}

init() {
// Let's register our Event Listener Callback!
listenerHandle = TemperatureRatingEvent.addListener(self, onTemperatureRatingEvent, interestedIn: .latestOnly)
}
}
```
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.

## `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 Expand Up @@ -456,9 +482,8 @@ The above example would use the `EventPoolLowestLoadBalancer` implementation, wh
## Features Coming Soon
`EventDrivenSwift` is an evolving and ever-improving Library, so here are lists of the features you can expect in future releases.

Version 4.3.0 (or 5.0.0 if interface-breaking changes are required):
- **Event Pool Scalers** - Dynamic Scaling for `EventPool` instances will be fully-implemented
- **Latest-Only Events** - A Dispatch option to replace any unprocessed (older) *Events* with the newest *Event* of that specific *Eventable* type. This will be useful for things like sensor readings, where you only care about the most recent value possible (because older values are no longer relevant)
Version 5.1.0 (or 6.0.0 if interface-breaking changes are required):
- **Event Pool Scalers** - Dynamic Scaling for `EventPool` instances will be fully-implemented (for the moment, no automatic Scaling will occur, and you cannot change the scale of an *Event Pool* once it has been initialised)

## License

Expand Down
10 changes: 5 additions & 5 deletions Sources/EventDrivenSwift/Central/EventCentral.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ final public class EventCentral: EventDispatcher, EventCentralable {
return _shared.eventCount
}
}

private var _eventListener: EventListenable?
internal var eventListener: EventListenable {
get {
Expand All @@ -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) -> EventListenerHandling where TEvent : Eventable {
return _shared.eventListener.addListener(requester, callback, forEventType: forEventType, executeOn: executeOn)
@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)
}

@inline(__always) public static func removeListener(_ token: UUID) {
Expand All @@ -87,11 +87,11 @@ final public class EventCentral: EventDispatcher, EventCentralable {
_shared.eventListener.removeListener(token, typeOf: typeOf)
}

@inline(__always) static public func scheduleQueue(_ event: Eventable, at: DispatchTime, priority: EventPriority) {
@inline(__always) public static func scheduleQueue(_ event: Eventable, at: DispatchTime, priority: EventPriority) {
_shared.scheduleQueue(event, at: at, priority: priority)
}

@inline(__always) static public func scheduleStack(_ event: Eventable, at: DispatchTime, priority: EventPriority) {
@inline(__always) public static func scheduleStack(_ event: Eventable, at: DispatchTime, priority: EventPriority) {
_shared.scheduleStack(event, at: at, priority: priority)
}

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

/**
Locates and removes the given Listener `token` (if it exists) from the Central Event Listener
Expand Down
36 changes: 31 additions & 5 deletions Sources/EventDrivenSwift/Event/Eventable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +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) -> EventListenerHandling

// @discardableResult static func addListener(_ requester: AnyObject?, _ eventType: any Eventable.Type, _ callback: @escaping TypedEventCallback<any Eventable.Type>, executeOn: ExecuteEventOn) -> UUID
@discardableResult static func addListener<TEvent: Eventable>(_ requester: AnyObject?, _ callback: @escaping TypedEventCallback<TEvent>, executeOn: ExecuteEventOn, interestedIn: EventListenerInterest) -> EventListenerHandling

/**
Locates and removes the given Listener `token` (if it exists) from the Central Event Listener
Expand All @@ -75,6 +73,20 @@ public protocol Eventable {
- token: The Token of the Listener you wish to remove
*/
static func removeListener(_ token: UUID)

/**
Returns the Fully-Qualified Type Name for this Eventable Type
- Author: Simon J. Stuart
- Version: 4.3.0
*/
func getEventTypeName() -> String

/**
Returns the Fully-Qualified Type Name for this Eventable Type
- Author: Simon J. Stuart
- Version: 4.3.0
*/
static func getEventTypeName() -> String
}

/**
Expand Down Expand Up @@ -110,12 +122,26 @@ 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) -> EventListenerHandling {
return EventCentral.addListener(requester, callback, forEventType: Self.self, executeOn: executeOn)
@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)
}

public static func removeListener(_ token: UUID) {
EventCentral.removeListener(token, typeOf: Self.self)
}
}

/**
Extension to provide central access to Event Type Names
- Author: Simon J. Stuart
- Version: 4.3.0
*/
extension Eventable {
@inline(__always) public func getEventTypeName() -> String {
return String(reflecting: type(of: self))
}

@inline(__always) public static func getEventTypeName() -> String {
return String(reflecting: self)
}
}
38 changes: 38 additions & 0 deletions Sources/EventDrivenSwift/Event/Wrappers/EventListeners.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// EventListeners.swift
// Copyright (c) 2022, Flowduino
// Authored by Simon J. Stuart on 21st August 2022
//
// Subject to terms, restrictions, and liability waiver of the MIT License
//

import Foundation
/*
public protocol EventListenerCallbackContainer {
associatedtype TEventType: Eventable

static func buildBlock(_ callback: @escaping ((_ event: TEventType, _ priority: EventPriority) -> ()), _ owner: AnyObject, _ executeOn: ExecuteEventOn) -> EventListenerHandling
}

@resultBuilder
public struct EventListenerCallback<TEventType: Eventable>: EventListenerCallbackContainer {
public static func buildBlock(_ callback: @escaping ((_ event: TEventType, _ priority: EventPriority) -> ()), _ owner: AnyObject, _ executeOn: ExecuteEventOn = .requesterThread) -> EventListenerHandling {
return TEventType.addListener(owner, callback, executeOn: executeOn)
}

init() {
print("Init called")
}
}

@resultBuilder
public struct EventListeners<TOwner: AnyObject> {
public static func buildBlock(_ owner: TOwner, _ executeOn: ExecuteEventOn = .requesterThread, _ eventType: Eventable.Type, _ events: EventCallback...) -> [EventListenerHandling] {
var results = [EventListenerHandling]()
for event in events {
// results.append(EventCentral.addListener(owner, event, forEventType: eventType))
}
return results
}
}
*/
8 changes: 4 additions & 4 deletions Sources/EventDrivenSwift/Event/Wrappers/EventMethod.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import Foundation

public typealias EventMethodTypedEventCallback<TOwner: AnyObject, TEvent: Any> = (_ sender: TOwner, _ event: TEvent, _ priority: EventPriority) -> ()
public typealias EventMethodTypedEventCallback<TOwner: AnyObject, TEvent: Any> = (_ sender: TOwner, _ event: TEvent, _ priority: EventPriority, _ dispatchTime: DispatchTime) -> ()

/**
Any Property wrapped with `EventMethod` will automatically conform to `EventMethodContainer`
Expand All @@ -28,7 +28,7 @@ public protocol EventMethodContainer {
/**
Decorate Typed Event Callback Closures as `var` with `@EventMethod<TEventType>` to automatically register them.
- Author: Simon J. Stuart
- Version: 4.1.0
- Version: 5.0.0
*/
@propertyWrapper
public struct EventMethod<TOwner: AnyObject, TEventType: Eventable>: EventMethodContainer {
Expand All @@ -37,9 +37,9 @@ public struct EventMethod<TOwner: AnyObject, TEventType: Eventable>: EventMethod

private weak var owner: AnyObject? = nil

private func callback(event: TEventType, priority: EventPriority) {
private func callback(event: TEventType, priority: EventPriority, dispatchTime: DispatchTime) {
if let typedOwner = owner as? TOwner {
wrappedValue?(typedOwner, event, priority)
wrappedValue?(typedOwner, event, priority, dispatchTime)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ open class EventDispatcher: EventHandler, EventDispatching {
@ThreadSafeSemaphore private var receivers = [String:[ReceiverContainer]]()

public func addReceiver(_ receiver: any EventReceiving, forEventType: Eventable.Type) {
let eventTypeName = String(reflecting: forEventType)
let eventTypeName = forEventType.getEventTypeName()

_receivers.withLock { receivers in
var bucket = receivers[eventTypeName]
Expand All @@ -50,7 +50,7 @@ open class EventDispatcher: EventHandler, EventDispatching {
}

public func removeReceiver(_ receiver: any EventReceiving, forEventType: Eventable.Type) {
let eventTypeName = String(reflecting: forEventType)
let eventTypeName = forEventType.getEventTypeName()

_receivers.withLock { receivers in
var bucket = receivers[eventTypeName]
Expand Down Expand Up @@ -82,8 +82,8 @@ open class EventDispatcher: EventHandler, EventDispatching {
- Author: Simon J. Stuart
- Version: 1.0.0
*/
override open func processEvent(_ event: any Eventable, dispatchMethod: EventDispatchMethod, priority: EventPriority) {
let eventTypeName = String(reflecting: type(of: event))
override open func processEvent(_ event: EventDispatchContainer, dispatchMethod: EventDispatchMethod, priority: EventPriority) {
let eventTypeName = event.event.getEventTypeName()

var snapReceivers = [String:[ReceiverContainer]]()

Expand All @@ -102,6 +102,7 @@ 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!

// so, we have a receiver... let's deal with it!
switch dispatchMethod {
Expand Down
Loading