How to conditionally buffer RACSignal values? - ios

I'm working on some code that interacts with a remote API via websockets. My data layer is responsible for establishing and monitoring the websocket connection. It also contains methods that can be used by the application to enqueue websocket messages to be sent. The application code should not be responsible for inspecting the state of the websocket connection, aka fire-and-forget.
Ideally, I'd like to data layer to function as follows:
When the data layer does not have a connection to the websocket endpoint (self.isConnected == NO), messages are buffered internally.
When a connection is becomes available (self.isConnected == YES), buffered messages are immediately sent, and any subsequent messages are sent immediately.
Here's what I've been able to come up with:
#import "RACSignal+Buffering.h"
#implementation RACSignal (Buffering)
- (RACSignal*)bufferWithSignal:(RACSignal*)shouldBuffer
{
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
RACCompoundDisposable *disposable = [RACCompoundDisposable compoundDisposable];
NSMutableArray* bufferedValues = [[NSMutableArray alloc] init];
__block BOOL buffering = NO;
void (^bufferHandler)() = ^{
if (!buffering)
{
for (id val in bufferedValues)
{
[subscriber sendNext:val];
}
[bufferedValues removeAllObjects];
}
};
RACDisposable* bufferDisposable = [shouldBuffer subscribeNext:^(NSNumber* shouldBuffer) {
buffering = shouldBuffer.boolValue;
bufferHandler();
}];
if (bufferDisposable)
{
[disposable addDisposable:bufferDisposable];
}
RACDisposable* valueDisposable = [self subscribeNext:^(id x) {
[bufferedValues addObject:x];
bufferHandler();
} error:^(NSError *error) {
[subscriber sendError:error];
} completed:^{
[subscriber sendCompleted];
}];
if (valueDisposable)
{
[disposable addDisposable:valueDisposable];
}
return disposable;
}];
}
#end
Lastly, this is pseudo-code for how it would be used:
#interface APIManager ()
#property (nonatomic) RACSubject* requests;
#end
#implementation WebsocketDataLayer
- (id)init
{
self = [super init];
if (self) {
RACSignal* connectedSignal = RACObserve(self, connected);
self.requests = [[RACSubject alloc] init];
RACSignal* bufferedApiRequests = [self.requests bufferWithSignal:connectedSignal];
[self rac_liftSelector:#selector(sendRequest:) withSignalsFromArray:#[bufferedApiRequests]];
}
return self;
}
- (void)enqueueRequest:(NSString*)request
{
[self.requests sendNext:request];
}
- (void)sendRequest:(NSString*)request
{
DebugLog(#"Making websocket request: %#", request);
}
#end
My question is: Is this the right approach for buffering values? Is there a more idiomatic RAC way of handling this?

Buffering can be thought of as something that applies to individual requests, which leads to a natural implementation using -flattenMap: and RACObserve:
#weakify(self);
RACSignal *bufferedRequests = [self.requests flattenMap:^(NSString *request) {
#strongify(self);
// Waits for self.connected to be YES, or checks that it already is,
// then forwards the request.
return [[[[RACObserve(self, connected)
ignore:#NO]
take:1]
// Replace the property value with our request.
mapReplace:request];
}];
If ordering is important, you can replace -flattenMap: with -map: plus -concat. These implementations avoid the need for any custom operators, and work without manual subscriptions (which are notoriously messy).

You do almost exactly the same as what is implemented in the bufferWithTime: operation and I can't think of any existing operations that would implement it more idiomatically. (Probably this is the reason why bufferWithTime was implemented in this way.) Reviewing your code using that implementation may reveal some faults you didn't think of.
But to be honest, this should not be so hard. There should exist a buffering operation that buffers the output and spews the contents when the trigger signal fires. Probably most buffering can be implemented in terms of this functionality, so having it would add value to the framework.

Related

Reactive Cocoa: trouble with switchToLatest in chain of operations

I'm having trouble disposing of one of the operations in a chained list of operations. I'm trying to use Reactive Cocoa to provide a stream of locations while the user is logged in and has granted access to location services.
I'm still pretty new to the world of Reactive Cocoa and functional reactive programming in general, but here's what I've got so far:
#weakify(self);
// signal of BOOL
RACSignal *loggedInSignal = [[self loggedInSignal] distinctUntilChanged];
// signal of CLAuthorizationStatus
RACSignal *currentStateSignal = [[self currentAuthorizationSignal] distinctUntilChanged];
// defer creation of the location stream signal until subscribed to
RACSignal *locationSignal = [RACSignal defer:^RACSignal *
{
#strongify(self);
// To get an uninterrupted stream of locations, just retry the stream if an error occurs
return [[self locationsSignal] retry];
}];
RACSignal *locationServicesDisabledSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber)
{
#strongify(self);
[self locationServicesDisabled];
[subscriber sendCompleted];
return nil;
}];
self.disposable = [[[[[[loggedInSignal map:^id(NSNumber *loggedIn)
{
return [loggedIn boolValue] ? currentStateSignal : [RACSignal empty];
}]
switchToLatest]
map:^id(NSNumber *currentStatus)
{
if (CLAuthorizationStatusCanBeginMonitoring([currentStatus intValue]))
{
return locationSignal;
}
return locationServicesDisabledSignal;
}]
switchToLatest]
subscribeNext:^(CLLocation *location)
{
#strongify(self);
CLLocationCoordinate2D coord = location.coordinate;
[self.output receivedNewLocationWithLatitude:coord.latitude
longitude:coord.longitude
accuracy:location.horizontalAccuracy
timestamp:location.timestamp];
}]
asScopedDisposable];
This works as expected when the authorisation status changes, starting/stopping the location stream. But when logging out, the location stream continues to provide locations. It's as if authStateSignal is not disposed of, adjusting the authorisation status will still start/stop the stream, even though loggedInSignal had returned NO as its last value.
I tried this:
(previous code as above)...
return locationServicesDisabledSignal;
}]
switchToLatest]
takeUntil:[loggedInSignal skip:1]]
subscribeNext:^(CLLocation *location)
{
(etc)...
after the second switchToLatest to stop that signal after the next log in/out, but besides feeling kind of "hack-ish", it stopped the location stream events after 1 log in/out, to never start them again. What is the proper method of handling this scenario?
Also, it feels like I'm using locationServicesDisabledSignal in the wrong way. I want a method to be called when authStateSignal returns a status indicating the location services are disabled, so that I can react accordingly, but putting the method call inside a [RACSignal createSignal:] block that way doesn't feel very Reactive Cocoa-ey. Any other tips on how to do any of what I'm trying the Reactive Cocoa way would be greatly appreciated too.
The reason it isn't working out for you is that when you return a [RACSignal empty]; values will nor propogate through your chain because [RACSignal empty]; does not send any next events. So Try this:
// signal of BOOL
RACSignal *loggedInSignal = [[self loggedInSignal] distinctUntilChanged];
// signal of CLAuthorizationStatus
RACSignal *currentStateSignal = [[self currentAuthorizationSignal] distinctUntilChanged];
// defer creation of the location stream signal until subscribed to
RACSignal *locationSignal = [RACSignal defer:^RACSignal *
{
#strongify(self);
// To get an uninterrupted stream of locations, just retry the stream if an error occurs
return [[self locationsSignal] retry];
}];
RACSignal *locationServicesDisabledSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber)
{
#strongify(self);
[self locationServicesDisabled];
[subscriber sendCompleted];
return nil;
}];
self.disposable = [[[RACSignal combineLatest:#[loggedInSignal, currentStateSignal]] flattenMap: ^RACStream * (RACTuple *tuple){
NSNumber* loggedIn = [tuple first];
NSNumber* currentStatus = [tuple second];
if([loggedIn boolValue] && CLAuthorizationStatusCanBeginMonitoring([currentStatus intValue])){
return locationSignal
}
else {
return locationServicesDisabledSignal
}
} ]
subscribeNext:^(CLLocation *location)
{
#strongify(self);
CLLocationCoordinate2D coord = location.coordinate;
[self.output receivedNewLocationWithLatitude:coord.latitude
longitude:coord.longitude
accuracy:location.horizontalAccuracy
timestamp:location.timestamp];
}]

