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