I am using ReactiveCocoa in an app which makes calls to remote web APIs. But before any thing can be retrieved from a given API host, the app must provide the user\'s creden
So, there are two major things going on here:
-getToken
to get the same values no matter what.In order to share side effects (#1 above), we'll use RACMulticastConnection. Like the documentation says:
A multicast connection encapsulates the idea of sharing one subscription to a signal to many subscribers. This is most often needed if the subscription to the underlying signal involves side-effects or shouldn't be called more than once.
Let's add one of those as a private property on the API client class:
@interface APIClient ()
@property (nonatomic, strong, readonly) RACMulticastConnection *tokenConnection;
@end
Now, this will solve the case of N current subscribers that all need the same future result (API calls waiting on the request token being in-flight), but we still need something else to ensure that future subscribers get the same result (the already-fetched token), no matter when they subscribe.
This is what RACReplaySubject is for:
A replay subject saves the values it is sent (up to its defined capacity) and resends those to new subscribers. It will also replay an error or completion.
To tie these two concepts together, we can use RACSignal's -multicast: method, which turns a normal signal into a connection by using a specific kind of subject.
We can hook up most of the behaviors at initialization time:
- (id)init {
self = [super init];
if (self == nil) return nil;
// Defer the invocation of -reallyGetToken until it's actually needed.
// The -defer: is only necessary if -reallyGetToken might kick off
// a request immediately.
RACSignal *deferredToken = [RACSignal defer:^{
return [self reallyGetToken];
}];
// Create a connection which only kicks off -reallyGetToken when
// -connect is invoked, shares the result with all subscribers, and
// pushes all results to a replay subject (so new subscribers get the
// retrieved value too).
_tokenConnection = [deferredToken multicast:[RACReplaySubject subject]];
return self;
}
Then, we implement -getToken
to trigger the fetch lazily:
- (RACSignal *)getToken {
// Performs the actual fetch if it hasn't started yet.
[self.tokenConnection connect];
return self.tokenConnection.signal;
}
Afterwards, anything that subscribes to the result of -getToken
(like -requestSignalWithURLRequest:
) will get the token if it hasn't been fetched yet, start fetching it if necessary, or wait for an in-flight request if there is one.
How about
...
@property (nonatomic, strong) RACSignal *getToken;
...
- (id)init {
self = [super init];
if (self == nil) return nil;
self.getToken = [[RACSignal defer:^{
return [self reallyGetToken];
}] replayLazily];
return self;
}
To be sure, this solution is functional identical to Justin's answer above. Basically we take advantage of the fact that convenience method already exists in RACSignal
's public API :)
Thinking about token will expire later and we have to refresh it.
I store token in a MutableProperty, and used a lock to prevent multiple expired request to refresh the token, once the token is gained or refreshed, just request again with new token.
For the first few requests, since there's no token, request signal will flatMap to error, and thus trigger refreshAT, meanwhile we do not have refreshToken, thus trigger refreshRT, and set both at and rt in the final step.
here's full code
static var headers = MutableProperty(["TICKET":""])
static let atLock = NSLock()
static let manager = Manager(
configuration: NSURLSessionConfiguration.defaultSessionConfiguration()
)
internal static func GET(path:String!, params:[String: String]) -> SignalProducer<[String: AnyObject], NSError> {
let reqSignal = SignalProducer<[String: AnyObject], NSError> {
sink, dispose in
manager.request(Router.GET(path: path, params: params))
.validate()
.responseJSON({ (response) -> Void in
if let error = response.result.error {
sink.sendFailed(error)
} else {
sink.sendNext(response.result.value!)
sink.sendCompleted()
}
})
}
return reqSignal.flatMapError { (error) -> SignalProducer<[String: AnyObject], NSError> in
return HHHttp.refreshAT()
}.flatMapError({ (error) -> SignalProducer<[String : AnyObject], NSError> in
return HHHttp.refreshRT()
}).then(reqSignal)
}
private static func refreshAT() -> SignalProducer<[String: AnyObject], NSError> {
return SignalProducer<[String: AnyObject], NSError> {
sink, dispose in
if atLock.tryLock() {
Alamofire.Manager.sharedInstance.request(.POST, "http://example.com/auth/refresh")
.validate()
.responseJSON({ (response) -> Void in
if let error = response.result.error {
sink.sendFailed(error)
} else {
let v = response.result.value!["data"]
headers.value.updateValue(v!["at"] as! String, forKey: "TICKET")
sink.sendCompleted()
}
atLock.unlock()
})
} else {
headers.signal.observe(Observer(next: { value in
print("get headers from local: \(value)")
sink.sendCompleted()
}))
}
}
}
private static func refreshRT() -> SignalProducer<[String: AnyObject], NSError> {
return SignalProducer<[String: AnyObject], NSError> {
sink, dispose in
Alamofire.Manager.sharedInstance.request(.POST, "http://example.com/auth/refresh")
.responseJSON({ (response) -> Void in
let v = response.result.value!["data"]
headers.value.updateValue(v!["at"] as! String, forKey: "TICKET")
sink.sendCompleted()
})
}
}