ReactiveCocoa, RACCommand show message or execute segue after completed

I'm using RACCommand for my UI button click event. I'm using MVVM architecture. In my ViewModel I have this:
#property (strong, nonatomic) RACCommand *executeRegistration;
Inside "init" I have this:
self.executeRegistration = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input)
{
return [self executeSearchSignal];
}];
Execute search signal is this:
- (RACSignal *)executeSearchSignal {
return [[[self.services insertUserRegistration]
registerUserName:self.userName]
logAll];
}
My "[self.services insertUserRegistration]
registerUserName:self.userName" is this:
#weakify(self);
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber)
{
#strongify(self);
Manager *manager = [Manager sharedManager];
manager._delegate = self;
RACSignal *successSignal =
[self rac_signalForSelector:#selector(manager:didSuccesWithoutError:)
fromProtocol:#protocol(ManagerDelegate)];
RACSignal *failSignal =
[self rac_signalForSelector:#selector(manager:didFailWithError:)
fromProtocol:#protocol(ManagerDelegate)];
[[successSignal map:^id(RACTuple *tuple)
{
return tuple.second;
}] subscribeNext:^(id x) {
[subscriber sendNext:x];
[subscriber sendCompleted];
}];
[[failSignal map:^id(RACTuple *tuple)
{
return tuple.second;
}] subscribeNext:^(id x) {
[subscriber sendError:x];
}];
[manager insertUserRegistration:name];
return nil;
}];
My ViewController has this in BindViewModel method:
[self.finishRegistrationButton.rac_command.executionSignals subscribeNext:^(RACSignal *loginSignal) {
// Log a message whenever we log in successfully.
[loginSignal subscribeCompleted:^{
NSLog(#"I'm here.");
}];
}];
self.finishRegistrationButton.rac_command = self.viewModel.executeRegistration;
With my "logAll" atribute I can see everything executing, the problem is it never goes into subsrcibeCompleted after everything is ok. I want to show error message if there is an error or perform segue if everything is ok. What am I doing wrong? Can you please explain how to to that properly? I'm stuck here for quite some time now.
I did it. RACCommand 'does not have subscribeError'. Signals sent does not include error events. There is special property 'errors'. In that property, every signal that sends error sends is as 'next'. so, solution is to use this:
[self.executeRegistration.executionSignals subscribeNext:^(RACSignal *signal){
[signal subscribeCompleted:^{
NSLog(#"Registered");
}];
}];
[self.executeRegistration.errors subscribeNext:^(id x) {
NSLog(#"Error");
}];
This is ok. There is no need to subscribeNext if you don't want every new value. This is actually really cool stuff, but I've read that it is too confusing why it is not sending errors as in classic implementation (it was for me :)). That will be included in 3.0 if I'm not mistaken.
self.finishRegistrationButton.rac_command.executionSignals returns a signal of signals, so you'll want to make use of switchToLatest, like so:
self.finishRegistrationButton.rac_command.executionSignals.switchToLatest
This function intercepts signals and switches to the latest received, sending next, errors and completes from that signal instead of the signal operated on. It's very handy for operations that send signals over time.
This code will only work for executionSignals that hold a single signal, if you want multiple targets then you'll need something a bit more complex. If you ever need to change your code to work that way, you might want to have a look at flattenMap:.

How to prevent a signal to send a next event before subsequent asynchronous calls are completed?

I am willing to prevent a RAC signal to send a next event message before some subsequents calls are completed.
Here is an example of how i have proceed so far:
- (RACSignal *) fetchNearbyDatasForLocation: (CLLocationCoordinate2D)coordinate {
RACSignal* finalSignal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
[subscriber sendNext:self.rawDatas];
[subscriber sendCompleted];
return nil;
}];
return [[[self.apiClient fetchNearbyDatasforLocation:coordinate]
flattenMap:^RACStream *(NSArray* datas) {
NSMutableArray* signals = [[NSMutableArray alloc] init];
self.rawDatas = datas;
for (SNPDataModel* data in datas) {
RACSignal* fetchExtraDataSignal = [self.apiClient fetchExtraDataInfoForData:data];
[signals addObject:fetchExtraDataSignal];
RAC(data, extraData) = fetchExtraDataSignal;
}
// will send a next message when all asynchronous call are completed
RACSignal* completedSignal = [RACSignal combineLatest:signals];
return completedSignal;
}]
flattenMap:^RACStream *(RACTuple* value) {
return finalSignal;
}];
}
Few explanation about my code here: I am trying to fetch an array of datas and send a signal with the same array when all fetchExtraDataSignal signals have completed (so the array should now have the extra data content at that time). Is there a better reactive way to achieve this? I would like to not rely on the property datas if that's possible.
Although I don't have self-confidence in understanding what you are meaning,
I recommend using "map", instead of the combination of "createSignal" and "flattenMap".
Like this.
- (RACSignal *) fetchNearbyDatasForLocation: (CLLocationCoordinate2D)coordinate {
return [[self.apiClient fetchNearbyDatasforLocation:coordinate]
flattenMap:^RACStream *(NSArray* datas)
{
/* ... */;
return [[RACSignal combineLatest:signals]
map:^(RACTuple *unused) { return datas; }]];
}];
}
And I strongly recommend don't use any mutable fields to communicate data among event handlers. That causes results of these become unpredictable when multiple signals are simultaneously working.

