Reactivecocoa: RAC Principles: How best to start `SignalProducer` only once?

Created on 22 May 2016  路  5Comments  路  Source: ReactiveCocoa/ReactiveCocoa

Hi - this came up just now during a discussion with a friend (cc @mgrebenets)

I have a UserManager which retrieves User objects from the network and wish to expose an interface on the UserManager as follows:

func getUsers() -> SignalProducer<User, ErrorType>

(I'm returning a SignalProducer as I want to flatMap this network request with requests to other network resources).

The catch is that I only want to start the network request wrapped up in the SignalProducer if there's not a request currently in progress.

My intuition to solve this went like this:

class UserManager {

    static let sharedManager = UserManager()

    private var requestSignal: Signal<User, NSError>? = nil

    func getUsers() -> SignalProducer<User, NSError> {
        if let requestSignal = requestSignal {
            print("There is already a request in progress so simply return a reference to its signal")
            return SignalProducer(signal: requestSignal)
        } else {
            let requestSignalProducer = SignalProducer<User, NSError> { observer, disposable in
                let url = NSURL(string:"http://jsonplaceholder.typicode.com/users/1")!
                let task = NSURLSession.sharedSession()
                    .dataTaskWithURL(url) { (data, response, error) in
                    if error != nil {
                        observer.sendFailed(NSError(domain:"", code:5, userInfo:nil))
                    } else {
                        let json = try! NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions())
                        let user = User(JSON: json)
                        print("Completed user request at \(NSDate())")
                        observer.sendNext(user)
                        observer.sendCompleted()
                    }
                }
                print("Started user request at \(NSDate())")
                task.resume()
            }
            requestSignalProducer.startWithSignal{ [weak self] (signal, disposable) in
                guard let `self` = self else { return }
                `self`.requestSignal = signal
                signal.observeCompleted {
                    print("Completing the user request signal")
                    `self`.requestSignal = nil
                }
            }
            return SignalProducer(signal: self.requestSignal!)
        }
    }
}

Buuuut the mutable state in the requestSignal feels really "Un-RAC". Would appreciate any pointers you could give on a solution which is more in line with the RAC principles.

Cheers! 馃榾

question

All 5 comments

Using replayLazily would simplify things a bit鈥攖hen you could save the SignalProducer directly instead of redirecting a signal.

Also, there's a race condition here, so you might want to use RAC's Atomic type. (If 2 threads call getUsers at the same time, you could could end up with 2 requests.)

I'd also recommend splitting the actual API code into a separate method or class. That will make the code a little easier to read and separate the concerns a little better.

struct API {
    static func getUsers() -> SignalProducer<User, NSError> {
            return SignalProducer<User, NSError> { observer, disposable in
                let url = NSURL(string:"http://jsonplaceholder.typicode.com/users/1")!
                let task = NSURLSession.sharedSession()
                    .dataTaskWithURL(url) { (data, response, error) in
                    if error != nil {
                        observer.sendFailed(NSError(domain:"", code:5, userInfo:nil))
                    } else {
                        let json = try! NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions())
                        let user = User(JSON: json)
                        print("Completed user request at \(NSDate())")
                        observer.sendNext(user)
                        observer.sendCompleted()
                    }
                }
                print("Started user request at \(NSDate())")
                task.resume()
         }
    }
}

class UserManager {
    static let sharedManager = UserManager()

    private var requestProducer: Atomic<SignalProducer<User, NSError>?> = Atomic(nil)

    func getUsers() -> SignalProducer<User, NSError> {
        return requestProducer.modify { producer in
            if let producer = producer {
                print("There is already a request in progress so simply return a reference to its signal")
                return producer
            }

            return API.getUsers()
                .on(completed: {
                    self.requestProcuder.modifify { _ in nil }
                })
               .replayLazily()
        }
    }
}

鈿狅笍 I typed this directly into the browser and it's untested. 鈿狅笍

There still is state, but it's contained a little better.

The RAC-iest way to do it would be to add a new operator that contains the state.

extension SignalProducer {
    func replayIfStarted() -> SignalProducer<Value, Error> {
        let active: Atomic<SignalProducer<Value, Error>?> = nil
        return active.modify { producer in
            if let producer = producer {
                return producer
            }

            return self
                .on(completed: {
                    active.modify { _ in nil }
                })
                .replayLazily()
        }
    }
}

I'd be sure to double check that against the RAC codebase, and also write some tests around the producer lifetime, to make sure I wrote it properly. 鈽猴笍

Nice! And apologies for the code-barf on my part, I should have prefaced that block with a "this is not prod code" message 馃槃

I'll check this out when I'm in front of Xcode; appreciate all your insight.

I'm going to close this. Feel free to reopen if you have questions after looking at it!

馃憤 thanks Matt

Here's another way to go about this problem: using Property!

final class UserManager {
    /// The public API is as easy as observing this.
    public let users: AnyProperty<[User]>
    private let usersMutableProperty = MutableProperty<[User]>([])

    init() {
        self.users = AnyProperty(self.usersMutableProperty)  
    }

    private func getUsers() -> SignalProducer<User, NSError> { }

    public func requestUsers() {
        /// This is not idea because multiple calls to this method would step onto one-another, just doing it like this for simplicity
        self.usersMutableProperty <~ self.usersRequest()
    }
}
Was this page helpful?
0 / 5 - 0 ratings