├── .gitignore ├── CollapsingFutures.xcodeproj └── project.pbxproj ├── License.txt ├── Podfile ├── Podfile.lock ├── README.md ├── TwistedOakCollapsingFutures.podspec ├── src ├── CollapsingFutures.h ├── NSArray+TOCFuture.h ├── NSArray+TOCFuture.m ├── TOCCancelToken+MoreConstructors.h ├── TOCCancelToken+MoreConstructors.m ├── TOCCancelTokenAndSource.h ├── TOCCancelTokenAndSource.m ├── TOCFuture+MoreContinuations.h ├── TOCFuture+MoreContinuations.m ├── TOCFuture+MoreContructors.h ├── TOCFuture+MoreContructors.m ├── TOCFutureAndSource.h ├── TOCFutureAndSource.m ├── TOCTimeout.h ├── TOCTimeout.m ├── TOCTypeDefs.h ├── TwistedOakCollapsingFutures.h └── internal │ ├── TOCInternal.h │ ├── TOCInternal_Array+Functional.h │ ├── TOCInternal_Array+Functional.m │ ├── TOCInternal_BlockObject.h │ ├── TOCInternal_BlockObject.m │ ├── TOCInternal_OnDeallocObject.h │ ├── TOCInternal_OnDeallocObject.m │ ├── TOCInternal_Racer.h │ └── TOCInternal_Racer.m └── test ├── Testing.h ├── Testing.m └── src ├── TOCCancelToken+MoreConstructorsTest.m ├── TOCCancelTokenTest.m ├── TOCFuture+MoreConstructorsTest.m ├── TOCFuture+MoreContinuationsTest.m ├── TOCFutureArrayUtilTest.m ├── TOCFutureSourceTest.m └── TOCFutureTest.m /.gitignore: -------------------------------------------------------------------------------- 1 | # Exclude the build directory 2 | build/* 3 | 4 | # Exclude temp nibs and swap files 5 | *~.nib 6 | *.swp 7 | 8 | # Exclude OS X folder attributes 9 | .DS_Store 10 | 11 | # Exclude user-specific XCode 3 and 4 files 12 | *.xcworkspace 13 | xcuserdata 14 | 15 | #Exclude pods 16 | Pods/* 17 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | Simplified BSD 2 | 3 | Copyright (c) 2013, Craig Gidney 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 7 | 8 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 9 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | link_with ['CollapsingFutures', 'CollapsingFuturesTests'] 2 | 3 | pod 'UnionFind', '~> 1.0' 4 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - UnionFind (1.0.1) 3 | 4 | DEPENDENCIES: 5 | - UnionFind (~> 1.0) 6 | 7 | SPEC CHECKSUMS: 8 | UnionFind: 45777a8b6878d3a602af3654cc3a09b87389d356 9 | 10 | COCOAPODS: 0.33.1 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Collapsing Futures 2 | ================== 3 | 4 | This is a library implementing [futures](https://en.wikipedia.org/wiki/Future_%28programming%29) in Objective-C, featuring: 5 | 6 | - **Types**: `TOCFuture` to represent eventual results, `TOCCancelToken` to propagate cancellation notifications, `TOCFutureSource` to produce and control an eventual result, and `TOCCancelTokenSource` to produce and control a cancel token. 7 | - **Automatic Collapsing**: You never have to worry about forgetting to unwrap or flatten a doubly-eventual future. A `[TOCFuture futureWithResult:[TOCFuture futureWithResult:@1]]` is automatically a `[TOCFuture futureWithResult:@1]`. 8 | - **Sticky Main Thread**: Callbacks registered from the main thread will get back on the main thread before executing. Makes UI work much easier. 9 | - **Cancellable Operations**: All asynchronous operations have variants that can be cancelled by cancelling the `TOCCancelToken` passed to the operation's `until:` or `unless:` parameter. 10 | - **Immortality Detection**: It is impossible to create new space leaks by consuming futures and tokens (but producers still have to be careful). If a reference cycle doesn't involve a token or future's source, it will be broken when the source is deallocated. 11 | - **Documentation**: Useful doc comments on every method and type, that don't just repeat the name, covering corner cases and in some cases basic usage hints. No 'getting started' guides yet, though. 12 | 13 | **Recent Changes** 14 | 15 | - Version 1. 16 | - Deprecated "TwistedOakCollapsingFutures.h" for "CollapsingFutures.h". 17 | - Futures are now equatable (by current state then by will-end-up-in-same-state-with-same-value). 18 | 19 | Installation 20 | ============ 21 | 22 | **Method #1: [CocoaPods](http://cocoapods.org/)** 23 | 24 | 1. In your [Podfile](http://guides.cocoapods.org/using/the-podfile.html), add `pod 'TwistedOakCollapsingFutures', '~> 1.0'` 25 | 2. Run `pod install` from the project directory 26 | 3. `#import "CollapsingFutures.h"` wherever you want to use futures, cancel tokens, or their category methods 27 | 28 | **Method #2: Manual** 29 | 30 | 1. Download one of the [releases](https://github.com/Strilanc/ObjC-CollapsingFutures/releases), or clone the repo 31 | 2. Copy the source files from the src/ folder into your project 32 | 3. Have ARC enabled 33 | 4. `#import "CollapsingFutures.h"` wherever you want to use futures, cancel tokens, or their category methods 34 | 35 | 36 | Usage 37 | ===== 38 | 39 | **External Content** 40 | 41 | - [Usage and benefits of collapsing futures](http://twistedoakstudios.com/blog/Post7149_collapsing-futures-in-objective-c) 42 | - [Usage and benefits of cancellation tokens](http://twistedoakstudios.com/blog/Post7391_cancellation-tokens-and-collapsing-futures-for-objective-c) 43 | - [How immortality detection works](http://twistedoakstudios.com/blog/Post7525_using-immortality-to-kill-accidental-callback-cycles) 44 | - [Explanation and motivation for the 'monadic' design of futures (in C++)](http://bartoszmilewski.com/2014/02/26/c17-i-see-a-monad-in-your-future/) 45 | 46 | **Using a Future** 47 | 48 | The following code is an example of how to make a `TOCFuture` *do* something. Use `thenDo` to make things happen when the future succeeds, and `catchDo` to make things happen when it fails (there's also `finallyDo` for cleanup): 49 | 50 | ```objective-c 51 | #import "CollapsingFutures.h" 52 | 53 | // ask for the address book, which is asynchronous because IOS may ask the user to allow it 54 | TOCFuture *futureAddressBook = SomeUtilityClass.asyncGetAddressBook; 55 | 56 | // if the user refuses access to the address book (or something else goes wrong), log the problem 57 | [futureAddressBook catchDo:^(id error) { 58 | NSLog("There was an error (%@) getting the address book.", error); 59 | }]; 60 | 61 | // if the user allowed access, use the address book 62 | [futureAddressBook thenDo:^(id arcAddressBook) { 63 | ABAddressBookRef addressBook = (__bridge ABAddressBookRef)arcAddressBook; 64 | 65 | ... do stuff with addressBook ... 66 | }]; 67 | ``` 68 | 69 | **Creating a Future** 70 | 71 | How does the `asyncGetAddressBook` method from the above example control the future it returns? 72 | 73 | In the simple case, where the result is already known, you use `TOCFuture futureWithResult:` or `TOCFuture futureWithFailure`. 74 | 75 | When the result is not known right away, the class `TOCFutureSource` is used. It has a `future` property that completes after the source's `trySetResult` or `trySetFailure` methods are called. 76 | 77 | Here's how `asyncGetAddressBook` is implemented: 78 | 79 | ```objective-c 80 | #import "CollapsingFutures.h" 81 | 82 | +(TOCFuture *) asyncGetAddressBook { 83 | CFErrorRef creationError = nil; 84 | ABAddressBookRef addressBookRef = ABAddressBookCreateWithOptions(NULL, &creationError); 85 | 86 | // did we fail right away? Then return an already-failed future 87 | if (creationError != nil) { 88 | return [TOCFuture futureWithFailure:(__bridge_transfer id)creationError]; 89 | } 90 | 91 | // we need to make an asynchronous call, so we'll use a future source 92 | // that way we can return its future right away and fill it in later 93 | TOCFutureSource *resultSource = [FutureSource new]; 94 | 95 | id arcAddressBook = (__bridge_transfer id)addressBookRef; // retain the address book in ARC land 96 | ABAddressBookRequestAccessWithCompletion(addressBookRef, ^(bool granted, CFErrorRef requestAccessError) { 97 | // time to fill in the future we returned 98 | if (granted) { 99 | [resultSource trySetResult:arcAddressBook]; 100 | } else { 101 | [resultSource trySetFailure:(__bridge id)requestAccessError]; 102 | } 103 | }); 104 | 105 | return resultSource.future; 106 | } 107 | ``` 108 | 109 | **Chaining Futures** 110 | 111 | Just creating and using futures is useful, but not what makes them powerful. The true power is in transformative methods like `then:` and `toc_thenAll` that both consume and produce futures. They make wiring up complicated asynchronous sequences look easy: 112 | 113 | ```objective-c 114 | #import "CollapsingFutures.h" 115 | 116 | +(TOCFuture *) sumOfFutures:(NSArray*)arrayOfFuturesOfNumbers { 117 | // we want all of the values to be ready before we bother summing 118 | TOCFuture* futureOfArrayOfNumbers = arrayOfFuturesOfNumbers.toc_thenAll; 119 | 120 | // once the array of values is ready, add up its entries to get the sum 121 | TOCFuture* futureSum = [futureOfArrayOfNumbers then:^(NSArray* numbers) { 122 | double total = 0; 123 | for (NSNumber* number in numbers) { 124 | total += number.doubleValue; 125 | } 126 | return @(total); 127 | }]; 128 | 129 | // this future will eventually contain the sum of the eventual numbers in the input array 130 | // if any of the evetnual numbers fails, this future will end up failing as well 131 | return futureSum; 132 | } 133 | ``` 134 | 135 | The ability to setup transformations to occur once futures are ready allows you to write truly asynchronous code, that doesn't block precious threads, with very little boilerplate and intuitive propagation of failures. 136 | 137 | Design Philosophy 138 | ================= 139 | 140 | - **No Blocking**: There are no blocking operations included in the library, because blocking on a future is a great way to introduce deadlocks and UI hangs. Use `then`/`finally`/`catch`/`whenCancelledDo` instead. 141 | 142 | - **No @catch-ing Errors**: Raised errors are not caught, even inside handlers given to methods like `then`. In Objective-C, raising an error is generally considered to be a fatal problem. Catching them and packaging them into the future's failure could allow the program to continue despite its state being seriously corrupted. 143 | 144 | - **No Garbage**: Registered callbacks aren't just ignored when they stop mattering; they are actively removed and discarded. When a cancel token's source is deallocated before cancelling the token, the handlers are not kept around. When `toc_finallyAll:unless:` is cancelled, it does not forget about the callbacks it registered. 145 | 146 | - **No Undefined Behavior**: Corner cases should be accounted for in the implementation, specification, documentation, and tests of a method. Set a future to be its own result, directly or indirectly? It becomes immortal. Call `whenCancelledDo:unless:` on a cancelled token with a cancelled token? The callback doesn't run. Etc. 147 | 148 | - **No Inheritance**: A future source is not a future extended to be writable. A future source *has a* future that it controls. This is fundamental to ensuring consumers can't create memory leaks involving futures. Only producers can sustain those leaks, because as soon as the source is deallocated its future becomes immortal and all its callbacks get discarded. 149 | 150 | Design Foibles 151 | ============== 152 | 153 | Bothing's perfect. This is a list of things that bother me, although they probably cause few or no problems in practice. I might fix them, but only between major versions because they require breaking changes. 154 | 155 | - **Mutating Equality**: Completed futures are equated by value, but incomplete futures are equated by reference. Convenient when you want to know if two completed futures are the same, but not so convenient if you put futures in a collection that uses hash codes for faster lookup (because the hash code changes!). If you insert incomplete futures into sets, or use them as dictionary keys, you may be unable to find them in the set after they've completed. 156 | - **Two cancels**: `TOCCancelTokenSource` has both `tryCancel` and `cancel`. The only difference is the returned value. Would be simpler to just have `cancel`. 157 | 158 | API Breakdown 159 | ============= 160 | 161 | **TOCFuture**: An eventual result. 162 | 163 | - `+futureWithResult:(id)resultValue`: Returns a future that has already succeeded with the given value. If the value is a future, collapse occurs and its result/failure is used instead. 164 | - `+futureWithFailure:(id)failureValue`: Returns a future that has already failed with the given value. Variants for timeout and cancel failures work similarly. 165 | - `+new`: Returns an immortal future. (Not very useful.) 166 | - `+futureFromOperation:(id (^)(void))operation dispatchedOnQueue:(dispatch_queue_t)queue`: Dispatches an asynchronous operation, exposing the result as a future. 167 | - `+futureFromOperation:(id(^)(void))operation invokedOnThread:(NSThread*)thread`: Runs an asynchronous operation, exposing the result as a future. 168 | - `+futureWithResult:(id)resultValue afterDelay:(NSTimeInterval)delayInSeconds`: Returns a future the completes after a delay. An `unless:` variant allows the future to be cancelled and the timing stuff cleaned up. 169 | - `+futureFromUntilOperation:withOperationTimeout:until:`: Augments an until-style asynchronous operation with a timeout, returning the may-timeout future. The operation is cancelled if the timeout expires before completion. The operation is cancelled and/or its result cleaned up when the token is cancelled. 170 | - `+futureFromUnlessOperation:withOperationTimeout:`: Augments an unless-style asynchronous operation with a timeout, returning the may-timeout future. The operation is cancelled if the timeout expires before the operation completes. An `unless` variant allows the operation to also be cancelled if a token is cancelled before it completes. 171 | - `cancelledOnCompletionToken`: Returns a `TOCCancelToken` that becomes cancelled when the future has succeeded or failed. 172 | - `state`: Determines if the future is still able to be set (incomplete), failed, succeeded, flattening, or known to be immortal. 173 | - `isIncomplete`: Determines if the future is still able to be set, flattening, or known to be immortal. 174 | - `hasResult`: Determines if the future has succeeded with a result. 175 | - `hasFailed`: Determines if the future has failed. 176 | - `hasFailedWithCancel`: Determines if the future was cancelled, i.e. failed with a `TOCCancelToken` as its failure. 177 | - `hasFailedWithTimeout`: Determines if the future timed out, i.e. failed with a `TOCTimeout` as its failure. 178 | - `forceGetResult`: Returns the future's result, but raises an exception if the future didn't succeed with a result. 179 | - `forceGetFailure`: Returns the future's result, but raises an exception if the future didn't fail. 180 | - `finally[Do]:block [unless:token]`: Runs a callback when the future succeeds or fails. Passes the completed future into the block. The non-Do variants return a future that will eventually contain the result of evaluating the result-returning block. 181 | - `then[Do]:block [unless:token]`: Runs a callback when the future succeeds. Passes the future's result into the block. The non-Do variants return a future that will eventually contain the result of evaluating the result-returning block, or else the same failure as the receiving future. 182 | - `catch[Do]:block [unless:token]`: Runs a callback when the future fails. Passes the future's failure into the block. The non-Do variants return a future that will eventually contain the same result, or else the result of evaluating the result-returning block. 183 | - `isEqualToFuture:(TOCFuture*)other`: Determines if this future is in the same state and, if completed, has the same result/failure as the other future. 184 | - `unless:(TOCCancelToken*)unless`: Returns a future that will have the same result, unless the given token is cancelled first in which case it fails due to cancellation. 185 | 186 | **TOCFutureSource**: Creates and controls a TOCFuture. 187 | 188 | - `+new`: Returns a new future source controlling a new future. 189 | - `+futureSourceUntil:(TOCCancelToken*)untilCancelledToken`: Returns a new future source controlling a new future, wired to automatically fail with cancellation if the given token is cancelled before the future is set. 190 | - `future`: Returns the future controlled by this source. 191 | - `trySetResult:(id)result`: Sets the controlled future to succeed with the given value. If the result is a future, collapse occurs. Returns false if the future was already set, whereas the force variant raises an exception. 192 | - `trySetFailure:(id)result`: Sets the controlled future to fail with the given value. Returns false if the future was already set, whereas the force variant raises an exception. Variants for cancellation and timeout work similarly. 193 | 194 | **TOCCancelToken**: Notifies you when operations should be cancelled. 195 | 196 | - `+cancelledToken`: Returns an already cancelled token. 197 | - `+immortalToken`: Returns a token that will never be cancelled. Just a `[TOCCancelToken new]` token, but acts exactly like a nil cancel token. 198 | - `state`: Determines if the cancel token is cancelled, still cancellable, or known to be immortal. 199 | - `isAlreadyCancelled`: Determines if the cancel token is already cancelled. 200 | - `canStillBeCancelled`: Determines if the cancel token is not cancelled and not known to be immortal. 201 | - `whenCancelledDo:(TOCCancelHandler)cancelHandler`, `whenCancelledDo:(TOCCancelHandler)cancelHandler unless:(TOCCancelToken*)unlessCancelledToken`: Registers a void callback to run after the token is cancelled. Runs inline if already cancelled. The unless variant allows the callback to be removed if it has not run and is no longer needed (indicated by the other token being cancelled first). 202 | - `+matchFirstToCancelBetween:(TOCCancelToken*)token1 and:(TOCCancelToken*)token2`: Returns a token that is the minimum of two tokens. It is cancelled as soon as either of them is cancelled. 203 | - `+matchLastToCancelBetween:(TOCCancelToken*)token1 and:(TOCCancelToken*)token2`: Returns a token that is the maximum of two tokens. It is cancelled only when both of them is cancelled. 204 | 205 | **TOCCancelTokenSource**: Creates and controls a cancel token. 206 | 207 | - `+new`: Returns a new cancel token source that controls a new cancel token. 208 | - `+cancelTokenSourceUntil:(TOCCancelToken*)untilCancelledToken`: Returns a cancel token source that controls a new cancel token, but wired to cancel automatically when the given token is cancelled. 209 | - `token`: Returns the cancel token controlled by the source. 210 | - `cancel`: Cancels the token controlled by the source. Does nothing if the token is already cancelled. 211 | - `tryCancel`: Cancels the token controlled by the source, returning false if it was already cancelled. 212 | 213 | **NSArray+**: *We are one. We are many. We are more.* 214 | 215 | - `toc_thenAll`, `toc_thenAllUnless:(TOCCancelToken*)unless`: Converts from array-of-future to future-of-array. Takes an array of futures and returns a future that succeeds with an array of those futures' results. If any of the futures fails, the returned future fails. Example: `@[[TOCFuture futureWithResult:@1], [TOCFuture futureWithResult:@2]].toc_thenAll` evaluates to `[TOCFuture futureWithResult:@[@1, @2]]`. 216 | - `toc_finallyAll`, `toc_finallyAllUnless:(TOCCancelToken*)unless`: Awaits the completion of many futures. Takes an array of futures and returns a future that completes with an array of the same futures, but only after they have all completed. Example: `@[[TOCFuture futureWithResult:@1], [TOCFuture futureWithFailure:@2]].toc_finallyAll` evaluates to `[TOCFuture futureWithResult:@[[TOCFuture futureWithResult:@1], [TOCFuture futureWithFailure:@2]]]`. 217 | - `toc_orderedByCompletion`, `toc_orderedByCompletionUnless:(TOCCancelToken*)unless`: Returns an array with the "same" futures, except re-ordered so futures that will complete later will come later in the array. Example: `@[[TOCFutureSource new].future, [TOCFuture futureWithResult:@1]].toc_orderedByCompletion` returns `@[[TOCFuture futureWithResult:@1], [TOCFutureSource new].future]`. 218 | - `toc_raceForWinnerLastingUntil:(TOCCancelToken*)untilCancelledToken`: Takes an array of `TOCUntilOperation` blocks. Each block is a cancellable asynchronous operation, returning a future and taking a cancel token that cancels the operations and/or cleans up the operation's result. The returned future completes with the result of the first operation to finish (or else all of their failures). The result of the returned future is cleaned up upon cancellation. 219 | 220 | Development 221 | =========== 222 | 223 | **How to Build:** 224 | 225 | 1. **Get Source Code**: [Clone this git repository to your machine](https://help.github.com/articles/fetching-a-remote). 226 | 227 | 2. **Get Dependencies**: [Have CocoaPods installed](http://guides.cocoapods.org/using/getting-started.html). Run `pod install` from the project directory. 228 | 229 | 3. **Open Workspace**: Open `CollapsingFutures.xworkspace` with XCode (not the project, the *workspace*). Run tests and confirm that they pass. 230 | -------------------------------------------------------------------------------- /TwistedOakCollapsingFutures.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "TwistedOakCollapsingFutures" 3 | s.version = "1.0.0" 4 | s.summary = "Futures without nesting issues." 5 | s.description = <<-DESC 6 | Makes representing and consuming asynchronous results simpler. 7 | 8 | * #import "CollapsingFutures.h" 9 | * Eventual results and failures are represented as a TOCFuture. 10 | * Produce and control a TOCFuture with a new TOCFutureSource. 11 | * Hook work-to-eventually-do onto a future using then/catch/finally methods. 12 | * Chain more work onto the future results of then/catch/finally. 13 | * No need to track if a TOCFuture has a result of type TOCFuture: always automatically flattened. 14 | * Cancel operations by giving them a TOCCancelToken controlled by a TOCCancelTokenSource. 15 | DESC 16 | s.homepage = "https://github.com/Strilanc/ObjC-CollapsingFutures" 17 | s.license = { :type => 'BSD', :file => 'License.txt' } 18 | s.author = { "Craig Gidney" => "craig.gidney@gmail.com" } 19 | s.source = { :git => "https://github.com/Strilanc/ObjC-CollapsingFutures.git", :tag => "v1.0.0" } 20 | s.source_files = 'src', 'src/**/*.{h,m}' 21 | s.requires_arc = true 22 | s.dependency 'UnionFind', '~> 1.0' 23 | end 24 | -------------------------------------------------------------------------------- /src/CollapsingFutures.h: -------------------------------------------------------------------------------- 1 | #import "NSArray+TOCFuture.h" 2 | 3 | #import "TOCCancelToken+MoreConstructors.h" 4 | #import "TOCCancelTokenAndSource.h" 5 | 6 | #import "TOCFutureAndSource.h" 7 | #import "TOCFuture+MoreContinuations.h" 8 | #import "TOCFuture+MoreContructors.h" 9 | 10 | #import "TOCTimeout.h" 11 | #import "TOCTypeDefs.h" 12 | -------------------------------------------------------------------------------- /src/NSArray+TOCFuture.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "TOCFutureAndSource.h" 3 | #import "TOCTypeDefs.h" 4 | 5 | @interface NSArray (TOCFuture) 6 | 7 | /*! 8 | * Returns a future that succeeds with all of the futures in the receiving array, once they have all completed or failed, unless cancelled. 9 | * 10 | * @pre All items in the receiving array must be instances of TOCFuture. 11 | * 12 | * @result A future whose result will be an array containing all of the given futures. 13 | * If the given cancel token is cancelled before all futures in the array complete, the result fails with a cancellation. 14 | * @see hasFailedWithCancel 15 | * 16 | * @discussion Can be thought of as wrapping an Array-of-Futures into a Future-of-Array-of-Completed-Futures. 17 | * 18 | * A nil cancel token is treated like a cancel token that can never be cancelled. 19 | */ 20 | -(TOCFuture*) toc_finallyAllUnless:(TOCCancelToken*)unlessCancelledToken; 21 | 22 | /*! 23 | * Returns a future that succeeds with all of the futures in the receiving array, once they have all completed or failed. 24 | * 25 | * @pre All items in the receiving array must be instances of TOCFuture. 26 | * 27 | * @result A future whose result will be an array containing all of the given futures. 28 | * 29 | * @discussion Can be thought of as wrapping an Array-of-Futures into a Future-of-Array-of-Completed-Futures. 30 | * 31 | * The future returned by this method always succeeds with a result. It is guaranteed to not contain a failure. 32 | */ 33 | -(TOCFuture*) toc_finallyAll; 34 | 35 | /*! 36 | * Eventually gets all of the results from the futures in the receiving array, unless cancelled. 37 | * 38 | * @pre All items in the receiving array must be instances of TOCFuture. 39 | * 40 | * @result A future whose result will be an array containing all the results of the given futures, 41 | * unless any of the given futures fail in which case the returned future will fail with an array containing all of the given futures. 42 | * If the given cancel token is cancelled before all futures in the array complete, the result fails with a cancellation. 43 | * @see hasFailedWithCancel 44 | * 45 | * @discussion Can be thought of as flipping an Array-of-Futures into a Future-of-Array in the 'obvious' way. 46 | * 47 | * For example, @[[TOCFuture futureWithResult:@1], [TOCFuture futureWithResult:@2]].toc_thenAll is equivalent to [TOCFuture futureWithResult@[@1, @2]]. 48 | * 49 | * A nil cancel token is treated like a cancel token that can never be cancelled. 50 | */ 51 | -(TOCFuture*) toc_thenAllUnless:(TOCCancelToken*)unlessCancelledToken; 52 | 53 | /*! 54 | * Eventually gets all of the results from the futures in the receiving array. 55 | * 56 | * @pre All items in the receiving array must be instances of TOCFuture. 57 | * 58 | * @result A future whose result will be an array containing all the results of the given futures, 59 | * unless any of the given futures fail in which case the returned future will fail with an array containing all of the given futures. 60 | * 61 | * @discussion Can be thought of as flipping an Array-of-Futures into a Future-of-Array in the 'obvious' way. 62 | * 63 | * For example, @[[TOCFuture futureWithResult:@1], [TOCFuture futureWithResult:@2]].toc_thenAll is equivalent to [TOCFuture futureWithResult@[@1, @2]]. 64 | */ 65 | -(TOCFuture*) toc_thenAll; 66 | 67 | /*! 68 | * Immediately returns an array containing futures matching those in the receiving array, but with futures that will complete later placed later in the array, unless cancelled. 69 | * 70 | * @pre All items in the receiving array must be instances of TOCFuture. 71 | * 72 | * @result An array of futures where earlier futures complete first and each future in the returned array is matched with one from the receiving array. 73 | * If the given cancel token is cancelled before all futures in the array complete, the result fails with fails with a cancellation. 74 | * @see hasFailedWithCancel 75 | * 76 | * @discussion When one of the given futures completes, its result or failure is immediately placed into the first incomplete future in the returned array. 77 | * 78 | * The order that futures completed in in the past is not remembered. 79 | * Futures that had already completed will end up in the same order in the returned array as they were in the receiving array. 80 | * 81 | * A nil cancel token is treated like a cancel token that can never be cancelled. 82 | */ 83 | -(NSArray*) toc_orderedByCompletionUnless:(TOCCancelToken*)unlessCancelledToken; 84 | 85 | /*! 86 | * Immediately returns an array containing futures matching those in the receiving array, but with futures that will complete later placed later in the array. 87 | * 88 | * @pre All items in the receiving array must be instances of TOCFuture. 89 | * 90 | * @result An array of futures where earlier futures complete first and each future in the returned array is matched with one from the receiving array. 91 | * 92 | * @discussion When one of the given futures completes, its result or failure is immediately placed into the first incomplete future in the returned array. 93 | * 94 | * The order that futures completed in in the past is not remembered. 95 | * Futures that had already completed will end up in the same order in the returned array as they were in the receiving array. 96 | */ 97 | -(NSArray*) toc_orderedByCompletion; 98 | 99 | /*! 100 | * Runs all the TOCUntilOperation blocks in the array, racing the asynchronous operations they start against each other, and returns the winner as a future. 101 | * IMPORTANT: An operation's result MUST be cleaned up if the cancel token given to the starter is cancelled EVEN IF the operation has already completed. 102 | * 103 | * @param untilCancelledToken When this token is cancelled, both the race AND THE WINNING RESULT are cancelled, terminated, cleaned up, and generally DEAD. 104 | * 105 | * @result A future that will contain the result of the first asynchronous operation to complete, or else fail with the failures of every operation. 106 | * If the untilCancelledToken is cancelled before the race is over, the resulting future fails with a cancellation. 107 | * 108 | * @pre All items in the array must be TOCUntilOperation blocks. 109 | * 110 | * @discussion An TOCUntilOperation block takes an untilCancelledToken and returns a TOCFuture*. 111 | * The block is expected to start an asynchronous operation whose result is terminated when the token is cancelled, even if the operation has already completed. 112 | * 113 | * The untilCancelledTokens given to each racing operation are dependent, but distinct, from the untilCancelledToken given to this method. 114 | * 115 | * When a racing operation has won, all the other operations are cancelled by cancelling the untilCancelledToken that was given to them. 116 | * 117 | * You are allowed to give a nil untilCancelledToken to this method. 118 | * However, the individual operations will still be given a non-nil untilCancelledToken so that their results can be cleaned up in cases where multiple complete at the same time. 119 | * 120 | * CAUTION: When writing the racing operations within the scope of the token being passed to this method, it is easy to accidentally use the wrong untilCancelledToken. 121 | * Double-check that the racing operation is depending on the token given to it by this method, and not the token you're giving to this method. 122 | * 123 | * @see TOCUntilOperation 124 | */ 125 | -(TOCFuture*) toc_raceForWinnerLastingUntil:(TOCCancelToken*)untilCancelledToken; 126 | 127 | @end 128 | -------------------------------------------------------------------------------- /src/NSArray+TOCFuture.m: -------------------------------------------------------------------------------- 1 | #import "NSArray+TOCFuture.h" 2 | #import "TOCFuture+MoreContinuations.h" 3 | #import "TOCInternal.h" 4 | #include 5 | 6 | @implementation NSArray (TOCFuture) 7 | 8 | -(TOCFuture*) toc_finallyAll { 9 | return [self toc_finallyAllUnless:nil]; 10 | } 11 | 12 | -(TOCFuture*) toc_finallyAllUnless:(TOCCancelToken*)unlessCancelledToken { 13 | NSArray* futures = [self copy]; // remove volatility (i.e. ensure not externally mutable) 14 | TOCInternal_need([futures allItemsAreKindOfClass:[TOCFuture class]]); 15 | 16 | TOCFutureSource* resultSource = [TOCFutureSource new]; 17 | 18 | TOCInternal_need(futures.count < INT_MAX); 19 | __block int remaining = (int)futures.count + 1; 20 | TOCCancelHandler doneHandler = ^() { 21 | if (OSAtomicDecrement32(&remaining) > 0) return; 22 | [resultSource trySetResult:futures]; 23 | }; 24 | 25 | for (TOCFuture* item in futures) { 26 | [item.cancelledOnCompletionToken whenCancelledDo:doneHandler 27 | unless:unlessCancelledToken]; 28 | } 29 | 30 | doneHandler(); 31 | 32 | [unlessCancelledToken whenCancelledDo:^{ [resultSource trySetFailedWithCancel]; } 33 | unless:resultSource.future.cancelledOnCompletionToken]; 34 | 35 | return resultSource.future; 36 | } 37 | 38 | -(TOCFuture*) toc_thenAll { 39 | return [self toc_thenAllUnless:nil]; 40 | } 41 | 42 | -(TOCFuture*) toc_thenAllUnless:(TOCCancelToken*)unlessCancelledToken { 43 | return [[self toc_finallyAllUnless:unlessCancelledToken] then:^id(NSArray* completedFutures) { 44 | NSMutableArray* results = [NSMutableArray array]; 45 | for (TOCFuture* item in completedFutures) { 46 | if (item.hasFailed) return [TOCFuture futureWithFailure:completedFutures]; 47 | [results addObject:item.forceGetResult]; 48 | } 49 | return results; 50 | }]; 51 | } 52 | 53 | -(NSArray*) toc_orderedByCompletion { 54 | return [self toc_orderedByCompletionUnless:nil]; 55 | } 56 | 57 | -(NSArray*) toc_orderedByCompletionUnless:(TOCCancelToken*)unlessCancelledToken { 58 | NSArray* futures = [self copy]; // remove volatility (i.e. ensure not externally mutable) 59 | TOCInternal_need([futures allItemsAreKindOfClass:[TOCFuture class]]); 60 | 61 | NSMutableArray* resultSources = [NSMutableArray array]; 62 | 63 | TOCInternal_need(futures.count <= INT_MAX); 64 | __block int nextIndexMinusOne = -1; 65 | TOCFutureFinallyHandler doneHandler = ^(TOCFuture *completed) { 66 | NSUInteger i = (NSUInteger)OSAtomicIncrement32Barrier(&nextIndexMinusOne); 67 | [resultSources[i] forceSetResult:completed]; 68 | }; 69 | 70 | for (TOCFuture* item in futures) { 71 | [resultSources addObject:[TOCFutureSource new]]; 72 | [[item unless:unlessCancelledToken] finallyDo:doneHandler]; 73 | } 74 | 75 | return [resultSources map:^(TOCFutureSource* source) { return source.future; }]; 76 | } 77 | 78 | -(TOCFuture*) toc_raceForWinnerLastingUntil:(TOCCancelToken*)untilCancelledToken { 79 | NSArray* starters = [self copy]; // remove volatility (i.e. ensure not externally mutable) 80 | TOCInternal_need(starters.count > 0); 81 | TOCInternal_need([starters allItemsAreKindOfClass:NSClassFromString(@"NSBlock")]); 82 | 83 | return [TOCInternal_Racer asyncRace:starters until:untilCancelledToken]; 84 | } 85 | 86 | @end 87 | -------------------------------------------------------------------------------- /src/TOCCancelToken+MoreConstructors.h: -------------------------------------------------------------------------------- 1 | #import "TOCCancelTokenAndSource.h" 2 | 3 | @interface TOCCancelToken (MoreConstructors) 4 | 5 | /*! 6 | * Returns a cancel token that will be cancelled when either of the given cancel tokens is cancelled. 7 | * 8 | * @param token1 A token whose cancellation forces the returned token to become cancelled. 9 | * If token1 does not become immortal, the returned token also can't become immortal. 10 | * 11 | * @param token2 Another token whose cancellation forces the returned token to become cancelled. 12 | * If token2 does not become immortal, the returned token also can't become immortal. 13 | * 14 | * @result A cancel token for the "minimum" of the two cancel tokens' lives. 15 | * 16 | * @discussion The returned cancel token is guaranteed to become immortal if both the given tokens become immortal. 17 | * 18 | * The result may be one of the given tokens, instead of a new token. Specifically: 19 | * 20 | * - If one of the given tokens has already been cancelled, it will be used as the result. 21 | * 22 | * - If one of the given tokens has already become immortal, the other will be used as the result. 23 | * 24 | * - If the given tokens are actually the same token, it is used as the result. 25 | */ 26 | +(TOCCancelToken*) matchFirstToCancelBetween:(TOCCancelToken*)token1 27 | and:(TOCCancelToken*)token2; 28 | 29 | /*! 30 | * Returns a cancel token that will be cancelled when both of the given cancel tokens are cancelled. 31 | * 32 | * @param token1 A token that must be cancelled in order for the returned token to be cancelled. 33 | * If token1 becomes immortal, the returned token becomes immortal. 34 | * 35 | * @param token2 Another token that must be cancelled in order for the returned token to be cancelled. 36 | * If token2 becomes immortal, the returned token becomes immortal. 37 | * 38 | * @result A cancel token for the "maximum" of the two cancel tokens' lives. 39 | * 40 | * @discussion The returned cancel token is guaranteed to become immortal if either of the given tokens becomes immortal. 41 | * 42 | * The result may be one of the given tokens, instead of a new token. Specifically: 43 | * 44 | * - If one of the given tokens has already been cancelled, the other will be used as the result. 45 | * 46 | * - If one of the given tokens has already become immortal, it will be used as the result. 47 | * 48 | * - If the given tokens are actually the same token, it is used as the result. 49 | */ 50 | +(TOCCancelToken*) matchLastToCancelBetween:(TOCCancelToken*)token1 51 | and:(TOCCancelToken*)token2; 52 | 53 | @end 54 | -------------------------------------------------------------------------------- /src/TOCCancelToken+MoreConstructors.m: -------------------------------------------------------------------------------- 1 | #import "TOCCancelToken+MoreConstructors.h" 2 | #import "TOCInternal.h" 3 | 4 | @implementation TOCCancelToken (MoreConstructors) 5 | 6 | +(TOCCancelToken*) matchFirstToCancelBetween:(TOCCancelToken*)token1 and:(TOCCancelToken*)token2 { 7 | // check for special cases where we can just give back one of the tokens 8 | if (token1 == token2) return token1; 9 | enum TOCCancelTokenState state1 = token1.state; 10 | enum TOCCancelTokenState state2 = token2.state; 11 | if (state1 == TOCCancelTokenState_Cancelled) return token1; 12 | if (state2 == TOCCancelTokenState_Immortal) return token1; 13 | if (state1 == TOCCancelTokenState_Immortal) return token2; 14 | if (state2 == TOCCancelTokenState_Cancelled) return token2; 15 | 16 | TOCCancelTokenSource* minSource = [TOCCancelTokenSource new]; 17 | void (^doCancel)(void) = [^{ [minSource cancel]; } copy]; 18 | [token1 whenCancelledDo:doCancel unless:minSource.token]; 19 | [token2 whenCancelledDo:doCancel unless:minSource.token]; 20 | return minSource.token; 21 | } 22 | 23 | +(TOCCancelToken*) matchLastToCancelBetween:(TOCCancelToken*)token1 and:(TOCCancelToken*)token2 { 24 | // check for special cases where we can just give back one of the tokens 25 | if (token1 == token2) return token1; 26 | enum TOCCancelTokenState state1 = token1.state; 27 | enum TOCCancelTokenState state2 = token2.state; 28 | if (state1 == TOCCancelTokenState_Immortal) return token1; 29 | if (state2 == TOCCancelTokenState_Cancelled) return token1; 30 | if (state1 == TOCCancelTokenState_Cancelled) return token2; 31 | if (state2 == TOCCancelTokenState_Immortal) return token2; 32 | 33 | // make a token source that will be cancelled when either of the input tokens goes immortal or is cancelled 34 | TOCCancelTokenSource* cancelledWhenEitherSettle = [TOCCancelTokenSource new]; 35 | void (^didSettle)(void) = [^{ [cancelledWhenEitherSettle cancel]; } copy]; 36 | TOCInternal_OnDeallocObject* onDealloc1 = [TOCInternal_OnDeallocObject onDeallocDo:didSettle]; 37 | TOCInternal_OnDeallocObject* onDealloc2 = [TOCInternal_OnDeallocObject onDeallocDo:didSettle]; 38 | [token1 whenCancelledDo:^{ [onDealloc1 poke]; [cancelledWhenEitherSettle cancel]; } 39 | unless:cancelledWhenEitherSettle.token]; 40 | [token2 whenCancelledDo:^{ [onDealloc2 poke]; [cancelledWhenEitherSettle cancel]; } 41 | unless:cancelledWhenEitherSettle.token]; 42 | 43 | // work to cancel the result can continue once one of the input tokens has settled 44 | TOCCancelTokenSource* maxSource = [TOCCancelTokenSource new]; 45 | [cancelledWhenEitherSettle.token whenCancelledDo:^{ 46 | enum TOCCancelTokenState settledState1 = token1.state; 47 | enum TOCCancelTokenState settledState2 = token2.state; 48 | 49 | // if either token is immortal, we can just return (resulting in the last ref to maxSource being lost and it going immortal) 50 | if (settledState1 == TOCCancelTokenState_Immortal) return; 51 | if (settledState2 == TOCCancelTokenState_Immortal) return; 52 | 53 | // avoid the token that's been cancelled, so we condition on the only one that can be alive 54 | TOCCancelToken* remainingToken = settledState1 == TOCCancelTokenState_Cancelled ? token2 : token1; 55 | [remainingToken whenCancelledDo:^{ 56 | [maxSource cancel]; 57 | }]; 58 | }]; 59 | return maxSource.token; 60 | } 61 | 62 | @end 63 | -------------------------------------------------------------------------------- /src/TOCCancelTokenAndSource.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @class TOCCancelTokenSource; 4 | 5 | @class TOCFutureSource; 6 | 7 | /*! 8 | * The states that a cancel token can be in. 9 | * 10 | * @discussion A nil cancel token is considered to be in the immortal state. 11 | */ 12 | enum TOCCancelTokenState { 13 | /*! 14 | * The cancel token is not cancelled and will never be cancelled. 15 | * 16 | * @discussion All 'whenCancelledDo' handlers given to tokens in this state will be discarded without being run. 17 | * 18 | * A token becomes immortal when its source is deallocated without having cancelled the token. 19 | * 20 | * nil tokens are always treated as if they were a token in the immortal state. 21 | */ 22 | TOCCancelTokenState_Immortal = 0, // note: immortal must be the default (0), to ensure nil acts like an immortal token when you get its state 23 | 24 | /*! 25 | * The cancel token is not cancelled, but may become cancelled (or immortal). 26 | * 27 | * @discussion All 'whenCancelledDo' handlers given to tokens in this state will be stored. 28 | * The handlers will be run when the token becomes cancelled, or discarded when it becomes immortal. 29 | * 30 | * Note that a token in this state is volatile. 31 | * While you checked that a token was still-cancellable, it may have concurrently been cancelled or become immortal. 32 | */ 33 | TOCCancelTokenState_StillCancellable = 1, 34 | 35 | /*! 36 | * The cancel token has been permanently cancelled. 37 | * 38 | * @discussion All 'whenCancelledDo' handlers given to tokens in this state will be run inline. 39 | */ 40 | TOCCancelTokenState_Cancelled = 2 41 | }; 42 | 43 | /*! 44 | * The type of block passed to a TOCCancelToken's 'whenCancelledDo' method, to be called when the token has been cancelled. 45 | */ 46 | typedef void (^TOCCancelHandler)(void); 47 | 48 | /*! 49 | * Notifies you when operations should be cancelled. 50 | * 51 | * @discussion TOCCancelToken is thread safe. 52 | * It can be accessed from multiple threads concurrently. 53 | * 54 | * Use `whenCancelledDo` on a cancel token to add a block to be called once it has been cancelled. 55 | * 56 | * Use a new instance of `TOCCancelTokenSource`to create and control your own `TOCCancelToken` instance. 57 | * 58 | * Use `isAlreadyCancelled` and `canStillBeCancelled` on a cancel token to inspect its current state. 59 | * 60 | * A cancel token can be in one of three states: cancelled, immortal, or still-cancellable. 61 | * 62 | * A token in the immortal state is permanently immortal and not cancelled. 63 | * Immortal tokens immediately discard (without running) all cancel handlers given to them. 64 | * The nil cancel token is considered to be immortal. 65 | * 66 | * A token in the cancelled state is permanently cancelled. 67 | * Cancelled tokens immediately run, then discard, all cancel handlers given to them. 68 | * 69 | * A token in the still-cancellable state can be cancelled by its source's `cancel` method or immortalized by its source being deallocated. 70 | * Still-cancellable tokens store all cancel handlers given to them, until they transition to being cancelled or immortal. 71 | * Still-cancellable tokens are volatile: while you inspect their state. they can be cancelled concurrently. 72 | */ 73 | @interface TOCCancelToken : NSObject 74 | 75 | /*! 76 | * Returns a cancel token that has already been cancelled. 77 | * 78 | * @result A `TOCCancelToken` in the cancelled state. 79 | */ 80 | +(TOCCancelToken *)cancelledToken; 81 | 82 | /*! 83 | * Returns a cancel token that will never ever be cancelled. 84 | * 85 | * @result A non-nil `TOCCancelToken` permanently in the uncancelled state. 86 | * 87 | * @discussion The result of this method is non-nil, even though a nil cancel token is supposed to be treated exactly like an immortal token. 88 | * Useful for cases where that equivalence is unfortunately broken (e.g. placing into an NSArray). 89 | */ 90 | +(TOCCancelToken *)immortalToken; 91 | 92 | /*! 93 | * Returns the current state of the receiving cancel token: cancelled, immortal, or still-cancellable. 94 | * 95 | * @discussion Tokens that are cancelled or immortal are stable. 96 | * Tokens that are still-cancellable are volatile. 97 | * While you checked that a token was in the still-cancellable state, it may have been concurrently cancelled or immortalized. 98 | */ 99 | -(enum TOCCancelTokenState) state; 100 | 101 | /*! 102 | * Determines if the receiving cancel token is in the cancelled state, as opposed to being still-cancellable or immortal. 103 | */ 104 | -(bool)isAlreadyCancelled; 105 | 106 | /*! 107 | * Determines if the receiving cancel token is in the still-cancellable state, as opposed to being cancelled or immortal. 108 | * 109 | * @discussion Cancel tokens that can still be cancelled are volatile. 110 | * While you checked that `canStillBeCancelled` returned true, the receiving cancel token may have been concurrently cancelled or immortalized. 111 | */ 112 | -(bool)canStillBeCancelled; 113 | 114 | // --- 115 | // note to self: 116 | // do NOT include an isImmortal method. A nil cancel token is supposed to act like an immortal token, but [nil isImmortal] would return false 117 | // --- 118 | 119 | /*! 120 | * Registers a cancel handler block to be called once the receiving token is cancelled. 121 | * 122 | * @param cancelHandler The block to call once the token is cancelled. 123 | * 124 | * @discussion If the token is already cancelled, the handler is run inline. 125 | * 126 | * If the token is or becomes immortal, the handler is discarded without being run. 127 | * 128 | * When registered from the main thread, the given handler is guaranteed to also run on the main thread. 129 | * When the receiving token is already cancelled, the given handler is run inline (before returning to the caller). 130 | * Otherwise the handler will run on the thread that cancels the token. 131 | */ 132 | -(void)whenCancelledDo:(TOCCancelHandler)cancelHandler; 133 | 134 | /*! 135 | * Registers a cancel handler block to be called once the receiving token is cancelled, unless another token is cancelled before the handler runs. 136 | * 137 | * @param cancelHandler The block to call once the receiving token is cancelled. 138 | * 139 | * @param unlessCancelledToken If this argument is cancelled before the receiving token, the handler is discarded without being called. 140 | * If it is cancelled after the receiving token, but before the handler has run, the handler may or may not run. 141 | * 142 | * @discussion If the unlessCancelledToken was already cancelled, the handler is discarded without being run. 143 | * 144 | * If the receiving token is immortal or becomes immortal, the handler is discarded without being run. 145 | * 146 | * If the same token is used as both the receiving and unlessCancelled tokens, the handler is discarded without being run. 147 | * 148 | * When registered from the main thread, the handler is guaranteed to be run on the main thread. 149 | * When the receiving token is already cancelled, the handler is run inline (before returning to the caller). 150 | * Otherwise the handler will run on the thread that cancels the token. 151 | * 152 | * If the unlessCancelledToken token is cancelled after the receiving token, but before the handler has been run, the handler may or may not run. 153 | * 154 | * A case where the handler is guaranteed not to be run, despite the unlessCancelledToken being cancelled after the receiving token, is when 155 | * the handler has been automatically queued onto the main thread but not run yet. 156 | * If you are on the main thread, and determine that the given unlessCancelledToken has been cancelled and the handler has not run yet, 157 | * then it is guaranteed that the handler will not be run. 158 | */ 159 | -(void) whenCancelledDo:(TOCCancelHandler)cancelHandler 160 | unless:(TOCCancelToken*)unlessCancelledToken; 161 | 162 | @end 163 | 164 | /*! 165 | * Creates and controls a `TOCCancelToken`. 166 | * 167 | * @discussion Use the token property to access the token controlled by a token source. 168 | * 169 | * Use the `cancel` or `tryCancel` methods to cancel the token controlled by a token source. 170 | * 171 | * When a token source is deallocated, without its token having been cancelled, its token becomes immortal. 172 | * Immortal tokens discard all cancel handlers without running them. 173 | */ 174 | @interface TOCCancelTokenSource : NSObject 175 | 176 | /*! 177 | * Returns the token controlled by the receiving token source. 178 | */ 179 | @property (readonly, nonatomic) TOCCancelToken* token; 180 | 181 | /*! 182 | * Cancels the token controlled by the receiving token source. 183 | * 184 | * @discussion If the token has already been cancelled, cancel has no effect. 185 | */ 186 | -(void) cancel; 187 | 188 | /*! 189 | * Attempts to cancel the token controlled by the receiving token source. 190 | * 191 | * @result True if the token controlled by this source transitioned to the cancelled state, or false if the token was already cancelled. 192 | */ 193 | -(bool) tryCancel; 194 | 195 | /*! 196 | * Creates and returns a cancel token source that is dependent on the given cancel token. 197 | * If the given cancel token is cancelled, the resulting cancel token source automatically cancels its own token. 198 | * 199 | * @param untilCancelledToken The token whose cancellation forces the resulting cancel token source's token to be cancelled. 200 | * Allowed to be nil, in which case the returned cancel token source is just a normal new cancel token source. 201 | * 202 | * @result A cancel token source that depends on the given cancel token. 203 | * 204 | * @discussion The returned cancel token source can still be cancelled normally. 205 | */ 206 | +(TOCCancelTokenSource*) cancelTokenSourceUntil:(TOCCancelToken*)untilCancelledToken; 207 | 208 | @end 209 | -------------------------------------------------------------------------------- /src/TOCCancelTokenAndSource.m: -------------------------------------------------------------------------------- 1 | #import "TOCCancelTokenAndSource.h" 2 | #import "TOCFutureAndSource.h" 3 | #import "TOCInternal.h" 4 | #include 5 | 6 | typedef void (^Remover)(void); 7 | typedef void (^SettledHandler)(void); 8 | 9 | @implementation TOCCancelToken { 10 | @private NSMutableArray* _cancelHandlers; 11 | @private NSMutableSet* _removableSettledHandlers; // run when the token is cancelled or immortal 12 | @private enum TOCCancelTokenState _state; 13 | } 14 | 15 | +(TOCCancelToken *)cancelledToken { 16 | static dispatch_once_t once; 17 | static TOCCancelToken* token = nil; 18 | dispatch_once(&once, ^{ 19 | token = [TOCCancelToken new]; 20 | token->_state = TOCCancelTokenState_Cancelled; 21 | }); 22 | return token; 23 | } 24 | 25 | +(TOCCancelToken *)immortalToken { 26 | static dispatch_once_t once; 27 | static TOCCancelToken* token = nil; 28 | dispatch_once(&once, ^{ 29 | token = [TOCCancelToken new]; 30 | // default state should be immortal 31 | assert(token->_state == TOCCancelTokenState_Immortal); 32 | }); 33 | return token; 34 | } 35 | 36 | +(TOCCancelToken*) _ForSource_cancellableToken { 37 | TOCCancelToken* token = [TOCCancelToken new]; 38 | token->_cancelHandlers = [NSMutableArray array]; 39 | token->_removableSettledHandlers = [NSMutableSet set]; 40 | token->_state = TOCCancelTokenState_StillCancellable; 41 | return token; 42 | } 43 | -(bool) _ForSource_tryImmortalize { 44 | NSSet* settledHandlersSnapshot; 45 | @synchronized(self) { 46 | if (_state != TOCCancelTokenState_StillCancellable) return false; 47 | _state = TOCCancelTokenState_Immortal; 48 | 49 | // need to copy+clear settled handlers, instead of just nil-ing the ref, because indirect references to it escape and may be kept alive indefinitely 50 | settledHandlersSnapshot = [_removableSettledHandlers copy]; 51 | [_removableSettledHandlers removeAllObjects]; 52 | _removableSettledHandlers = nil; 53 | 54 | _cancelHandlers = nil; 55 | } 56 | 57 | for (SettledHandler handler in settledHandlersSnapshot) { 58 | handler(); 59 | } 60 | return true; 61 | } 62 | -(bool) _ForSource_tryCancel { 63 | NSArray* cancelHandlersSnapshot; 64 | NSSet* settledHandlersSnapshot; 65 | @synchronized(self) { 66 | if (_state != TOCCancelTokenState_StillCancellable) return false; 67 | _state = TOCCancelTokenState_Cancelled; 68 | 69 | cancelHandlersSnapshot = _cancelHandlers; 70 | _cancelHandlers = nil; 71 | 72 | // need to copy+clear settled handlers, instead of just nil-ing the ref, because indirect references to it escape and may be kept alive indefinitely 73 | settledHandlersSnapshot = [_removableSettledHandlers copy]; 74 | [_removableSettledHandlers removeAllObjects]; 75 | _removableSettledHandlers = nil; 76 | } 77 | 78 | for (TOCCancelHandler handler in cancelHandlersSnapshot) { 79 | handler(); 80 | } 81 | for (SettledHandler handler in settledHandlersSnapshot) { 82 | handler(); 83 | } 84 | return true; 85 | } 86 | 87 | -(enum TOCCancelTokenState)state { 88 | @synchronized(self) { 89 | return _state; 90 | } 91 | } 92 | -(bool)isAlreadyCancelled { 93 | return self.state == TOCCancelTokenState_Cancelled; 94 | } 95 | -(bool)canStillBeCancelled { 96 | return self.state == TOCCancelTokenState_StillCancellable; 97 | } 98 | 99 | -(TOCCancelHandler) _preserveMainThreadness:(TOCCancelHandler)cancelHandler { 100 | if (!NSThread.isMainThread) return cancelHandler; 101 | 102 | return ^{ [TOCInternal_BlockObject performBlock:cancelHandler 103 | onThread:NSThread.mainThread]; }; 104 | } 105 | 106 | -(TOCCancelHandler) _preserveMainThreadness:(TOCCancelHandler)cancelHandler 107 | andCheck:(TOCCancelToken*)unlessCancelledToken { 108 | if (!NSThread.isMainThread) return cancelHandler; 109 | 110 | return [self _preserveMainThreadness:^{ 111 | // do a final check, to help the caller out 112 | // consider: if the unless token was cancelled after this token, but before the callback reached the main thread 113 | // in that situation, the caller may have already done UI things based on observing that the token was cancelled 114 | // they may be *extremely* surprised by the callback running their cleanup stuff again 115 | // we don't want to surprise them, so we do this polite check before calling 116 | if (unlessCancelledToken.isAlreadyCancelled) return; 117 | 118 | cancelHandler(); 119 | }]; 120 | } 121 | 122 | -(void)whenCancelledDo:(TOCCancelHandler)cancelHandler { 123 | TOCInternal_need(cancelHandler != nil); 124 | 125 | @synchronized(self) { 126 | if (_state == TOCCancelTokenState_Immortal) return; 127 | if (_state == TOCCancelTokenState_StillCancellable) { 128 | TOCCancelHandler safeHandler = [self _preserveMainThreadness:cancelHandler]; 129 | [_cancelHandlers addObject:safeHandler]; 130 | return; 131 | } 132 | } 133 | 134 | cancelHandler(); 135 | } 136 | 137 | -(Remover)_removable_whenSettledDo:(SettledHandler)settledHandler { 138 | TOCInternal_need(settledHandler != nil); 139 | @synchronized(self) { 140 | if (_state == TOCCancelTokenState_StillCancellable) { 141 | // to ensure we don't end up with two distinct copies of the block, move it to a local 142 | // (otherwise one copy will be made when adding to the array, and another when storing to the remove closure) 143 | // (so without this line, the added handler wouldn't be guaranteed removable, because the remover will try to remove the wrong instance) 144 | SettledHandler singleCopyOfHandler = [settledHandler copy]; 145 | 146 | [_removableSettledHandlers addObject:singleCopyOfHandler]; 147 | 148 | return ^{ 149 | @synchronized(self) { 150 | [self->_removableSettledHandlers removeObject:singleCopyOfHandler]; 151 | } 152 | }; 153 | } 154 | } 155 | 156 | settledHandler(); 157 | return nil; 158 | } 159 | 160 | -(void) whenCancelledDo:(TOCCancelHandler)cancelHandler 161 | unless:(TOCCancelToken*)unlessCancelledToken { 162 | TOCInternal_need(cancelHandler != nil); 163 | 164 | // fair warning: the following code is very difficult to get right. 165 | 166 | // optimistically do less work 167 | enum TOCCancelTokenState peekOtherState = unlessCancelledToken.state; 168 | if (peekOtherState == TOCCancelTokenState_Immortal) { 169 | [self whenCancelledDo:cancelHandler]; 170 | return; 171 | } 172 | if (unlessCancelledToken == self || peekOtherState == TOCCancelTokenState_Cancelled) { 173 | return; 174 | } 175 | 176 | // make a block that must be called twice to break the cancelling-each-other cycle 177 | // that way we can be sure we don't touch an un-initialized part of the cycle, by making one call only once setup is complete 178 | __block int callCount = 0; 179 | __block Remover removeHandlerFromOtherToSelf = nil; 180 | Remover onSecondCallRemoveHandlerFromOtherToSelf = ^{ 181 | if (OSAtomicIncrement32Barrier(&callCount) == 1) return; 182 | if (removeHandlerFromOtherToSelf == nil) return; // only occurs when the handler was already run and discarded anyways 183 | removeHandlerFromOtherToSelf(); 184 | removeHandlerFromOtherToSelf = nil; 185 | }; 186 | 187 | // make the cancel-each-other cycle, running the cancel handler if self is cancelled first 188 | TOCCancelHandler safeHandler = [self _preserveMainThreadness:cancelHandler 189 | andCheck:unlessCancelledToken]; 190 | __block Remover removeHandlerFromSelfToOther = [self _removable_whenSettledDo:^(){ 191 | // note: this self-reference is fine because it doesn't involve self's source, and gets cleared if the source is deallocated 192 | if (self->_state == TOCCancelTokenState_Cancelled) { 193 | safeHandler(); 194 | } 195 | onSecondCallRemoveHandlerFromOtherToSelf(); 196 | }]; 197 | removeHandlerFromOtherToSelf = [unlessCancelledToken _removable_whenSettledDo:^() { 198 | if (removeHandlerFromSelfToOther == nil) return; // only occurs when the handler was already run and discarded anyways 199 | removeHandlerFromSelfToOther(); 200 | removeHandlerFromSelfToOther = nil; 201 | }]; 202 | 203 | // allow the cycle to be broken 204 | onSecondCallRemoveHandlerFromOtherToSelf(); 205 | } 206 | 207 | -(NSString*) description { 208 | switch (self.state) { 209 | case TOCCancelTokenState_Cancelled: 210 | return @"Cancelled Token"; 211 | case TOCCancelTokenState_Immortal: 212 | return @"Uncancelled Token (Immortal)"; 213 | case TOCCancelTokenState_StillCancellable: 214 | return @"Uncancelled Token"; 215 | default: 216 | return @"Cancel token in an unrecognized state"; 217 | } 218 | } 219 | 220 | @end 221 | 222 | @implementation TOCCancelTokenSource 223 | 224 | @synthesize token; 225 | 226 | -(TOCCancelTokenSource*) init { 227 | self = [super init]; 228 | if (self) { 229 | self->token = [TOCCancelToken _ForSource_cancellableToken]; 230 | } 231 | return self; 232 | } 233 | 234 | +(TOCCancelTokenSource*) cancelTokenSourceUntil:(TOCCancelToken*)untilCancelledToken { 235 | TOCCancelTokenSource* source = [TOCCancelTokenSource new]; 236 | [untilCancelledToken whenCancelledDo:^{ [source cancel]; } 237 | unless:source.token]; 238 | return source; 239 | } 240 | 241 | -(void) dealloc { 242 | [token _ForSource_tryImmortalize]; 243 | } 244 | -(void) cancel { 245 | [token _ForSource_tryCancel]; 246 | } 247 | -(bool)tryCancel { 248 | return [token _ForSource_tryCancel]; 249 | } 250 | 251 | -(NSString*) description { 252 | return [NSString stringWithFormat:@"Cancel Token Source: %@", token]; 253 | } 254 | 255 | @end 256 | -------------------------------------------------------------------------------- /src/TOCFuture+MoreContinuations.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "TOCFutureAndSource.h" 3 | 4 | /*! 5 | * Utility methods involving continuing off of futures (then/catch/finally). 6 | */ 7 | @interface TOCFuture (MoreContinuations) 8 | 9 | /*! 10 | * Eventually evaluates a 'then' continuation on the receiving future's result, or else propagates the receiving future's failure. 11 | * 12 | * @result A future for the eventual result of evaluating the given 'then' block on the receiving future's result, or else a failure if the receiving future fails. 13 | * 14 | * @param resultContinuation The block to evaluate when the future succeeds with a result. 15 | * 16 | * @discussion If the receiving future has already succeeded with a result, the continuation is run inline. 17 | * 18 | * If the receiving future fails, instead of succeeding with a result, the continuation is not run and the failure is propagated into the returned future. 19 | * 20 | * If the continuation returns a future, instead of a normal value, then this method's result is automatically flattened to match that future instead of containing it. 21 | */ 22 | -(TOCFuture *)then:(TOCFutureThenContinuation)resultContinuation; 23 | 24 | /*! 25 | * Eventually matches the receiving future's result, or else evaluates a 'catch' continuation on the receiving future's failure. 26 | * 27 | * @param failureContinuation The block to evaluate when the future fails. 28 | * 29 | * @result A future for the eventual result of the receiving future, or else the eventual result of running the receiving future's failure through the given 'catch' block. 30 | * 31 | * @discussion If the receiving future has already failed, the continuation is run inline. 32 | * 33 | * If the continuation returns a future, instead of a normal value, then this method's result is automatically flattened to match that future instead of containing it. 34 | */ 35 | -(TOCFuture *)catch:(TOCFutureCatchContinuation)failureContinuation; 36 | 37 | /*! 38 | * Eventually evaluates a 'finally' continuation on the receiving future, once it has completed with a result or failed. 39 | * 40 | * @param completionContinuation The block to evaluate when the future fails or completes with a result. 41 | * 42 | * @result A future for the eventual result of evaluating the given 'finally' block on the receiving future once it has completed. 43 | * 44 | * @discussion If the receiving future has already completed, the continuation is run inline. 45 | * 46 | * If the continuation returns a future, instead of a normal value, then this method's result is automatically flattened to match that future instead of containing it. 47 | */ 48 | -(TOCFuture *)finally:(TOCFutureFinallyContinuation)completionContinuation; 49 | 50 | /*! 51 | * Eventually runs a 'then' handler on the receiving future's result. 52 | * 53 | * @param resultHandler The block to run when the future succeeds with a result. 54 | * 55 | * @discussion If the receiving future has already succeeded with a result, the handler is run inline. 56 | * 57 | * If the receiving future fails, instead of succeeding with a result, the handler is not run. 58 | */ 59 | -(void) thenDo:(TOCFutureThenHandler)resultHandler; 60 | 61 | /*! 62 | * Eventually runs a 'catch' handler on the receiving future's failure. 63 | * 64 | * @param failureHandler The block to run when the future fails. 65 | * 66 | * @discussion If the receiving future has already failed, the handler is run inline. 67 | * 68 | * If the receiving future succeeds with a result, instead of failing, the handler is not run. 69 | */ 70 | -(void) catchDo:(TOCFutureCatchHandler)failureHandler; 71 | 72 | /*! 73 | * Eventually runs a 'finally' handler on the receiving future, once it has completed with a result or failed. 74 | * 75 | * @param completionHandler The block to run when the future fails or completes with a result. 76 | * 77 | * @discussion If the receiving future has already completed with a result or failed, the handler is run inline. 78 | */ 79 | -(void) finallyDo:(TOCFutureFinallyHandler)completionHandler; 80 | 81 | /*! 82 | * Returns a future that will match the receiving future, except it immediately cancels if the given cancellation tokens is cancelled first. 83 | * 84 | * @param unlessCancelledToken The cancellation token that can be used to cause the resulting future to immediately fail with the cancellation token as its failure value. 85 | * A nil cancellation token corresponds to an immortal cancellation token. 86 | * 87 | * @result A future that will either contain the same result/failure as the receiving future, or else the given cancellation token when it is cancelled. 88 | * 89 | * @discussion If the receiving future is already completed and the given cancellation token is already cancelled, the cancellation "wins". 90 | * The resulting future will fail with the cancellation token as its failure value. 91 | * 92 | * When the cancellation token is immortal, the receiving future may be returned unmodified and unwrapped. 93 | * 94 | * Works by registering a continuation on the receiving future. 95 | * If the cancellation token is cancelled, this continuation is immediately cleaned up. 96 | */ 97 | -(TOCFuture*) unless:(TOCCancelToken*)unlessCancelledToken; 98 | 99 | @end 100 | -------------------------------------------------------------------------------- /src/TOCFuture+MoreContinuations.m: -------------------------------------------------------------------------------- 1 | #import "TOCFuture+MoreContinuations.h" 2 | #import "TOCInternal.h" 3 | 4 | @implementation TOCFuture (MoreContinuations) 5 | 6 | -(void)finallyDo:(TOCFutureFinallyHandler)completionHandler { 7 | [self finallyDo:completionHandler unless:nil]; 8 | } 9 | 10 | -(void)thenDo:(TOCFutureThenHandler)resultHandler { 11 | [self thenDo:resultHandler unless:nil]; 12 | } 13 | 14 | -(void)catchDo:(TOCFutureCatchHandler)failureHandler { 15 | [self catchDo:failureHandler unless:nil]; 16 | } 17 | 18 | -(TOCFuture *)finally:(TOCFutureFinallyContinuation)completionContinuation { 19 | return [self finally:completionContinuation unless:nil]; 20 | } 21 | 22 | -(TOCFuture *)then:(TOCFutureThenContinuation)resultContinuation { 23 | return [self then:resultContinuation unless:nil]; 24 | } 25 | 26 | -(TOCFuture *)catch:(TOCFutureCatchContinuation)failureContinuation { 27 | return [self catch:failureContinuation unless:nil]; 28 | } 29 | 30 | -(TOCFuture*) unless:(TOCCancelToken*)unlessCancelledToken { 31 | // optimistically do nothing, when given immortal cancel tokens 32 | if (unlessCancelledToken.state == TOCCancelTokenState_Immortal) { 33 | return self; 34 | } 35 | 36 | return [self finally:^(TOCFuture *completed) { return completed; } 37 | unless:unlessCancelledToken]; 38 | } 39 | 40 | @end 41 | -------------------------------------------------------------------------------- /src/TOCFuture+MoreContructors.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "TOCFutureAndSource.h" 3 | #import "TOCTypeDefs.h" 4 | #import "TOCTimeout.h" 5 | 6 | @interface TOCFuture (MoreConstructors) 7 | 8 | /*! 9 | * Returns a future that has already failed with a timeout failure. 10 | * 11 | * A timeout failure is just an instance of TOCTimeout. 12 | */ 13 | +(TOCFuture*) futureWithTimeoutFailure; 14 | 15 | /*! 16 | * Returns a future that has already failed with a cancellation failure. 17 | * 18 | * A cancellation failure is just an instance of TOCCancelToken. 19 | */ 20 | +(TOCFuture*) futureWithCancelFailure; 21 | 22 | /*! 23 | * Returns a future that completes with the value returned by a function run via grand central dispatch. 24 | * 25 | * @param operation The operation to eventually evaluate. 26 | * 27 | * @param queue The gcd queue to dispatch the operation on. 28 | * 29 | * @result A future that completes once the operation has been completed, and contains the operation's result. 30 | */ 31 | +(TOCFuture*) futureFromOperation:(id (^)(void))operation 32 | dispatchedOnQueue:(dispatch_queue_t)queue; 33 | 34 | /*! 35 | * Returns a future that completes with the value returned by a function run on a specified thread. 36 | * 37 | * @param operation The operation to eventually evaluate. 38 | * 39 | * @param thread The thread to perform the operation on. 40 | * 41 | * @result A future that completes once the operation has been completed, and contains the operation's result. 42 | */ 43 | +(TOCFuture*) futureFromOperation:(id(^)(void))operation 44 | invokedOnThread:(NSThread*)thread; 45 | 46 | /*! 47 | * Returns a future that will contain the given result after the given delay, unless cancelled. 48 | * 49 | * @param resultValue The value the resulting future should complete with, after the delay has passed. 50 | * 51 | * @param delayInSeconds The amount of time to wait, in seconds, before the resulting future completes. 52 | * Must not be negative or NaN (raises exception). 53 | * A delay of 0 results in a future that's already completed. 54 | * A delay of INFINITY result in an immortal future that never completes. 55 | * 56 | * @param unlessCancelledToken If this token is cancelled before the delay expires, the future immediately fails with the cancel token as its failure. 57 | * Any resources being used for the delay, such as NSTimers, will also be immediately cleaned up upon cancellation. 58 | * 59 | * @result A delayed future result, unless the delay is cancelled. 60 | */ 61 | +(TOCFuture*) futureWithResult:(id)resultValue 62 | afterDelay:(NSTimeInterval)delayInSeconds 63 | unless:(TOCCancelToken*)unlessCancelledToken; 64 | 65 | /*! 66 | * Returns a future that will contain the given result after the given delay. 67 | * 68 | * @param resultValue The value the resulting future should complete with, after the delay has passed. 69 | * 70 | * @param delayInSeconds The amount of time to wait, in seconds, before the resulting future completes. 71 | * Must not be negative or NaN (raises exception). 72 | * A delay of 0 results in a future that's already completed. 73 | * A delay of INFINITY result in an immortal future that never completes. 74 | * 75 | * @result A delayed future result. 76 | */ 77 | +(TOCFuture*) futureWithResult:(id)resultValue 78 | afterDelay:(NSTimeInterval)delayInSeconds; 79 | 80 | /*! 81 | * Returns a future that eventually contains the result, which lasts until the given token is cancelled, of an asynchronous operation as long as it completes before the given timeout. 82 | * 83 | * @param asyncOperationWithResultLastingUntilCancelled The asynchronous operation to evaluate. 84 | * 85 | * @param timeoutPeriodInSeconds The amount of time, in seconds, the asynchronous operation is given to finish before it is cancelled. 86 | * Must not be negative or NaN (raises exception). 87 | * A delay of 0 results in an immediate timeout failure without the operation even being run. 88 | * A delay of INFINITY results in the operation being run without a timeout. 89 | * 90 | * @param untilCancelledToken The operation, and its results, are cancelled when this token is cancelled. 91 | * 92 | * @result The eventual result of the asynchronous operation, unless the operation is cancelled or times out. 93 | * 94 | * @discussion If the operation has not yet completed when the untilCancelledToken is cancelled, the operation is immediately cancelled. 95 | * 96 | * If the operation has already completed when the untilCancelledToken is cancelled, its result is terminated / cleaned-up / dead. 97 | */ 98 | +(TOCFuture*) futureFromUntilOperation:(TOCUntilOperation)asyncOperationWithResultLastingUntilCancelled 99 | withOperationTimeout:(NSTimeInterval)timeoutPeriodInSeconds 100 | until:(TOCCancelToken*)untilCancelledToken; 101 | 102 | /*! 103 | * Returns a future for the eventual result of an asynchronous operation, unless the operation times out or is cancelled. 104 | * 105 | * @param asyncCancellableOperation The cancellable asynchronous operation to evaluate. 106 | * 107 | * @param timeoutPeriodInSeconds The amount of time, in seconds, the asynchronous operation is given to finish before it is cancelled due to a timeout. 108 | * Must not be negative or NaN (raises exception). 109 | * A delay of 0 results in an immediate timeout failure without the operation even being run. 110 | * A delay of INFINITY results in the operation being run without a timeout. 111 | * 112 | * @param unlessCancelledToken Cancelling this token will cancel the asynchronous operation, unless it has already finished. 113 | * 114 | * @result The eventual result of the asynchronous operation, or else a timeout or cancellation failure. 115 | * 116 | * @discussion If the operation times out, the resulting future fails with an instance of TOCTimeout as its failure. 117 | * 118 | * If the operation is cancelled, the resulting future fails with a cancel. 119 | * 120 | * If the operation finishes without being cancelled, the resulting future will match it. 121 | * 122 | * This method is unable to forward its result before the given asynchronous operation confirms it was cancelled (by cancelling the future it returned). 123 | * Otherwise it would be possible to leak a result that needed to be cleaned up, due to the operation's completion racing with timing out. 124 | */ 125 | +(TOCFuture*) futureFromUnlessOperation:(TOCUnlessOperation)asyncCancellableOperation 126 | withTimeout:(NSTimeInterval)timeoutPeriodInSeconds 127 | unless:(TOCCancelToken*)unlessCancelledToken; 128 | 129 | /*! 130 | * Returns a future for the eventual result of an asynchronous operation, unless the operation times out. 131 | * 132 | * @param asyncCancellableOperation The cancellable asynchronous operation to evaluate. 133 | * 134 | * @param timeoutPeriodInSeconds The amount of time, in seconds, the asynchronous operation is given to finish before it is cancelled due to a timeout. 135 | * Must not be negative or NaN (raises exception). 136 | * A delay of 0 results in an immediate timeout failure without the operation even being run. 137 | * A delay of INFINITY results in the operation being run without a timeout. 138 | * 139 | * @result The eventual result of the asynchronous operation, or else a timeout failure. 140 | * 141 | * @discussion If the operation times out, the resulting future fails with an instance of TOCTimeout as its failure. 142 | * 143 | * If the operation finishes without timing out or acknowledging the timeout, the resulting future will match it. 144 | * 145 | * This method is unable to forward its result before the given asynchronous operation confirms it was cancelled due to the timeout (by cancelling the future it returned). 146 | * Otherwise it would be possible to leak a result that needed to be cleaned up, due to the operation's completion racing with timing out. 147 | */ 148 | +(TOCFuture*) futureFromUnlessOperation:(TOCUnlessOperation)asyncCancellableOperation 149 | withTimeout:(NSTimeInterval)timeoutPeriodInSeconds; 150 | 151 | @end 152 | -------------------------------------------------------------------------------- /src/TOCFuture+MoreContructors.m: -------------------------------------------------------------------------------- 1 | #import "TOCFuture+MoreContructors.h" 2 | #import "TOCFuture+MoreContinuations.h" 3 | #import "TOCInternal.h" 4 | #import "NSArray+TOCFuture.h" 5 | 6 | @implementation TOCFuture (MoreConstructors) 7 | 8 | +(TOCFuture*) futureWithTimeoutFailure { 9 | return [TOCFuture futureWithFailure:[TOCTimeout new]]; 10 | } 11 | 12 | +(TOCFuture*) futureWithCancelFailure { 13 | return [TOCFuture futureWithFailure:TOCCancelToken.cancelledToken]; 14 | } 15 | 16 | +(TOCFuture*) futureFromOperation:(id (^)(void))operation 17 | dispatchedOnQueue:(dispatch_queue_t)queue { 18 | TOCInternal_need(operation != nil); 19 | 20 | TOCFutureSource* resultSource = [TOCFutureSource new]; 21 | 22 | dispatch_async(queue, ^{ [resultSource forceSetResult:operation()]; }); 23 | 24 | return resultSource.future; 25 | } 26 | +(TOCFuture*) futureFromOperation:(id(^)(void))operation 27 | invokedOnThread:(NSThread*)thread { 28 | TOCInternal_need(operation != nil); 29 | TOCInternal_need(thread != nil); 30 | 31 | TOCFutureSource* resultSource = [TOCFutureSource new]; 32 | 33 | [TOCInternal_BlockObject performBlock:^{ [resultSource forceSetResult:operation()]; } 34 | onThread:thread]; 35 | 36 | return resultSource.future; 37 | } 38 | 39 | +(TOCFuture*) futureWithResult:(id)resultValue 40 | afterDelay:(NSTimeInterval)delayInSeconds { 41 | return [self futureWithResult:resultValue 42 | afterDelay:delayInSeconds 43 | unless:nil]; 44 | } 45 | +(TOCFuture*) futureWithResult:(id)resultValue 46 | afterDelay:(NSTimeInterval)delayInSeconds 47 | unless:(TOCCancelToken*)unlessCancelledToken { 48 | TOCInternal_need(delayInSeconds >= 0); 49 | TOCInternal_need(!isnan(delayInSeconds)); 50 | 51 | if (delayInSeconds == 0) return [TOCFuture futureWithResult:resultValue]; 52 | __block id resultValueBlock = resultValue; 53 | 54 | TOCFutureSource* resultSource = [TOCFutureSource new]; 55 | if (delayInSeconds == INFINITY) return [resultSource.future unless:unlessCancelledToken]; 56 | 57 | double delayInNanoseconds = delayInSeconds * NSEC_PER_SEC; 58 | TOCInternal_need(delayInNanoseconds < INT64_MAX/2); 59 | 60 | dispatch_time_t then = dispatch_time(DISPATCH_TIME_NOW, (int64_t)delayInNanoseconds); 61 | dispatch_after(then, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 62 | [resultSource trySetResult:resultValueBlock]; 63 | }); 64 | [unlessCancelledToken whenCancelledDo:^{ 65 | if ([resultSource trySetFailedWithCancel]) { 66 | resultValueBlock = nil; 67 | } 68 | }]; 69 | 70 | 71 | return resultSource.future; 72 | } 73 | 74 | +(TOCFuture*) futureFromUntilOperation:(TOCUntilOperation)asyncOperationWithResultLastingUntilCancelled 75 | withOperationTimeout:(NSTimeInterval)timeoutPeriodInSeconds 76 | until:(TOCCancelToken*)untilCancelledToken { 77 | TOCInternal_need(asyncOperationWithResultLastingUntilCancelled != nil); 78 | TOCInternal_need(timeoutPeriodInSeconds >= 0); 79 | TOCInternal_need(!isnan(timeoutPeriodInSeconds)); 80 | 81 | if (timeoutPeriodInSeconds == 0) { 82 | return [TOCFuture futureWithTimeoutFailure]; 83 | } 84 | if (timeoutPeriodInSeconds == INFINITY) { 85 | return asyncOperationWithResultLastingUntilCancelled(untilCancelledToken); 86 | } 87 | 88 | TOCUntilOperation timeoutOperation = ^(TOCCancelToken* internalUntilCancelledToken) { 89 | return [TOCFuture futureWithResult:@[[TOCFuture futureWithTimeoutFailure]] 90 | afterDelay:timeoutPeriodInSeconds 91 | unless:internalUntilCancelledToken]; 92 | }; 93 | TOCUntilOperation wrappedOperation = ^(TOCCancelToken* internalUntilCancelledToken) { 94 | return [asyncOperationWithResultLastingUntilCancelled(internalUntilCancelledToken) finally:^(TOCFuture *completed) { 95 | return @[completed]; 96 | }]; 97 | }; 98 | 99 | NSArray* racingOperations = @[wrappedOperation, timeoutOperation]; 100 | TOCFuture* winner = [racingOperations toc_raceForWinnerLastingUntil:untilCancelledToken]; 101 | return [winner then:^(NSArray* wrappedResult) { 102 | return wrappedResult[0]; 103 | }]; 104 | } 105 | 106 | +(TOCFuture*) futureFromUnlessOperation:(TOCUnlessOperation)asyncCancellableOperation 107 | withTimeout:(NSTimeInterval)timeoutPeriodInSeconds 108 | unless:(TOCCancelToken*)unlessCancelledToken { 109 | TOCInternal_need(asyncCancellableOperation != nil); 110 | TOCInternal_need(timeoutPeriodInSeconds >= 0); 111 | TOCInternal_need(!isnan(timeoutPeriodInSeconds)); 112 | 113 | if (timeoutPeriodInSeconds == 0) { 114 | return [TOCFuture futureWithTimeoutFailure]; 115 | } 116 | if (timeoutPeriodInSeconds == INFINITY) { 117 | return asyncCancellableOperation(unlessCancelledToken); 118 | } 119 | 120 | // start the timeout countdown, making sure it cancels if the caller cancels 121 | TOCFuture* futureTimeout = [TOCFuture futureWithResult:nil 122 | afterDelay:timeoutPeriodInSeconds 123 | unless:unlessCancelledToken]; 124 | 125 | // start the operation, ensuring it cancels if the timeout finishes or is cancelled 126 | TOCFuture* futureOperationResult = asyncCancellableOperation(futureTimeout.cancelledOnCompletionToken); 127 | 128 | // wait for the operation to finish or be cancelled or timeout 129 | return [futureOperationResult finally:^(TOCFuture* completedOperationResult) { 130 | // detect when cancellation was due to timeout, and report appropriately 131 | bool wasCancelled = completedOperationResult.hasFailedWithCancel; 132 | bool wasNotCancelledExternally = !unlessCancelledToken.isAlreadyCancelled; 133 | bool wasTimeout = wasCancelled && wasNotCancelledExternally; 134 | if (wasTimeout) { 135 | return [TOCFuture futureWithTimeoutFailure]; 136 | } 137 | 138 | return completedOperationResult; 139 | }]; 140 | } 141 | 142 | +(TOCFuture*) futureFromUnlessOperation:(TOCUnlessOperation)asyncCancellableOperation 143 | withTimeout:(NSTimeInterval)timeoutPeriodInSeconds { 144 | TOCInternal_need(asyncCancellableOperation != nil); 145 | TOCInternal_need(timeoutPeriodInSeconds >= 0); 146 | TOCInternal_need(!isnan(timeoutPeriodInSeconds)); 147 | 148 | return [self futureFromUnlessOperation:asyncCancellableOperation 149 | withTimeout:timeoutPeriodInSeconds 150 | unless:nil]; 151 | } 152 | 153 | @end 154 | -------------------------------------------------------------------------------- /src/TOCFutureAndSource.m: -------------------------------------------------------------------------------- 1 | #import "TOCFutureAndSource.h" 2 | #import "TOCInternal.h" 3 | #import "TOCTimeout.h" 4 | #import "UnionFind.h" 5 | 6 | static NSObject* getSharedCycleDetectionLock() { 7 | static dispatch_once_t once; 8 | static NSObject* lock = nil; 9 | dispatch_once(&once, ^{ 10 | lock = [NSObject new]; 11 | }); 12 | return lock; 13 | } 14 | 15 | enum StartUnwrapResult { 16 | StartUnwrapResult_CycleDetected, 17 | StartUnwrapResult_Started, 18 | StartUnwrapResult_StartedAndFinished, 19 | StartUnwrapResult_AlreadySet 20 | }; 21 | 22 | @implementation TOCFuture { 23 | /// The future's final result, final failure, or flattening target 24 | @private id _value; 25 | /// Whether or not the future has already been told to flatten or complete or fail 26 | @private bool _hasBeenSet; 27 | /// Whether or not the future has succeeded vs failed 28 | @private bool _ifDoneHasSucceeded; 29 | 30 | /// Callback functionality is delegated to this token 31 | /// It is only cancelled *after* the above state fields have been set 32 | @private TOCCancelToken* _completionToken; 33 | 34 | /// Used for detection of immortal flattening cycles 35 | /// Must hold getSharedCycleDetectionLock() when touching this node 36 | @private UFDisjointSetNode* _cycleNode; 37 | } 38 | 39 | +(TOCFuture *)futureWithResult:(id)resultValue { 40 | if ([resultValue isKindOfClass:[TOCFuture class]]) { 41 | return resultValue; 42 | } 43 | 44 | TOCFuture *future = [TOCFuture new]; 45 | future->_completionToken = TOCCancelToken.cancelledToken; 46 | future->_ifDoneHasSucceeded = true; 47 | future->_value = resultValue; 48 | return future; 49 | } 50 | +(TOCFuture *)futureWithFailure:(id)failureValue { 51 | TOCFuture *future = [TOCFuture new]; 52 | future->_completionToken = TOCCancelToken.cancelledToken; 53 | future->_ifDoneHasSucceeded = false; 54 | future->_value = failureValue; 55 | return future; 56 | } 57 | 58 | +(TOCFuture*) _ForSource_completableFutureWithCompletionToken:(TOCCancelToken*)completionToken { 59 | TOCFuture* future = [TOCFuture new]; 60 | future->_completionToken = completionToken; 61 | return future; 62 | } 63 | 64 | -(UFDisjointSetNode*) _getInitCycleNode { 65 | if (_cycleNode == nil) _cycleNode = [UFDisjointSetNode new]; 66 | return _cycleNode; 67 | } 68 | 69 | -(enum StartUnwrapResult) _ForSource_tryStartUnwrapping:(TOCFuture*)targetFuture { 70 | TOCInternal_need(targetFuture != nil); 71 | 72 | // try set (without completing) 73 | @synchronized(self) { 74 | if (_hasBeenSet) return StartUnwrapResult_AlreadySet; 75 | _hasBeenSet = true; 76 | } 77 | 78 | // optimistically finish without doing cycle stuff 79 | if (!targetFuture.isIncomplete) { 80 | [self _ForSource_forceUnwrapToComplete:targetFuture]; 81 | return StartUnwrapResult_StartedAndFinished; 82 | } 83 | 84 | // look for flattening cycles 85 | @synchronized(getSharedCycleDetectionLock()) { 86 | bool didMerge = [self._getInitCycleNode unionWith:targetFuture._getInitCycleNode]; 87 | if (!didMerge) { 88 | // it's not necessary to clear the cycle nodes, but it frees up some memory 89 | // other futures in the cycle won't get their cycle nodes cleared 90 | // but node chains should be in EXTREMELY shallow trees 91 | // and the nodes can't keep futures alive, so there's no reference cycle 92 | _cycleNode = nil; 93 | targetFuture->_cycleNode = nil; 94 | return StartUnwrapResult_CycleDetected; 95 | } 96 | } 97 | 98 | return StartUnwrapResult_Started; 99 | } 100 | -(void) _ForSource_forceUnwrapToComplete:(TOCFuture*)future { 101 | TOCInternal_force(future != nil); 102 | TOCInternal_force(!future.isIncomplete); 103 | 104 | bool flatteningFutureSucceeded = [self _trySetOrComplete:future->_value 105 | succeeded:future->_ifDoneHasSucceeded 106 | isUnwiring:true]; 107 | TOCInternal_force(flatteningFutureSucceeded); 108 | } 109 | -(bool) _ForSource_tryComplete:(id)finalValue 110 | succeeded:(bool)succeeded { 111 | return [self _trySetOrComplete:finalValue 112 | succeeded:succeeded 113 | isUnwiring:false]; 114 | } 115 | -(bool) _trySetOrComplete:(id)finalValue 116 | succeeded:(bool)succeeded 117 | isUnwiring:(bool)unwiring { 118 | 119 | TOCInternal_need(![finalValue isKindOfClass:[TOCFuture class]]); 120 | 121 | @synchronized(self) { 122 | if (_hasBeenSet && !unwiring) return false; 123 | _hasBeenSet = true; 124 | _value = finalValue; 125 | _ifDoneHasSucceeded = succeeded; 126 | } 127 | @synchronized(getSharedCycleDetectionLock()) { 128 | _cycleNode = nil; 129 | } 130 | return true; 131 | } 132 | 133 | 134 | -(TOCCancelToken*) cancelledOnCompletionToken { 135 | return _completionToken; 136 | } 137 | 138 | -(enum TOCFutureState) state { 139 | enum TOCCancelTokenState completionCancelTokenState = _completionToken.state; 140 | switch (completionCancelTokenState) { 141 | case TOCCancelTokenState_Cancelled: 142 | return _ifDoneHasSucceeded ? TOCFutureState_CompletedWithResult : TOCFutureState_Failed; 143 | 144 | case TOCCancelTokenState_Immortal: 145 | return TOCFutureState_Immortal; 146 | 147 | case TOCCancelTokenState_StillCancellable: 148 | @synchronized(self) { 149 | return _hasBeenSet ? TOCFutureState_Flattening : TOCFutureState_AbleToBeSet; 150 | } 151 | 152 | default: 153 | TOCInternal_unexpectedEnum(completionCancelTokenState); 154 | } 155 | } 156 | -(bool)isIncomplete { 157 | return !_completionToken.isAlreadyCancelled; 158 | } 159 | -(bool)hasResult { 160 | return self.state == TOCFutureState_CompletedWithResult; 161 | } 162 | -(bool)hasFailed { 163 | return self.state == TOCFutureState_Failed; 164 | } 165 | -(bool)hasFailedWithCancel { 166 | return self.hasFailed && [self.forceGetFailure isKindOfClass:[TOCCancelToken class]]; 167 | } 168 | -(bool)hasFailedWithTimeout { 169 | return self.hasFailed && [self.forceGetFailure isKindOfClass:[TOCTimeout class]]; 170 | } 171 | -(id)forceGetResult { 172 | TOCInternal_force(self.hasResult); 173 | return _value; 174 | } 175 | -(id)forceGetFailure { 176 | TOCInternal_force(self.hasFailed); 177 | return _value; 178 | } 179 | 180 | -(void)finallyDo:(TOCFutureFinallyHandler)completionHandler 181 | unless:(TOCCancelToken *)unlessCancelledToken { 182 | TOCInternal_need(completionHandler != nil); 183 | 184 | // Reference cycle is fine. It is not self-sustaining. It gets removed if our source is deallocated. 185 | [_completionToken whenCancelledDo:^{ completionHandler(self); } 186 | unless:unlessCancelledToken]; 187 | } 188 | 189 | -(void)thenDo:(TOCFutureThenHandler)resultHandler 190 | unless:(TOCCancelToken *)unlessCancelledToken { 191 | TOCInternal_need(resultHandler != nil); 192 | 193 | // Reference cycle is fine. It is not self-sustaining. It gets removed if our source is deallocated. 194 | [_completionToken whenCancelledDo:^{ 195 | if (self->_ifDoneHasSucceeded) { 196 | resultHandler(self->_value); 197 | } 198 | } unless:unlessCancelledToken]; 199 | } 200 | 201 | -(void)catchDo:(TOCFutureCatchHandler)failureHandler 202 | unless:(TOCCancelToken *)unlessCancelledToken { 203 | TOCInternal_need(failureHandler != nil); 204 | 205 | // Reference cycle is fine. It is not self-sustaining. It gets removed if our source is deallocated. 206 | [_completionToken whenCancelledDo:^{ 207 | if (!self->_ifDoneHasSucceeded) { 208 | failureHandler(self->_value); 209 | } 210 | } unless:unlessCancelledToken]; 211 | } 212 | 213 | -(TOCFuture *)finally:(TOCFutureFinallyContinuation)completionContinuation 214 | unless:(TOCCancelToken *)unlessCancelledToken { 215 | TOCInternal_need(completionContinuation != nil); 216 | 217 | TOCFutureSource* resultSource = [TOCFutureSource futureSourceUntil:unlessCancelledToken]; 218 | 219 | // Reference cycle is fine. It is not self-sustaining. It gets removed if our source is deallocated. 220 | [_completionToken whenCancelledDo:^{ [resultSource trySetResult:completionContinuation(self)]; } 221 | unless:unlessCancelledToken]; 222 | 223 | return resultSource.future; 224 | } 225 | 226 | -(TOCFuture *)then:(TOCFutureThenContinuation)resultContinuation 227 | unless:(TOCCancelToken *)unlessCancelledToken { 228 | TOCInternal_need(resultContinuation != nil); 229 | 230 | TOCFutureSource* resultSource = [TOCFutureSource futureSourceUntil:unlessCancelledToken]; 231 | 232 | // Reference cycle is fine. It is not self-sustaining. It gets removed if our source is deallocated. 233 | [_completionToken whenCancelledDo:^{ 234 | if (self->_ifDoneHasSucceeded) { 235 | [resultSource trySetResult:resultContinuation(self->_value)]; 236 | } else { 237 | [resultSource trySetFailure:self->_value]; 238 | } 239 | } unless:unlessCancelledToken]; 240 | 241 | return resultSource.future; 242 | } 243 | 244 | -(TOCFuture *)catch:(TOCFutureCatchContinuation)failureContinuation 245 | unless:(TOCCancelToken *)unlessCancelledToken { 246 | TOCInternal_need(failureContinuation != nil); 247 | 248 | TOCFutureSource* resultSource = [TOCFutureSource futureSourceUntil:unlessCancelledToken]; 249 | 250 | // Reference cycle is fine. It is not self-sustaining. It gets removed if our source is deallocated. 251 | [_completionToken whenCancelledDo:^{ 252 | id v = self->_value; 253 | if (!self->_ifDoneHasSucceeded) v = failureContinuation(v); 254 | [resultSource trySetResult:v]; 255 | } unless:unlessCancelledToken]; 256 | 257 | return resultSource.future; 258 | } 259 | 260 | -(NSString*) description { 261 | switch (self.state) { 262 | case TOCFutureState_CompletedWithResult: 263 | return [NSString stringWithFormat:@"Future with Result: %@", _value]; 264 | case TOCFutureState_Failed: 265 | return [NSString stringWithFormat:@"Future with Failure: %@", _value]; 266 | case TOCFutureState_Flattening: 267 | return @"Incomplete Future [Set, Flattening Result]"; 268 | case TOCFutureState_Immortal: 269 | return @"Incomplete Future [Eternal]"; 270 | case TOCFutureState_AbleToBeSet: 271 | return @"Incomplete Future"; 272 | default: 273 | return @"Future in an unrecognized state"; 274 | } 275 | } 276 | -(BOOL)isEqual:(id)object { 277 | if (self == object) return YES; 278 | if (![object isKindOfClass:TOCFuture.class]) return NO; 279 | return [self isEqualToFuture:(TOCFuture*)object]; 280 | } 281 | -(BOOL)isEqualToFuture:(TOCFuture *)other { 282 | if (self == other) return YES; 283 | if (other == nil) return NO; 284 | 285 | enum TOCFutureState state1 = self.state; 286 | enum TOCFutureState state2 = other.state; 287 | if (state1 != state2) return NO; 288 | 289 | switch (state1) { 290 | case TOCFutureState_Immortal: 291 | return YES; 292 | 293 | case TOCFutureState_CompletedWithResult: 294 | case TOCFutureState_Failed: 295 | return _value == other->_value || [_value isEqual:other->_value]; 296 | 297 | case TOCFutureState_Flattening: 298 | case TOCFutureState_AbleToBeSet: 299 | default: 300 | return NO; 301 | } 302 | } 303 | -(NSUInteger)hash { 304 | enum TOCFutureState state = self.state; 305 | switch (state) { 306 | case TOCFutureState_Immortal: 307 | return NSUIntegerMax; 308 | 309 | case TOCFutureState_CompletedWithResult: 310 | return [_value hash]; 311 | 312 | case TOCFutureState_Failed: 313 | return ~[_value hash]; 314 | 315 | case TOCFutureState_Flattening: 316 | case TOCFutureState_AbleToBeSet: 317 | default: 318 | return super.hash; 319 | } 320 | } 321 | 322 | @end 323 | 324 | @implementation TOCFutureSource { 325 | @private TOCCancelTokenSource* _cancelledOnCompletedSource_ClearedOnSet; 326 | } 327 | 328 | @synthesize future; 329 | 330 | -(TOCFutureSource*) init { 331 | self = [super init]; 332 | if (self) { 333 | self->_cancelledOnCompletedSource_ClearedOnSet = [TOCCancelTokenSource new]; 334 | self->future = [TOCFuture _ForSource_completableFutureWithCompletionToken:self->_cancelledOnCompletedSource_ClearedOnSet.token]; 335 | } 336 | return self; 337 | } 338 | 339 | +(TOCFutureSource*) futureSourceUntil:(TOCCancelToken*)untilCancelledToken { 340 | TOCFutureSource* source = [TOCFutureSource new]; 341 | [untilCancelledToken whenCancelledDo:^{ [source trySetFailedWithCancel]; } 342 | unless:source.future.cancelledOnCompletionToken]; 343 | return source; 344 | } 345 | 346 | -(bool) _trySetAndFlattenResult:(TOCFuture*)result { 347 | enum StartUnwrapResult startUnwrapResult = [future _ForSource_tryStartUnwrapping:result]; 348 | 349 | bool didNotSet = startUnwrapResult == StartUnwrapResult_AlreadySet; 350 | if (didNotSet) return false; 351 | 352 | // transfer the only reference to the completion source into a local now, so we don't have to clear it in multiple cases 353 | TOCCancelTokenSource* cancelledOnCompletedSource = _cancelledOnCompletedSource_ClearedOnSet; 354 | _cancelledOnCompletedSource_ClearedOnSet = nil; 355 | 356 | switch (startUnwrapResult) { 357 | case StartUnwrapResult_StartedAndFinished: 358 | // nice: the result was already completed 359 | // cancel completion source to propagate that completion 360 | [cancelledOnCompletedSource cancel]; 361 | break; 362 | 363 | case StartUnwrapResult_CycleDetected: 364 | // this future will never complete 365 | // just allow our completion source to be discarded without being cancelled 366 | // that way its token will become immortal and our future will also become immortal 367 | break; 368 | 369 | case StartUnwrapResult_Started: { 370 | // future will complete later 371 | // keep completion source alive in closure until it can be cancelled 372 | // if result becomes immortal, the closure will be discarded and take the source with it (making our future immortal as well) 373 | [result.cancelledOnCompletionToken whenCancelledDo:^{ 374 | // future must be ready to be accessed before we propagate completion 375 | [self->future _ForSource_forceUnwrapToComplete:result]; 376 | [cancelledOnCompletedSource cancel]; 377 | }]; 378 | break; 379 | 380 | } default: 381 | // already checked StartUnwrapResult_AlreadySet above 382 | TOCInternal_unexpectedEnum(startUnwrapResult); 383 | } 384 | 385 | // this source is set (i.e. it can't be set anymore), even if its future is not completed yet or ever 386 | return true; 387 | } 388 | -(bool) _tryComplete:(id)value succeeded:(bool)succeeded { 389 | bool didSet = [future _ForSource_tryComplete:value succeeded:succeeded]; 390 | if (!didSet) return false; 391 | 392 | [_cancelledOnCompletedSource_ClearedOnSet cancel]; 393 | _cancelledOnCompletedSource_ClearedOnSet = nil; 394 | return true; 395 | } 396 | 397 | -(bool) trySetResult:(id)result { 398 | // automatic flattening 399 | if ([result isKindOfClass:[TOCFuture class]]) { 400 | return [self _trySetAndFlattenResult:result]; 401 | } 402 | 403 | return [self _tryComplete:result succeeded:true]; 404 | } 405 | -(bool) trySetFailure:(id)failure { 406 | return [self _tryComplete:failure succeeded:false]; 407 | } 408 | -(bool) trySetFailedWithCancel { 409 | return [self trySetFailure:TOCCancelToken.cancelledToken]; 410 | } 411 | -(bool) trySetFailedWithTimeout { 412 | return [self trySetFailure:[TOCTimeout new]]; 413 | } 414 | 415 | -(void) forceSetResult:(id)result { 416 | TOCInternal_force([self trySetResult:result]); 417 | } 418 | -(void) forceSetFailure:(id)failure { 419 | TOCInternal_force([self trySetFailure:failure]); 420 | } 421 | -(void) forceSetFailedWithCancel { 422 | TOCInternal_force([self trySetFailedWithCancel]); 423 | } 424 | -(void) forceSetFailedWithTimeout { 425 | TOCInternal_force([self trySetFailedWithTimeout]); 426 | } 427 | 428 | -(NSString*) description { 429 | return [NSString stringWithFormat:@"Future Source: %@", future]; 430 | } 431 | 432 | @end 433 | -------------------------------------------------------------------------------- /src/TOCTimeout.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | /*! 4 | * Instances of TOCTimeout are used to indicate a timeout failure occurred (e.g. by being the failure stored in a TOCFuture). 5 | * 6 | * @see hasFailedWithTimeout 7 | */ 8 | @interface TOCTimeout : NSObject 9 | 10 | @end 11 | -------------------------------------------------------------------------------- /src/TOCTimeout.m: -------------------------------------------------------------------------------- 1 | #import "TOCTimeout.h" 2 | 3 | @implementation TOCTimeout 4 | 5 | -(NSString *)description { 6 | return @"Timeout"; 7 | } 8 | 9 | @end 10 | -------------------------------------------------------------------------------- /src/TOCTypeDefs.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @class TOCFuture; 4 | @class TOCCancelToken; 5 | 6 | /*! 7 | * A block that starts an asynchronous operation whose result must be terminated when the given "until"-type cancel token is cancelled. 8 | * 9 | * @param untilCancelledToken Determines the lifetime of the operation's result. 10 | * The result must be cleaned up when the token is cancelled, even if the operation has completed. 11 | * In cases where the operation hasn't finished yet when the token is cancelled, it should clean up as soon as possible and cancel its result. 12 | * 13 | * @result A future representing the eventual result of the asynchronous operation. 14 | * Must be cleaned up when the untilCancelledToken is cancelled. 15 | * 16 | * @discussion The result MUST be terminated and cleaned up when untilCancelledToken is cancelled, EVEN IF the operation has already finished. 17 | * 18 | * If the operation has not finished yet, cancelling the untilCancelledToken should immediately cancel it and its result. 19 | */ 20 | typedef TOCFuture* (^TOCUntilOperation)(TOCCancelToken* untilCancelledToken); 21 | 22 | /*! 23 | * A block that starts an asynchronous operation and returns a future that will contain its result unless the operation is cancelled. 24 | * 25 | * @param unlessCancelledToken Cancelling this token before the operation has completed should immediately cancel the operation. 26 | * 27 | * @result A future representing the eventual result of the asynchronous operation, or a cancellation failure if the operation was cancelled. 28 | * 29 | * @discussion Cancelling the given token, after the operation has completed, should have no effect. 30 | * 31 | * The future returned by the operation should immediately transition to the cancelled state when the operation is cancelled. 32 | */ 33 | typedef TOCFuture* (^TOCUnlessOperation)(TOCCancelToken* unlessCancelledToken); 34 | -------------------------------------------------------------------------------- /src/TwistedOakCollapsingFutures.h: -------------------------------------------------------------------------------- 1 | #warning Use #import "CollapsingFutures.h" instead of deprecated #import "TwistedOakCollapsingFutures.h" 2 | 3 | #import "CollapsingFutures.h" 4 | -------------------------------------------------------------------------------- /src/internal/TOCInternal.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "TOCInternal_Array+Functional.h" 3 | #import "TOCInternal_BlockObject.h" 4 | #import "TOCInternal_Racer.h" 5 | #import "TOCInternal_OnDeallocObject.h" 6 | 7 | #define TOCInternal_need(expr) \ 8 | if (!(expr)) \ 9 | @throw([NSException exceptionWithName:NSInvalidArgumentException \ 10 | reason:[NSString stringWithFormat:@"A precondition ( require(%@) ) was not satisfied. ", (@#expr)] \ 11 | userInfo:nil]) 12 | 13 | #define TOCInternal_force(expr) \ 14 | if (!(expr)) \ 15 | @throw([NSException exceptionWithName:NSInternalInconsistencyException \ 16 | reason:[NSString stringWithFormat:@"A forced operation ( force(%@) ) failed to succeed.", (@#expr)] \ 17 | userInfo:nil]) 18 | 19 | #define TOCInternal_unexpectedEnum(expr) \ 20 | @throw([NSException exceptionWithName:NSInternalInconsistencyException \ 21 | reason:[NSString stringWithFormat:@"An unexpected enum value ( %@ = %d ) was encountered.", (@#expr), expr] \ 22 | userInfo:nil]) 23 | -------------------------------------------------------------------------------- /src/internal/TOCInternal_Array+Functional.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | typedef id (^TOCInternal_Projection)(id value); 4 | typedef bool (^TOCInternal_Predicate)(id value); 5 | 6 | @interface NSArray (TOCInternal_Functional) 7 | 8 | -(NSArray*) map:(TOCInternal_Projection)projection; 9 | 10 | -(NSArray*) where:(TOCInternal_Predicate)predicate; 11 | 12 | -(bool) allItemsSatisfy:(TOCInternal_Predicate)predicate; 13 | 14 | -(bool) allItemsAreKindOfClass:(Class)classInstance; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /src/internal/TOCInternal_Array+Functional.m: -------------------------------------------------------------------------------- 1 | #import "TOCInternal.h" 2 | 3 | @implementation NSArray (TOCInternal_Functional) 4 | 5 | -(NSArray*) map:(TOCInternal_Projection)projection { 6 | TOCInternal_need(projection != nil); 7 | 8 | NSMutableArray* results = [NSMutableArray arrayWithCapacity:self.count]; 9 | for (id item in self) { 10 | id projectedItem = projection(item); 11 | TOCInternal_need(projectedItem != nil); 12 | [results addObject:projectedItem]; 13 | } 14 | 15 | return [results copy]; // remove mutability 16 | } 17 | 18 | -(NSArray*) where:(TOCInternal_Predicate)predicate { 19 | TOCInternal_need(predicate != nil); 20 | 21 | NSMutableArray* results = [NSMutableArray arrayWithCapacity:self.count]; 22 | for (id item in self) { 23 | if (predicate(item)) { 24 | [results addObject:item]; 25 | } 26 | } 27 | 28 | return [results copy]; // remove mutability 29 | } 30 | 31 | -(bool) allItemsSatisfy:(TOCInternal_Predicate)predicate { 32 | TOCInternal_need(predicate != nil); 33 | 34 | for (id item in self) { 35 | if (!predicate(item)) { 36 | return false; 37 | } 38 | } 39 | return true; 40 | } 41 | 42 | -(bool) allItemsAreKindOfClass:(Class)classInstance { 43 | TOCInternal_need(classInstance != nil); 44 | return [self allItemsSatisfy:^bool(id value) { return [value isKindOfClass:classInstance]; }]; 45 | } 46 | 47 | @end 48 | -------------------------------------------------------------------------------- /src/internal/TOCInternal_BlockObject.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface TOCInternal_BlockObject : NSObject 4 | +(TOCInternal_BlockObject*) voidBlock:(void(^)(void))block; 5 | -(void)run; 6 | -(SEL)runSelector; 7 | +(void) performBlock:(void(^)(void))block onThread:(NSThread*)thread; 8 | +(void) performBlockOnNewThread:(void(^)(void))block; 9 | @end 10 | -------------------------------------------------------------------------------- /src/internal/TOCInternal_BlockObject.m: -------------------------------------------------------------------------------- 1 | #import "TOCInternal_BlockObject.h" 2 | #import "TOCInternal.h" 3 | 4 | @implementation TOCInternal_BlockObject { 5 | @private void (^block)(void); 6 | } 7 | 8 | +(TOCInternal_BlockObject*) voidBlock:(void(^)(void))block { 9 | TOCInternal_need(block != nil); 10 | 11 | TOCInternal_BlockObject* b = [TOCInternal_BlockObject new]; 12 | b->block = block; 13 | return b; 14 | } 15 | -(void)run { 16 | if (block) { 17 | block(); 18 | } 19 | } 20 | -(SEL)runSelector { 21 | return @selector(run); 22 | } 23 | 24 | +(void) performBlock:(void(^)(void))block onThread:(NSThread*)thread { 25 | TOCInternal_need(block != nil); 26 | 27 | if (thread == NSThread.currentThread) { 28 | block(); 29 | return; 30 | } 31 | 32 | TOCInternal_BlockObject* blockObject = [TOCInternal_BlockObject voidBlock:block]; 33 | [blockObject performSelector:blockObject.runSelector 34 | onThread:thread 35 | withObject:block 36 | waitUntilDone:NO]; 37 | } 38 | +(void) performBlockOnNewThread:(void(^)(void))block { 39 | TOCInternal_need(block != nil); 40 | 41 | TOCInternal_BlockObject* blockObject = [TOCInternal_BlockObject voidBlock:block]; 42 | [NSThread detachNewThreadSelector:blockObject.runSelector toTarget:blockObject withObject:nil]; 43 | } 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /src/internal/TOCInternal_OnDeallocObject.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface TOCInternal_OnDeallocObject : NSObject 4 | 5 | +(TOCInternal_OnDeallocObject*) onDeallocDo:(void(^)(void))block; 6 | 7 | -(void)poke; 8 | 9 | -(void)cancelDeallocAction; 10 | 11 | @end 12 | -------------------------------------------------------------------------------- /src/internal/TOCInternal_OnDeallocObject.m: -------------------------------------------------------------------------------- 1 | #import "TOCInternal_OnDeallocObject.h" 2 | #import "TOCInternal.h" 3 | 4 | @implementation TOCInternal_OnDeallocObject { 5 | @private void (^block)(void); 6 | } 7 | 8 | +(TOCInternal_OnDeallocObject*) onDeallocDo:(void(^)(void))block { 9 | TOCInternal_need(block != nil); 10 | TOCInternal_OnDeallocObject* obj = [TOCInternal_OnDeallocObject new]; 11 | obj->block = block; 12 | return obj; 13 | } 14 | 15 | -(void)poke { 16 | // this method is called from inside closures that want to keep a reference to this object 17 | } 18 | 19 | -(void)cancelDeallocAction { 20 | block = nil; 21 | } 22 | 23 | -(void)dealloc { 24 | if (block != nil) block(); 25 | } 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /src/internal/TOCInternal_Racer.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "TOCCancelTokenAndSource.h" 3 | #import "TOCFutureAndSource.h" 4 | #import "TOCTypeDefs.h" 5 | 6 | @interface TOCInternal_Racer : NSObject 7 | 8 | @property (readonly,nonatomic) TOCFuture* futureResult; 9 | @property (readonly,nonatomic) TOCCancelTokenSource* canceller; 10 | 11 | +(TOCFuture*) asyncRace:(NSArray*)starters 12 | until:(TOCCancelToken*)untilCancelledToken; 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /src/internal/TOCInternal_Racer.m: -------------------------------------------------------------------------------- 1 | #import "TOCInternal.h" 2 | #import "TOCFuture+MoreContinuations.h" 3 | #include 4 | 5 | @implementation TOCInternal_Racer 6 | 7 | @synthesize canceller, futureResult; 8 | 9 | +(TOCInternal_Racer*) racerStartedFrom:(TOCUntilOperation)starter 10 | until:(TOCCancelToken*)untilCancelledToken { 11 | TOCInternal_need(starter != nil); 12 | 13 | TOCInternal_Racer* racer = [TOCInternal_Racer new]; 14 | 15 | racer->canceller = [TOCCancelTokenSource cancelTokenSourceUntil:untilCancelledToken]; 16 | racer->futureResult = starter(racer.canceller.token); 17 | 18 | return racer; 19 | } 20 | 21 | +(TOCFuture*) asyncRace:(NSArray*)starters 22 | until:(TOCCancelToken*)untilCancelledToken { 23 | 24 | // start all operations (as racers that can be individually cancelled after-the-fact) 25 | NSArray* racers = [starters map:^(id starter) { return [TOCInternal_Racer racerStartedFrom:starter 26 | until:untilCancelledToken]; }]; 27 | 28 | // make a podium for the winner, assuming the race isn't called off 29 | TOCFutureSource* futureWinningRacerSource = [TOCFutureSource futureSourceUntil:untilCancelledToken]; 30 | 31 | // tell each racer how to get on the podium (or how to be a failure) 32 | __block int failedRacerCount = 0; 33 | TOCInternal_need(racers.count <= INT_MAX); 34 | for (TOCInternal_Racer* racer in racers) { 35 | [racer.futureResult finallyDo:^(TOCFuture *completed) { 36 | if (completed.hasResult) { 37 | // winner? 38 | [futureWinningRacerSource trySetResult:racer]; 39 | } else if (OSAtomicIncrement32(&failedRacerCount) == (int)racers.count) { 40 | // prefer to fail with a cancellation over failing with a list of cancellations 41 | if (untilCancelledToken.isAlreadyCancelled) return; 42 | 43 | // everyone is a failure, thus so are we 44 | NSArray* allFailures = [racers map:^(TOCInternal_Racer* r) { return r.futureResult.forceGetFailure; }]; 45 | [futureWinningRacerSource trySetFailure:allFailures]; 46 | } 47 | } unless:untilCancelledToken]; 48 | } 49 | 50 | // once there's a winner, cancel the other racers 51 | [futureWinningRacerSource.future thenDo:^(TOCInternal_Racer* winningRacer) { 52 | for (TOCInternal_Racer* racer in racers) { 53 | if (racer != winningRacer) { 54 | [racer.canceller cancel]; 55 | } 56 | } 57 | }]; 58 | 59 | // get the winning racer's result 60 | TOCFuture* futureWinnerResult = [futureWinningRacerSource.future then:^id(TOCInternal_Racer* winningRacer) { return winningRacer.futureResult; }]; 61 | 62 | return futureWinnerResult; 63 | } 64 | 65 | @end 66 | -------------------------------------------------------------------------------- /test/Testing.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "TOCFutureAndSource.h" 4 | #import 5 | 6 | vm_size_t peekAllocatedMemoryInBytes(void); 7 | bool testPassesConcurrently_helper(bool (^check)(void), NSTimeInterval delay); 8 | bool testCompletesConcurrently_helper(TOCFuture* future, NSTimeInterval timeout); 9 | bool futureHasResult(TOCFuture* future, id result); 10 | bool futureHasFailure(TOCFuture* future, id failure); 11 | int testTargetHits; 12 | bool equals(id obj1, id obj2); 13 | 14 | #define test(expressionExpectedToBeTrue) XCTAssertTrue(expressionExpectedToBeTrue, @"") 15 | #define testEq(value1, value2) test(equals(value1, value2)) 16 | 17 | #define testThrows(expressionExpectedToThrow) XCTAssertThrows(expressionExpectedToThrow, @"") 18 | #define testCompletesConcurrently(future) test(testCompletesConcurrently_helper(future, 2.0)) 19 | #define testDoesNotCompleteConcurrently(future) test(!testCompletesConcurrently_helper(future, 0.01)) 20 | #define testChurnUntil(condition) \ 21 | for (int _churnCounter_xxx = 0; _churnCounter_xxx < 5 && !(condition); _churnCounter_xxx++) { \ 22 | [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]]; \ 23 | } \ 24 | test(condition) 25 | 26 | #define testHitsTarget(expression) testTargetHits = 0; \ 27 | expression; \ 28 | test(testTargetHits == 1) 29 | #define testDoesNotHitTarget(expression) testTargetHits = 0; \ 30 | expression; \ 31 | test(testTargetHits == 0) 32 | #define hitTarget (testTargetHits++) 33 | 34 | #define testFutureHasResult(future, result) test(futureHasResult(future, result)) 35 | #define testFutureHasFailure(future, failure) test(futureHasFailure(future, failure)) 36 | 37 | @class DeallocToken; 38 | @interface DeallocCounter : NSObject 39 | @property (atomic) NSUInteger lostTokenCount; 40 | -(DeallocToken*) makeToken; 41 | @end 42 | @interface DeallocToken : NSObject 43 | +(DeallocToken*) token:(DeallocCounter*)parent; 44 | -(void) poke; 45 | @end 46 | -------------------------------------------------------------------------------- /test/Testing.m: -------------------------------------------------------------------------------- 1 | #import "Testing.h" 2 | 3 | int testTargetHits = 0; 4 | bool equals(id obj1, id obj2) { 5 | return obj1 == obj2 || [obj1 isEqual:obj2]; 6 | } 7 | 8 | vm_size_t peekAllocatedMemoryInBytes(void) { 9 | struct task_basic_info info; 10 | mach_msg_type_number_t size = sizeof(info); 11 | kern_return_t kerr = task_info(mach_task_self(), 12 | TASK_BASIC_INFO, 13 | (task_info_t)&info, 14 | &size); 15 | assert(kerr == KERN_SUCCESS); 16 | return info.resident_size; 17 | } 18 | bool futureHasResult(TOCFuture* future, id result) { 19 | return future.hasResult && equals(result, future.forceGetResult); 20 | } 21 | bool futureHasFailure(TOCFuture* future, id failure) { 22 | return future.hasFailed && equals(failure, future.forceGetFailure); 23 | } 24 | bool testPassesConcurrently_helper(bool (^check)(void), NSTimeInterval delay) { 25 | NSTimeInterval t = [[NSProcessInfo processInfo] systemUptime] + delay; 26 | while ([[NSProcessInfo processInfo] systemUptime] < t && !check()) { 27 | } 28 | return check(); 29 | } 30 | bool testCompletesConcurrently_helper(TOCFuture* future, NSTimeInterval delay) { 31 | return testPassesConcurrently_helper(^bool{ return !future.isIncomplete; }, delay); 32 | } 33 | 34 | @implementation DeallocCounter 35 | @synthesize lostTokenCount; 36 | -(DeallocToken*) makeToken { 37 | return [DeallocToken token:self]; 38 | } 39 | @end 40 | 41 | @implementation DeallocToken { 42 | @private DeallocCounter* parent; 43 | } 44 | +(DeallocToken*) token:(DeallocCounter*)parent { 45 | DeallocToken* token = [DeallocToken new]; 46 | token->parent = parent; 47 | return token; 48 | } 49 | -(void) dealloc { 50 | parent.lostTokenCount += 1; 51 | } 52 | -(void) poke { 53 | // tee hee! 54 | } 55 | @end 56 | -------------------------------------------------------------------------------- /test/src/TOCCancelToken+MoreConstructorsTest.m: -------------------------------------------------------------------------------- 1 | #import "Testing.h" 2 | #import "TOCCancelToken+MoreConstructors.h" 3 | 4 | @interface TOCCancelToken_MoreConstructorsTest : XCTestCase 5 | @end 6 | 7 | @implementation TOCCancelToken_MoreConstructorsTest 8 | 9 | -(void)testMatchFirstToCancel_SpecialCasesAreOptimized { 10 | TOCCancelTokenSource* s = [TOCCancelTokenSource new]; 11 | test([TOCCancelToken matchFirstToCancelBetween:TOCCancelToken.cancelledToken and:TOCCancelToken.cancelledToken].isAlreadyCancelled); 12 | test([TOCCancelToken matchLastToCancelBetween:TOCCancelToken.immortalToken and:TOCCancelToken.immortalToken].state == TOCCancelTokenState_Immortal); 13 | test([TOCCancelToken matchFirstToCancelBetween:s.token and:TOCCancelToken.cancelledToken].isAlreadyCancelled); 14 | test([TOCCancelToken matchFirstToCancelBetween:s.token and:TOCCancelToken.immortalToken] == s.token); 15 | test([TOCCancelToken matchFirstToCancelBetween:TOCCancelToken.cancelledToken and:s.token].isAlreadyCancelled); 16 | test([TOCCancelToken matchFirstToCancelBetween:TOCCancelToken.immortalToken and:s.token] == s.token); 17 | test([TOCCancelToken matchFirstToCancelBetween:s.token and:s.token] == s.token); 18 | } 19 | -(void)testMatchFirstToCancel_Cancel { 20 | TOCCancelTokenSource* s1 = [TOCCancelTokenSource new]; 21 | TOCCancelTokenSource* s2 = [TOCCancelTokenSource new]; 22 | TOCCancelToken* c1 = [TOCCancelToken matchFirstToCancelBetween:s1.token and:s2.token]; 23 | TOCCancelToken* c2 = [TOCCancelToken matchFirstToCancelBetween:s1.token and:s2.token]; 24 | 25 | test(c1.state == TOCCancelTokenState_StillCancellable); 26 | test(c2.state == TOCCancelTokenState_StillCancellable); 27 | 28 | [s1 cancel]; 29 | 30 | test(c1.state == TOCCancelTokenState_Cancelled); 31 | test(c2.state == TOCCancelTokenState_Cancelled); 32 | } 33 | -(void)testMatchFirstToCancel_Immortal { 34 | TOCCancelToken* c1; 35 | TOCCancelToken* c2; 36 | @autoreleasepool { 37 | TOCCancelTokenSource* s1 = [TOCCancelTokenSource new]; 38 | @autoreleasepool { 39 | TOCCancelTokenSource* s2 = [TOCCancelTokenSource new]; 40 | c1 = [TOCCancelToken matchFirstToCancelBetween:s1.token and:s2.token]; 41 | c2 = [TOCCancelToken matchFirstToCancelBetween:s1.token and:s2.token]; 42 | 43 | test(c1.state == TOCCancelTokenState_StillCancellable); 44 | test(c2.state == TOCCancelTokenState_StillCancellable); 45 | } 46 | 47 | test(c1.state == TOCCancelTokenState_StillCancellable); 48 | test(c2.state == TOCCancelTokenState_StillCancellable); 49 | } 50 | 51 | test(c1.state == TOCCancelTokenState_Immortal); 52 | test(c2.state == TOCCancelTokenState_Immortal); 53 | } 54 | 55 | -(void)testMatchLastToCancel_SpecialCasesAreOptimized { 56 | TOCCancelTokenSource* s = [TOCCancelTokenSource new]; 57 | test([TOCCancelToken matchLastToCancelBetween:TOCCancelToken.cancelledToken and:TOCCancelToken.cancelledToken].isAlreadyCancelled); 58 | test([TOCCancelToken matchLastToCancelBetween:TOCCancelToken.immortalToken and:TOCCancelToken.immortalToken].state == TOCCancelTokenState_Immortal); 59 | test([TOCCancelToken matchLastToCancelBetween:s.token and:TOCCancelToken.cancelledToken] == s.token); 60 | test([TOCCancelToken matchLastToCancelBetween:s.token and:TOCCancelToken.immortalToken].state == TOCCancelTokenState_Immortal); 61 | test([TOCCancelToken matchLastToCancelBetween:TOCCancelToken.cancelledToken and:s.token] == s.token); 62 | test([TOCCancelToken matchLastToCancelBetween:TOCCancelToken.immortalToken and:s.token].state == TOCCancelTokenState_Immortal); 63 | test([TOCCancelToken matchLastToCancelBetween:s.token and:s.token] == s.token); 64 | } 65 | -(void)testMatchLastToCancel_Cancel { 66 | TOCCancelTokenSource* s1 = [TOCCancelTokenSource new]; 67 | TOCCancelTokenSource* s2 = [TOCCancelTokenSource new]; 68 | TOCCancelToken* c1 = [TOCCancelToken matchLastToCancelBetween:s1.token and:s2.token]; 69 | TOCCancelToken* c2 = [TOCCancelToken matchLastToCancelBetween:s1.token and:s2.token]; 70 | 71 | test(c1.state == TOCCancelTokenState_StillCancellable); 72 | test(c2.state == TOCCancelTokenState_StillCancellable); 73 | 74 | [s1 cancel]; 75 | 76 | test(c1.state == TOCCancelTokenState_StillCancellable); 77 | test(c2.state == TOCCancelTokenState_StillCancellable); 78 | 79 | [s2 cancel]; 80 | 81 | test(c1.state == TOCCancelTokenState_Cancelled); 82 | test(c2.state == TOCCancelTokenState_Cancelled); 83 | } 84 | -(void)testMatchLastToCancel_Immortal { 85 | TOCCancelToken* c1; 86 | TOCCancelToken* c2; 87 | @autoreleasepool { 88 | TOCCancelTokenSource* s1 = [TOCCancelTokenSource new]; 89 | @autoreleasepool { 90 | TOCCancelTokenSource* s2 = [TOCCancelTokenSource new]; 91 | c1 = [TOCCancelToken matchLastToCancelBetween:s1.token and:s2.token]; 92 | c2 = [TOCCancelToken matchLastToCancelBetween:s1.token and:s2.token]; 93 | 94 | test(c1.state == TOCCancelTokenState_StillCancellable); 95 | test(c2.state == TOCCancelTokenState_StillCancellable); 96 | } 97 | 98 | test(c1.state == TOCCancelTokenState_Immortal); 99 | test(c2.state == TOCCancelTokenState_Immortal); 100 | } 101 | 102 | test(c1.state == TOCCancelTokenState_Immortal); 103 | test(c2.state == TOCCancelTokenState_Immortal); 104 | } 105 | 106 | @end 107 | -------------------------------------------------------------------------------- /test/src/TOCCancelTokenTest.m: -------------------------------------------------------------------------------- 1 | #import "Testing.h" 2 | #import "CollapsingFutures.h" 3 | #import "TOCInternal_BlockObject.h" 4 | #import 5 | 6 | @interface TOCCancelTokenTest : XCTestCase 7 | @end 8 | 9 | @implementation TOCCancelTokenTest 10 | 11 | -(void) testCancelTokenSourceCancel { 12 | TOCCancelTokenSource* s = [TOCCancelTokenSource new]; 13 | TOCCancelToken* c = s.token; 14 | test(c.canStillBeCancelled); 15 | test(!c.isAlreadyCancelled); 16 | 17 | [s cancel]; 18 | test(c.isAlreadyCancelled); 19 | test(!c.canStillBeCancelled); 20 | 21 | [s cancel]; 22 | test(c.isAlreadyCancelled); 23 | test(!c.canStillBeCancelled); 24 | test(c.state == TOCCancelTokenState_Cancelled); 25 | } 26 | -(void) testCancelTokenSourceTryCancel { 27 | TOCCancelTokenSource* s = [TOCCancelTokenSource new]; 28 | TOCCancelToken* c = s.token; 29 | test(c.canStillBeCancelled); 30 | test(!c.isAlreadyCancelled); 31 | test(c.state == TOCCancelTokenState_StillCancellable); 32 | 33 | test([s tryCancel]); 34 | test(c.isAlreadyCancelled); 35 | test(!c.canStillBeCancelled); 36 | test(c.state == TOCCancelTokenState_Cancelled); 37 | 38 | test(![s tryCancel]); 39 | test(c.isAlreadyCancelled); 40 | test(!c.canStillBeCancelled); 41 | test(c.state == TOCCancelTokenState_Cancelled); 42 | } 43 | -(void) testImmortalCancelToken { 44 | TOCCancelToken* c = TOCCancelToken.immortalToken; 45 | test(!c.canStillBeCancelled); 46 | test(!c.isAlreadyCancelled); 47 | test(c.state == TOCCancelTokenState_Immortal); 48 | 49 | testDoesNotHitTarget([c whenCancelledDo:^{ 50 | hitTarget; 51 | }]); 52 | } 53 | -(void) testCancelledCancelToken { 54 | TOCCancelToken* c = TOCCancelToken.cancelledToken; 55 | test(!c.canStillBeCancelled); 56 | test(c.isAlreadyCancelled); 57 | test(c.state == TOCCancelTokenState_Cancelled); 58 | 59 | __block bool hit = false; 60 | [c whenCancelledDo:^{ 61 | hit = true; 62 | }]; 63 | test(hit); 64 | } 65 | -(void) testDeallocSourceResultsInImmortalTokenAndDiscardedCallbacks { 66 | DeallocCounter* d = [DeallocCounter new]; 67 | 68 | TOCCancelToken* c = nil; 69 | @autoreleasepool { 70 | DeallocToken* dToken = [d makeToken]; 71 | 72 | TOCCancelTokenSource* s = [TOCCancelTokenSource new]; 73 | c = s.token; 74 | 75 | // retain dToken in closure held by token held by outside, so it can't dealloc unless closure deallocs 76 | [c whenCancelledDo:^{ 77 | test(false); 78 | [dToken poke]; 79 | }]; 80 | test(d.lostTokenCount == 0); 81 | } 82 | 83 | test(d.lostTokenCount == 1); 84 | test(c.state == TOCCancelTokenState_Immortal); 85 | } 86 | -(void) testConditionalCancelCallback { 87 | TOCCancelTokenSource* s = [TOCCancelTokenSource new]; 88 | TOCCancelTokenSource* u = [TOCCancelTokenSource new]; 89 | 90 | __block int hit1 = 0; 91 | __block int hit2 = 0; 92 | [s.token whenCancelledDo:^{ 93 | hit1++; 94 | } unless:u.token]; 95 | [u.token whenCancelledDo:^{ 96 | hit2++; 97 | } unless:s.token]; 98 | 99 | test(hit1 == 0 && hit2 == 0); 100 | [s cancel]; 101 | test(hit1 == 1); 102 | test(hit2 == 0); 103 | [u cancel]; 104 | test(hit1 == 1); 105 | test(hit2 == 0); 106 | } 107 | 108 | -(void) testConditionalCancelCallbackCanDeallocOnImmortalize { 109 | DeallocCounter* d = [DeallocCounter new]; 110 | 111 | TOCCancelToken* c1; 112 | TOCCancelToken* c2; 113 | @autoreleasepool { 114 | DeallocToken* dToken1 = [d makeToken]; 115 | DeallocToken* dToken2 = [d makeToken]; 116 | 117 | TOCCancelTokenSource* s1 = [TOCCancelTokenSource new]; 118 | TOCCancelTokenSource* s2 = [TOCCancelTokenSource new]; 119 | c1 = s1.token; 120 | c2 = s2.token; 121 | 122 | [c1 whenCancelledDo:^{ 123 | test(false); 124 | [dToken1 poke]; 125 | } unless:c2]; 126 | [c2 whenCancelledDo:^{ 127 | test(false); 128 | [dToken2 poke]; 129 | } unless:c1]; 130 | test(d.lostTokenCount == 0); 131 | } 132 | 133 | test(d.lostTokenCount == 2); 134 | test(c1.state == TOCCancelTokenState_Immortal); 135 | test(c2.state == TOCCancelTokenState_Immortal); 136 | } 137 | -(void) testConditionalCancelCallbackCanDeallocOnCancel { 138 | DeallocCounter* d = [DeallocCounter new]; 139 | 140 | TOCCancelToken* c1; 141 | TOCCancelToken* c2; 142 | @autoreleasepool { 143 | DeallocToken* dToken1 = [d makeToken]; 144 | DeallocToken* dToken2 = [d makeToken]; 145 | 146 | TOCCancelTokenSource* s1 = [TOCCancelTokenSource new]; 147 | TOCCancelTokenSource* s2 = [TOCCancelTokenSource new]; 148 | c1 = s1.token; 149 | c2 = s2.token; 150 | 151 | [c1 whenCancelledDo:^{ 152 | [dToken1 poke]; 153 | } unless:c2]; 154 | [c2 whenCancelledDo:^{ 155 | [dToken2 poke]; 156 | } unless:c1]; 157 | test(d.lostTokenCount == 0); 158 | 159 | [s1 cancel]; 160 | [s2 cancel]; 161 | } 162 | 163 | test(d.lostTokenCount == 2); 164 | test(c1.isAlreadyCancelled); 165 | test(c2.isAlreadyCancelled); 166 | } 167 | -(void) testDealloc_AfterCancel { 168 | DeallocCounter* d = [DeallocCounter new]; 169 | TOCCancelTokenSource* s; 170 | @autoreleasepool { 171 | s = [TOCCancelTokenSource new]; 172 | DeallocToken* dToken = [d makeToken]; 173 | [s.token whenCancelledDo:^{ 174 | [dToken poke]; 175 | }]; 176 | [s cancel]; 177 | } 178 | test(d.lostTokenCount == 1); 179 | test(s != nil); 180 | } 181 | 182 | -(void) testConditionalCancelCallbackCanDeallocOnHalfCancelHalfImmortalize { 183 | DeallocCounter* d = [DeallocCounter new]; 184 | 185 | TOCCancelToken* c1; 186 | TOCCancelToken* c2; 187 | @autoreleasepool { 188 | DeallocToken* dToken1 = [d makeToken]; 189 | DeallocToken* dToken2 = [d makeToken]; 190 | 191 | TOCCancelTokenSource* s1 = [TOCCancelTokenSource new]; 192 | TOCCancelTokenSource* s2 = [TOCCancelTokenSource new]; 193 | c1 = s1.token; 194 | c2 = s2.token; 195 | 196 | [c1 whenCancelledDo:^{ 197 | [dToken1 poke]; 198 | } unless:c2]; 199 | [c2 whenCancelledDo:^{ 200 | test(false); 201 | [dToken2 poke]; 202 | } unless:c1]; 203 | test(d.lostTokenCount == 0); 204 | 205 | [s1 cancel]; 206 | } 207 | 208 | test(d.lostTokenCount == 2); 209 | test(c1.isAlreadyCancelled); 210 | test(!c2.isAlreadyCancelled); 211 | test(!c2.canStillBeCancelled); 212 | } 213 | 214 | -(void) testSelfOnCancelledUnlesCancelledIsConsistentBeforeAndAfter { 215 | TOCCancelTokenSource* s = [TOCCancelTokenSource new]; 216 | 217 | __block int hit1 = 0; 218 | [s.token whenCancelledDo:^{ 219 | hit1++; 220 | } unless:s.token]; 221 | 222 | [s cancel]; 223 | 224 | __block int hit2 = 0; 225 | [s.token whenCancelledDo:^{ 226 | hit2++; 227 | } unless:s.token]; 228 | 229 | test(hit1 <= 1); 230 | test(hit2 <= 1); 231 | test(hit1 == hit2); 232 | } 233 | 234 | -(void) testOnCancelledUnlesCancelledWhenBothCancelledDoesNotRunCallback { 235 | __block int hit1 = 0; 236 | [TOCCancelToken.cancelledToken whenCancelledDo:^{ 237 | hit1++; 238 | } unless:TOCCancelToken.cancelledToken]; 239 | test(hit1 == 0); 240 | } 241 | 242 | -(void) testCancelTokenSourceUntil { 243 | TOCCancelTokenSource* s = [TOCCancelTokenSource new]; 244 | TOCCancelTokenSource* d = [TOCCancelTokenSource cancelTokenSourceUntil:s.token]; 245 | test(s.token.state == TOCCancelTokenState_StillCancellable); 246 | test(d.token.state == TOCCancelTokenState_StillCancellable); 247 | 248 | [s cancel]; 249 | test(s.token.state == TOCCancelTokenState_Cancelled); 250 | test(d.token.state == TOCCancelTokenState_Cancelled); 251 | } 252 | -(void) testCancelTokenSourceUntil_Reverse { 253 | TOCCancelTokenSource* s = [TOCCancelTokenSource new]; 254 | TOCCancelTokenSource* d = [TOCCancelTokenSource cancelTokenSourceUntil:s.token]; 255 | test(s.token.state == TOCCancelTokenState_StillCancellable); 256 | test(d.token.state == TOCCancelTokenState_StillCancellable); 257 | 258 | [d cancel]; 259 | test(s.token.state == TOCCancelTokenState_StillCancellable); 260 | test(d.token.state == TOCCancelTokenState_Cancelled); 261 | } 262 | -(void) testCancelTokenSourceUntil_CleansUpEagerly { 263 | TOCCancelTokenSource* s = [TOCCancelTokenSource new]; 264 | vm_size_t memoryBefore = peekAllocatedMemoryInBytes(); 265 | int repeats = 10000; 266 | vm_size_t slack = 50000; 267 | for (int i = 0; i < repeats; i++) { 268 | @autoreleasepool { 269 | TOCCancelTokenSource* d = [TOCCancelTokenSource cancelTokenSourceUntil:s.token]; 270 | [d cancel]; 271 | } 272 | } 273 | vm_size_t memoryAfter = peekAllocatedMemoryInBytes(); 274 | bool likelyIsNotCleaningUp = memoryAfter > memoryBefore + slack; 275 | test(!likelyIsNotCleaningUp); 276 | } 277 | 278 | -(void) testFutureSourceUntil { 279 | TOCCancelTokenSource* s = [TOCCancelTokenSource new]; 280 | TOCFutureSource* d = [TOCFutureSource futureSourceUntil:s.token]; 281 | test(s.token.state == TOCCancelTokenState_StillCancellable); 282 | test(d.future.state == TOCFutureState_AbleToBeSet); 283 | 284 | [s cancel]; 285 | test(s.token.state == TOCCancelTokenState_Cancelled); 286 | test(d.future.hasFailedWithCancel); 287 | } 288 | -(void) testFutureSourceUntil_Reverse { 289 | TOCCancelTokenSource* s = [TOCCancelTokenSource new]; 290 | TOCFutureSource* d = [TOCFutureSource futureSourceUntil:s.token]; 291 | test(s.token.state == TOCCancelTokenState_StillCancellable); 292 | test(d.future.state == TOCFutureState_AbleToBeSet); 293 | 294 | [d trySetResult:nil]; 295 | test(s.token.state == TOCCancelTokenState_StillCancellable); 296 | test(d.future.state == TOCFutureState_CompletedWithResult); 297 | } 298 | -(void) testFutureSourceUntil_CleansUpEagerly { 299 | TOCCancelTokenSource* s = [TOCCancelTokenSource new]; 300 | vm_size_t memoryBefore = peekAllocatedMemoryInBytes(); 301 | int repeats = 10000; 302 | vm_size_t slack = 50000; 303 | for (int i = 0; i < repeats; i++) { 304 | @autoreleasepool { 305 | TOCFutureSource* d = [TOCFutureSource futureSourceUntil:s.token]; 306 | [d trySetResult:nil]; 307 | } 308 | } 309 | vm_size_t memoryAfter = peekAllocatedMemoryInBytes(); 310 | bool likelyIsNotCleaningUp = memoryAfter > memoryBefore + slack; 311 | test(!likelyIsNotCleaningUp); 312 | } 313 | 314 | -(void) testWhenCancelledDo_StaysOnMainThread { 315 | TOCCancelTokenSource* c2 = [TOCCancelTokenSource new]; 316 | dispatch_after(DISPATCH_TIME_NOW, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 317 | TOCCancelTokenSource* c1 = [TOCCancelTokenSource new]; 318 | TOCFuture* f = [TOCFuture futureFromOperation:^id{ 319 | test(NSThread.isMainThread); 320 | [c1.token whenCancelledDo:^{ 321 | test(NSThread.isMainThread); 322 | [c2 cancel]; 323 | }]; 324 | return nil; 325 | } invokedOnThread:NSThread.mainThread]; 326 | 327 | test(!NSThread.isMainThread); 328 | testCompletesConcurrently(f); 329 | testFutureHasResult(f, nil); 330 | test(c2.token.state == TOCCancelTokenState_StillCancellable); 331 | 332 | [c1 cancel]; 333 | }); 334 | 335 | for (int i = 0; i < 5 && !c2.token.isAlreadyCancelled; i++) { 336 | [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]]; 337 | } 338 | test(c2.token.state == TOCCancelTokenState_Cancelled); 339 | } 340 | -(void) testWhenCancelledDoUnless_StaysOnMainThread { 341 | TOCCancelTokenSource* c2 = [TOCCancelTokenSource new]; 342 | dispatch_after(DISPATCH_TIME_NOW, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 343 | TOCCancelTokenSource* c1 = [TOCCancelTokenSource new]; 344 | TOCFuture* f = [TOCFuture futureFromOperation:^id{ 345 | test(NSThread.isMainThread); 346 | [c1.token whenCancelledDo:^{ 347 | test(NSThread.isMainThread); 348 | [c2 cancel]; 349 | } unless:c2.token]; 350 | return nil; 351 | } invokedOnThread:NSThread.mainThread]; 352 | 353 | test(!NSThread.isMainThread); 354 | testCompletesConcurrently(f); 355 | testFutureHasResult(f, nil); 356 | test(c2.token.state == TOCCancelTokenState_StillCancellable); 357 | 358 | [c1 cancel]; 359 | }); 360 | 361 | for (int i = 0; i < 5 && !c2.token.isAlreadyCancelled; i++) { 362 | [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]]; 363 | } 364 | test(c2.token.state == TOCCancelTokenState_Cancelled); 365 | } 366 | 367 | -(void) testWhenCancelledDoUnless_AfterTheFactCancellationStillStopsCallbacksOnMainThread { 368 | TOCCancelTokenSource* s1 = [TOCCancelTokenSource new]; 369 | TOCCancelTokenSource* s2 = [TOCCancelTokenSource new]; 370 | 371 | test(NSThread.isMainThread); 372 | __block int hits = 0; 373 | for (int i = 0; i < 5; i++) { 374 | [s1.token whenCancelledDo:^{ 375 | [s2 cancel]; 376 | hits += 1; 377 | } unless:s2.token]; 378 | } 379 | 380 | test(hits == 0); 381 | [s1 cancel]; 382 | test(hits == 1); 383 | } 384 | -(void) testWhenCancelledDoUnless_AfterTheFactCancellationStillStopsCallbacksOnMainThread_SecondThread { 385 | TOCCancelTokenSource* s1 = [TOCCancelTokenSource new]; 386 | TOCCancelTokenSource* s2 = [TOCCancelTokenSource new]; 387 | 388 | test(NSThread.isMainThread); 389 | __block int hits = 0; 390 | for (int i = 0; i < 5; i++) { 391 | [s1.token whenCancelledDo:^{ 392 | test(NSThread.isMainThread); 393 | [s2 cancel]; 394 | hits += 1; 395 | } unless:s2.token]; 396 | } 397 | 398 | test(hits == 0); 399 | [TOCInternal_BlockObject performBlockOnNewThread:^{[s1 cancel];}]; 400 | for (int i = 0; i < 10; i++) { 401 | [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]]; 402 | } 403 | test(hits == 1); 404 | } 405 | 406 | -(void)testThreadSafety_cancellationsNotLost { 407 | for (int runs = 0; runs < 50; runs++) { 408 | TOCCancelTokenSource* c = [TOCCancelTokenSource new]; 409 | __block int sched = 0; 410 | __block int ran = 0; 411 | const int n = 10000; 412 | 413 | dispatch_queue_t q1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 414 | dispatch_queue_t q2 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 415 | 416 | TOCCancelToken* d = c.token; 417 | dispatch_async(q1, ^{ 418 | while (OSAtomicIncrement32(&sched) <= n) { 419 | [d whenCancelledDo:^{ 420 | OSAtomicIncrement32(&ran); 421 | }]; 422 | } 423 | }); 424 | dispatch_async(q2, ^{ 425 | while (OSAtomicAdd32(0, &sched) == 0) { 426 | // waiting... 427 | } 428 | // quickly quickly! 429 | test(OSAtomicAdd32(0, &sched) < n); 430 | 431 | [c cancel]; 432 | }); 433 | 434 | 435 | for (int rep = 0; rep < 1000 && OSAtomicAdd32(0, &ran) < n; rep++) { 436 | usleep(1000*10); 437 | } 438 | test(OSAtomicAdd32(0, &ran) == n); 439 | } 440 | } 441 | 442 | @end 443 | -------------------------------------------------------------------------------- /test/src/TOCFuture+MoreConstructorsTest.m: -------------------------------------------------------------------------------- 1 | #import "Testing.h" 2 | #import "TOCFuture+MoreContructors.h" 3 | #import "TOCFuture+MoreContinuations.h" 4 | 5 | @interface TOCFutureExtraTest : XCTestCase 6 | @end 7 | 8 | @implementation TOCFutureExtraTest { 9 | @private NSThread* thread; 10 | @private NSRunLoop* runLoop; 11 | } 12 | 13 | -(void) setUp { 14 | thread = [[NSThread alloc] initWithTarget:self selector:@selector(runLoopUntilCancelled) object:nil]; 15 | [thread start]; 16 | 17 | while (true) { 18 | @synchronized(self) { 19 | if (runLoop != nil) break; 20 | } 21 | } 22 | } 23 | -(void) runLoopUntilCancelled { 24 | NSThread* curThread = [NSThread currentThread]; 25 | NSRunLoop* curRunLoop = NSRunLoop.currentRunLoop; 26 | @synchronized(self) { 27 | runLoop = curRunLoop; 28 | } 29 | while (![curThread isCancelled]) { 30 | [curRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]]; 31 | } 32 | } 33 | -(void) tearDown { 34 | [thread cancel]; 35 | } 36 | 37 | -(void)testFutureWithCancelFailure { 38 | test([[TOCFuture futureWithCancelFailure] hasFailedWithCancel]); 39 | test([[TOCFuture futureWithCancelFailure].forceGetFailure isKindOfClass:[TOCCancelToken class]]); 40 | } 41 | -(void)testFutureWithTimeoutFailure { 42 | test([[TOCFuture futureWithTimeoutFailure] hasFailedWithTimeout]); 43 | test([[TOCFuture futureWithTimeoutFailure].forceGetFailure isKindOfClass:[TOCTimeout class]]); 44 | } 45 | -(void)testFutureWithResultFromOperationOnThread { 46 | TOCFuture* f = [TOCFuture futureFromOperation:^{ return @1; } 47 | invokedOnThread:thread]; 48 | testCompletesConcurrently(f); 49 | testFutureHasResult(f, @1); 50 | } 51 | -(void)testFutureWithResultFromOperationDispatch { 52 | TOCFuture* f = [TOCFuture futureFromOperation:^{ return @1; } 53 | dispatchedOnQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)]; 54 | testCompletesConcurrently(f); 55 | testFutureHasResult(f, @1); 56 | } 57 | -(void)testFutureWithResultAfterDelay { 58 | testThrows([TOCFuture futureWithResult:@"X" afterDelay:-1]); 59 | testThrows([TOCFuture futureWithResult:@"X" afterDelay:-INFINITY]); 60 | testThrows([TOCFuture futureWithResult:@"X" afterDelay:NAN]); 61 | testFutureHasResult([TOCFuture futureWithResult:@"X" afterDelay:0], @"X"); 62 | testDoesNotCompleteConcurrently([TOCFuture futureWithResult:@"X" afterDelay:INFINITY]); 63 | 64 | TOCFuture* f = [TOCFuture futureWithResult:@1 65 | afterDelay:0.1]; 66 | 67 | testCompletesConcurrently(f); 68 | testFutureHasResult(f, @1); 69 | } 70 | -(void)testFutureWithResultAfterDelay_NotCompleted { 71 | testThrows([TOCFuture futureWithResult:@"X" afterDelay:-1]); 72 | testThrows([TOCFuture futureWithResult:@"X" afterDelay:-INFINITY]); 73 | testThrows([TOCFuture futureWithResult:@"X" afterDelay:NAN]); 74 | testFutureHasResult([TOCFuture futureWithResult:@"X" afterDelay:0], @"X"); 75 | testDoesNotCompleteConcurrently([TOCFuture futureWithResult:@"X" afterDelay:INFINITY]); 76 | 77 | TOCFuture* f = [TOCFuture futureWithResult:@1 78 | afterDelay:100.0]; 79 | testDoesNotCompleteConcurrently(f); 80 | } 81 | 82 | -(void)testFutureWithResultAfterDelayUnless_Preconditions { 83 | testThrows([TOCFuture futureWithResult:@"X" afterDelay:-1 unless:TOCCancelToken.immortalToken]); 84 | testThrows([TOCFuture futureWithResult:@"X" afterDelay:-1 unless:TOCCancelToken.cancelledToken]); 85 | 86 | testThrows([TOCFuture futureWithResult:@"X" afterDelay:-INFINITY unless:TOCCancelToken.immortalToken]); 87 | testThrows([TOCFuture futureWithResult:@"X" afterDelay:-INFINITY unless:TOCCancelToken.cancelledToken]); 88 | 89 | testThrows([TOCFuture futureWithResult:@"X" afterDelay:NAN unless:TOCCancelToken.immortalToken]); 90 | testThrows([TOCFuture futureWithResult:@"X" afterDelay:NAN unless:TOCCancelToken.cancelledToken]); 91 | } 92 | -(void)testFutureWithResultAfterDelayUnless_Succeed { 93 | TOCCancelTokenSource* s = [TOCCancelTokenSource new]; 94 | testFutureHasResult([TOCFuture futureWithResult:@"X" afterDelay:0 unless:s.token], @"X"); 95 | test([TOCFuture futureWithResult:@"X" afterDelay:INFINITY unless:TOCCancelToken.immortalToken].state == TOCFutureState_Immortal); 96 | test([TOCFuture futureWithResult:@"X" afterDelay:INFINITY unless:s.token].state != TOCCancelTokenState_Immortal); 97 | 98 | TOCFuture* f = [TOCFuture futureWithResult:@1 99 | afterDelay:0.05 100 | unless:s.token]; 101 | 102 | testCompletesConcurrently(f); 103 | testFutureHasResult(f, @1); 104 | } 105 | -(void)testFutureWithResultAfterDelayUnless_Cancel { 106 | TOCCancelTokenSource* s = [TOCCancelTokenSource new]; 107 | TOCFuture* f = [TOCFuture futureWithResult:@1 108 | afterDelay:10.0 109 | unless:s.token]; 110 | 111 | testDoesNotCompleteConcurrently(f); 112 | [s cancel]; 113 | test(f.hasFailedWithCancel); 114 | } 115 | -(void)testFutureWithResultAfterDelayUnless_AllowsDeallocCallback { 116 | TOCFuture* f; 117 | DeallocCounter* d = [DeallocCounter new]; 118 | @autoreleasepool { 119 | TOCCancelTokenSource* s = [TOCCancelTokenSource new]; 120 | DeallocToken* t = [d makeToken]; 121 | f = [TOCFuture futureWithResult:@1 122 | afterDelay:10.0 123 | unless:s.token]; 124 | [f finallyDo:^(TOCFuture *completed) { 125 | [t poke]; 126 | }]; 127 | test(d.lostTokenCount == 0); 128 | [s cancel]; 129 | } 130 | test(d.lostTokenCount == 1); 131 | test(f.hasFailedWithCancel); 132 | } 133 | -(void)testFutureWithResultAfterDelayUnless_AllowsDeallocArgument { 134 | TOCFuture* f; 135 | DeallocCounter* d = [DeallocCounter new]; 136 | @autoreleasepool { 137 | TOCCancelTokenSource* s = [TOCCancelTokenSource new]; 138 | DeallocToken* t = [d makeToken]; 139 | f = [TOCFuture futureWithResult:t 140 | afterDelay:10.0 141 | unless:s.token]; 142 | test(d.lostTokenCount == 0); 143 | [s cancel]; 144 | } 145 | test(d.lostTokenCount == 1); 146 | test(f.hasFailedWithCancel); 147 | } 148 | 149 | -(void)testHasFailedWithCancel { 150 | test(![TOCFutureSource new].future.hasFailedWithCancel); 151 | test(![TOCFuture futureWithResult:@0].hasFailedWithCancel); 152 | test(![TOCFuture futureWithResult:TOCCancelToken.cancelledToken].hasFailedWithCancel); 153 | test(![TOCFuture futureWithFailure:@0].hasFailedWithCancel); 154 | 155 | test([TOCFuture futureWithFailure:TOCCancelToken.cancelledToken].hasFailedWithCancel); 156 | } 157 | 158 | -(void) testFutureWithResultFromAsyncOperationWithResultLastingUntilCancelled_Immediate { 159 | TOCFutureSource* s = [TOCFutureSource new]; 160 | TOCUntilOperation t = ^(TOCCancelToken* until) { return s.future; }; 161 | 162 | testThrows([TOCFuture futureFromUntilOperation:nil 163 | withOperationTimeout:0 164 | until:nil]); 165 | testThrows([TOCFuture futureFromUntilOperation:t 166 | withOperationTimeout:NAN 167 | until:nil]); 168 | testThrows([TOCFuture futureFromUntilOperation:t 169 | withOperationTimeout:-1 170 | until:nil]); 171 | test([[TOCFuture futureFromUntilOperation:t 172 | withOperationTimeout:0 173 | until:nil] hasFailedWithTimeout]); 174 | test([[TOCFuture futureFromUntilOperation:t 175 | withOperationTimeout:1000 176 | until:TOCCancelToken.cancelledToken] hasFailedWithCancel]); 177 | testFutureHasResult([TOCFuture futureFromUntilOperation:^(TOCCancelToken* _){ return [TOCFuture futureWithResult:@1]; } 178 | withOperationTimeout:1000 179 | until:nil], @1); 180 | testFutureHasFailure([TOCFuture futureFromUntilOperation:^(TOCCancelToken* _){ return [TOCFuture futureWithFailure:@2]; } 181 | withOperationTimeout:1000 182 | until:nil], @2); 183 | } 184 | -(void) testFutureWithResultFromAsyncOperationWithResultLastingUntilCancelled_Timeout { 185 | TOCFutureSource* s = [TOCFutureSource new]; 186 | TOCUntilOperation t = ^(TOCCancelToken* until) { return s.future; }; 187 | 188 | TOCFuture* f0 = [TOCFuture futureFromUntilOperation:t 189 | withOperationTimeout:0.01 190 | until:nil]; 191 | TOCFuture* f1 = [TOCFuture futureFromUntilOperation:t 192 | withOperationTimeout:0.05 193 | until:nil]; 194 | TOCFuture* f2 = [TOCFuture futureFromUntilOperation:t 195 | withOperationTimeout:100.0 196 | until:nil]; 197 | 198 | testChurnUntil(f1.hasFailedWithTimeout); 199 | test(f0.hasFailedWithTimeout); 200 | test(f2.isIncomplete); 201 | [s trySetResult:@1]; 202 | testFutureHasResult(f2, @1); 203 | } 204 | -(void) testFutureWithResultFromAsyncOperationWithResultLastingUntilCancelled_CancelDuring { 205 | TOCFutureSource* s = [TOCFutureSource new]; 206 | TOCUntilOperation t = ^(TOCCancelToken* until) { [until whenCancelledDo:^{ [s trySetFailedWithCancel]; }]; return [TOCFutureSource new].future; }; 207 | 208 | TOCCancelTokenSource* c = [TOCCancelTokenSource new]; 209 | TOCFuture* f = [TOCFuture futureFromUntilOperation:t 210 | withOperationTimeout:100.0 211 | until:c.token]; 212 | test(f.isIncomplete); 213 | [c cancel]; 214 | test(s.future.hasFailedWithCancel); 215 | test(f.hasFailedWithCancel); 216 | } 217 | -(void) testFutureWithResultFromAsyncOperationWithResultLastingUntilCancelled_CancelAfter { 218 | TOCFutureSource* s = [TOCFutureSource new]; 219 | TOCCancelTokenSource* d = [TOCCancelTokenSource new]; 220 | TOCUntilOperation t = ^(TOCCancelToken* until) { [until whenCancelledDo:^{ [d tryCancel]; }]; return s.future; }; 221 | 222 | TOCCancelTokenSource* c = [TOCCancelTokenSource new]; 223 | TOCFuture* f = [TOCFuture futureFromUntilOperation:t 224 | withOperationTimeout:100.0 225 | until:c.token]; 226 | test(f.isIncomplete); 227 | [s forceSetResult:d]; 228 | testFutureHasResult(f, d); 229 | test(!d.token.isAlreadyCancelled); 230 | [c cancel]; 231 | test(d.token.isAlreadyCancelled); 232 | } 233 | 234 | -(void) testFutureWithResultFromAsyncCancellableOperationWithTimeout_Immediate { 235 | TOCFutureSource* s = [TOCFutureSource new]; 236 | TOCUnlessOperation t = ^(TOCCancelToken* unless) { return s.future; }; 237 | 238 | testThrows([TOCFuture futureFromUnlessOperation:nil 239 | withTimeout:100]); 240 | testThrows([TOCFuture futureFromUnlessOperation:t 241 | withTimeout:-1]); 242 | testThrows([TOCFuture futureFromUnlessOperation:t 243 | withTimeout:NAN]); 244 | test([[TOCFuture futureFromUnlessOperation:t 245 | withTimeout:0] hasFailedWithTimeout]); 246 | testFutureHasResult([TOCFuture futureFromUnlessOperation:^(TOCCancelToken* _){ return [TOCFuture futureWithResult:@1]; } 247 | withTimeout:100], @1); 248 | testFutureHasFailure([TOCFuture futureFromUnlessOperation:^(TOCCancelToken* _){ return [TOCFuture futureWithFailure:@2]; } 249 | withTimeout:100], @2); 250 | } 251 | -(void) testFutureWithResultFromAsyncCancellableOperationWithTimeout_WaitsForCancel { 252 | TOCFutureSource* s = [TOCFutureSource new]; 253 | TOCUnlessOperation t = ^(TOCCancelToken* unless) { return s.future; }; 254 | TOCCancelTokenSource* c = [TOCCancelTokenSource new]; 255 | TOCFuture* f = [TOCFuture futureFromUnlessOperation:t 256 | withTimeout:0.0001]; 257 | 258 | [c cancel]; 259 | testDoesNotCompleteConcurrently(f); 260 | [s trySetFailedWithCancel]; 261 | test(f.hasFailedWithTimeout); 262 | } 263 | -(void) testFutureWithResultFromAsyncCancellableOperationWithTimeoutWaitsForResult { 264 | TOCFutureSource* s = [TOCFutureSource new]; 265 | TOCUnlessOperation t = ^(TOCCancelToken* unless) { return s.future; }; 266 | TOCCancelTokenSource* c = [TOCCancelTokenSource new]; 267 | TOCFuture* f = [TOCFuture futureFromUnlessOperation:t 268 | withTimeout:0.0001]; 269 | 270 | [c cancel]; 271 | testDoesNotCompleteConcurrently(f); 272 | [s trySetResult:@1]; 273 | testFutureHasResult(f, @1); 274 | } 275 | -(void) testFutureWithResultFromAsyncCancellableOperationWithTimeout_WaitsForFailure { 276 | TOCFutureSource* s = [TOCFutureSource new]; 277 | TOCUnlessOperation t = ^(TOCCancelToken* unless) { return s.future; }; 278 | TOCCancelTokenSource* c = [TOCCancelTokenSource new]; 279 | TOCFuture* f = [TOCFuture futureFromUnlessOperation:t 280 | withTimeout:0.0001]; 281 | [c cancel]; 282 | testDoesNotCompleteConcurrently(f); 283 | [s trySetFailure:@2]; 284 | testFutureHasFailure(f, @2); 285 | } 286 | -(void) testFutureWithResultFromAsyncCancellableOperationWithTimeout_Timeout { 287 | TOCFutureSource* s = [TOCFutureSource new]; 288 | TOCUnlessOperation t = ^(TOCCancelToken* unless) { return [s.future unless:unless]; }; 289 | 290 | TOCFuture* f0 = [TOCFuture futureFromUnlessOperation:t 291 | withTimeout:0.01]; 292 | TOCFuture* f1 = [TOCFuture futureFromUnlessOperation:t 293 | withTimeout:0.05]; 294 | TOCFuture* f2 = [TOCFuture futureFromUnlessOperation:t 295 | withTimeout:100.0]; 296 | 297 | testChurnUntil(f1.hasFailedWithTimeout); 298 | test(f0.hasFailedWithTimeout); 299 | test(f2.isIncomplete); 300 | [s trySetResult:@1]; 301 | testFutureHasResult(f2, @1); 302 | } 303 | 304 | -(void) testFutureWithResultFromAsyncCancellableOperationWithTimeoutUnless_Immediate { 305 | TOCFutureSource* s = [TOCFutureSource new]; 306 | TOCUnlessOperation t = ^(TOCCancelToken* unless) { return [s.future unless:unless]; }; 307 | 308 | testThrows([TOCFuture futureFromUnlessOperation:nil 309 | withTimeout:100 310 | unless:nil]); 311 | testThrows([TOCFuture futureFromUnlessOperation:t 312 | withTimeout:-1 313 | unless:nil]); 314 | testThrows([TOCFuture futureFromUnlessOperation:t 315 | withTimeout:NAN 316 | unless:nil]); 317 | test([[TOCFuture futureFromUnlessOperation:t 318 | withTimeout:0 319 | unless:nil] hasFailedWithTimeout]); 320 | test([[TOCFuture futureFromUnlessOperation:t 321 | withTimeout:100 322 | unless:TOCCancelToken.cancelledToken] hasFailedWithCancel]); 323 | testFutureHasResult([TOCFuture futureFromUnlessOperation:^(TOCCancelToken* _){ return [TOCFuture futureWithResult:@1]; } 324 | withTimeout:100 325 | unless:nil], @1); 326 | testFutureHasFailure([TOCFuture futureFromUnlessOperation:^(TOCCancelToken* _){ return [TOCFuture futureWithFailure:@2]; } 327 | withTimeout:100 328 | unless:nil], @2); 329 | } 330 | -(void) testFutureWithResultFromAsyncCancellableOperationWithTimeoutUnless_WaitsForTimeoutCancel { 331 | TOCFutureSource* s = [TOCFutureSource new]; 332 | TOCUnlessOperation t = ^(TOCCancelToken* unless) { return s.future; }; 333 | TOCCancelTokenSource* c = [TOCCancelTokenSource new]; 334 | TOCFuture* f = [TOCFuture futureFromUnlessOperation:t 335 | withTimeout:0.0001 336 | unless:c.token]; 337 | 338 | testDoesNotCompleteConcurrently(f); 339 | [s trySetFailedWithCancel]; 340 | test(f.hasFailedWithTimeout); 341 | } 342 | -(void) testFutureWithResultFromAsyncCancellableOperationWithTimeoutUnless_WaitsForCancel { 343 | TOCFutureSource* s = [TOCFutureSource new]; 344 | TOCUnlessOperation t = ^(TOCCancelToken* unless) { return s.future; }; 345 | TOCCancelTokenSource* c = [TOCCancelTokenSource new]; 346 | TOCFuture* f = [TOCFuture futureFromUnlessOperation:t 347 | withTimeout:10000.0 348 | unless:c.token]; 349 | [c cancel]; 350 | testDoesNotCompleteConcurrently(f); 351 | [s trySetFailedWithCancel]; 352 | test(f.hasFailedWithCancel); 353 | } 354 | -(void) testFutureWithResultFromAsyncCancellableOperationWithTimeoutUnless_WaitsForResult { 355 | TOCFutureSource* s = [TOCFutureSource new]; 356 | TOCUnlessOperation t = ^(TOCCancelToken* unless) { return s.future; }; 357 | TOCCancelTokenSource* c = [TOCCancelTokenSource new]; 358 | TOCFuture* f = [TOCFuture futureFromUnlessOperation:t 359 | withTimeout:10000.0 360 | unless:c.token]; 361 | [c cancel]; 362 | testDoesNotCompleteConcurrently(f); 363 | [s trySetResult:@1]; 364 | testFutureHasResult(f, @1); 365 | } 366 | -(void) testFutureWithResultFromAsyncCancellableOperationWithTimeoutUnless_WaitsForFailure { 367 | TOCFutureSource* s = [TOCFutureSource new]; 368 | TOCUnlessOperation t = ^(TOCCancelToken* unless) { return s.future; }; 369 | TOCCancelTokenSource* c = [TOCCancelTokenSource new]; 370 | TOCFuture* f = [TOCFuture futureFromUnlessOperation:t 371 | withTimeout:10000.0 372 | unless:c.token]; 373 | [c cancel]; 374 | test(f.isIncomplete); 375 | [s trySetFailure:@2]; 376 | testFutureHasFailure(f, @2); 377 | } 378 | -(void) testFutureWithResultFromAsyncCancellableOperationWithTimeoutUnless_Timeout { 379 | TOCFutureSource* s = [TOCFutureSource new]; 380 | TOCUnlessOperation t = ^(TOCCancelToken* unless) { return [s.future unless:unless]; }; 381 | 382 | TOCFuture* f0 = [TOCFuture futureFromUnlessOperation:t 383 | withTimeout:0.01 384 | unless:nil]; 385 | TOCFuture* f1 = [TOCFuture futureFromUnlessOperation:t 386 | withTimeout:0.05 387 | unless:nil]; 388 | TOCFuture* f2 = [TOCFuture futureFromUnlessOperation:t 389 | withTimeout:100.0 390 | unless:nil]; 391 | 392 | testChurnUntil(f1.hasFailedWithTimeout); 393 | test(f0.hasFailedWithTimeout); 394 | test(f2.isIncomplete); 395 | [s trySetResult:@1]; 396 | testFutureHasResult(f2, @1); 397 | } 398 | -(void) testFutureWithResultFromAsyncCancellableOperationWithTimeoutUnless_CancelDuring { 399 | TOCFutureSource* s = [TOCFutureSource new]; 400 | TOCUnlessOperation t = ^(TOCCancelToken* unless) { [unless whenCancelledDo:^{ [s trySetFailedWithCancel]; }]; return s.future; }; 401 | 402 | TOCCancelTokenSource* c = [TOCCancelTokenSource new]; 403 | TOCFuture* f = [TOCFuture futureFromUnlessOperation:t 404 | withTimeout:100.0 405 | unless:c.token]; 406 | test(f.isIncomplete); 407 | [c cancel]; 408 | test(s.future.hasFailedWithCancel); 409 | test(f.hasFailedWithCancel); 410 | } 411 | 412 | @end 413 | -------------------------------------------------------------------------------- /test/src/TOCFuture+MoreContinuationsTest.m: -------------------------------------------------------------------------------- 1 | #import "Testing.h" 2 | #import "TOCFuture+MoreContinuations.h" 3 | 4 | @interface TOCFutureMoreContinuationsTest : XCTestCase 5 | @end 6 | 7 | @implementation TOCFutureMoreContinuationsTest 8 | 9 | -(void)testFinallyDo_Immediate { 10 | testDoesNotHitTarget([[TOCFutureSource new].future finallyDo:^(TOCFuture* completed) { hitTarget; }]); 11 | testHitsTarget([[TOCFuture futureWithResult:@7] finallyDo:^(TOCFuture* completed) { testFutureHasResult(completed, @7); hitTarget; }]); 12 | testHitsTarget([[TOCFuture futureWithFailure:@8] finallyDo:^(TOCFuture* completed) { testFutureHasFailure(completed, @8); hitTarget; }]); 13 | } 14 | -(void)testFinallyDo_DeferredResult { 15 | TOCFutureSource* s = [TOCFutureSource new]; 16 | TOCFuture* f = s.future; 17 | testDoesNotHitTarget([f finallyDo:^(TOCFuture* value) { test(value == f); hitTarget; }]); 18 | testHitsTarget([s trySetResult:@"X"]); 19 | } 20 | -(void)testFinallyDo_DeferredFail { 21 | TOCFutureSource* s = [TOCFutureSource new]; 22 | TOCFuture* f = s.future; 23 | testDoesNotHitTarget([f finallyDo:^(TOCFuture* value) { test(value == f); hitTarget; }]); 24 | testHitsTarget([s trySetFailure:@"X"]); 25 | } 26 | 27 | -(void)testThenDo_Immediate { 28 | testDoesNotHitTarget([[TOCFutureSource new].future thenDo:^(id result) { hitTarget; }]); 29 | testHitsTarget([[TOCFuture futureWithResult:@7] thenDo:^(id result) { testEq(result, @7); hitTarget; }]); 30 | testDoesNotHitTarget([[TOCFuture futureWithFailure:@8] thenDo:^(id result) { hitTarget; }]); 31 | } 32 | -(void)testThenDo_DeferredResult { 33 | TOCFutureSource* s = [TOCFutureSource new]; 34 | TOCFuture* f = s.future; 35 | testDoesNotHitTarget([f thenDo:^(id value) { testEq(value, @"X"); hitTarget; }]); 36 | testHitsTarget([s trySetResult:@"X"]); 37 | } 38 | -(void)testThenDo_DeferredFail { 39 | TOCFutureSource* s = [TOCFutureSource new]; 40 | TOCFuture* f = s.future; 41 | testDoesNotHitTarget([f thenDo:^(id value) { test(false); hitTarget; }]); 42 | testDoesNotHitTarget([s trySetFailure:@"X"]); 43 | } 44 | 45 | -(void)testCatchDo_Immediate { 46 | testDoesNotHitTarget([[TOCFutureSource new].future catchDo:^(id failure) { hitTarget; }]); 47 | testDoesNotHitTarget([[TOCFuture futureWithResult:@7] catchDo:^(id failure) { hitTarget; }]); 48 | testHitsTarget([[TOCFuture futureWithFailure:@8] catchDo:^(id failure) { testEq(failure, @8); hitTarget; }]); 49 | } 50 | -(void)testCatchDo_DeferredResult { 51 | TOCFutureSource* s = [TOCFutureSource new]; 52 | TOCFuture* f = s.future; 53 | testDoesNotHitTarget([f catchDo:^(id value) { test(false); hitTarget; }]); 54 | testDoesNotHitTarget([s trySetResult:@"X"]); 55 | } 56 | -(void)testCatchDo_DeferredFail { 57 | TOCFutureSource* s = [TOCFutureSource new]; 58 | TOCFuture* f = s.future; 59 | testDoesNotHitTarget([f catchDo:^(id value) { testEq(value, @"X"); hitTarget; }]); 60 | testHitsTarget([s trySetFailure:@"X"]); 61 | } 62 | 63 | -(void)testThen_Immediate { 64 | testFutureHasResult([[TOCFuture futureWithResult:@1] then:^(id result) { return @2; }], @2); 65 | testFutureHasFailure([[TOCFuture futureWithFailure:@3] then:^(id result) { return @4; }], @3); 66 | testFutureHasFailure([[TOCFuture futureWithResult:@5] then:^(id result) { return [TOCFuture futureWithFailure:@6]; }], @6); 67 | 68 | testDoesNotHitTarget([[TOCFutureSource new].future then:^id(id result) { hitTarget; return nil; }]); 69 | testHitsTarget([[TOCFuture futureWithResult:@7] then:^id(id result) { testEq(result, @7); hitTarget; return nil; }]); 70 | testDoesNotHitTarget([[TOCFuture futureWithFailure:@8] then:^id(id result) { hitTarget; return nil; }]); 71 | } 72 | -(void)testThen_DeferredResult { 73 | TOCFutureSource* s = [TOCFutureSource new]; 74 | TOCFuture* f = s.future; 75 | TOCFuture* f2 = [f then:^(id value) { testEq(value, @"X"); return @2; }]; 76 | test(f2.isIncomplete); 77 | [s trySetResult:@"X"]; 78 | testFutureHasResult(f2, @2); 79 | } 80 | -(void)testThen_DeferredFail { 81 | TOCFutureSource* s = [TOCFutureSource new]; 82 | TOCFuture* f = s.future; 83 | TOCFuture* f2 = [f then:^(id value) { test(false); return @2; }]; 84 | test(f2.isIncomplete); 85 | [s trySetFailure:@"X"]; 86 | testFutureHasFailure(f2, @"X"); 87 | } 88 | 89 | -(void)testCatch_Immediate { 90 | testFutureHasResult([[TOCFuture futureWithResult:@1] catch:^(id failure) { return @2; }], @1); 91 | testFutureHasResult([[TOCFuture futureWithFailure:@3] catch:^(id failure) { return @4; }], @4); 92 | testFutureHasFailure([[TOCFuture futureWithFailure:@5] catch:^(id failure) { return [TOCFuture futureWithFailure:@6]; }], @6); 93 | 94 | testDoesNotHitTarget([[TOCFutureSource new].future catch:^id(id failure) { hitTarget; return nil; }]); 95 | testDoesNotHitTarget([[TOCFuture futureWithResult:@1] catch:^id(id failure) { hitTarget; return nil; }]); 96 | testHitsTarget([[TOCFuture futureWithFailure:@8] catch:^id(id failure) { testEq(failure, @8); hitTarget; return nil; }]); 97 | } 98 | -(void)testCatch_DeferredResult { 99 | TOCFutureSource* s = [TOCFutureSource new]; 100 | TOCFuture* f = s.future; 101 | TOCFuture* f2 = [f catch:^(id value) { test(false); return @2; }]; 102 | test(f2.isIncomplete); 103 | [s trySetResult:@"X"]; 104 | testFutureHasResult(f2, @"X"); 105 | } 106 | -(void)testCatch_DeferredFail { 107 | TOCFutureSource* s = [TOCFutureSource new]; 108 | TOCFuture* f = s.future; 109 | TOCFuture* f2 = [f catch:^(id value) { testEq(value, @"X"); return @2; }]; 110 | test(f2.isIncomplete); 111 | [s trySetFailure:@"X"]; 112 | testFutureHasResult(f2, @2); 113 | } 114 | 115 | -(void)testFinally_Immediate { 116 | testFutureHasResult([[TOCFuture futureWithResult:@1] finally:^(TOCFuture *completed) { return @2; }], @2); 117 | testFutureHasResult([[TOCFuture futureWithFailure:@3] finally:^(TOCFuture *completed) { return @4; }], @4); 118 | testFutureHasFailure([[TOCFuture futureWithFailure:@5] finally:^(TOCFuture *completed) { return [TOCFuture futureWithFailure:@6]; }], @6); 119 | 120 | testDoesNotHitTarget([[TOCFutureSource new].future finally:^id(TOCFuture *completed) { hitTarget; return nil; }]); 121 | testHitsTarget([[TOCFuture futureWithResult:@7] finally:^id(TOCFuture *completed) { testFutureHasResult(completed, @7); hitTarget; return nil; }]); 122 | testHitsTarget([[TOCFuture futureWithFailure:@8] finally:^id(TOCFuture *completed) { testFutureHasFailure(completed, @8); hitTarget; return nil; }]); 123 | } 124 | -(void)testFinally_DeferredResult { 125 | TOCFutureSource* s = [TOCFutureSource new]; 126 | TOCFuture* f = s.future; 127 | TOCFuture* f2 = [f finally:^(id value) { testFutureHasResult(value, @"X"); return @2; }]; 128 | test(f2.isIncomplete); 129 | [s trySetResult:@"X"]; 130 | testFutureHasResult(f2, @2); 131 | } 132 | -(void)testFinally_DeferredFail { 133 | TOCFutureSource* s = [TOCFutureSource new]; 134 | TOCFuture* f = s.future; 135 | TOCFuture* f2 = [f finally:^(id value) { testFutureHasFailure(value, @"X"); return @2; }]; 136 | test(f2.isIncomplete); 137 | [s trySetFailure:@"X"]; 138 | testFutureHasResult(f2, @2); 139 | } 140 | 141 | -(void) testUnless_Immediate { 142 | TOCCancelToken* cc = TOCCancelToken.cancelledToken; 143 | 144 | testFutureHasResult([[TOCFuture futureWithResult:@1] unless:nil], @1); 145 | testFutureHasResult([[TOCFuture futureWithResult:@2] unless:TOCCancelToken.immortalToken], @2); 146 | 147 | testFutureHasFailure([[TOCFuture futureWithFailure:@3] unless:nil], @3); 148 | testFutureHasFailure([[TOCFuture futureWithFailure:@4] unless:TOCCancelToken.immortalToken], @4); 149 | 150 | testFutureHasFailure([[TOCFuture futureWithResult:@5] unless:cc], cc); 151 | testFutureHasFailure([[TOCFuture futureWithFailure:@6] unless:cc], cc); 152 | } 153 | -(void) testUnless_DeferredCompletion { 154 | TOCCancelTokenSource* c = [TOCCancelTokenSource new]; 155 | TOCFutureSource* s = [TOCFutureSource new]; 156 | 157 | TOCFuture* f = [s.future unless:c.token]; 158 | test(f.isIncomplete); 159 | [s trySetResult:@1]; 160 | testFutureHasResult(f, @1); 161 | } 162 | -(void) testUnless_DeferredFailure { 163 | TOCCancelTokenSource* c = [TOCCancelTokenSource new]; 164 | TOCFutureSource* s = [TOCFutureSource new]; 165 | 166 | TOCFuture* f = [s.future unless:c.token]; 167 | test(f.isIncomplete); 168 | [s trySetFailure:@2]; 169 | testFutureHasFailure(f, @2); 170 | } 171 | -(void) testUnless_DeferredCancel { 172 | TOCCancelTokenSource* c = [TOCCancelTokenSource new]; 173 | TOCFutureSource* s = [TOCFutureSource new]; 174 | 175 | TOCFuture* f = [s.future unless:c.token]; 176 | test(f.isIncomplete); 177 | [c cancel]; 178 | test(f.hasFailedWithCancel); 179 | } 180 | 181 | @end 182 | -------------------------------------------------------------------------------- /test/src/TOCFutureArrayUtilTest.m: -------------------------------------------------------------------------------- 1 | #import "Testing.h" 2 | #import "NSArray+TOCFuture.h" 3 | 4 | #define fut(X) [TOCFuture futureWithResult:X] 5 | #define futfail(X) [TOCFuture futureWithFailure:X] 6 | 7 | @interface TOCFutureArrayUtilTest : XCTestCase 8 | @end 9 | 10 | @implementation TOCFutureArrayUtilTest 11 | 12 | -(void)testOrderedByCompletion { 13 | testEq(@[].toc_orderedByCompletion, @[]); 14 | testThrows([(@[@1]) toc_orderedByCompletion]); 15 | 16 | NSArray* s = (@[[TOCFutureSource new], [TOCFutureSource new], [TOCFutureSource new]]); 17 | NSArray* f = (@[[s[0] future], [s[1] future], [s[2] future]]); 18 | NSArray* g = [f toc_orderedByCompletion]; 19 | test(g.count == f.count); 20 | 21 | test(((TOCFuture*)g[0]).isIncomplete); 22 | 23 | [s[1] trySetResult:@"A"]; 24 | testFutureHasResult(g[0], @"A"); 25 | test(((TOCFuture*)g[1]).isIncomplete); 26 | 27 | [s[2] trySetFailure:@"B"]; 28 | testFutureHasFailure(g[1], @"B"); 29 | test(((TOCFuture*)g[2]).isIncomplete); 30 | 31 | [s[0] trySetResult:@"C"]; 32 | testFutureHasResult(g[2], @"C"); 33 | 34 | // ordered by continuations, so after completion should preserve ordering of original array 35 | NSArray* g2 = [f toc_orderedByCompletion]; 36 | testFutureHasResult(g2[0], @"C"); 37 | testFutureHasResult(g2[1], @"A"); 38 | testFutureHasFailure(g2[2], @"B"); 39 | } 40 | -(void) testFinallyAll { 41 | testThrows(@[@1].toc_finallyAll); 42 | testFutureHasResult(@[].toc_finallyAll, @[]); 43 | 44 | TOCFuture* f = (@[fut(@1), futfail(@2)]).toc_finallyAll; 45 | test(f.hasResult); 46 | NSArray* x = f.forceGetResult; 47 | test(x.count == 2); 48 | testFutureHasResult(x[0], @1); 49 | testFutureHasFailure(x[1], @2); 50 | } 51 | -(void) testFinallyAll_Incomplete { 52 | TOCFutureSource* s = [TOCFutureSource new]; 53 | TOCFuture* f = (@[fut(@1), s.future, fut(@3)]).toc_finallyAll; 54 | test(f.isIncomplete); 55 | 56 | [s trySetFailure:@""]; 57 | test(f.hasResult); 58 | NSArray* x = f.forceGetResult; 59 | test(x.count == 3); 60 | testFutureHasResult(x[0], @1); 61 | testFutureHasFailure(x[1], @""); 62 | testFutureHasResult(x[2], @3); 63 | } 64 | -(void) testThenAll { 65 | testThrows(@[@1].toc_thenAll); 66 | testFutureHasResult(@[].toc_thenAll, @[]); 67 | testFutureHasResult((@[fut(@3)]).toc_thenAll, (@[@3])); 68 | testFutureHasResult((@[fut(@1), fut(@2)]).toc_thenAll, (@[@1, @2])); 69 | 70 | TOCFuture* f = @[fut(@1), futfail(@2)].toc_thenAll; 71 | test(f.hasFailed); 72 | NSArray* x = f.forceGetFailure; 73 | test(x.count == 2); 74 | testFutureHasResult(x[0], @1); 75 | testFutureHasFailure(x[1], @2); 76 | } 77 | -(void) testThenAll_Incomplete { 78 | TOCFutureSource* s = [TOCFutureSource new]; 79 | TOCFuture* f = @[fut(@1), s.future, fut(@3)].toc_thenAll; 80 | test(f.isIncomplete); 81 | [s trySetResult:@""]; 82 | testFutureHasResult(f, (@[@1, @"", @3])); 83 | } 84 | -(void) testAsyncRaceAsynchronousResultUntilCancelledOperationsUntil_Failures { 85 | testThrows([@[] toc_raceForWinnerLastingUntil:nil]); 86 | testThrows([@[@1] toc_raceForWinnerLastingUntil:nil]); 87 | testThrows([(@[[TOCFuture futureWithResult:@1]]) toc_raceForWinnerLastingUntil:nil]); 88 | } 89 | -(void) testAsyncRaceAsynchronousResultUntilCancelledOperationsUntil_Immediate { 90 | TOCUntilOperation immediate1 = ^(TOCCancelToken* until) { 91 | hitTarget; 92 | test(until != nil); 93 | return [TOCFuture futureWithResult:@1]; 94 | }; 95 | TOCUntilOperation immediateFail = ^(TOCCancelToken* until) { 96 | test(until != nil); 97 | return [TOCFuture futureWithFailure:@"bleh"]; 98 | }; 99 | 100 | testHitsTarget([@[immediate1] toc_raceForWinnerLastingUntil:nil]); 101 | testFutureHasResult([@[immediate1] toc_raceForWinnerLastingUntil:nil], @1); 102 | testFutureHasResult([(@[immediate1, immediateFail]) toc_raceForWinnerLastingUntil:nil], @1); 103 | testFutureHasResult([(@[immediateFail, immediate1]) toc_raceForWinnerLastingUntil:nil], @1); 104 | testFutureHasFailure([(@[immediateFail, immediateFail]) toc_raceForWinnerLastingUntil:nil], (@[@"bleh", @"bleh"])); 105 | } 106 | -(void) testAsyncRaceAsynchronousResultUntilCancelledOperationsUntil_Deferred_Win { 107 | TOCFutureSource* s = [TOCFutureSource new]; 108 | TOCUntilOperation t = ^(TOCCancelToken* until) { return s.future; }; 109 | 110 | TOCFuture* f = [@[t] toc_raceForWinnerLastingUntil:nil]; 111 | test(f.isIncomplete); 112 | 113 | [s trySetResult:@2]; 114 | testFutureHasResult(f, @2); 115 | } 116 | -(void) testAsyncRaceAsynchronousResultUntilCancelledOperationsUntil_Deferred_Fail { 117 | TOCFutureSource* s = [TOCFutureSource new]; 118 | TOCUntilOperation t = ^(TOCCancelToken* until) { return s.future; }; 119 | 120 | TOCFuture* f = [@[t] toc_raceForWinnerLastingUntil:nil]; 121 | test(f.isIncomplete); 122 | 123 | [s trySetFailure:@3]; 124 | testFutureHasFailure(f, @[@3]); 125 | } 126 | -(void) testAsyncRaceAsynchronousResultUntilCancelledOperationsUntil_Deferred_WinWin { 127 | TOCFutureSource* s1 = [TOCFutureSource new]; 128 | TOCFutureSource* s2 = [TOCFutureSource new]; 129 | TOCUntilOperation t1 = ^(TOCCancelToken* until) { return s1.future; }; 130 | TOCUntilOperation t2 = ^(TOCCancelToken* until) { return s2.future; }; 131 | 132 | TOCFuture* f1 = [@[t1, t2] toc_raceForWinnerLastingUntil:nil]; 133 | TOCFuture* f2 = [@[t2, t1] toc_raceForWinnerLastingUntil:nil]; 134 | 135 | test(f1.isIncomplete); 136 | test(f2.isIncomplete); 137 | [s1 forceSetResult:@4]; 138 | 139 | testFutureHasResult(f1, @4); 140 | testFutureHasResult(f2, @4); 141 | } 142 | -(void) testAsyncRaceAsynchronousResultUntilCancelledOperationsUntil_Deferred_FailFail { 143 | TOCFutureSource* s1 = [TOCFutureSource new]; 144 | TOCFutureSource* s2 = [TOCFutureSource new]; 145 | TOCUntilOperation t1 = ^(TOCCancelToken* until) { return s1.future; }; 146 | TOCUntilOperation t2 = ^(TOCCancelToken* until) { return s2.future; }; 147 | 148 | TOCFuture* f1 = [@[t1, t2] toc_raceForWinnerLastingUntil:nil]; 149 | TOCFuture* f2 = [@[t2, t1] toc_raceForWinnerLastingUntil:nil]; 150 | test(f1.isIncomplete); 151 | test(f2.isIncomplete); 152 | 153 | [s1 forceSetFailure:@5]; 154 | test(f1.isIncomplete); 155 | test(f2.isIncomplete); 156 | 157 | [s2 forceSetFailure:@6]; 158 | testFutureHasFailure(f1, (@[@5, @6])); 159 | testFutureHasFailure(f2, (@[@6, @5])); 160 | } 161 | -(void) testAsyncRaceAsynchronousResultUntilCancelledOperationsUntil_CancelsLosers { 162 | TOCFutureSource* s1 = [TOCFutureSource new]; 163 | TOCFutureSource* s2 = [TOCFutureSource new]; 164 | TOCFutureSource* s3 = [TOCFutureSource new]; 165 | TOCUntilOperation t1 = ^(TOCCancelToken* until) { [until whenCancelledDo:^{ [s1 trySetFailedWithCancel]; }]; return s1.future; }; 166 | TOCUntilOperation t2 = ^(TOCCancelToken* until) { [until whenCancelledDo:^{ [s2 trySetFailedWithCancel]; }]; return s2.future; }; 167 | TOCUntilOperation t3 = ^(TOCCancelToken* until) { [until whenCancelledDo:^{ [s3 trySetFailedWithCancel]; }]; return s3.future; }; 168 | 169 | TOCFuture* f = [@[t1, t2, t3] toc_raceForWinnerLastingUntil:nil]; 170 | test(s1.future.isIncomplete); 171 | test(s2.future.isIncomplete); 172 | test(s3.future.isIncomplete); 173 | test(f.isIncomplete); 174 | 175 | [s1 forceSetResult:@7]; 176 | testFutureHasResult(f, @7); 177 | testFutureHasResult(s1.future, @7); 178 | test(s2.future.hasFailedWithCancel); 179 | test(s3.future.hasFailedWithCancel); 180 | } 181 | -(void) testAsyncRaceAsynchronousResultUntilCancelledOperationsUntil_CancelDuring_OperationsFailToCancel { 182 | TOCFutureSource* s1 = [TOCFutureSource new]; 183 | TOCFutureSource* s2 = [TOCFutureSource new]; 184 | TOCUntilOperation t1 = ^(TOCCancelToken* until) { [until whenCancelledDo:^{ [s1 trySetFailedWithCancel]; }]; return [TOCFutureSource new].future; }; 185 | TOCUntilOperation t2 = ^(TOCCancelToken* until) { [until whenCancelledDo:^{ [s2 trySetFailedWithCancel]; }]; return [TOCFutureSource new].future; }; 186 | 187 | TOCCancelTokenSource* c = [TOCCancelTokenSource new]; 188 | TOCFuture* f = [@[t1, t2] toc_raceForWinnerLastingUntil:c.token]; 189 | test(s1.future.isIncomplete); 190 | test(s1.future.isIncomplete); 191 | test(f.isIncomplete); 192 | 193 | [c cancel]; 194 | test(f.hasFailedWithCancel); 195 | test(s1.future.hasFailedWithCancel); 196 | test(s2.future.hasFailedWithCancel); 197 | } 198 | -(void) testAsyncRaceAsynchronousResultUntilCancelledOperationsUntil_CancelDuringRaceRacersBeingCancelled { 199 | TOCFutureSource* s1 = [TOCFutureSource new]; 200 | TOCFutureSource* s2 = [TOCFutureSource new]; 201 | TOCUntilOperation t1 = ^(TOCCancelToken* until) { [until whenCancelledDo:^{ [s1 trySetFailedWithCancel]; }]; return s1.future; }; 202 | TOCUntilOperation t2 = ^(TOCCancelToken* until) { [until whenCancelledDo:^{ [s2 trySetFailedWithCancel]; }]; return s2.future; }; 203 | 204 | TOCCancelTokenSource* c = [TOCCancelTokenSource new]; 205 | TOCFuture* f = [@[t1, t2] toc_raceForWinnerLastingUntil:c.token]; 206 | test(s1.future.isIncomplete); 207 | test(s1.future.isIncomplete); 208 | test(f.isIncomplete); 209 | 210 | [c cancel]; 211 | test(f.hasFailedWithCancel); 212 | test(s1.future.hasFailedWithCancel); 213 | test(s2.future.hasFailedWithCancel); 214 | } 215 | 216 | @end 217 | -------------------------------------------------------------------------------- /test/src/TOCFutureSourceTest.m: -------------------------------------------------------------------------------- 1 | #import "Testing.h" 2 | #import "CollapsingFutures.h" 3 | 4 | @interface TOCFutureSourceTest : XCTestCase 5 | @end 6 | 7 | @implementation TOCFutureSourceTest { 8 | @private NSThread* thread; 9 | @private NSRunLoop* runLoop; 10 | } 11 | 12 | -(void) setUp { 13 | thread = [[NSThread alloc] initWithTarget:self selector:@selector(runLoopUntilCancelled) object:nil]; 14 | [thread start]; 15 | 16 | while (true) { 17 | @synchronized(self) { 18 | if (runLoop != nil) break; 19 | } 20 | } 21 | } 22 | -(void) runLoopUntilCancelled { 23 | NSThread* curThread = [NSThread currentThread]; 24 | NSRunLoop* curRunLoop = NSRunLoop.currentRunLoop; 25 | @synchronized(self) { 26 | runLoop = curRunLoop; 27 | } 28 | while (![curThread isCancelled]) { 29 | [curRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]]; 30 | } 31 | } 32 | -(void) tearDown { 33 | [thread cancel]; 34 | } 35 | 36 | -(void)testFailedFutureSource { 37 | TOCFutureSource* s = [TOCFutureSource new]; 38 | TOCFuture* f = s.future; 39 | test([s trySetFailure:@"X"]); 40 | 41 | test(!f.isIncomplete); 42 | test(f.hasFailed); 43 | test(!f.hasResult); 44 | testFutureHasFailure(f, @"X"); 45 | testThrows(f.forceGetResult); 46 | test(f.description != nil); 47 | test(f.state == TOCFutureState_Failed); 48 | 49 | testDoesNotHitTarget([f thenDo:^(id result) { hitTarget; }]); 50 | testHitsTarget([f catchDo:^(id result) { hitTarget; }]); 51 | testHitsTarget([f finallyDo:^(id result) { hitTarget; }]); 52 | testFutureHasFailure([f then:^(id result) { return @2; }], @"X"); 53 | testFutureHasResult([f catch:^(id result) { return @3; }], @3); 54 | testFutureHasResult([f finally:^(id result) { return @4; }], @4); 55 | } 56 | -(void)testSucceededFutureSource { 57 | TOCFutureSource* s = [TOCFutureSource new]; 58 | TOCFuture* f = s.future; 59 | test([s trySetResult:@"X"]); 60 | 61 | test(!f.isIncomplete); 62 | test(!f.hasFailed); 63 | test(f.hasResult); 64 | testFutureHasResult(f, @"X"); 65 | testThrows(f.forceGetFailure); 66 | test(f.description != nil); 67 | test(f.state == TOCFutureState_CompletedWithResult); 68 | 69 | testHitsTarget([f thenDo:^(id result) { hitTarget; }]); 70 | testDoesNotHitTarget([f catchDo:^(id result) { hitTarget; }]); 71 | testHitsTarget([f finallyDo:^(id result) { hitTarget; }]); 72 | testFutureHasResult([f then:^(id result) { return @2; }], @2); 73 | testFutureHasResult([f catch:^(id result) { return @3; }], @"X"); 74 | testFutureHasResult([f finally:^(id result) { return @4; }], @4); 75 | } 76 | -(void)testIncompleteFutureSource { 77 | TOCFutureSource* s = [TOCFutureSource new]; 78 | TOCFuture* f = s.future; 79 | 80 | test(f.isIncomplete); 81 | test(!f.hasFailed); 82 | test(!f.hasResult); 83 | testThrows(f.forceGetResult); 84 | testThrows(f.forceGetFailure); 85 | test(f.description != nil); 86 | test(f.state == TOCFutureState_AbleToBeSet); 87 | 88 | testDoesNotHitTarget([f thenDo:^(id result) { hitTarget; }]); 89 | testDoesNotHitTarget([f catchDo:^(id result) { hitTarget; }]); 90 | testDoesNotHitTarget([f finallyDo:^(id result) { hitTarget; }]); 91 | test([f then:^(id result) { return @2; }].isIncomplete); 92 | test([f catch:^(id result) { return @3; }].isIncomplete); 93 | test([f finally:^(id result) { return @4; }].isIncomplete); 94 | } 95 | 96 | -(void)testCollapsedFailedFutureSource { 97 | TOCFutureSource* s = [TOCFutureSource new]; 98 | TOCFuture* f = s.future; 99 | test([s trySetResult:[TOCFuture futureWithFailure:@"X"]]); 100 | 101 | test(!f.isIncomplete); 102 | test(f.hasFailed); 103 | test(!f.hasResult); 104 | testFutureHasFailure(f, @"X"); 105 | testThrows(f.forceGetResult); 106 | test(f.description != nil); 107 | test(f.state == TOCFutureState_Failed); 108 | 109 | testDoesNotHitTarget([f thenDo:^(id result) { hitTarget; }]); 110 | testHitsTarget([f catchDo:^(id result) { hitTarget; }]); 111 | testHitsTarget([f finallyDo:^(id result) { hitTarget; }]); 112 | testFutureHasFailure([f then:^(id result) { return @2; }], @"X"); 113 | testFutureHasResult([f catch:^(id result) { return @3; }], @3); 114 | testFutureHasResult([f finally:^(id result) { return @4; }], @4); 115 | } 116 | -(void)testCollapsedSucceededFutureSource { 117 | TOCFutureSource* s = [TOCFutureSource new]; 118 | TOCFuture* f = s.future; 119 | test([s trySetResult:[TOCFuture futureWithResult:@"X"]]); 120 | 121 | test(!f.isIncomplete); 122 | test(!f.hasFailed); 123 | test(f.hasResult); 124 | testFutureHasResult(f, @"X"); 125 | test([f.forceGetResult isEqual:@"X"]); 126 | testThrows(f.forceGetFailure); 127 | test(f.description != nil); 128 | test(f.state == TOCFutureState_CompletedWithResult); 129 | 130 | testHitsTarget([f thenDo:^(id result) { hitTarget; }]); 131 | testDoesNotHitTarget([f catchDo:^(id result) { hitTarget; }]); 132 | testHitsTarget([f finallyDo:^(id result) { hitTarget; }]); 133 | testFutureHasResult([f then:^(id result) { return @2; }], @2); 134 | testFutureHasResult([f catch:^(id result) { return @3; }], @"X"); 135 | testFutureHasResult([f finally:^(id result) { return @4; }], @4); 136 | } 137 | -(void)testCollapsedIncompleteFutureSource { 138 | TOCFutureSource* s = [TOCFutureSource new]; 139 | TOCFuture* f = s.future; 140 | test([s trySetResult:[TOCFutureSource new].future]); 141 | 142 | test(f.isIncomplete); 143 | test(!f.hasFailed); 144 | test(!f.hasResult); 145 | testThrows(f.forceGetResult); 146 | testThrows(f.forceGetFailure); 147 | test(f.description != nil); 148 | test(f.state == TOCFutureState_Flattening); 149 | 150 | testDoesNotHitTarget([f thenDo:^(id result) { hitTarget; }]); 151 | testDoesNotHitTarget([f catchDo:^(id result) { hitTarget; }]); 152 | testDoesNotHitTarget([f finallyDo:^(id result) { hitTarget; }]); 153 | test([f then:^(id result) { return @2; }].isIncomplete); 154 | test([f catch:^(id result) { return @3; }].isIncomplete); 155 | test([f finally:^(id result) { return @4; }].isIncomplete); 156 | } 157 | -(void)testImmortalFutureFromSource { 158 | TOCFuture* f; 159 | @autoreleasepool { 160 | f = [TOCFutureSource new].future; 161 | } 162 | 163 | test(f.state == TOCFutureState_Immortal); 164 | test(f.isIncomplete); 165 | test(!f.hasFailed); 166 | test(!f.hasResult); 167 | } 168 | 169 | -(void)testCyclicCollapsedFutureIsIncomplete { 170 | TOCFutureSource* s = [TOCFutureSource new]; 171 | TOCFuture* f = s.future; 172 | test([s trySetResult:f]); 173 | test(f.isIncomplete); 174 | } 175 | -(void)testDealloc_CompletedFutureDiscardsCallbacksAfterRunning { 176 | DeallocCounter* d = [DeallocCounter new]; 177 | TOCFutureSource* s; 178 | @autoreleasepool { 179 | s = [TOCFutureSource new]; 180 | DeallocToken* dToken = [d makeToken]; 181 | [s.future finallyDo:^(TOCFuture* completed){ 182 | [dToken poke]; 183 | } unless:nil]; 184 | test(d.lostTokenCount == 0); 185 | [s trySetResult:nil]; 186 | } 187 | test(d.lostTokenCount == 1); 188 | test(s != nil); 189 | } 190 | -(void)testDealloc_CyclicFutureDiscardsCallbacksWithoutRunning { 191 | DeallocCounter* d = [DeallocCounter new]; 192 | TOCFutureSource* s; 193 | @autoreleasepool { 194 | s = [TOCFutureSource new]; 195 | DeallocToken* dToken = [d makeToken]; 196 | [s.future finallyDo:^(TOCFuture* completed){ 197 | test(false); 198 | [dToken poke]; 199 | } unless:nil]; 200 | test(d.lostTokenCount == 0); 201 | [s trySetResult:s.future]; 202 | } 203 | test(d.lostTokenCount == 1); 204 | test(s != nil); 205 | } 206 | 207 | -(void)testTrySetMany1 { 208 | TOCFutureSource* f = [TOCFutureSource new]; 209 | test([f trySetResult:@1]); 210 | test(![f trySetResult:@2]); 211 | test(![f trySetResult:@1]); 212 | test(![f trySetFailure:@1]); 213 | } 214 | -(void)testTrySetMany2 { 215 | TOCFutureSource* f = [TOCFutureSource new]; 216 | test([f trySetResult:[TOCFutureSource new]]); 217 | test(![f trySetResult:@2]); 218 | test(![f trySetResult:@1]); 219 | test(![f trySetFailure:@1]); 220 | } 221 | -(void)testTrySetMany3 { 222 | TOCFutureSource* f = [TOCFutureSource new]; 223 | test([f trySetFailure:@1]); 224 | test(![f trySetResult:@2]); 225 | test(![f trySetResult:@1]); 226 | test(![f trySetFailure:@1]); 227 | } 228 | -(void)testTrySetMany4 { 229 | TOCFutureSource* f = [TOCFutureSource new]; 230 | test([f trySetFailure:[TOCFutureSource new]]); 231 | test(![f trySetResult:@2]); 232 | test(![f trySetResult:@1]); 233 | test(![f trySetFailure:@1]); 234 | } 235 | -(void)testDeferredResultTrySet { 236 | TOCFutureSource* f = [TOCFutureSource new]; 237 | TOCFutureSource* g = [TOCFutureSource new]; 238 | [f trySetResult:g.future]; 239 | test(f.future.isIncomplete); 240 | [g trySetResult:@1]; 241 | testFutureHasResult(f.future, @1); 242 | } 243 | -(void)testDeferredFailTrySet { 244 | TOCFutureSource* f = [TOCFutureSource new]; 245 | TOCFutureSource* g = [TOCFutureSource new]; 246 | [f trySetResult:g.future]; 247 | test(f.future.isIncomplete); 248 | [g trySetFailure:@1]; 249 | testFutureHasFailure(f.future, @1); 250 | } 251 | -(void) testForceSetResult { 252 | TOCFutureSource* f = [TOCFutureSource new]; 253 | [f forceSetResult:@1]; 254 | testThrows([f forceSetResult:@1]); 255 | testThrows([f forceSetResult:@2]); 256 | testThrows([f forceSetFailure:@3]); 257 | } 258 | -(void) testForceSetFailure { 259 | TOCFutureSource* f = [TOCFutureSource new]; 260 | [f forceSetFailure:@1]; 261 | testThrows([f forceSetResult:@1]); 262 | testThrows([f forceSetResult:@2]); 263 | testThrows([f forceSetFailure:@3]); 264 | } 265 | 266 | -(void) testTrySetFailedWithCancel { 267 | TOCFutureSource* f = [TOCFutureSource new]; 268 | test(f.future.state == TOCFutureState_AbleToBeSet); 269 | 270 | test([f trySetFailedWithCancel]); 271 | test(f.future.state == TOCFutureState_Failed); 272 | test(f.future.hasFailedWithCancel); 273 | test([f.future.forceGetFailure isKindOfClass:[TOCCancelToken class]]); 274 | 275 | test(![f trySetFailedWithCancel]); 276 | test(f.future.state == TOCFutureState_Failed); 277 | test(f.future.hasFailedWithCancel); 278 | } 279 | -(void) testForceSetFailedWithCancel { 280 | TOCFutureSource* f = [TOCFutureSource new]; 281 | test(f.future.state == TOCFutureState_AbleToBeSet); 282 | 283 | [f forceSetFailedWithCancel]; 284 | test(f.future.state == TOCFutureState_Failed); 285 | test(f.future.hasFailedWithCancel); 286 | test([f.future.forceGetFailure isKindOfClass:[TOCCancelToken class]]); 287 | 288 | testThrows([f forceSetFailedWithCancel]); 289 | test(f.future.state == TOCFutureState_Failed); 290 | test(f.future.hasFailedWithCancel); 291 | } 292 | 293 | -(void) testTrySetFailedWithTimeout { 294 | TOCFutureSource* f = [TOCFutureSource new]; 295 | test(f.future.state == TOCFutureState_AbleToBeSet); 296 | 297 | test([f trySetFailedWithTimeout]); 298 | test(f.future.state == TOCFutureState_Failed); 299 | test(f.future.hasFailedWithTimeout); 300 | test([f.future.forceGetFailure isKindOfClass:[TOCTimeout class]]); 301 | 302 | test(![f trySetFailedWithTimeout]); 303 | test(f.future.state == TOCFutureState_Failed); 304 | test(f.future.hasFailedWithTimeout); 305 | } 306 | -(void) testForceSetFailedWithTimeout { 307 | TOCFutureSource* f = [TOCFutureSource new]; 308 | test(f.future.state == TOCFutureState_AbleToBeSet); 309 | 310 | [f forceSetFailedWithTimeout]; 311 | test(f.future.state == TOCFutureState_Failed); 312 | test(f.future.hasFailedWithTimeout); 313 | test([f.future.forceGetFailure isKindOfClass:[TOCTimeout class]]); 314 | 315 | testThrows([f forceSetFailedWithTimeout]); 316 | test(f.future.state == TOCFutureState_Failed); 317 | test(f.future.hasFailedWithTimeout); 318 | } 319 | -(void) testImmortalPropagatesWhenFlattening { 320 | TOCFuture* f1; 321 | TOCFuture* f2; 322 | @autoreleasepool { 323 | TOCFutureSource* s1 = [TOCFutureSource new]; 324 | TOCFutureSource* s2 = [TOCFutureSource new]; 325 | f1 = s1.future; 326 | f2 = s2.future; 327 | [s2 forceSetResult:s1.future]; 328 | } 329 | test(f1.state == TOCFutureState_Immortal); 330 | test(f2.state == TOCFutureState_Immortal); 331 | } 332 | -(void) testUnwrappingWorksWhenSourceGoesAway { 333 | TOCFuture* f1; 334 | TOCFutureSource* s2 = [TOCFutureSource new]; 335 | @autoreleasepool { 336 | TOCFutureSource* s1 = [TOCFutureSource new]; 337 | f1 = s1.future; 338 | [s1 forceSetResult:s2.future]; 339 | } 340 | test(f1.state == TOCFutureState_Flattening); 341 | [s2 trySetResult:@2]; 342 | testFutureHasResult(f1, @2); 343 | } 344 | -(void) testImmortalityCycleCollapses { 345 | TOCFuture* f1; 346 | TOCFuture* f2; 347 | @autoreleasepool { 348 | TOCFutureSource* s1 = [TOCFutureSource new]; 349 | TOCFutureSource* s2 = [TOCFutureSource new]; 350 | f1 = s1.future; 351 | f2 = s2.future; 352 | [s2 forceSetResult:s1.future]; 353 | [s1 forceSetResult:s2.future]; 354 | } 355 | test(f1.state == TOCFutureState_Immortal); 356 | test(f2.state == TOCFutureState_Immortal); 357 | } 358 | -(void) testImmortalityComplexCycleCollapses { 359 | TOCFuture* f1; 360 | TOCFuture* f2; 361 | TOCFuture* f3; 362 | TOCFuture* f4; 363 | TOCFuture* f5; 364 | TOCFuture* f6; 365 | @autoreleasepool { 366 | TOCFutureSource* s1 = [TOCFutureSource new]; 367 | TOCFutureSource* s2 = [TOCFutureSource new]; 368 | TOCFutureSource* s3 = [TOCFutureSource new]; 369 | TOCFutureSource* s4 = [TOCFutureSource new]; 370 | TOCFutureSource* s5 = [TOCFutureSource new]; 371 | TOCFutureSource* s6 = [TOCFutureSource new]; 372 | f1 = s1.future; 373 | f2 = s2.future; 374 | f3 = s3.future; 375 | f4 = s4.future; 376 | f5 = s5.future; 377 | f6 = s6.future; 378 | [s1 forceSetResult:s3.future]; 379 | [s2 forceSetResult:s3.future]; 380 | 381 | [s3 forceSetResult:s4.future]; 382 | [s4 forceSetResult:s5.future]; 383 | [s5 forceSetResult:s6.future]; 384 | [s6 forceSetResult:s3.future]; 385 | } 386 | test(f1.state == TOCFutureState_Immortal); 387 | test(f2.state == TOCFutureState_Immortal); 388 | test(f3.state == TOCFutureState_Immortal); 389 | test(f4.state == TOCFutureState_Immortal); 390 | test(f5.state == TOCFutureState_Immortal); 391 | test(f6.state == TOCFutureState_Immortal); 392 | } 393 | -(void) testImmortalityCyclesCollapseEagerly { 394 | TOCFuture* f1; 395 | TOCFuture* f2; 396 | TOCFuture* f3; 397 | TOCFutureSource* s1; 398 | @autoreleasepool { 399 | s1 = [TOCFutureSource new]; 400 | TOCFutureSource* s2 = [TOCFutureSource new]; 401 | TOCFutureSource* s3 = [TOCFutureSource new]; 402 | f1 = s1.future; 403 | f2 = s2.future; 404 | f3 = s3.future; 405 | [s1 forceSetResult:s3.future]; 406 | [s2 forceSetResult:s3.future]; 407 | [s3 forceSetResult:s2.future]; 408 | } 409 | test(f1.state == TOCFutureState_Immortal); 410 | test(f2.state == TOCFutureState_Immortal); 411 | test(f3.state == TOCFutureState_Immortal); 412 | } 413 | -(void) testImmortalityCycleCollapses_LongCycles { 414 | for (NSUInteger n = 90; n < 100; n++) { 415 | NSMutableArray* futures = [NSMutableArray new]; 416 | @autoreleasepool { 417 | NSMutableArray* futureSources = [NSMutableArray new]; 418 | for (NSUInteger i = 0; i < n; i++) { 419 | TOCFutureSource* fs = [TOCFutureSource new]; 420 | [futureSources addObject:fs]; 421 | [futures addObject:fs.future]; 422 | } 423 | for (NSUInteger i = 0; i < n-1; i++) { 424 | TOCFutureSource* f1 = futureSources[i]; 425 | TOCFutureSource* f2 = futureSources[i+1]; 426 | [f1 trySetResult:f2.future]; 427 | } 428 | TOCFutureSource* fn = futureSources.lastObject; 429 | TOCFutureSource* f0 = futureSources.firstObject; 430 | [fn forceSetResult:f0.future]; 431 | 432 | bool someHaveImmortalized = false; 433 | for (TOCFuture* f in futures) { 434 | someHaveImmortalized |= (f.state == TOCFutureState_Immortal); 435 | } 436 | test(someHaveImmortalized); 437 | } 438 | test(futures.count > 0); 439 | for (TOCFuture* f in futures) { 440 | test(f.state == TOCFutureState_Immortal); 441 | } 442 | } 443 | } 444 | 445 | @end 446 | -------------------------------------------------------------------------------- /test/src/TOCFutureTest.m: -------------------------------------------------------------------------------- 1 | #import "Testing.h" 2 | #import "CollapsingFutures.h" 3 | 4 | @interface TOCFutureTest : XCTestCase 5 | @end 6 | 7 | @implementation TOCFutureTest 8 | 9 | -(void)testFailedFuture { 10 | TOCFuture* f = [TOCFuture futureWithFailure:@"X"]; 11 | 12 | test(!f.isIncomplete); 13 | test(f.hasFailed); 14 | test(!f.hasResult); 15 | test([f.forceGetFailure isEqual:@"X"]); 16 | testFutureHasFailure(f, @"X"); 17 | testThrows(f.forceGetResult); 18 | test(f.description != nil); 19 | test(f.state == TOCFutureState_Failed); 20 | 21 | // redundant check of continuations all in one place 22 | testDoesNotHitTarget([f thenDo:^(id result) { hitTarget; }]); 23 | testHitsTarget([f catchDo:^(id result) { hitTarget; }]); 24 | testHitsTarget([f finallyDo:^(id result) { hitTarget; }]); 25 | testFutureHasFailure([f then:^(id result) { return @2; }], @"X"); 26 | testFutureHasResult([f catch:^(id result) { return @3; }], @3); 27 | testFutureHasResult([f finally:^(id result) { return @4; }], @4); 28 | } 29 | -(void)testSucceededFuture { 30 | TOCFuture* f = [TOCFuture futureWithResult:@"X"]; 31 | 32 | test(!f.isIncomplete); 33 | test(!f.hasFailed); 34 | test(f.hasResult); 35 | test([f.forceGetResult isEqual:@"X"]); 36 | testFutureHasResult(f, @"X"); 37 | testThrows(f.forceGetFailure); 38 | test(f.description != nil); 39 | test(f.state == TOCFutureState_CompletedWithResult); 40 | 41 | // redundant check of continuations all in one place 42 | testHitsTarget([f thenDo:^(id result) { hitTarget; }]); 43 | testDoesNotHitTarget([f catchDo:^(id result) { hitTarget; }]); 44 | testHitsTarget([f finallyDo:^(id result) { hitTarget; }]); 45 | testFutureHasResult([f then:^(id result) { return @2; }], @2); 46 | testFutureHasResult([f catch:^(id result) { return @3; }], @"X"); 47 | testFutureHasResult([f finally:^(id result) { return @4; }], @4); 48 | } 49 | 50 | -(void)testFinallyDoUnless_Immediate { 51 | testDoesNotHitTarget([[TOCFutureSource new].future finallyDo:^(TOCFuture* completed) { hitTarget; } unless:nil]); 52 | testHitsTarget([[TOCFuture futureWithResult:@7] finallyDo:^(TOCFuture* completed) { testFutureHasResult(completed, @7); hitTarget; } unless:nil]); 53 | testHitsTarget([[TOCFuture futureWithFailure:@8] finallyDo:^(TOCFuture* completed) { testFutureHasFailure(completed, @8); hitTarget; } unless:nil]); 54 | 55 | TOCCancelTokenSource* c = [TOCCancelTokenSource new]; 56 | testDoesNotHitTarget([[TOCFutureSource new].future finallyDo:^(TOCFuture* completed) { hitTarget; } unless:c.token]); 57 | testHitsTarget([[TOCFuture futureWithResult:@7] finallyDo:^(TOCFuture* completed) { testFutureHasResult(completed, @7); hitTarget; } unless:c.token]); 58 | testHitsTarget([[TOCFuture futureWithFailure:@8] finallyDo:^(TOCFuture* completed) { testFutureHasFailure(completed, @8); hitTarget; } unless:c.token]); 59 | 60 | [c cancel]; 61 | testDoesNotHitTarget([[TOCFutureSource new].future finallyDo:^(TOCFuture* completed) { hitTarget; } unless:c.token]); 62 | testDoesNotHitTarget([[TOCFuture futureWithResult:@7] finallyDo:^(TOCFuture* completed) { hitTarget; } unless:c.token]); 63 | testDoesNotHitTarget([[TOCFuture futureWithFailure:@8] finallyDo:^(TOCFuture* completed) { hitTarget; } unless:c.token]); 64 | } 65 | -(void)testFinallyDoUnless_DeferredResult { 66 | TOCFutureSource* s = [TOCFutureSource new]; 67 | TOCFuture* f = s.future; 68 | TOCCancelTokenSource* c = [TOCCancelTokenSource new]; 69 | testDoesNotHitTarget([f finallyDo:^(TOCFuture* value) { test(value == f); hitTarget; } unless:[c token]]); 70 | testHitsTarget([s trySetResult:@"X"]); 71 | } 72 | -(void)testFinallyDoUnless_DeferredFail { 73 | TOCFutureSource* s = [TOCFutureSource new]; 74 | TOCFuture* f = s.future; 75 | TOCCancelTokenSource* c = [TOCCancelTokenSource new]; 76 | testDoesNotHitTarget([f finallyDo:^(TOCFuture* value) { test(value == f); hitTarget; } unless:[c token]]); 77 | testHitsTarget([s trySetFailure:@"X"]); 78 | } 79 | -(void)testFinallyDoUnless_DeferredCancel { 80 | TOCFutureSource* s = [TOCFutureSource new]; 81 | TOCFuture* f = s.future; 82 | TOCCancelTokenSource* c = [TOCCancelTokenSource new]; 83 | testDoesNotHitTarget([f finallyDo:^(TOCFuture* value) { test(false); hitTarget; } unless:[c token]]); 84 | [c cancel]; 85 | testDoesNotHitTarget([s trySetFailure:@"X"]); 86 | } 87 | 88 | -(void)testFinallyUnless_Immediate { 89 | testDoesNotHitTarget([[TOCFutureSource new].future finally:^id(TOCFuture* completed) { hitTarget; return nil; } unless:nil]); 90 | testHitsTarget([[TOCFuture futureWithResult:@7] finally:^id(TOCFuture* completed) { testFutureHasResult(completed, @7); hitTarget; return nil; } unless:nil]); 91 | testHitsTarget([[TOCFuture futureWithFailure:@8] finally:^id(TOCFuture* completed) { testFutureHasFailure(completed, @8); hitTarget; return nil; } unless:nil]); 92 | test([[TOCFutureSource new].future finally:^id(TOCFuture* completed) { return @1; } unless:nil].isIncomplete); 93 | testFutureHasResult([[TOCFuture futureWithResult:@7] finally:^id(TOCFuture* completed) { return @2; } unless:nil], @2); 94 | testFutureHasResult([[TOCFuture futureWithFailure:@8] finally:^id(TOCFuture* completed) { return @3; } unless:nil], @3); 95 | 96 | TOCCancelTokenSource* c = [TOCCancelTokenSource new]; 97 | testDoesNotHitTarget([[TOCFutureSource new].future finally:^id(TOCFuture* completed) { hitTarget; return nil; } unless:c.token]); 98 | testHitsTarget([[TOCFuture futureWithResult:@7] finally:^id(TOCFuture* completed) { testFutureHasResult(completed, @7); hitTarget; return nil; } unless:c.token]); 99 | testHitsTarget([[TOCFuture futureWithFailure:@8] finally:^id(TOCFuture* completed) { testFutureHasFailure(completed, @8); hitTarget; return nil; } unless:c.token]); 100 | test([[TOCFutureSource new].future finally:^id(TOCFuture* completed) { return @1; } unless:c.token].isIncomplete); 101 | testFutureHasResult([[TOCFuture futureWithResult:@7] finally:^id(TOCFuture* completed) { return @2; } unless:c.token], @2); 102 | testFutureHasResult([[TOCFuture futureWithFailure:@8] finally:^id(TOCFuture* completed) { return @3; } unless:c.token], @3); 103 | 104 | [c cancel]; 105 | testDoesNotHitTarget([[TOCFutureSource new].future finally:^id(TOCFuture* completed) { hitTarget; return nil; } unless:c.token]); 106 | testDoesNotHitTarget([[TOCFuture futureWithResult:@7] finally:^id(TOCFuture* completed) { hitTarget; return nil; } unless:c.token]); 107 | testDoesNotHitTarget([[TOCFuture futureWithFailure:@8] finally:^id(TOCFuture* completed) { hitTarget; return nil; } unless:c.token]); 108 | test([[TOCFutureSource new].future finally:^id(TOCFuture* completed) { return @1; } unless:c.token].hasFailedWithCancel); 109 | test([[TOCFuture futureWithResult:@7] finally:^id(TOCFuture* completed) { return @2; } unless:c.token].hasFailedWithCancel); 110 | test([[TOCFuture futureWithFailure:@8] finally:^id(TOCFuture* completed) { return @3; } unless:c.token].hasFailedWithCancel); 111 | } 112 | 113 | -(void)testThenDoUnless_Immediate { 114 | testDoesNotHitTarget([[TOCFutureSource new].future thenDo:^(id result) { hitTarget; } unless:nil]); 115 | testHitsTarget([[TOCFuture futureWithResult:@7] thenDo:^(id result) { testEq(result, @7); hitTarget; } unless:nil]); 116 | testDoesNotHitTarget([[TOCFuture futureWithFailure:@8] thenDo:^(id result) { hitTarget; } unless:nil]); 117 | 118 | TOCCancelTokenSource* c = [TOCCancelTokenSource new]; 119 | testDoesNotHitTarget([[TOCFutureSource new].future thenDo:^(id result) { hitTarget; } unless:c.token]); 120 | testHitsTarget([[TOCFuture futureWithResult:@7] thenDo:^(id result) { testEq(result, @7); hitTarget; } unless:c.token]); 121 | testDoesNotHitTarget([[TOCFuture futureWithFailure:@8] thenDo:^(id result) { hitTarget; } unless:c.token]); 122 | 123 | [c cancel]; 124 | testDoesNotHitTarget([[TOCFutureSource new].future thenDo:^(id result) { hitTarget; } unless:c.token]); 125 | testDoesNotHitTarget([[TOCFuture futureWithResult:@7] thenDo:^(id result) { hitTarget; } unless:c.token]); 126 | testDoesNotHitTarget([[TOCFuture futureWithFailure:@8] thenDo:^(id result) { hitTarget; } unless:c.token]); 127 | } 128 | 129 | -(void)testThenUnless_Immediate { 130 | testDoesNotHitTarget([[TOCFutureSource new].future then:^id(id result) { hitTarget; return nil; } unless:nil]); 131 | testHitsTarget([[TOCFuture futureWithResult:@7] then:^id(id result) { testEq(result, @7); hitTarget; return nil; } unless:nil]); 132 | testDoesNotHitTarget([[TOCFuture futureWithFailure:@8] then:^id(id result) { hitTarget; return nil; } unless:nil]); 133 | test([[TOCFutureSource new].future then:^id(id result) { return @1; } unless:nil].isIncomplete); 134 | testFutureHasResult([[TOCFuture futureWithResult:@7] then:^id(id result) { return @2; } unless:nil], @2); 135 | testFutureHasFailure([[TOCFuture futureWithFailure:@8] then:^id(id result) { return @3; } unless:nil], @8); 136 | 137 | TOCCancelTokenSource* c = [TOCCancelTokenSource new]; 138 | testDoesNotHitTarget([[TOCFutureSource new].future then:^id(id result) { hitTarget; return nil; } unless:c.token]); 139 | testHitsTarget([[TOCFuture futureWithResult:@7] then:^id(id result) { testEq(result, @7); hitTarget; return nil; } unless:c.token]); 140 | testDoesNotHitTarget([[TOCFuture futureWithFailure:@8] then:^id(id result) { hitTarget; return nil; } unless:c.token]); 141 | test([[TOCFutureSource new].future then:^id(id result) { return @1; } unless:c.token].isIncomplete); 142 | testFutureHasResult([[TOCFuture futureWithResult:@7] then:^id(id result) { return @2; } unless:c.token], @2); 143 | testFutureHasFailure([[TOCFuture futureWithFailure:@8] then:^id(id result) { return @3; } unless:c.token], @8); 144 | 145 | [c cancel]; 146 | testDoesNotHitTarget([[TOCFutureSource new].future then:^id(id result) { hitTarget; return nil; } unless:c.token]); 147 | testDoesNotHitTarget([[TOCFuture futureWithResult:@7] then:^id(id result) { hitTarget; return nil; } unless:c.token]); 148 | testDoesNotHitTarget([[TOCFuture futureWithFailure:@8] then:^id(id result) { hitTarget; return nil; } unless:c.token]); 149 | test([[TOCFutureSource new].future then:^id(id result) { return @1; } unless:c.token].hasFailedWithCancel); 150 | test([[TOCFuture futureWithResult:@7] then:^id(id result) { return @2; } unless:c.token].hasFailedWithCancel); 151 | test([[TOCFuture futureWithFailure:@8] then:^id(id result) { return @3; } unless:c.token].hasFailedWithCancel); 152 | } 153 | 154 | -(void)testCatchDoUnless_Immediate { 155 | testDoesNotHitTarget([[TOCFutureSource new].future catchDo:^(id result) { hitTarget; } unless:nil]); 156 | testDoesNotHitTarget([[TOCFuture futureWithResult:@7] catchDo:^(id result) { hitTarget; } unless:nil]); 157 | testHitsTarget([[TOCFuture futureWithFailure:@8] catchDo:^(id result) { testEq(result, @8); hitTarget; } unless:nil]); 158 | 159 | TOCCancelTokenSource* c = [TOCCancelTokenSource new]; 160 | testDoesNotHitTarget([[TOCFutureSource new].future catchDo:^(id result) { hitTarget; } unless:c.token]); 161 | testDoesNotHitTarget([[TOCFuture futureWithResult:@7] catchDo:^(id result) { hitTarget; } unless:c.token]); 162 | testHitsTarget([[TOCFuture futureWithFailure:@8] catchDo:^(id result) { testEq(result, @8); hitTarget; } unless:c.token]); 163 | 164 | [c cancel]; 165 | testDoesNotHitTarget([[TOCFutureSource new].future catchDo:^(id result) { hitTarget; } unless:c.token]); 166 | testDoesNotHitTarget([[TOCFuture futureWithResult:@7] catchDo:^(id result) { hitTarget; } unless:c.token]); 167 | testDoesNotHitTarget([[TOCFuture futureWithFailure:@8] catchDo:^(id result) { hitTarget; } unless:c.token]); 168 | } 169 | 170 | -(void)testCatchUnless_Immediate { 171 | testDoesNotHitTarget([[TOCFutureSource new].future catch:^id(id failure) { hitTarget; return nil; } unless:nil]); 172 | testDoesNotHitTarget([[TOCFuture futureWithResult:@7] catch:^id(id failure) { hitTarget; return nil; } unless:nil]); 173 | testHitsTarget([[TOCFuture futureWithFailure:@8] catch:^id(id failure) { testEq(failure, @8); hitTarget; return nil; } unless:nil]); 174 | test([[TOCFutureSource new].future catch:^id(id failure) { return @1; } unless:nil].isIncomplete); 175 | testFutureHasResult([[TOCFuture futureWithResult:@7] catch:^id(id failure) { return @2; } unless:nil], @7); 176 | testFutureHasResult([[TOCFuture futureWithFailure:@8] catch:^id(id failure) { return @3; } unless:nil], @3); 177 | 178 | TOCCancelTokenSource* c = [TOCCancelTokenSource new]; 179 | testDoesNotHitTarget([[TOCFutureSource new].future catch:^id(id failure) { hitTarget; return nil; } unless:c.token]); 180 | testDoesNotHitTarget([[TOCFuture futureWithResult:@7] catch:^id(id failure) { hitTarget; return nil; } unless:c.token]); 181 | testHitsTarget([[TOCFuture futureWithFailure:@8] catch:^id(id failure) { testEq(failure, @8); hitTarget; return nil; } unless:c.token]); 182 | test([[TOCFutureSource new].future catch:^id(id failure) { return @1; } unless:c.token].isIncomplete); 183 | testFutureHasResult([[TOCFuture futureWithResult:@7] catch:^id(id failure) { return @2; } unless:c.token], @7); 184 | testFutureHasResult([[TOCFuture futureWithFailure:@8] catch:^id(id failure) { return @3; } unless:c.token], @3); 185 | 186 | [c cancel]; 187 | testDoesNotHitTarget([[TOCFutureSource new].future catch:^id(id failure) { hitTarget; return nil; } unless:c.token]); 188 | testDoesNotHitTarget([[TOCFuture futureWithResult:@7] catch:^id(id failure) { hitTarget; return nil; } unless:c.token]); 189 | testDoesNotHitTarget([[TOCFuture futureWithFailure:@8] catch:^id(id failure) { hitTarget; return nil; } unless:c.token]); 190 | test([[TOCFutureSource new].future catch:^id(id failure) { return @1; } unless:c.token].hasFailedWithCancel); 191 | test([[TOCFuture futureWithResult:@7] catch:^id(id failure) { return @2; } unless:c.token].hasFailedWithCancel); 192 | test([[TOCFuture futureWithFailure:@8] catch:^id(id failure) { return @3; } unless:c.token].hasFailedWithCancel); 193 | } 194 | 195 | -(void) testFinallyDo_StaysOnMainThread { 196 | TOCCancelTokenSource* c2 = [TOCCancelTokenSource new]; 197 | dispatch_after(DISPATCH_TIME_NOW, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 198 | TOCFutureSource* c1 = [TOCFutureSource new]; 199 | TOCFuture* f = [TOCFuture futureFromOperation:^id{ 200 | test(NSThread.isMainThread); 201 | [c1.future finallyDo:^(TOCFuture* completed){ 202 | test(NSThread.isMainThread); 203 | [c2 cancel]; 204 | } unless:c2.token]; 205 | return nil; 206 | } invokedOnThread:NSThread.mainThread]; 207 | 208 | test(!NSThread.isMainThread); 209 | testCompletesConcurrently(f); 210 | testFutureHasResult(f, nil); 211 | test(c2.token.state == TOCCancelTokenState_StillCancellable); 212 | 213 | [c1 trySetResult:nil]; 214 | }); 215 | 216 | for (int i = 0; i < 5 && !c2.token.isAlreadyCancelled; i++) { 217 | [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]]; 218 | } 219 | test(c2.token.state == TOCCancelTokenState_Cancelled); 220 | } 221 | 222 | -(void)testFutureEquality_viaObject { 223 | TOCFuture* fres = [TOCFuture futureWithResult:@1]; 224 | TOCFuture* ferr = [TOCFuture futureWithFailure:@1]; 225 | TOCFuture* fimm = [TOCFutureSource new].future; 226 | test(fimm.state == TOCFutureState_Immortal); 227 | TOCFutureSource* sinc = [TOCFutureSource new]; 228 | TOCFuture* finc = sinc.future; 229 | TOCFutureSource* slat = [TOCFutureSource new]; 230 | [slat forceSetResult:finc]; 231 | TOCFuture* flat = slat.future; 232 | 233 | // result 234 | test([fres isEqual:fres]); 235 | test([fres isEqual:[TOCFuture futureWithResult:@1]]); 236 | test(fres.hash == [TOCFuture futureWithResult:@1].hash); 237 | test([fres isEqual:[TOCFuture futureWithResult:fres]]); 238 | test(fres.hash == [TOCFuture futureWithResult:fres].hash); 239 | test(![fres isEqual:[TOCFuture futureWithFailure:fres]]); 240 | test(![fres isEqual:[TOCFuture futureWithResult:@2]]); 241 | test(![fres isEqual:ferr]); 242 | test(![fres isEqual:fimm]); 243 | test(![fres isEqual:finc]); 244 | test(![fres isEqual:sinc]); 245 | test(![fres isEqual:flat]); 246 | test(![fres isEqual:nil]); 247 | test(![fres isEqual:@1]); 248 | test(![fres isEqual:@[]]); 249 | 250 | // error 251 | test([ferr isEqual:ferr]); 252 | test([ferr isEqual:[TOCFuture futureWithFailure:@1]]); 253 | test(ferr.hash == [TOCFuture futureWithFailure:@1].hash); 254 | test([ferr isEqual:[TOCFuture futureWithResult:ferr]]); 255 | test(ferr.hash == [TOCFuture futureWithResult:ferr].hash); 256 | test(![ferr isEqual:[TOCFuture futureWithFailure:ferr]]); 257 | test(![ferr isEqual:[TOCFuture futureWithFailure:@2]]); 258 | test(![ferr isEqual:fres]); 259 | test(![ferr isEqual:fimm]); 260 | test(![ferr isEqual:finc]); 261 | test(![ferr isEqual:sinc]); 262 | test(![ferr isEqual:flat]); 263 | test(![ferr isEqual:nil]); 264 | test(![ferr isEqual:@1]); 265 | test(![ferr isEqual:@[]]); 266 | 267 | // immortal 268 | TOCFuture* imm2 = [TOCFutureSource new].future; 269 | test(imm2.state == TOCFutureState_Immortal); 270 | test([fimm isEqual:fimm]); 271 | test([fimm isEqual:imm2]); 272 | test(fimm.hash == imm2.hash); 273 | test([fimm isEqual:[TOCFuture futureWithResult:fimm]]); 274 | test(fimm.hash == [TOCFuture futureWithResult:fimm].hash); 275 | test(![fimm isEqual:[TOCFuture futureWithFailure:fimm]]); 276 | test(![fimm isEqual:ferr]); 277 | test(![fimm isEqual:fres]); 278 | test(![fimm isEqual:finc]); 279 | test(![fimm isEqual:sinc]); 280 | test(![fimm isEqual:flat]); 281 | test(![fimm isEqual:nil]); 282 | test(![fimm isEqual:@1]); 283 | test(![fimm isEqual:@[]]); 284 | 285 | // incomplete 286 | TOCFutureSource* sinc2 = [TOCFutureSource new]; 287 | TOCFuture* finc2 = sinc2.future; 288 | test([finc isEqual:finc]); 289 | test([finc isEqual:[TOCFuture futureWithResult:finc]]); // due to returning argument 290 | test(![finc isEqual:finc2]); 291 | test(![finc isEqual:[TOCFuture futureWithFailure:finc]]); 292 | test(![finc isEqual:ferr]); 293 | test(![finc isEqual:fres]); 294 | test(![finc isEqual:fimm]); 295 | test(![finc isEqual:sinc]); 296 | test(![finc isEqual:flat]); 297 | test(![finc isEqual:nil]); 298 | test(![finc isEqual:@1]); 299 | test(![finc isEqual:@[]]); 300 | 301 | // flattening 302 | test([flat isEqual:flat]); 303 | test([flat isEqual:[TOCFuture futureWithResult:flat]]); // due to returning argument 304 | test(![flat isEqual:finc2]); 305 | test(![flat isEqual:[TOCFuture futureWithFailure:fimm]]); 306 | test(![flat isEqual:ferr]); 307 | test(![flat isEqual:fres]); 308 | // (flat vs finc not specified due to efficiency issues keeping dependencies flattened) 309 | test(![flat isEqual:sinc]); 310 | test(![flat isEqual:fimm]); 311 | test(![flat isEqual:nil]); 312 | test(![flat isEqual:@1]); 313 | test(![flat isEqual:@[]]); 314 | 315 | // after completion 316 | [sinc forceSetResult:@2]; 317 | test([finc isEqual:[TOCFuture futureWithResult:@2]]); 318 | test(finc.hash == [TOCFuture futureWithResult:@2].hash); 319 | [sinc2 forceSetFailure:@3]; 320 | test([finc2 isEqual:[TOCFuture futureWithFailure:@3]]); 321 | test(finc2.hash == [TOCFuture futureWithFailure:@3].hash); 322 | 323 | // nil value 324 | test([[TOCFuture futureWithResult:nil] isEqual:[TOCFuture futureWithResult:nil]]); 325 | test(![[TOCFuture futureWithResult:nil] isEqual:[TOCFuture futureWithResult:@1]]); 326 | test(![[TOCFuture futureWithResult:@1] isEqual:[TOCFuture futureWithResult:nil]]); 327 | test([[TOCFuture futureWithFailure:nil] isEqual:[TOCFuture futureWithFailure:nil]]); 328 | test(![[TOCFuture futureWithFailure:nil] isEqual:[TOCFuture futureWithFailure:@1]]); 329 | test(![[TOCFuture futureWithFailure:@1] isEqual:[TOCFuture futureWithFailure:nil]]); 330 | } 331 | 332 | -(void)testFutureEquality_direct { 333 | TOCFuture* fres = [TOCFuture futureWithResult:@1]; 334 | TOCFuture* ferr = [TOCFuture futureWithFailure:@1]; 335 | TOCFuture* fimm = [TOCFutureSource new].future; 336 | test(fimm.state == TOCFutureState_Immortal); 337 | TOCFutureSource* sinc = [TOCFutureSource new]; 338 | TOCFuture* finc = sinc.future; 339 | TOCFutureSource* slat = [TOCFutureSource new]; 340 | [slat forceSetResult:finc]; 341 | TOCFuture* flat = slat.future; 342 | 343 | // result 344 | test([fres isEqualToFuture:fres]); 345 | test([fres isEqualToFuture:[TOCFuture futureWithResult:@1]]); 346 | test([fres isEqualToFuture:[TOCFuture futureWithResult:fres]]); 347 | test(![fres isEqualToFuture:[TOCFuture futureWithFailure:fres]]); 348 | test(![fres isEqualToFuture:[TOCFuture futureWithResult:@2]]); 349 | test(![fres isEqualToFuture:ferr]); 350 | test(![fres isEqualToFuture:fimm]); 351 | test(![fres isEqualToFuture:finc]); 352 | test(![fres isEqualToFuture:flat]); 353 | test(![fres isEqualToFuture:nil]); 354 | 355 | // error 356 | test([ferr isEqualToFuture:ferr]); 357 | test([ferr isEqualToFuture:[TOCFuture futureWithFailure:@1]]); 358 | test([ferr isEqualToFuture:[TOCFuture futureWithResult:ferr]]); 359 | test(![ferr isEqualToFuture:[TOCFuture futureWithFailure:ferr]]); 360 | test(![ferr isEqualToFuture:[TOCFuture futureWithFailure:@2]]); 361 | test(![ferr isEqualToFuture:fres]); 362 | test(![ferr isEqualToFuture:fimm]); 363 | test(![ferr isEqualToFuture:finc]); 364 | test(![ferr isEqualToFuture:flat]); 365 | test(![ferr isEqualToFuture:nil]); 366 | 367 | // immortal 368 | TOCFuture* imm2 = [TOCFutureSource new].future; 369 | test(imm2.state == TOCFutureState_Immortal); 370 | test([fimm isEqualToFuture:fimm]); 371 | test([fimm isEqualToFuture:imm2]); 372 | test(fimm.hash == imm2.hash); 373 | test([fimm isEqualToFuture:[TOCFuture futureWithResult:fimm]]); 374 | test(fimm.hash == [TOCFuture futureWithResult:fimm].hash); 375 | test(![fimm isEqualToFuture:[TOCFuture futureWithFailure:fimm]]); 376 | test(![fimm isEqualToFuture:ferr]); 377 | test(![fimm isEqualToFuture:fres]); 378 | test(![fimm isEqualToFuture:finc]); 379 | test(![fimm isEqualToFuture:flat]); 380 | test(![fimm isEqualToFuture:nil]); 381 | 382 | // incomplete 383 | TOCFutureSource* sinc2 = [TOCFutureSource new]; 384 | TOCFuture* finc2 = sinc2.future; 385 | test([finc isEqualToFuture:finc]); 386 | test([finc isEqualToFuture:[TOCFuture futureWithResult:finc]]); // due to returning argument 387 | test(![finc isEqualToFuture:finc2]); 388 | test(![finc isEqualToFuture:[TOCFuture futureWithFailure:finc]]); 389 | test(![finc isEqualToFuture:ferr]); 390 | test(![finc isEqualToFuture:fres]); 391 | test(![finc isEqualToFuture:fimm]); 392 | test(![finc isEqualToFuture:flat]); 393 | test(![finc isEqualToFuture:nil]); 394 | 395 | // flattening 396 | test([flat isEqualToFuture:flat]); 397 | test([flat isEqualToFuture:[TOCFuture futureWithResult:flat]]); // due to returning argument 398 | test(![flat isEqualToFuture:finc2]); 399 | test(![flat isEqualToFuture:[TOCFuture futureWithFailure:fimm]]); 400 | test(![flat isEqualToFuture:ferr]); 401 | test(![flat isEqualToFuture:fres]); 402 | // (flat vs finc not specified due to efficiency issues keeping dependencies flattened) 403 | test(![flat isEqualToFuture:fimm]); 404 | test(![flat isEqualToFuture:nil]); 405 | 406 | // after completion 407 | // note: complete differently lest the above comparison be technically implementation defined 408 | [sinc forceSetResult:@2]; 409 | [sinc2 forceSetFailure:@3]; 410 | test([finc isEqualToFuture:[TOCFuture futureWithResult:@2]]); 411 | test(finc.hash == [TOCFuture futureWithResult:@2].hash); 412 | test([finc2 isEqualToFuture:[TOCFuture futureWithFailure:@3]]); 413 | test(finc2.hash == [TOCFuture futureWithFailure:@3].hash); 414 | 415 | // nil value 416 | test([[TOCFuture futureWithResult:nil] isEqualToFuture:[TOCFuture futureWithResult:nil]]); 417 | test(![[TOCFuture futureWithResult:nil] isEqualToFuture:[TOCFuture futureWithResult:@1]]); 418 | test(![[TOCFuture futureWithResult:@1] isEqualToFuture:[TOCFuture futureWithResult:nil]]); 419 | test([[TOCFuture futureWithFailure:nil] isEqualToFuture:[TOCFuture futureWithFailure:nil]]); 420 | test(![[TOCFuture futureWithFailure:nil] isEqualToFuture:[TOCFuture futureWithFailure:@1]]); 421 | test(![[TOCFuture futureWithFailure:@1] isEqualToFuture:[TOCFuture futureWithFailure:nil]]); 422 | } 423 | 424 | @end 425 | --------------------------------------------------------------------------------