Help defining username available check behavior #1236

Closed
olegam opened this Issue Apr 9, 2014 · 3 comments

Projects

None yet

3 participants

@olegam
olegam commented Apr 9, 2014

I am trying define a behavior in my view model to check for availability of a username. The view model's userTemplate.username property is bound to a textfields rac_textSignal. The checking mechanism can be in one of 5 states and the view will reflect that. The states are:

typedef NS_ENUM(NSUInteger, UsernameAvailabilityCheckStatus) {
    UsernameAvailabilityCheckStatusEmpty = 0,
    UsernameAvailabilityCheckStatusChecking,
    UsernameAvailabilityCheckStatusAvailable,
    UsernameAvailabilityCheckStatusUnavailable,
    UsernameAvailabilityCheckStatusFailed,
};

My current implementation is like this:

- (void)setupUsernameAvailabilityChecking {
    RACSignal *usernameSignal = RACObserve(self.userTemplate, username);
    RAC(self, availabilityStatus) = [usernameSignal flattenMap:^RACStream *(NSString *username) {
        if (username.length == 0) return [RACSignal return:@(UsernameAvailabilityCheckStatusEmpty)];
        RACSignal *checkNameSignal = [[FIBAPIClient sharedInstance] getUsernameAvailabilityFor:username ignoreCache:NO];
        return [RACSignal merge:@[
            [RACSignal return:@(UsernameAvailabilityCheckStatusChecking)],
            [[[[checkNameSignal throttle:kUsernameCheckThrottleInterval] materialize] map:^id(RACEvent *event) {
                if (event.eventType == RACEventTypeNext) {
                    NSDictionary *result = event.value;
                    NSNumber *existsNumber = result[@"exists"];
                    if (!existsNumber) return [RACEvent eventWithValue:@(UsernameAvailabilityCheckStatusFailed)];
                    UsernameAvailabilityCheckStatus status = [existsNumber boolValue] ?
                            UsernameAvailabilityCheckStatusUnavailable : UsernameAvailabilityCheckStatusAvailable;
                    return [RACEvent eventWithValue:@(status)];
                } else if (event.eventType == RACEventTypeError) {
                    return [RACEvent eventWithValue:@(UsernameAvailabilityCheckStatusFailed)];
                }
                return event;
            }] dematerialize],
        ]];
    }];
}

This works, but has one flaw: when the user types faster than the network can check for availability (even with throttling of input) then there is a race condition on what request finishes first. Preferable I would cancel older requests when making a new one. Any ideas on how I could do that?

Suggestions on how to improve the code in general is also welcome.

@notxcain
notxcain commented Apr 9, 2014

Try this one

- (void)setupUsernameAvailabilityChecking {
    RAC(self, availabilityStatus) = [[[RACObserve(self.userTemplate, username)
                                      throttle:kUsernameCheckThrottleInterval]
                                      map:^(NSString *username) {
                                          if (username.length == 0) return [RACSignal return:@(UsernameAvailabilityCheckStatusEmpty)];
                                          return [[[[[FIBAPIClient sharedInstance]
                                                getUsernameAvailabilityFor:username ignoreCache:NO]
                                              map:^(NSDictionary *result) {
                                                  NSNumber *existsNumber = result[@"exists"];
                                                  if (!existsNumber) return @(UsernameAvailabilityCheckStatusFailed);
                                                  UsernameAvailabilityCheckStatus status = [existsNumber boolValue] ? UsernameAvailabilityCheckStatusUnavailable : UsernameAvailabilityCheckStatusAvailable;
                                                  return @(status);
                                              }]
                                             catch:^(NSError *error) {
                                                  return [RACSignal return:@(UsernameAvailabilityCheckStatusFailed)];
                                              }] startWith:@(UsernameAvailabilityCheckStatusChecking)];
                                      }]
                                      switchToLatest];
}

Sorry for formatting.
In general:

  1. Replaced flattenMap: with map: and switchToLatest. So unfinished user fetch is cancelled.
  2. Replaced materialize/dematerialize with catch:
  3. Replaced merge: with startWith: because this is the exact behavior seems you want.
@olegam
olegam commented Apr 9, 2014

Awesome! Looks like this is exactly what I needed.

@olegam olegam closed this Apr 9, 2014
@notxcain
notxcain commented Apr 9, 2014

👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment