3

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?

Swifty
  • 839
  • 2
  • 15
  • 40
bogen
  • 9,954
  • 9
  • 50
  • 89
  • Can you turn the `isTimedOut` into a Signal? Does the `completeAction` contain the ID of the DeviceAction that just completed? – Daniel T. Nov 05 '18 at 14:19
  • @DanielT. I don't think thats a problem no, but is it a problem running through the pending list every second to see if any timers are timed out? seems it might be more efficent given thosands of actions. i also should get a "free" run if any action is added, since i can iterate through then also – bogen Nov 05 '18 at 14:21

2 Answers2

2

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.
}
Daniel T.
  • 32,821
  • 6
  • 50
  • 72
  • Was not avare of this pattern, but looks very promising! I will test this, never thought about doing it this way actually – bogen Nov 05 '18 at 14:35
  • For some other ideas: https://medium.com/@danielt1263/recipes-for-combining-observables-in-rxswift-ec4f8157265f – Daniel T. Nov 05 '18 at 14:38
1

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.

jjoelson
  • 5,771
  • 5
  • 31
  • 51
  • Interesting way of using scan instead of combineLatest, will check out, thanks! – bogen Nov 05 '18 at 14:43
  • Yeah, this is a frequently useful pattern in reactive programming when you are trying to maintain state. The `scan` holds the state and the enum cases are basically like methods that modify the state in some way. – jjoelson Nov 05 '18 at 14:58