├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── LinuxMain.stencil ├── codecov.yml ├── jazzy.yml ├── ranger.yml ├── sourcery.yml └── workflows │ ├── cd.yml │ ├── ci-podspec.yml │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── .gitmodules ├── .tidelift.yml ├── .travis.yml ├── Documentation ├── Appendix.md ├── CommonPatterns.md ├── Examples │ ├── ImageCache.md │ ├── URLSession+BadResponseErrors.swift │ └── detweet.swift ├── FAQ.md ├── GettingStarted.md ├── Installation.md ├── ObjectiveC.md ├── README.md └── Troubleshooting.md ├── LICENSE ├── Package.swift ├── Package@swift-4.2.swift ├── Package@swift-5.0.swift ├── Package@swift-5.3.swift ├── PromiseKit.playground ├── Contents.swift ├── contents.xcplayground └── playground.xcworkspace │ └── contents.xcworkspacedata ├── PromiseKit.podspec ├── PromiseKit.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── xcshareddata │ └── xcschemes │ └── PromiseKit.xcscheme ├── README.md ├── Sources ├── AnyPromise+Private.h ├── AnyPromise.h ├── AnyPromise.m ├── AnyPromise.swift ├── Async.swift ├── Box.swift ├── Catchable.swift ├── Combine.swift ├── Configuration.swift ├── CustomStringConvertible.swift ├── Deprecations.swift ├── Error.swift ├── Guarantee.swift ├── Info.plist ├── LogEvent.swift ├── NSMethodSignatureForBlock.m ├── PMKCallVariadicBlock.m ├── Promise.swift ├── PromiseKit.h ├── Resolver.swift ├── Resources │ └── PrivacyInfo.xcprivacy ├── Thenable.swift ├── after.m ├── after.swift ├── dispatch_promise.m ├── firstly.swift ├── fwd.h ├── hang.m ├── hang.swift ├── join.m ├── race.m ├── race.swift ├── when.m └── when.swift ├── Tests ├── A+ │ ├── 0.0.0.swift │ ├── 2.1.2.swift │ ├── 2.1.3.swift │ ├── 2.2.2.swift │ ├── 2.2.3.swift │ ├── 2.2.4.swift │ ├── 2.2.6.swift │ ├── 2.2.7.swift │ ├── 2.3.1.swift │ ├── 2.3.2.swift │ ├── 2.3.4.swift │ ├── README.md │ └── XCTestManifests.swift ├── Bridging │ ├── BridgingTests.m │ ├── BridgingTests.swift │ ├── Infrastructure.h │ ├── Infrastructure.m │ └── Infrastructure.swift ├── CoreObjC │ ├── AnyPromiseTests.m │ ├── AnyPromiseTests.swift │ ├── HangTests.m │ ├── JoinTests.m │ ├── PMKManifoldTests.m │ ├── RaceTests.m │ └── WhenTests.m ├── CorePromise │ ├── AfterTests.swift │ ├── AsyncTests.swift │ ├── CancellableErrorTests.swift │ ├── CatchableTests.swift │ ├── CombineTests.swift │ ├── DefaultDispatchQueueTests.swift │ ├── ErrorTests.swift │ ├── GuaranteeTests.swift │ ├── HangTests.swift │ ├── LoggingTests.swift │ ├── PromiseTests.swift │ ├── RaceTests.swift │ ├── RegressionTests.swift │ ├── ResolverTests.swift │ ├── StressTests.swift │ ├── ThenableTests.swift │ ├── Utilities.swift │ ├── WhenConcurrentTests.swift │ ├── WhenResolvedTests.swift │ ├── WhenTests.swift │ ├── XCTestManifests.swift │ └── ZalgoTests.swift ├── DeprecationTests.swift ├── JS-A+ │ ├── .gitignore │ ├── AllTests.swift │ ├── JSAdapter.swift │ ├── JSPromise.swift │ ├── JSUtils.swift │ ├── MockNodeEnvironment.swift │ ├── README.md │ ├── index.js │ ├── package-lock.json │ ├── package.json │ └── webpack.config.js └── LinuxMain.swift └── tea.yaml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | tidelift: "cocoapods/PromiseKit" 2 | patreon: "mxcl" 3 | github: mxcl 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | [PLEASE READ THE TROUBLESHOOTING GUIDE](https://github.com/mxcl/PromiseKit/blob/master/Documentation/Troubleshooting.md). 2 | 3 | --- 4 | 5 | You read the guide but it didn’t help? OK, we’re here to help. 6 | 7 | * Please specify the PromiseKit major version you are using 8 | * [Please format your code in triple backticks and ensure readable indentation](https://help.github.com/articles/creating-and-highlighting-code-blocks/) 9 | * Please specify how you installed PromiseKit, ie. Carthage, CocoaPods, SwiftPM or other. If other provide DETAILED information about how you are integrating PromiseKit. 10 | 11 | If you ignore this template we will close your ticket and link to this template until you provide this necessary information. We cannot help you without it. 12 | -------------------------------------------------------------------------------- /.github/LinuxMain.stencil: -------------------------------------------------------------------------------- 1 | @testable import Core 2 | @testable import A_ 3 | import XCTest 4 | 5 | //TODO get this to run on CI and don’t have it committed 6 | //NOTE problem is Sourcery doesn’t support Linux currently 7 | //USAGE: cd PromiseKit/Sources/.. && sourcery --config .github/sourcery.yml 8 | 9 | {% for type in types.classes|based:"XCTestCase" %} 10 | extension {{ type.name }} { 11 | static var allTests = [ 12 | {% for method in type.methods %}{% if method.parameters.count == 0 and method.shortName|hasPrefix:"test" %} ("{{ method.shortName }}", {{type.name}}.{{ method.shortName }}), 13 | {% endif %}{% endfor %}] 14 | } 15 | 16 | {% endfor %} 17 | XCTMain([ 18 | {% for type in types.classes|based:"XCTestCase" %}{% if not type.annotations.excludeFromLinuxMain %} testCase({{ type.name }}.allTests), 19 | {% endif %}{% endfor %}]) 20 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "Tests" 3 | - "README.md" 4 | - "Documentation" 5 | - ".travis.yml" 6 | 7 | codecov: 8 | notify: 9 | require_ci_to_pass: yes 10 | 11 | coverage: 12 | precision: 1 13 | round: up 14 | range: "70...100" 15 | 16 | status: 17 | project: yes 18 | patch: 19 | default: 20 | threshold: 0.2% 21 | changes: no 22 | 23 | parsers: 24 | gcov: 25 | branch_detection: 26 | conditional: yes 27 | loop: yes 28 | method: no 29 | macro: no 30 | 31 | comment: off 32 | -------------------------------------------------------------------------------- /.github/jazzy.yml: -------------------------------------------------------------------------------- 1 | module: PromiseKit 2 | custom_categories: 3 | - name: Core Components 4 | children: 5 | - Promise 6 | - Guarantee 7 | - Thenable 8 | - CatchMixin 9 | - Resolver 10 | xcodebuild_arguments: 11 | - UseModernBuildSystem=NO 12 | output: 13 | ../output 14 | # output directory is relative to config file… ugh 15 | readme: 16 | Documentation/README.md 17 | theme: 18 | fullwidth 19 | -------------------------------------------------------------------------------- /.github/ranger.yml: -------------------------------------------------------------------------------- 1 | merges: 2 | - action: delete_branch 3 | -------------------------------------------------------------------------------- /.github/sourcery.yml: -------------------------------------------------------------------------------- 1 | sources: 2 | include: 3 | - ../Tests/A+ 4 | - ../Tests/CorePromise 5 | exclude: 6 | - ../Tests/A+/0.0.0.swift 7 | - ../Tests/CorePromise/Utilities.swift 8 | templates: 9 | include: 10 | - LinuxMain.stencil 11 | output: 12 | ../Tests/LinuxMain.swift 13 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version: 6 | required: true 7 | jobs: 8 | pods: 9 | runs-on: macos-latest 10 | steps: 11 | 12 | - name: Start Deployment 13 | uses: bobheadxi/deployments@v0.5.2 14 | id: deployment 15 | with: 16 | step: start 17 | token: ${{ secrets.GITHUB_TOKEN }} 18 | env: pods 19 | 20 | - uses: actions/checkout@v3 21 | with: 22 | submodules: true 23 | 24 | - run: pod trunk push --allow-warnings --skip-tests --skip-import-validation 25 | env: 26 | COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} 27 | 28 | - name: Seal Deployment 29 | uses: bobheadxi/deployments@v0.5.2 30 | if: always() 31 | with: 32 | step: finish 33 | token: ${{ secrets.GITHUB_TOKEN }} 34 | status: ${{ job.status }} 35 | deployment_id: ${{ steps.deployment.outputs.deployment_id }} 36 | 37 | # carthage: 38 | # runs-on: macos-latest 39 | # steps: 40 | 41 | # - name: Start Deployment 42 | # uses: bobheadxi/deployments@v0.5.2 43 | # id: deployment 44 | # with: 45 | # step: start 46 | # token: ${{ secrets.GITHUB_TOKEN }} 47 | # env: carthage 48 | 49 | # - uses: maxim-lobanov/setup-xcode@v1 50 | # with: 51 | # xcode-version: ^11 52 | # # Waiting on https://github.com/Carthage/Carthage/issues/3103 for Xcode 12 53 | 54 | # - uses: joutvhu/get-release@v1 55 | # id: release 56 | # with: 57 | # tag_name: ${{ github.event.inputs.version }} 58 | # env: 59 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 60 | 61 | # - uses: actions/checkout@v2 62 | # - run: carthage build --no-skip-current --platform macOS,iOS,watchOS,tvOS --archive 63 | # - run: mv PromiseKit.framework.zip PromiseKit-$v.framework.zip 64 | 65 | # - uses: actions/upload-release-asset@v1 66 | # with: 67 | # upload_url: ${{ steps.release.outputs.upload_url }} 68 | # asset_path: ./PromiseKit-${{ github.event.inputs.version }}.framework.zip 69 | # asset_name: PromiseKit-${{ github.event.inputs.version }}.framework.zip 70 | # asset_content_type: application/zip 71 | # env: 72 | # GITHUB_TOKEN: ${{ github.token }} 73 | 74 | # - name: Seal Deployment 75 | # uses: bobheadxi/deployments@v0.5.2 76 | # if: always() 77 | # with: 78 | # step: finish 79 | # token: ${{ secrets.GITHUB_TOKEN }} 80 | # status: ${{ job.status }} 81 | # deployment_id: ${{ steps.deployment.outputs.deployment_id }} 82 | 83 | docs: 84 | runs-on: macos-latest 85 | steps: 86 | 87 | - name: Start Deployment 88 | uses: bobheadxi/deployments@v0.5.2 89 | id: deployment 90 | with: 91 | step: start 92 | token: ${{ secrets.GITHUB_TOKEN }} 93 | env: docs 94 | 95 | - uses: actions/checkout@v2 96 | - run: gem install jazzy 97 | - run: | 98 | jazzy --config .github/jazzy.yml \ 99 | --github_url 'https://github.com/mxcl/PromiseKit' \ 100 | --module-version ${{ github.event.inputs.version }} 101 | - run: git remote update 102 | - run: git checkout gh-pages 103 | - run: rm -rf reference/v6 104 | - run: mv output reference/v6 105 | - run: git add reference/v6 106 | - run: git config user.name github-actions 107 | - run: git config user.email github-actions@github.com 108 | - run: git commit -m 'Updated docs for v${{ github.event.inputs.version }}' 109 | - run: git remote add secure-origin https://${{ secrets.JAZZY_PAT }}@github.com/mxcl/PromiseKit.git 110 | - run: git push secure-origin gh-pages 111 | 112 | - name: Seal Deployment 113 | uses: bobheadxi/deployments@v0.5.2 114 | if: always() 115 | with: 116 | step: finish 117 | token: ${{ secrets.GITHUB_TOKEN }} 118 | status: ${{ job.status }} 119 | deployment_id: ${{ steps.deployment.outputs.deployment_id }} 120 | -------------------------------------------------------------------------------- /.github/workflows/ci-podspec.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | pull_request: 4 | paths: 5 | - PromiseKit.podspec 6 | jobs: 7 | lint: 8 | runs-on: macos-15 9 | steps: 10 | - uses: maxim-lobanov/setup-xcode@v1 11 | with: 12 | xcode-version: 16 13 | - uses: maxim-lobanov/setup-cocoapods@v1 14 | with: 15 | version: 1.16.2 16 | - uses: actions/checkout@v2 17 | with: 18 | submodules: true 19 | - run: pod lib lint --fail-fast --allow-warnings 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | workflow_call: 6 | pull_request: 7 | paths: 8 | - Sources/** 9 | - Tests/** 10 | - .github/workflows/ci.yml 11 | - PromiseKit.xcodeproj/** 12 | 13 | concurrency: 14 | group: ${{ github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | linux-build: 19 | name: Linux (Swift ${{ matrix.swift }}) 20 | runs-on: ubuntu-20.04 21 | strategy: 22 | matrix: 23 | swift: 24 | - '4.0' 25 | - '4.1' 26 | - '4.2' 27 | container: 28 | image: swift:${{ matrix.swift }} 29 | steps: 30 | - uses: actions/checkout@v1 # DO NOT UPDATE ∵ failure on older containers due to old glibc 31 | - run: swift build -Xswiftc -warnings-as-errors -Xswiftc -swift-version -Xswiftc 3 32 | - run: swift build # can’t test ∵ generated linuxmain requires Swift 5 33 | 34 | linux-test: 35 | name: Linux (Swift ${{ matrix.swift }}) 36 | runs-on: ubuntu-latest 37 | continue-on-error: true 38 | strategy: 39 | matrix: 40 | swift: 41 | - '5.0' 42 | - '5.1' 43 | - '5.2' 44 | - '5.3' 45 | - '5.4' 46 | - '5.5' 47 | - '5.6' 48 | - '5.7' 49 | - '5.8' 50 | - '5.9' 51 | - '5.10' 52 | - '6.0' 53 | container: 54 | image: swift:${{ matrix.swift }} 55 | steps: 56 | - uses: actions/checkout@v1 # DO NOT UPDATE ∵ failure on older containers due to old glibc 57 | - run: swift build -Xswiftc -warnings-as-errors -Xswiftc -swift-version -Xswiftc 4 58 | if: ${{ matrix.swift < 6 }} 59 | - run: swift build -Xswiftc -warnings-as-errors -Xswiftc -swift-version -Xswiftc 4.2 60 | if: ${{ matrix.swift < 6 }} 61 | - run: swift test --enable-code-coverage --parallel 62 | 63 | - name: Generate Coverage Report 64 | if: ${{ matrix.swift >= 6 }} 65 | run: | 66 | apt-get -qq update 67 | apt-get -qq install llvm-18 curl 68 | export b=$(swift build --show-bin-path) && llvm-cov-18 \ 69 | export -format lcov \ 70 | -instr-profile=$b/codecov/default.profdata \ 71 | --ignore-filename-regex='\.build/' \ 72 | $b/*.xctest \ 73 | > info.lcov 74 | 75 | - uses: codecov/codecov-action@v1 76 | with: 77 | file: ./info.lcov 78 | if: ${{ matrix.swift >= 6 }} 79 | 80 | test: 81 | runs-on: macos-latest 82 | name: ${{ matrix.platform }} 83 | continue-on-error: true 84 | strategy: 85 | matrix: 86 | platform: 87 | - macOS 88 | - watchOS 89 | - tvOS 90 | - iOS 91 | # - mac-catalyst 92 | # - visionOS 93 | # ^^ both are unavailable in the github-hosted runners 94 | steps: 95 | - uses: actions/checkout@v4 96 | - uses: mxcl/xcodebuild@v3 97 | with: 98 | platform: ${{ matrix.platform }} 99 | code-coverage: true 100 | - uses: codecov/codecov-action@v1 101 | 102 | test-android: 103 | name: Android 104 | runs-on: ubuntu-latest 105 | steps: 106 | - uses: actions/checkout@v4 107 | - uses: skiptools/swift-android-action@v2 108 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: semantic version to publish 8 | required: true 9 | 10 | concurrency: 11 | group: publish-{{github.event.inputs.version}} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | ci: 16 | uses: ./.github/workflows/ci.yml 17 | 18 | lint: 19 | runs-on: macos-latest 20 | strategy: 21 | matrix: 22 | xcode: 23 | - ^16 24 | steps: 25 | - uses: maxim-lobanov/setup-xcode@v1 26 | with: 27 | xcode-version: ${{ matrix.xcode }} 28 | - uses: actions/checkout@v3 29 | with: 30 | submodules: true 31 | - run: pod lib lint --fail-fast --allow-warnings 32 | 33 | create-release: 34 | runs-on: ubuntu-latest 35 | needs: [ci, lint] 36 | env: 37 | v: ${{ github.event.inputs.version }} 38 | steps: 39 | - uses: actions/checkout@v3 40 | with: 41 | fetch-depth: 0 # zero means “all” (or push fails) 42 | - name: Update committed versions 43 | run: | 44 | ruby -i -pe "sub(/CURRENT_PROJECT_VERSION = [0-9.]+/, 'CURRENT_PROJECT_VERSION = $v')" PromiseKit.xcodeproj/project.pbxproj 45 | ruby -i -pe "sub(/s.version = '[0-9.]+'/, 's.version = \'$v\'')" PromiseKit.podspec 46 | - run: | 47 | ! (git diff --quiet) 48 | - name: Commit 49 | run: | 50 | git config user.name github-actions 51 | git config user.email github-actions@github.com 52 | git commit -am "PromiseKit $v" 53 | git push 54 | - uses: softprops/action-gh-release@v1 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | with: 58 | tag_name: ${{ github.event.inputs.version }} 59 | name: ${{ github.event.inputs.version }} 60 | 61 | cd: 62 | needs: create-release 63 | runs-on: ubuntu-latest 64 | steps: 65 | - uses: aurelien-baudet/workflow-dispatch@v2 66 | with: 67 | workflow: CD 68 | token: ${{ secrets.JAZZY_PAT }} 69 | inputs: "{\"version\": \"${{ github.event.inputs.version }}\"}" 70 | ref: ${{ env.GITHUB_REF_NAME }} # or uses the SHA rather than branch and thus the above commit is not used 71 | wait-for-completion: true 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.xcodeproj/**/xcuserdata/ 2 | *.xcscmblueprint 3 | /Carthage 4 | /.build 5 | .DS_Store 6 | DerivedData 7 | /Extensions/Carthage 8 | /Tests/JS-A+/build 9 | .swiftpm/ 10 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Extensions/Foundation"] 2 | path = Extensions/Foundation 3 | url = https://github.com/PromiseKit/Foundation.git 4 | [submodule "Extensions/UIKit"] 5 | path = Extensions/UIKit 6 | url = https://github.com/PromiseKit/UIKit.git 7 | [submodule "Extensions/Accounts"] 8 | path = Extensions/Accounts 9 | url = https://github.com/PromiseKit/Accounts.git 10 | [submodule "Extensions/MessagesUI"] 11 | path = Extensions/MessagesUI 12 | url = https://github.com/PromiseKit/MessagesUI.git 13 | [submodule "Extensions/WatchConnectivity"] 14 | path = Extensions/WatchConnectivity 15 | url = https://github.com/PromiseKit/WatchConnectivity.git 16 | [submodule "Extensions/Photos"] 17 | path = Extensions/Photos 18 | url = https://github.com/PromiseKit/Photos.git 19 | [submodule "Extensions/MapKit"] 20 | path = Extensions/MapKit 21 | url = https://github.com/PromiseKit/MapKit.git 22 | [submodule "Extensions/CloudKit"] 23 | path = Extensions/CloudKit 24 | url = https://github.com/PromiseKit/CloudKit.git 25 | [submodule "Extensions/AddressBook"] 26 | path = Extensions/AddressBook 27 | url = https://github.com/PromiseKit/AddressBook.git 28 | [submodule "Extensions/AssetsLibrary"] 29 | path = Extensions/AssetsLibrary 30 | url = https://github.com/PromiseKit/AssetsLibrary.git 31 | [submodule "Extensions/CoreLocation"] 32 | path = Extensions/CoreLocation 33 | url = https://github.com/PromiseKit/CoreLocation.git 34 | [submodule "Extensions/QuartzCore"] 35 | path = Extensions/QuartzCore 36 | url = https://github.com/PromiseKit/QuartzCore.git 37 | [submodule "Extensions/Social"] 38 | path = Extensions/Social 39 | url = https://github.com/PromiseKit/Social.git 40 | [submodule "Extensions/StoreKit"] 41 | path = Extensions/StoreKit 42 | url = https://github.com/PromiseKit/StoreKit.git 43 | [submodule "Extensions/Bolts"] 44 | path = Extensions/Bolts 45 | url = https://github.com/PromiseKit/Bolts.git 46 | [submodule "Extensions/CoreBluetooth"] 47 | path = Extensions/CoreBluetooth 48 | url = https://github.com/PromiseKit/CoreBluetooth.git 49 | [submodule "Extensions/EventKit"] 50 | path = Extensions/EventKit 51 | url = https://github.com/PromiseKit/EventKit.git 52 | [submodule "Extensions/SystemConfiguration"] 53 | path = Extensions/SystemConfiguration 54 | url = https://github.com/PromiseKit/SystemConfiguration 55 | [submodule "Extensions/Alamofire"] 56 | path = Extensions/Alamofire 57 | url = https://github.com/PromiseKit/Alamofire 58 | [submodule "Extensions/OMGHTTPURLRQ"] 59 | path = Extensions/OMGHTTPURLRQ 60 | url = https://github.com/PromiseKit/OMGHTTPURLRQ 61 | [submodule "Extensions/AVFoundation"] 62 | path = Extensions/AVFoundation 63 | url = https://github.com/PromiseKit/AVFoundation 64 | [submodule "Extensions/HomeKit"] 65 | path = Extensions/HomeKit 66 | url = https://github.com/PromiseKit/HomeKit.git 67 | [submodule "Extensions/HealthKit"] 68 | path = Extensions/HealthKit 69 | url = https://github.com/PromiseKit/PMKHealthKit 70 | -------------------------------------------------------------------------------- /.tidelift.yml: -------------------------------------------------------------------------------- 1 | extra_ignore_directories: [ Tests ] 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: osx 2 | language: swift 3 | osx_image: xcode9.4 4 | 5 | if: tag =~ /^[0-9]+\.[0-9]+\.[0-9]+/ 6 | 7 | stages: 8 | - swiftpm 9 | - carthage 10 | 11 | jobs: 12 | include: 13 | - &swiftpm 14 | stage: swiftpm 15 | osx_image: xcode9.4 16 | script: swift build 17 | - <<: *swiftpm 18 | osx_image: xcode10.3 19 | - <<: *swiftpm 20 | osx_image: xcode11.6 21 | 22 | - &carthage 23 | stage: carthage 24 | osx_image: xcode9.4 25 | install: sed -i '' "s/SWIFT_TREAT_WARNINGS_AS_ERRORS = NO;/SWIFT_TREAT_WARNINGS_AS_ERRORS = YES;/" *.xcodeproj/project.pbxproj 26 | script: carthage build --no-skip-current 27 | - <<: *carthage 28 | osx_image: xcode10.3 29 | - <<: *carthage 30 | osx_image: xcode11.6 31 | -------------------------------------------------------------------------------- /Documentation/Appendix.md: -------------------------------------------------------------------------------- 1 | # Common Misusage 2 | 3 | ## Doubling up Promises 4 | 5 | Don’t do this: 6 | 7 | ```swift 8 | func toggleNetworkSpinnerWithPromise<T>(funcToCall: () -> Promise<T>) -> Promise<T> { 9 | return Promise { seal in 10 | firstly { 11 | setNetworkActivityIndicatorVisible(true) 12 | return funcToCall() 13 | }.then { result in 14 | seal.fulfill(result) 15 | }.always { 16 | setNetworkActivityIndicatorVisible(false) 17 | }.catch { err in 18 | seal.reject(err) 19 | } 20 | } 21 | } 22 | ``` 23 | 24 | Do this: 25 | 26 | ```swift 27 | func toggleNetworkSpinnerWithPromise<T>(funcToCall: () -> Promise<T>) -> Promise<T> { 28 | return firstly { 29 | setNetworkActivityIndicatorVisible(true) 30 | return funcToCall() 31 | }.always { 32 | setNetworkActivityIndicatorVisible(false) 33 | } 34 | } 35 | ``` 36 | 37 | You already *had* a promise, you don’t need to wrap it in another promise. 38 | 39 | 40 | ## Optionals in Promises 41 | 42 | When we see `Promise<Item?>`, it usually implies a misuse of promises. For 43 | example: 44 | 45 | ```swift 46 | return firstly { 47 | getItems() 48 | }.then { items -> Promise<[Item]?> in 49 | guard !items.isEmpty else { 50 | return .value(nil) 51 | } 52 | return Promise(value: items) 53 | } 54 | ``` 55 | 56 | The second `then` chooses to return `nil` in some circumstances. This choice 57 | imposes the need to check for `nil` on the consumer of the promise. 58 | 59 | It's usually better to shunt these sorts of exceptions away from the 60 | happy path and onto the error path. In this case, we can create a specific 61 | error type for this condition: 62 | 63 | ```swift 64 | return firstly { 65 | getItems() 66 | }.map { items -> [Item]> in 67 | guard !items.isEmpty else { 68 | throw MyError.emptyItems 69 | } 70 | return items 71 | } 72 | ``` 73 | 74 | > *Note*: Use `compactMap` when an API outside your control returns an Optional and you want to generate an error instead of propagating `nil`. 75 | 76 | # Tips n’ Tricks 77 | 78 | ## Background-Loaded Member Variables 79 | 80 | ```swift 81 | class MyViewController: UIViewController { 82 | private let ambience: Promise<AVAudioPlayer> = DispatchQueue.global().async(.promise) { 83 | guard let asset = NSDataAsset(name: "CreepyPad") else { throw PMKError.badInput } 84 | let player = try AVAudioPlayer(data: asset.data) 85 | player.prepareToPlay() 86 | return player 87 | } 88 | } 89 | ``` 90 | 91 | ## Chaining Animations 92 | 93 | ```swift 94 | firstly { 95 | UIView.animate(.promise, duration: 0.3) { 96 | self.button1.alpha = 0 97 | } 98 | }.then { 99 | UIView.animate(.promise, duration: 0.3) { 100 | self.button2.alpha = 1 101 | } 102 | }.then { 103 | UIView.animate(.promise, duration: 0.3) { 104 | adjustConstraints() 105 | self.view.layoutIfNeeded() 106 | } 107 | } 108 | ``` 109 | 110 | 111 | ## Voiding Promises 112 | 113 | It is often convenient to erase the type of a promise to facilitate chaining. 114 | For example, `UIView.animate(.promise)` returns `Guarantee<Bool>` because UIKit’s 115 | completion API supplies a `Bool`. However, we usually don’t need this value and 116 | can chain more simply if it is discarded (that is, converted to `Void`). We can use 117 | `asVoid()` to achieve this conversion: 118 | 119 | ```swift 120 | UIView.animate(.promise, duration: 0.3) { 121 | self.button1.alpha = 0 122 | }.asVoid().done(self.nextStep) 123 | ``` 124 | 125 | For situations in which we are combining many promises into a `when`, `asVoid()` 126 | becomes essential: 127 | 128 | ```swift 129 | let p1 = foo() 130 | let p2 = bar() 131 | let p3 = baz() 132 | //… 133 | let p10 = fluff() 134 | 135 | when(fulfilled: p1.asVoid(), p2.asVoid(), /*…*/, p10.asVoid()).then { 136 | let value1 = p1.value! // safe bang since all the promises fulfilled 137 | // … 138 | let value10 = p10.value! 139 | }.catch { 140 | //… 141 | } 142 | ``` 143 | 144 | You normally don't have to do this explicitly because `when` does it for you 145 | for up to 5 parameters. 146 | 147 | 148 | ## Blocking (Await) 149 | 150 | Sometimes you have to block the main thread to await completion of an asynchronous task. 151 | In these cases, you can (with caution) use `wait`: 152 | 153 | ```swift 154 | public extension UNUserNotificationCenter { 155 | var wasPushRequested: Bool { 156 | let settings = Guarantee(resolver: getNotificationSettings).wait() 157 | return settings != .notDetermined 158 | } 159 | } 160 | ``` 161 | 162 | The task under the promise **must not** call back onto the current thread or it 163 | will deadlock. 164 | 165 | ## Starting a Chain on a Background Queue/Thread 166 | 167 | `firstly` deliberately does not take a queue. A detailed rationale for this choice 168 | can be found in the ticket tracker. 169 | 170 | So, if you want to start a chain by dispatching to the background, you have to use 171 | `DispatchQueue.async`: 172 | 173 | ```swift 174 | DispatchQueue.global().async(.promise) { 175 | return value 176 | }.done { value in 177 | //… 178 | } 179 | ``` 180 | 181 | However, this function cannot return a promise because of Swift compiler ambiguity 182 | issues. Thus, if you must start a promise on a background queue, you need to 183 | do something like this: 184 | 185 | 186 | ```swift 187 | Promise { seal in 188 | DispatchQueue.global().async { 189 | seal(value) 190 | } 191 | }.done { value in 192 | //… 193 | } 194 | ``` 195 | 196 | Or more simply (though with caveats; see the documentation for `wait`): 197 | 198 | ```swift 199 | DispatchQueue.global().async(.promise) { 200 | return try fetch().wait() 201 | }.done { value in 202 | //… 203 | } 204 | ``` 205 | 206 | However, you shouldn't need to do this often. If you find yourself wanting to use 207 | this technique, perhaps you should instead modify the code for `fetch` to make it do 208 | its work on a background thread. 209 | 210 | Promises abstract asynchronicity, so exploit and support that model. Design your 211 | APIs so that consumers don’t have to care what queue your functions run on. 212 | -------------------------------------------------------------------------------- /Documentation/Examples/ImageCache.md: -------------------------------------------------------------------------------- 1 | # Image Cache with Promises 2 | 3 | Here is an example of a simple image cache that uses promises to simplify the 4 | state machine: 5 | 6 | ```swift 7 | import Foundation 8 | import PromiseKit 9 | 10 | /** 11 | * Small (10 images) 12 | * Thread-safe 13 | * Consolidates multiple requests to the same URLs 14 | * Removes stale entries (FIXME well, strictly we may delete while fetching from cache, but this is unlikely and non-fatal) 15 | * Completely _ignores_ server caching headers! 16 | */ 17 | 18 | private let q = DispatchQueue(label: "org.promisekit.cache.image", attributes: .concurrent) 19 | private var active: [URL: Promise<Data>] = [:] 20 | private var cleanup = Promise() 21 | 22 | 23 | public func fetch(image url: URL) -> Promise<Data> { 24 | var promise: Promise<Data>? 25 | q.sync { 26 | promise = active[url] 27 | } 28 | if let promise = promise { 29 | return promise 30 | } 31 | 32 | q.sync { 33 | promise = Promise(.start) { 34 | 35 | let dst = try url.cacheDestination() 36 | 37 | guard !FileManager.default.isReadableFile(atPath: dst.path) else { 38 | return Promise(dst) 39 | } 40 | 41 | return Promise { seal in 42 | URLSession.shared.downloadTask(with: url) { tmpurl, _, error in 43 | do { 44 | guard let tmpurl = tmpurl else { throw error ?? E.unexpectedError } 45 | try FileManager.default.moveItem(at: tmpurl, to: dst) 46 | seal.fulfill(dst) 47 | } catch { 48 | seal.reject(error) 49 | } 50 | }.resume() 51 | } 52 | 53 | }.then(on: .global(QoS: .userInitiated)) { 54 | try Data(contentsOf: $0) 55 | } 56 | 57 | active[url] = promise 58 | 59 | if cleanup.isFulfilled { 60 | cleanup = promise!.asVoid().then(on: .global(QoS: .utility), execute: docleanup) 61 | } 62 | } 63 | 64 | return promise! 65 | } 66 | 67 | public func cached(image url: URL) -> Data? { 68 | guard let dst = try? url.cacheDestination() else { 69 | return nil 70 | } 71 | return try? Data(contentsOf: dst) 72 | } 73 | 74 | 75 | public func cache(destination remoteUrl: URL) throws -> URL { 76 | return try remoteUrl.cacheDestination() 77 | } 78 | 79 | private func cache() throws -> URL { 80 | guard let dst = FileManager.default.docs? 81 | .appendingPathComponent("Library") 82 | .appendingPathComponent("Caches") 83 | .appendingPathComponent("cache.img") 84 | else { 85 | throw E.unexpectedError 86 | } 87 | 88 | try FileManager.default.createDirectory(at: dst, withIntermediateDirectories: true, attributes: [:]) 89 | 90 | return dst 91 | } 92 | 93 | private extension URL { 94 | func cacheDestination() throws -> URL { 95 | 96 | var fn = String(hashValue) 97 | let ext = pathExtension 98 | 99 | // many of Apple's functions don’t recognize file type 100 | // unless we preserve the file extension 101 | if !ext.isEmpty { 102 | fn += ".\(ext)" 103 | } 104 | 105 | return try cache().appendingPathComponent(fn) 106 | } 107 | } 108 | 109 | enum E: Error { 110 | case unexpectedError 111 | case noCreationTime 112 | } 113 | 114 | private func docleanup() throws { 115 | var contents = try FileManager.default 116 | .contentsOfDirectory(at: try cache(), includingPropertiesForKeys: [.creationDateKey]) 117 | .map { url -> (Date, URL) in 118 | guard let date = try url.resourceValues(forKeys: [.creationDateKey]).creationDate else { 119 | throw E.noCreationTime 120 | } 121 | return (date, url) 122 | }.sorted(by: { 123 | $0.0 > $1.0 124 | }) 125 | 126 | while contents.count > 10 { 127 | let rm = contents.popLast()!.1 128 | try FileManager.default.removeItem(at: rm) 129 | } 130 | } 131 | ```` -------------------------------------------------------------------------------- /Documentation/Examples/URLSession+BadResponseErrors.swift: -------------------------------------------------------------------------------- 1 | Promise(.pending) { seal in 2 | URLSession.shared.dataTask(with: rq, completionHandler: { data, rsp, error in 3 | if let data = data { 4 | seal.fulfill(data) 5 | } else if let error = error { 6 | if case URLError.badServerResponse = error, let rsp = rsp as? HTTPURLResponse { 7 | seal.reject(Error.badResponse(rsp.statusCode)) 8 | } else { 9 | seal.reject(error) 10 | } 11 | } else { 12 | seal.reject(PMKError.invalidCallingConvention) 13 | } 14 | }) 15 | } 16 | 17 | enum Error: Swift.Error { 18 | case badUrl 19 | case badResponse(Int) 20 | } 21 | -------------------------------------------------------------------------------- /Documentation/Examples/detweet.swift: -------------------------------------------------------------------------------- 1 | #!/usr/bin/swift sh 2 | import Foundation 3 | import PromiseKit // @mxcl ~> 6.5 4 | import Swifter // @mattdonnelly == b27a89 5 | let swifter = Swifter( 6 | consumerKey: "FILL", 7 | consumerSecret: "ME", 8 | oauthToken: "IN", 9 | oauthTokenSecret: "https://developer.twitter.com/en/docs/basics/apps/overview.html" 10 | ) 11 | 12 | extension JSON { 13 | var date: Date? { 14 | guard let string = string else { return nil } 15 | 16 | let formatter = DateFormatter() 17 | formatter.dateFormat = "EEE MMM dd HH:mm:ss Z yyyy" 18 | return formatter.date(from: string) 19 | } 20 | } 21 | 22 | let twoMonthsAgo = Date() - 24*60*60*30*2 23 | 24 | print("Deleting qualifying tweets before:", twoMonthsAgo) 25 | 26 | func deleteTweets(maxID: String? = nil) -> Promise<Void> { 27 | return Promise { seal in 28 | swifter.getTimeline(for: "mxcl", count: 200, maxID: maxID, success: { json in 29 | 30 | if json.array!.count <= 1 { 31 | // if we get one result for a requested maxID, we're done 32 | return seal.fulfill() 33 | } 34 | 35 | for item in json.array! { 36 | let date = item["created_at"].date! 37 | guard date < twoMonthsAgo, item["favorite_count"].integer! < 2 else { 38 | continue 39 | } 40 | swifter.destroyTweet(forID: id, success: { _ in 41 | print("D:", item["text"].string!) 42 | }, failure: seal.reject) 43 | } 44 | 45 | let next = json.array!.last!["id_str"].string! 46 | deleteTweets(maxID: next).pipe(to: seal.resolve) 47 | 48 | }, failure: seal.reject) 49 | } 50 | } 51 | 52 | func deleteFavorites(maxID: String? = nil) -> Promise<Void> { 53 | return Promise { seal in 54 | swifter.getRecentlyFavoritedTweets(count: 200, maxID: maxID, success: { json in 55 | 56 | if json.array!.count <= 1 { 57 | return seal.fulfill() 58 | } 59 | 60 | for item in json.array! { 61 | guard item["created_at"].date! < twoMonthsAgo else { continue } 62 | 63 | swifter.unfavoriteTweet(forID: item["id_str"].string!, success: { _ in 64 | print("D❤️:", item["text"].string!) 65 | }, failure: seal.reject) 66 | } 67 | 68 | let next = json.array!.last!["id_str"].string! 69 | deleteFavorites(maxID: next).pipe(to: seal.resolve) 70 | 71 | }, failure: seal.reject) 72 | } 73 | } 74 | 75 | func unblockPeople(cursor: String? = nil) -> Promise<Void> { 76 | return Promise { seal in 77 | swifter.getBlockedUsersIDs(stringifyIDs: "true", cursor: cursor, success: { json, prev, next in 78 | for id in json.array! { 79 | print("Unblocking:", id) 80 | swifter.unblockUser(for: .id(id.string!)) 81 | } 82 | 83 | if let next = next, !next.isEmpty, next != prev, next != "0" { 84 | unblockPeople(cursor: next).pipe(to: seal.resolve) 85 | } else { 86 | seal.fulfill() 87 | } 88 | 89 | }, failure: seal.reject) 90 | } 91 | } 92 | 93 | firstly { 94 | when(fulfilled: deleteTweets(), deleteFavorites(), unblockPeople()) 95 | }.done { 96 | exit(0) 97 | }.catch { 98 | print("error:", $0) 99 | exit(1) 100 | } 101 | 102 | RunLoop.main.run() 103 | -------------------------------------------------------------------------------- /Documentation/README.md: -------------------------------------------------------------------------------- 1 | # Contents 2 | 3 | * [README](../README.md) 4 | * Handbook 5 | * [Getting Started](GettingStarted.md) 6 | * [Promises: Common Patterns](CommonPatterns.md) 7 | * [Frequently Asked Questions](FAQ.md) 8 | * Manual 9 | * [Installation Guide](Installation.md) 10 | * [Objective-C Guide](ObjectiveC.md) 11 | * [Troubleshooting](Troubleshooting.md) 12 | * [Appendix](Appendix.md) 13 | * [Examples](Examples) 14 | * [API Reference](https://mxcl.dev/PromiseKit/reference/v6/Classes/Promise.html) 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016-present, Max Howell; mxcl@me.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | 3 | import PackageDescription 4 | 5 | let pkg = Package(name: "PromiseKit") 6 | pkg.products = [ 7 | .library(name: "PromiseKit", targets: ["PromiseKit"]), 8 | ] 9 | 10 | let pmk: Target = .target(name: "PromiseKit") 11 | pmk.path = "Sources" 12 | pmk.exclude = [ 13 | "AnyPromise.swift", 14 | "AnyPromise.m", 15 | "PMKCallVariadicBlock.m", 16 | "dispatch_promise.m", 17 | "join.m", 18 | "when.m", 19 | "NSMethodSignatureForBlock.m", 20 | "after.m", 21 | "hang.m", 22 | "race.m", 23 | "Deprecations.swift" 24 | ] 25 | pkg.swiftLanguageVersions = [3, 4, 5] 26 | pkg.targets = [ 27 | pmk, 28 | .testTarget(name: "APlus", dependencies: ["PromiseKit"], path: "Tests/A+"), 29 | .testTarget(name: "CorePromise", dependencies: ["PromiseKit"], path: "Tests/CorePromise"), 30 | ] 31 | -------------------------------------------------------------------------------- /Package@swift-4.2.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.2 2 | 3 | import PackageDescription 4 | 5 | let pkg = Package(name: "PromiseKit") 6 | pkg.products = [ 7 | .library(name: "PromiseKit", targets: ["PromiseKit"]), 8 | ] 9 | 10 | let pmk: Target = .target(name: "PromiseKit") 11 | pmk.path = "Sources" 12 | pmk.exclude = [ 13 | "AnyPromise.swift", 14 | "AnyPromise.m", 15 | "PMKCallVariadicBlock.m", 16 | "dispatch_promise.m", 17 | "join.m", 18 | "when.m", 19 | "NSMethodSignatureForBlock.m", 20 | "after.m", 21 | "hang.m", 22 | "race.m", 23 | "Deprecations.swift" 24 | ] 25 | pkg.swiftLanguageVersions = [.v3, .v4, .v4_2] 26 | pkg.targets = [ 27 | pmk, 28 | .testTarget(name: "APlus", dependencies: ["PromiseKit"], path: "Tests/A+"), 29 | .testTarget(name: "CorePromise", dependencies: ["PromiseKit"], path: "Tests/CorePromise"), 30 | ] 31 | -------------------------------------------------------------------------------- /Package@swift-5.0.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | 3 | import PackageDescription 4 | 5 | let pkg = Package(name: "PromiseKit") 6 | pkg.platforms = [ 7 | .macOS(.v10_10), .iOS(.v8), .tvOS(.v9), .watchOS(.v2) 8 | ] 9 | pkg.products = [ 10 | .library(name: "PromiseKit", targets: ["PromiseKit"]), 11 | ] 12 | 13 | let pmk: Target = .target(name: "PromiseKit") 14 | pmk.path = "Sources" 15 | pmk.exclude = [ 16 | "AnyPromise.swift", 17 | "AnyPromise.m", 18 | "PMKCallVariadicBlock.m", 19 | "dispatch_promise.m", 20 | "join.m", 21 | "when.m", 22 | "NSMethodSignatureForBlock.m", 23 | "after.m", 24 | "hang.m", 25 | "race.m", 26 | "Deprecations.swift" 27 | ] 28 | pkg.swiftLanguageVersions = [.v4, .v4_2, .v5] 29 | pkg.targets = [ 30 | pmk, 31 | .testTarget(name: "APlus", dependencies: ["PromiseKit"], path: "Tests/A+"), 32 | .testTarget(name: "CorePromise", dependencies: ["PromiseKit"], path: "Tests/CorePromise"), 33 | ] 34 | -------------------------------------------------------------------------------- /Package@swift-5.3.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | import PackageDescription 4 | 5 | let pkg = Package(name: "PromiseKit") 6 | pkg.platforms = [ 7 | .macOS(.v10_10), .iOS(.v9), .tvOS(.v9), .watchOS(.v2) 8 | ] 9 | pkg.products = [ 10 | .library(name: "PromiseKit", targets: ["PromiseKit"]), 11 | ] 12 | 13 | let pmk: Target = .target(name: "PromiseKit") 14 | pmk.path = "Sources" 15 | pmk.resources = [ 16 | .process("Resources/PrivacyInfo.xcprivacy") 17 | ] 18 | pmk.exclude = [ 19 | "AnyPromise.swift", 20 | "AnyPromise.m", 21 | "PMKCallVariadicBlock.m", 22 | "dispatch_promise.m", 23 | "join.m", 24 | "when.m", 25 | "NSMethodSignatureForBlock.m", 26 | "after.m", 27 | "hang.m", 28 | "race.m", 29 | "Deprecations.swift", 30 | "Info.plist" 31 | ] 32 | pkg.swiftLanguageVersions = [.v4, .v4_2, .v5] 33 | pkg.targets = [ 34 | pmk, 35 | .testTarget(name: "APlus", dependencies: ["PromiseKit"], path: "Tests/A+", exclude: ["README.md"]), 36 | .testTarget(name: "CorePromise", dependencies: ["PromiseKit"], path: "Tests/CorePromise"), 37 | ] 38 | -------------------------------------------------------------------------------- /PromiseKit.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | import PlaygroundSupport 2 | 3 | // Is this erroring? If so open the `.xcodeproj` and build the 4 | // framework for a macOS target (usually labeled: “My Mac”). 5 | // Then select `PromiseKit.playground` from inside Xcode. 6 | import PromiseKit 7 | 8 | 9 | func promise3() -> Promise<Int> { 10 | return after(.seconds(1)).map{ 3 } 11 | } 12 | 13 | firstly { 14 | Promise.value(1) 15 | }.map { _ in 16 | 2 17 | }.then { _ in 18 | promise3() 19 | }.done { 20 | print($0) // => 3 21 | }.catch { error in 22 | // only happens for errors 23 | }.finally { 24 | PlaygroundPage.current.finishExecution() 25 | } 26 | 27 | PlaygroundPage.current.needsIndefiniteExecution = true 28 | -------------------------------------------------------------------------------- /PromiseKit.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="yes"?> 2 | <playground version='3.0' sdk='macosx' auto-termination-delay='2'> 3 | <sections> 4 | <code source-file-name='section-1.swift'/> 5 | </sections> 6 | <timeline fileName='timeline.xctimeline'/> 7 | </playground> -------------------------------------------------------------------------------- /PromiseKit.playground/playground.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <Workspace 3 | version = "1.0"> 4 | <FileRef 5 | location = "self:"> 6 | </FileRef> 7 | </Workspace> 8 | -------------------------------------------------------------------------------- /PromiseKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <Workspace 3 | version = "1.0"> 4 | <FileRef 5 | location = "self:"> 6 | </FileRef> 7 | </Workspace> 8 | -------------------------------------------------------------------------------- /PromiseKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 | <plist version="1.0"> 4 | <dict> 5 | <key>IDEDidComputeMac32BitWarning</key> 6 | <true/> 7 | </dict> 8 | </plist> 9 | -------------------------------------------------------------------------------- /PromiseKit.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 | <plist version="1.0"> 4 | <dict> 5 | <key>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key> 6 | <false/> 7 | </dict> 8 | </plist> 9 | -------------------------------------------------------------------------------- /PromiseKit.xcodeproj/xcshareddata/xcschemes/PromiseKit.xcscheme: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <Scheme 3 | LastUpgradeVersion = "1250" 4 | version = "1.3"> 5 | <BuildAction 6 | parallelizeBuildables = "YES" 7 | buildImplicitDependencies = "NO"> 8 | <BuildActionEntries> 9 | <BuildActionEntry 10 | buildForTesting = "NO" 11 | buildForRunning = "YES" 12 | buildForProfiling = "YES" 13 | buildForArchiving = "YES" 14 | buildForAnalyzing = "YES"> 15 | <BuildableReference 16 | BuildableIdentifier = "primary" 17 | BlueprintIdentifier = "63B0AC561D595E1B00FA21D9" 18 | BuildableName = "PromiseKit.framework" 19 | BlueprintName = "PromiseKit" 20 | ReferencedContainer = "container:PromiseKit.xcodeproj"> 21 | </BuildableReference> 22 | </BuildActionEntry> 23 | </BuildActionEntries> 24 | </BuildAction> 25 | <TestAction 26 | buildConfiguration = "Debug" 27 | selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" 28 | selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" 29 | shouldUseLaunchSchemeArgsEnv = "YES" 30 | codeCoverageEnabled = "YES" 31 | onlyGenerateCoverageForSpecifiedTargets = "YES"> 32 | <CodeCoverageTargets> 33 | <BuildableReference 34 | BuildableIdentifier = "primary" 35 | BlueprintIdentifier = "63B0AC561D595E1B00FA21D9" 36 | BuildableName = "PromiseKit.framework" 37 | BlueprintName = "PromiseKit" 38 | ReferencedContainer = "container:PromiseKit.xcodeproj"> 39 | </BuildableReference> 40 | </CodeCoverageTargets> 41 | <Testables> 42 | <TestableReference 43 | skipped = "NO" 44 | parallelizable = "YES" 45 | testExecutionOrdering = "random"> 46 | <BuildableReference 47 | BuildableIdentifier = "primary" 48 | BlueprintIdentifier = "630019011D596292003B4E30" 49 | BuildableName = "PMKCoreTests.xctest" 50 | BlueprintName = "PMKCoreTests" 51 | ReferencedContainer = "container:PromiseKit.xcodeproj"> 52 | </BuildableReference> 53 | </TestableReference> 54 | <TestableReference 55 | skipped = "NO" 56 | parallelizable = "YES" 57 | testExecutionOrdering = "random"> 58 | <BuildableReference 59 | BuildableIdentifier = "primary" 60 | BlueprintIdentifier = "6317518B1D59766500A9DDDC" 61 | BuildableName = "PMKA+Tests.xctest" 62 | BlueprintName = "PMKA+Tests" 63 | ReferencedContainer = "container:PromiseKit.xcodeproj"> 64 | </BuildableReference> 65 | </TestableReference> 66 | <TestableReference 67 | skipped = "NO" 68 | parallelizable = "YES" 69 | testExecutionOrdering = "random"> 70 | <BuildableReference 71 | BuildableIdentifier = "primary" 72 | BlueprintIdentifier = "631411321D59795700E24B9E" 73 | BuildableName = "PMKBridgeTests.xctest" 74 | BlueprintName = "PMKBridgeTests" 75 | ReferencedContainer = "container:PromiseKit.xcodeproj"> 76 | </BuildableReference> 77 | </TestableReference> 78 | <TestableReference 79 | skipped = "NO" 80 | parallelizable = "YES" 81 | testExecutionOrdering = "random"> 82 | <BuildableReference 83 | BuildableIdentifier = "primary" 84 | BlueprintIdentifier = "633027E0203CC0060037E136" 85 | BuildableName = "PMKDeprecatedTests.xctest" 86 | BlueprintName = "PMKDeprecatedTests" 87 | ReferencedContainer = "container:PromiseKit.xcodeproj"> 88 | </BuildableReference> 89 | </TestableReference> 90 | <TestableReference 91 | skipped = "NO" 92 | parallelizable = "YES" 93 | testExecutionOrdering = "random"> 94 | <BuildableReference 95 | BuildableIdentifier = "primary" 96 | BlueprintIdentifier = "C0244E4E2047A6CB00ACB4AC" 97 | BuildableName = "PMKJSA+Tests.xctest" 98 | BlueprintName = "PMKJSA+Tests" 99 | ReferencedContainer = "container:PromiseKit.xcodeproj"> 100 | </BuildableReference> 101 | </TestableReference> 102 | </Testables> 103 | </TestAction> 104 | <LaunchAction 105 | buildConfiguration = "Debug" 106 | selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" 107 | selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" 108 | launchStyle = "0" 109 | useCustomWorkingDirectory = "NO" 110 | ignoresPersistentStateOnLaunch = "NO" 111 | debugDocumentVersioning = "YES" 112 | debugServiceExtension = "internal" 113 | allowLocationSimulation = "YES"> 114 | <MacroExpansion> 115 | <BuildableReference 116 | BuildableIdentifier = "primary" 117 | BlueprintIdentifier = "63B0AC561D595E1B00FA21D9" 118 | BuildableName = "PromiseKit.framework" 119 | BlueprintName = "PromiseKit" 120 | ReferencedContainer = "container:PromiseKit.xcodeproj"> 121 | </BuildableReference> 122 | </MacroExpansion> 123 | </LaunchAction> 124 | <ProfileAction 125 | buildConfiguration = "Release" 126 | shouldUseLaunchSchemeArgsEnv = "YES" 127 | savedToolIdentifier = "" 128 | useCustomWorkingDirectory = "NO" 129 | debugDocumentVersioning = "YES"> 130 | <MacroExpansion> 131 | <BuildableReference 132 | BuildableIdentifier = "primary" 133 | BlueprintIdentifier = "63B0AC561D595E1B00FA21D9" 134 | BuildableName = "PromiseKit.framework" 135 | BlueprintName = "PromiseKit" 136 | ReferencedContainer = "container:PromiseKit.xcodeproj"> 137 | </BuildableReference> 138 | </MacroExpansion> 139 | </ProfileAction> 140 | <AnalyzeAction 141 | buildConfiguration = "Debug"> 142 | </AnalyzeAction> 143 | <ArchiveAction 144 | buildConfiguration = "Release" 145 | revealArchiveInOrganizer = "YES"> 146 | </ArchiveAction> 147 | </Scheme> 148 | -------------------------------------------------------------------------------- /Sources/AnyPromise+Private.h: -------------------------------------------------------------------------------- 1 | @import Foundation.NSError; 2 | @import Foundation.NSPointerArray; 3 | 4 | #if TARGET_OS_IPHONE 5 | #define NSPointerArrayMake(N) ({ \ 6 | NSPointerArray *aa = [NSPointerArray strongObjectsPointerArray]; \ 7 | aa.count = N; \ 8 | aa; \ 9 | }) 10 | #else 11 | static inline NSPointerArray * __nonnull NSPointerArrayMake(NSUInteger count) { 12 | #pragma clang diagnostic push 13 | #pragma clang diagnostic ignored "-Wdeprecated-declarations" 14 | NSPointerArray *aa = [[NSPointerArray class] respondsToSelector:@selector(strongObjectsPointerArray)] 15 | ? [NSPointerArray strongObjectsPointerArray] 16 | : [NSPointerArray pointerArrayWithStrongObjects]; 17 | #pragma clang diagnostic pop 18 | aa.count = count; 19 | return aa; 20 | } 21 | #endif 22 | 23 | #define IsError(o) [o isKindOfClass:[NSError class]] 24 | #define IsPromise(o) [o isKindOfClass:[AnyPromise class]] 25 | 26 | #import "AnyPromise.h" 27 | 28 | @class PMKArray; 29 | 30 | @interface AnyPromise () 31 | - (void)__pipe:(void(^ __nonnull)(__nullable id))block NS_REFINED_FOR_SWIFT; 32 | @end 33 | -------------------------------------------------------------------------------- /Sources/AnyPromise.m: -------------------------------------------------------------------------------- 1 | #if __has_include("PromiseKit-Swift.h") 2 | #import "PromiseKit-Swift.h" 3 | #else 4 | #import <PromiseKit/PromiseKit-Swift.h> 5 | #endif 6 | #import "PMKCallVariadicBlock.m" 7 | #import "AnyPromise+Private.h" 8 | #import "AnyPromise.h" 9 | 10 | NSString *const PMKErrorDomain = @"PMKErrorDomain"; 11 | 12 | 13 | @implementation AnyPromise { 14 | __AnyPromise *d; 15 | } 16 | 17 | - (instancetype)initWith__D:(__AnyPromise *)dd { 18 | self = [super init]; 19 | if (self) self->d = dd; 20 | return self; 21 | } 22 | 23 | - (instancetype)initWithResolver:(PMKResolver __strong *)resolver { 24 | self = [super init]; 25 | if (self) 26 | d = [[__AnyPromise alloc] initWithResolver:^(void (^resolve)(id)) { 27 | *resolver = resolve; 28 | }]; 29 | return self; 30 | } 31 | 32 | + (instancetype)promiseWithResolverBlock:(void (^)(PMKResolver _Nonnull))resolveBlock { 33 | id d = [[__AnyPromise alloc] initWithResolver:resolveBlock]; 34 | return [[self alloc] initWith__D:d]; 35 | } 36 | 37 | + (instancetype)promiseWithValue:(id)value { 38 | //TODO provide a more efficient route for sealed promises 39 | id d = [[__AnyPromise alloc] initWithResolver:^(void (^resolve)(id)) { 40 | resolve(value); 41 | }]; 42 | return [[self alloc] initWith__D:d]; 43 | } 44 | 45 | //TODO remove if possible, but used by when.m 46 | - (void)__pipe:(void (^)(id _Nullable))block { 47 | [d __pipe:block]; 48 | } 49 | 50 | //NOTE used by AnyPromise.swift 51 | - (id)__d { 52 | return d; 53 | } 54 | 55 | - (AnyPromise *(^)(id))then { 56 | return ^(id block) { 57 | return [self->d __thenOn:dispatch_get_main_queue() execute:^(id obj) { 58 | return PMKCallVariadicBlock(block, obj); 59 | }]; 60 | }; 61 | } 62 | 63 | - (AnyPromise *(^)(dispatch_queue_t, id))thenOn { 64 | return ^(dispatch_queue_t queue, id block) { 65 | return [self->d __thenOn:queue execute:^(id obj) { 66 | return PMKCallVariadicBlock(block, obj); 67 | }]; 68 | }; 69 | } 70 | 71 | - (AnyPromise *(^)(id))thenInBackground { 72 | return ^(id block) { 73 | return [self->d __thenOn:dispatch_get_global_queue(0, 0) execute:^(id obj) { 74 | return PMKCallVariadicBlock(block, obj); 75 | }]; 76 | }; 77 | } 78 | 79 | - (AnyPromise *(^)(dispatch_queue_t, id))catchOn { 80 | return ^(dispatch_queue_t q, id block) { 81 | return [self->d __catchOn:q execute:^(id obj) { 82 | return PMKCallVariadicBlock(block, obj); 83 | }]; 84 | }; 85 | } 86 | 87 | - (AnyPromise *(^)(id))catch { 88 | return ^(id block) { 89 | return [self->d __catchOn:dispatch_get_main_queue() execute:^(id obj) { 90 | return PMKCallVariadicBlock(block, obj); 91 | }]; 92 | }; 93 | } 94 | 95 | - (AnyPromise *(^)(id))catchInBackground { 96 | return ^(id block) { 97 | return [self->d __catchOn:dispatch_get_global_queue(0, 0) execute:^(id obj) { 98 | return PMKCallVariadicBlock(block, obj); 99 | }]; 100 | }; 101 | } 102 | 103 | - (AnyPromise *(^)(dispatch_block_t))ensure { 104 | return ^(dispatch_block_t block) { 105 | return [self->d __ensureOn:dispatch_get_main_queue() execute:block]; 106 | }; 107 | } 108 | 109 | - (AnyPromise *(^)(dispatch_queue_t, dispatch_block_t))ensureOn { 110 | return ^(dispatch_queue_t queue, dispatch_block_t block) { 111 | return [self->d __ensureOn:queue execute:block]; 112 | }; 113 | } 114 | 115 | - (AnyPromise *(^)(dispatch_block_t))ensureInBackground { 116 | return ^(dispatch_block_t block) { 117 | return [self->d __ensureOn:dispatch_get_global_queue(QOS_CLASS_UNSPECIFIED, 0) execute:block]; 118 | }; 119 | } 120 | 121 | - (id)wait { 122 | return [d __wait]; 123 | } 124 | 125 | - (BOOL)pending { 126 | return [[d valueForKey:@"__pending"] boolValue]; 127 | } 128 | 129 | - (BOOL)rejected { 130 | return IsError([d __value]); 131 | } 132 | 133 | - (BOOL)fulfilled { 134 | return !self.rejected; 135 | } 136 | 137 | - (id)value { 138 | id obj = [d __value]; 139 | 140 | if ([obj isKindOfClass:[PMKArray class]]) { 141 | return obj[0]; 142 | } else { 143 | return obj; 144 | } 145 | } 146 | 147 | @end 148 | 149 | 150 | 151 | @implementation AnyPromise (Adapters) 152 | 153 | + (instancetype)promiseWithAdapterBlock:(void (^)(PMKAdapter))block { 154 | return [self promiseWithResolverBlock:^(PMKResolver resolve) { 155 | block(^(id value, id error){ 156 | resolve(error ?: value); 157 | }); 158 | }]; 159 | } 160 | 161 | + (instancetype)promiseWithIntegerAdapterBlock:(void (^)(PMKIntegerAdapter))block { 162 | return [self promiseWithResolverBlock:^(PMKResolver resolve) { 163 | block(^(NSInteger value, id error){ 164 | if (error) { 165 | resolve(error); 166 | } else { 167 | resolve(@(value)); 168 | } 169 | }); 170 | }]; 171 | } 172 | 173 | + (instancetype)promiseWithBooleanAdapterBlock:(void (^)(PMKBooleanAdapter adapter))block { 174 | return [self promiseWithResolverBlock:^(PMKResolver resolve) { 175 | block(^(BOOL value, id error){ 176 | if (error) { 177 | resolve(error); 178 | } else { 179 | resolve(@(value)); 180 | } 181 | }); 182 | }]; 183 | } 184 | 185 | @end 186 | -------------------------------------------------------------------------------- /Sources/Async.swift: -------------------------------------------------------------------------------- 1 | #if swift(>=5.5) 2 | #if canImport(_Concurrency) 3 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 4 | public extension Guarantee { 5 | func async() async -> T { 6 | await withCheckedContinuation { continuation in 7 | done { value in 8 | continuation.resume(returning: value) 9 | } 10 | } 11 | } 12 | } 13 | 14 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 15 | public extension Promise { 16 | func async() async throws -> T { 17 | try await withCheckedThrowingContinuation { continuation in 18 | done { value in 19 | continuation.resume(returning: value) 20 | }.catch(policy: .allErrors) { error in 21 | continuation.resume(throwing: error) 22 | } 23 | } 24 | } 25 | } 26 | #endif 27 | #endif 28 | 29 | -------------------------------------------------------------------------------- /Sources/Box.swift: -------------------------------------------------------------------------------- 1 | import Dispatch 2 | 3 | enum Sealant<R> { 4 | case pending(Handlers<R>) 5 | case resolved(R) 6 | } 7 | 8 | final class Handlers<R> { 9 | var bodies: [(R) -> Void] = [] 10 | func append(_ item: @escaping(R) -> Void) { bodies.append(item) } 11 | } 12 | 13 | /// - Remark: not protocol ∵ http://www.russbishop.net/swift-associated-types-cont 14 | class Box<T> { 15 | func inspect() -> Sealant<T> { fatalError() } 16 | func inspect(_: (Sealant<T>) -> Void) { fatalError() } 17 | func seal(_: T) {} 18 | } 19 | 20 | final class SealedBox<T>: Box<T> { 21 | let value: T 22 | 23 | init(value: T) { 24 | self.value = value 25 | } 26 | 27 | override func inspect() -> Sealant<T> { 28 | return .resolved(value) 29 | } 30 | } 31 | 32 | class EmptyBox<T>: Box<T> { 33 | private var sealant = Sealant<T>.pending(.init()) 34 | private let barrier = DispatchQueue(label: "org.promisekit.barrier", attributes: .concurrent) 35 | 36 | override func seal(_ value: T) { 37 | var handlers: Handlers<T>! 38 | barrier.sync(flags: .barrier) { 39 | guard case .pending(let _handlers) = self.sealant else { 40 | return // already fulfilled! 41 | } 42 | handlers = _handlers 43 | self.sealant = .resolved(value) 44 | } 45 | 46 | //FIXME we are resolved so should `pipe(to:)` be called at this instant, “thens are called in order” would be invalid 47 | //NOTE we don’t do this in the above `sync` because that could potentially deadlock 48 | //THOUGH since `then` etc. typically invoke after a run-loop cycle, this issue is somewhat less severe 49 | 50 | if let handlers = handlers { 51 | handlers.bodies.forEach{ $0(value) } 52 | } 53 | 54 | //TODO solution is an unfortunate third state “sealed” where then's get added 55 | // to a separate handler pool for that state 56 | // any other solution has potential races 57 | } 58 | 59 | override func inspect() -> Sealant<T> { 60 | var rv: Sealant<T>! 61 | barrier.sync { 62 | rv = self.sealant 63 | } 64 | return rv 65 | } 66 | 67 | override func inspect(_ body: (Sealant<T>) -> Void) { 68 | var sealed = false 69 | barrier.sync(flags: .barrier) { 70 | switch sealant { 71 | case .pending: 72 | // body will append to handlers, so we must stay barrier’d 73 | body(sealant) 74 | case .resolved: 75 | sealed = true 76 | } 77 | } 78 | if sealed { 79 | // we do this outside the barrier to prevent potential deadlocks 80 | // it's safe because we never transition away from this state 81 | body(sealant) 82 | } 83 | } 84 | } 85 | 86 | 87 | extension Optional where Wrapped: DispatchQueue { 88 | @inline(__always) 89 | func async(flags: DispatchWorkItemFlags?, _ body: @escaping() -> Void) { 90 | switch self { 91 | case .none: 92 | body() 93 | case .some(let q): 94 | if let flags = flags { 95 | q.async(flags: flags, execute: body) 96 | } else { 97 | q.async(execute: body) 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Sources/Combine.swift: -------------------------------------------------------------------------------- 1 | #if swift(>=4.1) 2 | #if canImport(Combine) 3 | import Combine 4 | 5 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 6 | public extension Guarantee { 7 | func future() -> Future<T, Never> { 8 | .init { [weak self] promise in 9 | self?.done { value in 10 | promise(.success(value)) 11 | } 12 | } 13 | } 14 | } 15 | 16 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 17 | public extension Promise { 18 | func future() -> Future<T, Error> { 19 | .init { [weak self] promise in 20 | self?.done { value in 21 | promise(.success(value)) 22 | }.catch { error in 23 | promise(.failure(error)) 24 | } 25 | } 26 | } 27 | } 28 | 29 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 30 | public extension Future { 31 | func promise() -> PromiseKit.Promise<Output> { 32 | return .init { [weak self] resolver in 33 | var cancellable: AnyCancellable? 34 | cancellable = self?.sink(receiveCompletion: { completion in 35 | cancellable?.cancel() 36 | cancellable = nil 37 | switch completion { 38 | case .failure(let error): 39 | resolver.reject(error) 40 | case .finished: 41 | break 42 | } 43 | }, receiveValue: { value in 44 | cancellable?.cancel() 45 | cancellable = nil 46 | resolver.fulfill(value) 47 | }) 48 | } 49 | } 50 | } 51 | 52 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 53 | public extension Future where Failure == Never { 54 | func guarantee() -> Guarantee<Output> { 55 | return .init { [weak self] resolver in 56 | var cancellable: AnyCancellable? 57 | cancellable = self?.sink(receiveValue: { value in 58 | cancellable?.cancel() 59 | cancellable = nil 60 | resolver(value) 61 | }) 62 | } 63 | } 64 | } 65 | #endif 66 | #endif 67 | -------------------------------------------------------------------------------- /Sources/Configuration.swift: -------------------------------------------------------------------------------- 1 | import Dispatch 2 | 3 | /** 4 | PromiseKit’s configurable parameters. 5 | 6 | Do not change these after any Promise machinery executes as the configuration object is not thread-safe. 7 | 8 | We would like it to be, but sadly `Swift` does not expose `dispatch_once` et al. which is what we used to use in order to make the configuration immutable once first used. 9 | */ 10 | public struct PMKConfiguration { 11 | /// The default queues that promises handlers dispatch to 12 | public var Q: (map: DispatchQueue?, return: DispatchQueue?) = (map: DispatchQueue.main, return: DispatchQueue.main) 13 | 14 | /// The default catch-policy for all `catch` and `resolve` 15 | public var catchPolicy = CatchPolicy.allErrorsExceptCancellation 16 | 17 | /// The closure used to log PromiseKit events. 18 | /// Not thread safe; change before processing any promises. 19 | /// - Note: The default handler calls `print()` 20 | public var logHandler: (LogEvent) -> Void = { event in 21 | switch event { 22 | case .waitOnMainThread: 23 | print("PromiseKit: warning: `wait()` called on main thread!") 24 | case .pendingPromiseDeallocated: 25 | print("PromiseKit: warning: pending promise deallocated") 26 | case .pendingGuaranteeDeallocated: 27 | print("PromiseKit: warning: pending guarantee deallocated") 28 | case .cauterized (let error): 29 | print("PromiseKit:cauterized-error: \(error)") 30 | } 31 | } 32 | } 33 | 34 | /// Modify this as soon as possible in your application’s lifetime 35 | public var conf = PMKConfiguration() 36 | -------------------------------------------------------------------------------- /Sources/CustomStringConvertible.swift: -------------------------------------------------------------------------------- 1 | 2 | extension Promise: CustomStringConvertible { 3 | /// - Returns: A description of the state of this promise. 4 | public var description: String { 5 | switch result { 6 | case nil: 7 | return "Promise(…\(T.self))" 8 | case .rejected(let error)?: 9 | return "Promise(\(error))" 10 | case .fulfilled(let value)?: 11 | return "Promise(\(value))" 12 | } 13 | } 14 | } 15 | 16 | extension Promise: CustomDebugStringConvertible { 17 | /// - Returns: A debug-friendly description of the state of this promise. 18 | public var debugDescription: String { 19 | switch box.inspect() { 20 | case .pending(let handlers): 21 | return "Promise<\(T.self)>.pending(handlers: \(handlers.bodies.count))" 22 | case .resolved(.rejected(let error)): 23 | return "Promise<\(T.self)>.rejected(\(type(of: error)).\(error))" 24 | case .resolved(.fulfilled(let value)): 25 | return "Promise<\(T.self)>.fulfilled(\(value))" 26 | } 27 | } 28 | } 29 | 30 | #if !SWIFT_PACKAGE 31 | extension AnyPromise { 32 | /// - Returns: A description of the state of this promise. 33 | override open var description: String { 34 | switch box.inspect() { 35 | case .pending: 36 | return "AnyPromise(…)" 37 | case .resolved(let obj?): 38 | return "AnyPromise(\(obj))" 39 | case .resolved(nil): 40 | return "AnyPromise(nil)" 41 | } 42 | } 43 | } 44 | #endif 45 | -------------------------------------------------------------------------------- /Sources/Deprecations.swift: -------------------------------------------------------------------------------- 1 | import Dispatch 2 | 3 | @available(*, deprecated, message: "See `init(resolver:)`") 4 | public func wrap<T>(_ body: (@escaping (T?, Error?) -> Void) throws -> Void) -> Promise<T> { 5 | return Promise { seal in 6 | try body(seal.resolve) 7 | } 8 | } 9 | 10 | @available(*, deprecated, message: "See `init(resolver:)`") 11 | public func wrap<T>(_ body: (@escaping (T, Error?) -> Void) throws -> Void) -> Promise<T> { 12 | return Promise { seal in 13 | try body(seal.resolve) 14 | } 15 | } 16 | 17 | @available(*, deprecated, message: "See `init(resolver:)`") 18 | public func wrap<T>(_ body: (@escaping (Error?, T?) -> Void) throws -> Void) -> Promise<T> { 19 | return Promise { seal in 20 | try body(seal.resolve) 21 | } 22 | } 23 | 24 | @available(*, deprecated, message: "See `init(resolver:)`") 25 | public func wrap(_ body: (@escaping (Error?) -> Void) throws -> Void) -> Promise<Void> { 26 | return Promise { seal in 27 | try body(seal.resolve) 28 | } 29 | } 30 | 31 | @available(*, deprecated, message: "See `init(resolver:)`") 32 | public func wrap<T>(_ body: (@escaping (T) -> Void) throws -> Void) -> Promise<T> { 33 | return Promise { seal in 34 | try body(seal.fulfill) 35 | } 36 | } 37 | 38 | public extension Promise { 39 | @available(*, deprecated, message: "See `ensure`") 40 | func always(on q: DispatchQueue = .main, execute body: @escaping () -> Void) -> Promise { 41 | return ensure(on: q, body) 42 | } 43 | } 44 | 45 | public extension Thenable { 46 | #if PMKFullDeprecations 47 | /// disabled due to ambiguity with the other `.flatMap` 48 | @available(*, deprecated, message: "See: `compactMap`") 49 | func flatMap<U>(on: DispatchQueue? = conf.Q.map, _ transform: @escaping(T) throws -> U?) -> Promise<U> { 50 | return compactMap(on: on, transform) 51 | } 52 | #endif 53 | } 54 | 55 | public extension Thenable where T: Sequence { 56 | #if PMKFullDeprecations 57 | /// disabled due to ambiguity with the other `.map` 58 | @available(*, deprecated, message: "See: `mapValues`") 59 | func map<U>(on: DispatchQueue? = conf.Q.map, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U]> { 60 | return mapValues(on: on, transform) 61 | } 62 | 63 | /// disabled due to ambiguity with the other `.flatMap` 64 | @available(*, deprecated, message: "See: `flatMapValues`") 65 | func flatMap<U: Sequence>(on: DispatchQueue? = conf.Q.map, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U.Iterator.Element]> { 66 | return flatMapValues(on: on, transform) 67 | } 68 | #endif 69 | 70 | @available(*, deprecated, message: "See: `filterValues`") 71 | func filter(on: DispatchQueue? = conf.Q.map, test: @escaping (T.Iterator.Element) -> Bool) -> Promise<[T.Iterator.Element]> { 72 | return filterValues(on: on, test) 73 | } 74 | } 75 | 76 | public extension Thenable where T: Collection { 77 | @available(*, deprecated, message: "See: `firstValue`") 78 | var first: Promise<T.Iterator.Element> { 79 | return firstValue 80 | } 81 | 82 | @available(*, deprecated, message: "See: `lastValue`") 83 | var last: Promise<T.Iterator.Element> { 84 | return lastValue 85 | } 86 | } 87 | 88 | public extension Thenable where T: Sequence, T.Iterator.Element: Comparable { 89 | @available(*, deprecated, message: "See: `sortedValues`") 90 | func sorted(on: DispatchQueue? = conf.Q.map) -> Promise<[T.Iterator.Element]> { 91 | return sortedValues(on: on) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Sources/Error.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum PMKError: Error { 4 | /** 5 | The completionHandler with form `(T?, Error?)` was called with `(nil, nil)`. 6 | This is invalid as per Cocoa/Apple calling conventions. 7 | */ 8 | case invalidCallingConvention 9 | 10 | /** 11 | A handler returned its own promise. 99% of the time, this is likely a 12 | programming error. It is also invalid per Promises/A+. 13 | */ 14 | case returnedSelf 15 | 16 | /** `when()`, `race()` etc. were called with invalid parameters, eg. an empty array. */ 17 | case badInput 18 | 19 | /// The operation was cancelled 20 | case cancelled 21 | 22 | /// `nil` was returned from `flatMap` 23 | @available(*, deprecated, message: "See: `compactMap`") 24 | case flatMap(Any, Any.Type) 25 | 26 | /// `nil` was returned from `compactMap` 27 | case compactMap(Any, Any.Type) 28 | 29 | /** 30 | The lastValue or firstValue of a sequence was requested but the sequence was empty. 31 | 32 | Also used if all values of this collection failed the test passed to `firstValue(where:)`. 33 | */ 34 | case emptySequence 35 | 36 | /// no winner in `race(fulfilled:)` 37 | case noWinner 38 | } 39 | 40 | extension PMKError: CustomDebugStringConvertible { 41 | public var debugDescription: String { 42 | switch self { 43 | case .flatMap(let obj, let type): 44 | return "Could not `flatMap<\(type)>`: \(obj)" 45 | case .compactMap(let obj, let type): 46 | return "Could not `compactMap<\(type)>`: \(obj)" 47 | case .invalidCallingConvention: 48 | return "A closure was called with an invalid calling convention, probably (nil, nil)" 49 | case .returnedSelf: 50 | return "A promise handler returned itself" 51 | case .badInput: 52 | return "Bad input was provided to a PromiseKit function" 53 | case .cancelled: 54 | return "The asynchronous sequence was cancelled" 55 | case .emptySequence: 56 | return "The first or last element was requested for an empty sequence" 57 | case .noWinner: 58 | return "All thenables passed to race(fulfilled:) were rejected" 59 | } 60 | } 61 | } 62 | 63 | extension PMKError: LocalizedError { 64 | public var errorDescription: String? { 65 | return debugDescription 66 | } 67 | } 68 | 69 | 70 | //////////////////////////////////////////////////////////// Cancellation 71 | 72 | /// An error that may represent the cancelled condition 73 | public protocol CancellableError: Error { 74 | /// returns true if this Error represents a cancelled condition 75 | var isCancelled: Bool { get } 76 | } 77 | 78 | extension Error { 79 | public var isCancelled: Bool { 80 | do { 81 | throw self 82 | } catch PMKError.cancelled { 83 | return true 84 | } catch let error as CancellableError { 85 | return error.isCancelled 86 | } catch URLError.cancelled { 87 | return true 88 | } catch CocoaError.userCancelled { 89 | return true 90 | } catch let error as NSError { 91 | #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) 92 | let domain = error.domain 93 | let code = error.code 94 | return ("SKErrorDomain", 2) == (domain, code) 95 | #else 96 | return false 97 | #endif 98 | } catch { 99 | return false 100 | } 101 | } 102 | } 103 | 104 | /// Used by `catch` and `recover` 105 | public enum CatchPolicy { 106 | /// Indicates that `catch` or `recover` handle all error types including cancellable-errors. 107 | case allErrors 108 | 109 | /// Indicates that `catch` or `recover` handle all error except cancellable-errors. 110 | case allErrorsExceptCancellation 111 | } 112 | -------------------------------------------------------------------------------- /Sources/Info.plist: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 | <plist version="1.0"> 4 | <dict> 5 | <key>CFBundleDevelopmentRegion</key> 6 | <string>en</string> 7 | <key>CFBundleExecutable</key> 8 | <string>$(EXECUTABLE_NAME)</string> 9 | <key>CFBundleIdentifier</key> 10 | <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> 11 | <key>CFBundleInfoDictionaryVersion</key> 12 | <string>6.0</string> 13 | <key>CFBundleName</key> 14 | <string>$(PRODUCT_NAME)</string> 15 | <key>CFBundlePackageType</key> 16 | <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> 17 | <key>CFBundleShortVersionString</key> 18 | <string>$(CURRENT_PROJECT_VERSION)</string> 19 | <key>CFBundleSignature</key> 20 | <string>????</string> 21 | <key>CFBundleVersion</key> 22 | <string>1</string> 23 | <key>UISupportedInterfaceOrientations</key> 24 | <array> 25 | <string>UIInterfaceOrientationPortrait</string> 26 | </array> 27 | </dict> 28 | </plist> 29 | -------------------------------------------------------------------------------- /Sources/LogEvent.swift: -------------------------------------------------------------------------------- 1 | /** 2 | The PromiseKit events which may be logged. 3 | 4 | ```` 5 | /// A promise or guarantee has blocked the main thread 6 | case waitOnMainThread 7 | 8 | /// A promise has been deallocated without being resolved 9 | case pendingPromiseDeallocated 10 | 11 | /// An error which occurred while fulfilling a promise was swallowed 12 | case cauterized(Error) 13 | 14 | /// Errors which give a string error message 15 | case misc (String) 16 | ```` 17 | */ 18 | public enum LogEvent { 19 | /// A promise or guarantee has blocked the main thread 20 | case waitOnMainThread 21 | 22 | /// A promise has been deallocated without being resolved 23 | case pendingPromiseDeallocated 24 | 25 | /// A guarantee has been deallocated without being resolved 26 | case pendingGuaranteeDeallocated 27 | 28 | /// An error which occurred while resolving a promise was swallowed 29 | case cauterized(Error) 30 | } 31 | -------------------------------------------------------------------------------- /Sources/NSMethodSignatureForBlock.m: -------------------------------------------------------------------------------- 1 | #import <Foundation/NSMethodSignature.h> 2 | 3 | struct PMKBlockLiteral { 4 | void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock 5 | int flags; 6 | int reserved; 7 | void (*invoke)(void *, ...); 8 | struct block_descriptor { 9 | unsigned long int reserved; // NULL 10 | unsigned long int size; // sizeof(struct Block_literal_1) 11 | // optional helper functions 12 | void (*copy_helper)(void *dst, void *src); // IFF (1<<25) 13 | void (*dispose_helper)(void *src); // IFF (1<<25) 14 | // required ABI.2010.3.16 15 | const char *signature; // IFF (1<<30) 16 | } *descriptor; 17 | // imported variables 18 | }; 19 | 20 | typedef NS_OPTIONS(NSUInteger, PMKBlockDescriptionFlags) { 21 | PMKBlockDescriptionFlagsHasCopyDispose = (1 << 25), 22 | PMKBlockDescriptionFlagsHasCtor = (1 << 26), // helpers have C++ code 23 | PMKBlockDescriptionFlagsIsGlobal = (1 << 28), 24 | PMKBlockDescriptionFlagsHasStret = (1 << 29), // IFF BLOCK_HAS_SIGNATURE 25 | PMKBlockDescriptionFlagsHasSignature = (1 << 30) 26 | }; 27 | 28 | // It appears 10.7 doesn't support quotes in method signatures. Remove them 29 | // via @rabovik's method. See https://github.com/OliverLetterer/SLObjectiveCRuntimeAdditions/pull/2 30 | #if defined(__MAC_OS_X_VERSION_MIN_REQUIRED) && __MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_10_8 31 | NS_INLINE static const char * pmk_removeQuotesFromMethodSignature(const char *str){ 32 | char *result = malloc(strlen(str) + 1); 33 | BOOL skip = NO; 34 | char *to = result; 35 | char c; 36 | while ((c = *str++)) { 37 | if ('"' == c) { 38 | skip = !skip; 39 | continue; 40 | } 41 | if (skip) continue; 42 | *to++ = c; 43 | } 44 | *to = '\0'; 45 | return result; 46 | } 47 | #endif 48 | 49 | static NSMethodSignature *NSMethodSignatureForBlock(id block) { 50 | if (!block) 51 | return nil; 52 | 53 | struct PMKBlockLiteral *blockRef = (__bridge struct PMKBlockLiteral *)block; 54 | PMKBlockDescriptionFlags flags = (PMKBlockDescriptionFlags)blockRef->flags; 55 | 56 | if (flags & PMKBlockDescriptionFlagsHasSignature) { 57 | void *signatureLocation = blockRef->descriptor; 58 | signatureLocation += sizeof(unsigned long int); 59 | signatureLocation += sizeof(unsigned long int); 60 | 61 | if (flags & PMKBlockDescriptionFlagsHasCopyDispose) { 62 | signatureLocation += sizeof(void(*)(void *dst, void *src)); 63 | signatureLocation += sizeof(void (*)(void *src)); 64 | } 65 | 66 | const char *signature = (*(const char **)signatureLocation); 67 | #if defined(__MAC_OS_X_VERSION_MIN_REQUIRED) && __MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_10_8 68 | signature = pmk_removeQuotesFromMethodSignature(signature); 69 | NSMethodSignature *nsSignature = [NSMethodSignature signatureWithObjCTypes:signature]; 70 | free((void *)signature); 71 | 72 | return nsSignature; 73 | #endif 74 | return [NSMethodSignature signatureWithObjCTypes:signature]; 75 | } 76 | return 0; 77 | } 78 | -------------------------------------------------------------------------------- /Sources/PMKCallVariadicBlock.m: -------------------------------------------------------------------------------- 1 | #import "NSMethodSignatureForBlock.m" 2 | #import <Foundation/NSDictionary.h> 3 | #import <Foundation/NSException.h> 4 | #import "AnyPromise+Private.h" 5 | #import <Foundation/NSError.h> 6 | #import <dispatch/once.h> 7 | #import <string.h> 8 | 9 | #ifndef PMKLog 10 | #define PMKLog NSLog 11 | #endif 12 | 13 | @interface PMKArray : NSObject { 14 | @public 15 | id objs[3]; 16 | NSUInteger count; 17 | } @end 18 | 19 | @implementation PMKArray 20 | 21 | - (id)objectAtIndexedSubscript:(NSUInteger)idx { 22 | if (count <= idx) { 23 | // this check is necessary due to lack of checks in `pmk_safely_call_block` 24 | return nil; 25 | } 26 | return objs[idx]; 27 | } 28 | 29 | @end 30 | 31 | id __PMKArrayWithCount(NSUInteger count, ...) { 32 | PMKArray *this = [PMKArray new]; 33 | this->count = count; 34 | va_list args; 35 | va_start(args, count); 36 | for (NSUInteger x = 0; x < count; ++x) 37 | this->objs[x] = va_arg(args, id); 38 | va_end(args); 39 | return this; 40 | } 41 | 42 | 43 | static inline id _PMKCallVariadicBlock(id frock, id result) { 44 | NSCAssert(frock, @""); 45 | 46 | NSMethodSignature *sig = NSMethodSignatureForBlock(frock); 47 | const NSUInteger nargs = sig.numberOfArguments; 48 | const char rtype = sig.methodReturnType[0]; 49 | 50 | #define call_block_with_rtype(type) ({^type{ \ 51 | switch (nargs) { \ 52 | case 1: \ 53 | return ((type(^)(void))frock)(); \ 54 | case 2: { \ 55 | const id arg = [result class] == [PMKArray class] ? result[0] : result; \ 56 | return ((type(^)(id))frock)(arg); \ 57 | } \ 58 | case 3: { \ 59 | type (^block)(id, id) = frock; \ 60 | return [result class] == [PMKArray class] \ 61 | ? block(result[0], result[1]) \ 62 | : block(result, nil); \ 63 | } \ 64 | case 4: { \ 65 | type (^block)(id, id, id) = frock; \ 66 | return [result class] == [PMKArray class] \ 67 | ? block(result[0], result[1], result[2]) \ 68 | : block(result, nil, nil); \ 69 | } \ 70 | default: \ 71 | @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"PromiseKit: The provided block’s argument count is unsupported." userInfo:nil]; \ 72 | }}();}) 73 | 74 | switch (rtype) { 75 | case 'v': 76 | call_block_with_rtype(void); 77 | return nil; 78 | case '@': 79 | return call_block_with_rtype(id) ?: nil; 80 | case '*': { 81 | char *str = call_block_with_rtype(char *); 82 | return str ? @(str) : nil; 83 | } 84 | case 'c': return @(call_block_with_rtype(char)); 85 | case 'i': return @(call_block_with_rtype(int)); 86 | case 's': return @(call_block_with_rtype(short)); 87 | case 'l': return @(call_block_with_rtype(long)); 88 | case 'q': return @(call_block_with_rtype(long long)); 89 | case 'C': return @(call_block_with_rtype(unsigned char)); 90 | case 'I': return @(call_block_with_rtype(unsigned int)); 91 | case 'S': return @(call_block_with_rtype(unsigned short)); 92 | case 'L': return @(call_block_with_rtype(unsigned long)); 93 | case 'Q': return @(call_block_with_rtype(unsigned long long)); 94 | case 'f': return @(call_block_with_rtype(float)); 95 | case 'd': return @(call_block_with_rtype(double)); 96 | case 'B': return @(call_block_with_rtype(_Bool)); 97 | case '^': 98 | if (strcmp(sig.methodReturnType, "^v") == 0) { 99 | call_block_with_rtype(void); 100 | return nil; 101 | } 102 | // else fall through! 103 | default: 104 | @throw [NSException exceptionWithName:@"PromiseKit" reason:@"PromiseKit: Unsupported method signature." userInfo:nil]; 105 | } 106 | } 107 | 108 | static id PMKCallVariadicBlock(id frock, id result) { 109 | @try { 110 | return _PMKCallVariadicBlock(frock, result); 111 | } @catch (id thrown) { 112 | if ([thrown isKindOfClass:[NSString class]]) 113 | return thrown; 114 | if ([thrown isKindOfClass:[NSError class]]) 115 | return thrown; 116 | 117 | // we don’t catch objc exceptions: they are meant to crash your app 118 | @throw thrown; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Sources/Promise.swift: -------------------------------------------------------------------------------- 1 | import class Foundation.Thread 2 | import Dispatch 3 | 4 | /** 5 | A `Promise` is a functional abstraction around a failable asynchronous operation. 6 | - See: `Thenable` 7 | */ 8 | public final class Promise<T>: Thenable, CatchMixin { 9 | let box: Box<Result<T>> 10 | 11 | fileprivate init(box: SealedBox<Result<T>>) { 12 | self.box = box 13 | } 14 | 15 | /** 16 | Initialize a new fulfilled promise. 17 | 18 | We do not provide `init(value:)` because Swift is “greedy” 19 | and would pick that initializer in cases where it should pick 20 | one of the other more specific options leading to Promises with 21 | `T` that is eg: `Error` or worse `(T->Void,Error->Void)` for 22 | uses of our PMK < 4 pending initializer due to Swift trailing 23 | closure syntax (nothing good comes without pain!). 24 | 25 | Though often easy to detect, sometimes these issues would be 26 | hidden by other type inference leading to some nasty bugs in 27 | production. 28 | 29 | In PMK5 we tried to work around this by making the pending 30 | initializer take the form `Promise(.pending)` but this led to 31 | bad migration errors for PMK4 users. Hence instead we quickly 32 | released PMK6 and now only provide this initializer for making 33 | sealed & fulfilled promises. 34 | 35 | Usage is still (usually) good: 36 | 37 | guard foo else { 38 | return .value(bar) 39 | } 40 | */ 41 | public static func value(_ value: T) -> Promise<T> { 42 | return Promise(box: SealedBox(value: .fulfilled(value))) 43 | } 44 | 45 | /// Initialize a new rejected promise. 46 | public init(error: Error) { 47 | box = SealedBox(value: .rejected(error)) 48 | } 49 | 50 | /// Initialize a new promise bound to the provided `Thenable`. 51 | public init<U: Thenable>(_ bridge: U) where U.T == T { 52 | box = EmptyBox() 53 | bridge.pipe(to: box.seal) 54 | } 55 | 56 | /// Initialize a new promise that can be resolved with the provided `Resolver`. 57 | public init(resolver body: (Resolver<T>) throws -> Void) { 58 | box = EmptyBox() 59 | let resolver = Resolver(box) 60 | do { 61 | try body(resolver) 62 | } catch { 63 | resolver.reject(error) 64 | } 65 | } 66 | 67 | /// - Returns: a tuple of a new pending promise and its `Resolver`. 68 | public class func pending() -> (promise: Promise<T>, resolver: Resolver<T>) { 69 | return { ($0, Resolver($0.box)) }(Promise<T>(.pending)) 70 | } 71 | 72 | /// - See: `Thenable.pipe` 73 | public func pipe(to: @escaping(Result<T>) -> Void) { 74 | switch box.inspect() { 75 | case .pending: 76 | box.inspect { 77 | switch $0 { 78 | case .pending(let handlers): 79 | handlers.append(to) 80 | case .resolved(let value): 81 | to(value) 82 | } 83 | } 84 | case .resolved(let value): 85 | to(value) 86 | } 87 | } 88 | 89 | /// - See: `Thenable.result` 90 | public var result: Result<T>? { 91 | switch box.inspect() { 92 | case .pending: 93 | return nil 94 | case .resolved(let result): 95 | return result 96 | } 97 | } 98 | 99 | init(_: PMKUnambiguousInitializer) { 100 | box = EmptyBox() 101 | } 102 | } 103 | 104 | public extension Promise { 105 | /** 106 | Blocks this thread, so—you know—don’t call this on a serial thread that 107 | any part of your chain may use. Like the main thread for example. 108 | */ 109 | func wait() throws -> T { 110 | 111 | if Thread.isMainThread { 112 | conf.logHandler(LogEvent.waitOnMainThread) 113 | } 114 | 115 | var result = self.result 116 | 117 | if result == nil { 118 | let group = DispatchGroup() 119 | group.enter() 120 | pipe { result = $0; group.leave() } 121 | group.wait() 122 | } 123 | 124 | switch result! { 125 | case .rejected(let error): 126 | throw error 127 | case .fulfilled(let value): 128 | return value 129 | } 130 | } 131 | } 132 | 133 | #if swift(>=3.1) 134 | extension Promise where T == Void { 135 | /// Initializes a new promise fulfilled with `Void` 136 | public convenience init() { 137 | self.init(box: SealedBox(value: .fulfilled(Void()))) 138 | } 139 | 140 | /// Returns a new promise fulfilled with `Void` 141 | public static var value: Promise<Void> { 142 | return .value(Void()) 143 | } 144 | } 145 | #endif 146 | 147 | 148 | public extension DispatchQueue { 149 | /** 150 | Asynchronously executes the provided closure on a dispatch queue. 151 | 152 | DispatchQueue.global().async(.promise) { 153 | try md5(input) 154 | }.done { md5 in 155 | //… 156 | } 157 | 158 | - Parameter body: The closure that resolves this promise. 159 | - Returns: A new `Promise` resolved by the result of the provided closure. 160 | - Note: There is no Promise/Thenable version of this due to Swift compiler ambiguity issues. 161 | */ 162 | @available(macOS 10.10, iOS 8.0, tvOS 9.0, watchOS 2.0, *) 163 | final func async<T>(_: PMKNamespacer, group: DispatchGroup? = nil, qos: DispatchQoS = .default, flags: DispatchWorkItemFlags = [], execute body: @escaping () throws -> T) -> Promise<T> { 164 | let promise = Promise<T>(.pending) 165 | async(group: group, qos: qos, flags: flags) { 166 | do { 167 | promise.box.seal(.fulfilled(try body())) 168 | } catch { 169 | promise.box.seal(.rejected(error)) 170 | } 171 | } 172 | return promise 173 | } 174 | } 175 | 176 | 177 | /// used by our extensions to provide unambiguous functions with the same name as the original function 178 | public enum PMKNamespacer { 179 | case promise 180 | } 181 | 182 | enum PMKUnambiguousInitializer { 183 | case pending 184 | } 185 | -------------------------------------------------------------------------------- /Sources/PromiseKit.h: -------------------------------------------------------------------------------- 1 | #import <PromiseKit/fwd.h> 2 | #import <PromiseKit/AnyPromise.h> 3 | 4 | #import <Foundation/NSObjCRuntime.h> // `FOUNDATION_EXPORT` 5 | 6 | FOUNDATION_EXPORT double PromiseKitVersionNumber; 7 | FOUNDATION_EXPORT const unsigned char PromiseKitVersionString[]; 8 | -------------------------------------------------------------------------------- /Sources/Resolver.swift: -------------------------------------------------------------------------------- 1 | /// An object for resolving promises 2 | public final class Resolver<T> { 3 | let box: Box<Result<T>> 4 | 5 | init(_ box: Box<Result<T>>) { 6 | self.box = box 7 | } 8 | 9 | deinit { 10 | if case .pending = box.inspect() { 11 | conf.logHandler(.pendingPromiseDeallocated) 12 | } 13 | } 14 | } 15 | 16 | public extension Resolver { 17 | /// Fulfills the promise with the provided value 18 | func fulfill(_ value: T) { 19 | box.seal(.fulfilled(value)) 20 | } 21 | 22 | /// Rejects the promise with the provided error 23 | func reject(_ error: Error) { 24 | box.seal(.rejected(error)) 25 | } 26 | 27 | /// Resolves the promise with the provided result 28 | func resolve(_ result: Result<T>) { 29 | box.seal(result) 30 | } 31 | 32 | /// Resolves the promise with the provided value or error 33 | func resolve(_ obj: T?, _ error: Error?) { 34 | if let error = error { 35 | reject(error) 36 | } else if let obj = obj { 37 | fulfill(obj) 38 | } else { 39 | reject(PMKError.invalidCallingConvention) 40 | } 41 | } 42 | 43 | /// Fulfills the promise with the provided value unless the provided error is non-nil 44 | func resolve(_ obj: T, _ error: Error?) { 45 | if let error = error { 46 | reject(error) 47 | } else { 48 | fulfill(obj) 49 | } 50 | } 51 | 52 | /// Resolves the promise, provided for non-conventional value-error ordered completion handlers. 53 | func resolve(_ error: Error?, _ obj: T?) { 54 | resolve(obj, error) 55 | } 56 | } 57 | 58 | #if swift(>=3.1) 59 | extension Resolver where T == Void { 60 | /// Fulfills the promise unless error is non-nil 61 | public func resolve(_ error: Error?) { 62 | if let error = error { 63 | reject(error) 64 | } else { 65 | fulfill(()) 66 | } 67 | } 68 | #if false 69 | // disabled ∵ https://github.com/mxcl/PromiseKit/issues/990 70 | 71 | /// Fulfills the promise 72 | public func fulfill() { 73 | self.fulfill(()) 74 | } 75 | #else 76 | /// Fulfills the promise 77 | /// - Note: underscore is present due to: https://github.com/mxcl/PromiseKit/issues/990 78 | public func fulfill_() { 79 | self.fulfill(()) 80 | } 81 | #endif 82 | } 83 | #endif 84 | 85 | #if swift(>=5.0) 86 | extension Resolver { 87 | /// Resolves the promise with the provided result 88 | public func resolve<E: Error>(_ result: Swift.Result<T, E>) { 89 | switch result { 90 | case .failure(let error): self.reject(error) 91 | case .success(let value): self.fulfill(value) 92 | } 93 | } 94 | } 95 | #endif 96 | 97 | public enum Result<T> { 98 | case fulfilled(T) 99 | case rejected(Error) 100 | } 101 | 102 | public extension PromiseKit.Result { 103 | var isFulfilled: Bool { 104 | switch self { 105 | case .fulfilled: 106 | return true 107 | case .rejected: 108 | return false 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Sources/Resources/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 | <plist version="1.0"> 4 | <dict> 5 | <key>NSPrivacyTracking</key> 6 | <false/> 7 | </dict> 8 | </plist> 9 | -------------------------------------------------------------------------------- /Sources/after.m: -------------------------------------------------------------------------------- 1 | #import "AnyPromise.h" 2 | @import Dispatch; 3 | @import Foundation.NSDate; 4 | @import Foundation.NSValue; 5 | 6 | /// @return A promise that fulfills after the specified duration. 7 | AnyPromise *PMKAfter(NSTimeInterval duration) { 8 | return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { 9 | dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(duration * NSEC_PER_SEC)); 10 | dispatch_after(time, dispatch_get_global_queue(QOS_CLASS_UNSPECIFIED, 0), ^{ 11 | resolve(@(duration)); 12 | }); 13 | }]; 14 | } 15 | -------------------------------------------------------------------------------- /Sources/after.swift: -------------------------------------------------------------------------------- 1 | import struct Foundation.TimeInterval 2 | import Dispatch 3 | 4 | /** 5 | after(seconds: 1.5).then { 6 | //… 7 | } 8 | 9 | - Returns: A guarantee that resolves after the specified duration. 10 | */ 11 | public func after(seconds: TimeInterval) -> Guarantee<Void> { 12 | let (rg, seal) = Guarantee<Void>.pending() 13 | let when = DispatchTime.now() + seconds 14 | #if swift(>=4.0) 15 | q.asyncAfter(deadline: when) { seal(()) } 16 | #else 17 | q.asyncAfter(deadline: when, execute: seal) 18 | #endif 19 | return rg 20 | } 21 | 22 | /** 23 | after(.seconds(2)).then { 24 | //… 25 | } 26 | 27 | - Returns: A guarantee that resolves after the specified duration. 28 | */ 29 | public func after(_ interval: DispatchTimeInterval) -> Guarantee<Void> { 30 | let (rg, seal) = Guarantee<Void>.pending() 31 | let when = DispatchTime.now() + interval 32 | #if swift(>=4.0) 33 | q.asyncAfter(deadline: when) { seal(()) } 34 | #else 35 | q.asyncAfter(deadline: when, execute: seal) 36 | #endif 37 | return rg 38 | } 39 | 40 | private var q: DispatchQueue { 41 | if #available(macOS 10.10, iOS 8.0, tvOS 9.0, watchOS 2.0, *) { 42 | return DispatchQueue.global(qos: .default) 43 | } else { 44 | return DispatchQueue.global(priority: .default) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/dispatch_promise.m: -------------------------------------------------------------------------------- 1 | #import "AnyPromise.h" 2 | @import Dispatch; 3 | 4 | AnyPromise *dispatch_promise_on(dispatch_queue_t queue, id block) { 5 | return [AnyPromise promiseWithValue:nil].thenOn(queue, block); 6 | } 7 | 8 | AnyPromise *dispatch_promise(id block) { 9 | return dispatch_promise_on(dispatch_get_global_queue(QOS_CLASS_UNSPECIFIED, 0), block); 10 | } 11 | -------------------------------------------------------------------------------- /Sources/firstly.swift: -------------------------------------------------------------------------------- 1 | import Dispatch 2 | 3 | /** 4 | Judicious use of `firstly` *may* make chains more readable. 5 | 6 | Compare: 7 | 8 | URLSession.shared.dataTask(url: url1).then { 9 | URLSession.shared.dataTask(url: url2) 10 | }.then { 11 | URLSession.shared.dataTask(url: url3) 12 | } 13 | 14 | With: 15 | 16 | firstly { 17 | URLSession.shared.dataTask(url: url1) 18 | }.then { 19 | URLSession.shared.dataTask(url: url2) 20 | }.then { 21 | URLSession.shared.dataTask(url: url3) 22 | } 23 | 24 | - Note: the block you pass executes immediately on the current thread/queue. 25 | */ 26 | public func firstly<U: Thenable>(execute body: () throws -> U) -> Promise<U.T> { 27 | do { 28 | let rp = Promise<U.T>(.pending) 29 | try body().pipe(to: rp.box.seal) 30 | return rp 31 | } catch { 32 | return Promise(error: error) 33 | } 34 | } 35 | 36 | /// - See: firstly() 37 | public func firstly<T>(execute body: () -> Guarantee<T>) -> Guarantee<T> { 38 | return body() 39 | } 40 | -------------------------------------------------------------------------------- /Sources/hang.m: -------------------------------------------------------------------------------- 1 | #import "AnyPromise.h" 2 | #import "AnyPromise+Private.h" 3 | @import CoreFoundation.CFRunLoop; 4 | 5 | /** 6 | Suspends the active thread waiting on the provided promise. 7 | 8 | @return The value of the provided promise once resolved. 9 | */ 10 | id PMKHang(AnyPromise *promise) { 11 | if (promise.pending) { 12 | static CFRunLoopSourceContext context; 13 | 14 | CFRunLoopRef runLoop = CFRunLoopGetCurrent(); 15 | CFRunLoopSourceRef runLoopSource = CFRunLoopSourceCreate(NULL, 0, &context); 16 | CFRunLoopAddSource(runLoop, runLoopSource, kCFRunLoopDefaultMode); 17 | 18 | promise.ensure(^{ 19 | CFRunLoopStop(runLoop); 20 | }); 21 | while (promise.pending) { 22 | CFRunLoopRun(); 23 | } 24 | CFRunLoopRemoveSource(runLoop, runLoopSource, kCFRunLoopDefaultMode); 25 | CFRelease(runLoopSource); 26 | } 27 | 28 | return promise.value; 29 | } 30 | -------------------------------------------------------------------------------- /Sources/hang.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CoreFoundation 3 | 4 | /** 5 | Runs the active run-loop until the provided promise resolves. 6 | 7 | This is for debug and is not a generally safe function to use in your applications. We mostly provide it for use in testing environments. 8 | 9 | Still if you like, study how it works (by reading the sources!) and use at your own risk. 10 | 11 | - Returns: The value of the resolved promise 12 | - Throws: An error, should the promise be rejected 13 | - See: `wait()` 14 | */ 15 | public func hang<T>(_ promise: Promise<T>) throws -> T { 16 | #if os(Linux) || os(Android) 17 | #if swift(>=4) 18 | let runLoopMode: CFRunLoopMode = kCFRunLoopDefaultMode 19 | #else 20 | // isMainThread is not yet implemented on Linux. 21 | let runLoopModeRaw = RunLoopMode.defaultRunLoopMode.rawValue._bridgeToObjectiveC() 22 | let runLoopMode: CFString = unsafeBitCast(runLoopModeRaw, to: CFString.self) 23 | #endif 24 | #else 25 | guard Thread.isMainThread else { 26 | // hang doesn't make sense on threads that aren't the main thread. 27 | // use `.wait()` on those threads. 28 | fatalError("Only call hang() on the main thread.") 29 | } 30 | let runLoopMode: CFRunLoopMode = CFRunLoopMode.defaultMode 31 | #endif 32 | 33 | if promise.isPending { 34 | var context = CFRunLoopSourceContext() 35 | let runLoop = CFRunLoopGetCurrent() 36 | let runLoopSource = CFRunLoopSourceCreate(nil, 0, &context) 37 | CFRunLoopAddSource(runLoop, runLoopSource, runLoopMode) 38 | 39 | _ = promise.ensure { 40 | CFRunLoopStop(runLoop) 41 | } 42 | 43 | while promise.isPending { 44 | CFRunLoopRun() 45 | } 46 | CFRunLoopRemoveSource(runLoop, runLoopSource, runLoopMode) 47 | } 48 | 49 | switch promise.result! { 50 | case .rejected(let error): 51 | throw error 52 | case .fulfilled(let value): 53 | return value 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/join.m: -------------------------------------------------------------------------------- 1 | @import Foundation.NSDictionary; 2 | #import "AnyPromise+Private.h" 3 | #import <libkern/OSAtomic.h> 4 | @import Foundation.NSError; 5 | @import Foundation.NSNull; 6 | #import "PromiseKit.h" 7 | #import <stdatomic.h> 8 | 9 | /** 10 | Waits on all provided promises. 11 | 12 | `PMKWhen` rejects as soon as one of the provided promises rejects. `PMKJoin` waits on all provided promises, then rejects if any of those promises rejects, otherwise it fulfills with values from the provided promises. 13 | 14 | - Returns: A new promise that resolves once all the provided promises resolve. 15 | */ 16 | AnyPromise *PMKJoin(NSArray *promises) { 17 | if (promises == nil) 18 | return [AnyPromise promiseWithValue:[NSError errorWithDomain:PMKErrorDomain code:PMKInvalidUsageError userInfo:@{NSLocalizedDescriptionKey: @"PMKJoin(nil)"}]]; 19 | 20 | if (promises.count == 0) 21 | return [AnyPromise promiseWithValue:promises]; 22 | 23 | return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { 24 | NSPointerArray *results = NSPointerArrayMake(promises.count); 25 | __block atomic_int countdown = promises.count; 26 | __block BOOL rejected = NO; 27 | 28 | [promises enumerateObjectsUsingBlock:^(AnyPromise *promise, NSUInteger ii, BOOL *stop) { 29 | [promise __pipe:^(id value) { 30 | 31 | if (IsError(value)) { 32 | rejected = YES; 33 | } 34 | 35 | //FIXME surely this isn't thread safe on multiple cores? 36 | [results replacePointerAtIndex:ii withPointer:(__bridge void *)(value ?: [NSNull null])]; 37 | 38 | atomic_fetch_sub_explicit(&countdown, 1, memory_order_relaxed); 39 | 40 | if (countdown == 0) { 41 | if (!rejected) { 42 | resolve(results.allObjects); 43 | } else { 44 | id userInfo = @{PMKJoinPromisesKey: promises}; 45 | id err = [NSError errorWithDomain:PMKErrorDomain code:PMKJoinError userInfo:userInfo]; 46 | resolve(err); 47 | } 48 | } 49 | }]; 50 | 51 | (void) stop; 52 | }]; 53 | }]; 54 | } 55 | -------------------------------------------------------------------------------- /Sources/race.m: -------------------------------------------------------------------------------- 1 | #import "AnyPromise+Private.h" 2 | #import <libkern/OSAtomic.h> 3 | 4 | #pragma clang diagnostic push 5 | #pragma clang diagnostic ignored "-Wdeprecated-declarations" 6 | // ^^ OSAtomicDecrement32 is deprecated on watchOS 7 | 8 | AnyPromise *PMKRace(NSArray *promises) { 9 | if (promises == nil || promises.count == 0) 10 | return [AnyPromise promiseWithValue:[NSError errorWithDomain:PMKErrorDomain code:PMKInvalidUsageError userInfo:@{NSLocalizedDescriptionKey: @"PMKRace(nil)"}]]; 11 | 12 | return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { 13 | for (AnyPromise *promise in promises) { 14 | [promise __pipe:resolve]; 15 | } 16 | }]; 17 | } 18 | 19 | /** 20 | Waits for one promise to fulfill 21 | 22 | @note If there are no fulfilled promises, the returned promise is rejected with `PMKNoWinnerError`. 23 | @param promises The promises to fulfill. 24 | @return The promise that was fulfilled first. 25 | */ 26 | AnyPromise *PMKRaceFulfilled(NSArray *promises) { 27 | if (promises == nil || promises.count == 0) 28 | return [AnyPromise promiseWithValue:[NSError errorWithDomain:PMKErrorDomain code:PMKInvalidUsageError userInfo:@{NSLocalizedDescriptionKey: @"PMKRaceFulfilled(nil)"}]]; 29 | 30 | __block int32_t countdown = (int32_t)[promises count]; 31 | 32 | return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { 33 | for (__strong AnyPromise* promise in promises) { 34 | [promise __pipe:^(id value) { 35 | if (IsError(value)) { 36 | if (OSAtomicDecrement32(&countdown) == 0) { 37 | id err = [NSError errorWithDomain:PMKErrorDomain code:PMKNoWinnerError userInfo:@{NSLocalizedDescriptionKey: @"PMKRaceFulfilled(nil)"}]; 38 | resolve(err); 39 | } 40 | } else { 41 | resolve(value); 42 | } 43 | }]; 44 | } 45 | }]; 46 | } 47 | 48 | #pragma GCC diagnostic pop 49 | -------------------------------------------------------------------------------- /Sources/race.swift: -------------------------------------------------------------------------------- 1 | import Dispatch 2 | 3 | @inline(__always) 4 | private func _race<U: Thenable>(_ thenables: [U]) -> Promise<U.T> { 5 | let rp = Promise<U.T>(.pending) 6 | for thenable in thenables { 7 | thenable.pipe(to: rp.box.seal) 8 | } 9 | return rp 10 | } 11 | 12 | /** 13 | Waits for one promise to resolve 14 | 15 | race(promise1, promise2, promise3).then { winner in 16 | //… 17 | } 18 | 19 | - Returns: The promise that resolves first 20 | - Warning: If the first resolution is a rejection, the returned promise is rejected 21 | */ 22 | public func race<U: Thenable>(_ thenables: U...) -> Promise<U.T> { 23 | return _race(thenables) 24 | } 25 | 26 | /** 27 | Waits for one promise to resolve 28 | 29 | race(promise1, promise2, promise3).then { winner in 30 | //… 31 | } 32 | 33 | - Returns: The promise that resolves first 34 | - Warning: If the first resolution is a rejection, the returned promise is rejected 35 | - Remark: If the provided array is empty the returned promise is rejected with PMKError.badInput 36 | */ 37 | public func race<U: Thenable>(_ thenables: [U]) -> Promise<U.T> { 38 | guard !thenables.isEmpty else { 39 | return Promise(error: PMKError.badInput) 40 | } 41 | return _race(thenables) 42 | } 43 | 44 | /** 45 | Waits for one guarantee to resolve 46 | 47 | race(promise1, promise2, promise3).then { winner in 48 | //… 49 | } 50 | 51 | - Returns: The guarantee that resolves first 52 | */ 53 | public func race<T>(_ guarantees: Guarantee<T>...) -> Guarantee<T> { 54 | let rg = Guarantee<T>(.pending) 55 | for guarantee in guarantees { 56 | guarantee.pipe(to: rg.box.seal) 57 | } 58 | return rg 59 | } 60 | 61 | /** 62 | Waits for one promise to fulfill 63 | 64 | race(fulfilled: [promise1, promise2, promise3]).then { winner in 65 | //… 66 | } 67 | 68 | - Returns: The promise that was fulfilled first. 69 | - Warning: Skips all rejected promises. 70 | - Remark: If the provided array is empty, the returned promise is rejected with `PMKError.badInput`. If there are no fulfilled promises, the returned promise is rejected with `PMKError.noWinner`. 71 | */ 72 | public func race<U: Thenable>(fulfilled thenables: [U]) -> Promise<U.T> { 73 | var countdown = thenables.count 74 | guard countdown > 0 else { 75 | return Promise(error: PMKError.badInput) 76 | } 77 | 78 | let rp = Promise<U.T>(.pending) 79 | 80 | let barrier = DispatchQueue(label: "org.promisekit.barrier.race", attributes: .concurrent) 81 | 82 | for promise in thenables { 83 | promise.pipe { result in 84 | barrier.sync(flags: .barrier) { 85 | switch result { 86 | case .rejected: 87 | guard rp.isPending else { return } 88 | countdown -= 1 89 | if countdown == 0 { 90 | rp.box.seal(.rejected(PMKError.noWinner)) 91 | } 92 | case .fulfilled(let value): 93 | guard rp.isPending else { return } 94 | countdown = 0 95 | rp.box.seal(.fulfilled(value)) 96 | } 97 | } 98 | } 99 | } 100 | 101 | return rp 102 | } 103 | -------------------------------------------------------------------------------- /Sources/when.m: -------------------------------------------------------------------------------- 1 | @import Foundation.NSDictionary; 2 | #import "AnyPromise+Private.h" 3 | @import Foundation.NSProgress; 4 | #import <libkern/OSAtomic.h> 5 | @import Foundation.NSError; 6 | @import Foundation.NSNull; 7 | #import "PromiseKit.h" 8 | 9 | #pragma clang diagnostic push 10 | #pragma clang diagnostic ignored "-Wdeprecated-declarations" 11 | // ^^ OSAtomicDecrement32 is deprecated on watchOS 12 | 13 | 14 | // NSProgress resources: 15 | // * https://robots.thoughtbot.com/asynchronous-nsprogress 16 | // * http://oleb.net/blog/2014/03/nsprogress/ 17 | // NSProgress! Beware! 18 | // * https://github.com/AFNetworking/AFNetworking/issues/2261 19 | 20 | /** 21 | Wait for all promises in a set to resolve. 22 | 23 | @note If *any* of the provided promises reject, the returned promise is immediately rejected with that error. 24 | @warning In the event of rejection the other promises will continue to resolve and, as per any other promise, will either fulfill or reject. This is the right pattern for `getter` style asynchronous tasks, but often for `setter` tasks (eg. storing data on a server), you most likely will need to wait on all tasks and then act based on which have succeeded and which have failed, in such situations use `when(resolved:)`. 25 | @param promises The promises upon which to wait before the returned promise resolves. 26 | @note PMKWhen provides NSProgress. 27 | @return A new promise that resolves when all the provided promises fulfill or one of the provided promises rejects. 28 | */ 29 | AnyPromise *PMKWhen(id promises) { 30 | if (promises == nil) 31 | return [AnyPromise promiseWithValue:[NSError errorWithDomain:PMKErrorDomain code:PMKInvalidUsageError userInfo:@{NSLocalizedDescriptionKey: @"PMKWhen(nil)"}]]; 32 | 33 | if ([promises isKindOfClass:[NSArray class]] || [promises isKindOfClass:[NSDictionary class]]) { 34 | if ([promises count] == 0) 35 | return [AnyPromise promiseWithValue:promises]; 36 | } else if ([promises isKindOfClass:[AnyPromise class]]) { 37 | promises = @[promises]; 38 | } else { 39 | return [AnyPromise promiseWithValue:promises]; 40 | } 41 | 42 | #ifndef PMKDisableProgress 43 | NSProgress *progress = [NSProgress progressWithTotalUnitCount:(int64_t)[promises count]]; 44 | progress.pausable = NO; 45 | progress.cancellable = NO; 46 | #else 47 | struct PMKProgress { 48 | int completedUnitCount; 49 | int totalUnitCount; 50 | double fractionCompleted; 51 | }; 52 | __block struct PMKProgress progress; 53 | #endif 54 | 55 | __block int32_t countdown = (int32_t)[promises count]; 56 | BOOL const isdict = [promises isKindOfClass:[NSDictionary class]]; 57 | 58 | return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { 59 | NSInteger index = 0; 60 | 61 | for (__strong id key in promises) { 62 | AnyPromise *promise = isdict ? promises[key] : key; 63 | if (!isdict) key = @(index); 64 | 65 | if (![promise isKindOfClass:[AnyPromise class]]) 66 | promise = [AnyPromise promiseWithValue:promise]; 67 | 68 | [promise __pipe:^(id value){ 69 | if (progress.fractionCompleted >= 1) 70 | return; 71 | 72 | if (IsError(value)) { 73 | progress.completedUnitCount = progress.totalUnitCount; 74 | 75 | NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:[(NSError *)value userInfo] ?: @{}]; 76 | userInfo[PMKFailingPromiseIndexKey] = key; 77 | [userInfo setObject:value forKey:NSUnderlyingErrorKey]; 78 | id err = [[NSError alloc] initWithDomain:[value domain] code:[value code] userInfo:userInfo]; 79 | resolve(err); 80 | } 81 | else if (OSAtomicDecrement32(&countdown) == 0) { 82 | progress.completedUnitCount = progress.totalUnitCount; 83 | 84 | id results; 85 | if (isdict) { 86 | results = [NSMutableDictionary new]; 87 | for (id key in promises) { 88 | id promise = promises[key]; 89 | results[key] = IsPromise(promise) ? ((AnyPromise *)promise).value : promise; 90 | } 91 | } else { 92 | results = [NSMutableArray new]; 93 | for (AnyPromise *promise in promises) { 94 | id value = IsPromise(promise) ? (promise.value ?: [NSNull null]) : promise; 95 | [results addObject:value]; 96 | } 97 | } 98 | resolve(results); 99 | } else { 100 | progress.completedUnitCount++; 101 | } 102 | }]; 103 | } 104 | }]; 105 | } 106 | 107 | #pragma GCC diagnostic pop 108 | -------------------------------------------------------------------------------- /Tests/A+/2.1.2.swift: -------------------------------------------------------------------------------- 1 | import PromiseKit 2 | import XCTest 3 | 4 | class Test212: XCTestCase { 5 | func test() { 6 | describe("2.1.2.1: When fulfilled, a promise: must not transition to any other state.") { 7 | testFulfilled { promise, expectation, _ in 8 | promise.test(onFulfilled: expectation.fulfill, onRejected: { XCTFail() }) 9 | } 10 | 11 | specify("trying to fulfill then immediately reject") { d, expectation in 12 | d.promise.test(onFulfilled: expectation.fulfill, onRejected: { XCTFail() }) 13 | d.fulfill() 14 | d.reject(Error.dummy) 15 | } 16 | 17 | specify("trying to fulfill then reject, delayed") { d, expectation in 18 | d.promise.test(onFulfilled: expectation.fulfill, onRejected: { XCTFail() }) 19 | after(ticks: 1) { 20 | d.fulfill() 21 | d.reject(Error.dummy) 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/A+/2.1.3.swift: -------------------------------------------------------------------------------- 1 | import PromiseKit 2 | import XCTest 3 | 4 | class Test213: XCTestCase { 5 | func test() { 6 | describe("2.1.3.1: When rejected, a promise: must not transition to any other state.") { 7 | testRejected { promise, expectation, _ in 8 | promise.test(onFulfilled: { XCTFail() }, onRejected: expectation.fulfill) 9 | } 10 | 11 | specify("trying to reject then immediately fulfill") { d, expectation in 12 | d.promise.test(onFulfilled: { XCTFail() }, onRejected: expectation.fulfill) 13 | d.reject(Error.dummy) 14 | d.fulfill() 15 | } 16 | 17 | specify("trying to reject then fulfill, delayed") { d, expectation in 18 | d.promise.test(onFulfilled: { XCTFail() }, onRejected: expectation.fulfill) 19 | after(ticks: 1) { 20 | d.reject(Error.dummy) 21 | d.fulfill() 22 | } 23 | } 24 | 25 | specify("trying to reject immediately then fulfill delayed") { d, expectation in 26 | d.promise.test(onFulfilled: { XCTFail() }, onRejected: expectation.fulfill) 27 | d.reject(Error.dummy) 28 | after(ticks: 1) { 29 | d.fulfill() 30 | } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/A+/2.2.2.swift: -------------------------------------------------------------------------------- 1 | import PromiseKit 2 | import XCTest 3 | 4 | class Test222: XCTestCase { 5 | func test() { 6 | describe("2.2.2: If `onFulfilled` is a function,") { 7 | describe("2.2.2.1: it must be called after `promise` is fulfilled, with `promise`’s fulfillment value as its first argument.") { 8 | testFulfilled { promise, expectation, sentinel in 9 | promise.done { 10 | XCTAssertEqual(sentinel, $0) 11 | expectation.fulfill() 12 | }.silenceWarning() 13 | } 14 | } 15 | 16 | describe("2.2.2.2: it must not be called before `promise` is fulfilled") { 17 | specify("fulfilled after a delay") { d, expectation in 18 | var called = false 19 | d.promise.done { 20 | called = true 21 | expectation.fulfill() 22 | }.silenceWarning() 23 | after(ticks: 5) { 24 | XCTAssertFalse(called) 25 | d.fulfill() 26 | } 27 | } 28 | specify("never fulfilled") { d, expectation in 29 | d.promise.done{ XCTFail() }.silenceWarning() 30 | after(ticks: 1000, execute: expectation.fulfill) 31 | } 32 | } 33 | 34 | describe("2.2.2.3: it must not be called more than once.") { 35 | specify("already-fulfilled") { _, expectation in 36 | let ex = (expectation, mkex()) 37 | Promise().done { 38 | ex.0.fulfill() 39 | }.silenceWarning() 40 | after(ticks: 1000) { 41 | ex.1.fulfill() 42 | } 43 | } 44 | specify("trying to fulfill a pending promise more than once, immediately") { d, expectation in 45 | d.promise.done(expectation.fulfill).silenceWarning() 46 | d.fulfill() 47 | d.fulfill() 48 | } 49 | specify("trying to fulfill a pending promise more than once, delayed") { d, expectation in 50 | d.promise.done(expectation.fulfill).silenceWarning() 51 | after(ticks: 5) { 52 | d.fulfill() 53 | d.fulfill() 54 | } 55 | } 56 | specify("trying to fulfill a pending promise more than once, immediately then delayed") { d, expectation in 57 | let ex = (expectation, mkex()) 58 | d.promise.done(ex.0.fulfill).silenceWarning() 59 | d.fulfill() 60 | after(ticks: 5) { 61 | d.fulfill() 62 | } 63 | after(ticks: 10, execute: ex.1.fulfill) 64 | } 65 | specify("when multiple `then` calls are made, spaced apart in time") { d, expectation in 66 | let ex = (expectation, self.expectation(description: ""), self.expectation(description: ""), self.expectation(description: "")) 67 | 68 | do { 69 | d.promise.done(ex.0.fulfill).silenceWarning() 70 | } 71 | after(ticks: 5) { 72 | d.promise.done(ex.1.fulfill).silenceWarning() 73 | } 74 | after(ticks: 10) { 75 | d.promise.done(ex.2.fulfill).silenceWarning() 76 | } 77 | after(ticks: 15) { 78 | d.fulfill() 79 | ex.3.fulfill() 80 | } 81 | } 82 | specify("when `then` is interleaved with fulfillment") { d, expectation in 83 | let ex = (expectation, self.expectation(description: ""), self) 84 | 85 | d.promise.done(ex.0.fulfill).silenceWarning() 86 | d.fulfill() 87 | d.promise.done(ex.1.fulfill).silenceWarning() 88 | } 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Tests/A+/2.2.3.swift: -------------------------------------------------------------------------------- 1 | import PromiseKit 2 | import XCTest 3 | 4 | class Test223: XCTestCase { 5 | func test() { 6 | describe("2.2.3: If `onRejected` is a function,") { 7 | describe("2.2.3.1: it must be called after `promise` is rejected, with `promise`’s rejection reason as its first argument.") { 8 | testRejected { promise, expectation, sentinel in 9 | promise.catch { error in 10 | if case Error.sentinel(let value) = error { 11 | XCTAssertEqual(value, sentinel) 12 | } else { 13 | XCTFail() 14 | } 15 | expectation.fulfill() 16 | } 17 | } 18 | } 19 | describe("2.2.3.2: it must not be called before `promise` is rejected") { 20 | specify("rejected after a delay") { d, expectation in 21 | var called = false 22 | d.promise.catch { _ in 23 | called = true 24 | expectation.fulfill() 25 | } 26 | after(ticks: 1) { 27 | XCTAssertFalse(called) 28 | d.reject(Error.dummy) 29 | } 30 | } 31 | specify("never rejected") { d, expectation in 32 | d.promise.catch { _ in XCTFail() } 33 | after(ticks: 1, execute: expectation.fulfill) 34 | } 35 | } 36 | describe("2.2.3.3: it must not be called more than once.") { 37 | specify("already-rejected") { d, expectation in 38 | var timesCalled = 0 39 | Promise<Int>(error: Error.dummy).catch { _ in 40 | XCTAssertEqual(++timesCalled, 1) 41 | } 42 | after(ticks: 2) { 43 | XCTAssertEqual(timesCalled, 1) 44 | expectation.fulfill() 45 | } 46 | } 47 | specify("trying to reject a pending promise more than once, immediately") { d, expectation in 48 | d.promise.catch{_ in expectation.fulfill() } 49 | d.reject(Error.dummy) 50 | d.reject(Error.dummy) 51 | } 52 | specify("trying to reject a pending promise more than once, delayed") { d, expectation in 53 | d.promise.catch{_ in expectation.fulfill() } 54 | after(ticks: 1) { 55 | d.reject(Error.dummy) 56 | d.reject(Error.dummy) 57 | } 58 | } 59 | specify("trying to reject a pending promise more than once, immediately then delayed") { d, expectation in 60 | d.promise.catch{_ in expectation.fulfill() } 61 | d.reject(Error.dummy) 62 | after(ticks: 1) { 63 | d.reject(Error.dummy) 64 | } 65 | } 66 | specify("when multiple `then` calls are made, spaced apart in time") { d, expectation in 67 | let mk = { self.expectation(description: "") } 68 | let ex = (expectation, mk(), mk(), mk()) 69 | 70 | do { 71 | d.promise.catch{ _ in ex.0.fulfill() } 72 | } 73 | after(ticks: 1) { 74 | d.promise.catch{ _ in ex.1.fulfill() } 75 | } 76 | after(ticks: 2) { 77 | d.promise.catch{ _ in ex.2.fulfill() } 78 | } 79 | after(ticks: 3) { 80 | d.reject(Error.dummy) 81 | ex.3.fulfill() 82 | } 83 | } 84 | specify("when `then` is interleaved with rejection") { d, expectation in 85 | let ex = (expectation, self.expectation(description: "")) 86 | d.promise.catch{ _ in ex.0.fulfill() } 87 | d.reject(Error.dummy) 88 | d.promise.catch{ _ in ex.1.fulfill() } 89 | } 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Tests/A+/2.2.7.swift: -------------------------------------------------------------------------------- 1 | import PromiseKit 2 | import XCTest 3 | 4 | class Test227: XCTestCase { 5 | func test() { 6 | describe("2.2.7: `then` must return a promise: `promise2 = promise1.then(onFulfilled, onRejected)") { 7 | describe("2.2.7.2: If either `onFulfilled` or `onRejected` throws an exception `e`, `promise2` must be rejected with `e` as the reason.") { 8 | 9 | testFulfilled { promise1, expectation, _ in 10 | let sentinel = arc4random() 11 | let promise2 = promise1.done { _ in throw Error.sentinel(sentinel) } 12 | 13 | promise2.catch { 14 | if case Error.sentinel(let x) = $0, x == sentinel { 15 | expectation.fulfill() 16 | } 17 | } 18 | } 19 | 20 | testRejected { promise1, expectation, _ in 21 | let sentinel = arc4random() 22 | let promise2 = promise1.recover { _ -> Promise<UInt32> in throw Error.sentinel(sentinel) } 23 | 24 | promise2.catch { error in 25 | if case Error.sentinel(let x) = error, x == sentinel { 26 | expectation.fulfill() 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Tests/A+/2.3.1.swift: -------------------------------------------------------------------------------- 1 | import PromiseKit 2 | import XCTest 3 | 4 | class Test231: XCTestCase { 5 | func test() { 6 | describe("2.3.1: If `promise` and `x` refer to the same object, reject `promise` with a `TypeError' as the reason.") { 7 | specify("via return from a fulfilled promise") { d, expectation in 8 | var promise: Promise<Void>! 9 | promise = Promise().then { () -> Promise<Void> in 10 | return promise 11 | } 12 | promise.catch { err in 13 | if case PMKError.returnedSelf = err { 14 | expectation.fulfill() 15 | } 16 | } 17 | } 18 | specify("via return from a rejected promise") { d, expectation in 19 | var promise: Promise<Void>! 20 | promise = Promise<Void>(error: Error.dummy).recover { _ -> Promise<Void> in 21 | return promise 22 | } 23 | promise.catch { err in 24 | if case PMKError.returnedSelf = err { 25 | expectation.fulfill() 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Tests/A+/2.3.2.swift: -------------------------------------------------------------------------------- 1 | import PromiseKit 2 | import XCTest 3 | 4 | class Test232: XCTestCase { 5 | func test() { 6 | describe("2.3.2: If `x` is a promise, adopt its state") { 7 | describe("2.3.2.1: If `x` is pending, `promise` must remain pending until `x` is fulfilled or rejected.") { 8 | 9 | func xFactory() -> Promise<UInt32> { 10 | return Promise.pending().promise 11 | } 12 | 13 | testPromiseResolution(factory: xFactory) { promise, expectation in 14 | var wasFulfilled = false; 15 | var wasRejected = false; 16 | 17 | promise.test(onFulfilled: { wasFulfilled = true }, onRejected: { wasRejected = true }) 18 | 19 | after(ticks: 4) { 20 | XCTAssertFalse(wasFulfilled) 21 | XCTAssertFalse(wasRejected) 22 | expectation.fulfill() 23 | } 24 | } 25 | } 26 | 27 | describe("2.3.2.2: If/when `x` is fulfilled, fulfill `promise` with the same value.") { 28 | describe("`x` is already-fulfilled") { 29 | let sentinel = arc4random() 30 | 31 | func xFactory() -> Promise<UInt32> { 32 | return .value(sentinel) 33 | } 34 | 35 | testPromiseResolution(factory: xFactory) { promise, expectation in 36 | promise.done { 37 | XCTAssertEqual($0, sentinel) 38 | expectation.fulfill() 39 | }.silenceWarning() 40 | } 41 | } 42 | describe("`x` is eventually-fulfilled") { 43 | let sentinel = arc4random() 44 | 45 | func xFactory() -> Promise<UInt32> { 46 | return Promise { seal in 47 | after(ticks: 2) { 48 | seal.fulfill(sentinel) 49 | } 50 | } 51 | } 52 | 53 | testPromiseResolution(factory: xFactory) { promise, expectation in 54 | promise.done { 55 | XCTAssertEqual($0, sentinel) 56 | expectation.fulfill() 57 | }.silenceWarning() 58 | } 59 | } 60 | } 61 | 62 | describe("2.3.2.3: If/when `x` is rejected, reject `promise` with the same reason.") { 63 | describe("`x` is already-rejected") { 64 | let sentinel = arc4random() 65 | 66 | func xFactory() -> Promise<UInt32> { 67 | return Promise(error: Error.sentinel(sentinel)) 68 | } 69 | 70 | testPromiseResolution(factory: xFactory) { promise, expectation in 71 | promise.catch { err in 72 | if case Error.sentinel(let value) = err, value == sentinel { 73 | expectation.fulfill() 74 | } 75 | } 76 | } 77 | } 78 | describe("`x` is eventually-rejected") { 79 | let sentinel = arc4random() 80 | 81 | func xFactory() -> Promise<UInt32> { 82 | return Promise { seal in 83 | after(ticks: 2) { 84 | seal.reject(Error.sentinel(sentinel)) 85 | } 86 | } 87 | } 88 | 89 | testPromiseResolution(factory: xFactory) { promise, expectation in 90 | promise.catch { err in 91 | if case Error.sentinel(let value) = err, value == sentinel { 92 | expectation.fulfill() 93 | } 94 | } 95 | } 96 | } 97 | } 98 | } 99 | } 100 | } 101 | 102 | 103 | ///////////////////////////////////////////////////////////////////////// 104 | 105 | extension Test232 { 106 | fileprivate func testPromiseResolution(factory: @escaping () -> Promise<UInt32>, line: UInt = #line, test: (Promise<UInt32>, XCTestExpectation) -> Void) { 107 | specify("via return from a fulfilled promise", file: #file, line: line) { d, expectation in 108 | let promise = Promise.value(arc4random()).then { _ in factory() } 109 | test(promise, expectation) 110 | } 111 | specify("via return from a rejected promise", file: #file, line: line) { d, expectation in 112 | let promise: Promise<UInt32> = Promise(error: Error.dummy).recover { _ in factory() } 113 | test(promise, expectation) 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Tests/A+/2.3.4.swift: -------------------------------------------------------------------------------- 1 | import PromiseKit 2 | import XCTest 3 | 4 | 5 | class Test234: XCTestCase { 6 | func test() { 7 | describe("2.3.4: If `x` is not an object or function, fulfill `promise` with `x`") { 8 | testFulfilled { promise, exception, _ in 9 | promise.map { value -> UInt32 in 10 | return 1 11 | }.done { value in 12 | XCTAssertEqual(value, 1) 13 | exception.fulfill() 14 | }.silenceWarning() 15 | } 16 | testRejected { promise, expectation, _ in 17 | promise.recover { _ -> Promise<UInt32> in 18 | return .value(UInt32(1)) 19 | }.done { value in 20 | XCTAssertEqual(value, 1) 21 | expectation.fulfill() 22 | }.silenceWarning() 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/A+/README.md: -------------------------------------------------------------------------------- 1 | Resources 2 | ========= 3 | * https://github.com/promises-aplus/promises-tests 4 | 5 | 6 | Skipped 7 | ======= 8 | * 2.3.3: Otherwise, if x is an object or function. 9 | This spec is a NOOP for Swift: 10 | - We have decided not to interact with other Promises A+ implementations 11 | - functions cannot have properties 12 | * 2.3.3.4: If then is not a function, fulfill promise with x. 13 | - See: The 2.3.4 suite. 14 | -------------------------------------------------------------------------------- /Tests/A+/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(ObjectiveC) 2 | import XCTest 3 | 4 | #if os(Android) 5 | extension XCTestExpectation { 6 | func fulfill() { 7 | fulfill(#file, line: #line) 8 | } 9 | } 10 | #endif 11 | 12 | extension Test212 { 13 | // DO NOT MODIFY: This is autogenerated, use: 14 | // `swift test --generate-linuxmain` 15 | // to regenerate. 16 | static let __allTests__Test212 = [ 17 | ("test", test), 18 | ] 19 | } 20 | 21 | extension Test213 { 22 | // DO NOT MODIFY: This is autogenerated, use: 23 | // `swift test --generate-linuxmain` 24 | // to regenerate. 25 | static let __allTests__Test213 = [ 26 | ("test", test), 27 | ] 28 | } 29 | 30 | extension Test222 { 31 | // DO NOT MODIFY: This is autogenerated, use: 32 | // `swift test --generate-linuxmain` 33 | // to regenerate. 34 | static let __allTests__Test222 = [ 35 | ("test", test), 36 | ] 37 | } 38 | 39 | extension Test223 { 40 | // DO NOT MODIFY: This is autogenerated, use: 41 | // `swift test --generate-linuxmain` 42 | // to regenerate. 43 | static let __allTests__Test223 = [ 44 | ("test", test), 45 | ] 46 | } 47 | 48 | extension Test224 { 49 | // DO NOT MODIFY: This is autogenerated, use: 50 | // `swift test --generate-linuxmain` 51 | // to regenerate. 52 | static let __allTests__Test224 = [ 53 | ("test", test), 54 | ] 55 | } 56 | 57 | extension Test226 { 58 | // DO NOT MODIFY: This is autogenerated, use: 59 | // `swift test --generate-linuxmain` 60 | // to regenerate. 61 | static let __allTests__Test226 = [ 62 | ("test", test), 63 | ] 64 | } 65 | 66 | extension Test227 { 67 | // DO NOT MODIFY: This is autogenerated, use: 68 | // `swift test --generate-linuxmain` 69 | // to regenerate. 70 | static let __allTests__Test227 = [ 71 | ("test", test), 72 | ] 73 | } 74 | 75 | extension Test231 { 76 | // DO NOT MODIFY: This is autogenerated, use: 77 | // `swift test --generate-linuxmain` 78 | // to regenerate. 79 | static let __allTests__Test231 = [ 80 | ("test", test), 81 | ] 82 | } 83 | 84 | extension Test232 { 85 | // DO NOT MODIFY: This is autogenerated, use: 86 | // `swift test --generate-linuxmain` 87 | // to regenerate. 88 | static let __allTests__Test232 = [ 89 | ("test", test), 90 | ] 91 | } 92 | 93 | extension Test234 { 94 | // DO NOT MODIFY: This is autogenerated, use: 95 | // `swift test --generate-linuxmain` 96 | // to regenerate. 97 | static let __allTests__Test234 = [ 98 | ("test", test), 99 | ] 100 | } 101 | 102 | public func __allTests() -> [XCTestCaseEntry] { 103 | return [ 104 | testCase(Test212.__allTests__Test212), 105 | testCase(Test213.__allTests__Test213), 106 | testCase(Test222.__allTests__Test222), 107 | testCase(Test223.__allTests__Test223), 108 | testCase(Test224.__allTests__Test224), 109 | testCase(Test226.__allTests__Test226), 110 | testCase(Test227.__allTests__Test227), 111 | testCase(Test231.__allTests__Test231), 112 | testCase(Test232.__allTests__Test232), 113 | testCase(Test234.__allTests__Test234), 114 | ] 115 | } 116 | #endif 117 | -------------------------------------------------------------------------------- /Tests/Bridging/BridgingTests.m: -------------------------------------------------------------------------------- 1 | @import PromiseKit; 2 | @import XCTest; 3 | #import "Infrastructure.h" 4 | 5 | 6 | @interface BridgingTests: XCTestCase @end @implementation BridgingTests 7 | 8 | - (void)testChainAnyPromiseFromSwiftCode { 9 | XCTestExpectation *ex = [self expectationWithDescription:@""]; 10 | AnyPromise *promise = PMKAfter(0.02); 11 | for (int x = 0; x < 100; ++x) { 12 | promise = promise.then(^{ 13 | return [[[PromiseBridgeHelper alloc] init] bridge1]; 14 | }); 15 | } 16 | promise.then(^{ 17 | [ex fulfill]; 18 | }); 19 | [self waitForExpectationsWithTimeout:20 handler:nil]; 20 | } 21 | 22 | - (void)test626 { 23 | XCTestExpectation *ex = [self expectationWithDescription:@""]; 24 | 25 | testCase626().then(^{ 26 | XCTFail(); 27 | }).ensure(^{ 28 | [ex fulfill]; 29 | }); 30 | 31 | [self waitForExpectationsWithTimeout:20 handler:nil]; 32 | } 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /Tests/Bridging/Infrastructure.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | @class AnyPromise; 3 | 4 | AnyPromise *PMKDummyAnyPromise_YES(void); 5 | AnyPromise *PMKDummyAnyPromise_Manifold(void); 6 | AnyPromise *PMKDummyAnyPromise_Error(void); 7 | 8 | __attribute__((objc_runtime_name("PMKPromiseBridgeHelper"))) 9 | __attribute__((objc_subclassing_restricted)) 10 | @interface PromiseBridgeHelper: NSObject 11 | - (AnyPromise *)bridge1; 12 | @end 13 | 14 | AnyPromise *testCase626(void); 15 | -------------------------------------------------------------------------------- /Tests/Bridging/Infrastructure.m: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | @import PromiseKit; 3 | #import "Infrastructure.h" 4 | 5 | AnyPromise *PMKDummyAnyPromise_YES(void) { 6 | return [AnyPromise promiseWithValue:@YES]; 7 | } 8 | 9 | AnyPromise *PMKDummyAnyPromise_Manifold(void) { 10 | return [AnyPromise promiseWithValue:PMKManifold(@YES, @NO, @NO)]; 11 | } 12 | 13 | AnyPromise *PMKDummyAnyPromise_Error(void) { 14 | return [AnyPromise promiseWithValue:[NSError errorWithDomain:@"a" code:1 userInfo:nil]]; 15 | } 16 | 17 | @implementation PromiseBridgeHelper (objc) 18 | 19 | - (AnyPromise *)bridge2 { 20 | return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { 21 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 22 | resolve(@123); 23 | }); 24 | }]; 25 | } 26 | 27 | @end 28 | 29 | #import "PMKBridgeTests-Swift.h" 30 | 31 | AnyPromise *testCase626(void) { 32 | return PMKWhen(@[[TestPromise626 promise], [TestPromise626 promise]]).then(^(id value){ 33 | NSLog(@"Success: %@", value); 34 | }).catch(^(NSError *error) { 35 | NSLog(@"Error: %@", error); 36 | @throw error; 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /Tests/Bridging/Infrastructure.swift: -------------------------------------------------------------------------------- 1 | import PromiseKit 2 | 3 | // for BridgingTests.m 4 | @objc(PMKPromiseBridgeHelper) class PromiseBridgeHelper: NSObject { 5 | @objc func bridge1() -> AnyPromise { 6 | let p = after(.milliseconds(10)) 7 | return AnyPromise(p) 8 | } 9 | } 10 | 11 | enum MyError: Error { 12 | case PromiseError 13 | } 14 | 15 | @objc class TestPromise626: NSObject { 16 | 17 | @objc class func promise() -> AnyPromise { 18 | let promise: Promise<String> = Promise { seal in 19 | seal.reject(MyError.PromiseError) 20 | } 21 | 22 | return AnyPromise(promise) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Tests/CoreObjC/AnyPromiseTests.swift: -------------------------------------------------------------------------------- 1 | import PromiseKit 2 | import XCTest 3 | 4 | class AnyPromiseTests: XCTestCase { 5 | func testFulfilledResult() { 6 | switch AnyPromise(Promise.value(true)).result { 7 | case .fulfilled(let obj as Bool)? where obj: 8 | break 9 | default: 10 | XCTFail() 11 | } 12 | } 13 | 14 | func testRejectedResult() { 15 | switch AnyPromise(Promise<Int>(error: PMKError.badInput)).result { 16 | case .rejected(let err)?: 17 | print(err) 18 | break 19 | default: 20 | XCTFail() 21 | } 22 | } 23 | 24 | func testPendingResult() { 25 | switch AnyPromise(Promise<Int>.pending().promise).result { 26 | case nil: 27 | break 28 | default: 29 | XCTFail() 30 | } 31 | } 32 | 33 | func testCustomStringConvertible() { 34 | XCTAssertEqual("\(AnyPromise(Promise<Int>.pending().promise))", "AnyPromise(…)") 35 | XCTAssertEqual("\(AnyPromise(Promise.value(1)))", "AnyPromise(1)") 36 | XCTAssertEqual("\(AnyPromise(Promise<Int?>.value(nil)))", "AnyPromise(nil)") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Tests/CoreObjC/HangTests.m: -------------------------------------------------------------------------------- 1 | @import PromiseKit; 2 | @import XCTest; 3 | 4 | @interface HangTests: XCTestCase @end @implementation HangTests 5 | 6 | - (void)test { 7 | __block int x = 0; 8 | id value = PMKHang(PMKAfter(0.02).then(^{ x++; return 1; })); 9 | XCTAssertEqual(x, 1); 10 | XCTAssertEqualObjects(value, @1); 11 | } 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Tests/CoreObjC/JoinTests.m: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | @import PromiseKit; 3 | @import XCTest; 4 | 5 | 6 | @interface JoinTests: XCTestCase @end @implementation JoinTests 7 | 8 | - (void)test_73_join { 9 | XCTestExpectation *ex1 = [self expectationWithDescription:@""]; 10 | 11 | __block void (^fulfiller)(id) = nil; 12 | AnyPromise *promise = [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { 13 | fulfiller = resolve; 14 | }]; 15 | 16 | PMKJoin(@[ 17 | [AnyPromise promiseWithValue:[NSError errorWithDomain:@"dom" code:1 userInfo:nil]], 18 | promise, 19 | [AnyPromise promiseWithValue:[NSError errorWithDomain:@"dom" code:2 userInfo:nil]] 20 | ]).then(^{ 21 | XCTFail(); 22 | }).catch(^(NSError *error){ 23 | id promises = error.userInfo[PMKJoinPromisesKey]; 24 | 25 | int cume = 0, cumv = 0; 26 | 27 | for (AnyPromise *promise in promises) { 28 | if ([promise.value isKindOfClass:[NSError class]]) { 29 | cume |= [promise.value code]; 30 | } else { 31 | cumv |= [promise.value unsignedIntValue]; 32 | } 33 | } 34 | 35 | XCTAssertTrue(cumv == 4); 36 | XCTAssertTrue(cume == 3); 37 | 38 | [ex1 fulfill]; 39 | }); 40 | fulfiller(@4); 41 | [self waitForExpectationsWithTimeout:1 handler:nil]; 42 | } 43 | 44 | - (void)test_74_join_no_errors { 45 | XCTestExpectation *ex1 = [self expectationWithDescription:@""]; 46 | PMKJoin(@[ 47 | [AnyPromise promiseWithValue:@1], 48 | [AnyPromise promiseWithValue:@2] 49 | ]).then(^(NSArray *values, id errors) { 50 | XCTAssertEqualObjects(values, (@[@1, @2])); 51 | XCTAssertNil(errors); 52 | [ex1 fulfill]; 53 | }); 54 | [self waitForExpectationsWithTimeout:1 handler:nil]; 55 | } 56 | 57 | 58 | - (void)test_75_join_no_success { 59 | XCTestExpectation *ex1 = [self expectationWithDescription:@""]; 60 | PMKJoin(@[ 61 | [AnyPromise promiseWithValue:[NSError errorWithDomain:@"dom" code:1 userInfo:nil]], 62 | [AnyPromise promiseWithValue:[NSError errorWithDomain:@"dom" code:2 userInfo:nil]], 63 | ]).then(^{ 64 | XCTFail(); 65 | }).catch(^(NSError *error){ 66 | XCTAssertNotNil(error.userInfo[PMKJoinPromisesKey]); 67 | [ex1 fulfill]; 68 | }); 69 | [self waitForExpectationsWithTimeout:1 handler:nil]; 70 | } 71 | 72 | - (void)test_76_join_fulfills_if_empty_input { 73 | XCTestExpectation *ex1 = [self expectationWithDescription:@""]; 74 | PMKJoin(@[]).then(^(id a, id b, id c){ 75 | XCTAssertEqualObjects(@[], a); 76 | XCTAssertNil(b); 77 | XCTAssertNil(c); 78 | [ex1 fulfill]; 79 | }); 80 | [self waitForExpectationsWithTimeout:1 handler:nil]; 81 | } 82 | 83 | - (void)test_join_nil { 84 | NSArray *foo = nil; 85 | NSError *err = PMKJoin(foo).value; 86 | XCTAssertEqual(err.domain, PMKErrorDomain); 87 | XCTAssertEqual(err.code, PMKInvalidUsageError); 88 | } 89 | 90 | @end 91 | -------------------------------------------------------------------------------- /Tests/CoreObjC/PMKManifoldTests.m: -------------------------------------------------------------------------------- 1 | @import PromiseKit; 2 | @import XCTest; 3 | 4 | @interface PMKManifoldTests: XCTestCase @end @implementation PMKManifoldTests 5 | 6 | - (void)test_62_access_extra_elements { 7 | id ex1 = [self expectationWithDescription:@""]; 8 | 9 | [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { 10 | resolve(PMKManifold(@1)); 11 | }].then(^(id o, id m, id n){ 12 | XCTAssertNil(m, @"Accessing extra elements should not crash"); 13 | XCTAssertNil(n, @"Accessing extra elements should not crash"); 14 | XCTAssertEqualObjects(o, @1); 15 | [ex1 fulfill]; 16 | }); 17 | 18 | [self waitForExpectationsWithTimeout:1 handler:nil]; 19 | } 20 | 21 | - (void)test_63_then_manifold { 22 | id ex1 = [self expectationWithDescription:@""]; 23 | 24 | [AnyPromise promiseWithValue:@0].then(^{ 25 | return PMKManifold(@1, @2, @3); 26 | }).then(^(id o1, id o2, id o3){ 27 | XCTAssertEqualObjects(o1, @1); 28 | XCTAssertEqualObjects(o2, @2); 29 | XCTAssertEqualObjects(o3, @3); 30 | [ex1 fulfill]; 31 | }); 32 | 33 | [self waitForExpectationsWithTimeout:1 handler:nil]; 34 | } 35 | 36 | - (void)test_63_then_manifold_with_nil { 37 | id ex1 = [self expectationWithDescription:@""]; 38 | 39 | [AnyPromise promiseWithValue:@0].then(^{ 40 | return PMKManifold(@1, nil, @3); 41 | }).then(^(id o1, id o2, id o3){ 42 | XCTAssertEqualObjects(o1, @1); 43 | XCTAssertEqualObjects(o2, nil); 44 | XCTAssertEqualObjects(o3, @3); 45 | [ex1 fulfill]; 46 | }); 47 | 48 | [self waitForExpectationsWithTimeout:1 handler:nil]; 49 | } 50 | 51 | - (void)test_65_manifold_fulfill_value { 52 | id ex1 = [self expectationWithDescription:@""]; 53 | 54 | AnyPromise *promise = [AnyPromise promiseWithValue:@1].then(^{ 55 | return PMKManifold(@123, @2); 56 | }); 57 | 58 | promise.then(^(id a, id b){ 59 | XCTAssertNotNil(a); 60 | XCTAssertNotNil(b); 61 | [ex1 fulfill]; 62 | }); 63 | 64 | [self waitForExpectationsWithTimeout:1 handler:nil]; 65 | 66 | XCTAssertEqualObjects(promise.value, @123); 67 | } 68 | 69 | - (void)test_37_PMKMany_2 { 70 | id ex1 = [self expectationWithDescription:@""]; 71 | 72 | PMKAfter(0.02).then(^{ 73 | return PMKManifold(@1, @2); 74 | }).then(^(id a, id b){ 75 | XCTAssertEqualObjects(a, @1); 76 | XCTAssertEqualObjects(b, @2); 77 | [ex1 fulfill]; 78 | }); 79 | 80 | [self waitForExpectationsWithTimeout:1 handler:nil]; 81 | } 82 | 83 | @end 84 | -------------------------------------------------------------------------------- /Tests/CoreObjC/RaceTests.m: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | @import PromiseKit; 3 | @import XCTest; 4 | #define PMKTestErrorDomain @"PMKTestErrorDomain" 5 | 6 | static inline NSError *dummyWithCode(NSInteger code) { 7 | return [NSError errorWithDomain:PMKTestErrorDomain code:rand() userInfo:@{NSLocalizedDescriptionKey: @(code).stringValue}]; 8 | } 9 | 10 | @interface RaceTests : XCTestCase @end @implementation RaceTests 11 | 12 | - (void)test_race { 13 | id ex = [self expectationWithDescription:@""]; 14 | id p = PMKAfter(0.1).then(^{ return @2; }); 15 | PMKRace(@[PMKAfter(10), PMKAfter(20), p]).then(^(id obj){ 16 | XCTAssertEqual(2, [obj integerValue]); 17 | [ex fulfill]; 18 | }); 19 | [self waitForExpectationsWithTimeout:1 handler:nil]; 20 | } 21 | 22 | - (void)test_race_empty { 23 | id ex = [self expectationWithDescription:@""]; 24 | PMKRace(@[]).then(^(NSArray* array){ 25 | XCTFail(); 26 | [ex fulfill]; 27 | }).catch(^(NSError *e){ 28 | XCTAssertEqual(e.domain, PMKErrorDomain); 29 | XCTAssertEqual(e.code, PMKInvalidUsageError); 30 | XCTAssertEqualObjects(e.userInfo[NSLocalizedDescriptionKey], @"PMKRace(nil)"); 31 | [ex fulfill]; 32 | }); 33 | [self waitForExpectationsWithTimeout:1 handler:nil]; 34 | } 35 | 36 | - (void)test_race_fullfilled { 37 | id ex = [self expectationWithDescription:@""]; 38 | NSArray* promises = @[ 39 | PMKAfter(1).then(^{ return dummyWithCode(1); }), 40 | PMKAfter(2).then(^{ return dummyWithCode(2); }), 41 | PMKAfter(5).then(^{ return @1; }), 42 | PMKAfter(4).then(^{ return @2; }), 43 | PMKAfter(3).then(^{ return dummyWithCode(3); }) 44 | ]; 45 | PMKRaceFulfilled(promises).then(^(id obj){ 46 | XCTAssertEqual(2, [obj integerValue]); 47 | [ex fulfill]; 48 | }).catch(^{ 49 | XCTFail(); 50 | [ex fulfill]; 51 | }); 52 | [self waitForExpectationsWithTimeout:10 handler:nil]; 53 | } 54 | 55 | - (void)test_race_fulfilled_empty { 56 | id ex = [self expectationWithDescription:@""]; 57 | PMKRaceFulfilled(@[]).then(^(NSArray* array){ 58 | XCTFail(); 59 | [ex fulfill]; 60 | }).catch(^(NSError *e){ 61 | XCTAssertEqual(e.domain, PMKErrorDomain); 62 | XCTAssertEqual(e.code, PMKInvalidUsageError); 63 | XCTAssertEqualObjects(e.userInfo[NSLocalizedDescriptionKey], @"PMKRaceFulfilled(nil)"); 64 | [ex fulfill]; 65 | }); 66 | [self waitForExpectationsWithTimeout:1 handler:nil]; 67 | } 68 | 69 | - (void)test_race_fullfilled_with_no_winner { 70 | id ex = [self expectationWithDescription:@""]; 71 | NSArray* promises = @[ 72 | PMKAfter(1).then(^{ return dummyWithCode(1); }), 73 | PMKAfter(2).then(^{ return dummyWithCode(2); }), 74 | PMKAfter(3).then(^{ return dummyWithCode(3); }) 75 | ]; 76 | PMKRaceFulfilled(promises).then(^(id obj){ 77 | XCTFail(); 78 | [ex fulfill]; 79 | }).catch(^(NSError *e){ 80 | XCTAssertEqual(e.domain, PMKErrorDomain); 81 | XCTAssertEqual(e.code, PMKNoWinnerError); 82 | XCTAssertEqualObjects(e.userInfo[NSLocalizedDescriptionKey], @"PMKRaceFulfilled(nil)"); 83 | [ex fulfill]; 84 | }); 85 | [self waitForExpectationsWithTimeout:10 handler:nil]; 86 | } 87 | 88 | @end 89 | -------------------------------------------------------------------------------- /Tests/CorePromise/AfterTests.swift: -------------------------------------------------------------------------------- 1 | import PromiseKit 2 | import XCTest 3 | 4 | class AfterTests: XCTestCase { 5 | func testZero() { 6 | let ex2 = expectation(description: "") 7 | after(seconds: 0).done(ex2.fulfill) 8 | waitForExpectations(timeout: 2, handler: nil) 9 | 10 | let ex3 = expectation(description: "") 11 | after(.seconds(0)).done(ex3.fulfill) 12 | waitForExpectations(timeout: 2, handler: nil) 13 | 14 | #if !SWIFT_PACKAGE 15 | let ex4 = expectation(description: "") 16 | __PMKAfter(0).done{ _ in ex4.fulfill() }.silenceWarning() 17 | waitForExpectations(timeout: 2, handler: nil) 18 | #endif 19 | } 20 | 21 | func testNegative() { 22 | let ex2 = expectation(description: "") 23 | after(seconds: -1).done(ex2.fulfill) 24 | waitForExpectations(timeout: 2, handler: nil) 25 | 26 | let ex3 = expectation(description: "") 27 | after(.seconds(-1)).done(ex3.fulfill) 28 | waitForExpectations(timeout: 2, handler: nil) 29 | 30 | #if !SWIFT_PACKAGE 31 | let ex4 = expectation(description: "") 32 | __PMKAfter(-1).done{ _ in ex4.fulfill() }.silenceWarning() 33 | waitForExpectations(timeout: 2, handler: nil) 34 | #endif 35 | } 36 | 37 | func testPositive() { 38 | let ex2 = expectation(description: "") 39 | after(seconds: 1).done(ex2.fulfill) 40 | waitForExpectations(timeout: 2, handler: nil) 41 | 42 | let ex3 = expectation(description: "") 43 | after(.seconds(1)).done(ex3.fulfill) 44 | waitForExpectations(timeout: 2, handler: nil) 45 | 46 | #if !SWIFT_PACKAGE 47 | let ex4 = expectation(description: "") 48 | __PMKAfter(1).done{ _ in ex4.fulfill() }.silenceWarning() 49 | waitForExpectations(timeout: 2, handler: nil) 50 | #endif 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Tests/CorePromise/AsyncTests.swift: -------------------------------------------------------------------------------- 1 | import PromiseKit 2 | import XCTest 3 | 4 | private enum Error: Swift.Error { case dummy } 5 | 6 | class AsyncTests: XCTestCase { 7 | 8 | #if swift(>=5.5) 9 | #if canImport(_Concurrency) 10 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 11 | func testAsyncPromiseValue() async throws { 12 | let promise = after(.milliseconds(100)).then(on: nil){ Promise.value(1) } 13 | let value = try await promise.async() 14 | XCTAssertEqual(value, 1) 15 | } 16 | 17 | @available(iOS, deprecated: 13.0) 18 | @available(macOS, deprecated: 10.15) 19 | @available(tvOS, deprecated: 13.0) 20 | @available(watchOS, deprecated: 6.0) 21 | func testAsyncPromiseValue() { 22 | 23 | } 24 | #else 25 | func testAsyncPromiseValue() { 26 | 27 | } 28 | #endif 29 | #else 30 | func testAsyncPromiseValue() { 31 | 32 | } 33 | #endif 34 | 35 | #if swift(>=5.5) 36 | #if canImport(_Concurrency) 37 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 38 | func testAsyncGuaranteeValue() async { 39 | let guarantee = after(.milliseconds(100)).then(on: nil){ Guarantee.value(1) } 40 | let value = await guarantee.async() 41 | XCTAssertEqual(value, 1) 42 | } 43 | 44 | @available(iOS, deprecated: 13.0) 45 | @available(macOS, deprecated: 10.15) 46 | @available(tvOS, deprecated: 13.0) 47 | @available(watchOS, deprecated: 6.0) 48 | func testAsyncGuaranteeValue() { 49 | 50 | } 51 | #else 52 | func testAsyncGuaranteeValue() { 53 | 54 | } 55 | #endif 56 | #else 57 | func testAsyncGuaranteeValue() { 58 | 59 | } 60 | #endif 61 | 62 | #if swift(>=5.5) 63 | #if canImport(_Concurrency) 64 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 65 | func testAsyncPromiseThrow() async throws { 66 | do { 67 | let promise = after(.milliseconds(100)).then(on: nil){ Promise(error: Error.dummy) }.then(on: nil){ Promise.value(1) } 68 | try await _ = promise.async() 69 | XCTAssert(false) 70 | } catch { 71 | switch error as? Error { 72 | case .dummy: 73 | XCTAssert(true) 74 | default: 75 | XCTAssert(false) 76 | } 77 | } 78 | } 79 | 80 | @available(iOS, deprecated: 13.0) 81 | @available(macOS, deprecated: 10.15) 82 | @available(tvOS, deprecated: 13.0) 83 | @available(watchOS, deprecated: 6.0) 84 | func testAsyncPromiseThrow() { 85 | 86 | } 87 | #else 88 | func testAsyncPromiseThrow() { 89 | 90 | } 91 | #endif 92 | #else 93 | func testAsyncPromiseThrow() { 94 | 95 | } 96 | #endif 97 | 98 | #if swift(>=5.5) 99 | #if canImport(_Concurrency) 100 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 101 | func testAsyncPromiseCancel() async throws { 102 | do { 103 | let p = after(seconds: 0).done { _ in 104 | throw LocalError.cancel 105 | }.done { 106 | XCTFail() 107 | } 108 | p.catch { _ in 109 | XCTFail() 110 | } 111 | try await p.async() 112 | XCTAssert(false) 113 | } catch { 114 | guard let cancellableError = error as? CancellableError else { return XCTFail("Unexpected error type") } 115 | XCTAssertTrue(cancellableError.isCancelled) 116 | } 117 | } 118 | 119 | @available(iOS, deprecated: 13.0) 120 | @available(macOS, deprecated: 10.15) 121 | @available(tvOS, deprecated: 13.0) 122 | @available(watchOS, deprecated: 6.0) 123 | func testAsyncPromiseCancel() { 124 | 125 | } 126 | #else 127 | func testAsyncPromiseCancel() { 128 | 129 | } 130 | #endif 131 | #else 132 | func testAsyncPromiseCancel() { 133 | 134 | } 135 | #endif 136 | } 137 | 138 | private enum LocalError: CancellableError { 139 | case notCancel 140 | case cancel 141 | 142 | var isCancelled: Bool { 143 | switch self { 144 | case .notCancel: return false 145 | case .cancel: return true 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Tests/CorePromise/CancellableErrorTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PromiseKit 3 | import XCTest 4 | 5 | #if canImport(StoreKit) 6 | import StoreKit 7 | #endif 8 | 9 | class CancellationTests: XCTestCase { 10 | func testCancellation() { 11 | let ex1 = expectation(description: "") 12 | 13 | let p = after(seconds: 0).done { _ in 14 | throw LocalError.cancel 15 | }.done { 16 | XCTFail() 17 | } 18 | p.catch { _ in 19 | XCTFail() 20 | } 21 | p.catch(policy: .allErrors) { 22 | XCTAssertTrue($0.isCancelled) 23 | ex1.fulfill() 24 | } 25 | 26 | waitForExpectations(timeout: 60) 27 | } 28 | 29 | func testThrowCancellableErrorThatIsNotCancelled() { 30 | let expct = expectation(description: "") 31 | 32 | after(seconds: 0).done { _ in 33 | throw LocalError.notCancel 34 | }.done { 35 | XCTFail() 36 | }.catch { 37 | XCTAssertFalse($0.isCancelled) 38 | expct.fulfill() 39 | } 40 | 41 | waitForExpectations(timeout: 1) 42 | } 43 | 44 | func testRecoverWithCancellation() { 45 | let ex1 = expectation(description: "") 46 | let ex2 = expectation(description: "") 47 | 48 | let p = after(seconds: 0).done { _ in 49 | throw CocoaError.cancelled 50 | }.recover(policy: .allErrors) { err -> Promise<Void> in 51 | ex1.fulfill() 52 | XCTAssertTrue(err.isCancelled) 53 | throw err 54 | }.done { _ in 55 | XCTFail() 56 | } 57 | p.catch { _ in 58 | XCTFail() 59 | } 60 | p.catch(policy: .allErrors) { 61 | XCTAssertTrue($0.isCancelled) 62 | ex2.fulfill() 63 | } 64 | 65 | waitForExpectations(timeout: 1) 66 | } 67 | 68 | func testFoundationBridging1() { 69 | let ex = expectation(description: "") 70 | 71 | let p = after(seconds: 0).done { _ in 72 | throw CocoaError.cancelled 73 | } 74 | p.catch { _ in 75 | XCTFail() 76 | } 77 | p.catch(policy: .allErrors) { 78 | XCTAssertTrue($0.isCancelled) 79 | ex.fulfill() 80 | } 81 | 82 | waitForExpectations(timeout: 1) 83 | } 84 | 85 | func testFoundationBridging2() { 86 | let ex = expectation(description: "") 87 | 88 | let p = Promise().done { 89 | throw URLError.cancelled 90 | } 91 | p.catch { _ in 92 | XCTFail() 93 | } 94 | p.catch(policy: .allErrors) { 95 | XCTAssertTrue($0.isCancelled) 96 | ex.fulfill() 97 | } 98 | 99 | waitForExpectations(timeout: 1) 100 | } 101 | 102 | func testDoesntCrashSwift() { 103 | #if os(macOS) 104 | // Previously exposed a bridging crash in Swift 105 | // NOTE nobody was brave enough or diligent enough to report this to Apple :{ 106 | // NOTE no Linux test since this constructor doesn’t exist there 107 | XCTAssertFalse(NSError().isCancelled) 108 | #endif 109 | 110 | #if canImport(StoreKit) 111 | if #available(watchOS 6.2, *) { 112 | do { 113 | let err = SKError(.paymentCancelled) 114 | XCTAssertTrue(err.isCancelled) 115 | throw err 116 | } catch { 117 | XCTAssertTrue(error.isCancelled) 118 | } 119 | 120 | XCTAssertFalse(SKError(.clientInvalid).isCancelled) 121 | } 122 | #endif 123 | } 124 | 125 | func testBridgeToNSError() { 126 | // Swift.Error types must be cast to NSError for the bridging to occur. 127 | // The below would throw an expection about an invalid selector without a cast: 128 | // `(error as AnyObject).value(forKey: "domain")` 129 | // This simply checks to make sure `isCancelled` is not making that mistake. 130 | 131 | class TestingError: Error { } 132 | 133 | XCTAssertFalse(TestingError().isCancelled) 134 | } 135 | 136 | #if swift(>=3.2) 137 | func testIsCancelled() { 138 | XCTAssertTrue(PMKError.cancelled.isCancelled) 139 | XCTAssertTrue(URLError.cancelled.isCancelled) 140 | XCTAssertTrue(CocoaError.cancelled.isCancelled) 141 | XCTAssertFalse(CocoaError(_nsError: NSError(domain: NSCocoaErrorDomain, code: CocoaError.Code.coderInvalidValue.rawValue)).isCancelled) 142 | } 143 | #endif 144 | } 145 | 146 | private enum LocalError: CancellableError { 147 | case notCancel 148 | case cancel 149 | 150 | var isCancelled: Bool { 151 | switch self { 152 | case .notCancel: return false 153 | case .cancel: return true 154 | } 155 | } 156 | } 157 | 158 | private extension URLError { 159 | static var cancelled: URLError { 160 | return .init(_nsError: NSError(domain: NSURLErrorDomain, code: URLError.Code.cancelled.rawValue)) 161 | } 162 | } 163 | 164 | private extension CocoaError { 165 | static var cancelled: CocoaError { 166 | return .init(_nsError: NSError(domain: NSCocoaErrorDomain, code: CocoaError.Code.userCancelled.rawValue)) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /Tests/CorePromise/DefaultDispatchQueueTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PMKDefaultDispatchQueue.test.swift 3 | // PromiseKit 4 | // 5 | // Created by David Rodriguez on 4/14/16. 6 | // Copyright © 2016 Max Howell. All rights reserved. 7 | // 8 | 9 | import class Foundation.Thread 10 | import PromiseKit 11 | import Dispatch 12 | import XCTest 13 | 14 | private enum Error: Swift.Error { case dummy } 15 | 16 | 17 | class PMKDefaultDispatchQueueTest: XCTestCase { 18 | 19 | let myQueue = DispatchQueue(label: "myQueue") 20 | 21 | override func setUp() { 22 | // can actually only set the default queue once 23 | // - See: PMKSetDefaultDispatchQueue 24 | conf.Q = (myQueue, myQueue) 25 | } 26 | 27 | override func tearDown() { 28 | conf.Q = (.main, .main) 29 | } 30 | 31 | func testOverrodeDefaultThenQueue() { 32 | let ex = expectation(description: "resolving") 33 | 34 | Promise.value(1).then { _ -> Promise<Void> in 35 | ex.fulfill() 36 | XCTAssertFalse(Thread.isMainThread) 37 | return Promise() 38 | }.silenceWarning() 39 | 40 | XCTAssertTrue(Thread.isMainThread) 41 | 42 | waitForExpectations(timeout: 1) 43 | } 44 | 45 | func testOverrodeDefaultCatchQueue() { 46 | let ex = expectation(description: "resolving") 47 | 48 | Promise<Int>(error: Error.dummy).catch { _ in 49 | ex.fulfill() 50 | XCTAssertFalse(Thread.isMainThread) 51 | } 52 | 53 | XCTAssertTrue(Thread.isMainThread) 54 | 55 | waitForExpectations(timeout: 1) 56 | } 57 | 58 | func testOverrodeDefaultAlwaysQueue() { 59 | let ex = expectation(description: "resolving") 60 | 61 | Promise.value(1).ensure { 62 | ex.fulfill() 63 | XCTAssertFalse(Thread.isMainThread) 64 | }.silenceWarning() 65 | 66 | XCTAssertTrue(Thread.isMainThread) 67 | 68 | waitForExpectations(timeout: 1) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Tests/CorePromise/ErrorTests.swift: -------------------------------------------------------------------------------- 1 | import PromiseKit 2 | import XCTest 3 | 4 | class PMKErrorTests: XCTestCase { 5 | func testCustomStringConvertible() { 6 | XCTAssertNotNil(PMKError.invalidCallingConvention.errorDescription) 7 | XCTAssertNotNil(PMKError.returnedSelf.errorDescription) 8 | XCTAssertNotNil(PMKError.badInput.errorDescription) 9 | XCTAssertNotNil(PMKError.cancelled.errorDescription) 10 | XCTAssertNotNil(PMKError.compactMap(1, Int.self).errorDescription) 11 | XCTAssertNotNil(PMKError.emptySequence.errorDescription) 12 | } 13 | 14 | func testCustomDebugStringConvertible() { 15 | XCTAssertFalse(PMKError.invalidCallingConvention.debugDescription.isEmpty) 16 | XCTAssertFalse(PMKError.returnedSelf.debugDescription.isEmpty) 17 | XCTAssertNotNil(PMKError.badInput.debugDescription.isEmpty) 18 | XCTAssertFalse(PMKError.cancelled.debugDescription.isEmpty) 19 | XCTAssertFalse(PMKError.compactMap(1, Int.self).debugDescription.isEmpty) 20 | XCTAssertFalse(PMKError.emptySequence.debugDescription.isEmpty) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/CorePromise/GuaranteeTests.swift: -------------------------------------------------------------------------------- 1 | import PromiseKit 2 | import XCTest 3 | 4 | class GuaranteeTests: XCTestCase { 5 | func testInit() { 6 | let ex = expectation(description: "") 7 | Guarantee { seal in 8 | seal(1) 9 | }.done { 10 | XCTAssertEqual(1, $0) 11 | ex.fulfill() 12 | } 13 | wait(for: [ex], timeout: 10) 14 | } 15 | 16 | func testMap() { 17 | let ex = expectation(description: "") 18 | 19 | Guarantee.value(1).map { 20 | $0 * 2 21 | }.done { 22 | XCTAssertEqual(2, $0) 23 | ex.fulfill() 24 | } 25 | 26 | wait(for: [ex], timeout: 10) 27 | } 28 | 29 | func testMapByKeyPath() { 30 | let ex = expectation(description: "") 31 | 32 | Guarantee.value(Person(name: "Max")).map(\.name).done { 33 | XCTAssertEqual("Max", $0) 34 | ex.fulfill() 35 | } 36 | 37 | wait(for: [ex], timeout: 10) 38 | } 39 | 40 | func testWait() { 41 | XCTAssertEqual(after(.milliseconds(100)).map(on: nil){ 1 }.wait(), 1) 42 | } 43 | 44 | func testMapValues() { 45 | let ex = expectation(description: "") 46 | 47 | Guarantee.value([1, 2, 3]) 48 | .mapValues { $0 * 2 } 49 | .done { values in 50 | XCTAssertEqual([2, 4, 6], values) 51 | ex.fulfill() 52 | } 53 | 54 | wait(for: [ex], timeout: 10) 55 | } 56 | 57 | func testMapValuesByKeyPath() { 58 | let ex = expectation(description: "") 59 | 60 | Guarantee.value([Person(name: "Max"), Person(name: "Roman"), Person(name: "John")]) 61 | .mapValues(\.name) 62 | .done { values in 63 | XCTAssertEqual(["Max", "Roman", "John"], values) 64 | ex.fulfill() 65 | } 66 | 67 | wait(for: [ex], timeout: 10) 68 | } 69 | 70 | func testFlatMapValues() { 71 | let ex = expectation(description: "") 72 | 73 | Guarantee.value([1, 2, 3]) 74 | .flatMapValues { [$0, $0] } 75 | .done { values in 76 | XCTAssertEqual([1, 1, 2, 2, 3, 3], values) 77 | ex.fulfill() 78 | } 79 | 80 | wait(for: [ex], timeout: 10) 81 | } 82 | 83 | func testCompactMapValues() { 84 | let ex = expectation(description: "") 85 | 86 | Guarantee.value(["1","2","a","3"]) 87 | .compactMapValues { Int($0) } 88 | .done { values in 89 | XCTAssertEqual([1, 2, 3], values) 90 | ex.fulfill() 91 | } 92 | 93 | wait(for: [ex], timeout: 10) 94 | } 95 | 96 | func testCompactMapValuesByKeyPath() { 97 | let ex = expectation(description: "") 98 | 99 | Guarantee.value([Person(name: "Max"), Person(name: "Roman", age: 26), Person(name: "John", age: 23)]) 100 | .compactMapValues(\.age) 101 | .done { values in 102 | XCTAssertEqual([26, 23], values) 103 | ex.fulfill() 104 | } 105 | 106 | wait(for: [ex], timeout: 10) 107 | } 108 | 109 | func testThenMap() { 110 | 111 | let ex = expectation(description: "") 112 | 113 | Guarantee.value([1, 2, 3]) 114 | .thenMap { Guarantee.value($0 * 2) } 115 | .done { values in 116 | XCTAssertEqual([2, 4, 6], values) 117 | ex.fulfill() 118 | } 119 | 120 | wait(for: [ex], timeout: 10) 121 | } 122 | 123 | func testThenFlatMap() { 124 | 125 | let ex = expectation(description: "") 126 | 127 | Guarantee.value([1, 2, 3]) 128 | .thenFlatMap { Guarantee.value([$0, $0]) } 129 | .done { values in 130 | XCTAssertEqual([1, 1, 2, 2, 3, 3], values) 131 | ex.fulfill() 132 | } 133 | 134 | wait(for: [ex], timeout: 10) 135 | } 136 | 137 | func testFilterValues() { 138 | 139 | let ex = expectation(description: "") 140 | 141 | Guarantee.value([1, 2, 3]) 142 | .filterValues { $0 > 1 } 143 | .done { values in 144 | XCTAssertEqual([2, 3], values) 145 | ex.fulfill() 146 | } 147 | 148 | wait(for: [ex], timeout: 10) 149 | } 150 | 151 | func testFilterValuesByKeyPath() { 152 | 153 | let ex = expectation(description: "") 154 | 155 | Guarantee.value([Person(name: "Max"), Person(name: "Roman", age: 26, isStudent: false), Person(name: "John", age: 23, isStudent: true)]) 156 | .filterValues(\.isStudent) 157 | .done { values in 158 | XCTAssertEqual([Person(name: "John", age: 23, isStudent: true)], values) 159 | ex.fulfill() 160 | } 161 | 162 | wait(for: [ex], timeout: 10) 163 | } 164 | 165 | func testSorted() { 166 | 167 | let ex = expectation(description: "") 168 | 169 | Guarantee.value([5, 2, 3, 4, 1]) 170 | .sortedValues() 171 | .done { values in 172 | XCTAssertEqual([1, 2, 3, 4, 5], values) 173 | ex.fulfill() 174 | } 175 | 176 | wait(for: [ex], timeout: 10) 177 | } 178 | 179 | func testSortedBy() { 180 | 181 | let ex = expectation(description: "") 182 | 183 | Guarantee.value([5, 2, 3, 4, 1]) 184 | .sortedValues { $0 > $1 } 185 | .done { values in 186 | XCTAssertEqual([5, 4, 3, 2, 1], values) 187 | ex.fulfill() 188 | } 189 | 190 | wait(for: [ex], timeout: 10) 191 | } 192 | 193 | #if swift(>=3.1) 194 | func testNoAmbiguityForValue() { 195 | let ex = expectation(description: "") 196 | let a = Guarantee<Void>.value 197 | let b = Guarantee<Void>.value(Void()) 198 | let c = Guarantee<Void>.value(()) 199 | when(fulfilled: a, b, c).done { 200 | ex.fulfill() 201 | }.cauterize() 202 | wait(for: [ex], timeout: 10) 203 | } 204 | #endif 205 | } 206 | -------------------------------------------------------------------------------- /Tests/CorePromise/HangTests.swift: -------------------------------------------------------------------------------- 1 | import PromiseKit 2 | import XCTest 3 | 4 | class HangTests: XCTestCase { 5 | func test() { 6 | let ex = expectation(description: "block executed") 7 | do { 8 | let value = try hang(after(seconds: 0.02).then { _ -> Promise<Int> in 9 | ex.fulfill() 10 | return .value(1) 11 | }) 12 | XCTAssertEqual(value, 1) 13 | } catch { 14 | XCTFail("Unexpected error") 15 | } 16 | waitForExpectations(timeout: 0) 17 | } 18 | 19 | enum Error: Swift.Error { 20 | case test 21 | } 22 | 23 | func testError() { 24 | var value = 0 25 | do { 26 | _ = try hang(after(seconds: 0.02).done { 27 | value = 1 28 | throw Error.test 29 | }) 30 | XCTAssertEqual(value, 1) 31 | } catch Error.test { 32 | return 33 | } catch { 34 | XCTFail("Unexpected error (expected Error.test)") 35 | } 36 | XCTFail("Expected error but no error was thrown") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Tests/CorePromise/PromiseTests.swift: -------------------------------------------------------------------------------- 1 | import PromiseKit 2 | import Dispatch 3 | import XCTest 4 | 5 | class PromiseTests: XCTestCase { 6 | func testIsPending() { 7 | XCTAssertTrue(Promise<Void>.pending().promise.isPending) 8 | XCTAssertFalse(Promise().isPending) 9 | XCTAssertFalse(Promise<Void>(error: Error.dummy).isPending) 10 | } 11 | 12 | func testIsResolved() { 13 | XCTAssertFalse(Promise<Void>.pending().promise.isResolved) 14 | XCTAssertTrue(Promise().isResolved) 15 | XCTAssertTrue(Promise<Void>(error: Error.dummy).isResolved) 16 | } 17 | 18 | func testIsFulfilled() { 19 | XCTAssertFalse(Promise<Void>.pending().promise.isFulfilled) 20 | XCTAssertTrue(Promise().isFulfilled) 21 | XCTAssertFalse(Promise<Void>(error: Error.dummy).isFulfilled) 22 | } 23 | 24 | func testIsRejected() { 25 | XCTAssertFalse(Promise<Void>.pending().promise.isRejected) 26 | XCTAssertTrue(Promise<Void>(error: Error.dummy).isRejected) 27 | XCTAssertFalse(Promise().isRejected) 28 | } 29 | 30 | @available(macOS 10.10, iOS 2.0, tvOS 10.0, watchOS 2.0, *) 31 | func testDispatchQueueAsyncExtensionReturnsPromise() { 32 | let ex = expectation(description: "") 33 | 34 | DispatchQueue.global().async(.promise) { () -> Int in 35 | XCTAssertFalse(Thread.isMainThread) 36 | return 1 37 | }.done { one in 38 | XCTAssertEqual(one, 1) 39 | ex.fulfill() 40 | } 41 | 42 | waitForExpectations(timeout: 1) 43 | } 44 | 45 | @available(macOS 10.10, iOS 2.0, tvOS 10.0, watchOS 2.0, *) 46 | func testDispatchQueueAsyncExtensionCanThrowInBody() { 47 | let ex = expectation(description: "") 48 | 49 | DispatchQueue.global().async(.promise) { () -> Int in 50 | throw Error.dummy 51 | }.done { _ in 52 | XCTFail() 53 | }.catch { _ in 54 | ex.fulfill() 55 | } 56 | 57 | waitForExpectations(timeout: 1) 58 | } 59 | 60 | func testCustomStringConvertible() { 61 | XCTAssertEqual(Promise<Int>.pending().promise.debugDescription, "Promise<Int>.pending(handlers: 0)") 62 | XCTAssertEqual(Promise().debugDescription, "Promise<()>.fulfilled(())") 63 | XCTAssertEqual(Promise<String>(error: Error.dummy).debugDescription, "Promise<String>.rejected(Error.dummy)") 64 | 65 | XCTAssertEqual("\(Promise<Int>.pending().promise)", "Promise(…Int)") 66 | XCTAssertEqual("\(Promise.value(3))", "Promise(3)") 67 | XCTAssertEqual("\(Promise<Void>(error: Error.dummy))", "Promise(dummy)") 68 | } 69 | 70 | func testCannotFulfillWithError() { 71 | 72 | // sadly this test proves the opposite :( 73 | // left here so maybe one day we can prevent instantiation of `Promise<Error>` 74 | 75 | _ = Promise { seal in 76 | seal.fulfill(Error.dummy) 77 | } 78 | 79 | _ = Promise<Error>.pending() 80 | 81 | _ = Promise.value(Error.dummy) 82 | 83 | _ = Promise().map { Error.dummy } 84 | } 85 | 86 | #if swift(>=3.1) 87 | func testCanMakeVoidPromise() { 88 | _ = Promise() 89 | _ = Guarantee() 90 | } 91 | #endif 92 | 93 | enum Error: Swift.Error { 94 | case dummy 95 | } 96 | 97 | func testThrowInInitializer() { 98 | let p = Promise<Void> { _ in 99 | throw Error.dummy 100 | } 101 | XCTAssertTrue(p.isRejected) 102 | guard let err = p.error, case Error.dummy = err else { return XCTFail() } 103 | } 104 | 105 | func testThrowInFirstly() { 106 | let ex = expectation(description: "") 107 | 108 | firstly { () -> Promise<Int> in 109 | throw Error.dummy 110 | }.catch { 111 | XCTAssertEqual($0 as? Error, Error.dummy) 112 | ex.fulfill() 113 | } 114 | 115 | wait(for: [ex], timeout: 10) 116 | } 117 | 118 | func testWait() throws { 119 | let p = after(.milliseconds(100)).then(on: nil){ Promise.value(1) } 120 | XCTAssertEqual(try p.wait(), 1) 121 | 122 | do { 123 | let p = after(.milliseconds(100)).map(on: nil){ throw Error.dummy } 124 | try p.wait() 125 | XCTFail() 126 | } catch { 127 | XCTAssertEqual(error as? Error, Error.dummy) 128 | } 129 | } 130 | 131 | func testPipeForResolved() { 132 | let ex = expectation(description: "") 133 | Promise.value(1).done { 134 | XCTAssertEqual(1, $0) 135 | ex.fulfill() 136 | }.silenceWarning() 137 | wait(for: [ex], timeout: 10) 138 | } 139 | 140 | #if swift(>=3.1) 141 | func testNoAmbiguityForValue() { 142 | let ex = expectation(description: "") 143 | let a = Promise<Void>.value 144 | let b = Promise<Void>.value(Void()) 145 | let c = Promise<Void>.value(()) 146 | when(fulfilled: a, b, c).done { 147 | ex.fulfill() 148 | }.cauterize() 149 | wait(for: [ex], timeout: 10) 150 | } 151 | #endif 152 | } 153 | -------------------------------------------------------------------------------- /Tests/CorePromise/RaceTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import PromiseKit 3 | 4 | class RaceTests: XCTestCase { 5 | func test1() { 6 | let ex = expectation(description: "") 7 | race(after(.milliseconds(10)).then{ Promise.value(1) }, after(seconds: 1).map{ 2 }).done { index in 8 | XCTAssertEqual(index, 1) 9 | ex.fulfill() 10 | }.silenceWarning() 11 | waitForExpectations(timeout: 1, handler: nil) 12 | } 13 | 14 | func test2() { 15 | let ex = expectation(description: "") 16 | race(after(seconds: 1).map{ 1 }, after(.milliseconds(10)).map{ 2 }).done { index in 17 | XCTAssertEqual(index, 2) 18 | ex.fulfill() 19 | } 20 | waitForExpectations(timeout: 1, handler: nil) 21 | } 22 | 23 | func test1Array() { 24 | let ex = expectation(description: "") 25 | let promises = [after(.milliseconds(10)).map{ 1 }, after(seconds: 1).map{ 2 }] 26 | race(promises).done { index in 27 | XCTAssertEqual(index, 1) 28 | ex.fulfill() 29 | }.silenceWarning() 30 | waitForExpectations(timeout: 1, handler: nil) 31 | } 32 | 33 | func test2Array() { 34 | let ex = expectation(description: "") 35 | race(after(seconds: 1).map{ 1 }, after(.milliseconds(10)).map{ 2 }).done { index in 36 | XCTAssertEqual(index, 2) 37 | ex.fulfill() 38 | } 39 | waitForExpectations(timeout: 1, handler: nil) 40 | } 41 | 42 | func testEmptyArray() { 43 | let ex = expectation(description: "") 44 | let empty = [Promise<Int>]() 45 | race(empty).catch { 46 | guard case PMKError.badInput = $0 else { return XCTFail() } 47 | ex.fulfill() 48 | } 49 | wait(for: [ex], timeout: 10) 50 | } 51 | 52 | func testFulfilled() { 53 | enum Error: Swift.Error { case test1, test2, test3 } 54 | let ex = expectation(description: "") 55 | let promises: [Promise<Int>] = [after(seconds: 1).map { _ in throw Error.test1 }, after(seconds: 2).map { _ in throw Error.test2 }, after(seconds: 5).map { 1 }, after(seconds: 4).map { 2 }, after(seconds: 3).map { _ in throw Error.test3 }] 56 | race(fulfilled: promises).done { 57 | XCTAssertEqual($0, 2) 58 | ex.fulfill() 59 | }.catch { _ in 60 | XCTFail() 61 | ex.fulfill() 62 | } 63 | wait(for: [ex], timeout: 10) 64 | } 65 | 66 | func testFulfilledEmptyArray() { 67 | let ex = expectation(description: "") 68 | let empty = [Promise<Int>]() 69 | race(fulfilled: empty).catch { 70 | guard case PMKError.badInput = $0 else { return XCTFail() } 71 | ex.fulfill() 72 | } 73 | wait(for: [ex], timeout: 10) 74 | } 75 | 76 | func testFulfilledWithNoWinner() { 77 | enum Error: Swift.Error { case test1, test2 } 78 | let ex = expectation(description: "") 79 | let promises: [Promise<Int>] = [after(seconds: 1).map { _ in throw Error.test1 }, after(seconds: 2).map { _ in throw Error.test2 }] 80 | race(fulfilled: promises).done { _ in 81 | XCTFail() 82 | ex.fulfill() 83 | }.catch { 84 | guard let pmkError = $0 as? PMKError else { return XCTFail() } 85 | guard case .noWinner = pmkError else { return XCTFail() } 86 | guard pmkError.debugDescription == "All thenables passed to race(fulfilled:) were rejected" else { return XCTFail() } 87 | ex.fulfill() 88 | } 89 | wait(for: [ex], timeout: 10) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Tests/CorePromise/RegressionTests.swift: -------------------------------------------------------------------------------- 1 | import PromiseKit 2 | import XCTest 3 | 4 | class RegressionTests: XCTestCase { 5 | func testReturningPreviousPromiseWorks() { 6 | 7 | // regression test because we were doing this wrong 8 | // in our A+ tests implementation for spec: 2.3.1 9 | 10 | do { 11 | let promise1 = Promise() 12 | let promise2 = promise1.then(on: nil) { promise1 } 13 | promise2.catch(on: nil) { _ in XCTFail() } 14 | } 15 | do { 16 | enum Error: Swift.Error { case dummy } 17 | 18 | let promise1 = Promise<Void>(error: Error.dummy) 19 | let promise2 = promise1.recover(on: nil) { _ in promise1 } 20 | promise2.catch(on: nil) { err in 21 | if case PMKError.returnedSelf = err { 22 | XCTFail() 23 | } 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tests/CorePromise/StressTests.swift: -------------------------------------------------------------------------------- 1 | import PromiseKit 2 | import Dispatch 3 | import XCTest 4 | 5 | class StressTests: XCTestCase { 6 | func testThenDataRace() { 7 | let e1 = expectation(description: "") 8 | 9 | //will crash if then doesn't protect handlers 10 | stressDataRace(expectation: e1, stressFunction: { promise in 11 | promise.done { s in 12 | XCTAssertEqual("ok", s) 13 | return 14 | }.silenceWarning() 15 | }, fulfill: { "ok" }) 16 | 17 | waitForExpectations(timeout: 10, handler: nil) 18 | } 19 | 20 | @available(macOS 10.10, iOS 2.0, tvOS 10.0, watchOS 2.0, *) 21 | func testThensAreSequentialForLongTime() { 22 | var values = [Int]() 23 | let ex = expectation(description: "") 24 | var promise = DispatchQueue.global().async(.promise){ 0 } 25 | let N = 1000 26 | for x in 1..<N { 27 | promise = promise.then { y -> Guarantee<Int> in 28 | values.append(y) 29 | XCTAssertEqual(x - 1, y) 30 | return DispatchQueue.global().async(.promise) { x } 31 | } 32 | } 33 | promise.done { x in 34 | values.append(x) 35 | XCTAssertEqual(values, (0..<N).map{ $0 }) 36 | ex.fulfill() 37 | } 38 | waitForExpectations(timeout: 10, handler: nil) 39 | } 40 | 41 | func testZalgoDataRace() { 42 | let e1 = expectation(description: "") 43 | 44 | //will crash if zalgo doesn't protect handlers 45 | stressDataRace(expectation: e1, stressFunction: { promise in 46 | promise.done(on: nil) { s in 47 | XCTAssertEqual("ok", s) 48 | }.silenceWarning() 49 | }, fulfill: { 50 | return "ok" 51 | }) 52 | 53 | waitForExpectations(timeout: 10, handler: nil) 54 | } 55 | } 56 | 57 | private enum Error: Swift.Error { 58 | case Dummy 59 | } 60 | 61 | private func stressDataRace<T: Equatable>(expectation e1: XCTestExpectation, iterations: Int = 1000, stressFactor: Int = 10, stressFunction: @escaping (Promise<T>) -> Void, fulfill f: @escaping () -> T) { 62 | let group = DispatchGroup() 63 | let queue = DispatchQueue(label: "the.domain.of.Zalgo", attributes: .concurrent) 64 | 65 | for _ in 0..<iterations { 66 | let (promise, seal) = Promise<T>.pending() 67 | 68 | DispatchQueue.concurrentPerform(iterations: stressFactor) { n in 69 | stressFunction(promise) 70 | } 71 | 72 | queue.async(group: group) { 73 | seal.fulfill(f()) 74 | } 75 | } 76 | 77 | group.notify(queue: queue, execute: e1.fulfill) 78 | } 79 | -------------------------------------------------------------------------------- /Tests/CorePromise/Utilities.swift: -------------------------------------------------------------------------------- 1 | import PromiseKit 2 | 3 | extension Promise { 4 | func silenceWarning() {} 5 | } 6 | 7 | #if os(Linux) || os(Android) 8 | import func CoreFoundation._CFIsMainThread 9 | 10 | extension Thread { 11 | // `isMainThread` is not implemented yet in swift-corelibs-foundation. 12 | static var isMainThread: Bool { 13 | return _CFIsMainThread() 14 | } 15 | } 16 | 17 | import XCTest 18 | 19 | extension XCTestCase { 20 | func wait(for: [XCTestExpectation], timeout: TimeInterval, file: StaticString = #file, line: UInt = #line) { 21 | #if !(swift(>=4.0) && !swift(>=4.1)) 22 | let line = Int(line) 23 | #endif 24 | waitForExpectations(timeout: timeout, file: file, line: line) 25 | } 26 | } 27 | 28 | extension XCTestExpectation { 29 | func fulfill() { 30 | fulfill(#file, line: #line) 31 | } 32 | } 33 | #endif 34 | -------------------------------------------------------------------------------- /Tests/CorePromise/WhenConcurrentTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import PromiseKit 3 | 4 | class WhenConcurrentTestCase_Swift: XCTestCase { 5 | 6 | func testWhen() { 7 | let e = expectation(description: "") 8 | 9 | var numbers = (0..<42).makeIterator() 10 | let squareNumbers = numbers.map { $0 * $0 } 11 | 12 | let generator = AnyIterator<Guarantee<Int>> { 13 | guard let number = numbers.next() else { 14 | return nil 15 | } 16 | 17 | return after(.milliseconds(10)).map { 18 | return number * number 19 | } 20 | } 21 | 22 | when(fulfilled: generator, concurrently: 5).done { numbers in 23 | if numbers == squareNumbers { 24 | e.fulfill() 25 | } 26 | }.silenceWarning() 27 | 28 | waitForExpectations(timeout: 3, handler: nil) 29 | } 30 | 31 | func testWhenEmptyGenerator() { 32 | let e = expectation(description: "") 33 | 34 | let generator = AnyIterator<Promise<Int>> { 35 | return nil 36 | } 37 | 38 | when(fulfilled: generator, concurrently: 5).done { numbers in 39 | if numbers.count == 0 { 40 | e.fulfill() 41 | } 42 | }.silenceWarning() 43 | 44 | waitForExpectations(timeout: 1, handler: nil) 45 | } 46 | 47 | func testWhenGeneratorError() { 48 | enum LocalError: Error { 49 | case Unknown 50 | case DivisionByZero 51 | } 52 | 53 | let expectedErrorIndex = 42 54 | let expectedError = LocalError.DivisionByZero 55 | 56 | let e = expectation(description: "") 57 | 58 | var numbers = (-expectedErrorIndex..<expectedErrorIndex).makeIterator() 59 | 60 | let generator = AnyIterator<Promise<Int>> { 61 | guard let number = numbers.next() else { 62 | return nil 63 | } 64 | 65 | return after(.milliseconds(10)).then { _ -> Promise<Int> in 66 | if number != 0 { 67 | return Promise(error: expectedError) 68 | } else { 69 | return .value(100500 / number) 70 | } 71 | } 72 | } 73 | 74 | when(fulfilled: generator, concurrently: 3) 75 | .catch { error in 76 | guard let error = error as? LocalError else { 77 | return 78 | } 79 | guard case .DivisionByZero = error else { 80 | return 81 | } 82 | e.fulfill() 83 | } 84 | 85 | waitForExpectations(timeout: 3, handler: nil) 86 | } 87 | 88 | func testWhenConcurrency() { 89 | let expectedConcurrently = 4 90 | var currentConcurrently = 0 91 | var maxConcurrently = 0 92 | 93 | let e = expectation(description: "") 94 | 95 | var numbers = (0..<42).makeIterator() 96 | 97 | let generator = AnyIterator<Promise<Int>> { 98 | currentConcurrently += 1 99 | maxConcurrently = max(maxConcurrently, currentConcurrently) 100 | 101 | guard let number = numbers.next() else { 102 | return nil 103 | } 104 | 105 | return after(.milliseconds(10)).then(on: .main) { _ -> Promise<Int> in 106 | currentConcurrently -= 1 107 | return .value(number * number) 108 | } 109 | } 110 | 111 | when(fulfilled: generator, concurrently: expectedConcurrently).done { _ in 112 | XCTAssertEqual(expectedConcurrently, maxConcurrently) 113 | e.fulfill() 114 | }.silenceWarning() 115 | 116 | waitForExpectations(timeout: 3) 117 | } 118 | 119 | func testWhenConcurrencyLessThanZero() { 120 | let generator = AnyIterator<Promise<Int>> { XCTFail(); return nil } 121 | 122 | let p1 = when(fulfilled: generator, concurrently: 0) 123 | let p2 = when(fulfilled: generator, concurrently: -1) 124 | 125 | guard let e1 = p1.error else { return XCTFail() } 126 | guard let e2 = p2.error else { return XCTFail() } 127 | guard case PMKError.badInput = e1 else { return XCTFail() } 128 | guard case PMKError.badInput = e2 else { return XCTFail() } 129 | } 130 | 131 | func testStopsDequeueingOnceRejected() { 132 | let ex = expectation(description: "") 133 | enum Error: Swift.Error { case dummy } 134 | 135 | var x: UInt = 0 136 | let generator = AnyIterator<Promise<Void>> { 137 | x += 1 138 | switch x { 139 | case 0: 140 | fatalError() 141 | case 1: 142 | return Promise() 143 | case 2: 144 | return Promise(error: Error.dummy) 145 | case _: 146 | XCTFail() 147 | return nil 148 | } 149 | } 150 | 151 | when(fulfilled: generator, concurrently: 1).done { 152 | XCTFail("\($0)") 153 | }.catch { error in 154 | ex.fulfill() 155 | } 156 | 157 | waitForExpectations(timeout: 3) 158 | } 159 | 160 | func testWhenResolvedContinuesWhenRejected() { 161 | #if swift(>=5.3) 162 | let ex = expectation(description: "") 163 | enum Error: Swift.Error { case dummy } 164 | 165 | var x: UInt = 0 166 | let generator = AnyIterator<Promise<Void>> { 167 | x += 1 168 | switch x { 169 | case 0: 170 | fatalError() 171 | case 1: 172 | return Promise() 173 | case 2: 174 | return Promise(error: Error.dummy) 175 | case 3: 176 | return Promise() 177 | case _: 178 | return nil 179 | } 180 | } 181 | 182 | when(resolved: generator, concurrently: 1).done { results in 183 | XCTAssertEqual(results.count, 3) 184 | ex.fulfill() 185 | } 186 | 187 | waitForExpectations(timeout: 3) 188 | #endif 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /Tests/CorePromise/WhenResolvedTests.swift: -------------------------------------------------------------------------------- 1 | // Created by Austin Feight on 3/19/16. 2 | // Copyright © 2016 Max Howell. All rights reserved. 3 | 4 | import PromiseKit 5 | import XCTest 6 | 7 | class JoinTests: XCTestCase { 8 | func testImmediates() { 9 | let successPromise = Promise() 10 | 11 | var joinFinished = false 12 | when(resolved: successPromise).done(on: nil) { _ in joinFinished = true } 13 | XCTAssert(joinFinished, "Join immediately finishes on fulfilled promise") 14 | 15 | let promise2 = Promise.value(2) 16 | let promise3 = Promise.value(3) 17 | let promise4 = Promise.value(4) 18 | var join2Finished = false 19 | when(resolved: promise2, promise3, promise4).done(on: nil) { _ in join2Finished = true } 20 | XCTAssert(join2Finished, "Join immediately finishes on fulfilled promises") 21 | } 22 | 23 | func testFulfilledAfterAllResolve() { 24 | let (promise1, seal1) = Promise<Void>.pending() 25 | let (promise2, seal2) = Promise<Void>.pending() 26 | let (promise3, seal3) = Promise<Void>.pending() 27 | 28 | var finished = false 29 | when(resolved: promise1, promise2, promise3).done(on: nil) { _ in finished = true } 30 | XCTAssertFalse(finished, "Not all promises have resolved") 31 | 32 | seal1.fulfill_() 33 | XCTAssertFalse(finished, "Not all promises have resolved") 34 | 35 | seal2.fulfill_() 36 | XCTAssertFalse(finished, "Not all promises have resolved") 37 | 38 | seal3.fulfill_() 39 | XCTAssert(finished, "All promises have resolved") 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Tests/CorePromise/ZalgoTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import PromiseKit 3 | 4 | class ZalgoTests: XCTestCase { 5 | func test1() { 6 | var resolved = false 7 | Promise.value(1).done(on: nil) { _ in 8 | resolved = true 9 | }.silenceWarning() 10 | XCTAssertTrue(resolved) 11 | } 12 | 13 | func test2() { 14 | let p1 = Promise.value(1).map(on: nil) { x in 15 | return 2 16 | } 17 | XCTAssertEqual(p1.value!, 2) 18 | 19 | var x = 0 20 | 21 | let (p2, seal) = Promise<Int>.pending() 22 | p2.done(on: nil) { _ in 23 | x = 1 24 | }.silenceWarning() 25 | XCTAssertEqual(x, 0) 26 | 27 | seal.fulfill(1) 28 | XCTAssertEqual(x, 1) 29 | } 30 | 31 | // returning a pending promise from its own zalgo’d then handler doesn’t hang 32 | func test3() { 33 | let ex = (expectation(description: ""), expectation(description: "")) 34 | 35 | var p1: Promise<Void>! 36 | p1 = after(.milliseconds(100)).then(on: nil) { _ -> Promise<Void> in 37 | ex.0.fulfill() 38 | return p1 39 | } 40 | 41 | p1.catch { err in 42 | defer{ ex.1.fulfill() } 43 | guard case PMKError.returnedSelf = err else { return XCTFail() } 44 | } 45 | 46 | waitForExpectations(timeout: 1) 47 | } 48 | 49 | // return a sealed promise from its own zalgo’d then handler doesn’t hang 50 | func test4() { 51 | let ex = expectation(description: "") 52 | let p1 = Promise.value(1) 53 | p1.then(on: nil) { _ -> Promise<Int> in 54 | ex.fulfill() 55 | return p1 56 | }.silenceWarning() 57 | waitForExpectations(timeout: 1) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Tests/DeprecationTests.swift: -------------------------------------------------------------------------------- 1 | import PromiseKit 2 | import XCTest 3 | 4 | class DeprecationTests: XCTestCase { 5 | func testWrap1() { 6 | let dummy = 10 7 | 8 | func completion(_ body: (_ a: Int?, _ b: Error?) -> Void) { 9 | body(dummy, nil) 10 | } 11 | 12 | let ex = expectation(description: "") 13 | wrap(completion).done { 14 | XCTAssertEqual($0, dummy) 15 | ex.fulfill() 16 | } 17 | wait(for: [ex], timeout: 10) 18 | } 19 | 20 | func testWrap2() { 21 | let dummy = 10 22 | 23 | func completion(_ body: (_ a: Int, _ b: Error?) -> Void) { 24 | body(dummy, nil) 25 | } 26 | 27 | let ex = expectation(description: "") 28 | wrap(completion).done { 29 | XCTAssertEqual($0, dummy) 30 | ex.fulfill() 31 | } 32 | wait(for: [ex], timeout: 10) 33 | } 34 | 35 | func testWrap3() { 36 | let dummy = 10 37 | 38 | func completion(_ body: (_ a: Error?, _ b: Int?) -> Void) { 39 | body(nil, dummy) 40 | } 41 | 42 | let ex = expectation(description: "") 43 | wrap(completion).done { 44 | XCTAssertEqual($0, dummy) 45 | ex.fulfill() 46 | } 47 | wait(for: [ex], timeout: 10) 48 | } 49 | 50 | func testWrap4() { 51 | let dummy = 10 52 | 53 | func completion(_ body: (_ a: Error?) -> Void) { 54 | body(nil) 55 | } 56 | 57 | let ex = expectation(description: "") 58 | wrap(completion).done { 59 | ex.fulfill() 60 | } 61 | wait(for: [ex], timeout: 10) 62 | } 63 | 64 | func testWrap5() { 65 | let dummy = 10 66 | 67 | func completion(_ body: (_ a: Int) -> Void) { 68 | body(dummy) 69 | } 70 | 71 | let ex = expectation(description: "") 72 | wrap(completion).done { 73 | XCTAssertEqual($0, dummy) 74 | ex.fulfill() 75 | } 76 | wait(for: [ex], timeout: 10) 77 | } 78 | 79 | func testAlways() { 80 | let ex = expectation(description: "") 81 | Promise.value(1).always(execute: ex.fulfill) 82 | wait(for: [ex], timeout: 10) 83 | } 84 | 85 | #if PMKFullDeprecations 86 | func testFlatMap() { 87 | let ex = expectation(description: "") 88 | Promise.value(1).flatMap { _ -> Int? in 89 | nil 90 | }.catch { 91 | //TODO should be `flatMap`, but how to enact that without causing 92 | // compiler to warn when building PromiseKit for end-users? LOL 93 | guard case PMKError.compactMap = $0 else { return XCTFail() } 94 | ex.fulfill() 95 | } 96 | wait(for: [ex], timeout: 10) 97 | } 98 | 99 | func testSequenceMap() { 100 | let ex = expectation(description: "") 101 | Promise.value([1, 2]).map { 102 | $0 + 1 103 | }.done { 104 | XCTAssertEqual($0, [2, 3]) 105 | ex.fulfill() 106 | }.silenceWarning() 107 | wait(for: [ex], timeout: 10) 108 | } 109 | 110 | func testSequenceFlatMap() { 111 | let ex = expectation(description: "") 112 | Promise.value([1, 2]).flatMap { 113 | [$0 + 1, $0 + 2] 114 | }.done { 115 | XCTAssertEqual($0, [2, 3, 3, 4]) 116 | ex.fulfill() 117 | }.silenceWarning() 118 | wait(for: [ex], timeout: 10) 119 | } 120 | #endif 121 | 122 | func testSequenceFilter() { 123 | let ex = expectation(description: "") 124 | Promise.value([0, 1, 2, 3]).filter { 125 | $0 < 2 126 | }.done { 127 | XCTAssertEqual($0, [0, 1]) 128 | ex.fulfill() 129 | }.silenceWarning() 130 | wait(for: [ex], timeout: 10) 131 | } 132 | 133 | func testSorted() { 134 | let ex = expectation(description: "") 135 | Promise.value([5, 2, 1, 8]).sorted().done { 136 | XCTAssertEqual($0, [1,2,5,8]) 137 | ex.fulfill() 138 | } 139 | wait(for: [ex], timeout: 10) 140 | } 141 | 142 | func testFirst() { 143 | XCTAssertEqual(Promise.value([1,2]).first.value, 1) 144 | } 145 | 146 | func testLast() { 147 | XCTAssertEqual(Promise.value([1,2]).last.value, 2) 148 | } 149 | 150 | func testPMKErrorFlatMap() { 151 | XCTAssertNotNil(PMKError.flatMap(1, Int.self).errorDescription) 152 | } 153 | } 154 | 155 | 156 | extension Promise { 157 | func silenceWarning() {} 158 | } 159 | -------------------------------------------------------------------------------- /Tests/JS-A+/.gitignore: -------------------------------------------------------------------------------- 1 | # From https://github.com/github/gitignore/blob/master/Node.gitignore 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # Typescript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # next.js build output 63 | .next 64 | -------------------------------------------------------------------------------- /Tests/JS-A+/AllTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AllTests.swift 3 | // PMKJSA+Tests 4 | // 5 | // Created by Lois Di Qual on 2/28/18. 6 | // 7 | 8 | #if swift(>=3.2) && !os(watchOS) 9 | 10 | import XCTest 11 | import PromiseKit 12 | import JavaScriptCore 13 | 14 | class AllTests: XCTestCase { 15 | 16 | func testAll() { 17 | 18 | let scriptPath = URL(fileURLWithPath: #file).deletingLastPathComponent().appendingPathComponent("build/build.js") 19 | guard FileManager.default.fileExists(atPath: scriptPath.path) else { 20 | return print("Skipping JS-A+: see README for instructions on how to build") 21 | } 22 | 23 | guard let script = try? String(contentsOf: scriptPath) else { 24 | return XCTFail("Couldn't read content of test suite JS file") 25 | } 26 | 27 | let context = JSUtils.sharedContext 28 | 29 | // Add a global exception handler 30 | context.exceptionHandler = { context, exception in 31 | guard let exception = exception else { 32 | return XCTFail("Unknown JS exception") 33 | } 34 | JSUtils.printStackTrace(exception: exception, includeExceptionDescription: true) 35 | } 36 | 37 | // Setup mock functions (timers, console.log, etc) 38 | let environment = MockNodeEnvironment() 39 | environment.setup(with: context) 40 | 41 | // Expose JSPromise in the javascript context 42 | context.setObject(JSPromise.self, forKeyedSubscript: "JSPromise" as NSString) 43 | 44 | // Create adapter 45 | guard let adapter = JSValue(object: NSDictionary(), in: context) else { 46 | fatalError("Couldn't create adapter") 47 | } 48 | adapter.setObject(JSAdapter.resolved, forKeyedSubscript: "resolved" as NSString) 49 | adapter.setObject(JSAdapter.rejected, forKeyedSubscript: "rejected" as NSString) 50 | adapter.setObject(JSAdapter.deferred, forKeyedSubscript: "deferred" as NSString) 51 | 52 | // Evaluate contents of `build.js`, which exposes `runTests` in the global context 53 | context.evaluateScript(script) 54 | guard let runTests = context.objectForKeyedSubscript("runTests") else { 55 | return XCTFail("Couldn't find `runTests` in JS context") 56 | } 57 | 58 | // Create a callback that's called whenever there's a failure 59 | let onFail: @convention(block) (JSValue, JSValue) -> Void = { test, error in 60 | guard let test = test.toString(), let error = error.toString() else { 61 | return XCTFail("Unknown test failure") 62 | } 63 | XCTFail("\(test) failed: \(error)") 64 | } 65 | let onFailValue: JSValue = JSValue(object: onFail, in: context) 66 | 67 | // Create a new callback that we'll send to `runTest` so that it notifies when tests are done running. 68 | let expectation = self.expectation(description: "async") 69 | let onDone: @convention(block) (JSValue) -> Void = { failures in 70 | expectation.fulfill() 71 | } 72 | let onDoneValue: JSValue = JSValue(object: onDone, in: context) 73 | 74 | // If there's a need to only run one specific test, uncomment the next line and comment the one after 75 | // let testName: JSValue = JSValue(object: "2.3.1", in: context) 76 | let testName = JSUtils.undefined 77 | 78 | // Call `runTests` 79 | runTests.call(withArguments: [adapter, onFailValue, onDoneValue, testName]) 80 | self.wait(for: [expectation], timeout: 60) 81 | } 82 | } 83 | 84 | #endif 85 | -------------------------------------------------------------------------------- /Tests/JS-A+/JSAdapter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSAdapter.swift 3 | // PMKJSA+Tests 4 | // 5 | // Created by Lois Di Qual on 3/2/18. 6 | // 7 | 8 | #if !os(watchOS) 9 | 10 | import Foundation 11 | import JavaScriptCore 12 | import PromiseKit 13 | 14 | enum JSAdapter { 15 | 16 | static let resolved: @convention(block) (JSValue) -> JSPromise = { value in 17 | return JSPromise(promise: .value(value)) 18 | } 19 | 20 | static let rejected: @convention(block) (JSValue) -> JSPromise = { reason in 21 | let error = JSUtils.JSError(reason: reason) 22 | let promise = Promise<JSValue>(error: error) 23 | return JSPromise(promise: promise) 24 | } 25 | 26 | static let deferred: @convention(block) () -> JSValue = { 27 | 28 | let context = JSContext.current() 29 | 30 | guard let object = JSValue(object: NSDictionary(), in: context) else { 31 | fatalError("Couldn't create object") 32 | } 33 | 34 | let pendingPromise = Promise<JSValue>.pending() 35 | let jsPromise = JSPromise(promise: pendingPromise.promise) 36 | 37 | // promise 38 | object.setObject(jsPromise, forKeyedSubscript: "promise" as NSString) 39 | 40 | // resolve 41 | let resolve: @convention(block) (JSValue) -> Void = { value in 42 | pendingPromise.resolver.fulfill(value) 43 | } 44 | object.setObject(resolve, forKeyedSubscript: "resolve" as NSString) 45 | 46 | // reject 47 | let reject: @convention(block) (JSValue) -> Void = { reason in 48 | let error = JSUtils.JSError(reason: reason) 49 | pendingPromise.resolver.reject(error) 50 | } 51 | object.setObject(reject, forKeyedSubscript: "reject" as NSString) 52 | 53 | return object 54 | } 55 | } 56 | 57 | #endif 58 | -------------------------------------------------------------------------------- /Tests/JS-A+/JSPromise.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSPromise.swift 3 | // PMKJSA+Tests 4 | // 5 | // Created by Lois Di Qual on 3/1/18. 6 | // 7 | 8 | #if !os(watchOS) 9 | 10 | import Foundation 11 | import XCTest 12 | import PromiseKit 13 | import JavaScriptCore 14 | 15 | @objc protocol JSPromiseProtocol: JSExport { 16 | func then(_: JSValue, _: JSValue) -> JSPromise 17 | } 18 | 19 | class JSPromise: NSObject, JSPromiseProtocol { 20 | 21 | let promise: Promise<JSValue> 22 | 23 | init(promise: Promise<JSValue>) { 24 | self.promise = promise 25 | } 26 | 27 | func then(_ onFulfilled: JSValue, _ onRejected: JSValue) -> JSPromise { 28 | 29 | // Keep a reference to the returned promise so we can comply to 2.3.1 30 | var returnedPromiseRef: Promise<JSValue>? 31 | 32 | let afterFulfill = promise.then { value -> Promise<JSValue> in 33 | 34 | // 2.2.1: ignored if not a function 35 | guard JSUtils.isFunction(value: onFulfilled) else { 36 | return .value(value) 37 | } 38 | 39 | // Call `onFulfilled` 40 | // 2.2.5: onFulfilled/onRejected must be called as functions (with no `this` value) 41 | guard let returnValue = try JSUtils.call(function: onFulfilled, arguments: [JSUtils.undefined, value]) else { 42 | return .value(value) 43 | } 44 | 45 | // Extract JSPromise.promise if available, or use plain return value 46 | if let jsPromise = returnValue.toObjectOf(JSPromise.self) as? JSPromise { 47 | 48 | // 2.3.1: if returned value is the promise that `then` returned, throw TypeError 49 | if jsPromise.promise === returnedPromiseRef { 50 | throw JSUtils.JSError(reason: JSUtils.typeError(message: "Returned self")) 51 | } 52 | return jsPromise.promise 53 | } else { 54 | return .value(returnValue) 55 | } 56 | } 57 | 58 | let afterReject = promise.recover { error -> Promise<JSValue> in 59 | 60 | // 2.2.1: ignored if not a function 61 | guard let jsError = error as? JSUtils.JSError, JSUtils.isFunction(value: onRejected) else { 62 | throw error 63 | } 64 | 65 | // Call `onRejected` 66 | // 2.2.5: onFulfilled/onRejected must be called as functions (with no `this` value) 67 | guard let returnValue = try JSUtils.call(function: onRejected, arguments: [JSUtils.undefined, jsError.reason]) else { 68 | throw error 69 | } 70 | 71 | // Extract JSPromise.promise if available, or use plain return value 72 | if let jsPromise = returnValue.toObjectOf(JSPromise.self) as? JSPromise { 73 | 74 | // 2.3.1: if returned value is the promise that `then` returned, throw TypeError 75 | if jsPromise.promise === returnedPromiseRef { 76 | throw JSUtils.JSError(reason: JSUtils.typeError(message: "Returned self")) 77 | } 78 | return jsPromise.promise 79 | } else { 80 | return .value(returnValue) 81 | } 82 | } 83 | 84 | let newPromise = Promise<Result<JSValue>> { resolver in 85 | _ = promise.tap(resolver.fulfill) 86 | }.then(on: nil) { result -> Promise<JSValue> in 87 | switch result { 88 | case .fulfilled: return afterFulfill 89 | case .rejected: return afterReject 90 | } 91 | } 92 | returnedPromiseRef = newPromise 93 | 94 | return JSPromise(promise: newPromise) 95 | } 96 | } 97 | 98 | #endif 99 | -------------------------------------------------------------------------------- /Tests/JS-A+/JSUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSUtils.swift 3 | // PMKJSA+Tests 4 | // 5 | // Created by Lois Di Qual on 3/2/18. 6 | // 7 | 8 | #if !os(watchOS) 9 | 10 | import Foundation 11 | import JavaScriptCore 12 | 13 | enum JSUtils { 14 | 15 | class JSError: Error { 16 | let reason: JSValue 17 | init(reason: JSValue) { 18 | self.reason = reason 19 | } 20 | } 21 | 22 | static let sharedContext: JSContext = { 23 | guard let context = JSContext() else { 24 | fatalError("Couldn't create JS context") 25 | } 26 | return context 27 | }() 28 | 29 | static var undefined: JSValue { 30 | guard let undefined = JSValue(undefinedIn: JSUtils.sharedContext) else { 31 | fatalError("Couldn't create `undefined` value") 32 | } 33 | return undefined 34 | } 35 | 36 | static func typeError(message: String) -> JSValue { 37 | let message = message.replacingOccurrences(of: "\"", with: "\\\"") 38 | let script = "new TypeError(\"\(message)\")" 39 | guard let result = sharedContext.evaluateScript(script) else { 40 | fatalError("Couldn't create TypeError") 41 | } 42 | return result 43 | } 44 | 45 | // @warning: relies on lodash to be present 46 | static func isFunction(value: JSValue) -> Bool { 47 | guard let context = value.context else { 48 | return false 49 | } 50 | guard let lodash = context.objectForKeyedSubscript("_") else { 51 | fatalError("Couldn't get lodash in JS context") 52 | } 53 | guard let result = lodash.invokeMethod("isFunction", withArguments: [value]) else { 54 | fatalError("Couldn't invoke _.isFunction") 55 | } 56 | return result.toBool() 57 | } 58 | 59 | // Calls a JS function using `Function.prototype.call` and throws any potential exception wrapped in a JSError 60 | static func call(function: JSValue, arguments: [JSValue]) throws -> JSValue? { 61 | 62 | let context = JSUtils.sharedContext 63 | 64 | // Create a new exception handler that will store a potential exception 65 | // thrown in the handler. Save the value of the old handler. 66 | var caughtException: JSValue? 67 | let savedExceptionHandler = context.exceptionHandler 68 | context.exceptionHandler = { context, exception in 69 | caughtException = exception 70 | } 71 | 72 | // Call the handler 73 | let returnValue = function.invokeMethod("call", withArguments: arguments) 74 | context.exceptionHandler = savedExceptionHandler 75 | 76 | // If an exception was caught, throw it 77 | if let exception = caughtException { 78 | throw JSError(reason: exception) 79 | } 80 | 81 | return returnValue 82 | } 83 | 84 | static func printCurrentStackTrace() { 85 | guard let exception = JSUtils.sharedContext.evaluateScript("new Error()") else { 86 | return print("Couldn't get current stack trace") 87 | } 88 | printStackTrace(exception: exception, includeExceptionDescription: false) 89 | } 90 | 91 | static func printStackTrace(exception: JSValue, includeExceptionDescription: Bool) { 92 | guard let lineNumber = exception.objectForKeyedSubscript("line"), 93 | let column = exception.objectForKeyedSubscript("column"), 94 | let message = exception.objectForKeyedSubscript("message"), 95 | let stacktrace = exception.objectForKeyedSubscript("stack")?.toString() else { 96 | return print("Couldn't print stack trace") 97 | } 98 | 99 | if includeExceptionDescription { 100 | print("JS Exception at \(lineNumber):\(column): \(message)") 101 | } 102 | 103 | let lines = stacktrace.split(separator: "\n").map { "\t> \($0)" }.joined(separator: "\n") 104 | print(lines) 105 | } 106 | } 107 | 108 | #if !swift(>=3.2) 109 | extension String { 110 | func split(separator: Character, omittingEmptySubsequences: Bool = true) -> [String] { 111 | return characters.split(separator: separator, omittingEmptySubsequences: omittingEmptySubsequences).map(String.init) 112 | } 113 | 114 | var first: Character? { 115 | return characters.first 116 | } 117 | } 118 | #endif 119 | 120 | #endif 121 | -------------------------------------------------------------------------------- /Tests/JS-A+/MockNodeEnvironment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockNodeEnvironment.swift 3 | // PMKJSA+Tests 4 | // 5 | // Created by Lois Di Qual on 3/1/18. 6 | // 7 | 8 | #if swift(>=3.2) && !os(watchOS) 9 | 10 | import Foundation 11 | import JavaScriptCore 12 | 13 | class MockNodeEnvironment { 14 | 15 | private var timers: [UInt32: Timer] = [:] 16 | 17 | func setup(with context: JSContext) { 18 | 19 | // console.log / console.error 20 | setupConsole(context: context) 21 | 22 | // setTimeout 23 | let setTimeout: @convention(block) (JSValue, Double) -> UInt32 = { function, intervalMs in 24 | let timerID = self.addTimer(interval: intervalMs / 1000, repeats: false, function: function) 25 | return timerID 26 | } 27 | context.setObject(setTimeout, forKeyedSubscript: "setTimeout" as NSString) 28 | 29 | // clearTimeout 30 | let clearTimeout: @convention(block) (JSValue) -> Void = { timeoutID in 31 | guard timeoutID.isNumber else { 32 | return 33 | } 34 | self.removeTimer(timerID: timeoutID.toUInt32()) 35 | } 36 | context.setObject(clearTimeout, forKeyedSubscript: "clearTimeout" as NSString) 37 | 38 | // setInterval 39 | let setInterval: @convention(block) (JSValue, Double) -> UInt32 = { function, intervalMs in 40 | let timerID = self.addTimer(interval: intervalMs / 1000, repeats: true, function: function) 41 | return timerID 42 | } 43 | context.setObject(setInterval, forKeyedSubscript: "setInterval" as NSString) 44 | 45 | // clearInterval 46 | let clearInterval: @convention(block) (JSValue) -> Void = { intervalID in 47 | guard intervalID.isNumber else { 48 | return 49 | } 50 | self.removeTimer(timerID: intervalID.toUInt32()) 51 | } 52 | context.setObject(clearInterval, forKeyedSubscript: "clearInterval" as NSString) 53 | } 54 | 55 | private func setupConsole(context: JSContext) { 56 | 57 | guard let console = context.objectForKeyedSubscript("console") else { 58 | fatalError("Couldn't get global `console` object") 59 | } 60 | 61 | let consoleLog: @convention(block) () -> Void = { 62 | guard let arguments = JSContext.currentArguments(), let format = arguments.first as? JSValue else { 63 | return 64 | } 65 | 66 | let otherArguments = arguments.dropFirst() 67 | if otherArguments.count == 0 { 68 | print(format) 69 | } else { 70 | 71 | let otherArguments = otherArguments.compactMap { $0 as? JSValue } 72 | let format = format.toString().replacingOccurrences(of: "%s", with: "%@") 73 | let expectedTypes = format.split(separator: "%", omittingEmptySubsequences: false).dropFirst().compactMap { $0.first }.map { String($0) } 74 | 75 | let typedArguments = otherArguments.enumerated().compactMap { index, value -> CVarArg? in 76 | let expectedType = expectedTypes[index] 77 | let converted: CVarArg 78 | switch expectedType { 79 | case "s": converted = value.toString() 80 | case "d": converted = value.toInt32() 81 | case "f": converted = value.toDouble() 82 | default: converted = value.toString() 83 | } 84 | return converted 85 | } 86 | 87 | let output = String(format: format, arguments: typedArguments) 88 | print(output) 89 | } 90 | } 91 | console.setObject(consoleLog, forKeyedSubscript: "log" as NSString) 92 | console.setObject(consoleLog, forKeyedSubscript: "error" as NSString) 93 | } 94 | 95 | private func addTimer(interval: TimeInterval, repeats: Bool, function: JSValue) -> UInt32 { 96 | let block = BlockOperation { 97 | DispatchQueue.main.async { 98 | function.call(withArguments: []) 99 | } 100 | } 101 | let timer = Timer.scheduledTimer(timeInterval: interval, target: block, selector: #selector(Operation.main), userInfo: nil, repeats: repeats) 102 | let rawHash = UUID().uuidString.hashValue 103 | #if swift(>=4.0) 104 | let hash = UInt32(truncatingIfNeeded: rawHash) 105 | #else 106 | let hash = UInt32(truncatingBitPattern: rawHash) 107 | #endif 108 | timers[hash] = timer 109 | return hash 110 | } 111 | 112 | private func removeTimer(timerID: UInt32) { 113 | guard let timer = timers[timerID] else { 114 | return print("Couldn't find timer \(timerID)") 115 | } 116 | timer.invalidate() 117 | timers[timerID] = nil 118 | } 119 | } 120 | 121 | 122 | #if swift(>=4.0) && !swift(>=4.1) || !swift(>=3.3) 123 | extension Sequence { 124 | func compactMap<T>(_ transform: (Self.Element) throws -> T?) rethrows -> [T] { 125 | return try flatMap(transform) 126 | } 127 | } 128 | #endif 129 | #endif 130 | -------------------------------------------------------------------------------- /Tests/JS-A+/README.md: -------------------------------------------------------------------------------- 1 | Promises/A+ Compliance Test Suite (JavaScript) 2 | ============================================== 3 | 4 | What is this? 5 | ------------- 6 | 7 | This contains the necessary Swift and JS files to run the Promises/A+ compliance test suite from PromiseKit's unit tests. 8 | 9 | - Promise/A+ Spec: <https://promisesaplus.com/> 10 | - Compliance Test Suite: <https://github.com/promises-aplus/promises-tests> 11 | 12 | Run tests 13 | --------- 14 | 15 | ``` 16 | $ npm install 17 | $ npm run build 18 | ``` 19 | 20 | then open `PromiseKit.xcodeproj` and run the `PMKJSA+Tests` unit test scheme. 21 | 22 | Known limitations 23 | ----------------- 24 | 25 | See `ignoredTests` in `index.js`. 26 | 27 | 28 | - 2.3.3 is disabled: Otherwise, if x is an object or function. This spec is a NOOP for Swift: 29 | - We have decided not to interact with other Promises A+ implementations 30 | - functions cannot have properties 31 | 32 | Upgrade the test suite 33 | ---------------------- 34 | 35 | ``` 36 | $ npm install --save promises-aplus-tests@latest 37 | $ npm run build 38 | ``` 39 | 40 | Develop 41 | ------- 42 | 43 | JavaScriptCore is a bit tedious to work with so here are a couple tips in case you're trying to debug the test suite. 44 | 45 | If you're editing JS files, enable live rebuilds: 46 | 47 | ``` 48 | $ npm run watch 49 | ``` 50 | 51 | If you're editing Swift files, a couple things you can do: 52 | 53 | - You can adjust `testName` in `AllTests.swift` to only run one test suite 54 | - You can call `JSUtils.printCurrentStackTrace()` at any time. It won't contain line numbers but some of the frame names might help. 55 | 56 | How it works 57 | ------------ 58 | 59 | The Promises/A+ test suite is written in JavaScript but PromiseKit is written in Swift/ObjC. For the test suite to run against swift code, we expose a promise wrapper `JSPromise` inside a JavaScriptCore context. This is done in a regular XCTestCase. 60 | 61 | Since JavaScriptCore doesn't support CommonJS imports, we inline all the JavaScript code into `build/build.js` using webpack. This includes all the npm dependencies (`promises-aplus-tests`, `mocha`, `sinon`, etc) as well as the glue code in `index.js`. 62 | 63 | `build.js` exposes one global variable `runTests(adapter, onFail, onDone, [testName])`. In our XCTestCase, a shared JavaScriptCore context is created, `build.js` is evaluated and now `runTests` is accessible from the Swift context. 64 | 65 | In our swift test, we create a JS-bridged `JSPromise` which only has one method `then(onFulfilled, onRejected) -> Promise`. It wraps a swift `Promise` and delegates call `then` calls to it. 66 | 67 | An [adapter](https://github.com/promises-aplus/promises-tests#adapters) – plain JS object which provides `revoled(value), rejected(reason), and deferred()` – is passed to `runTests` to run the whole JavaScript test suite. 68 | 69 | Errors and end events are reported back to Swift and piped to `XCTFail()` if necessary. 70 | 71 | Since JavaScriptCore isn't a node/web environment, there is quite a bit of stubbing necessary for all this to work: 72 | 73 | - The `fs` module is stubbed with an empty function 74 | - `console.log` redirects to `Swift.print` and provides only basic format parsing 75 | - `setTimeout/setInterval` are implemented with `Swift.Timer` behind the scenes and stored in a `[TimerID: Timer]` map. 76 | -------------------------------------------------------------------------------- /Tests/JS-A+/index.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | require('mocha') 3 | 4 | // Ignored by design 5 | const ignoredTests = [ 6 | '2.3.3' 7 | ] 8 | 9 | module.exports = function(adapter, onFail, onDone, testName) { 10 | 11 | global.adapter = adapter 12 | const mocha = new Mocha({ ui: 'bdd' }) 13 | 14 | // Require all tests 15 | console.log('Loading test files') 16 | const requireTest = require.context('promises-aplus-tests/lib/tests', false, /\.js$/) 17 | requireTest.keys().forEach(file => { 18 | 19 | let currentTestName = _.replace(_.replace(file, './', ''), '.js', '') 20 | if (testName && currentTestName !== testName) { 21 | return 22 | } 23 | 24 | if (_.includes(ignoredTests, currentTestName)) { 25 | return 26 | } 27 | 28 | console.log(`\t${currentTestName}`) 29 | mocha.suite.emit('pre-require', global, file, mocha) 30 | mocha.suite.emit('require', requireTest(file), file, mocha) 31 | mocha.suite.emit('post-require', global, file, mocha) 32 | }) 33 | 34 | const runner = mocha.run(failures => { 35 | onDone(failures) 36 | }) 37 | 38 | runner.on('fail', (test, err) => { 39 | console.error(err) 40 | onFail(test.title, err) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /Tests/JS-A+/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "webpack-cli", 4 | "watch": "webpack-cli --watch --mode development" 5 | }, 6 | "dependencies": { 7 | "babel-core": "^6.26.0", 8 | "babel-loader": "^7.1.3", 9 | "babel-preset-env": "^1.6.1", 10 | "lodash": "^4.17.21", 11 | "mocha": "^5.0.1", 12 | "promises-aplus-tests": "^2.1.2", 13 | "sinon": "^4.4.2", 14 | "webpack": "^4.0.1", 15 | "webpack-cli": "^2.0.9" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/JS-A+/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | context: __dirname, 6 | entry: './index.js', 7 | output: { 8 | path: __dirname + '/build', 9 | filename: 'build.js', 10 | library: 'runTests' 11 | }, 12 | stats: { 13 | warnings: false 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.js$/, 19 | exclude: /(node_modules)/, 20 | use: { 21 | loader: 'babel-loader', 22 | options: { 23 | presets: ['env'] 24 | } 25 | } 26 | } 27 | ] 28 | }, 29 | node: { 30 | fs: 'empty' 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import APlus 4 | import CorePromise 5 | 6 | var tests = [XCTestCaseEntry]() 7 | tests += APlus.__allTests() 8 | tests += CorePromise.__allTests() 9 | 10 | XCTMain(tests) 11 | -------------------------------------------------------------------------------- /tea.yaml: -------------------------------------------------------------------------------- 1 | # https://tea.xyz/what-is-this-file 2 | # created with https://mash.pkgx.sh/mxcl/tea-register 3 | --- 4 | version: 1.0.0 5 | codeOwners: 6 | - '0x5E2DE4A68df811AAAD32d71fb065e6946fA5C8d9' # mxcl 7 | quorum: 1 8 | --------------------------------------------------------------------------------