Making signal retry only to new subscribers

I am using the typical pattern of a cached-replay signal:
- (RACSignal *)resultSignal {
if (!_resultSignal) {
_resultSignal = [self createResultSignal];
}
return _resultSignal;
}
- (RACSignal *)createResultSignal {
return [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[someObject doWorkWithCompletionBlock:^(id result) {
[subscriber sendNext:result];
[subscriber sendCompleted];
}
failure:^(NSError *error) {
[subscriber sendError:error];
}];
return nil;
}] replay];
}
This works great, but now I want to add the ability to retry if the work failed. I can use -[RACSignal retry] or -[RACSignal retry:], but this would not notify the subscribers of an error immediately. Instead, what I want is for existing subscribers to this signal to get the error, but subsequent calls to -resultSignal to receive a new signal, or retry the existing one.
I could -catch: this signal and set _resultSignal to nil in the block, but then I'd have to worry about race conditions, and I don't think that'd be the "reactive" way to do this.
What would be the appropriate way of implementing this behavior?
Update:
Thanks to #joshaber! I ended up going with my suggested approach, and I think it's not too bad:
- (RACSignal *)resultSignal {
#synchronized(_resultSignal) {
if (!_resultSignal) {
#weakify(self);
_resultSignal = [[self createResultSignal] catch:^RACSignal *(NSError *error) {
#strongify(self);
self.resultSignal = nil;
return [RACSignal error:error];
}];
}
return _resultSignal;
}
}
- (void)setResultSignal:(RACSignal *)signal {
#synchronized(_resultSignal) {
_resultSignal = signal;
}
}
I've been thinking about this for a while. Does it help if I wish I had a great answer? ;)
I could -catch: this signal and set _resultSignal to nil in the block, but then I'd have to worry about race conditions, and I don't think that'd be the "reactive" way to do this.
This is what I'd lean towards.
Alternatively, you could use a signal of signals. It'd send whatever the latest signal is. Existing subscribers would be subscribed to the old signal and new subscribers would get the retried signal. But that'd probably require using a RACSubject manually, so I don't know that the additional complexity is worth it, compared to the other solutions.

Retrying an asynchronous operation using ReactiveCocoa

I'm using ReactiveCocoa signals to represent calls to RESTful backend in our system. Each RESTful invocation should receive a token as one of the parameters. The token itself is received from authentication API call.
All works fine and we're now introduced token expiration, so the backend access class may need to reauthorize itself if the API call fails with HTTP code 403. I want to make this operation completely transparent for the callers, this is the best I came up with:
- (RACSignal *)apiCallWithSession:(Session *)session base:(NSString *)base params:(NSDictionary *)params get:(BOOL)get {
NSMutableDictionary* p = [params mutableCopy];
p[#"token"] = session.token;
RACSubject *subject = [RACReplaySubject subject];
RACSignal *first = [self apiCall:base params:p get:get]; // this returns the signal representing the asynchronous HTTP operation
#weakify(self);
[first subscribeNext:^(id x) {
[subject sendNext:x]; // if it works, all is fine
} error:^(NSError *error) {
#strongify(self);
// if it doesn't work, try re-requesting a token
RACSignal *f = [[self action:#"logon" email:session.user.email password:session.user.password]
flattenMap:^RACStream *(NSDictionary *json) { // and map it to the other instance of the original signal to proceed with new token
NSString *token = json[#"token"];
p[#"token"] = token;
session.token = token;
return [self apiCall:base params:p get:get];
}];
// all signal updates are forwarded, we're only re-requesting token once
[f subscribeNext:^(id x) {
[subject sendNext:x];
} error:^(NSError *error) {
[subject sendError:error];
} completed:^{
[subject sendCompleted];
}];
} completed:^{
[subject sendCompleted];
}];
return subject;
}
Is this the right way to do it?
First of all, subscriptions and subjects should generally be avoided as much as possible. Nested subscriptions, in particular, are quite an anti-pattern—usually there are signal operators that can replace them.
In this case, we need to take advantage of the fact that signals can represent deferred work, and create only one signal to perform the actual request:
// This was originally the `first` signal.
RACSignal *apiCall = [RACSignal defer:^{
return [self apiCall:base params:p get:get];
}];
The use of +defer: here ensures that no work will begin until subscription. An important corollary is that the work can be repeated by subscribing multiple times.
For example, if we catch an error, we can try fetching a token, then return the same deferred signal to indicate that it should be attempted again:
return [[apiCall
catch:^(NSError *error) {
// If an error occurs, try requesting a token.
return [[self
action:#"logon" email:session.user.email password:session.user.password]
flattenMap:^(NSDictionary *json) {
NSString *token = json[#"token"];
p[#"token"] = token;
session.token = token;
// Now that we have a token, try the original API call again.
return apiCall;
}];
}]
replay];
The use of -replay replaces the RACReplaySubject that was there before, and makes the request start immediately; however, it could also be -replayLazily or even eliminated completely (to redo the call once per subscription).
That's it! It's important to point out that no explicit subscription was needed just to set up the work that will be performed. Subscription should generally only occur at the "leaves" of the program—where the caller actually requests that work be performed.

Resources