
GitHub Availability Report: August 2023
In August, we experienced two incidents that resulted in degraded performance across GitHub services.
Native apps spend a lot of time waiting and then reacting. We wait for the user to do something in the UI. Wait for a network call to respond. Wait…
Native apps spend a lot of time waiting and then reacting. We wait for the user to do something in the UI. Wait for a network call to respond. Wait for an asynchronous operation to complete. Wait for some dependent value to change. And then they react.
But all those things—all that waiting and reacting—is usually handled in many disparate ways. That makes it hard for us to reason about them, chain them, or compose them in any uniform, high-level way. We can do better.
That’s why we’ve open-sourced a piece of the magic behind GitHub for Mac: ReactiveCocoa (RAC).
RAC is a framework for composing and transforming sequences of values.
Let’s get more concrete. ReactiveCocoa gives us a lot of cool stuff:
Those all might seem a little random until you realize that RAC is all about handling these cases where we’re waiting for some new value and then reacting.
The real beauty of RAC is that it can adapt to a lot of different, commonly-encountered scenarios.
Enough talk. Let’s see what it actually looks like.
RAC can piggyback on KVO (key-value observing) to give us a sequence of values from a KVO-compliant property. For example, we can watch for changes to our username
property:
[RACAble(self.username) subscribeNext:^(NSString *newName) {
NSLog(@"%@", newName);
}];
That’s cool, but it’s really just a nicer API around KVO. The really cool stuff happens when we compose sequences to express complex behavior.
Let’s suppose we want to check if the user entered a specific username, but only if it’s within the first three values they entered:
[[[[RACAble(self.username)
distinctUntilChanged]
take:3]
filter:^(NSString *newUsername) {
return [newUsername isEqualToString:@"joshaber"];
}]
subscribeNext:^(id _) {
NSLog(@"Hi me!");
}];
We watch username
for changes, filter out non-distinct changes, take only the first three non-distinct values, and then if the new value is “joshaber”, we print out a nice welcome.
Think about what we’d have to do to implement that without RAC. We’d have to:
username
.RAC lets us do the same thing with less state, less boilerplate, better code locality, and better expression of our intent.
We can combine sequences:
[[RACSignal
combineLatest:@[ RACAble(self.password), RACAble(self.passwordConfirmation) ]
reduce:^(NSString *currentPassword, NSString *currentConfirmPassword) {
return [NSNumber numberWithBool:[currentConfirmPassword isEqualToString:currentPassword]];
}]
subscribeNext:^(NSNumber *passwordsMatch) {
self.createEnabled = [passwordsMatch boolValue];
}];
Any time our password
or passwordConfirmation
properties change, we combine the latest values from both and reduce them to a BOOL of whether or not they matched. Then we enable or disable the create button with that result.
We can adapt RAC to give us powerful bindings with conditions and transformations:
[self
rac_bind:@keypath(self.helpLabel.text)
to:[[RACAble(self.help)
filter:^(NSString *newHelp) {
return newHelp != nil;
}]
map:^(NSString *newHelp) {
return [newHelp uppercaseString];
}]];
That binds our help label’s text to our help
property when the help
property isn’t nil and after uppercasing the string (because users love being YELLED AT).
RAC also fits quite nicely with async operations.
For example, we can call a block once multiple concurrent operations have completed:
[[RACSignal
merge:@[ [client fetchUserRepos], [client fetchOrgRepos] ]]
subscribeCompleted:^{
NSLog(@"They're both done!");
}];
Or chain async operations:
[[[[client
loginUser]
flattenMap:^(id _) {
return [client loadCachedMessages];
}]
flattenMap:^(id _) {
return [client fetchMessages];
}]
subscribeCompleted:^{
NSLog(@"Fetched all messages.");
}];
That will login, load the cached messages, then fetch the remote messages, and then print “Fetched all messages.”
Or we can trivially move work to a background queue:
[[[[[client
fetchUserWithUsername:@"joshaber"]
deliverOn:[RACScheduler scheduler]]
map:^(User *user) {
// this is on a background queue
return [[NSImage alloc] initWithContentsOfURL:user.avatarURL];
}]
deliverOn:RACScheduler.mainThreadScheduler]
subscribeNext:^(NSImage *image) {
// now we're back on the main queue
self.imageView.image = image;
}];
Or easily deal with potential race conditions. For example, we could update a property with the result of an asynchronous call, but only if the property doesn’t change before the async call completes:
[[[self
loadDefaultMessageInBackground]
takeUntil:RACAble(self.message)]
toProperty:@keypath(self.message) onObject:self];
RAC is fundamentally pretty simple. It’s all signals all the way down. _(Until you reach turtles.)_
Subscribers subscribe to signals. Signals send their subscribers ‘next’, ‘error’, and ‘completed’ events. So if it’s all just signals sending events, the key question becomes when do those events get sent?
Signals define their own behavior with respect to if and when events are sent. We can create our own signals using +[RACSignal createSignal:]
:
RACSignal *helloWorld = [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
[subscriber sendNext:@"Hello, "];
[subscriber sendNext:@"world!"];
[subscriber sendCompleted];
return nil;
}];
The block we give to +[RACSignal createSignal:]
is called whenever the signal gets a new subscriber. The new subscriber is passed into the block so that we can then send it events. In the above example, we created a signal that sends “Hello, “, and then “world!”, and then completes.
We could then create another signal based off our helloWorld
signal:
RACSignal *joiner = [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
NSMutableArray *strings = [NSMutableArray array];
return [helloWorld subscribeNext:^(NSString *x) {
[strings addObject:x];
} error:^(NSError *error) {
[subscriber sendError:error];
} completed:^{
[subscriber sendNext:[strings componentsJoinedByString:@""]];
[subscriber sendCompleted];
}];
}];
Now we have a joiner
signal. When someone subscribes to joiner
, it subscribes to our helloWorld
signal. It adds all the values it receives from helloWorld
and then when helloWorld
completes, it joins all the strings it received into a single string, sends that, and completes.
In this way, we can build signals on each other to express complex behaviors.
RAC implements a set of operations on RACSignal
that do exactly that. They take the source signal and return a new signal with some defined behavior.
ReactiveCocoa works on both Mac and iOS. See the README for more info and check out the Mac demo project for some practical examples.
For .NET developers, this all might sound eerily familiar. ReactiveCocoa essentially an Objective-C version of .NET’s Reactive Extensions (Rx).
Most of the principles of Rx apply to RAC as well. There are some really good Rx resources out there: