-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This is an `Operation` subclass that wraps a `Promise`, including deferred execution of the handler that resolves the promise. Fixes #58.
- Loading branch information
Showing
17 changed files
with
1,289 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
// | ||
// TWLAsyncOperation.h | ||
// Tomorrowland | ||
// | ||
// Created by Lily Ballard on 8/18/20. | ||
// Copyright © 2020 Lily Ballard. All rights reserved. | ||
// | ||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or | ||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license | ||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your | ||
// option. This file may not be copied, modified, or distributed | ||
// except according to those terms. | ||
// | ||
|
||
#import <Foundation/Foundation.h> | ||
|
||
NS_ASSUME_NONNULL_BEGIN | ||
|
||
@interface TWLAsyncOperation : NSOperation | ||
@end | ||
|
||
NS_ASSUME_NONNULL_END |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
// | ||
// TWLPromiseOperation.h | ||
// Tomorrowland | ||
// | ||
// Created by Lily Ballard on 8/19/20. | ||
// Copyright © 2020 Lily Ballard. All rights reserved. | ||
// | ||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or | ||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license | ||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your | ||
// option. This file may not be copied, modified, or distributed | ||
// except according to those terms. | ||
// | ||
|
||
#import <Foundation/Foundation.h> | ||
#import <Tomorrowland/TWLAsyncOperation.h> | ||
|
||
@class TWLContext; | ||
@class TWLPromise<ValueType,ErrorType>; | ||
@class TWLResolver<ValueType,ErrorType>; | ||
|
||
NS_ASSUME_NONNULL_BEGIN | ||
|
||
/// An \c NSOperation subclass that wraps a promise. | ||
/// | ||
/// \c TWLPromiseOperation is an \c NSOperation subclass that wraps a promise. It doesn't invoke its | ||
/// callback until the operation has been started, and the operation is marked as finished when the | ||
/// promise is resolved. | ||
/// | ||
/// The associated promise can be retrieved at any time with the \c .promise property, even before | ||
/// the operation has started. Requesting cancellation of the promise will cancel the operation, but | ||
/// if the operation has already started it's up to the provided handler to handle the cancellation | ||
/// request. | ||
/// | ||
/// \note Cancelling the operation or the associated promise before the operation has started will | ||
/// always cancel the promise without executing the provided handler, regardless of whether the | ||
/// handler itself supports cancellation. | ||
NS_SWIFT_NAME(ObjCPromiseOperation) | ||
@interface TWLPromiseOperation<__covariant ValueType, __covariant ErrorType> : TWLAsyncOperation | ||
|
||
@property (atomic, readonly) TWLPromise<ValueType,ErrorType> *promise; | ||
|
||
+ (instancetype)newOnContext:(TWLContext *)context handler:(void (^)(TWLResolver<ValueType,ErrorType> *resolver))handler NS_SWIFT_UNAVAILABLE("use init(on:_:)"); | ||
- (instancetype)initOnContext:(TWLContext *)context handler:(void (^)(TWLResolver<ValueType,ErrorType> *resolver))handler NS_SWIFT_NAME(init(on:_:)) NS_DESIGNATED_INITIALIZER; | ||
|
||
+ (instancetype)new NS_UNAVAILABLE; | ||
- (instancetype)init NS_UNAVAILABLE; | ||
- (void)main NS_UNAVAILABLE; | ||
|
||
@end | ||
|
||
NS_ASSUME_NONNULL_END |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
// | ||
// TWLPromiseOperation.m | ||
// Tomorrowland | ||
// | ||
// Created by Lily Ballard on 8/19/20. | ||
// Copyright © 2020 Lily Ballard. All rights reserved. | ||
// | ||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or | ||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license | ||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your | ||
// option. This file may not be copied, modified, or distributed | ||
// except according to those terms. | ||
// | ||
|
||
#import "TWLPromiseOperation.h" | ||
#import "TWLAsyncOperation+Private.h" | ||
#import "TWLPromisePrivate.h" | ||
#import "TWLPromiseBox.h" | ||
#import "TWLContextPrivate.h" | ||
|
||
@implementation TWLPromiseOperation { | ||
TWLContext * _Nullable _context; | ||
void (^ _Nullable _callback)(TWLResolver * _Nonnull); | ||
|
||
/// The box for our internal promise. | ||
TWLObjCPromiseBox * _Nonnull _box; | ||
|
||
/// The actual promise we return to our callers. | ||
/// | ||
/// This is a child of our internal promise. This way we can observe cancellation requests while | ||
/// our box is still in the delayed state, and when we go out of scope the promise will get | ||
/// cancelled if the callback was never invoked. | ||
TWLPromise * _Nonnull _promise; | ||
} | ||
|
||
+ (instancetype)newOnContext:(TWLContext *)context handler:(void (^)(TWLResolver<id,id> * _Nonnull))handler { | ||
return [[self alloc] initOnContext:context handler:handler]; | ||
} | ||
|
||
- (instancetype)initOnContext:(TWLContext *)context handler:(void (^)(TWLResolver<id,id> * _Nonnull))handler { | ||
if ((self = [super init])) { | ||
_context = context; | ||
TWLResolver *childResolver; | ||
TWLPromise *childPromise = [[TWLPromise alloc] initWithResolver:&childResolver]; | ||
_promise = childPromise; | ||
TWLPromise *promise = [[TWLPromise alloc] initDelayed]; | ||
_box = promise->_box; | ||
_callback = ^(TWLResolver * _Nonnull resolver) { | ||
// We piped data from the inner promise to the outer promise at the end of -init | ||
// already, but we need to propagate cancellation the other way. We're deferring that | ||
// until now because cancelling a box in the delayed state is ignored. By waiting until | ||
// now, we ensure that the box is in the empty state instead and therefore will accept | ||
// cancellation. We're still running the handler, but this way the handler can check for | ||
// cancellation requests. | ||
__weak TWLObjCPromiseBox *box = promise->_box; | ||
[childResolver whenCancelRequestedOnContext:TWLContext.immediate handler:^(TWLResolver * _Nonnull resolver) { | ||
[box propagateCancel]; | ||
}]; | ||
// Seal the inner promise box now. This way cancellation will propagate if appropriate. | ||
// Note: We can't just nil out the promise unless we want to add autorelease pools. | ||
[promise->_box seal]; | ||
// Now we can invoke the original handler. | ||
handler(resolver); | ||
}; | ||
|
||
// Observe the promise now in order to set our operation state | ||
__weak typeof(self) weakSelf = self; | ||
[promise tapOnContext:TWLContext.immediate handler:^(id _Nullable value, id _Nullable error) { | ||
// Regardless of the result, mark ourselves as finished. | ||
// We can only get resolved if we've been started. | ||
weakSelf.state = TWLAsyncOperationStateFinished; | ||
}]; | ||
// If someone requests cancellation of the promise, treat that as asking the operation | ||
// itself to cancel. | ||
[childResolver whenCancelRequestedOnContext:TWLContext.immediate handler:^(TWLResolver * _Nonnull resolver) { | ||
typeof(self) this = weakSelf; | ||
if (this | ||
// -cancel invokes this callback; let's not invoke -cancel again. | ||
// It should be safe to do so, but it will fire duplicate KVO notices. | ||
&& !this.cancelled) | ||
{ | ||
[this cancel]; | ||
} | ||
}]; | ||
// Pipe data from the delayed box to our child promise now. This way if we never actually | ||
// execute the callback, we'll get informed of cancellation. | ||
[promise enqueueCallbackWithBox:childPromise->_box willPropagateCancel:YES]; // the propagateCancel happens in the callback | ||
} | ||
return self; | ||
} | ||
|
||
- (void)dealloc { | ||
// If we're thrown away without executing, we need to clean up. | ||
// Since the box is in the delayed state, it won't just cancel automatically. | ||
[self emptyAndCancel]; | ||
} | ||
|
||
// We could probably synthesize this, but it's a const ivar past initialization, so we don't need | ||
// the synthesized lock. | ||
- (TWLPromise *)promise { | ||
return _promise; | ||
} | ||
|
||
- (void)cancel { | ||
// Call super first so .cancelled is true. | ||
[super cancel]; | ||
// Now request cancellation of the promise. | ||
[_promise requestCancel]; | ||
// This does mean a KVO observer of the "isCancelled" key can act on the change prior to our | ||
// promise being requested to cancel, but that should be meaningless; this is only even | ||
// externally observable if the KVO observer has access to the promise's resolver. | ||
} | ||
|
||
- (void)main { | ||
// Check if our promise has requested to cancel. | ||
// We're doing this over just testing self.cancelled to handle the super edge case where one | ||
// thread requests the promise to cancel at the same time as another thread starts the | ||
// operation. Requesting our promise to cancel places it in the cancelled state prior to setting | ||
// self.cancelled, which leaves a race where the promise is cancelled but the operation is not. | ||
// If we were checking self.cancelled we could get into a situation where the handler executes | ||
// and cannot tell that it was asked to cancel. | ||
// The opposite is safe, if we cancel the operation and the operation starts before the promise | ||
// is marked as cancelled, the cancellation will eventually be exposed to the handler, so it can | ||
// take action accordingly. | ||
if (_promise->_box.unfencedState == TWLPromiseBoxStateCancelling) { | ||
[self emptyAndCancel]; | ||
} else { | ||
[self execute]; | ||
} | ||
} | ||
|
||
- (void)execute { | ||
if ([_box transitionStateTo:TWLPromiseBoxStateEmpty]) { | ||
TWLResolver *resolver = [[TWLResolver alloc] initWithBox:_box]; | ||
void (^callback)(TWLResolver *) = _callback; | ||
TWLContext *context = _context; | ||
_context = nil; | ||
_callback = nil; | ||
[context executeIsSynchronous:NO block:^{ | ||
callback(resolver); | ||
}]; | ||
} | ||
} | ||
|
||
- (void)emptyAndCancel { | ||
if ([_box transitionStateTo:TWLPromiseBoxStateEmpty]) { | ||
_context = nil; | ||
_callback = nil; | ||
[_box resolveOrCancelWithValue:nil error:nil]; | ||
} | ||
} | ||
|
||
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
// | ||
// TWLAsyncOperation+Private.h | ||
// Tomorrowland | ||
// | ||
// Created by Lily Ballard on 8/18/20. | ||
// Copyright © 2020 Lily Ballard. All rights reserved. | ||
// | ||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or | ||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license | ||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your | ||
// option. This file may not be copied, modified, or distributed | ||
// except according to those terms. | ||
// | ||
|
||
#import <Foundation/Foundation.h> | ||
#import "TWLAsyncOperation.h" | ||
|
||
NS_ASSUME_NONNULL_BEGIN | ||
|
||
typedef NS_ENUM(NSUInteger, TWLAsyncOperationState) { | ||
TWLAsyncOperationStateInitial = 0, | ||
TWLAsyncOperationStateExecuting, | ||
TWLAsyncOperationStateFinished, | ||
}; | ||
|
||
/// An operation class to subclass for writing asynchronous operations. | ||
/// | ||
/// This operation clss is marked as asynchronous by default and maintains an atomic \c state | ||
/// property that is used to send the appropriate KVO notifications. | ||
/// | ||
/// Subclasses should override \c -main which will be called automatically by \c -start when the | ||
/// operation is ready. When the \c -main method is complete it must set \c state to | ||
/// \c TWLAsyncOperationStateFinished. It must also check for cancellation and handle this | ||
/// appropriately. When the \c -main method is executed the \c state will already be set to | ||
/// \c TWLAsyncOperationStateExecuting. | ||
@interface TWLAsyncOperation () | ||
|
||
/// The state property that controls the \c isExecuting and \c isFinished properties. | ||
/// | ||
/// Setting this automatically sends the KVO notices for those other properties. | ||
/// | ||
/// \note This property uses relaxed memory ordering. If the operation writes state that must be | ||
/// visible to observers from other threads it needs to manage the synchronization itself. | ||
@property (atomic) TWLAsyncOperationState state __attribute__((swift_private)); | ||
|
||
// Do not override this method. | ||
- (void)start; | ||
|
||
// Override this method. When the operation is complete, set \c state to | ||
// \c TWLAsyncOperationStateFinished. Do not call \c super. | ||
- (void)main; | ||
|
||
@end | ||
|
||
NS_ASSUME_NONNULL_END |
Oops, something went wrong.