Given a list of timers, how to output if one of them completed, while also being able to reset the list?

81 views Asked by At

I have an output signal that should output when one of a given set of timers time out, complete or when the entire list is reset.

enum DeviceActionStatus {
    case pending
    case completed
    case failed
}

struct DeviceAction {

    let start: Date
    let status: DeviceActionStatus 
    func isTimedOut() -> Bool // if start is over 30 seconds ago
    let id: String

}

Output signal:

let pendingActionUpdated: Signal<[DeviceAction], NoError>

Inputs:

let completeAction: Signal<String, NoError>
let tick: Signal<Void, NoError>  // runs every 1 second and should iterate to see if any DeviceAction is timed out
let addAction: Signal<DeviceAction, NoError> 
let resetAllActions: Signal<Void, NoError>

It should output an array of all running device actions.

let output = Signal.combineLatest( 
                     addAction,
                     resetAllActions,
                     tick,
                     Signal.merge(
                          completeAction,
                          tick.take(first: 1).map { _ in "InvalidActionId" }
                     )) // make sure the combinelatest can fire initially 

I've tried sending this to a .scan to cumulate every time the addAction is fired, and resetting every time the resetAllActions is fired, but since there is no way of knowing which of those fired, i can't get the logic to work. How can I both cumulate a growing list while also being able to run through it and being able to reset it when I want?

2

There are 2 answers

2
Daniel T. On BEST ANSWER

This looks like a job for the merge/enum pattern. I'm more an RxSwift guy myself, but if you map each of your Signals into an enum and merge them, then you can receive them properly into your scan...

enum ActionEvent {
    case complete(String)
    case tick
    case add(DeviceAction)
    case reset
}

merge(
    completeAction.map { ActionEvent.complete($0) },
    tick.map { ActionEvent.tick },
    addAction.map { ActionEvent.add($0) },
    resetAllActions.map { ActionEvent.reset }
).scan([DeviceAction]()) { actions, event in 
    switch event {
    case let .complete(id):
        return actions.filter { $0.id != id }
    case .tick:
        return actions.filter { $0.isTimedOut() == false }
    case let .add(action):
        return actions + [action]
    case .reset:
        let resetDate = Date()
        return actions.map { $0.start = resetDate }
        // or
        return []
        // depending on what "reset" means.
}
2
jjoelson On

It's a bit difficult to see the full use case here, so I will just describe how I would distinguish between addAction and resultAllActions being fired, leaving the rest of the design alone.

You can merge those two into one signal prior to the Signal.combineLatest. In order to do that, you will need to map them to the same type. An enum is perfect for this:

enum Action {
    case add(DeviceAction)
    case resetAll
}

Now you can map each signal and merge them into a single signal:

let action = Signal.merge(
    addAction.map { Action.add($0) },
    resetAllActions.map { _ in Action.resetAll })

Now you can switch on the value in your scan and determine whether it's a new action being added or a reset.