FBControlCore/Async/FBFuture.m (922 lines of code) (raw):
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "FBFuture.h"
#import "FBCollectionOperations.h"
#import "FBControlCore.h"
@class FBFutureContext_Teardown;
// A class that encapsulates a mutable array of FBFutureContext_Teardown for
// a threadsafety.
@interface FBFutureTeardowns : NSObject
- (void)addObject:(FBFutureContext_Teardown *)object;
- (FBFutureContext_Teardown *)pop;
- (void)addObjectsFromArray:(NSArray<FBFutureContext_Teardown *> *)array;
- (NSArray<FBFutureContext_Teardown *> *)asArray;
@end
@implementation FBFutureTeardowns {
NSMutableArray<FBFutureContext_Teardown *> *_data;
dispatch_queue_t _queue;
}
- (instancetype)init {
return [self initWithArray:@[]];
}
- (instancetype)initWithArray:(NSArray<FBFutureContext_Teardown *> *)teardowns {
self = [super init];
if (self) {
_data = [teardowns mutableCopy];
_queue = dispatch_queue_create("com.facebook.fbcontrolcore.FBFutureTeardowns", DISPATCH_QUEUE_SERIAL);
}
return self;
}
- (void)addObject:(FBFutureContext_Teardown *)object {
dispatch_sync(_queue, ^{
[self->_data addObject:object];
});
}
- (FBFutureContext_Teardown *)pop {
__block FBFutureContext_Teardown *result;
dispatch_sync(_queue, ^{
result = [self->_data lastObject];
[self->_data removeLastObject];
});
return result;
}
- (void)addObjectsFromArray:(NSArray<FBFutureContext_Teardown *> *)array {
dispatch_sync(_queue, ^{
[self->_data addObjectsFromArray:array];
});
}
- (NSArray<FBFutureContext_Teardown *> *)asArray {
__block NSArray<FBFutureContext_Teardown *> *result;
dispatch_sync(_queue, ^{
result = [self->_data copy];
});
return result;
}
@end
/**
A String Mirror of the State.
*/
typedef NSString *FBFutureStateString NS_STRING_ENUM;
FBFutureStateString const FBFutureStateStringRunning = @"running";
FBFutureStateString const FBFutureStateStringDone = @"done";
FBFutureStateString const FBFutureStateStringFailed = @"error";
FBFutureStateString const FBFutureStateStringCancelled = @"cancelled";
static FBFutureStateString FBFutureStateStringFromState(FBFutureState state)
{
switch (state) {
case FBFutureStateRunning:
return FBFutureStateStringRunning;
case FBFutureStateDone:
return FBFutureStateStringDone;
case FBFutureStateFailed:
return FBFutureStateStringFailed;
case FBFutureStateCancelled:
return FBFutureStateStringCancelled;
default:
return @"";
}
}
dispatch_time_t FBCreateDispatchTimeFromDuration(NSTimeInterval inDuration)
{
return dispatch_time(DISPATCH_TIME_NOW, (int64_t)(inDuration * NSEC_PER_SEC));
}
static void final_resolveUntil(FBMutableFuture *final, dispatch_queue_t queue, FBFuture *(^resolveUntil)(void)) {
if (final.hasCompleted) {
return;
}
FBFuture<id> *future = resolveUntil();
[future onQueue:queue notifyOfCompletion:^(FBFuture<id> *resolved) {
switch (resolved.state) {
case FBFutureStateCancelled:
[final cancel];
return;
case FBFutureStateDone:
[final resolveWithResult:resolved.result];
return;
case FBFutureStateFailed:
final_resolveUntil(final, queue, resolveUntil);
return;
default:
return;
}
}];
}
@interface FBFuture_Handler : NSObject
@property (nonatomic, strong, readonly) dispatch_queue_t queue;
@property (nonatomic, strong, readonly) void (^handler)(FBFuture *);
@end
@implementation FBFuture_Handler
- (instancetype)initWithQueue:(dispatch_queue_t)queue handler:(void (^)(FBFuture *))handler
{
self = [super init];
if (!self) {
return nil;
}
_queue = queue;
_handler = handler;
return self;
}
@end
@interface FBFuture_Cancellation : NSObject
@property (nonatomic, strong, readonly) dispatch_queue_t queue;
@property (nonatomic, strong, readonly) FBFuture<NSNull *> *(^handler)(void);
@end
@implementation FBFuture_Cancellation
- (instancetype)initWithQueue:(dispatch_queue_t)queue handler:(FBFuture<NSNull *> *(^)(void))handler
{
self = [super init];
if (!self) {
return nil;
}
_queue = queue;
_handler = handler;
return self;
}
@end
@interface FBFutureContext_Teardown : NSObject
@property (nonatomic, strong, readonly) FBFuture *future;
@property (nonatomic, strong, readonly) dispatch_queue_t queue;
@property (nonatomic, strong, readonly) FBFuture<NSNull *> * (^action)(id, FBFutureState);
@end
@implementation FBFutureContext_Teardown
- (instancetype)initWithFuture:(FBFuture *)future queue:(dispatch_queue_t)queue action:(FBFuture<NSNull *> * (^)(id, FBFutureState))action
{
self = [super init];
if (!self) {
return nil;
}
_future = future;
_queue = queue;
_action = action;
return self;
}
- (FBFuture<NSNull *> *)performTeardown:(FBFutureState)endState
{
NSAssert(self.future.state != FBFutureStateRunning, @"Performing teardown on an unresolved future is not-permitted.");
FBFuture<NSNull *> * (^action)(id, FBFutureState) = self.action;
FBMutableFuture<NSNull *> *teardownCompleted = FBMutableFuture.future;
// By this point the future will actually be resolved.
// The reason for this notifyOfCompletion, is that we can use it for the queue-bounce to the queue that the action is expected to be called on.
[self.future onQueue:self.queue notifyOfCompletion:^(FBFuture *resolved) {
if (resolved.result) {
FBFuture<NSNull *> *resolvedTeardownCompleted = action(resolved.result, endState);
[teardownCompleted resolveFromFuture:resolvedTeardownCompleted];
} else {
[teardownCompleted resolveWithResult:NSNull.null];
}
}];
return teardownCompleted;
}
@end
@interface FBFutureContext ()
@property (nonatomic, readonly) FBFutureTeardowns *teardowns;
@end
@implementation FBFutureContext
#pragma mark Initializers
+ (FBFutureContext *)futureContextWithFuture:(FBFuture *)future;
{
return [[self alloc] initWithFuture:future teardowns:[FBFutureTeardowns new]];
}
+ (FBFutureContext *)futureContextWithResult:(id)result
{
return [self futureContextWithFuture:[FBFuture futureWithResult:result]];
}
+ (FBFutureContext *)futureContextWithError:(NSError *)error
{
return [self futureContextWithFuture:[FBFuture futureWithError:error]];
}
+ (FBFutureContext<NSArray<id> *> *)futureContextWithFutureContexts:(NSArray<FBFutureContext *> *)contexts
{
NSMutableArray<FBFuture *> *futures = NSMutableArray.array;
FBFutureTeardowns *teardowns = [[FBFutureTeardowns alloc] init];
for (FBFutureContext *context in contexts) {
[futures addObject:context.future];
[teardowns addObjectsFromArray:[context.teardowns asArray]];
}
FBFuture<NSArray<id> *> *future = [FBFuture futureWithFutures:futures];
return [[FBFutureContext alloc] initWithFuture:future teardowns:teardowns];
}
- (instancetype)initWithFuture:(FBFuture *)future teardowns:(FBFutureTeardowns *)teardowns
{
self = [super init];
if (!self) {
return nil;
}
_future = future;
_teardowns = teardowns;
return self;
}
#pragma mark Public
- (FBFuture *)onQueue:(dispatch_queue_t)queue pop:(FBFuture * (^)(id))pop
{
return [[self.future
onQueue:queue fmap:pop]
onQueue:queue notifyOfCompletion:^(FBFuture *resolved) {
NSArray<FBFutureContext_Teardown *> *teardowns = [self.teardowns asArray];
[FBFutureContext popTeardowns:teardowns.reverseObjectEnumerator state:resolved.state];
}];
}
- (FBFutureContext *)onQueue:(dispatch_queue_t)queue pend:(FBFuture * (^)(id result))fmap
{
FBFuture *next = [self.future onQueue:queue fmap:fmap];
return [[FBFutureContext alloc] initWithFuture:next teardowns:self.teardowns];
}
- (FBFutureContext *)onQueue:(dispatch_queue_t)queue push:(FBFutureContext * (^)(id))fmap
{
dispatch_queue_t nextContextQueue = dispatch_queue_create("com.facebook.fbcontrolcore.next_context", DISPATCH_QUEUE_SERIAL);
__block FBFutureContext *nextContext = nil;
FBFuture *future = [self.future onQueue:queue fmap:^(id result) {
FBFutureContext *resolved = fmap(result);
dispatch_sync(nextContextQueue, ^{
[nextContext.teardowns addObjectsFromArray:[resolved.teardowns asArray]];
});
return resolved.future;
}];
dispatch_sync(nextContextQueue, ^{
nextContext = [[FBFutureContext alloc] initWithFuture:future teardowns:self.teardowns];
});
return nextContext;
}
- (FBFutureContext *)onQueue:(dispatch_queue_t)queue replace:(FBFutureContext * (^)(id))replace
{
dispatch_queue_t nextContextQueue = dispatch_queue_create("com.facebook.fbcontrolcore.next_context", DISPATCH_QUEUE_SERIAL);
FBFutureContext_Teardown *top = [self.teardowns pop];
__block FBFutureContext *nextContext = nil;
FBFuture *future = [[self.future
onQueue:queue fmap:^(id result) {
FBFutureContext *resolved = replace(result);
dispatch_sync(nextContextQueue, ^{
[nextContext.teardowns addObjectsFromArray:[resolved.teardowns asArray]];
});
return resolved.future;
}]
onQueue:queue chain:^(FBFuture *resolved) {
return [[top performTeardown:resolved.state] chainReplace:resolved];
}];
dispatch_sync(nextContextQueue, ^{
nextContext = [[FBFutureContext alloc] initWithFuture:future teardowns:self.teardowns];
});
return nextContext;
}
- (FBFutureContext *)onQueue:(dispatch_queue_t)queue handleError:(nonnull FBFuture * _Nonnull (^)(NSError * _Nonnull))handler
{
FBFuture *next = [self.future onQueue:queue handleError:handler];
return [[FBFutureContext alloc] initWithFuture:next teardowns:self.teardowns];
}
- (FBFutureContext *)onQueue:(dispatch_queue_t)queue contextualTeardown:( FBFuture<NSNull *> * (^)(id, FBFutureState) )action
{
FBFutureContext_Teardown *teardown = [[FBFutureContext_Teardown alloc] initWithFuture:self.future queue:queue action:action];
[self.teardowns addObject:teardown];
return self;
}
- (FBFuture *)onQueue:(dispatch_queue_t)queue enter:(id (^)(id result, FBMutableFuture<NSNull *> *teardown))enter
{
FBMutableFuture *started = FBMutableFuture.future;
[[self
onQueue:queue pop:^(id contextValue){
FBMutableFuture<NSNull *> *completed = FBMutableFuture.future;
id mappedValue = enter(contextValue, completed);
[started resolveWithResult:mappedValue];
return completed;
}]
onQueue:queue handleError:^(NSError *error) {
[started resolveWithError:error];
return [FBFuture futureWithError:error];
}];
return started;
}
#pragma mark Private
+ (FBFuture<NSNull *> *)popTeardowns:(NSEnumerator<FBFutureContext_Teardown *> *)teardowns state:(FBFutureState)state
{
FBFutureContext_Teardown *teardown = teardowns.nextObject;
if (!teardown) {
return FBFuture.empty;
}
return [[teardown
performTeardown:state]
onQueue:teardown.queue chain:^(id _) {
return [self popTeardowns:teardowns state:state];
}];
}
@end
@interface FBFuture ()
@property (atomic, copy, nullable, readwrite) NSString *name;
@property (nonatomic, strong, readonly) NSMutableArray<FBFuture_Handler *> *handlers;
@property (nonatomic, strong, nullable, readwrite) NSMutableArray<FBFuture_Cancellation *> *cancelResponders;
@property (nonatomic, strong, nullable, readwrite) FBFuture<NSNull *> *resolvedCancellation;
@end
@implementation FBFuture
@synthesize error = _error, result = _result, state = _state;
#pragma mark Initializers
+ (FBFuture *)futureWithResult:(id)result
{
FBMutableFuture *future = FBMutableFuture.future;
return [future resolveWithResult:result];
}
+ (FBFuture *)futureWithError:(NSError *)error
{
FBMutableFuture *future = FBMutableFuture.future;
return [future resolveWithError:error];
}
+ (FBFuture *)futureWithDelay:(NSTimeInterval)delay future:(FBFuture *)future
{
FBMutableFuture *delayed = FBMutableFuture.future;
dispatch_after(FBCreateDispatchTimeFromDuration(delay), FBFuture.internalQueue, ^{
[delayed resolveFromFuture:future];
});
return [delayed onQueue:FBFuture.internalQueue respondToCancellation:^{
[future cancel];
return FBFuture.empty;
}];
}
+ (instancetype)resolveValue:( id(^)(NSError **) )resolve
{
NSError *error = nil;
id result = resolve(&error);
if (result) {
return [FBFuture futureWithResult:result];
} else {
return [FBFuture futureWithError:error];
}
}
+ (instancetype)onQueue:(dispatch_queue_t)queue resolveValue:(id(^)(NSError **))resolve;
{
FBMutableFuture *future = FBMutableFuture.future;
dispatch_async(queue, ^{
NSError *error = nil;
id result = resolve(&error);
if (!result) {
NSCAssert(error, @"Error must be set on nil return");
[future resolveWithError:error];
} else {
[future resolveWithResult:result];
}
});
return future;
}
+ (instancetype)onQueue:(dispatch_queue_t)queue resolve:( FBFuture *(^)(void) )resolve
{
FBMutableFuture *future = FBMutableFuture.future;
dispatch_async(queue, ^{
FBFuture *resolved = resolve();
[future resolveFromFuture:resolved];
});
return future;
}
+ (FBFuture<NSNull *> *)onQueue:(dispatch_queue_t)queue resolveWhen:(BOOL (^)(void))resolveWhen
{
return [self onQueue:queue resolveOrFailWhen:^FBFutureLoopState(NSError **errorOut) {
if (resolveWhen()){
return FBFutureLoopFinished;
} else {
return FBFutureLoopContinue;
}
}];
}
+ (FBFuture<NSNull *> *)onQueue:(dispatch_queue_t)queue resolveOrFailWhen:(FBFutureLoopState (^)(NSError ** errorOut))resolveOrFailWhen
{
FBMutableFuture *future = FBMutableFuture.future;
dispatch_async(queue, ^{
const NSTimeInterval interval = 0.1;
const dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(timer, FBCreateDispatchTimeFromDuration(interval), (uint64_t)(interval * NSEC_PER_SEC), (uint64_t)(interval * NSEC_PER_SEC / 10));
dispatch_source_set_event_handler(timer, ^{
if (future.state != FBFutureStateRunning) {
dispatch_cancel(timer);
} else {
NSError *error = nil;
switch(resolveOrFailWhen(&error)){
case FBFutureLoopContinue:
//Continue running
break;
case FBFutureLoopFailed:
dispatch_cancel(timer);
NSCAssert(error != nil, @"Expected error to be set when returning FBFutureLoopFailed");
[future resolveWithError:error];
break;
case FBFutureLoopFinished:
dispatch_cancel(timer);
NSCAssert(error == nil, @"Error must be set on nil when return FBFutureLoopFinished");
[future resolveWithResult:NSNull.null];
break;
}
}
});
dispatch_resume(timer);
});
return future;
}
+ (FBFuture<id> *)onQueue:(dispatch_queue_t)queue resolveUntil:(FBFuture<id> *(^)(void))resolveUntil
{
FBMutableFuture *final = FBMutableFuture.future;
dispatch_async(queue, ^{
final_resolveUntil(final, queue, resolveUntil);
});
return final;
}
- (instancetype)timeout:(NSTimeInterval)timeout waitingFor:(NSString *)format, ...
{
NSParameterAssert(timeout > 0);
va_list args;
va_start(args, format);
NSString *description = [[NSString alloc] initWithFormat:format arguments:args];
va_end(args);
FBFuture *timeoutFuture = [[[FBControlCoreError
describeFormat:@"Timed out after %f seconds waiting for %@", timeout, description]
failFuture]
delay:timeout];
return [FBFuture race:@[self, timeoutFuture]];
}
- (FBFuture *)onQueue:(dispatch_queue_t)queue
{
FBMutableFuture *future = FBMutableFuture.future;
dispatch_async(queue, ^{
[future resolveFromFuture:self];
});
return future;
}
- (FBFuture *)onQueue:(dispatch_queue_t)queue timeout:(NSTimeInterval)timeout handler:(FBFuture * (^)(void))handler
{
return [FBFuture
race:@[
self,
[[FBFuture
futureWithDelay:timeout future:FBFuture.empty]
onQueue:queue fmap:^(id _) {
return handler();
}],
]
];
}
+ (FBFuture *)futureWithFutures:(NSArray<FBFuture *> *)futures
{
if (futures.count == 0) {
return [FBFuture futureWithResult:@[]];
}
FBMutableFuture *compositeFuture = FBMutableFuture.future;
NSMutableArray *results = [[FBCollectionOperations arrayWithObject:NSNull.null count:futures.count] mutableCopy];
dispatch_queue_t queue = dispatch_queue_create("com.facebook.fbcontrolcore.future.composite", DISPATCH_QUEUE_SERIAL);
__block NSUInteger remaining = futures.count;
// `futureCompleted` must be called on `queue`.
void (^futureCompleted)(FBFuture *, NSUInteger) = ^(FBFuture *future, NSUInteger index) {
if (compositeFuture.hasCompleted) {
return;
}
FBFutureState state = future.state;
switch (state) {
case FBFutureStateDone:
results[index] = future.result;
remaining--;
if (remaining == 0) {
[compositeFuture resolveWithResult:[results copy]];
}
return;
case FBFutureStateFailed:
[compositeFuture resolveWithError:future.error];
return;
case FBFutureStateCancelled:
[compositeFuture cancel];
return;
default:
NSCAssert(NO, @"Unexpected state in callback %@", FBFutureStateStringFromState(state));
return;
}
};
for (NSUInteger index = 0; index < futures.count; index++) {
FBFuture *future = futures[index];
if (future.hasCompleted) {
// The reason that this is done in-line is to avoid work being
// asynchronous when not necessary. For example a future-of-futures where
// the input futures have resolved already should resolve immediately.
// The dispatch_sync ensures that in this case, the composed future is
// resolved before returning from the constructor.
// It's OK to use dispatch_sync here: queue is local; there is no dispatch
// calls within futureCompleted().
// @lint-ignore FBOBJCDISCOURAGEDFUNCTION
dispatch_sync(queue, ^{
futureCompleted(future, index);
});
} else {
[future onQueue:queue notifyOfCompletion:^(FBFuture *innerFuture){
futureCompleted(innerFuture, index);
}];
}
}
return compositeFuture;
}
+ (FBFuture *)race:(NSArray<FBFuture *> *)futures
{
NSParameterAssert(futures.count > 0);
FBMutableFuture *compositeFuture = FBMutableFuture.future;
dispatch_queue_t queue = dispatch_queue_create("com.facebook.fbcontrolcore.future.race", DISPATCH_QUEUE_SERIAL);
__block NSUInteger remainingCounter = futures.count;
void (^cancelAllFutures)(void) = ^{
for (FBFuture *future in futures) {
[future cancel];
}
};
// `futureCompleted` must be called on `queue`.
void (^futureCompleted)(FBFuture *future) = ^(FBFuture *future){
remainingCounter--;
if (future.result) {
[compositeFuture resolveWithResult:future.result];
cancelAllFutures();
return;
}
if (future.error) {
[compositeFuture resolveWithError:future.error];
cancelAllFutures();
return;
}
if (remainingCounter == 0) {
[compositeFuture cancel];
}
};
for (FBFuture *future in futures) {
if (future.hasCompleted) {
// The reason that this is done in-line is to avoid work being
// asynchronous when not necessary. For example a future-of-futures where
// the input futures have resolved already should resolve immediately.
// The dispatch_sync ensures that in this case, the composed future is
// resolved before returning from the constructor.
// It's OK to use dispatch_sync here: queue is local; there is no dispatch
// calls within futureCompleted()
// @lint-ignore FBOBJCDISCOURAGEDFUNCTION
dispatch_sync(queue, ^{
futureCompleted(future);
});
} else {
[future onQueue:queue notifyOfCompletion:futureCompleted];
}
}
return compositeFuture;
}
+ (FBFuture<NSNull *> *)empty
{
FBMutableFuture *future = [FBMutableFuture futureWithName:@"Empty"];
return [future resolveWithResult:NSNull.null];
}
- (instancetype)init
{
return [self initWithName:nil];
}
- (instancetype)initWithName:(NSString *)name
{
self = [super init];
if (!self) {
return nil;
}
_state = FBFutureStateRunning;
_handlers = [NSMutableArray array];
_cancelResponders = [NSMutableArray array];
_name = name;
return self;
}
#pragma mark NSObject
- (NSString *)description
{
NSString *state = [NSString stringWithFormat:@"Future %@", FBFutureStateStringFromState(self.state)];
NSString *name = self.name;
if (name) {
return [NSString stringWithFormat:@"%@ %@", name, state];
}
return state;
}
#pragma mark Cancellation
- (FBFuture<NSNull *> *)cancel
{
@synchronized (self) {
if (self.resolvedCancellation) {
return self.resolvedCancellation;
}
if (self.state != FBFutureStateRunning) {
return FBFuture.empty;
}
}
NSArray<FBFuture_Cancellation *> *cancelResponders = [self resolveAsCancelled];
@synchronized (self) {
self.resolvedCancellation = [FBFuture resolveCancellationResponders:cancelResponders forOriginalName:self.name];
return self.resolvedCancellation;
}
}
- (instancetype)onQueue:(dispatch_queue_t)queue respondToCancellation:(FBFuture<NSNull *> *(^)(void))handler
{
NSParameterAssert(queue);
NSParameterAssert(handler);
@synchronized(self) {
[self.cancelResponders addObject:[[FBFuture_Cancellation alloc] initWithQueue:queue handler:handler]];
return self;
}
}
#pragma mark Completion Notification
- (instancetype)onQueue:(dispatch_queue_t)queue notifyOfCompletion:(void (^)(FBFuture *))handler
{
NSParameterAssert(queue);
NSParameterAssert(handler);
@synchronized (self) {
if (self.state == FBFutureStateRunning) {
FBFuture_Handler *wrapper = [[FBFuture_Handler alloc] initWithQueue:queue handler:handler];
[self.handlers addObject:wrapper];
} else {
dispatch_async(queue, ^{
handler(self);
});
}
}
return self;
}
- (instancetype)onQueue:(dispatch_queue_t)queue doOnResolved:(void (^)(id))handler
{
return [self onQueue:queue map:^(id result) {
handler(result);
return result;
}];
}
#pragma mark Deriving new Futures
- (FBFuture *)onQueue:(dispatch_queue_t)queue chain:(FBFuture *(^)(FBFuture *))chain
{
FBMutableFuture *chained = FBMutableFuture.future;
[self onQueue:queue notifyOfCompletion:^(FBFuture *future) {
FBFuture *next = chain(future);
NSCAssert([next isKindOfClass:FBFuture.class], @"chained value is not a Future, got %@", next);
[next onQueue:queue notifyOfCompletion:^(FBFuture *final) {
FBFutureState state = final.state;
switch (state) {
case FBFutureStateFailed:
[chained resolveWithError:final.error];
break;
case FBFutureStateDone:
[chained resolveWithResult:final.result];
break;
case FBFutureStateCancelled:
[chained cancel];
break;
default:
NSCAssert(NO, @"Invalid State %lu", (unsigned long)state);
}
}];
}];
// Chaining: 'self' References 'chained'
// Cancellation: 'chained' references 'self'
// Break the cycle, if weakSelf is nullified, this is fine as completion has been processed already.
__weak typeof(self) weakSelf = self;
return [chained onQueue:FBFuture.internalQueue respondToCancellation:^{
[weakSelf cancel];
return FBFuture.empty;
}];
}
- (FBFuture *)onQueue:(dispatch_queue_t)queue fmap:(FBFuture * (^)(id result))fmap
{
FBMutableFuture *chained = FBMutableFuture.future;
[self onQueue:queue notifyOfCompletion:^(FBFuture *future) {
if (future.error) {
[chained resolveWithError:future.error];
return;
}
if (future.state == FBFutureStateCancelled) {
[chained cancel];
return;
}
FBFuture *fmapped = fmap(future.result);
NSCAssert([fmapped isKindOfClass:FBFuture.class], @"fmap'ped value is not a Future, got %@", fmapped);
[fmapped onQueue:queue notifyOfCompletion:^(FBFuture *next) {
if (next.error) {
[chained resolveWithError:next.error];
return;
}
[chained resolveWithResult:next.result];
}];
}];
// Chaining: 'self' References 'chained'
// Cancellation: 'chained' references 'self'
// Break the cycle, if weakSelf is nullified, this is fine as completion has been processed already.
__weak typeof(self) weakSelf = self;
return [chained onQueue:FBFuture.internalQueue respondToCancellation:^{
[weakSelf cancel];
return FBFuture.empty;
}];
}
- (FBFuture *)onQueue:(dispatch_queue_t)queue map:(id (^)(id result))map
{
return [self onQueue:queue fmap:^FBFuture *(id result) {
id next = map(result);
return [FBFuture futureWithResult:next];
}];
}
- (FBFuture *)onQueue:(dispatch_queue_t)queue handleError:(FBFuture * (^)(NSError *))handler
{
return [self onQueue:queue chain:^(FBFuture *future) {
return future.error ? handler(future.error) : future;
}];
}
- (FBFuture *)mapReplace:(id)replacement
{
return [self onQueue:FBFuture.internalQueue map:^(id _) {
return replacement;
}];
}
- (FBFuture *)chainReplace:(FBFuture *)replacement
{
return [self onQueue:FBFuture.internalQueue chain:^FBFuture *(FBFuture *_) {
return replacement;
}];
}
- (FBFuture *)fallback:(id)replacement
{
return [self onQueue:FBFuture.internalQueue handleError:^(NSError *_) {
return [FBFuture futureWithResult:replacement];
}];
}
- (FBFuture *)delay:(NSTimeInterval)delay
{
return [FBFuture futureWithDelay:delay future:self];
}
- (FBFuture *)rephraseFailure:(NSString *)format, ...
{
va_list args;
va_start(args, format);
NSString *string = [[NSString alloc] initWithFormat:format arguments:args];
va_end(args);
return [self onQueue:FBFuture.internalQueue chain:^(FBFuture *future) {
NSError *error = future.error;
if (!error) {
return future;
}
return [[[FBControlCoreError
describe:string]
causedBy:error]
failFuture];
}];
}
#pragma mark Creating Context
- (FBFutureContext *)onQueue:(dispatch_queue_t)queue contextualTeardown:( FBFuture<NSNull *> * (^)(id, FBFutureState))action
{
FBFutureContext_Teardown *teardown = [[FBFutureContext_Teardown alloc] initWithFuture:self queue:queue action:action];
return [[FBFutureContext alloc] initWithFuture:self teardowns:[[FBFutureTeardowns alloc] initWithArray:@[teardown]]];
}
- (FBFutureContext *)onQueue:(dispatch_queue_t)queue pushTeardown:(FBFutureContext *(^)(id))fmap
{
FBFutureTeardowns *teardowns = [FBFutureTeardowns new];
FBFuture *future = [self onQueue:queue fmap:^(id value) {
FBFutureContext *chained = fmap(value);
for (FBFutureContext_Teardown *teardown in [chained.teardowns asArray]) {
[teardowns addObject:[[FBFutureContext_Teardown alloc] initWithFuture:chained.future queue:teardown.queue action:teardown.action]];
}
return chained.future;
}];
return [[FBFutureContext alloc] initWithFuture:future teardowns:teardowns];
}
#pragma mark Metadata
- (FBFuture *)named:(NSString *)name
{
self.name = name;
return self;
}
- (FBFuture *)nameFormat:(NSString *)format, ...
{
va_list args;
va_start(args, format);
NSString *name = [[NSString alloc] initWithFormat:format arguments:args];
va_end(args);
return [self named:name];
}
- (FBFuture *)logCompletion:(id<FBControlCoreLogger>)logger withPurpose:(NSString *)format, ...
{
va_list args;
va_start(args, format);
NSString *string = [[NSString alloc] initWithFormat:format arguments:args];
va_end(args);
return [self onQueue:FBFuture.internalQueue notifyOfCompletion:^(FBFuture *resolved) {
[logger logFormat:@"Completed %@ with state '%@'", string, resolved];
}];
}
#pragma mark - Properties
- (BOOL)hasCompleted
{
FBFutureState state = self.state;
return state != FBFutureStateRunning;
}
- (NSError *)error
{
@synchronized (self) {
return self->_error;
}
}
- (id)result
{
@synchronized (self) {
return self->_result;
}
}
- (FBFutureState)state
{
@synchronized (self) {
return _state;
}
}
- (void)setError:(NSError *)error
{
_error = error;
}
- (void)setState:(FBFutureState)state
{
_state = state;
}
- (void)setResult:(id)result
{
_result = result;
}
#pragma mark FBMutableFuture Implementation
- (instancetype)resolveWithResult:(id)result
{
@synchronized (self) {
if (self.state == FBFutureStateRunning) {
self.result = result;
self.state = FBFutureStateDone;
[self fireAllHandlers];
self.cancelResponders = nil;
}
}
return self;
}
- (instancetype)resolveWithError:(NSError *)error
{
@synchronized (self) {
if (self.state == FBFutureStateRunning) {
self.error = error;
self.state = FBFutureStateFailed;
[self fireAllHandlers];
self.cancelResponders = nil;
}
}
return self;
}
- (instancetype)resolveFromFuture:(FBFuture *)future
{
void (^resolve)(FBFuture *future) = ^(FBFuture *resolvedFuture){
FBFutureState state = resolvedFuture.state;
switch (state) {
case FBFutureStateFailed:
[self resolveWithError:resolvedFuture.error];
return;
case FBFutureStateDone:
[self resolveWithResult:resolvedFuture.result];
return;
case FBFutureStateCancelled:
[self cancel];
return;
default:
NSCAssert(NO, @"Invalid State %lu", (unsigned long)state);
}
};
if (future.hasCompleted) {
resolve(future);
} else {
[future onQueue:FBFuture.internalQueue notifyOfCompletion:resolve];
}
return self;
}
#pragma mark Private
- (NSArray<FBFuture_Cancellation *> *)resolveAsCancelled
{
@synchronized (self) {
if (self.state == FBFutureStateRunning) {
self.state = FBFutureStateCancelled;
[self fireAllHandlers];
}
NSArray<FBFuture_Cancellation *> *cancelResponders = self.cancelResponders;
self.cancelResponders = nil;
return cancelResponders;
}
}
- (void)fireAllHandlers
{
for (FBFuture_Handler *handler in self.handlers) {
dispatch_async(handler.queue, ^{
handler.handler(self);
});
}
[self.handlers removeAllObjects];
}
+ (FBFuture<NSNull *> *)resolveCancellationResponders:(NSArray<FBFuture_Cancellation *> *)cancelResponders forOriginalName:(NSString *)originalName
{
NSString *name = [NSString stringWithFormat:@"Cancellation of %@", originalName];
if (cancelResponders.count == 0) {
return [FBFuture.empty named:name];
} else if (cancelResponders.count == 1) {
FBFuture_Cancellation *cancelResponder = cancelResponders[0];
return [[FBFuture onQueue:cancelResponder.queue resolve:cancelResponder.handler] named:name];
} else {
NSMutableArray<FBFuture<NSNull *> *> *futures = [NSMutableArray array];
for (FBFuture_Cancellation *cancelResponder in cancelResponders) {
[futures addObject:[FBFuture onQueue:cancelResponder.queue resolve:cancelResponder.handler]];
}
return [[[FBFuture futureWithFutures:futures] mapReplace:NSNull.null] named:name];
}
}
+ (dispatch_queue_t)internalQueue
{
return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
}
#pragma mark KVO
+ (NSSet<NSString *> *)keyPathsForValuesAffectingHasCompleted
{
return [NSSet setWithObjects:NSStringFromSelector(@selector(state)), nil];
}
@end
@implementation FBMutableFuture
- (instancetype)resolveWithError:(NSError *)error
{
return [super resolveWithError:error];
}
- (instancetype)resolveWithResult:(id)result
{
return [super resolveWithResult:result];
}
- (instancetype)resolveFromFuture:(FBFuture *)future
{
return [super resolveFromFuture:future];
}
+ (FBMutableFuture *)future
{
return [self futureWithName:nil];
}
+ (FBMutableFuture *)futureWithName:(NSString *)name
{
return [[FBMutableFuture alloc] initWithName:name];
}
+ (FBMutableFuture *)futureWithNameFormat:(NSString *)format, ...
{
va_list args;
va_start(args, format);
NSString *name = [[NSString alloc] initWithFormat:format arguments:args];
va_end(args);
return [self futureWithName:name];
}
@end