New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Help defining username available check behavior #1236

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

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

This comment has been minimized.

Show comment
Hide comment
@notxcain

notxcain 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.

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

This comment has been minimized.

Show comment
Hide comment
@olegam

olegam Apr 9, 2014

Awesome! Looks like this is exactly what I needed.

olegam commented Apr 9, 2014

Awesome! Looks like this is exactly what I needed.

@olegam olegam closed this Apr 9, 2014

@notxcain

This comment has been minimized.

Show comment
Hide comment
@notxcain

notxcain commented Apr 9, 2014

👍

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