72 Commits

Author SHA1 Message Date
Saleem Abdulrasool 1c6f02c7ed COpenCOmbineHelpers: add an explicit modulemap
Introduce an explicit modulemap as not all build systems will write out
a modulemap for you.  Add the explicit modulemap to allow building with
alternative build systems.
2023-10-20 09:15:55 +03:00
Sergej Jaskiewicz 8576f0d579 Prepare the 0.14.0 release 2023-04-23 18:11:38 +02:00
Sergej Jaskiewicz 8f8ef5057d Update for Xcode 14 2023-04-23 18:11:38 +02:00
Sergej Jaskiewicz ff31c43375 I forgot that we've fixed ObservableObject 2022-02-02 14:00:46 +03:00
Sergej Jaskiewicz 27d76e1fed Remove unneeded files 2022-02-01 21:44:46 +03:00
Sergej Jaskiewicz 5823523b72 Move 'Contributing' section from README to a separate file 2022-02-01 21:43:30 +03:00
Sergej Jaskiewicz 6f7c3c4b47 Remove Slack badge from README.md 2022-02-01 21:22:41 +03:00
Sergej Jaskiewicz 4d9a7d6a5b Mention 0.13.0 in the changelog 2022-02-01 21:22:09 +03:00
Sergej Jaskiewicz 9cf67e3637 Prepare for release 0.13.0 2022-02-01 21:17:50 +03:00
Sergej Jaskiewicz 3877609ba2 fixup! Fix TSan false positives on Ubuntu 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 734e7e39cb Make async tests more reliable 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 2085bb7593 Fix tests on Xcode 10.3 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 5b247a5a01 Fix TSan false positives on Ubuntu 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 64f436c748 Disable TSan when testing with Xcode 10 and 13 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz b4e6313814 Fix some data races in tests 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz a0cf895c8c Don't generate LinuxMain.swift on newer Swift versions 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz dec7d4a569 Bump swift-tools-version 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz fdc7550ff7 Fix SwiftLint, make it strict 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 135dc9a8ab Fix TSan tests on macOS 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 37a4fe400f Show GHA status in README 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 7b466153a6 Fix Swift 5.5 tests on Windows and Wasm 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz baac42a0ad Migrate macOS tests from CircleCI to GitHub Actions 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz fd05f5c8ff Migrate pod lib lint from CircleCI to GitHub Actions 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 8bfdcd4295 Migrate compatibility tests from CircleCI to GitHub Actions 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 90454807b4 WASM -> Wasm 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 77374fa820 Convert Windows GHA workflow to matrix 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 8eda9d7e3d Migrate Linux tests from CircleCI to GitHub Actions 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 35cfe51c72 Generate LinuxMain.swift on Windows 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 42c0fa02ae Disable WASM tests on Swift 5.5
They don't compile due to presence of async test methods
2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 999a29cdf9 Support async tests in discover_tests.py 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 36edf4819b Run WASM tests with Swift 5.5 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz dfac8a9da7 Add manifest specifically for Swift 5.4 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 070ed94d18 Fix CI config 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz ea8938db72 Add tests for Publisher concurrency extensions, fix implementation 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 4392b4610c Add tests for Future concurrency extensions 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz c96f2e300d Update availability annotations for concurrency extensions 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 94de7bae46 Update the list of supported platforms 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz ed1b06ba51 Test with Swift 5.5.1 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 4b2c87a0bb Update Future implementation 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 0243fd063d Enable concurrency only since Swift 5.5 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 4fed5e9a5a Simplify a helper in Package.swift 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 80a4915715 Enable testing with Swift 5.4 on WASM 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 4716805f12 Make it compile 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 5490ff9be9 Enable testing with Swift 5.5 on Windows 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz c911862a24 [Xcode 13] Implement async/await support for publishers (no tests yet) 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz f823f7b18c Introduce take() helper method 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 02d1494ce9 [Xcode 13] Fix implementation so tests pass 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz f69bf6af64 [Xcode 13] Update tests so they pass in Combine compatibility mode 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 866d837cdf [Xcode 13] Add new APIs to RemainingSwiftInterface.swift 2022-02-01 18:58:40 +03:00
Marcus Ficner 7d0a8cd6f8 Fix typo in Publishers.FlatMap.swift (#228) 2022-01-23 12:37:12 +00:00
Max Desiatov dfd3cdf890 Migrate SwiftLint checks from CircleCI to GHA (#226)
SwiftLint integration with Danger no longer works on CircleCI. I'm migrating it to GitHub Actions in a way that's known to work in other repositories.

* Create swiftlint.yml

* Update config.yml
2021-11-29 16:56:32 +01:00
ArthurChi ef0288e075 Implement Zip operator (#222)
Co-authored-by: Eric Patey <eric@patey.com>
Co-authored-by: Max Desiatov <max@desiatov.com>
Co-authored-by: ArthurChi <chijie@bytedance.com>
Co-Authored-By: Sergej Jaskiewicz <broadwaylamb@users.noreply.github.com>
2021-11-22 00:29:57 +01:00
Max Desiatov f219d6f6a5 Fix Slack invite link in README.md (#224)
Resolves #223.
2021-11-03 21:37:18 +01:00
Sergej Jaskiewicz 710dfa2715 Mention Windows in README.md 2021-09-24 16:26:09 +03:00
Sergej Jaskiewicz 791625ff3b Disable running tests on iOS 9.3
CircleCI have deprecated the image :(
2021-09-24 16:26:09 +03:00
Sergej Jaskiewicz 7e4cdde419 "Fix" Publishers.Breakpoint tests on Windows 2021-09-24 16:26:09 +03:00
Sergej Jaskiewicz 096e245d02 Support Windows threads in tests 2021-09-24 16:26:09 +03:00
Sergej Jaskiewicz 1879860f35 I'm so tired of the Swift team breaking things on non-Darwin platforms
https://forums.swift.org/t/formalizing-the-unavailability-of-core-foundation/40216
2021-09-24 16:26:09 +03:00
Sergej Jaskiewicz ace5778817 Support Windows in Package.swift 2021-09-24 16:26:09 +03:00
Sergej Jaskiewicz 12700a0500 Make COpenCombineHelpers buildable on Windows 2021-09-24 16:26:09 +03:00
Max Desiatov 6c8108f9dc Test with Windows on GitHub Actions 2021-09-24 16:26:09 +03:00
ArthurChi b27b2c31ce Subscribers reentrancy bugs fix (#211)
Co-authored-by: VassilyChi <chijie@bytedance.com>
2021-07-29 01:48:45 +03:00
Sergej Jaskiewicz 3d3adb564b Release the Suffix publisher in Concatenate's Inner 2021-07-29 01:48:45 +03:00
Sergej Jaskiewicz 925bee4af9 Fix reentrancy bugs in Subscribers.Assign 2021-07-29 01:48:45 +03:00
Sergej Jaskiewicz adcee8c14d Fix reentrancy bugs in Subscribers.Sink 2021-07-29 01:48:45 +03:00
dependabot[bot] 29126ac259 Bump addressable from 2.7.0 to 2.8.0 (#212) 2021-07-13 07:54:20 +00:00
Sergej Jaskiewicz bab8e08d2f Work around SwiftLint nested configuration bug
There is a bug introduced in SwiftLint 0.43.0 (?) when nested configurations don't work.
Nested configurations let us place additional .swiftlint.yml files in subdirectories that
specify rules that should only apply to that subdirectory. This is broken now.
2021-06-21 17:38:33 +03:00
Sergej Jaskiewicz 4060ee9f57 Fix compatibility with Xcode 12.5 toolchain and SDKs 2021-06-21 17:38:33 +03:00
Sergej Jaskiewicz 5996772433 Bump Xcode version for compatibility testing 2021-02-22 20:47:35 +03:00
Sergej Jaskiewicz cd45c77fac Implement Publishers.PrefixUntilOutput 2021-02-22 20:47:35 +03:00
Stuart Austin e618d179fe Add Publishers.Throttle implementation (#195)
* Publishers.Throttle implementation with tests

* Fix Throttle lint errors and removed expectation from throttle tests. Add additional test for cancelling a subscription before a scheduled value is emitted

* Fix VirtualTimeScheduler's executeSchedulesActions default deadline not being far enough into the future on 32-bit platforms.

* Fixed multiple lint errors

* Improve Publishers.Throttle code coverage by removing enum for pending emissions

* Additional Throttle test for cancelling a Subscriber when an output has been scheduled

* ThrottleTests now run on WASI
2021-02-18 13:56:55 +00:00
Marcus Scherer 4fa5f48c19 Fix typo (#204) 2021-02-08 19:41:49 +03:00
119 changed files with 7889 additions and 1608 deletions
-297
View File
@@ -1,297 +0,0 @@
macOS_tests_steps: &macOS_tests_steps
steps:
- checkout
- run:
name: Building and running tests in debug mode with coverage
command: |
make test-debug \
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
SWIFT_TEST_FLAGS="--enable-code-coverage --build-path .build-test-debug"
xcrun llvm-cov show \
-instr-profile=.build-test-debug/debug/codecov/default.profdata \
.build-test-debug/debug/OpenCombinePackageTests.xctest/Contents/MacOS/OpenCombinePackageTests \
> coverage.txt
- run:
name: Building and running tests in debug mode with TSan
command: |
make test-debug-sanitize-thread \
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
SWIFT_TEST_FLAGS="--build-path .build-test-debug-sanitize-thread"
- run:
name: Building and running tests in release mode
command: |
make test-release \
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
SWIFT_TEST_FLAGS="--build-path .build-test-release"
- run:
name: Generating Xcode project
command: make generate-xcodeproj SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors"
- run:
name: Building for testing on macOS 10.15.0 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild build-for-testing \
-scheme OpenCombine-Package \
-sdk macosx10.15 \
-derivedDataPath DerivedData \
| tee xcodebuild_build-for-testing.log \
| xcpretty
- store_artifacts:
path: xcodebuild_build-for-testing.log
- run:
name: Testing on macOS 10.15.0 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild test-without-building \
-scheme OpenCombine-Package \
-sdk macosx10.15 \
-derivedDataPath DerivedData \
| tee xcodebuild_test-without-building.log \
| xcpretty --report junit -o build/reports/results.xml
- store_artifacts:
path: xcodebuild_test-without-building.log
- store_test_results:
path: build/reports
- run:
name: Uploading code coverage
command: |
bash <(curl -s https://codecov.io/bash) -D DerivedData
ubuntu_tests_steps: &ubuntu_tests_steps
steps:
- checkout
- run:
name: Installing dependencies
command: |
apt update -y
apt upgrade -y
apt install -y curl python3.8
- run:
name: "Generating LinuxMain.swift"
command: python3.8 utils/discover_tests.py
- run:
name: Building and running tests in debug mode with coverage
command: |
make test-debug \
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
SWIFT_TEST_FLAGS="--enable-code-coverage \
--disable-index-store \
--build-path .build-test-debug"
llvm-cov show \
-instr-profile=.build-test-debug/debug/codecov/default.profdata \
.build-test-debug/debug/OpenCombinePackageTests.xctest \
> coverage.txt
- run:
name: Building and running tests in debug mode with TSan
command: |
make test-debug-sanitize-thread \
SWIFT_TEST_FLAGS="--disable-index-store \
--build-path .build-test-debug-sanitize-thread" \
- run:
name: Building and running tests in release mode
command: |
make test-release \
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
SWIFT_TEST_FLAGS="--build-path .build-test-release"
- run:
name: Uploading code coverage
command: |
bash <(curl -s https://codecov.io/bash)
version: 2
jobs:
"Execute tests on macOS 10.15.0 (Xcode 11.3.0, Swift 5.1.3)":
macos:
xcode: "11.3.0"
environment:
SWIFT_VERSION: "5.1.3"
<<: *macOS_tests_steps
"Execute tests on macOS 10.15.0 (Xcode 12.1.0, Swift 5.3.0)":
macos:
xcode: "12.1.0"
environment:
SWIFT_VERSION: "5.3.0"
<<: *macOS_tests_steps
"Execute compatibility tests on iOS 14.2 (Xcode 12.2.0, Swift 5.3.1)":
macos:
xcode: "12.2.0"
environment:
SWIFT_VERSION: "5.3.1"
steps:
- checkout
- run:
name: Generating Xcode project
command: make generate-compatibility-xcodeproj
- run:
name: Building for testing on iOS 14.2 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild build-for-testing \
-scheme OpenCombine-Package \
-destination "platform=iOS Simulator,name=iPhone 11,OS=14.2" \
-derivedDataPath DerivedData \
| tee xcodebuild_build-for-testing.log \
| xcpretty
- store_artifacts:
path: xcodebuild_build-for-testing.log
- run:
name: Testing against Combine on iOS 14.2 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild test-without-building \
-scheme OpenCombine-Package \
-destination "platform=iOS Simulator,name=iPhone 11,OS=14.2" \
-derivedDataPath DerivedData \
| tee xcodebuild_test-without-building.log \
| xcpretty --report junit -o build/reports/results.xml
- store_artifacts:
path: xcodebuild_test-without-building.log
- store_test_results:
path: build/reports
"Execute tests on iOS 9.3 (Xcode 10.2.1, Swift 5.0.1)":
macos:
xcode: "10.2.1"
environment:
BUNDLE_PATH: .bundle # path to install gems and use for caching
SWIFT_VERSION: "5.0.1"
steps:
- checkout
- run:
name: Installing gem dependencies
command: bundle install && bundle clean
- restore_cache:
keys:
- v1-simulator-cache-{{ arch }}
- run:
# CircleCI doesn't have an iOS 9 simulator, so we need to install it manually.
name: Installing iOS 9 simulator
command: |
bundle exec xcversion simulators --install="iOS 9.3"
bundle exec xcversion simulators
xcrun simctl list
- save_cache:
key: v1-simulator-cache-{{ arch }}
paths:
- ~/Library/Caches/XcodeInstall
- run:
name: Generating Xcode project
command: |
make generate-xcodeproj SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors"
xcodebuild -scheme OpenCombine-Package -showdestinations
- run:
name: Building for testing on iOS 9.3 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild build-for-testing \
-scheme OpenCombine-Package \
-destination "platform=iOS Simulator,name=iPhone 4s,OS=9.3" \
-derivedDataPath DerivedData \
| tee xcodebuild_build-for-testing.log \
| xcpretty
- store_artifacts:
path: xcodebuild_build-for-testing.log
- run:
name: Testing on iOS 9.3 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild test-without-building \
-scheme OpenCombine-Package \
-destination "platform=iOS Simulator,name=iPhone 4s,OS=9.3" \
-derivedDataPath DerivedData \
| tee xcodebuild_test-without-building.log \
| xcpretty --report junit -o build/reports/results.xml
- store_artifacts:
path: xcodebuild_test-without-building.log
- store_test_results:
path: build/reports
- run:
name: Uploading code coverage
command: |
bash <(curl -s https://codecov.io/bash) -D DerivedData
"Execute tests on Ubuntu 18.04 (Swift 5.0)":
docker:
- image: swift:5.0-bionic
environment:
SWIFT_VERSION: "5.0"
<<: *ubuntu_tests_steps
"Execute tests on Ubuntu 18.04 (Swift 5.1)":
docker:
- image: swift:5.1-bionic
environment:
SWIFT_VERSION: "5.1"
<<: *ubuntu_tests_steps
"Execute tests on Ubuntu 18.04 (Swift 5.2)":
docker:
- image: swift:5.2-bionic
environment:
SWIFT_VERSION: "5.2"
<<: *ubuntu_tests_steps
"Execute tests on Ubuntu 18.04 (Swift 5.3)":
docker:
- image: swift:5.3-bionic
environment:
SWIFT_VERSION: "5.3"
<<: *ubuntu_tests_steps
"Run SwiftLint and Danger":
macos:
xcode: "11.3.0"
environment:
HOMEBREW_NO_AUTO_UPDATE: "1"
steps:
- checkout
- run:
name: Install SwiftLint
command: |
brew install swiftlint
- run:
name: Install danger-swift
command: |
brew install danger/tap/danger-swift
- run:
name: Run danger-swift
command: danger-swift ci
"Run Pod spec lint":
macos:
xcode: "11.3.0"
environment:
HOMEBREW_NO_AUTO_UPDATE: "1"
steps:
- checkout
- run:
name: Pod lib lint
command: |
pod lib lint --allow-warnings --verbose
workflows:
version: 2
"OpenCombine: execute tests on macOS":
jobs:
- "Execute tests on macOS 10.15.0 (Xcode 11.3.0, Swift 5.1.3)"
- "Execute tests on macOS 10.15.0 (Xcode 12.1.0, Swift 5.3.0)"
"OpenCombine: execute compatibility tests":
jobs:
- "Execute compatibility tests on iOS 14.2 (Xcode 12.2.0, Swift 5.3.1)"
"OpenCombine: execute tests on iOS":
jobs:
- "Execute tests on iOS 9.3 (Xcode 10.2.1, Swift 5.0.1)"
"OpenCombine: execute tests on Linux":
jobs:
- "Execute tests on Ubuntu 18.04 (Swift 5.0)"
- "Execute tests on Ubuntu 18.04 (Swift 5.1)"
- "Execute tests on Ubuntu 18.04 (Swift 5.2)"
- "Execute tests on Ubuntu 18.04 (Swift 5.3)"
"OpenCombine: run SwiftLint and Danger":
jobs:
- "Run SwiftLint and Danger"
"OpenCombine: validate podspec files":
jobs:
- "Run Pod spec lint"
+16
View File
@@ -0,0 +1,16 @@
name: CocoaPods
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
validate_podspec:
name: Run pod lib lint
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- name: Run pod lib lint
run: pod lib lint --allow-warnings --verbose
+19
View File
@@ -0,0 +1,19 @@
name: Compatibility tests
on:
push:
branches: [master]
pull_request:
branches: [master]
schedule:
- cron: "0 9 * * 1" # Every Monday at 9:00 AM
jobs:
compatibility_tests_macos:
name: Execute compatibility tests
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- name: Run tests against Apple's Combine
run: make test-compatibility
+130
View File
@@ -0,0 +1,130 @@
name: macOS
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
# This job is not a part of the macos_test job because of
# the 'This copy of libswiftCore.dylib requires an OS version prior to 10.14.4.' error.
# We have to invoke install_name_tool and patch the test executable
# to work around this error.
#
# Other combinations of Xcode and macOS versions don't lead to this error.
swift_5_0_test:
name: Execute tests (macos-10.15, 10.3)
runs-on: macos-10.15
steps:
- uses: actions/checkout@v2
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: "10.3"
- name: Swift version
run: swift --version
- name: Build and run tests in debug mode with coverage
run: |
swift build \
--build-tests \
-c debug \
-Xswiftc -warnings-as-errors \
-Xswiftc -profile-generate \
-Xswiftc -profile-coverage-mapping \
--build-path .build-test-debug
install_name_tool \
-rpath /Applications/Xcode_10.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx /usr/lib/swift \
.build-test-debug/debug/OpenCombinePackageTests.xctest/Contents/MacOS/OpenCombinePackageTests
install_name_tool \
-add_rpath /Applications/Xcode_10.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx \
.build-test-debug/debug/OpenCombinePackageTests.xctest/Contents/MacOS/OpenCombinePackageTests
swift test \
--skip-build \
--enable-code-coverage \
--build-path .build-test-debug
xcrun llvm-cov show \
-instr-profile=.build-test-debug/debug/codecov/default.profdata \
.build-test-debug/debug/OpenCombinePackageTests.xctest/Contents/MacOS/OpenCombinePackageTests \
> coverage.txt
- name: Build and run tests in release mode
run: |
swift build \
--build-tests \
-c release \
-Xswiftc -warnings-as-errors \
-Xswiftc -profile-generate \
-Xswiftc -profile-coverage-mapping \
--build-path .build-test-release
install_name_tool \
-rpath /Applications/Xcode_10.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx /usr/lib/swift \
.build-test-release/release/OpenCombinePackageTests.xctest/Contents/MacOS/OpenCombinePackageTests
install_name_tool \
-add_rpath /Applications/Xcode_10.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx \
.build-test-release/release/OpenCombinePackageTests.xctest/Contents/MacOS/OpenCombinePackageTests
swift test \
--skip-build \
-c release \
--enable-code-coverage \
--build-path .build-test-release
- uses: codecov/codecov-action@v2
with:
verbose: true
macos_test:
name: Execute tests
strategy:
fail-fast: false
matrix:
include:
- os: macos-10.15
xcode-version: "11.3.1" # Swift 5.1.3
- os: macos-10.15
xcode-version: "11.7" # Swift 5.2.4
- os: macos-11
xcode-version: "12.4" # Swift 5.3.2
- os: macos-11
xcode-version: "12.5.1" # Swift 5.4.2
- os: macos-11
xcode-version: "13.2.1" # Swift 5.5.2
- os: macos-12
xcode-version: "13.4.1" # Swift 5.6.1
- os: macos-12
xcode-version: "14.2" # Swift 5.7.2
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: ${{ matrix.xcode-version }}
- name: Swift version
run: swift --version
- name: Build and run tests in debug mode with coverage
run: |
swift test \
-c debug \
-Xswiftc -warnings-as-errors \
--enable-code-coverage \
--build-path .build-test-debug
xcrun llvm-cov show \
-instr-profile=.build-test-debug/debug/codecov/default.profdata \
.build-test-debug/debug/OpenCombinePackageTests.xctest/Contents/MacOS/OpenCombinePackageTests \
> coverage.txt
- name: Build and run tests in debug mode with TSan
if: ${{ matrix.xcode-version != '13.2.1' && matrix.xcode-version != '13.4.1' }} # https://bugs.swift.org/browse/SR-15444
run: |
swift test \
-c debug \
--sanitize thread \
-Xswiftc -warnings-as-errors \
--build-path .build-test-debug-sanitize-thread
- name: Build and run tests in release mode
run: |
swift test \
-c release \
-Xswiftc -warnings-as-errors \
--enable-code-coverage \
--build-path .build-test-release
- uses: codecov/codecov-action@v2
with:
verbose: true
+26
View File
@@ -0,0 +1,26 @@
name: SwiftLint
on:
pull_request:
paths:
- ".github/workflows/swiftlint.yml"
- ".swiftlint.yml"
- "**/*.swift"
jobs:
SwiftLint:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v1
# Fetch current versions of files
- name: Fetch base ref
run: |
git fetch --prune --no-tags --depth=1 origin +refs/heads/${{ github.base_ref }}:refs/heads/${{ github.base_ref }}
# Diff pull request to current files, then SwiftLint changed files
- name: GitHub Action for SwiftLint
uses: mayk-it/action-swiftlint@3.2.2
env:
DIFF_BASE: ${{ github.base_ref }}
DIFF_HEAD: HEAD
with:
args: --strict
+57
View File
@@ -0,0 +1,57 @@
name: Ubuntu
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
ubuntu_test:
name: Execute tests on Ubuntu
strategy:
fail-fast: false
matrix:
swift_version: ["5.0", "5.1", "5.2", "5.3", "5.4", "5.5", "5.6", "5.7"]
runs-on: ubuntu-latest
container: swift:${{ matrix.swift_version }}-bionic
steps:
- uses: actions/checkout@v2
- name: Generating LinuxMain.swift
if: >-
${{ matrix.swift_version == '5.0' ||
matrix.swift_version == '5.1' ||
matrix.swift_version == '5.2' ||
matrix.swift_version == '5.3' }}
run: |
apt update -y
apt upgrade -y
apt install -y python3.8
python3.8 utils/discover_tests.py
- name: Building and running tests in debug mode with coverage
run: |
swift test \
-c debug \
-Xswiftc -warnings-as-errors \
--enable-code-coverage \
--build-path .build-test-debug
llvm-cov show \
-instr-profile=.build-test-debug/debug/codecov/default.profdata \
.build-test-debug/debug/OpenCombinePackageTests.xctest \
> coverage.txt
- name: Building and running tests in debug mode with TSan
if: ${{ matrix.swift_version != '5.0' }} # There are false positives there
run: |
swift test \
-c debug \
--sanitize thread \
--build-path .build-test-debug-sanitize-thread
- name: Building and running tests in release mode
run: |
swift test \
-c release \
-Xswiftc -warnings-as-errors \
--build-path .build-test-release
- uses: codecov/codecov-action@v2
with:
verbose: true
+34 -2
View File
@@ -1,4 +1,4 @@
name: SwiftWasm
name: Wasm
on:
push:
@@ -7,10 +7,42 @@ on:
branches: [master]
jobs:
carton_wasmer_test:
carton_wasmer_test_5_3:
name: "Execute tests on Wasm (Swift 5.3)"
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: swiftwasm/swiftwasm-action@v5.3
carton_wasmer_test_5_4:
name: "Execute tests on Wasm (Swift 5.4)"
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: swiftwasm/swiftwasm-action@v5.4
carton_wasmer_test_5_5:
name: "Execute tests on Wasm (Swift 5.5)"
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: swiftwasm/swiftwasm-action@v5.5
carton_wasmer_test_5_6:
name: "Execute tests on Wasm (Swift 5.6)"
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: swiftwasm/swiftwasm-action@v5.6
carton_wasmer_test_5_7:
name: "Execute tests on Wasm (Swift 5.7)"
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: swiftwasm/swiftwasm-action@v5.7
+32
View File
@@ -0,0 +1,32 @@
name: Windows
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
windows_test:
name: Execute tests on Windows
strategy:
fail-fast: false
matrix:
include:
- os: windows-2019
swift_version: "5.4.2"
- os: windows-2019
swift_version: "5.5.1"
- os: windows-2019
swift_version: "5.6.1"
- os: windows-2019
swift_version: "5.7.2"
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: compnerd/gha-setup-swift@main
with:
branch: swift-${{ matrix.swift_version }}-release
tag: ${{ matrix.swift_version }}-RELEASE
- name: Building and running tests in debug mode
run: swift test
+3
View File
@@ -2,7 +2,10 @@ included:
- Sources
- Tests
child_config: Tests/.swiftlint.yml
disabled_rules:
- blanket_disable_command
- block_based_kvo
- class_delegate_protocol
- colon
+24
View File
@@ -1,3 +1,27 @@
# 0.14.0 (23 Apr 2023)
This release is compatible with Xcode 14.2 and Swift 5.7
### Additions
- Primary associated type support for `Publisher`, `Subscriber`, `ConnectablePublisher`, `Subject` and `Scheduler` protocols (#239)
### Bugfixes
- Fixed nullifying the reference to parent in `Future`'s conduit (#239)
# 0.13.0 (1 Feb 2022)
This release is compatible with Xcode 13.2.
### Additions
- Windows support (thank you @MaxDesiatov!)
- `Publishers.Throttle` (#195, thank you @stuaustin)
- `Publishers.PrefixUntilOutput` (#206)
- `Publishers.Zip` (#222, thank you @MaxDesiatov and @ArthurChi)
- `async`/`await` extensions: `Future.value` and `Publisher.values` (#219)
### Bugfixes
- Fixed reentrancy bugs in ` Subscribers.Sink` and `Subscribers.Assign` (#210)
- Fixed lifecycle bugs in `Publishers.Concatenate` (#210)
# 0.12.0 (29 Jan 2021)
This release adds a new `OpenCombineShim` product that will conditionally re-export either
+60
View File
@@ -0,0 +1,60 @@
# Contributing
In order to work on this project you will need Xcode 10.2 and Swift 5.0 or later.
Please refer to the [issue #1](https://github.com/OpenCombine/OpenCombine/issues/1) for the list of operators that remain unimplemented, as well as the [RemainingCombineInterface.swift](https://github.com/OpenCombine/OpenCombine/blob/master/RemainingCombineInterface.swift) file. The latter contains the generated interface of Apple's Combine from the latest Xcode version. When the functionality is implemented in OpenCombine, it should be removed from the RemainingCombineInterface.swift file.
You can refer to [this repo](https://github.com/OpenCombine/combine-interfaces) to observe Apple's Combine API and documentation changes between different Xcode (beta) versions.
You can run compatibility tests against Apple's Combine. In order to do that you will need either macOS 10.14 with iOS 13 simulator installed (since the only way we can get Apple's Combine on macOS 10.14 is using the simulator), or macOS 10.15 (Apple's Combine is bundled with the OS). Execute the following command from the root of the package:
```
$ make test-compatibility
```
Or enable the `-DOPENCOMBINE_COMPATIBILITY_TEST` compiler flag in Xcode's build settings. Note that on iOS only the latter will work.
> NOTE: Before starting to work on some feature, please consult the [GitHub project](https://github.com/OpenCombine/OpenCombine/projects/2) to make sure that nobody's already making progress on the same feature! If not, then please create a draft PR to indicate that you're beginning your work.
#### Releasing a new version
1. Create a new branch from master and call it `release/<major>.<minor>.<patch>`.
1. Replace the usages of the old version in `README.md` with the new version (make sure to check the [Swift Package Manager](#swift-package-manager) and [CocoaPods](#cocoapods) sections).
1. Bump the version in `OpenCombine.podspec`, `OpenCombineDispatch.podspec` and `OpenCombineFoundation.podspec`. In the latter two you will also need to set the `spec.dependency "OpenCombine"` property to the **previous** version. Why? Because otherwise the `pod lib lint` command that we run on our regular CI will fail when validating the `OpenCombineDispatch` and `OpenCombineFoundation` podspecs, since the dependencies are not yet in the trunk. If we set the dependencies to the previous version (which is already in the trunk), everything will be fine. This is purely to make the CI work. The clients will not experience any issues, since the version is specified as `>=`.
1. Create a pull request to master for the release branch and make sure the CI passes.
1. Merge the pull request.
1. In the GitHub web interface on the [releases](https://github.com/OpenCombine/OpenCombine/releases) page, click the **Draft a new release** button.
1. The **Tag version** and **Release title** fields should be filled with the version number.
1. The description of the release should be consistent with the previous releases. It is a good practice to divide the description into several sections: additions, bugfixes, known issues etc. Also, be sure to mention the nicknames of the contributors of the new release.
1. Publish the release.
1. Switch to the master branch and pull the changes.
1. Push the release to CocoaPods trunk. For that, execute the following commands:
```
pod trunk push OpenCombine.podspec --verbose --allow-warnings
pod trunk push OpenCombineDispatch.podspec --verbose --allow-warnings
pod trunk push OpenCombineFoundation.podspec --verbose --allow-warnings
```
Note that you need to be one of the owners of the pod for that.
#### GYB
Some publishers in OpenCombine (like `Publishers.MapKeyPath`, `Publishers.Merge`) exist in several
different flavors in order to support several arities. For example, there are also `Publishers.MapKeyPath2`
and `Publishers.MapKeyPath3`, which are very similar but different enough that Swift's type system
can't help us here (because there's no support for variadic generics). Maintaining multiple instances of
those generic types is tedious and error-prone (they can get out of sync), so we use the GYB tool for
generating those instances from a template.
GYB is a Python script that evaluates Python code written inside a template file, so it's very flexible —
templates can be arbitrarily complex. There is a good article about GYB on
[NSHipster](https://nshipster.com/swift-gyb/).
GYB is part of the [Swift Open Source Project](https://github.com/apple/swift/blob/master/utils/gyb.py)
and can be distributed under the same license as Swift itself.
GYB template files have the `.gyb` extension. Run `make gyb` to generate Swift code from those
templates. The generated files are prefixed with `GENERATED-` and are checked into source control. Those
files should never be edited directly. Instead, the `.gyb` template should be edited, and after that the files
should be regenerated using `make gyb`.
-76
View File
@@ -1,76 +0,0 @@
import Danger
import Foundation
extension StringProtocol {
func dropSuffix<S: StringProtocol>(_ suffix: S) -> SubSequence {
if hasSuffix(suffix) {
return self[..<index(endIndex, offsetBy: -suffix.count)]
} else {
return self[...]
}
}
func directoryAndFileName() -> (SubSequence, SubSequence) {
let lastPathSeparator = lastIndex(of: "/")
if let lastPathSeparator = lastPathSeparator {
return (self[..<lastPathSeparator], self[index(after: lastPathSeparator)...])
} else {
return (".", self[...])
}
}
}
let danger = Danger()
let allCreatedAndModified = danger.git.createdFiles + danger.git.modifiedFiles
do {
// Fail if the committer modified a GYB template but forgot to run `make gyb`.
let modifiedTemplates = allCreatedAndModified.filter { $0.hasSuffix(".gyb") }
for modifiedTemplate in modifiedTemplates {
let (directory, filename) = modifiedTemplate.directoryAndFileName()
let generated = "\(directory)/GENERATED-\(filename.dropSuffix(".gyb"))"
if !allCreatedAndModified.contains(generated) {
fail("""
A template \(modifiedTemplate) was modified, but the file \(generated) \
was not regenerated.
Run `make gyb` from the root of the project and commit the changes.
""")
}
}
}
do {
// Fail if the committer modified a generated file.
// A template should be modified instead.
for modifiedGeneratedFile in danger.git.modifiedFiles
where modifiedGeneratedFile.contains("GENERATED-")
{
let template = modifiedGeneratedFile
.replacingOccurrences(of: "GENERATED-", with: "") + ".gyb"
if !danger.git.modifiedFiles.contains(template) {
fail("""
A generated file \(modifiedGeneratedFile) was modified, but \
the template it was generated from was not modified.
Please modify the template \(template) instead, \
run `make gyb` from the root of the project and commit the changes.
""")
}
}
}
SwiftLint.lint(inline: true,
configFile: ".swiftlint.yml",
strict: true,
lintAllFiles: true)
if danger.warnings.isEmpty, danger.fails.isEmpty {
markdown("LGTM")
}
-3
View File
@@ -1,3 +0,0 @@
source 'https://rubygems.org'
gem 'xcode-install'
-162
View File
@@ -1,162 +0,0 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.1)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
atomos (0.1.3)
babosa (1.0.3)
claide (1.0.3)
colored (1.2)
colored2 (3.1.2)
commander-fastlane (4.4.6)
highline (~> 1.7.2)
declarative (0.0.10)
declarative-option (0.1.0)
digest-crc (0.4.1)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.5)
emoji_regex (1.0.1)
excon (0.71.0)
faraday (0.17.0)
multipart-post (>= 1.2, < 3)
faraday-cookie_jar (0.0.6)
faraday (>= 0.7.4)
http-cookie (~> 1.0.0)
faraday_middleware (0.13.1)
faraday (>= 0.7.4, < 1.0)
fastimage (2.1.7)
fastlane (2.134.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
babosa (>= 1.0.2, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
commander-fastlane (>= 4.4.6, < 5.0.0)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 2.0)
excon (>= 0.45.0, < 1.0.0)
faraday (~> 0.17)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 0.13.1)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-api-client (>= 0.21.2, < 0.24.0)
google-cloud-storage (>= 1.15.0, < 2.0.0)
highline (>= 1.7.2, < 2.0.0)
json (< 3.0.0)
jwt (~> 2.1.0)
mini_magick (>= 4.9.4, < 5.0.0)
multi_xml (~> 0.5)
multipart-post (~> 2.0.0)
plist (>= 3.1.0, < 4.0.0)
public_suffix (~> 2.0.0)
rubyzip (>= 1.3.0, < 2.0.0)
security (= 0.1.3)
simctl (~> 1.6.3)
slack-notifier (>= 2.0.0, < 3.0.0)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (>= 1.4.5, < 2.0.0)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.8.1, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
gh_inspector (1.1.3)
google-api-client (0.23.9)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.5, < 0.7.0)
httpclient (>= 2.8.1, < 3.0)
mime-types (~> 3.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
signet (~> 0.9)
google-cloud-core (1.3.2)
google-cloud-env (~> 1.0)
google-cloud-env (1.2.1)
faraday (~> 0.11)
google-cloud-storage (1.16.0)
digest-crc (~> 0.4)
google-api-client (~> 0.23)
google-cloud-core (~> 1.2)
googleauth (>= 0.6.2, < 0.10.0)
googleauth (0.6.7)
faraday (~> 0.12)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (~> 0.7)
highline (1.7.10)
http-cookie (1.0.3)
domain_name (~> 0.5)
httpclient (2.8.3)
json (2.3.1)
jwt (2.1.0)
memoist (0.16.1)
mime-types (3.3)
mime-types-data (~> 3.2015)
mime-types-data (3.2019.1009)
mini_magick (4.9.5)
multi_json (1.14.1)
multi_xml (0.6.0)
multipart-post (2.0.0)
nanaimo (0.2.6)
naturally (2.2.0)
os (1.0.1)
plist (3.5.0)
public_suffix (2.0.5)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rouge (2.0.7)
rubyzip (1.3.0)
security (0.1.3)
signet (0.11.0)
addressable (~> 2.3)
faraday (~> 0.9)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.6)
CFPropertyList
naturally
slack-notifier (2.3.2)
terminal-notifier (2.0.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
tty-cursor (0.7.0)
tty-screen (0.7.0)
tty-spinner (0.9.1)
tty-cursor (~> 0.7)
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.6)
unicode-display_width (1.6.0)
word_wrap (1.0.0)
xcode-install (2.6.2)
claide (>= 0.9.1, < 1.1.0)
fastlane (>= 2.1.0, < 3.0.0)
xcodeproj (1.13.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.2.6)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.0)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
ruby
DEPENDENCIES
xcode-install
BUNDLED WITH
2.0.1
+3 -3
View File
@@ -1,17 +1,17 @@
Pod::Spec.new do |spec|
spec.name = "OpenCombine"
spec.version = "0.12.0"
spec.version = "0.14.0"
spec.summary = "Open source implementation of Apple's Combine framework for processing values over time."
spec.description = <<-DESC
An open source implementation of Apple's Combine framework for processing values over time.
DESC
spec.homepage = "https://github.com/broadwaylamb/OpenCombine/"
spec.homepage = "https://github.com/OpenCombine/OpenCombine/"
spec.license = "MIT"
spec.authors = { "Sergej Jaskiewicz" => "jaskiewiczs@icloud.com" }
spec.source = { :git => "https://github.com/broadwaylamb/OpenCombine.git", :tag => "#{spec.version}" }
spec.source = { :git => "https://github.com/OpenCombine/OpenCombine.git", :tag => "#{spec.version}" }
spec.swift_version = "5.0"
+4 -4
View File
@@ -1,17 +1,17 @@
Pod::Spec.new do |spec|
spec.name = "OpenCombineDispatch"
spec.version = "0.12.0"
spec.version = "0.14.0"
spec.summary = "OpenCombine + Dispatch interoperability"
spec.description = <<-DESC
Extends `DispatchQueue` with conformance to the `Scheduler` protocol
DESC
spec.homepage = "https://github.com/broadwaylamb/OpenCombine/"
spec.homepage = "https://github.com/OpenCombine/OpenCombine/"
spec.license = "MIT"
spec.authors = { "Sergej Jaskiewicz" => "jaskiewiczs@icloud.com" }
spec.source = { :git => "https://github.com/broadwaylamb/OpenCombine.git", :tag => "#{spec.version}" }
spec.source = { :git => "https://github.com/OpenCombine/OpenCombine.git", :tag => "#{spec.version}" }
spec.swift_version = "5.0"
@@ -21,5 +21,5 @@ Pod::Spec.new do |spec|
spec.tvos.deployment_target = "9.0"
spec.source_files = "Sources/OpenCombineDispatch/**/*.swift"
spec.dependency "OpenCombine", '>= 0.10.2'
spec.dependency "OpenCombine", '>= 0.13.0'
end
+4 -4
View File
@@ -1,17 +1,17 @@
Pod::Spec.new do |spec|
spec.name = "OpenCombineFoundation"
spec.version = "0.12.0"
spec.version = "0.14.0"
spec.summary = "OpenCombine + OpenCombineFoundation interoperability"
spec.description = <<-DESC
Adds publishers to Foundation types like NotificationCenter, URLSession etc.
DESC
spec.homepage = "https://github.com/broadwaylamb/OpenCombine/"
spec.homepage = "https://github.com/OpenCombine/OpenCombine/"
spec.license = "MIT"
spec.authors = { "Sergej Jaskiewicz" => "jaskiewiczs@icloud.com" }
spec.source = { :git => "https://github.com/broadwaylamb/OpenCombine.git", :tag => "#{spec.version}" }
spec.source = { :git => "https://github.com/OpenCombine/OpenCombine.git", :tag => "#{spec.version}" }
spec.swift_version = "5.0"
@@ -21,5 +21,5 @@ Pod::Spec.new do |spec|
spec.tvos.deployment_target = "9.0"
spec.source_files = "Sources/OpenCombineFoundation/**/*.swift"
spec.dependency "OpenCombine", '>= 0.10.2'
spec.dependency "OpenCombine", '>= 0.13.0'
end
+8 -9
View File
@@ -1,4 +1,4 @@
// swift-tools-version:5.3
// swift-tools-version:5.5
import PackageDescription
@@ -6,13 +6,14 @@ import PackageDescription
// See: https://bugs.swift.org/browse/SR-13814
let supportedPlatforms: [Platform] = [
.macOS,
.macCatalyst,
.iOS,
.watchOS,
.tvOS,
.driverKit,
.linux,
.android,
// Disable Windows because of https://bugs.swift.org/browse/SR-13817
// .windows,
.windows,
.wasi,
]
@@ -33,6 +34,8 @@ let package = Package(
condition: .when(platforms: supportedPlatforms.except([.wasi])))
],
exclude: [
"RootProtocols.swift.gyb",
"Concurrency/Publisher+Concurrency.swift.gyb",
"Publishers/Publishers.Encode.swift.gyb",
"Publishers/Publishers.MapKeyPath.swift.gyb",
"Publishers/Publishers.Catch.swift.gyb"
@@ -73,17 +76,13 @@ let package = Package(
]
)
],
cxxLanguageStandard: .cxx1z
cxxLanguageStandard: .cxx17
)
// MARK: Helpers
extension Array where Element == Platform {
func except(_ exceptions: [Platform]) -> [Platform] {
// See: https://bugs.swift.org/browse/SR-13813
let exceptionsDescriptions = exceptions.map(String.init(describing:))
return filter { platform in
!exceptionsDescriptions.contains(String(describing: platform))
}
return filter { !exceptions.contains($0) }
}
}
+91
View File
@@ -0,0 +1,91 @@
// swift-tools-version:5.3
import PackageDescription
// This list should be updated whenever SwiftPM adds support for a new platform.
// See: https://bugs.swift.org/browse/SR-13814
let supportedPlatforms: [Platform] = [
.macOS,
.iOS,
.watchOS,
.tvOS,
.linux,
.android,
// Disable Windows because of https://bugs.swift.org/browse/SR-13817
// .windows,
.wasi,
]
let package = Package(
name: "OpenCombine",
products: [
.library(name: "OpenCombine", targets: ["OpenCombine"]),
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
.library(name: "OpenCombineFoundation", targets: ["OpenCombineFoundation"]),
.library(name: "OpenCombineShim", targets: ["OpenCombineShim"]),
],
targets: [
.target(name: "COpenCombineHelpers"),
.target(
name: "OpenCombine",
dependencies: [
.target(name: "COpenCombineHelpers",
condition: .when(platforms: supportedPlatforms.except([.wasi])))
],
exclude: [
"RootProtocols.swift.gyb",
"Concurrency/Publisher+Concurrency.swift.gyb",
"Publishers/Publishers.Encode.swift.gyb",
"Publishers/Publishers.MapKeyPath.swift.gyb",
"Publishers/Publishers.Catch.swift.gyb"
],
swiftSettings: [.define("WASI", .when(platforms: [.wasi]))]
),
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
.target(
name: "OpenCombineFoundation",
dependencies: [
"OpenCombine",
.target(name: "COpenCombineHelpers",
condition: .when(platforms: supportedPlatforms.except([.wasi])))
]
),
.target(
name: "OpenCombineShim",
dependencies: [
"OpenCombine",
.target(name: "OpenCombineDispatch",
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
.target(name: "OpenCombineFoundation",
condition: .when(platforms: supportedPlatforms.except([.wasi])))
]
),
.testTarget(
name: "OpenCombineTests",
dependencies: [
"OpenCombine",
.target(name: "OpenCombineDispatch",
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
.target(name: "OpenCombineFoundation",
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
],
swiftSettings: [
.unsafeFlags(["-enable-testing"]),
.define("WASI", .when(platforms: [.wasi]))
]
)
],
cxxLanguageStandard: .cxx1z
)
// MARK: Helpers
extension Array where Element == Platform {
func except(_ exceptions: [Platform]) -> [Platform] {
// See: https://bugs.swift.org/browse/SR-13813
let exceptionsDescriptions = exceptions.map(String.init(describing:))
return filter { platform in
!exceptionsDescriptions.contains(String(describing: platform))
}
}
}
+86
View File
@@ -0,0 +1,86 @@
// swift-tools-version:5.4
import PackageDescription
// This list should be updated whenever SwiftPM adds support for a new platform.
// See: https://bugs.swift.org/browse/SR-13814
let supportedPlatforms: [Platform] = [
.macOS,
.iOS,
.watchOS,
.tvOS,
.linux,
.android,
.windows,
.wasi,
]
let package = Package(
name: "OpenCombine",
products: [
.library(name: "OpenCombine", targets: ["OpenCombine"]),
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
.library(name: "OpenCombineFoundation", targets: ["OpenCombineFoundation"]),
.library(name: "OpenCombineShim", targets: ["OpenCombineShim"]),
],
targets: [
.target(name: "COpenCombineHelpers"),
.target(
name: "OpenCombine",
dependencies: [
.target(name: "COpenCombineHelpers",
condition: .when(platforms: supportedPlatforms.except([.wasi])))
],
exclude: [
"RootProtocols.swift.gyb",
"Concurrency/Publisher+Concurrency.swift.gyb",
"Publishers/Publishers.Encode.swift.gyb",
"Publishers/Publishers.MapKeyPath.swift.gyb",
"Publishers/Publishers.Catch.swift.gyb"
],
swiftSettings: [.define("WASI", .when(platforms: [.wasi]))]
),
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
.target(
name: "OpenCombineFoundation",
dependencies: [
"OpenCombine",
.target(name: "COpenCombineHelpers",
condition: .when(platforms: supportedPlatforms.except([.wasi])))
]
),
.target(
name: "OpenCombineShim",
dependencies: [
"OpenCombine",
.target(name: "OpenCombineDispatch",
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
.target(name: "OpenCombineFoundation",
condition: .when(platforms: supportedPlatforms.except([.wasi])))
]
),
.testTarget(
name: "OpenCombineTests",
dependencies: [
"OpenCombine",
.target(name: "OpenCombineDispatch",
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
.target(name: "OpenCombineFoundation",
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
],
swiftSettings: [
.unsafeFlags(["-enable-testing"]),
.define("WASI", .when(platforms: [.wasi]))
]
)
],
cxxLanguageStandard: .cxx17
)
// MARK: Helpers
extension Array where Element == Platform {
func except(_ exceptions: [Platform]) -> [Platform] {
return filter { !exceptions.contains($0) }
}
}
+19 -70
View File
@@ -1,14 +1,20 @@
# OpenCombine
[![OpenCombine](https://circleci.com/gh/OpenCombine/OpenCombine.svg?style=svg)](https://circleci.com/gh/OpenCombine/OpenCombine)
[![codecov](https://codecov.io/gh/OpenCombine/OpenCombine/branch/master/graph/badge.svg)](https://codecov.io/gh/OpenCombine/OpenCombine)
![Language](https://img.shields.io/badge/Swift-5.0-orange.svg)
![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20iOS%20%7C%20watchOS%20%7C%20tvOS%20%7C%20Wasm-lightgrey.svg)
![Cocoapods](https://img.shields.io/cocoapods/v/OpenCombine?color=blue)
[<img src="https://img.shields.io/badge/slack-OpenCombine-yellow.svg?logo=slack">](https://join.slack.com/t/opencombine/shared_invite/enQtNzE2MjE5NzkxODI0LTYxMjkzNDUxZWViZWI1Njc2YjBhODgxNjRjOTdkZTcxOGU2ZjJjZjYxMGI3NWZkN2RkNGFmZTUzNmU3MGE2ZWM)
[![Cocoapods](https://img.shields.io/cocoapods/v/OpenCombine?color=blue)](https://cocoapods.org/pods/OpenCombine)
Open-source implementation of Apple's [Combine](https://developer.apple.com/documentation/combine) framework for processing values over time.
The main goal of this project is to provide a compatible, reliable and efficient implementation which can be used on Apple's operating systems before macOS 10.15 and iOS 13, as well as Linux and WebAssembly.
The main goal of this project is to provide a compatible, reliable and efficient implementation which can be used on Apple's operating systems before macOS 10.15 and iOS 13, as well as Linux, Windows and WebAssembly.
| **CI Status** |
|---|
|[![Compatibility tests](https://github.com/OpenCombine/OpenCombine/actions/workflows/compatibility_tests.yml/badge.svg)](https://github.com/OpenCombine/OpenCombine/actions/workflows/compatibility_tests.yml)|
|[![macOS](https://github.com/OpenCombine/OpenCombine/actions/workflows/macos.yml/badge.svg)](https://github.com/OpenCombine/OpenCombine/actions/workflows/macos.yml)|
|[![Ubuntu](https://github.com/OpenCombine/OpenCombine/actions/workflows/ubuntu.yml/badge.svg)](https://github.com/OpenCombine/OpenCombine/actions/workflows/ubuntu.yml)|
|[![Windows](https://github.com/OpenCombine/OpenCombine/actions/workflows/windows.yml/badge.svg)](https://github.com/OpenCombine/OpenCombine/actions/workflows/windows.yml)|
|[![Wasm](https://github.com/OpenCombine/OpenCombine/actions/workflows/wasm.yml/badge.svg)](https://github.com/OpenCombine/OpenCombine/actions/workflows/wasm.yml)|
### Installation
`OpenCombine` contains three public targets: `OpenCombine`, `OpenCombineFoundation` and `OpenCombineDispatch` (the fourth one, `COpenCombineHelpers`, is considered private. Don't import it in your projects).
@@ -26,7 +32,7 @@ To add `OpenCombine` to your [SwiftPM](https://swift.org/package-manager/) packa
```swift
dependencies: [
.package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.12.0")
.package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.14.0")
],
targets: [
.target(
@@ -54,72 +60,11 @@ To do so, open Xcode, use **File** → **Swift Packages** → **Add Package Depe
To add `OpenCombine` to a project using [CocoaPods](https://cocoapods.org/), add `OpenCombine` and `OpenCombineDispatch` to the list of target dependencies in your `Podfile`.
```ruby
pod 'OpenCombine', '~> 0.12.0'
pod 'OpenCombineDispatch', '~> 0.12.0'
pod 'OpenCombineFoundation', '~> 0.12.0'
pod 'OpenCombine', '~> 0.14.0'
pod 'OpenCombineDispatch', '~> 0.14.0'
pod 'OpenCombineFoundation', '~> 0.14.0'
```
### Contributing
In order to work on this project you will need Xcode 10.2 and Swift 5.0 or later.
Please refer to the [issue #1](https://github.com/OpenCombine/OpenCombine/issues/1) for the list of operators that remain unimplemented, as well as the [RemainingCombineInterface.swift](https://github.com/OpenCombine/OpenCombine/blob/master/RemainingCombineInterface.swift) file. The latter contains the generated interface of Apple's Combine from the latest Xcode 11 version. When the functionality is implemented in OpenCombine, it should be removed from the RemainingCombineInterface.swift file.
You can refer to [this repo](https://github.com/OpenCombine/combine-interfaces) to observe Apple's Combine API and documentation changes between different Xcode (beta) versions.
You can run compatibility tests against Apple's Combine. In order to do that you will need either macOS 10.14 with iOS 13 simulator installed (since the only way we can get Apple's Combine on macOS 10.14 is using the simulator), or macOS 10.15 (Apple's Combine is bundled with the OS). Execute the following command from the root of the package:
```
$ make test-compatibility
```
Or enable the `-DOPENCOMBINE_COMPATIBILITY_TEST` compiler flag in Xcode's build settings. Note that on iOS only the latter will work.
> NOTE: Before starting to work on some feature, please consult the [GitHub project](https://github.com/OpenCombine/OpenCombine/projects/2) to make sure that nobody's already making progress on the same feature! If not, then please create a draft PR to indicate that you're beginning your work.
#### Releasing a new version
1. Create a new branch from master and call it `release/<major>.<minor>.<patch>`.
1. Replace the usages of the old version in `README.md` with the new version (make sure to check the [Swift Package Manager](#swift-package-manager) and [CocoaPods](#cocoapods) sections).
1. Bump the version in `OpenCombine.podspec`, `OpenCombineDispatch.podspec` and `OpenCombineFoundation.podspec`. In the latter two you will also need to set the `spec.dependency "OpenCombine"` property to the **previous** version. Why? Because otherwise the `pod lib lint` command that we run on our regular CI will fail when validating the `OpenCombineDispatch` and `OpenCombineFoundation` podspecs, since the dependencies are not yet in the trunk. If we set the dependencies to the previous version (which is already in the trunk), everything will be fine. This is purely to make the CI work. The clients will not experience any issues, since the version is specified as `>=`.
1. Create a pull request to master for the release branch and make sure the CI passes.
1. Merge the pull request.
1. In the GitHub web interface on the [releases](https://github.com/OpenCombine/OpenCombine/releases) page, click the **Draft a new release** button.
1. The **Tag version** and **Release title** fields should be filled with the version number.
1. The description of the release should be consistent with the previous releases. It is a good practice to divide the description into several sections: additions, bugfixes, known issues etc. Also, be sure to mention the nicknames of the contributors of the new release.
1. Publish the release.
1. Switch to the master branch and pull the changes.
1. Push the release to CocoaPods trunk. For that, execute the following commands:
```
pod trunk push OpenCombine.podspec --verbose --allow-warnings
pod trunk push OpenCombineDispatch.podspec --verbose --allow-warnings
pod trunk push OpenCombineFoundation.podspec --verbose --allow-warnings
```
Note that you need to be one of the owners of the pod for that.
#### GYB
Some publishers in OpenCombine (like `Publishers.MapKeyPath`, `Publishers.Merge`) exist in several
different flavors in order to support several arities. For example, there are also `Publishers.MapKeyPath2`
and `Publishers.MapKeyPath3`, which are very similar but different enough that Swift's type system
can't help us here (because there's no support for variadic generics). Maintaining multiple instances of
those generic types is tedious and error-prone (they can get out of sync), so we use the GYB tool for
generating those instances from a template.
GYB is a Python script that evaluates Python code written inside a template file, so it's very flexible —
templates can be arbitrarily complex. There is a good article about GYB on
[NSHipster](https://nshipster.com/swift-gyb/).
GYB is part of the [Swift Open Source Project](https://github.com/apple/swift/blob/master/utils/gyb.py)
and can be distributed under the same license as Swift itself.
GYB template files have the `.gyb` extension. Run `make gyb` to generate Swift code from those
templates. The generated files are prefixed with `GENERATED-` and are checked into source control. Those
files should never be edited directly. Instead, the `.gyb` template should be edited, and after that the files
should be regenerated using `make gyb`.
#### Debugger Support
The file `opencombine_lldb.py` defines some `lldb` type summaries for easier debugging. These type summaries improve the way `lldb` and Xcode display some OpenCombine values.
@@ -132,3 +77,7 @@ Currently, `opencombine_lldb.py` defines type summaries for these types:
- `Subscribers.Demand`
- That's all for now.
### Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md).
+58 -204
View File
@@ -244,47 +244,6 @@ extension Publisher {
public func collect<S>(_ strategy: Publishers.TimeGroupingStrategy<S>, options: S.SchedulerOptions? = nil) -> Publishers.CollectByTime<Self, S> where S : Scheduler
}
extension Publishers {
public struct PrefixUntilOutput<Upstream, Other> : Publisher where Upstream : Publisher, Other : Publisher {
/// The kind of values published by this publisher.
public typealias Output = Upstream.Output
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// Another publisher, whose first output causes this publisher to finish.
public let other: Other
public init(upstream: Upstream, other: Other)
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<S>(subscriber: S) where S : Subscriber, Upstream.Failure == S.Failure, Upstream.Output == S.Input
}
}
extension Publisher {
/// Republishes elements until another publisher emits an element.
///
/// After the second publisher publishes an element, the publisher returned by this method finishes.
///
/// - Parameter publisher: A second publisher.
/// - Returns: A publisher that republishes elements until the second publisher publishes an element.
public func prefix<P>(untilOutputFrom publisher: P) -> Publishers.PrefixUntilOutput<Self, P> where P : Publisher
}
extension Publishers {
/// A publisher created by applying the merge function to two upstream publishers.
@@ -675,6 +634,64 @@ extension Publisher {
public func merge(with other: Self) -> Publishers.MergeMany<Self>
}
extension Publishers {
/// A publisher that attempts to recreate its subscription to a failed upstream publisher.
public struct Retry<Upstream> : Publisher where Upstream : Publisher {
/// The kind of values published by this publisher.
public typealias Output = Upstream.Output
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// The maximum number of retry attempts to perform.
///
/// If `nil`, this publisher attempts to reconnect with the upstream publisher an unlimited number of times.
public let retries: Int?
/// Creates a publisher that attempts to recreate its subscription to a failed upstream publisher.
///
/// - Parameters:
/// - upstream: The publisher from which this publisher receives its elements.
/// - retries: The maximum number of retry attempts to perform. If `nil`, this publisher attempts to reconnect with the upstream publisher an unlimited number of times.
public init(upstream: Upstream, retries: Int?)
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<S>(subscriber: S) where S : Subscriber, Upstream.Failure == S.Failure, Upstream.Output == S.Input
}
}
extension Publisher {
/// Attempts to recreate a failed subscription with the upstream publisher using a specified number of attempts to establish the connection.
///
/// After exceeding the specified number of retries, the publisher passes the failure to the downstream receiver.
/// - Parameter retries: The number of times to attempt to recreate the subscription.
/// - Returns: A publisher that attempts to recreate its subscription to a failed upstream publisher.
public func retry(_ retries: Int) -> Publishers.Retry<Self>
}
extension Publisher {
/// Attempts to recreate a failed subscription with the upstream publisher using a specified number of attempts to establish the connection.
///
/// After exceeding the specified number of retries, the publisher passes the failure to the downstream receiver.
/// - Parameter retries: The number of times to attempt to recreate the subscription.
/// - Returns: A publisher that attempts to recreate its subscription to a failed upstream publisher.
public func retry(_ retries: Int) -> Publishers.Retry<Self>
}
extension Publishers {
/// A publisher that publishes either the most-recent or first element published by the upstream publisher in a specified time interval.
@@ -758,169 +775,6 @@ extension Publisher {
public func throttle<S>(for interval: S.SchedulerTimeType.Stride, scheduler: S, latest: Bool) -> Publishers.Throttle<Self, S> where S : Scheduler
}
extension Publishers {
/// A publisher created by applying the zip function to two upstream publishers.
public struct Zip<A, B> : Publisher where A : Publisher, B : Publisher, A.Failure == B.Failure {
/// The kind of values published by this publisher.
public typealias Output = (A.Output, B.Output)
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = A.Failure
public let a: A
public let b: B
public init(_ a: A, _ b: B)
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<S>(subscriber: S) where S : Subscriber, B.Failure == S.Failure, S.Input == (A.Output, B.Output)
}
/// A publisher created by applying the zip function to three upstream publishers.
public struct Zip3<A, B, C> : Publisher where A : Publisher, B : Publisher, C : Publisher, A.Failure == B.Failure, B.Failure == C.Failure {
/// The kind of values published by this publisher.
public typealias Output = (A.Output, B.Output, C.Output)
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = A.Failure
public let a: A
public let b: B
public let c: C
public init(_ a: A, _ b: B, _ c: C)
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<S>(subscriber: S) where S : Subscriber, C.Failure == S.Failure, S.Input == (A.Output, B.Output, C.Output)
}
/// A publisher created by applying the zip function to four upstream publishers.
public struct Zip4<A, B, C, D> : Publisher where A : Publisher, B : Publisher, C : Publisher, D : Publisher, A.Failure == B.Failure, B.Failure == C.Failure, C.Failure == D.Failure {
/// The kind of values published by this publisher.
public typealias Output = (A.Output, B.Output, C.Output, D.Output)
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = A.Failure
public let a: A
public let b: B
public let c: C
public let d: D
public init(_ a: A, _ b: B, _ c: C, _ d: D)
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<S>(subscriber: S) where S : Subscriber, D.Failure == S.Failure, S.Input == (A.Output, B.Output, C.Output, D.Output)
}
}
extension Publisher {
/// Combine elements from another publisher and deliver pairs of elements as tuples.
///
/// The returned publisher waits until both publishers have emitted an event, then delivers the oldest unconsumed event from each publisher together as a tuple to the subscriber.
/// For example, if publisher `P1` emits elements `a` and `b`, and publisher `P2` emits event `c`, the zip publisher emits the tuple `(a, c)`. It wont emit a tuple with event `b` until `P2` emits another event.
/// If either upstream publisher finishes successfuly or fails with an error, the zipped publisher does the same.
///
/// - Parameter other: Another publisher.
/// - Returns: A publisher that emits pairs of elements from the upstream publishers as tuples.
public func zip<P>(_ other: P) -> Publishers.Zip<Self, P> where P : Publisher, Self.Failure == P.Failure
/// Combine elements from another publisher and deliver a transformed output.
///
/// The returned publisher waits until both publishers have emitted an event, then delivers the oldest unconsumed event from each publisher together as a tuple to the subscriber.
/// For example, if publisher `P1` emits elements `a` and `b`, and publisher `P2` emits event `c`, the zip publisher emits the tuple `(a, c)`. It wont emit a tuple with event `b` until `P2` emits another event.
/// If either upstream publisher finishes successfuly or fails with an error, the zipped publisher does the same.
///
/// - Parameter other: Another publisher.
/// - transform: A closure that receives the most recent value from each publisher and returns a new value to publish.
/// - Returns: A publisher that emits pairs of elements from the upstream publishers as tuples.
public func zip<P, T>(_ other: P, _ transform: @escaping (Self.Output, P.Output) -> T) -> Publishers.Map<Publishers.Zip<Self, P>, T> where P : Publisher, Self.Failure == P.Failure
/// Combine elements from two other publishers and deliver groups of elements as tuples.
///
/// The returned publisher waits until all three publishers have emitted an event, then delivers the oldest unconsumed event from each publisher as a tuple to the subscriber.
/// For example, if publisher `P1` emits elements `a` and `b`, and publisher `P2` emits elements `c` and `d`, and publisher `P3` emits the event `e`, the zip publisher emits the tuple `(a, c, e)`. It wont emit a tuple with elements `b` or `d` until `P3` emits another event.
/// If any upstream publisher finishes successfuly or fails with an error, the zipped publisher does the same.
///
/// - Parameters:
/// - publisher1: A second publisher.
/// - publisher2: A third publisher.
/// - Returns: A publisher that emits groups of elements from the upstream publishers as tuples.
public func zip<P, Q>(_ publisher1: P, _ publisher2: Q) -> Publishers.Zip3<Self, P, Q> where P : Publisher, Q : Publisher, Self.Failure == P.Failure, P.Failure == Q.Failure
/// Combine elements from two other publishers and deliver a transformed output.
///
/// The returned publisher waits until all three publishers have emitted an event, then delivers the oldest unconsumed event from each publisher as a tuple to the subscriber.
/// For example, if publisher `P1` emits elements `a` and `b`, and publisher `P2` emits elements `c` and `d`, and publisher `P3` emits the event `e`, the zip publisher emits the tuple `(a, c, e)`. It wont emit a tuple with elements `b` or `d` until `P3` emits another event.
/// If any upstream publisher finishes successfuly or fails with an error, the zipped publisher does the same.
///
/// - Parameters:
/// - publisher1: A second publisher.
/// - publisher2: A third publisher.
/// - transform: A closure that receives the most recent value from each publisher and returns a new value to publish.
/// - Returns: A publisher that emits groups of elements from the upstream publishers as tuples.
public func zip<P, Q, T>(_ publisher1: P, _ publisher2: Q, _ transform: @escaping (Self.Output, P.Output, Q.Output) -> T) -> Publishers.Map<Publishers.Zip3<Self, P, Q>, T> where P : Publisher, Q : Publisher, Self.Failure == P.Failure, P.Failure == Q.Failure
/// Combine elements from three other publishers and deliver groups of elements as tuples.
///
/// The returned publisher waits until all four publishers have emitted an event, then delivers the oldest unconsumed event from each publisher as a tuple to the subscriber.
/// For example, if publisher `P1` emits elements `a` and `b`, and publisher `P2` emits elements `c` and `d`, and publisher `P3` emits the elements `e` and `f`, and publisher `P4` emits the event `g`, the zip publisher emits the tuple `(a, c, e, g)`. It wont emit a tuple with elements `b`, `d`, or `f` until `P4` emits another event.
/// If any upstream publisher finishes successfuly or fails with an error, the zipped publisher does the same.
///
/// - Parameters:
/// - publisher1: A second publisher.
/// - publisher2: A third publisher.
/// - publisher3: A fourth publisher.
/// - Returns: A publisher that emits groups of elements from the upstream publishers as tuples.
public func zip<P, Q, R>(_ publisher1: P, _ publisher2: Q, _ publisher3: R) -> Publishers.Zip4<Self, P, Q, R> where P : Publisher, Q : Publisher, R : Publisher, Self.Failure == P.Failure, P.Failure == Q.Failure, Q.Failure == R.Failure
/// Combine elements from three other publishers and deliver a transformed output.
///
/// The returned publisher waits until all four publishers have emitted an event, then delivers the oldest unconsumed event from each publisher as a tuple to the subscriber.
/// For example, if publisher `P1` emits elements `a` and `b`, and publisher `P2` emits elements `c` and `d`, and publisher `P3` emits the elements `e` and `f`, and publisher `P4` emits the event `g`, the zip publisher emits the tuple `(a, c, e, g)`. It wont emit a tuple with elements `b`, `d`, or `f` until `P4` emits another event.
/// If any upstream publisher finishes successfuly or fails with an error, the zipped publisher does the same.
///
/// - Parameters:
/// - publisher1: A second publisher.
/// - publisher2: A third publisher.
/// - publisher3: A fourth publisher.
/// - transform: A closure that receives the most recent value from each publisher and returns a new value to publish.
/// - Returns: A publisher that emits groups of elements from the upstream publishers as tuples.
public func zip<P, Q, R, T>(_ publisher1: P, _ publisher2: Q, _ publisher3: R, _ transform: @escaping (Self.Output, P.Output, Q.Output, R.Output) -> T) -> Publishers.Map<Publishers.Zip4<Self, P, Q, R>, T> where P : Publisher, Q : Publisher, R : Publisher, Self.Failure == P.Failure, P.Failure == Q.Failure, Q.Failure == R.Failure
}
extension Publishers.CombineLatest : Equatable where A : Equatable, B : Equatable {
/// Returns a Boolean value that indicates whether two publishers are equivalent.
@@ -10,13 +10,31 @@
#include <atomic>
#include <cstdlib>
#include <system_error>
#include <pthread.h>
#include <signal.h>
#if __has_include(<pthread.h>)
# include <pthread.h>
# define OPENCOMBINE_HAS_PTHREAD 1
#else
# define OPENCOMBINE_HAS_PTHREAD 0
#endif
#if __has_include(<signal.h>)
# include <signal.h>
# define OPENCOMBINE_HAS_SIGNAL_HANDLING 1
#else
# define OPENCOMBINE_HAS_SIGNAL_HANDLING 0
#endif
#ifdef _WIN32
# include <windows.h>
#endif
#ifdef __APPLE__
#include <os/lock.h>
#endif // __APPLE__
#include <mutex>
// Throwing exceptions through language boundaries is undefined behavior,
// so we must catch all of them in our extern "C" functions.
#define OPENCOMBINE_HANDLE_EXCEPTION_BEGIN try {
@@ -47,10 +65,11 @@ public:
virtual void unlock() = 0;
virtual void assertOwner() {}
virtual ~PlatformIndependentMutex() noexcept(false) {}
virtual ~PlatformIndependentMutex() {}
};
class PThreadMutex : PlatformIndependentMutex {
#if OPENCOMBINE_HAS_PTHREAD
class PThreadMutex final : PlatformIndependentMutex {
private:
pthread_mutex_t mutex_;
public:
@@ -66,20 +85,16 @@ public:
PThreadMutex(PThreadMutex&&) = delete;
PThreadMutex& operator=(PThreadMutex&&) = delete;
void lock() override final {
void lock() override {
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutex_lock(&mutex_));
}
void unlock() override final {
void unlock() override {
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutex_unlock(&mutex_));
}
~PThreadMutex() {
// Yep, this destructor may throw. This is deliberate, since pthread_mutex_destroy
// may fail.
//
// The altrenative is to just silently ignore the error, which is even worse.
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutex_destroy(&mutex_));
pthread_mutex_destroy(&mutex_);
}
protected:
class Attributes {
@@ -107,12 +122,8 @@ protected:
setType(PTHREAD_MUTEX_ERRORCHECK);
}
~Attributes() noexcept(false) {
// Yep, this destructor may throw. This is deliberate,
// since pthread_mutexattr_destroy may fail.
//
// The altrenative is to just silently ignore the error, which is even worse.
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutexattr_destroy(&attrs_));
~Attributes() {
pthread_mutexattr_destroy(&attrs_);
}
private:
void setType(int type) {
@@ -124,21 +135,7 @@ protected:
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutex_init(&mutex_, attributes.raw()));
}
};
class PThreadRecursiveMutex final : PThreadMutex {
public:
PThreadRecursiveMutex() {
Attributes attrs;
attrs.setRecursive();
initialize(attrs);
}
PThreadRecursiveMutex(const PThreadRecursiveMutex&) = delete;
PThreadRecursiveMutex& operator=(const PThreadRecursiveMutex&) = delete;
PThreadRecursiveMutex(PThreadRecursiveMutex&&) = delete;
PThreadRecursiveMutex& operator=(PThreadRecursiveMutex&&) = delete;
};
#endif // OPENCOMBINE_HAS_PTHREAD
#ifdef __APPLE__
@@ -167,9 +164,32 @@ public:
};
#endif // __APPLE__
} // end anonymous namespace
template <typename Mu>
class GenericMutex final : PlatformIndependentMutex {
Mu mutex_;
public:
extern "C" {
GenericMutex() = default;
GenericMutex(const GenericMutex&) = delete;
GenericMutex& operator=(const GenericMutex&) = delete;
GenericMutex(GenericMutex&&) = delete;
GenericMutex& operator=(GenericMutex&&) = delete;
void lock() override {
mutex_.lock();
}
void unlock() override {
mutex_.unlock();
}
};
using StdMutex = GenericMutex<std::mutex>;
using StdRecursiveMutex = GenericMutex<std::recursive_mutex>;
} // end anonymous namespace
uint64_t opencombine_next_combine_identifier(void) {
return next_combine_identifier.fetch_add(1);
@@ -177,24 +197,27 @@ uint64_t opencombine_next_combine_identifier(void) {
OpenCombineUnfairLock opencombine_unfair_lock_alloc(void) {
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
#ifdef __APPLE__
if (__builtin_available(macOS 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *)) {
return {new OSUnfairLock};
} else {
return {new PThreadMutex};
}
#else
#elif OPENCOMBINE_HAS_PTHREAD
// When possible, use pthread mutex implementation, because it allows
// setting the PTHREAD_MUTEX_ERRORCHECK attribute, which makes
// recursive locking a hard error instead of UB.
return {new PThreadMutex};
#else
return {new StdMutex};
#endif
OPENCOMBINE_HANDLE_EXCEPTION_END
}
OpenCombineUnfairRecursiveLock opencombine_unfair_recursive_lock_alloc(void) {
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
// TODO: Use os_unfair_recursive_lock on Darwin as soon as it becomes public API.
return {new PThreadRecursiveMutex};
return {new StdRecursiveMutex};
OPENCOMBINE_HANDLE_EXCEPTION_END
}
@@ -237,7 +260,9 @@ void opencombine_unfair_recursive_lock_dealloc(OpenCombineUnfairRecursiveLock lo
}
void opencombine_stop_in_debugger(void) {
#if _WIN32
DebugBreak();
#elif OPENCOMBINE_HAS_SIGNAL_HANDLING
raise(SIGTRAP);
#endif
}
} // extern "C"
@@ -0,0 +1,3 @@
module COpenCombineHelpers {
header "COpenCombineHelpers.h"
}
+1 -1
View File
@@ -9,7 +9,7 @@ extension Publisher {
/// Wraps this publisher with a type eraser.
///
/// Use `eraseToAnyPublisher()` to expose an instance of `AnyPublishe`` to
/// Use `eraseToAnyPublisher()` to expose an instance of `AnyPublisher`` to
/// the downstream subscriber, rather than this publishers actual type.
/// This form of _type erasure_ preserves abstraction across API boundaries, such as
/// different modules.
@@ -0,0 +1,24 @@
//
// ConcurrencyHelpers.swift
//
//
// Created by Sergej Jaskiewicz on 14.11.2022.
//
#if canImport(_Concurrency) && compiler(>=5.5)
import _Concurrency
#endif
#if (canImport(_Concurrency) && compiler(>=5.5) || compiler(>=5.5.1)) && swift(<5.7)
/// A polyfill for pre-5.7 Swift versions.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
internal func withTaskCancellationHandler<T>( // swiftlint:disable:this generic_type_name
operation: () async throws -> T,
onCancel handler: @Sendable () -> Void
) async rethrows -> T {
return try await withTaskCancellationHandler(
handler: handler,
operation: operation
)
}
#endif
@@ -0,0 +1,136 @@
//
// Future+Concurrency.swift
//
//
// Created by Sergej Jaskiewicz on 28.08.2021.
//
#if canImport(_Concurrency) && compiler(>=5.5)
import _Concurrency
#endif
#if canImport(_Concurrency) && compiler(>=5.5) || compiler(>=5.5.1)
extension Future where Failure == Never {
/// The published value of the future, delivered asynchronously.
///
/// This property subscribes to the `Future` and delivers the value asynchronously
/// when the `Future` publishes it. Use this property when you want to use
/// the `async`-`await` syntax with a `Future`.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public var value: Output {
get async {
await ContinuationSubscriber.withUnsafeSubscription(self)
}
}
}
extension Future {
/// The published value of the future or an error, delivered asynchronously.
///
/// This property subscribes to the `Future` and delivers the value asynchronously
/// when the `Future` publishes it. If the `Future` terminates with an error,
/// the awaiting caller receives the error instead. Use this property when you want
/// to the `async`-`await` syntax with a `Future` whose `Failure` type is not `Never`.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public var value: Output {
get async throws {
try await ContinuationSubscriber.withUnsafeThrowingSubscription(self)
}
}
}
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
private final class ContinuationSubscriber<Input,
UpstreamFailure: Error,
ErrorOrNever: Error>
: Subscriber
{
typealias Failure = UpstreamFailure
private var continuation: UnsafeContinuation<Input, ErrorOrNever>?
private var subscription: Subscription?
private let lock = UnfairLock.allocate()
private init(_ continuation: UnsafeContinuation<Input, ErrorOrNever>) {
self.continuation = continuation
}
deinit {
lock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard self.subscription == nil else {
assertionFailure("Unexpected state: received subscription twice")
lock.unlock()
subscription.cancel()
return
}
self.subscription = subscription
lock.unlock()
subscription.request(.max(1))
}
func receive(_ input: Input) -> Subscribers.Demand {
lock.lock()
if let continuation = self.continuation.take() {
lock.unlock()
continuation.resume(returning: input)
} else {
assertionFailure("Unexpected state: already completed")
lock.unlock()
}
return .none
}
func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
subscription = nil
lock.unlock()
completion.failure.map(handleFailure)
}
private func handleFailure(_ error: Failure) {
lock.lock()
if let continuation = self.continuation.take() {
lock.unlock()
continuation.resume(throwing: error as! ErrorOrNever)
} else {
assertionFailure("Unexpected state: already completed")
lock.unlock()
}
}
}
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension ContinuationSubscriber where ErrorOrNever == Error {
fileprivate static func withUnsafeThrowingSubscription<Upstream: Publisher>(
_ upstream: Upstream
) async throws -> Input
where Upstream.Output == Input,
Upstream.Failure == UpstreamFailure
{
try await withUnsafeThrowingContinuation { continuation in
upstream.subscribe(ContinuationSubscriber(continuation))
}
}
}
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension ContinuationSubscriber where UpstreamFailure == Never, ErrorOrNever == Never {
fileprivate static func withUnsafeSubscription<Upstream: Publisher>(
_ upstream: Upstream
) async -> Input
where Upstream.Output == Input,
Upstream.Failure == Never
{
await withUnsafeContinuation { continuation in
upstream.subscribe(ContinuationSubscriber(continuation))
}
}
}
#endif
@@ -0,0 +1,397 @@
//
//
// Auto-generated from GYB template. DO NOT EDIT!
//
//
//
//
// Publisher+Concurrency.swift
//
//
// Created by Sergej Jaskiewicz on 28.08.2021.
//
#if canImport(_Concurrency) && compiler(>=5.5)
import _Concurrency
#endif
#if canImport(_Concurrency) && compiler(>=5.5) || compiler(>=5.5.1)
extension Publisher where Failure == Never {
/// The elements produced by the publisher, as an asynchronous sequence.
///
/// This property provides an `AsyncPublisher`, which allows you to use
/// the Swift `async`-`await` syntax to receive the publisher's elements.
/// Because `AsyncPublisher` conforms to `AsyncSequence`, you iterate over its
/// elements with a `for`-`await`-`in` loop, rather than attaching a subscriber.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public var values: AsyncPublisher<Self> {
return .init(self)
}
}
/// A publisher that exposes its elements as an asynchronous sequence.
///
/// `AsyncPublisher` conforms to `AsyncSequence`, which allows callers to receive
/// values with the `for`-`await`-`in` syntax, rather than attaching a `Subscriber`.
///
/// Use the `values` property of the `Publisher` protocol to wrap an existing publisher
/// with an instance of this type.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public struct AsyncPublisher<Upstream: Publisher>: AsyncSequence
where Upstream.Failure == Never
{
public typealias Element = Upstream.Output
/// The iterator that produces elements of the asynchronous publisher sequence.
public struct Iterator: AsyncIteratorProtocol {
public typealias Element = Upstream.Output
fileprivate let inner: Inner
/// Produces the next element in the prefix sequence.
///
/// - Returns: The next published element, or `nil` if the publisher finishes
/// normally.
public mutating func next() async -> Element? {
return await withTaskCancellationHandler(
operation: { [inner] in await inner.next() },
onCancel: { [inner] in inner.cancel() }
)
}
}
/// The type of asynchronous iterator that produces elements of this
/// asynchronous sequence.
public typealias AsyncIterator = Iterator
private let publisher: Upstream
/// Creates a publisher that exposes elements received from an upstream publisher as
/// a throwing asynchronous sequence.
///
/// - Parameter publisher: An upstream publisher. The asynchronous publisher converts
/// elements received from this publisher into an asynchronous sequence.
public init(_ publisher: Upstream) {
self.publisher = publisher
}
/// Creates the asynchronous iterator that produces elements of this asynchronous
/// sequence.
///
/// - Returns: An instance of the `AsyncIterator` type used to produce elements of
/// the asynchronous sequence.
public func makeAsyncIterator() -> Iterator {
let inner = Iterator.Inner()
publisher.subscribe(inner)
return Iterator(inner: inner)
}
}
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension AsyncPublisher.Iterator {
fileprivate final class Inner: Subscriber, Cancellable {
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private enum State {
case awaitingSubscription
case subscribed(Subscription)
case terminal
}
private let lock = UnfairLock.allocate()
private var pending: [UnsafeContinuation<Input?, Never>] = []
private var state = State.awaitingSubscription
private var pendingDemand = Subscribers.Demand.none
deinit {
lock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard case .awaitingSubscription = state else {
lock.unlock()
subscription.cancel()
return
}
state = .subscribed(subscription)
let pendingDemand = self.pendingDemand
self.pendingDemand = .none
lock.unlock()
if pendingDemand != .none {
subscription.request(pendingDemand)
}
}
func receive(_ input: Input) -> Subscribers.Demand {
lock.lock()
guard case .subscribed = state else {
let pending = self.pending.take()
lock.unlock()
pending.resumeAllWithNil()
return .none
}
precondition(!pending.isEmpty, "Received an output without requesting demand")
let continuation = pending.removeFirst()
lock.unlock()
continuation.resume(returning: input)
return .none
}
func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
state = .terminal
let pending = self.pending.take()
lock.unlock()
pending.resumeAllWithNil()
}
func cancel() {
lock.lock()
let pending = self.pending.take()
guard case .subscribed(let subscription) = state else {
state = .terminal
lock.unlock()
pending.resumeAllWithNil()
return
}
state = .terminal
lock.unlock()
subscription.cancel()
pending.resumeAllWithNil()
}
fileprivate func next() async -> Input? {
return await withUnsafeContinuation { continuation in
lock.lock()
switch state {
case .awaitingSubscription:
pending.append(continuation)
pendingDemand += 1
lock.unlock()
case .subscribed(let subscription):
pending.append(continuation)
lock.unlock()
subscription.request(.max(1))
case .terminal:
lock.unlock()
continuation.resume(returning: nil)
}
}
}
}
}
extension Publisher {
/// The elements produced by the publisher, as a throwing asynchronous sequence.
///
/// This property provides an `AsyncThrowingPublisher`, which allows you to use
/// the Swift `async`-`await` syntax to receive the publisher's elements.
/// Because `AsyncPublisher` conforms to `AsyncSequence`, you iterate over its
/// elements with a `for`-`await`-`in` loop, rather than attaching a subscriber.
/// If the publisher terminates with an error, the awaiting caller receives the error
/// as a `throw`.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public var values: AsyncThrowingPublisher<Self> {
return .init(self)
}
}
/// A publisher that exposes its elements as a throwing asynchronous sequence.
///
/// `AsyncThrowingPublisher` conforms to `AsyncSequence`, which allows callers to receive
/// values with the `for`-`await`-`in` syntax, rather than attaching a `Subscriber`.
/// If the upstream publisher terminates with an error, `AsyncThrowingPublisher` throws
/// the error to the awaiting caller.
///
/// Use the `values` property of the `Publisher` protocol to wrap an existing publisher
/// with an instance of this type.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public struct AsyncThrowingPublisher<Upstream: Publisher>: AsyncSequence
{
public typealias Element = Upstream.Output
/// The iterator that produces elements of the asynchronous publisher sequence.
public struct Iterator: AsyncIteratorProtocol {
public typealias Element = Upstream.Output
fileprivate let inner: Inner
/// Produces the next element in the prefix sequence.
///
/// - Returns: The next published element, or `nil` if the publisher finishes
/// normally.
/// If the publisher terminates with an error, the call point receives
/// the error as a `throw`.
public mutating func next() async throws -> Element? {
return try await withTaskCancellationHandler(
operation: { [inner] in try await inner.next() },
onCancel: { [inner] in inner.cancel() }
)
}
}
/// The type of asynchronous iterator that produces elements of this
/// asynchronous sequence.
public typealias AsyncIterator = Iterator
private let publisher: Upstream
/// Creates a publisher that exposes elements received from an upstream publisher as
/// an asynchronous sequence.
///
/// - Parameter publisher: An upstream publisher. The asynchronous publisher converts
/// elements received from this publisher into an asynchronous sequence.
public init(_ publisher: Upstream) {
self.publisher = publisher
}
/// Creates the asynchronous iterator that produces elements of this asynchronous
/// sequence.
///
/// - Returns: An instance of the `AsyncIterator` type used to produce elements of
/// the asynchronous sequence.
public func makeAsyncIterator() -> Iterator {
let inner = Iterator.Inner()
publisher.subscribe(inner)
return Iterator(inner: inner)
}
}
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension AsyncThrowingPublisher.Iterator {
fileprivate final class Inner: Subscriber, Cancellable {
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private enum State {
case awaitingSubscription
case subscribed(Subscription)
case terminal(Error?)
}
private let lock = UnfairLock.allocate()
private var pending: [UnsafeContinuation<Input?, Error>] = []
private var state = State.awaitingSubscription
private var pendingDemand = Subscribers.Demand.none
deinit {
lock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard case .awaitingSubscription = state else {
lock.unlock()
subscription.cancel()
return
}
state = .subscribed(subscription)
let pendingDemand = self.pendingDemand
self.pendingDemand = .none
lock.unlock()
if pendingDemand != .none {
subscription.request(pendingDemand)
}
}
func receive(_ input: Input) -> Subscribers.Demand {
lock.lock()
guard case .subscribed = state else {
let pending = self.pending.take()
lock.unlock()
pending.resumeAllWithNil()
return .none
}
precondition(!pending.isEmpty, "Received an output without requesting demand")
let continuation = pending.removeFirst()
lock.unlock()
continuation.resume(returning: input)
return .none
}
func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
switch state {
case .awaitingSubscription, .subscribed:
if let continuation = pending.first {
state = .terminal(nil)
let remaining = pending.take().dropFirst()
lock.unlock()
switch completion {
case .finished:
continuation.resume(returning: nil)
case .failure(let error):
continuation.resume(throwing: error)
}
remaining.resumeAllWithNil()
} else {
state = .terminal(completion.failure)
lock.unlock()
}
case .terminal:
let pending = self.pending.take()
lock.unlock()
pending.resumeAllWithNil()
}
}
func cancel() {
lock.lock()
let pending = self.pending.take()
guard case .subscribed(let subscription) = state else {
state = .terminal(nil)
lock.unlock()
pending.resumeAllWithNil()
return
}
state = .terminal(nil)
lock.unlock()
subscription.cancel()
pending.resumeAllWithNil()
}
fileprivate func next() async throws -> Input? {
return try await withUnsafeThrowingContinuation { continuation in
lock.lock()
switch state {
case .awaitingSubscription:
pending.append(continuation)
pendingDemand += 1
lock.unlock()
case .subscribed(let subscription):
pending.append(continuation)
lock.unlock()
subscription.request(.max(1))
case .terminal(nil):
lock.unlock()
continuation.resume(returning: nil)
case .terminal(let error?):
state = .terminal(nil)
lock.unlock()
continuation.resume(throwing: error)
}
}
}
}
}
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension Sequence {
fileprivate func resumeAllWithNil<Output, Failure: Error>()
where Element == UnsafeContinuation<Output?, Failure>
{
for continuation in self {
continuation.resume(returning: nil)
}
}
}
#endif
@@ -0,0 +1,249 @@
${template_header}
//
// Publisher+Concurrency.swift
//
//
// Created by Sergej Jaskiewicz on 28.08.2021.
//
#if canImport(_Concurrency) && compiler(>=5.5)
import _Concurrency
#endif
#if canImport(_Concurrency) && compiler(>=5.5) || compiler(>=5.5.1)
%{
instantiations = [('AsyncPublisher', False), ('AsyncThrowingPublisher', True)]
}%
% for instantiation, throwing in instantiations:
extension Publisher ${'' if throwing else 'where Failure == Never '}{
/// The elements produced by the publisher, as ${'a throwing' if throwing else 'an'} asynchronous sequence.
///
/// This property provides an `${instantiation}`, which allows you to use
/// the Swift `async`-`await` syntax to receive the publisher's elements.
/// Because `AsyncPublisher` conforms to `AsyncSequence`, you iterate over its
/// elements with a `for`-`await`-`in` loop, rather than attaching a subscriber.
% if throwing:
/// If the publisher terminates with an error, the awaiting caller receives the error
/// as a `throw`.
% end
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public var values: ${instantiation}<Self> {
return .init(self)
}
}
/// A publisher that exposes its elements as ${'a throwing' if throwing else 'an'} asynchronous sequence.
///
/// `${instantiation}` conforms to `AsyncSequence`, which allows callers to receive
/// values with the `for`-`await`-`in` syntax, rather than attaching a `Subscriber`.
% if throwing:
/// If the upstream publisher terminates with an error, `${instantiation}` throws
/// the error to the awaiting caller.
% end
///
/// Use the `values` property of the `Publisher` protocol to wrap an existing publisher
/// with an instance of this type.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public struct ${instantiation}<Upstream: Publisher>: AsyncSequence
% if not throwing:
where Upstream.Failure == Never
% end
{
public typealias Element = Upstream.Output
/// The iterator that produces elements of the asynchronous publisher sequence.
public struct Iterator: AsyncIteratorProtocol {
public typealias Element = Upstream.Output
fileprivate let inner: Inner
/// Produces the next element in the prefix sequence.
///
/// - Returns: The next published element, or `nil` if the publisher finishes
/// normally.
% if throwing:
/// If the publisher terminates with an error, the call point receives
/// the error as a `throw`.
% end
public mutating func next() async ${'throws ' if throwing else ''}-> Element? {
return ${'try ' if throwing else ''}await withTaskCancellationHandler(
operation: { [inner] in ${'try ' if throwing else ''}await inner.next() },
onCancel: { [inner] in inner.cancel() }
)
}
}
/// The type of asynchronous iterator that produces elements of this
/// asynchronous sequence.
public typealias AsyncIterator = Iterator
private let publisher: Upstream
/// Creates a publisher that exposes elements received from an upstream publisher as
% if throwing:
/// an asynchronous sequence.
% else:
/// a throwing asynchronous sequence.
% end
///
/// - Parameter publisher: An upstream publisher. The asynchronous publisher converts
/// elements received from this publisher into an asynchronous sequence.
public init(_ publisher: Upstream) {
self.publisher = publisher
}
/// Creates the asynchronous iterator that produces elements of this asynchronous
/// sequence.
///
/// - Returns: An instance of the `AsyncIterator` type used to produce elements of
/// the asynchronous sequence.
public func makeAsyncIterator() -> Iterator {
let inner = Iterator.Inner()
publisher.subscribe(inner)
return Iterator(inner: inner)
}
}
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension ${instantiation}.Iterator {
fileprivate final class Inner: Subscriber, Cancellable {
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private enum State {
case awaitingSubscription
case subscribed(Subscription)
case terminal${'(Error?)' if throwing else ''}
}
private let lock = UnfairLock.allocate()
private var pending: [UnsafeContinuation<Input?, ${'Error' if throwing else 'Never'}>] = []
private var state = State.awaitingSubscription
private var pendingDemand = Subscribers.Demand.none
deinit {
lock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard case .awaitingSubscription = state else {
lock.unlock()
subscription.cancel()
return
}
state = .subscribed(subscription)
let pendingDemand = self.pendingDemand
self.pendingDemand = .none
lock.unlock()
if pendingDemand != .none {
subscription.request(pendingDemand)
}
}
func receive(_ input: Input) -> Subscribers.Demand {
lock.lock()
guard case .subscribed = state else {
let pending = self.pending.take()
lock.unlock()
pending.resumeAllWithNil()
return .none
}
precondition(!pending.isEmpty, "Received an output without requesting demand")
let continuation = pending.removeFirst()
lock.unlock()
continuation.resume(returning: input)
return .none
}
func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
% if throwing:
switch state {
case .awaitingSubscription, .subscribed:
if let continuation = pending.first {
state = .terminal(nil)
let remaining = pending.take().dropFirst()
lock.unlock()
switch completion {
case .finished:
continuation.resume(returning: nil)
case .failure(let error):
continuation.resume(throwing: error)
}
remaining.resumeAllWithNil()
} else {
state = .terminal(completion.failure)
lock.unlock()
}
case .terminal:
let pending = self.pending.take()
lock.unlock()
pending.resumeAllWithNil()
}
% else:
state = .terminal
let pending = self.pending.take()
lock.unlock()
pending.resumeAllWithNil()
% end
}
func cancel() {
lock.lock()
let pending = self.pending.take()
guard case .subscribed(let subscription) = state else {
state = .terminal${'(nil)' if throwing else ''}
lock.unlock()
pending.resumeAllWithNil()
return
}
state = .terminal${'(nil)' if throwing else ''}
lock.unlock()
subscription.cancel()
pending.resumeAllWithNil()
}
fileprivate func next() async ${'throws ' if throwing else ''}-> Input? {
return ${'try ' if throwing else ''}await withUnsafe${'Throwing' if throwing else ''}Continuation { continuation in
lock.lock()
switch state {
case .awaitingSubscription:
pending.append(continuation)
pendingDemand += 1
lock.unlock()
case .subscribed(let subscription):
pending.append(continuation)
lock.unlock()
subscription.request(.max(1))
case .terminal${'(nil)' if throwing else ''}:
lock.unlock()
continuation.resume(returning: nil)
% if throwing:
case .terminal(let error?):
state = .terminal(nil)
lock.unlock()
continuation.resume(throwing: error)
% end
}
}
}
}
}
% end
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension Sequence {
fileprivate func resumeAllWithNil<Output, Failure: Error>()
where Element == UnsafeContinuation<Output?, Failure>
{
for continuation in self {
continuation.resume(returning: nil)
}
}
}
#endif
@@ -1,24 +0,0 @@
//
// ConnectablePublisher.swift
//
//
// Created by Sergej Jaskiewicz on 14.06.2019.
//
/// A publisher that provides an explicit means of connecting and canceling publication.
///
/// Use a `ConnectablePublisher` when you need to perform additional configuration or
/// setup prior to producing any elements.
///
/// This publisher doesnt produce any elements until you call its `connect()` method.
///
/// Use `makeConnectable()` to create a `ConnectablePublisher` from any publisher whose
/// failure type is `Never`.
public protocol ConnectablePublisher: Publisher {
/// Connects to the publisher, allowing it to produce elements, and returns
/// an instance with which to cancel publishing.
///
/// - Returns: A `Cancellable` instance that you use to cancel publishing.
func connect() -> Cancellable
}
+5 -10
View File
@@ -108,8 +108,7 @@ public final class CurrentValueSubject<Output, Failure: Error>: Subject {
}
active = false
self.completion = completion
let downstreams = self.downstreams
self.downstreams.removeAll()
let downstreams = self.downstreams.take()
lock.unlock()
downstreams.forEach { conduit in
conduit.finish(completion: completion)
@@ -181,13 +180,11 @@ extension CurrentValueSubject {
override func finish(completion: Subscribers.Completion<Failure>) {
lock.lock()
guard let downstream = self.downstream else {
guard let downstream = self.downstream.take() else {
lock.unlock()
return
}
self.downstream = nil
let parent = self.parent
self.parent = nil
let parent = self.parent.take()
lock.unlock()
parent?.disassociate(self)
downstreamLock.lock()
@@ -227,13 +224,11 @@ extension CurrentValueSubject {
override func cancel() {
lock.lock()
if self.downstream == nil {
if downstream.take() == nil {
lock.unlock()
return
}
self.downstream = nil
let parent = self.parent
self.parent = nil
let parent = self.parent.take()
lock.unlock()
parent?.disassociate(self)
}
+68 -52
View File
@@ -43,8 +43,7 @@ public final class Future<Output, Failure: Error>: Publisher {
return
}
self.result = result
let downstreams = self.downstreams
self.downstreams.removeAll()
let downstreams = self.downstreams.take()
lock.unlock()
switch result {
case .success(let output):
@@ -87,12 +86,32 @@ extension Future {
CustomPlaygroundDisplayConvertible
where Downstream.Input == Output, Downstream.Failure == Failure
{
private enum State {
case active(Downstream, hasAnyDemand: Bool)
case terminal
fileprivate var parent: Future?
var downstream: Downstream? {
switch self {
case .active(let downstream, hasAnyDemand: _):
return downstream
case .terminal:
return nil
}
}
fileprivate var downstream: Downstream?
var hasAnyDemand: Bool {
switch self {
case .active(_, let hasAnyDemand):
return hasAnyDemand
case .terminal:
return false
}
}
}
fileprivate var hasAnyDemand = false
private var parent: Future?
private var state: State
private var lock = UnfairLock.allocate()
@@ -100,7 +119,7 @@ extension Future {
fileprivate init(parent: Future, downstream: Downstream) {
self.parent = parent
self.downstream = downstream
self.state = .active(downstream, hasAnyDemand: false)
}
deinit {
@@ -108,21 +127,8 @@ extension Future {
downstreamLock.deallocate()
}
fileprivate func fulfill(_ result: Result<Output, Failure>) {
lock.lock()
guard let downstream = self.downstream else {
lock.unlock()
return
}
let parent = self.parent
if case .success = result, !hasAnyDemand {
lock.unlock()
return
}
self.downstream = nil
self.parent = nil
lock.unlock()
downstreamLock.lock()
fileprivate func lockedFulfill(downstream: Downstream,
result: Result<Output, Failure>) {
switch result {
case .success(let output):
_ = downstream.receive(output)
@@ -130,7 +136,27 @@ extension Future {
case .failure(let error):
downstream.receive(completion: .failure(error))
}
}
fileprivate func fulfill(_ result: Result<Output, Failure>) {
lock.lock()
guard case let .active(downstream, hasAnyDemand) = state else {
lock.unlock()
return
}
if case .success = result, !hasAnyDemand {
lock.unlock()
return
}
state = .terminal
lock.unlock()
downstreamLock.lock()
lockedFulfill(downstream: downstream, result: result)
downstreamLock.unlock()
lock.lock()
let parent = self.parent.take()
lock.unlock()
parent?.disassociate(self)
}
@@ -150,47 +176,37 @@ extension Future {
override func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
lock.lock()
guard let downstream = self.downstream, let parent = self.parent else {
guard case .active(let downstream, hasAnyDemand: _) = state else {
lock.unlock()
return
}
hasAnyDemand = true
state = .active(downstream, hasAnyDemand: true)
parent.lock.lock()
guard let result = parent.result else {
parent.lock.unlock()
if let parent = parent, let result = parent.result {
// If the promise is already resolved, send the result downstream
// immediately
state = .terminal
lock.unlock()
downstreamLock.lock()
lockedFulfill(downstream: downstream, result: result)
downstreamLock.unlock()
parent.disassociate(self)
} else {
lock.unlock()
return
}
parent.lock.unlock()
self.downstream = nil
self.parent = nil
lock.unlock()
downstreamLock.lock()
switch result {
case .success(let output):
_ = downstream.receive(output)
downstream.receive(completion: .finished)
case .failure(let error):
// This branch is not reachable under normal circumstances,
// but may be reachable in case of a race condition.
downstream.receive(completion: .failure(error))
}
downstreamLock.unlock()
parent.disassociate(self)
}
override func cancel() {
lock.lock()
if self.downstream == nil {
switch state {
case .active:
state = .terminal
let parent = self.parent.take()
lock.unlock()
parent?.disassociate(self)
case .terminal:
lock.unlock()
return
}
self.downstream = nil
let parent = self.parent
self.parent = nil
lock.unlock()
parent?.disassociate(self)
}
var description: String { return "Future" }
@@ -200,8 +216,8 @@ extension Future {
defer { lock.unlock() }
let children: [Mirror.Child] = [
("parent", parent as Any),
("downstream", downstream as Any),
("hasAnyDemand", hasAnyDemand),
("downstream", state.downstream as Any),
("hasAnyDemand", state.hasAnyDemand),
("subject", parent as Any)
]
return Mirror(self, children: children)
+426
View File
@@ -0,0 +1,426 @@
//
//
// Auto-generated from GYB template. DO NOT EDIT!
//
//
//
//
// RootProtocols.swift
// OpenCombine
//
// Created by Sergej Jaskiewicz on 10.06.2019.
//
#if compiler(>=5.7)
/// Declares that a type can transmit a sequence of values over time.
///
/// A publisher delivers elements to one or more `Subscriber` instances.
/// The subscribers `Input` and `Failure` associated types must match the `Output` and
/// `Failure` types declared by the publisher.
/// The publisher implements the `receive(subscriber:)`method to accept a subscriber.
///
/// After this, the publisher can call the following methods on the subscriber:
/// - `receive(subscription:)`: Acknowledges the subscribe request and returns
/// a `Subscription` instance. The subscriber uses the subscription to demand elements
/// from the publisher and can use it to cancel publishing.
/// - `receive(_:)`: Delivers one element from the publisher to the subscriber.
/// - `receive(completion:)`: Informs the subscriber that publishing has ended,
/// either normally or with an error.
///
/// Every `Publisher` must adhere to this contract for downstream subscribers to function
/// correctly.
///
/// Extensions on `Publisher` define a wide variety of _operators_ that you compose to
/// create sophisticated event-processing chains.
/// Each operator returns a type that implements the `Publisher` protocol
/// Most of these types exist as extensions on the `Publishers` enumeration.
/// For example, the `map(_:)` operator returns an instance of `Publishers.Map`.
///
/// # Creating Your Own Publishers
///
/// Rather than implementing the `Publisher` protocol yourself, you can create your own
/// publisher by using one of several types provided by the OpenCombine framework:
///
/// - Use a concrete subclass of `Subject`, such as `PassthroughSubject`, to publish
/// values on-demand by calling its `send(_:)` method.
/// - Use a `CurrentValueSubject` to publish whenever you update the subjects underlying
/// value.
/// - Add the `@Published` annotation to a property of one of your own types. In doing so,
/// the property gains a publisher that emits an event whenever the propertys value
/// changes. See the `Published` type for an example of this approach.
public protocol Publisher<Output, Failure> {
/// The kind of values published by this publisher.
associatedtype Output
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
associatedtype Failure: Error
/// Attaches the specified subscriber to this publisher.
///
/// Always call this function instead of `receive(subscriber:)`.
/// Adopters of `Publisher` must implement `receive(subscriber:)`. The implementation
/// of `subscribe(_:)` provided by `Publisher` calls through to
/// `receive(subscriber:)`.
///
/// - Parameter subscriber: The subscriber to attach to this publisher. After
/// attaching, the subscriber can start to receive values.
func receive<Subscriber: OpenCombine.Subscriber>(subscriber: Subscriber)
where Failure == Subscriber.Failure, Output == Subscriber.Input
}
/// A publisher that exposes a method for outside callers to publish elements.
///
/// A subject is a publisher that you can use to inject values into a stream, by calling
/// its `send()` method. This can be useful for adapting existing imperative code to the
/// Combine model.
public protocol Subject<Output, Failure>: AnyObject, Publisher {
/// Sends a value to the subscriber.
///
/// - Parameter value: The value to send.
func send(_ value: Output)
/// Sends a completion signal to the subscriber.
///
/// - Parameter completion: A `Completion` instance which indicates whether publishing
/// has finished normally or failed with an error.
func send(completion: Subscribers.Completion<Failure>)
/// Sends a subscription to the subscriber.
///
/// This call provides the `Subject` an opportunity to establish demand for any new
/// upstream subscriptions.
///
/// - Parameter subscription: The subscription instance through which the subscriber
/// can request elements.
func send(subscription: Subscription)
}
/// A publisher that provides an explicit means of connecting and canceling publication.
///
/// Use a `ConnectablePublisher` when you need to perform additional configuration or
/// setup prior to producing any elements.
///
/// This publisher doesnt produce any elements until you call its `connect()` method.
///
/// Use `makeConnectable()` to create a `ConnectablePublisher` from any publisher whose
/// failure type is `Never`.
public protocol ConnectablePublisher<Output, Failure>: Publisher {
/// Connects to the publisher, allowing it to produce elements, and returns
/// an instance with which to cancel publishing.
///
/// - Returns: A `Cancellable` instance that you use to cancel publishing.
func connect() -> Cancellable
}
/// A protocol that declares a type that can receive input from a publisher.
///
/// A `Subscriber` instance receives a stream of elements from a `Publisher`, along with
/// life cycle events describing changes to their relationship. A given subscribers
/// `Input` and `Failure` associated types must match the `Output` and `Failure` of its
/// corresponding publisher.
///
/// You connect a subscriber to a publisher by calling the publishers `subscribe(_:)`
/// method. After making this call, the publisher invokes the subscribers
/// `receive(subscription:)` method. This gives the subscriber a `Subscription` instance,
/// which it uses to demand elements from the publisher, and to optionally cancel
/// the subscription. After the subscriber makes an initial demand, the publisher calls
/// `receive(_:)`, possibly asynchronously, to deliver newly-published elements.
/// If the publisher stops publishing, it calls `receive(completion:)`, using a parameter
/// of type `Subscribers.Completion` to indicate whether publishing completes normally or
/// with an error.
///
/// OpenCombine provides the following subscribers as operators on the `Publisher` type:
///
/// - `sink(receiveCompletion:receiveValue:)` executes arbitrary closures when
/// it receives a completion signal and each time it receives a new element.
/// - `assign(to:on:)` writes each newly-received value to a property identified by
/// a key path on a given instance.
public protocol Subscriber<Input, Failure>: CustomCombineIdentifierConvertible {
/// The kind of values this subscriber receives.
associatedtype Input
/// The kind of errors this subscriber might receive.
///
/// Use `Never` if this `Subscriber` cannot receive errors.
associatedtype Failure: Error
/// Tells the subscriber that it has successfully subscribed to the publisher and may
/// request items.
///
/// Use the received `Subscription` to request items from the publisher.
/// - Parameter subscription: A subscription that represents the connection between
/// publisher and subscriber.
func receive(subscription: Subscription)
/// Tells the subscriber that the publisher has produced an element.
///
/// - Parameter input: The published element.
/// - Returns: A `Subscribers.Demand` instance indicating how many more elements
/// the subscriber expects to receive.
func receive(_ input: Input) -> Subscribers.Demand
/// Tells the subscriber that the publisher has completed publishing, either normally
/// or with an error.
///
/// - Parameter completion: A `Subscribers.Completion` case indicating whether
/// publishing completed normally or with an error.
func receive(completion: Subscribers.Completion<Failure>)
}
/// A protocol that defines when and how to execute a closure.
///
/// You can use a scheduler to execute code as soon as possible, or after a future date.
/// Individual scheduler implementations use whatever time-keeping system makes sense
/// for them. Schedulers express this as their `SchedulerTimeType`. Since this type
/// conforms to `SchedulerTimeIntervalConvertible`, you can always express these times
/// with the convenience functions like `.milliseconds(500)`. Schedulers can accept
/// options to control how they execute the actions passed to them. These options may
/// control factors like which threads or dispatch queues execute the actions.
public protocol Scheduler<SchedulerTimeType> {
/// Describes an instant in time for this scheduler.
associatedtype SchedulerTimeType: Strideable
where SchedulerTimeType.Stride: SchedulerTimeIntervalConvertible
/// A type that defines options accepted by the scheduler.
///
/// This type is freely definable by each `Scheduler`. Typically, operations that
/// take a `Scheduler` parameter will also take `SchedulerOptions`.
associatedtype SchedulerOptions
/// This schedulers definition of the current moment in time.
var now: SchedulerTimeType { get }
/// The minimum tolerance allowed by the scheduler.
var minimumTolerance: SchedulerTimeType.Stride { get }
/// Performs the action at the next possible opportunity.
func schedule(options: SchedulerOptions?, _ action: @escaping () -> Void)
/// Performs the action at some time after the specified date.
func schedule(after date: SchedulerTimeType,
tolerance: SchedulerTimeType.Stride,
options: SchedulerOptions?,
_ action: @escaping () -> Void)
/// Performs the action at some time after the specified date, at the specified
/// frequency, optionally taking into account tolerance if possible.
func schedule(after date: SchedulerTimeType,
interval: SchedulerTimeType.Stride,
tolerance: SchedulerTimeType.Stride,
options: SchedulerOptions?,
_ action: @escaping () -> Void) -> Cancellable
}
#else
/// Declares that a type can transmit a sequence of values over time.
///
/// A publisher delivers elements to one or more `Subscriber` instances.
/// The subscribers `Input` and `Failure` associated types must match the `Output` and
/// `Failure` types declared by the publisher.
/// The publisher implements the `receive(subscriber:)`method to accept a subscriber.
///
/// After this, the publisher can call the following methods on the subscriber:
/// - `receive(subscription:)`: Acknowledges the subscribe request and returns
/// a `Subscription` instance. The subscriber uses the subscription to demand elements
/// from the publisher and can use it to cancel publishing.
/// - `receive(_:)`: Delivers one element from the publisher to the subscriber.
/// - `receive(completion:)`: Informs the subscriber that publishing has ended,
/// either normally or with an error.
///
/// Every `Publisher` must adhere to this contract for downstream subscribers to function
/// correctly.
///
/// Extensions on `Publisher` define a wide variety of _operators_ that you compose to
/// create sophisticated event-processing chains.
/// Each operator returns a type that implements the `Publisher` protocol
/// Most of these types exist as extensions on the `Publishers` enumeration.
/// For example, the `map(_:)` operator returns an instance of `Publishers.Map`.
///
/// # Creating Your Own Publishers
///
/// Rather than implementing the `Publisher` protocol yourself, you can create your own
/// publisher by using one of several types provided by the OpenCombine framework:
///
/// - Use a concrete subclass of `Subject`, such as `PassthroughSubject`, to publish
/// values on-demand by calling its `send(_:)` method.
/// - Use a `CurrentValueSubject` to publish whenever you update the subjects underlying
/// value.
/// - Add the `@Published` annotation to a property of one of your own types. In doing so,
/// the property gains a publisher that emits an event whenever the propertys value
/// changes. See the `Published` type for an example of this approach.
public protocol Publisher {
/// The kind of values published by this publisher.
associatedtype Output
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
associatedtype Failure: Error
/// Attaches the specified subscriber to this publisher.
///
/// Always call this function instead of `receive(subscriber:)`.
/// Adopters of `Publisher` must implement `receive(subscriber:)`. The implementation
/// of `subscribe(_:)` provided by `Publisher` calls through to
/// `receive(subscriber:)`.
///
/// - Parameter subscriber: The subscriber to attach to this publisher. After
/// attaching, the subscriber can start to receive values.
func receive<Subscriber: OpenCombine.Subscriber>(subscriber: Subscriber)
where Failure == Subscriber.Failure, Output == Subscriber.Input
}
/// A publisher that exposes a method for outside callers to publish elements.
///
/// A subject is a publisher that you can use to inject values into a stream, by calling
/// its `send()` method. This can be useful for adapting existing imperative code to the
/// Combine model.
public protocol Subject: AnyObject, Publisher {
/// Sends a value to the subscriber.
///
/// - Parameter value: The value to send.
func send(_ value: Output)
/// Sends a completion signal to the subscriber.
///
/// - Parameter completion: A `Completion` instance which indicates whether publishing
/// has finished normally or failed with an error.
func send(completion: Subscribers.Completion<Failure>)
/// Sends a subscription to the subscriber.
///
/// This call provides the `Subject` an opportunity to establish demand for any new
/// upstream subscriptions.
///
/// - Parameter subscription: The subscription instance through which the subscriber
/// can request elements.
func send(subscription: Subscription)
}
/// A publisher that provides an explicit means of connecting and canceling publication.
///
/// Use a `ConnectablePublisher` when you need to perform additional configuration or
/// setup prior to producing any elements.
///
/// This publisher doesnt produce any elements until you call its `connect()` method.
///
/// Use `makeConnectable()` to create a `ConnectablePublisher` from any publisher whose
/// failure type is `Never`.
public protocol ConnectablePublisher: Publisher {
/// Connects to the publisher, allowing it to produce elements, and returns
/// an instance with which to cancel publishing.
///
/// - Returns: A `Cancellable` instance that you use to cancel publishing.
func connect() -> Cancellable
}
/// A protocol that declares a type that can receive input from a publisher.
///
/// A `Subscriber` instance receives a stream of elements from a `Publisher`, along with
/// life cycle events describing changes to their relationship. A given subscribers
/// `Input` and `Failure` associated types must match the `Output` and `Failure` of its
/// corresponding publisher.
///
/// You connect a subscriber to a publisher by calling the publishers `subscribe(_:)`
/// method. After making this call, the publisher invokes the subscribers
/// `receive(subscription:)` method. This gives the subscriber a `Subscription` instance,
/// which it uses to demand elements from the publisher, and to optionally cancel
/// the subscription. After the subscriber makes an initial demand, the publisher calls
/// `receive(_:)`, possibly asynchronously, to deliver newly-published elements.
/// If the publisher stops publishing, it calls `receive(completion:)`, using a parameter
/// of type `Subscribers.Completion` to indicate whether publishing completes normally or
/// with an error.
///
/// OpenCombine provides the following subscribers as operators on the `Publisher` type:
///
/// - `sink(receiveCompletion:receiveValue:)` executes arbitrary closures when
/// it receives a completion signal and each time it receives a new element.
/// - `assign(to:on:)` writes each newly-received value to a property identified by
/// a key path on a given instance.
public protocol Subscriber: CustomCombineIdentifierConvertible {
/// The kind of values this subscriber receives.
associatedtype Input
/// The kind of errors this subscriber might receive.
///
/// Use `Never` if this `Subscriber` cannot receive errors.
associatedtype Failure: Error
/// Tells the subscriber that it has successfully subscribed to the publisher and may
/// request items.
///
/// Use the received `Subscription` to request items from the publisher.
/// - Parameter subscription: A subscription that represents the connection between
/// publisher and subscriber.
func receive(subscription: Subscription)
/// Tells the subscriber that the publisher has produced an element.
///
/// - Parameter input: The published element.
/// - Returns: A `Subscribers.Demand` instance indicating how many more elements
/// the subscriber expects to receive.
func receive(_ input: Input) -> Subscribers.Demand
/// Tells the subscriber that the publisher has completed publishing, either normally
/// or with an error.
///
/// - Parameter completion: A `Subscribers.Completion` case indicating whether
/// publishing completed normally or with an error.
func receive(completion: Subscribers.Completion<Failure>)
}
/// A protocol that defines when and how to execute a closure.
///
/// You can use a scheduler to execute code as soon as possible, or after a future date.
/// Individual scheduler implementations use whatever time-keeping system makes sense
/// for them. Schedulers express this as their `SchedulerTimeType`. Since this type
/// conforms to `SchedulerTimeIntervalConvertible`, you can always express these times
/// with the convenience functions like `.milliseconds(500)`. Schedulers can accept
/// options to control how they execute the actions passed to them. These options may
/// control factors like which threads or dispatch queues execute the actions.
public protocol Scheduler {
/// Describes an instant in time for this scheduler.
associatedtype SchedulerTimeType: Strideable
where SchedulerTimeType.Stride: SchedulerTimeIntervalConvertible
/// A type that defines options accepted by the scheduler.
///
/// This type is freely definable by each `Scheduler`. Typically, operations that
/// take a `Scheduler` parameter will also take `SchedulerOptions`.
associatedtype SchedulerOptions
/// This schedulers definition of the current moment in time.
var now: SchedulerTimeType { get }
/// The minimum tolerance allowed by the scheduler.
var minimumTolerance: SchedulerTimeType.Stride { get }
/// Performs the action at the next possible opportunity.
func schedule(options: SchedulerOptions?, _ action: @escaping () -> Void)
/// Performs the action at some time after the specified date.
func schedule(after date: SchedulerTimeType,
tolerance: SchedulerTimeType.Stride,
options: SchedulerOptions?,
_ action: @escaping () -> Void)
/// Performs the action at some time after the specified date, at the specified
/// frequency, optionally taking into account tolerance if possible.
func schedule(after date: SchedulerTimeType,
interval: SchedulerTimeType.Stride,
tolerance: SchedulerTimeType.Stride,
options: SchedulerOptions?,
_ action: @escaping () -> Void) -> Cancellable
}
#endif
@@ -11,6 +11,12 @@ internal enum ConduitList<Output, Failure: Error> {
case many(Set<ConduitBase<Output, Failure>>)
}
extension ConduitList: HasDefaultValue {
init() {
self = .empty
}
}
extension ConduitList {
internal mutating func insert(_ conduit: ConduitBase<Output, Failure>) {
switch self {
@@ -50,8 +56,4 @@ extension ConduitList {
self = .many(set)
}
}
internal mutating func removeAll() {
self = .empty
}
}
@@ -187,8 +187,7 @@ extension PublishedSubject {
return
}
self.downstream = nil
let parent = self.parent
self.parent = nil
let parent = self.parent.take()
lock.unlock()
parent?.disassociate(self)
}
@@ -48,8 +48,6 @@ internal class ReduceProducer<Downstream: Subscriber,
private var upstreamCompleted = false
private var empty = true
internal init(downstream: Downstream, initial: Output?, reduce: Reducer) {
self.downstream = downstream
self.initial = initial
@@ -100,7 +98,9 @@ internal class ReduceProducer<Downstream: Subscriber,
return
}
upstreamCompleted = true
self.completed = downstreamRequested || empty
if downstreamRequested {
self.completed = true
}
let completed = self.completed
let result = self.result
lock.unlock()
@@ -157,7 +157,6 @@ extension ReduceProducer: Subscriber {
lock.unlock()
return .none
}
empty = false
lock.unlock()
// Combine doesn't hold the lock when calling `receive(newValue:)`.
+30
View File
@@ -0,0 +1,30 @@
//
// Utils.swift
//
//
// Created by Sergej Jaskiewicz on 28.08.2021.
//
internal protocol HasDefaultValue {
init()
}
extension HasDefaultValue {
@inline(__always)
internal mutating func take() -> Self {
let taken = self
self = .init()
return taken
}
}
extension Array: HasDefaultValue {}
extension Dictionary: HasDefaultValue {}
extension Optional: HasDefaultValue {
init() {
self = nil
}
}
+5 -10
View File
@@ -85,8 +85,7 @@ public final class PassthroughSubject<Output, Failure: Error>: Subject {
}
active = false
self.completion = completion
let downstreams = self.downstreams
self.downstreams.removeAll()
let downstreams = self.downstreams.take()
lock.unlock()
downstreams.forEach { conduit in
conduit.finish(completion: completion)
@@ -168,13 +167,11 @@ extension PassthroughSubject {
override func finish(completion: Subscribers.Completion<Failure>) {
lock.lock()
guard let downstream = self.downstream else {
guard let downstream = self.downstream.take() else {
lock.unlock()
return
}
self.downstream = nil
let parent = self.parent
self.parent = nil
let parent = self.parent.take()
lock.unlock()
parent?.disassociate(self)
downstreamLock.lock()
@@ -197,13 +194,11 @@ extension PassthroughSubject {
override func cancel() {
lock.lock()
if self.downstream == nil {
if downstream.take() == nil {
lock.unlock()
return
}
self.downstream = nil
let parent = self.parent
self.parent = nil
let parent = self.parent.take()
lock.unlock()
parent?.disassociate(self)
}
@@ -0,0 +1,53 @@
//
// Publisher+Subscribe.swift
//
//
// Created by Sergej Jaskiewicz on 23.04.2023.
//
extension Publisher {
/// Attaches the specified subscriber to this publisher.
///
/// Always call this function instead of `receive(subscriber:)`.
/// Adopters of `Publisher` must implement `receive(subscriber:)`.
/// The implementation of `subscribe(_:)` in this extension calls through to
/// `receive(subscriber:)`.
/// - SeeAlso: `receive(subscriber:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`. After attaching,
/// the subscriber can start to receive values.
public func subscribe<Subscriber: OpenCombine.Subscriber>(_ subscriber: Subscriber)
where Failure == Subscriber.Failure, Output == Subscriber.Input
{
if let hook = DebugHook.getGlobalHook() {
if var marker = subscriber as? SubscriberTapMarker {
let anySubscriber = marker.inner
as! AnySubscriber<Subscriber.Input, Subscriber.Failure>
hook.willReceive(publisher: self, subscriber: anySubscriber)
receive(subscriber: subscriber)
hook.didReceive(publisher: self, subscriber: anySubscriber)
} else {
let tap = SubscriberTap(subscriber: subscriber)
hook.willReceive(publisher: self, subscriber: subscriber)
receive(subscriber: tap)
hook.didReceive(publisher: self, subscriber: subscriber)
}
} else {
receive(subscriber: subscriber)
}
}
/// Attaches the specified subject to this publisher.
///
/// - Parameter subject: The subject to attach to this publisher.
public func subscribe<Subject: OpenCombine.Subject>(
_ subject: Subject
) -> AnyCancellable
where Failure == Subject.Failure, Output == Subject.Output
{
let subscriber = SubjectSubscriber(subject)
self.subscribe(subscriber)
return AnyCancellable(subscriber)
}
}
-112
View File
@@ -1,112 +0,0 @@
//
// Publisher.swift
// OpenCombine
//
// Created by Sergej Jaskiewicz on 10.06.2019.
//
/// Declares that a type can transmit a sequence of values over time.
///
/// A publisher delivers elements to one or more `Subscriber` instances.
/// The subscribers `Input` and `Failure` associated types must match the `Output` and
/// `Failure` types declared by the publisher.
/// The publisher implements the `receive(subscriber:)`method to accept a subscriber.
///
/// After this, the publisher can call the following methods on the subscriber:
/// - `receive(subscription:)`: Acknowledges the subscribe request and returns
/// a `Subscription` instance. The subscriber uses the subscription to demand elements
/// from the publisher and can use it to cancel publishing.
/// - `receive(_:)`: Delivers one element from the publisher to the subscriber.
/// - `receive(completion:)`: Informs the subscriber that publishing has ended,
/// either normally or with an error.
///
/// Every `Publisher` must adhere to this contract for downstream subscribers to function
/// correctly.
///
/// Extensions on `Publisher` define a wide variety of _operators_ that you compose to
/// create sophisticated event-processing chains.
/// Each operator returns a type that implements the `Publisher` protocol
/// Most of these types exist as extensions on the `Publishers` enumeration.
/// For example, the `map(_:)` operator returns an instance of `Publishers.Map`.
///
/// # Creating Your Own Publishers
///
/// Rather than implementing the `Publisher` protocol yourself, you can create your own
/// publisher by using one of several types provided by the OpenCombine framework:
///
/// - Use a concrete subclass of `Subject`, such as `PassthroughSubject`, to publish
/// values on-demand by calling its `send(_:)` method.
/// - Use a `CurrentValueSubject` to publish whenever you update the subjects underlying
/// value.
/// - Add the `@Published` annotation to a property of one of your own types. In doing so,
/// the property gains a publisher that emits an event whenever the propertys value
/// changes. See the `Published` type for an example of this approach.
public protocol Publisher {
/// The kind of values published by this publisher.
associatedtype Output
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
associatedtype Failure: Error
/// Attaches the specified subscriber to this publisher.
///
/// Always call this function instead of `receive(subscriber:)`.
/// Adopters of `Publisher` must implement `receive(subscriber:)`. The implementation
/// of `subscribe(_:)` provided by `Publisher` calls through to
/// `receive(subscriber:)`.
///
/// - Parameter subscriber: The subscriber to attach to this publisher. After
/// attaching, the subscriber can start to receive values.
func receive<Subscriber: OpenCombine.Subscriber>(subscriber: Subscriber)
where Failure == Subscriber.Failure, Output == Subscriber.Input
}
extension Publisher {
/// Attaches the specified subscriber to this publisher.
///
/// Always call this function instead of `receive(subscriber:)`.
/// Adopters of `Publisher` must implement `receive(subscriber:)`.
/// The implementation of `subscribe(_:)` in this extension calls through to
/// `receive(subscriber:)`.
/// - SeeAlso: `receive(subscriber:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`. After attaching,
/// the subscriber can start to receive values.
public func subscribe<Subscriber: OpenCombine.Subscriber>(_ subscriber: Subscriber)
where Failure == Subscriber.Failure, Output == Subscriber.Input
{
if let hook = DebugHook.getGlobalHook() {
if var marker = subscriber as? SubscriberTapMarker {
let anySubscriber = marker.inner
as! AnySubscriber<Subscriber.Input, Subscriber.Failure>
hook.willReceive(publisher: self, subscriber: anySubscriber)
receive(subscriber: subscriber)
hook.didReceive(publisher: self, subscriber: anySubscriber)
} else {
let tap = SubscriberTap(subscriber: subscriber)
hook.willReceive(publisher: self, subscriber: subscriber)
receive(subscriber: tap)
hook.didReceive(publisher: self, subscriber: subscriber)
}
} else {
receive(subscriber: subscriber)
}
}
/// Attaches the specified subject to this publisher.
///
/// - Parameter subject: The subject to attach to this publisher.
public func subscribe<Subject: OpenCombine.Subject>(
_ subject: Subject
) -> AnyCancellable
where Failure == Subject.Failure, Output == Subject.Output
{
let subscriber = SubjectSubscriber(subject)
self.subscribe(subscriber)
return AnyCancellable(subscriber)
}
}
@@ -222,8 +222,7 @@ extension Publishers.Encode {
} catch {
lock.lock()
finished = true
let subscription = self.subscription
self.subscription = nil
let subscription = self.subscription.take()
lock.unlock()
subscription?.cancel()
downstream.receive(completion: .failure(error))
@@ -252,11 +251,10 @@ extension Publishers.Encode {
func cancel() {
lock.lock()
guard !finished, let subscription = self.subscription else {
guard !finished, let subscription = self.subscription.take() else {
lock.unlock()
return
}
self.subscription = nil
finished = true
lock.unlock()
subscription.cancel()
@@ -336,8 +334,7 @@ extension Publishers.Decode {
} catch {
lock.lock()
finished = true
let subscription = self.subscription
self.subscription = nil
let subscription = self.subscription.take()
lock.unlock()
subscription?.cancel()
downstream.receive(completion: .failure(error))
@@ -366,11 +363,10 @@ extension Publishers.Decode {
func cancel() {
lock.lock()
guard !finished, let subscription = self.subscription else {
guard !finished, let subscription = self.subscription.take() else {
lock.unlock()
return
}
self.subscription = nil
finished = true
lock.unlock()
subscription.cancel()
@@ -11,6 +11,8 @@
// Created by Sergej Jaskiewicz on 03/10/2019.
//
// swiftlint:disable large_tuple
extension Publisher {
/// Publishes the value of the key path.
///
@@ -31,7 +33,7 @@ extension Publisher {
/// .sink {
/// print ("Rolled: \($0)")
/// }
/// // Prints "Rolled: 6 (or some other random value).
/// // Prints "Rolled: 4 (or some other random value).
///
/// - Parameters:
/// - keyPath: The key path of a property on `Output`.
@@ -68,7 +70,7 @@ extension Publisher {
/// (total \(values.0 + values.1))
/// """)
/// }
/// // Prints "Rolled: 5, 3 (total: 8)" (or other random values).
/// // Prints "Rolled: 4, 1 (total: 5)" (or other random values).
///
/// - Parameters:
/// - keyPath0: The key path of a property on `Output`.
@@ -110,7 +112,7 @@ extension Publisher {
/// (total \(values.0 + values.1 + values.2))
/// """)
/// }
/// // Prints "Rolled: 2, 4, 3 (total: 9)" (or other random values).
/// // Prints "Rolled: 3, 5, 4 (total: 12)" (or other random values).
///
/// - Parameters:
/// - keyPath0: The key path of a property on `Output`.
+1 -2
View File
@@ -293,8 +293,7 @@ extension Just {
func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
guard let downstream = self.downstream else { return }
self.downstream = nil
guard let downstream = self.downstream.take() else { return }
_ = downstream.receive(value)
downstream.receive(completion: .finished)
}
@@ -122,8 +122,7 @@ extension Optional.OCombine {
func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
guard let downstream = self.downstream else { return }
self.downstream = nil
guard let downstream = self.downstream.take() else { return }
_ = downstream.receive(output)
downstream.receive(completion: .finished)
}
@@ -10,10 +10,10 @@ extension Publisher {
/// Raises a fatal error when its upstream publisher fails, and otherwise republishes
/// all received input.
///
/// Use `assertNoFailure()` for internal sanity checks that are active during testing.
/// However, it is important to note that, like its Swift counterpart
/// Use `assertNoFailure()` for internal integrity checks that are active during
/// testing. However, it is important to note that, like its Swift counterpart
/// `fatalError(_:)`, the `assertNoFailure()` operator asserts a fatal exception when
/// triggered in both development/testing _and_ shipping versions of code.
/// triggered during development and testing, _and_ in shipping versions of code.
///
/// In the example below, a `CurrentValueSubject` publishes the initial and second
/// values successfully. The third value, containing a `genericSubjectError`, causes
@@ -57,8 +57,8 @@ extension Publishers {
/// A publisher that raises a fatal error upon receiving any failure, and otherwise
/// republishes all received input.
///
/// Use this function for internal sanity checks that are active during testing but
/// do not impact performance of shipping code.
/// Use this function for internal integrity checks that are active during testing but
/// don't affect performance of shipping code.
public struct AssertNoFailure<Upstream: Publisher>: Publisher {
public typealias Output = Upstream.Output
@@ -331,9 +331,7 @@ extension Publishers.Buffer {
private func lockedPop(_ demand: Subscribers.Demand) -> [Input] {
assert(demand > 0)
guard let max = demand.max else {
let poppedValues = self.values
self.values = []
return poppedValues
return values.take()
}
let poppedValues = Array(values.prefix(max))
@@ -128,8 +128,7 @@ extension Publishers.CollectByCount {
lock.unlock()
return .none
}
let output = self.buffer
self.buffer = []
let output = self.buffer.take()
lock.unlock()
return downstream.receive(output) * count
}
@@ -143,8 +142,7 @@ extension Publishers.CollectByCount {
if buffer.isEmpty {
lock.unlock()
} else {
let buffer = self.buffer
self.buffer = []
let buffer = self.buffer.take()
lock.unlock()
_ = downstream.receive(buffer)
}
@@ -168,10 +166,9 @@ extension Publishers.CollectByCount {
func cancel() {
lock.lock()
if let subscription = self.subscription {
if let subscription = self.subscription.take() {
buffer = []
finished = true
self.subscription = nil
lock.unlock()
subscription.cancel()
} else {
@@ -233,7 +233,7 @@ extension Publishers.Concatenate {
private var suffixState = SubscriptionStatus.awaitingSubscription
private let suffix: Suffix
private var suffix: Suffix?
private var pending = Subscribers.Demand.none
@@ -266,8 +266,18 @@ extension Publishers.Concatenate {
prefixState.subscription ?? suffixState.subscription
prefixState = .terminal
suffixState = .terminal
lock.unlock()
upstreamSubscription?.cancel()
// We MUST release the object AFTER unlocking the lock,
// since releasing it may trigger execution of arbitrary code,
// for example, if the object has a deinit.
// When the object deallocates, its deinit is called, and holding
// the lock at that moment can lead to deadlocks.
withExtendedLifetime(suffix) {
suffix = nil
lock.unlock()
upstreamSubscription?.cancel()
}
}
var description: String { return "Concatenate" }
@@ -320,7 +330,7 @@ extension Publishers.Concatenate {
lock.unlock()
switch completion {
case .finished:
suffix.subscribe(SuffixSubscriber(inner: self))
suffix?.subscribe(SuffixSubscriber(inner: self))
case .failure:
downstream.receive(completion: completion)
}
@@ -212,8 +212,7 @@ extension Publishers.Debounce {
let generation = currentGeneration
currentValue = input
let due = scheduler.now.advanced(by: dueTime)
let previousCancellers = self.currentCancellers
currentCancellers.removeAll()
let previousCancellers = self.currentCancellers.take()
currentCancellers[generation] = .pending
lock.unlock()
let newCanceller = scheduler.schedule(after: due,
@@ -238,8 +237,7 @@ extension Publishers.Debounce {
return
}
state = .terminal
let previousCancellers = currentCancellers
currentCancellers.removeAll()
let previousCancellers = currentCancellers.take()
lock.unlock()
for canceller in previousCancellers.values {
canceller.cancel()
@@ -268,8 +266,7 @@ extension Publishers.Debounce {
return
}
state = .terminal
let previousCancellers = currentCancellers
currentCancellers.removeAll()
let previousCancellers = currentCancellers.take()
lock.unlock()
for canceller in previousCancellers.values {
canceller.cancel()
@@ -306,11 +303,10 @@ extension Publishers.Debounce {
return
}
guard let canceller = currentCancellers[generation] else {
guard let canceller = currentCancellers[generation].take() else {
lock.unlock()
return
}
currentCancellers[generation] = nil
let hasAnyDemand = downstreamDemand != .none
if hasAnyDemand {
@@ -139,8 +139,7 @@ extension Publishers.Drop {
func cancel() {
lock.lock()
let subscription = self.subscription
self.subscription = nil
let subscription = self.subscription.take()
lock.unlock()
subscription?.cancel()
}
@@ -204,8 +204,7 @@ extension Publishers.DropUntilOutput {
}
otherFinished = true
if let upstreamSubscription = self.upstreamSubscription {
self.upstreamSubscription = nil
if let upstreamSubscription = self.upstreamSubscription.take() {
lock.unlock()
upstreamSubscription.cancel()
} else {
@@ -229,10 +228,8 @@ extension Publishers.DropUntilOutput {
func cancel() {
lock.lock()
let upstreamSubscription = self.upstreamSubscription
let otherSubscription = self.otherSubscription
self.upstreamSubscription = nil
self.otherSubscription = nil
let upstreamSubscription = self.upstreamSubscription.take()
let otherSubscription = self.otherSubscription.take()
cancelled = true
lock.unlock()
@@ -221,8 +221,7 @@ extension Publishers.${instantiation} {
} catch {
lock.lock()
finished = true
let subscription = self.subscription
self.subscription = nil
let subscription = self.subscription.take()
lock.unlock()
subscription?.cancel()
downstream.receive(completion: .failure(error))
@@ -251,11 +250,10 @@ extension Publishers.${instantiation} {
func cancel() {
lock.lock()
guard !finished, let subscription = self.subscription else {
guard !finished, let subscription = self.subscription.take() else {
lock.unlock()
return
}
self.subscription = nil
finished = true
lock.unlock()
subscription.cancel()
@@ -14,7 +14,7 @@ extension Publisher {
/// the elements from one kind of publisher into a new publisher that is sent
/// to subscribers. Use `flatMap(maxPublishers:_:)` when you want to create a new
/// series of events for downstream subscribers based on the received value.
/// The closure creates the new `Publishe`` based on the received value.
/// The closure creates the new `Publisher` based on the received value.
/// The new `Publisher` can emit more than one event, and successful completion of
/// the new `Publisher` does not complete the overall stream.
/// Failure of the new `Publisher` will fail the overall stream.
@@ -304,8 +304,7 @@ extension Publishers.FlatMap {
}
if demand == .unlimited {
downstreamDemand = .unlimited
let buffer = self.buffer
self.buffer = []
let buffer = self.buffer.take()
let subscriptions = self.subscriptions
lock.unlock()
downstreamLock.lock()
@@ -361,10 +360,8 @@ extension Publishers.FlatMap {
return
}
cancelledOrCompleted = true
let subscriptions = self.subscriptions
self.subscriptions = [:]
let outerSubscription = self.outerSubscription
self.outerSubscription = nil
let subscriptions = self.subscriptions.take()
let outerSubscription = self.outerSubscription.take()
lock.unlock()
for (_, subscription) in subscriptions {
subscription.cancel()
@@ -450,8 +447,7 @@ extension Publishers.FlatMap {
return
}
cancelledOrCompleted = true
let subscriptions = self.subscriptions
self.subscriptions = [:]
let subscriptions = self.subscriptions.take()
lock.unlock()
for (i, subscription) in subscriptions where i != index {
subscription.cancel()
@@ -139,12 +139,12 @@ extension Publishers.HandleEvents {
private var status = SubscriptionStatus.awaitingSubscription
private let lock = UnfairLock.allocate()
public var receiveSubscription: ((Subscription) -> Void)?
public var receiveOutput: ((Upstream.Output) -> Void)?
public var receiveCompletion:
fileprivate var receiveSubscription: ((Subscription) -> Void)?
fileprivate var receiveOutput: ((Upstream.Output) -> Void)?
fileprivate var receiveCompletion:
((Subscribers.Completion<Upstream.Failure>) -> Void)?
public var receiveCancel: (() -> Void)?
public var receiveRequest: ((Subscribers.Demand) -> Void)?
fileprivate var receiveCancel: (() -> Void)?
fileprivate var receiveRequest: ((Subscribers.Demand) -> Void)?
private let downstream: Downstream
init(_ events: Publishers.HandleEvents<Upstream>, downstream: Downstream) {
@@ -6,6 +6,8 @@ ${template_header}
// Created by Sergej Jaskiewicz on 03/10/2019.
//
// swiftlint:disable large_tuple
%{
from gyb_opencombine_support import (
suffix_variadic,
@@ -0,0 +1,197 @@
//
// Publishers.PrefixUntilOutput.swift
//
//
// Created by Sergej Jaskiewicz on 08.11.2020.
//
extension Publisher {
/// Republishes elements until another publisher emits an element.
///
/// After the second publisher publishes an element, the publisher returned by this
/// method finishes.
///
/// - Parameter publisher: A second publisher.
/// - Returns: A publisher that republishes elements until the second publisher
/// publishes an element.
public func prefix<Other: Publisher>(
untilOutputFrom publisher: Other
) -> Publishers.PrefixUntilOutput<Self, Other> {
return .init(upstream: self, other: publisher)
}
}
extension Publishers {
public struct PrefixUntilOutput<Upstream: Publisher, Other: Publisher>: Publisher {
public typealias Output = Upstream.Output
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// Another publisher, whose first output causes this publisher to finish.
public let other: Other
public init(upstream: Upstream, other: Other) {
self.upstream = upstream
self.other = other
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Failure == Failure, Downstream.Input == Output
{
upstream.subscribe(Inner(downstream: subscriber, trigger: other))
}
}
}
extension Publishers.PrefixUntilOutput {
private final class Inner<Downstream: Subscriber>
: Subscriber,
Subscription
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private struct Termination: Subscriber {
let inner: Inner
var combineIdentifier: CombineIdentifier {
return inner.combineIdentifier
}
func receive(subscription: Subscription) {
inner.terminationReceive(subscription: subscription)
}
func receive(_ input: Other.Output) -> Subscribers.Demand {
return inner.terminationReceive(input)
}
func receive(completion: Subscribers.Completion<Other.Failure>) {
inner.terminationReceive(completion: completion)
}
}
private var termination: Termination?
private var prefixState = SubscriptionStatus.awaitingSubscription
private var terminationState = SubscriptionStatus.awaitingSubscription
private var triggered = false
private let lock = UnfairLock.allocate()
private let downstream: Downstream
init(downstream: Downstream, trigger: Other) {
self.downstream = downstream
let termination = Termination(inner: self)
self.termination = termination
trigger.subscribe(termination)
}
deinit {
lock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard case .awaitingSubscription = prefixState else {
lock.unlock()
subscription.cancel()
return
}
prefixState = triggered ? .terminal : .subscribed(subscription)
lock.unlock()
downstream.receive(subscription: self)
}
func receive(_ input: Input) -> Subscribers.Demand {
lock.lock()
guard case .subscribed = prefixState else {
lock.unlock()
return .none
}
lock.unlock()
return downstream.receive(input)
}
func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
let prefixState = self.prefixState
let terminationSubscription = terminationState.subscription
self.prefixState = .terminal
terminationState = .terminal
termination = nil
lock.unlock()
terminationSubscription?.cancel()
if case .subscribed = prefixState {
downstream.receive(completion: completion)
}
}
func request(_ demand: Subscribers.Demand) {
lock.lock()
guard case let .subscribed(subscription) = prefixState else {
lock.unlock()
return
}
lock.unlock()
subscription.request(demand)
}
func cancel() {
lock.lock()
let prefixSubscription = prefixState.subscription
let terminationSubscription = terminationState.subscription
prefixState = .terminal
terminationState = .terminal
lock.unlock()
prefixSubscription?.cancel()
terminationSubscription?.cancel()
}
// MARK: - Private
private func terminationReceive(subscription: Subscription) {
lock.lock()
guard case .awaitingSubscription = terminationState else {
lock.unlock()
subscription.cancel()
return
}
terminationState = .subscribed(subscription)
lock.unlock()
subscription.request(.max(1))
}
private func terminationReceive(_ input: Other.Output) -> Subscribers.Demand {
lock.lock()
guard case .subscribed = terminationState else {
lock.unlock()
return .none
}
let prefixSubscription = prefixState.subscription
prefixState = .terminal
terminationState = .terminal
termination = nil
triggered = true
lock.unlock()
prefixSubscription?.cancel()
downstream.receive(completion: .finished)
return .none
}
private func terminationReceive(
completion: Subscribers.Completion<Other.Failure>
) {
lock.lock()
terminationState = .terminal
termination = nil
lock.unlock()
}
}
}
@@ -75,9 +75,7 @@ extension Publishers {
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Upstream.Output == Downstream.Input, Downstream.Failure == Failure
{
let inner = Inner(downstream: subscriber, output: output)
upstream.subscribe(inner)
subscriber.receive(subscription: inner)
upstream.subscribe(Inner(downstream: subscriber, output: output))
}
}
}
@@ -123,11 +121,8 @@ extension Publishers.ReplaceError {
return
}
status = .subscribed(subscription)
let pendingDemand = self.pendingDemand
lock.unlock()
if pendingDemand != .none {
subscription.request(pendingDemand)
}
downstream.receive(subscription: self)
}
func receive(_ input: Input) -> Subscribers.Demand {
@@ -150,7 +145,7 @@ extension Publishers.ReplaceError {
func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
guard case .subscribed = status else {
guard case .subscribed = status, !terminated else {
lock.unlock()
return
}
@@ -230,8 +230,7 @@ extension Publishers.SwitchToLatest {
return .none
}
if let currentInnerSubscription = self.currentInnerSubscription {
self.currentInnerSubscription = nil
if let currentInnerSubscription = self.currentInnerSubscription.take() {
lock.unlock()
currentInnerSubscription.cancel()
lock.lock()
@@ -272,8 +271,7 @@ extension Publishers.SwitchToLatest {
lock.unlock()
}
case .failure:
let currentInnerSubscription = self.currentInnerSubscription
self.currentInnerSubscription = nil
let currentInnerSubscription = self.currentInnerSubscription.take()
sentCompletion = true
lock.unlock()
currentInnerSubscription?.cancel()
@@ -298,10 +296,8 @@ extension Publishers.SwitchToLatest {
func cancel() {
lock.lock()
cancelled = true
let currentInnerSubscription = self.currentInnerSubscription
self.currentInnerSubscription = nil
let outerSubscription = self.outerSubscription
self.outerSubscription = nil
let currentInnerSubscription = self.currentInnerSubscription.take()
let outerSubscription = self.outerSubscription.take()
lock.unlock()
currentInnerSubscription?.cancel()
@@ -386,8 +382,7 @@ extension Publishers.SwitchToLatest {
return
}
cancelled = true
let outerSubscription = self.outerSubscription
self.outerSubscription = nil
let outerSubscription = self.outerSubscription.take()
sentCompletion = true
lock.unlock()
outerSubscription?.cancel()
@@ -0,0 +1,359 @@
//
// Publishers.Throttle.swift
//
//
// Created by Stuart Austin on 14/11/2020.
//
extension Publisher {
// swiftlint:disable generic_type_name line_length
/// Publishes either the most-recent or first element published by the upstream
/// publisher in the specified time interval.
///
/// Use `throttle(for:scheduler:latest:`` to selectively republish elements from
/// an upstream publisher during an interval you specify. Other elements received from
/// the upstream in the throttling interval arent republished.
///
/// In the example below, a `Timer.TimerPublisher` produces elements on 3-second
/// intervals; the `throttle(for:scheduler:latest:)` operator delivers the first
/// event, then republishes only the latest event in the following ten second
/// intervals:
///
/// cancellable = Timer.publish(every: 3.0, on: .main, in: .default)
/// .autoconnect()
/// .print("\(Date().description)")
/// .throttle(for: 10.0, scheduler: RunLoop.main, latest: true)
/// .sink(
/// receiveCompletion: { print ("Completion: \($0).") },
/// receiveValue: { print("Received Timestamp \($0).") }
/// )
///
/// // Prints:
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:26:57 +0000)
/// // Received Timestamp 2020-03-19 18:26:57 +0000.
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:27:00 +0000)
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:27:03 +0000)
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:27:06 +0000)
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:27:09 +0000)
/// // Received Timestamp 2020-03-19 18:27:09 +0000.
///
/// - Parameters:
/// - interval: The interval at which to find and emit either the most recent or
/// the first element, expressed in the time system of the scheduler.
/// - scheduler: The scheduler on which to publish elements.
/// - latest: A Boolean value that indicates whether to publish the most recent
/// element. If `false`, the publisher emits the first element received during
/// the interval.
/// - Returns: A publisher that emits either the most-recent or first element received
/// during the specified interval.
public func throttle<S>(for interval: S.SchedulerTimeType.Stride,
scheduler: S,
latest: Bool) -> Publishers.Throttle<Self, S>
where S: Scheduler
{
return .init(upstream: self,
interval: interval,
scheduler: scheduler,
latest: latest)
}
// swiftlint:enable generic_type_name line_length
}
extension Publishers {
/// A publisher that publishes either the most-recent or first element published by
/// the upstream publisher in a specified time interval.
public struct Throttle<Upstream, Context>: Publisher
where Upstream: Publisher, Context: Scheduler
{
/// The kind of values published by this publisher.
public typealias Output = Upstream.Output
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// The interval in which to find and emit the most recent element.
public let interval: Context.SchedulerTimeType.Stride
/// The scheduler on which to publish elements.
public let scheduler: Context
/// A Boolean value indicating whether to publish the most recent element.
///
/// If `false`, the publisher emits the first element received during
/// the interval.
public let latest: Bool
public init(upstream: Upstream,
interval: Context.SchedulerTimeType.Stride,
scheduler: Context,
latest: Bool) {
self.upstream = upstream
self.interval = interval
self.scheduler = scheduler
self.latest = latest
}
// swiftlint:disable generic_type_name
/// Attaches the specified subscriber to this publisher.
///
/// Implementations of ``Publisher`` must implement this method.
///
/// The provided implementation of ``Publisher/subscribe(_:)-4u8kn``calls
/// this method.
///
/// - Parameter subscriber: The subscriber to attach to this ``Publisher``,
/// after which it can receive values.
public func receive<S>(subscriber: S)
where S: Subscriber, Upstream.Failure == S.Failure, Upstream.Output == S.Input
{
let inner = Inner(interval: interval,
scheduler: scheduler,
latest: latest,
downstream: subscriber)
upstream.subscribe(inner)
}
// swiftlint:enable generic_type_name
}
}
extension Publishers.Throttle {
private final class Inner<Downstream: Subscriber>
: Subscriber,
Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private enum State {
case awaitingSubscription(Downstream)
case subscribed(Subscription, Downstream)
case pendingTerminal(Subscription, Downstream)
case terminal
}
private let lock = UnfairLock.allocate()
private let interval: Context.SchedulerTimeType.Stride
private let scheduler: Context
private let latest: Bool
private var state: State
private let downstreamLock = UnfairRecursiveLock.allocate()
private var lastEmissionTime: Context.SchedulerTimeType?
private var pendingInput: Input?
private var pendingCompletion: Subscribers.Completion<Failure>?
private var demand: Subscribers.Demand = .none
private var lastTime: Context.SchedulerTimeType
init(interval: Context.SchedulerTimeType.Stride,
scheduler: Context,
latest: Bool,
downstream: Downstream) {
self.state = .awaitingSubscription(downstream)
self.interval = interval
self.scheduler = scheduler
self.latest = latest
self.lastTime = scheduler.now
}
deinit {
lock.deallocate()
downstreamLock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard case let .awaitingSubscription(downstream) = state else {
lock.unlock()
subscription.cancel()
return
}
self.lastTime = scheduler.now
state = .subscribed(subscription, downstream)
lock.unlock()
subscription.request(.unlimited)
downstreamLock.lock()
downstream.receive(subscription: self)
downstreamLock.unlock()
}
func receive(_ input: Input) -> Subscribers.Demand {
lock.lock()
guard case .subscribed = state else {
lock.unlock()
return .none
}
let lastTime = scheduler.now
self.lastTime = lastTime
guard demand > .none else {
lock.unlock()
return .none
}
let hasScheduledOutput = (pendingInput != nil || pendingCompletion != nil)
if hasScheduledOutput && latest {
pendingInput = input
lock.unlock()
} else if !hasScheduledOutput {
let minimumEmissionTime =
lastEmissionTime.map { $0.advanced(by: interval) }
let emissionTime =
minimumEmissionTime.map { Swift.max(lastTime, $0) } ?? lastTime
demand -= 1
pendingInput = input
lock.unlock()
let action: () -> Void = { [weak self] in
self?.scheduledEmission()
}
if emissionTime == lastTime {
scheduler.schedule(action)
} else {
scheduler.schedule(after: emissionTime, action)
}
} else {
lock.unlock()
}
return .none
}
func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
guard case let .subscribed(subscription, downstream) = state else {
lock.unlock()
return
}
let lastTime = scheduler.now
self.lastTime = lastTime
state = .pendingTerminal(subscription, downstream)
let hasScheduledOutput = (pendingInput != nil || pendingCompletion != nil)
if hasScheduledOutput && pendingCompletion == nil {
pendingCompletion = completion
lock.unlock()
} else if !hasScheduledOutput {
pendingCompletion = completion
lock.unlock()
scheduler.schedule { [weak self] in
self?.scheduledEmission()
}
} else {
lock.unlock()
}
}
private func scheduledEmission() {
lock.lock()
let downstream: Downstream
switch state {
case .awaitingSubscription, .terminal:
lock.unlock()
return
case let .subscribed(_, foundDownstream),
let .pendingTerminal(_, foundDownstream):
downstream = foundDownstream
}
if self.pendingInput != nil && self.pendingCompletion == nil {
lastEmissionTime = scheduler.now
}
let pendingInput = self.pendingInput.take()
let pendingCompletion = self.pendingCompletion.take()
if pendingCompletion != nil {
state = .terminal
}
lock.unlock()
downstreamLock.lock()
let newDemand: Subscribers.Demand
if let input = pendingInput {
newDemand = downstream.receive(input)
} else {
newDemand = .none
}
if let completion = pendingCompletion {
downstream.receive(completion: completion)
}
downstreamLock.unlock()
guard newDemand > 0 else { return }
self.lock.lock()
demand += newDemand
self.lock.unlock()
}
func request(_ demand: Subscribers.Demand) {
guard demand > 0 else { return }
lock.lock()
guard case .subscribed = state else {
lock.unlock()
return
}
self.demand += demand
lock.unlock()
}
func cancel() {
lock.lock()
let subscription: Subscription?
switch state {
case let .subscribed(existingSubscription, _),
let .pendingTerminal(existingSubscription, _):
subscription = existingSubscription
case .awaitingSubscription, .terminal:
subscription = nil
}
state = .terminal
lock.unlock()
subscription?.cancel()
}
var description: String { return "Throttle" }
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
var playgroundDescription: Any { return description }
}
}
@@ -0,0 +1,720 @@
//
// Publishers.Zip.swift
//
// Created by Eric Patey on 29.08.2019.
//
// swiftlint:disable large_tuple
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
extension Publishers {
/// A publisher created by applying the zip function to two upstream publishers.
public struct Zip<UpstreamA: Publisher, UpstreamB: Publisher>: Publisher
where UpstreamA.Failure == UpstreamB.Failure
{
/// The kind of values published by this publisher.
public typealias Output = (UpstreamA.Output, UpstreamB.Output)
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = UpstreamA.Failure
public let a: UpstreamA
public let b: UpstreamB
public init(_ a: UpstreamA, _ b: UpstreamB) {
self.a = a
self.b = b
}
/// This function is called to attach the specified `Subscriber` to this
/// `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<Downstream: Subscriber>(subscriber: Downstream) where
UpstreamB.Failure == Downstream.Failure,
Downstream.Input == (UpstreamA.Output, UpstreamB.Output)
{
_ = Inner<Downstream>(downstream: subscriber, a, b)
}
}
/// A publisher created by applying the zip function to three upstream publishers.
public struct Zip3<UpstreamA: Publisher,
UpstreamB: Publisher,
UpstreamC: Publisher>
: Publisher
where UpstreamA.Failure == UpstreamB.Failure,
UpstreamB.Failure == UpstreamC.Failure
{
/// The kind of values published by this publisher.
public typealias Output = (UpstreamA.Output, UpstreamB.Output, UpstreamC.Output)
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = UpstreamA.Failure
public let a: UpstreamA
public let b: UpstreamB
public let c: UpstreamC
public init(_ a: UpstreamA, _ b: UpstreamB, _ c: UpstreamC) {
self.a = a
self.b = b
self.c = c
}
/// This function is called to attach the specified `Subscriber` to this
/// `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<Downstream>(subscriber: Downstream)
where Downstream: Subscriber,
UpstreamC.Failure == Downstream.Failure,
Downstream.Input == (UpstreamA.Output, UpstreamB.Output, UpstreamC.Output)
{
_ = Inner<Downstream>(downstream: subscriber, a, b, c)
}
}
/// A publisher created by applying the zip function to four upstream publishers.
public struct Zip4<
UpstreamA: Publisher,
UpstreamB: Publisher,
UpstreamC: Publisher,
UpstreamD: Publisher
>: Publisher where
UpstreamA.Failure == UpstreamB.Failure,
UpstreamB.Failure == UpstreamC.Failure,
UpstreamC.Failure == UpstreamD.Failure
{
/// The kind of values published by this publisher.
public typealias Output = (
UpstreamA.Output,
UpstreamB.Output,
UpstreamC.Output,
UpstreamD.Output)
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = UpstreamA.Failure
public let a: UpstreamA
public let b: UpstreamB
public let c: UpstreamC
public let d: UpstreamD
public init(_ a: UpstreamA, _ b: UpstreamB, _ c: UpstreamC, _ d: UpstreamD) {
self.a = a
self.b = b
self.c = c
self.d = d
}
/// This function is called to attach the specified `Subscriber` to this
/// `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where UpstreamD.Failure == Downstream.Failure,
Downstream.Input == (
UpstreamA.Output,
UpstreamB.Output,
UpstreamC.Output,
UpstreamD.Output)
{
_ = Inner<Downstream>(downstream: subscriber, a, b, c, d)
}
}
}
extension Publisher {
/// Combine elements from another publisher and deliver pairs of elements as tuples.
///
/// The returned publisher waits until both publishers have emitted an event, then
/// delivers the oldest unconsumed event from each publisher together as a tuple to
/// the subscriber.
/// For example, if publisher `P1` emits elements `a` and `b`, and publisher `P2`
/// emits event `c`, the zip publisher emits the tuple `(a, c)`. It wont emit a
/// tuple with event `b` until `P2` emits another event.
/// If either upstream publisher finishes successfuly or fails with an error, the
/// zipped publisher does the same.
///
/// - Parameter other: Another publisher.
/// - Returns: A publisher that emits pairs of elements from the upstream publishers
/// as tuples.
public func zip<Other>(_ other: Other) -> Publishers.Zip<Self, Other>
where Other: Publisher, Self.Failure == Other.Failure
{
return Publishers.Zip(self, other)
}
/// Combine elements from another publisher and deliver a transformed output.
///
/// The returned publisher waits until both publishers have emitted an event, then
/// delivers the oldest unconsumed event from each publisher together as a tuple to
/// the subscriber.
/// For example, if publisher `P1` emits elements `a` and `b`, and publisher `P2`
/// emits event `c`, the zip publisher emits the tuple `(a, c)`. It wont emit a tuple
/// with event `b` until `P2` emits another event.
/// If either upstream publisher finishes successfuly or fails with an error, the
/// zipped publisher does the same.
///
/// - Parameter other: Another publisher.
/// - transform: A closure that receives the most recent value from each publisher
/// and returns a new value to publish.
/// - Returns: A publisher that emits pairs of elements from the upstream publishers
/// as tuples.
public func zip<Other, Result>(
_ other: Other,
_ transform: @escaping (Self.Output, Other.Output) -> Result)
-> Publishers.Map<Publishers.Zip<Self, Other>, Result>
where Other: Publisher, Self.Failure == Other.Failure
{
return Publishers.Map(upstream: Publishers.Zip(self, other), transform: transform)
}
/// Combine elements from two other publishers and deliver groups of elements as
/// tuples.
///
/// The returned publisher waits until all three publishers have emitted an event,
/// then delivers the oldest unconsumed event from each publisher as a tuple to the
/// subscriber.
/// For example, if publisher `P1` emits elements `a` and `b`, and publisher `P2`
/// emits elements `c` and `d`, and publisher `P3` emits the event `e`, the zip
/// publisher emits the tuple `(a, c, e)`. It wont emit a tuple with elements `b` or
/// `d` until `P3` emits another event.
/// If any upstream publisher finishes successfuly or fails with an error, the zipped
/// publisher does the same.
///
/// - Parameters:
/// - publisher1: A second publisher.
/// - publisher2: A third publisher.
/// - Returns: A publisher that emits groups of elements from the upstream publishers
/// as tuples.
public func zip<Other1, Other2>(_ publisher1: Other1, _ publisher2: Other2)
-> Publishers.Zip3<Self, Other1, Other2>
where Other1: Publisher,
Other2: Publisher,
Self.Failure == Other1.Failure,
Other1.Failure == Other2.Failure
{
return Publishers.Zip3(self, publisher1, publisher2)
}
/// Combine elements from two other publishers and deliver a transformed output.
///
/// The returned publisher waits until all three publishers have emitted an event,
/// then delivers the oldest unconsumed event from each publisher as a tuple to the
/// subscriber.
/// For example, if publisher `P1` emits elements `a` and `b`, and publisher `P2`
/// emits elements `c` and `d`, and publisher `P3` emits the event `e`, the zip
/// publisher emits the tuple `(a, c, e)`. It wont emit a tuple with elements `b` or
/// `d` until `P3` emits another event.
/// If any upstream publisher finishes successfuly or fails with an error, the zipped
/// publisher does the same.
///
/// - Parameters:
/// - publisher1: A second publisher.
/// - publisher2: A third publisher.
/// - transform: A closure that receives the most recent value from each publisher
/// and returns a new value to publish.
/// - Returns: A publisher that emits groups of elements from the upstream publishers
/// as tuples.
public func zip<Other1, Other2, Result>(
_ publisher1: Other1,
_ publisher2: Other2,
_ transform: @escaping (Self.Output, Other1.Output, Other2.Output) -> Result)
-> Publishers.Map<Publishers.Zip3<Self, Other1, Other2>, Result>
where Other1: Publisher,
Other2: Publisher,
Self.Failure == Other1.Failure,
Other1.Failure == Other2.Failure
{
return Publishers.Map(upstream: Publishers.Zip3(self, publisher1, publisher2),
transform: transform)
}
/// Combine elements from three other publishers and deliver groups of elements as
/// tuples.
///
/// The returned publisher waits until all four publishers have emitted an event, then
/// delivers the oldest unconsumed event from each publisher as a tuple to the
/// subscriber.
/// For example, if publisher `P1` emits elements `a` and `b`, and publisher `P2`
/// emits elements `c` and `d`, and publisher `P3` emits the elements `e` and `f`, and
/// publisher `P4` emits the event `g`, the zip publisher emits the tuple
/// `(a, c, e, g)`. It wont emit a tuple with elements `b`, `d`, or `f` until `P4`
/// emits another event.
/// If any upstream publisher finishes successfuly or fails with an error, the zipped
/// publisher does the same.
///
/// - Parameters:
/// - publisher1: A second publisher.
/// - publisher2: A third publisher.
/// - publisher3: A fourth publisher.
/// - Returns: A publisher that emits groups of elements from the upstream publishers
/// as tuples.
public func zip<Other1, Other2, Other3>(_ publisher1: Other1,
_ publisher2: Other2,
_ publisher3: Other3)
-> Publishers.Zip4<Self, Other1, Other2, Other3>
where Other1: Publisher,
Other2: Publisher,
Other3: Publisher,
Self.Failure == Other1.Failure,
Other1.Failure == Other2.Failure,
Other2.Failure == Other3.Failure
{
return Publishers.Zip4(self, publisher1, publisher2, publisher3)
}
/// Combine elements from three other publishers and deliver a transformed output.
///
/// The returned publisher waits until all four publishers have emitted an event, then
/// delivers the oldest unconsumed event from each publisher as a tuple to the
/// subscriber.
/// For example, if publisher `P1` emits elements `a` and `b`, and publisher `P2`
/// emits elements `c` and `d`, and publisher `P3` emits the elements `e` and `f`, and
/// publisher `P4` emits the event `g`, the zip publisher emits the tuple
/// `(a, c, e, g)`. It wont emit a tuple with elements `b`, `d`, or `f` until `P4`
/// emits another event.
/// If any upstream publisher finishes successfuly or fails with an error, the zipped
/// publisher does the same.
///
/// - Parameters:
/// - publisher1: A second publisher.
/// - publisher2: A third publisher.
/// - publisher3: A fourth publisher.
/// - transform: A closure that receives the most recent value from each publisher
/// and returns a new value to publish.
/// - Returns: A publisher that emits groups of elements from the upstream publishers
/// as tuples.
public func zip<Other1, Other2, Other3, Result>(
_ publisher1: Other1,
_ publisher2: Other2,
_ publisher3: Other3,
_ transform: @escaping (Self.Output, Other1.Output, Other2.Output, Other3.Output)
-> Result)
-> Publishers.Map<Publishers.Zip4<Self, Other1, Other2, Other3>, Result>
where Other1: Publisher,
Other2: Publisher,
Other3: Publisher,
Self.Failure == Other1.Failure,
Other1.Failure == Other2.Failure,
Other2.Failure == Other3.Failure
{
return Publishers.Map(upstream: Publishers.Zip4(self,
publisher1,
publisher2,
publisher3),
transform: transform)
}
}
extension Publishers.Zip {
private class Inner<Downstream: Subscriber>: InnerBase<Downstream>
where Downstream.Failure == Failure,
Downstream.Input == (UpstreamA.Output, UpstreamB.Output)
{
private lazy var aSubscriber = ChildSubscriber<UpstreamA, Downstream>(self, 0)
private lazy var bSubscriber = ChildSubscriber<UpstreamB, Downstream>(self, 1)
init(downstream: Downstream, _ a: UpstreamA, _ b: UpstreamB) {
super.init(downstream: downstream)
a.subscribe(aSubscriber)
b.subscribe(bSubscriber)
}
override fileprivate var upstreamSubscriptions: [ChildSubscription] {
return [aSubscriber, bSubscriber]
}
override fileprivate func dequeueValue() -> Downstream.Input {
return (aSubscriber.dequeueValue(), bSubscriber.dequeueValue())
}
}
}
extension Publishers.Zip3 {
private class Inner<Downstream: Subscriber>: InnerBase<Downstream>
where Downstream.Failure == Failure,
Downstream.Input == (UpstreamA.Output, UpstreamB.Output, UpstreamC.Output)
{
private lazy var aSubscriber = ChildSubscriber<UpstreamA, Downstream>(self, 0)
private lazy var bSubscriber = ChildSubscriber<UpstreamB, Downstream>(self, 1)
private lazy var cSubscriber = ChildSubscriber<UpstreamC, Downstream>(self, 2)
init(downstream: Downstream, _ a: UpstreamA, _ b: UpstreamB, _ c: UpstreamC) {
super.init(downstream: downstream)
a.subscribe(aSubscriber)
b.subscribe(bSubscriber)
c.subscribe(cSubscriber)
}
override fileprivate var upstreamSubscriptions: [ChildSubscription] {
return [aSubscriber, bSubscriber, cSubscriber]
}
override fileprivate func dequeueValue() -> Downstream.Input {
return (aSubscriber.dequeueValue(),
bSubscriber.dequeueValue(),
cSubscriber.dequeueValue())
}
}
}
extension Publishers.Zip4 {
private class Inner<Downstream: Subscriber>: InnerBase<Downstream>
where Downstream.Failure == Failure,
Downstream.Input == (
UpstreamA.Output,
UpstreamB.Output,
UpstreamC.Output,
UpstreamD.Output)
{
private lazy var aSubscriber = ChildSubscriber<UpstreamA, Downstream>(self, 0)
private lazy var bSubscriber = ChildSubscriber<UpstreamB, Downstream>(self, 1)
private lazy var cSubscriber = ChildSubscriber<UpstreamC, Downstream>(self, 2)
private lazy var dSubscriber = ChildSubscriber<UpstreamD, Downstream>(self, 3)
init(downstream: Downstream,
_ a: UpstreamA,
_ b: UpstreamB,
_ c: UpstreamC,
_ d: UpstreamD)
{
super.init(downstream: downstream)
a.subscribe(aSubscriber)
b.subscribe(bSubscriber)
c.subscribe(cSubscriber)
d.subscribe(dSubscriber)
}
override fileprivate var upstreamSubscriptions: [ChildSubscription] {
return [aSubscriber, bSubscriber, cSubscriber, dSubscriber]
}
override fileprivate func dequeueValue() -> Downstream.Input {
return (aSubscriber.dequeueValue(),
bSubscriber.dequeueValue(),
cSubscriber.dequeueValue(),
dSubscriber.dequeueValue())
}
}
}
private class InnerBase<Downstream: Subscriber>: CustomStringConvertible {
let description = "Zip"
private let lock = UnfairRecursiveLock.allocate()
private let downstream: Downstream
private var downstreamDemand = Subscribers.Demand.none
private var valueIsBeingProcessed = false
private var value: Downstream.Input?
private var isFinished = false
// The following two pieces of state are a hacky implementation of subtle Apple
// concurrency behaviors. Specifically, when Zip is processing an upstream child value
// and sending a resulting value downstream, multiple behaviors are changed.
// 1. If a downstream demand request comes in during this period, the demand request
// for that specific triggering upstream child will be communiated via the result
// of `.receive(_ input:)` INSTEAD of a later `.request(_ demand:)` call.
// (AppleRef: 001)
// 2. If an upstream `.finished` comes in during this time period, the "finished
// asssessment check" (AppleRef: 002) is skipped.
// If an upstream value is being processed when a downstream demand request comes in,
// the demand for that specfic upstream child will be communiated via the result
// of `.receive(_ input:)` INSTEAD of a later `.request(_ demand:)` call.
private final var processingValueForChild: ChildSubscription?
private final var demandReceivedWhileProcessing: Subscribers.Demand?
init(downstream: Downstream) {
self.downstream = downstream
}
deinit {
lock.deallocate()
}
fileprivate var upstreamSubscriptions: [ChildSubscription] {
abstractMethod()
}
fileprivate func dequeueValue() -> Downstream.Input {
abstractMethod()
}
fileprivate final func receivedSubscription(for child: ChildSubscription) {
lock.lock()
child.state = .active
let sendSubscriptionDownstream = upstreamSubscriptions
.filter { $0.state == .waitingForSubscription }
.isEmpty
lock.unlock()
if sendSubscriptionDownstream {
self.sendSubscriptionDownstream()
}
}
fileprivate final func receivedChildValue(
child: ChildSubscription,
_ lockedStoreValue: () -> Void
) -> Subscribers.Demand {
lock.lock()
lockedStoreValue()
defer {
checkShouldFinish()
lock.unlock()
}
if let dequeuedValue = maybeDequeueValue() {
value = dequeuedValue
assert(processingValueForChild == nil)
processingValueForChild = child
valueIsBeingProcessed = true
return processValue() ?? .none
} else {
return .none
}
}
fileprivate final func receivedCompletion(
_ completion: Subscribers.Completion<Downstream.Failure>,
forChild child: ChildSubscription)
{
switch completion {
case .failure:
downstream.receive(completion: completion)
lock.lock()
child.state = .failed
let subscriptionsToCancel = upstreamSubscriptions
lock.unlock()
subscriptionsToCancel.forEach { $0.cancel() }
case .finished:
lock.lock()
child.state = .finished
if !valueIsBeingProcessed {
valueIsBeingProcessed = true
if processingValueForChild == nil &&
!areMoreValuesPossible &&
!isFinished {
sendFinishDownstream()
} else {
processValue()
}
}
lock.unlock()
}
}
private func checkShouldFinish() {
if processingValueForChild == nil && upstreamSubscriptions.shouldFinish() {
sendFinishDownstream()
isFinished = true
}
}
private func maybeDequeueValue() -> Downstream.Input? {
return hasCompleteValueAvailable ? dequeueValue() : nil
}
private func sendSubscriptionDownstream() {
downstream.receive(subscription: self)
}
private var hasCompleteValueAvailable: Bool {
return upstreamSubscriptions.allSatisfy { $0.hasValue }
}
private var areMoreValuesPossible: Bool {
// More values are possible if all children are (active || have surplus)
return upstreamSubscriptions
.allSatisfy { $0.state == .active || $0.hasValue }
}
@discardableResult
private func processValue() -> Subscribers.Demand? {
assert(valueIsBeingProcessed)
lock.lock()
defer {
valueIsBeingProcessed = false
processingValueForChild = nil
demandReceivedWhileProcessing = nil
lock.unlock()
}
if let value = self.value {
if downstreamDemand != .none {
downstreamDemand -= 1
}
let newDemand = downstream.receive(value)
if newDemand != .none {
downstreamDemand += newDemand
demandReceivedWhileProcessing = newDemand
}
self.value = nil
}
return demandReceivedWhileProcessing
}
private func sendRequestUpstream(demand: Subscribers.Demand) {
lock.lock()
let subscriptionsToRequest = upstreamSubscriptions
.filter { $0.childIndex != processingValueForChild?.childIndex }
lock.unlock()
subscriptionsToRequest.forEach { $0.request(demand) }
}
private func sendFinishDownstream() {
downstream.receive(completion: .finished)
lock.lock()
let activeChildren = upstreamSubscriptions.filter { $0.state == .active }
lock.unlock()
activeChildren.forEach { $0.cancel() }
}
}
extension InnerBase: Subscription {
fileprivate final func request(_ demand: Subscribers.Demand) {
guard demand != .none else {
fatalError()
}
lock.lock()
downstreamDemand += demand
sendRequestUpstream(demand: demand)
if valueIsBeingProcessed {
demandReceivedWhileProcessing = demand
} else {
valueIsBeingProcessed = true
processValue()
}
lock.unlock()
}
fileprivate final func cancel() {
lock.lock()
let subscriptionsToCancel = upstreamSubscriptions
lock.unlock()
subscriptionsToCancel.forEach { $0.cancel() }
}
}
extension Array where Element == ChildSubscription {
func shouldFinish() -> Bool {
for subscription in self
where subscription.state == .finished && !subscription.hasValue{
return true
}
return false
}
}
private enum ChildState {
case waitingForSubscription
case active
case finished
case failed
case canceled
}
// Note that it's critical that this protocol not have any associated types - specifically
// note that it does not refer to `Upstream`.
// This allows `InnerBase` to do most of the heavy lifting without regard to the
// upstream publisher's value type.
private protocol ChildSubscription: AnyObject, Subscription {
var state: ChildState { get set }
var childIndex: Int { get }
var hasValue: Bool { get }
}
private final class ChildSubscriber<Upstream: Publisher, Downstream: Subscriber>
where Upstream.Failure == Downstream.Failure
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
fileprivate final var state: ChildState = .waitingForSubscription
fileprivate final var upstreamSubscription: Subscription?
private var values = [Upstream.Output]()
private unowned let parent: InnerBase<Downstream>
fileprivate let childIndex: Int
init(_ parent: InnerBase<Downstream>, _ childIndex: Int) {
self.parent = parent
self.childIndex = childIndex
}
fileprivate final func dequeueValue() -> Upstream.Output {
return values.remove(at: 0)
}
}
extension ChildSubscriber: ChildSubscription {
fileprivate final var hasValue: Bool {
return !values.isEmpty
}
}
extension ChildSubscriber: Subscription {
fileprivate final func request(_ demand: Subscribers.Demand) {
upstreamSubscription?.request(demand)
}
}
extension ChildSubscriber: Cancellable {
fileprivate final func cancel() {
upstreamSubscription?.cancel()
upstreamSubscription = nil
}
}
extension ChildSubscriber: Subscriber {
fileprivate final func receive(subscription: Subscription) {
if upstreamSubscription == nil {
upstreamSubscription = subscription
parent.receivedSubscription(for: self)
} else {
subscription.cancel()
}
}
fileprivate final func receive(_ input: Input) -> Subscribers.Demand {
return parent.receivedChildValue(child: self) { values.append(input) }
}
fileprivate final func receive(completion: Subscribers.Completion<Failure>) {
parent.receivedCompletion(completion, forChild: self)
}
}
@@ -136,8 +136,7 @@ extension Result.OCombine {
func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
guard let downstream = self.downstream else { return }
self.downstream = nil
guard let downstream = self.downstream.take() else { return }
_ = downstream.receive(output)
downstream.receive(completion: .finished)
}
+220
View File
@@ -0,0 +1,220 @@
${template_header}
//
// RootProtocols.swift
// OpenCombine
//
// Created by Sergej Jaskiewicz on 10.06.2019.
//
%{
variants = [(True, '#if compiler(>=5.7)'), (False, '#else')]
}%
% for primary_associated_types_supported, guard in variants:
${guard}
/// Declares that a type can transmit a sequence of values over time.
///
/// A publisher delivers elements to one or more `Subscriber` instances.
/// The subscribers `Input` and `Failure` associated types must match the `Output` and
/// `Failure` types declared by the publisher.
/// The publisher implements the `receive(subscriber:)`method to accept a subscriber.
///
/// After this, the publisher can call the following methods on the subscriber:
/// - `receive(subscription:)`: Acknowledges the subscribe request and returns
/// a `Subscription` instance. The subscriber uses the subscription to demand elements
/// from the publisher and can use it to cancel publishing.
/// - `receive(_:)`: Delivers one element from the publisher to the subscriber.
/// - `receive(completion:)`: Informs the subscriber that publishing has ended,
/// either normally or with an error.
///
/// Every `Publisher` must adhere to this contract for downstream subscribers to function
/// correctly.
///
/// Extensions on `Publisher` define a wide variety of _operators_ that you compose to
/// create sophisticated event-processing chains.
/// Each operator returns a type that implements the `Publisher` protocol
/// Most of these types exist as extensions on the `Publishers` enumeration.
/// For example, the `map(_:)` operator returns an instance of `Publishers.Map`.
///
/// # Creating Your Own Publishers
///
/// Rather than implementing the `Publisher` protocol yourself, you can create your own
/// publisher by using one of several types provided by the OpenCombine framework:
///
/// - Use a concrete subclass of `Subject`, such as `PassthroughSubject`, to publish
/// values on-demand by calling its `send(_:)` method.
/// - Use a `CurrentValueSubject` to publish whenever you update the subjects underlying
/// value.
/// - Add the `@Published` annotation to a property of one of your own types. In doing so,
/// the property gains a publisher that emits an event whenever the propertys value
/// changes. See the `Published` type for an example of this approach.
public protocol Publisher${'<Output, Failure>' if primary_associated_types_supported else ''} {
/// The kind of values published by this publisher.
associatedtype Output
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
associatedtype Failure: Error
/// Attaches the specified subscriber to this publisher.
///
/// Always call this function instead of `receive(subscriber:)`.
/// Adopters of `Publisher` must implement `receive(subscriber:)`. The implementation
/// of `subscribe(_:)` provided by `Publisher` calls through to
/// `receive(subscriber:)`.
///
/// - Parameter subscriber: The subscriber to attach to this publisher. After
/// attaching, the subscriber can start to receive values.
func receive<Subscriber: OpenCombine.Subscriber>(subscriber: Subscriber)
where Failure == Subscriber.Failure, Output == Subscriber.Input
}
/// A publisher that exposes a method for outside callers to publish elements.
///
/// A subject is a publisher that you can use to inject values into a stream, by calling
/// its `send()` method. This can be useful for adapting existing imperative code to the
/// Combine model.
public protocol Subject${'<Output, Failure>' if primary_associated_types_supported else ''}: AnyObject, Publisher {
/// Sends a value to the subscriber.
///
/// - Parameter value: The value to send.
func send(_ value: Output)
/// Sends a completion signal to the subscriber.
///
/// - Parameter completion: A `Completion` instance which indicates whether publishing
/// has finished normally or failed with an error.
func send(completion: Subscribers.Completion<Failure>)
/// Sends a subscription to the subscriber.
///
/// This call provides the `Subject` an opportunity to establish demand for any new
/// upstream subscriptions.
///
/// - Parameter subscription: The subscription instance through which the subscriber
/// can request elements.
func send(subscription: Subscription)
}
/// A publisher that provides an explicit means of connecting and canceling publication.
///
/// Use a `ConnectablePublisher` when you need to perform additional configuration or
/// setup prior to producing any elements.
///
/// This publisher doesnt produce any elements until you call its `connect()` method.
///
/// Use `makeConnectable()` to create a `ConnectablePublisher` from any publisher whose
/// failure type is `Never`.
public protocol ConnectablePublisher${'<Output, Failure>' if primary_associated_types_supported else ''}: Publisher {
/// Connects to the publisher, allowing it to produce elements, and returns
/// an instance with which to cancel publishing.
///
/// - Returns: A `Cancellable` instance that you use to cancel publishing.
func connect() -> Cancellable
}
/// A protocol that declares a type that can receive input from a publisher.
///
/// A `Subscriber` instance receives a stream of elements from a `Publisher`, along with
/// life cycle events describing changes to their relationship. A given subscribers
/// `Input` and `Failure` associated types must match the `Output` and `Failure` of its
/// corresponding publisher.
///
/// You connect a subscriber to a publisher by calling the publishers `subscribe(_:)`
/// method. After making this call, the publisher invokes the subscribers
/// `receive(subscription:)` method. This gives the subscriber a `Subscription` instance,
/// which it uses to demand elements from the publisher, and to optionally cancel
/// the subscription. After the subscriber makes an initial demand, the publisher calls
/// `receive(_:)`, possibly asynchronously, to deliver newly-published elements.
/// If the publisher stops publishing, it calls `receive(completion:)`, using a parameter
/// of type `Subscribers.Completion` to indicate whether publishing completes normally or
/// with an error.
///
/// OpenCombine provides the following subscribers as operators on the `Publisher` type:
///
/// - `sink(receiveCompletion:receiveValue:)` executes arbitrary closures when
/// it receives a completion signal and each time it receives a new element.
/// - `assign(to:on:)` writes each newly-received value to a property identified by
/// a key path on a given instance.
public protocol Subscriber${'<Input, Failure>' if primary_associated_types_supported else ''}: CustomCombineIdentifierConvertible {
/// The kind of values this subscriber receives.
associatedtype Input
/// The kind of errors this subscriber might receive.
///
/// Use `Never` if this `Subscriber` cannot receive errors.
associatedtype Failure: Error
/// Tells the subscriber that it has successfully subscribed to the publisher and may
/// request items.
///
/// Use the received `Subscription` to request items from the publisher.
/// - Parameter subscription: A subscription that represents the connection between
/// publisher and subscriber.
func receive(subscription: Subscription)
/// Tells the subscriber that the publisher has produced an element.
///
/// - Parameter input: The published element.
/// - Returns: A `Subscribers.Demand` instance indicating how many more elements
/// the subscriber expects to receive.
func receive(_ input: Input) -> Subscribers.Demand
/// Tells the subscriber that the publisher has completed publishing, either normally
/// or with an error.
///
/// - Parameter completion: A `Subscribers.Completion` case indicating whether
/// publishing completed normally or with an error.
func receive(completion: Subscribers.Completion<Failure>)
}
/// A protocol that defines when and how to execute a closure.
///
/// You can use a scheduler to execute code as soon as possible, or after a future date.
/// Individual scheduler implementations use whatever time-keeping system makes sense
/// for them. Schedulers express this as their `SchedulerTimeType`. Since this type
/// conforms to `SchedulerTimeIntervalConvertible`, you can always express these times
/// with the convenience functions like `.milliseconds(500)`. Schedulers can accept
/// options to control how they execute the actions passed to them. These options may
/// control factors like which threads or dispatch queues execute the actions.
public protocol Scheduler${'<SchedulerTimeType>' if primary_associated_types_supported else ''} {
/// Describes an instant in time for this scheduler.
associatedtype SchedulerTimeType: Strideable
where SchedulerTimeType.Stride: SchedulerTimeIntervalConvertible
/// A type that defines options accepted by the scheduler.
///
/// This type is freely definable by each `Scheduler`. Typically, operations that
/// take a `Scheduler` parameter will also take `SchedulerOptions`.
associatedtype SchedulerOptions
/// This schedulers definition of the current moment in time.
var now: SchedulerTimeType { get }
/// The minimum tolerance allowed by the scheduler.
var minimumTolerance: SchedulerTimeType.Stride { get }
/// Performs the action at the next possible opportunity.
func schedule(options: SchedulerOptions?, _ action: @escaping () -> Void)
/// Performs the action at some time after the specified date.
func schedule(after date: SchedulerTimeType,
tolerance: SchedulerTimeType.Stride,
options: SchedulerOptions?,
_ action: @escaping () -> Void)
/// Performs the action at some time after the specified date, at the specified
/// frequency, optionally taking into account tolerance if possible.
func schedule(after date: SchedulerTimeType,
interval: SchedulerTimeType.Stride,
tolerance: SchedulerTimeType.Stride,
options: SchedulerOptions?,
_ action: @escaping () -> Void) -> Cancellable
}
% end
#endif
-45
View File
@@ -29,51 +29,6 @@ public protocol SchedulerTimeIntervalConvertible {
static func nanoseconds(_ ns: Int) -> Self
}
/// A protocol that defines when and how to execute a closure.
///
/// You can use a scheduler to execute code as soon as possible, or after a future date.
/// Individual scheduler implementations use whatever time-keeping system makes sense
/// for them. Schedulers express this as their `SchedulerTimeType`. Since this type
/// conforms to `SchedulerTimeIntervalConvertible`, you can always express these times
/// with the convenience functions like `.milliseconds(500)`. Schedulers can accept
/// options to control how they execute the actions passed to them. These options may
/// control factors like which threads or dispatch queues execute the actions.
public protocol Scheduler {
/// Describes an instant in time for this scheduler.
associatedtype SchedulerTimeType: Strideable
where SchedulerTimeType.Stride: SchedulerTimeIntervalConvertible
/// A type that defines options accepted by the scheduler.
///
/// This type is freely definable by each `Scheduler`. Typically, operations that
/// take a `Scheduler` parameter will also take `SchedulerOptions`.
associatedtype SchedulerOptions
/// This schedulers definition of the current moment in time.
var now: SchedulerTimeType { get }
/// The minimum tolerance allowed by the scheduler.
var minimumTolerance: SchedulerTimeType.Stride { get }
/// Performs the action at the next possible opportunity.
func schedule(options: SchedulerOptions?, _ action: @escaping () -> Void)
/// Performs the action at some time after the specified date.
func schedule(after date: SchedulerTimeType,
tolerance: SchedulerTimeType.Stride,
options: SchedulerOptions?,
_ action: @escaping () -> Void)
/// Performs the action at some time after the specified date, at the specified
/// frequency, optionally taking into account tolerance if possible.
func schedule(after date: SchedulerTimeType,
interval: SchedulerTimeType.Stride,
tolerance: SchedulerTimeType.Stride,
options: SchedulerOptions?,
_ action: @escaping () -> Void) -> Cancellable
}
extension Scheduler {
/// Performs the action at some time after the specified date, using the schedulers
+17
View File
@@ -0,0 +1,17 @@
//
// Subject+Void.swift
// OpenCombine
//
// Created by Sergej Jaskiewicz on 10.06.2019.
//
extension Subject where Output == Void {
/// Sends a void value to the subscriber.
///
/// Use `Void` inputs and outputs when you want to signal that an event has occurred,
/// but dont need to send the event itself.
public func send() {
send(())
}
}
-45
View File
@@ -1,45 +0,0 @@
//
// Subject.swift
// OpenCombine
//
// Created by Sergej Jaskiewicz on 10.06.2019.
//
/// A publisher that exposes a method for outside callers to publish elements.
///
/// A subject is a publisher that you can use to inject values into a stream, by calling
/// its `send()` method. This can be useful for adapting existing imperative code to the
/// Combine model.
public protocol Subject: AnyObject, Publisher {
/// Sends a value to the subscriber.
///
/// - Parameter value: The value to send.
func send(_ value: Output)
/// Sends a completion signal to the subscriber.
///
/// - Parameter completion: A `Completion` instance which indicates whether publishing
/// has finished normally or failed with an error.
func send(completion: Subscribers.Completion<Failure>)
/// Sends a subscription to the subscriber.
///
/// This call provides the `Subject` an opportunity to establish demand for any new
/// upstream subscriptions.
///
/// - Parameter subscription: The subscription instance through which the subscriber
/// can request elements.
func send(subscription: Subscription)
}
extension Subject where Output == Void {
/// Sends a void value to the subscriber.
///
/// Use `Void` inputs and outputs when you want to signal that an event has occurred,
/// but dont need to send the event itself.
public func send() {
send(())
}
}
+20
View File
@@ -0,0 +1,20 @@
//
// Subscriber+Void.swift
// OpenCombine
//
// Created by Sergej Jaskiewicz on 10.06.2019.
//
extension Subscriber where Input == Void {
/// Tells the subscriber that a publisher of void elements is ready to receive further
/// requests.
///
/// Use `Void` inputs and outputs when you want to signal that an event has occurred,
/// but dont need to send the event itself.
/// - Returns: A `Subscribers.Demand` instance indicating how many more elements
/// the subscriber expects to receive.
public func receive() -> Subscribers.Demand {
return receive(())
}
}
-76
View File
@@ -1,76 +0,0 @@
//
// Subscriber.swift
// OpenCombine
//
// Created by Sergej Jaskiewicz on 10.06.2019.
//
/// A protocol that declares a type that can receive input from a publisher.
///
/// A `Subscriber` instance receives a stream of elements from a `Publisher`, along with
/// life cycle events describing changes to their relationship. A given subscribers
/// `Input` and `Failure` associated types must match the `Output` and `Failure` of its
/// corresponding publisher.
///
/// You connect a subscriber to a publisher by calling the publishers `subscribe(_:)`
/// method. After making this call, the publisher invokes the subscribers
/// `receive(subscription:)` method. This gives the subscriber a `Subscription` instance,
/// which it uses to demand elements from the publisher, and to optionally cancel
/// the subscription. After the subscriber makes an initial demand, the publisher calls
/// `receive(_:)`, possibly asynchronously, to deliver newly-published elements.
/// If the publisher stops publishing, it calls `receive(completion:)`, using a parameter
/// of type `Subscribers.Completion` to indicate whether publishing completes normally or
/// with an error.
///
/// OpenCombine provides the following subscribers as operators on the `Publisher` type:
///
/// - `sink(receiveCompletion:receiveValue:)` executes arbitrary closures when
/// it receives a completion signal and each time it receives a new element.
/// - `assign(to:on:)` writes each newly-received value to a property identified by
/// a key path on a given instance.
public protocol Subscriber: CustomCombineIdentifierConvertible {
/// The kind of values this subscriber receives.
associatedtype Input
/// The kind of errors this subscriber might receive.
///
/// Use `Never` if this `Subscriber` cannot receive errors.
associatedtype Failure: Error
/// Tells the subscriber that it has successfully subscribed to the publisher and may
/// request items.
///
/// Use the received `Subscription` to request items from the publisher.
/// - Parameter subscription: A subscription that represents the connection between
/// publisher and subscriber.
func receive(subscription: Subscription)
/// Tells the subscriber that the publisher has produced an element.
///
/// - Parameter input: The published element.
/// - Returns: A `Subscribers.Demand` instance indicating how many more elements
/// the subscriber expects to receive.
func receive(_ input: Input) -> Subscribers.Demand
/// Tells the subscriber that the publisher has completed publishing, either normally
/// or with an error.
///
/// - Parameter completion: A `Subscribers.Completion` case indicating whether
/// publishing completed normally or with an error.
func receive(completion: Subscribers.Completion<Failure>)
}
extension Subscriber where Input == Void {
/// Tells the subscriber that a publisher of void elements is ready to receive further
/// requests.
///
/// Use `Void` inputs and outputs when you want to signal that an event has occurred,
/// but dont need to send the event itself.
/// - Returns: A `Subscribers.Demand` instance indicating how many more elements
/// the subscriber expects to receive.
public func receive() -> Subscribers.Demand {
return receive(())
}
}
@@ -112,8 +112,17 @@ extension Subscribers {
lock.assertOwner()
#endif
status = .terminal
object = nil
lock.unlock()
// We MUST release the object AFTER unlocking the lock,
// since releasing it may trigger execution of arbitrary code,
// for example, if the object has a deinit.
// When the object deallocates, its deinit is called, and holding
// the lock at that moment can lead to deadlocks.
withExtendedLifetime(object) {
object = nil
lock.unlock()
}
}
}
}
@@ -5,6 +5,10 @@
// Created by Sergej Jaskiewicz on 11.06.2019.
//
#if canImport(_Concurrency) && compiler(>=5.5)
import _Concurrency
#endif
extension Subscribers {
/// A signal that a publisher doesnt produce additional elements, either due to
@@ -23,6 +27,10 @@ extension Subscribers.Completion: Equatable where Failure: Equatable {}
extension Subscribers.Completion: Hashable where Failure: Hashable {}
#if canImport(_Concurrency) && compiler(>=5.5) || compiler(>=5.5.1)
extension Subscribers.Completion: Sendable {}
#endif
extension Subscribers.Completion {
private enum CodingKeys: String, CodingKey {
case success = "success"
@@ -70,4 +78,13 @@ extension Subscribers.Completion {
return .failure(error)
}
}
internal var failure: Failure? {
switch self {
case .finished:
return nil
case .failure(let failure):
return failure
}
}
}
@@ -5,7 +5,11 @@
// Created by Sergej Jaskiewicz on 10.06.2019.
//
// swiftlint:disable shorthand_operator - because of false positives here
// swiftlint:disable attributes
#if canImport(_Concurrency) && compiler(>=5.5)
import _Concurrency
#endif
extension Subscribers {
@@ -466,3 +470,7 @@ extension Subscribers {
}
}
}
#if canImport(_Concurrency) && compiler(>=5.5) || compiler(>=5.5.1)
extension Subscribers.Demand: Sendable {}
#endif
@@ -73,8 +73,22 @@ extension Subscribers {
public func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
status = .terminal
let receiveCompletion = self.receiveCompletion
terminateAndConsumeLock()
self.receiveCompletion = { _ in }
// We MUST release the closures AFTER unlocking the lock,
// since releasing a closure may trigger execution of arbitrary code,
// for example, if the closure captures an object with a deinit.
// When closure deallocates, the object's deinit is called, and holding
// the lock at that moment can lead to deadlocks.
// See https://github.com/OpenCombine/OpenCombine/issues/208
withExtendedLifetime(receiveValue) {
receiveValue = { _ in }
lock.unlock()
}
receiveCompletion(completion)
}
@@ -84,18 +98,21 @@ extension Subscribers {
lock.unlock()
return
}
terminateAndConsumeLock()
subscription.cancel()
}
private func terminateAndConsumeLock() {
#if DEBUG
lock.assertOwner()
#endif
status = .terminal
receiveValue = { _ in }
receiveCompletion = { _ in }
lock.unlock()
// We MUST release the closures AFTER unlocking the lock,
// since releasing a closure may trigger execution of arbitrary code,
// for example, if the closure captures an object with a deinit.
// When closure deallocates, the object's deinit is called, and holding
// the lock at that moment can lead to deadlocks.
// See https://github.com/OpenCombine/OpenCombine/issues/208
withExtendedLifetime((receiveValue, receiveCompletion)) {
receiveCompletion = { _ in }
receiveValue = { _ in }
lock.unlock()
}
subscription.cancel()
}
}
}
+1
View File
@@ -5,6 +5,7 @@
// Created by Sergej Jaskiewicz on 27.09.2020.
//
// swiftlint:disable:next type_name
public protocol _Introspection: AnyObject {
func willReceive<Upstream: Publisher, Downstream: Subscriber>(
@@ -50,13 +50,7 @@ extension DispatchQueue {
/// - Parameter other: Another dispatch queue time.
/// - Returns: The time interval between this time and the provided time.
public func distance(to other: SchedulerTimeType) -> Stride {
let start = dispatchTime.rawValue
let end = other.dispatchTime.rawValue
return .nanoseconds(
end >= start
? Int(Int64(bitPattern: end) - Int64(bitPattern: start))
: -Int(Int64(bitPattern: start) - Int64(bitPattern: end))
)
return Stride(dispatchTime.polyfillDistance(to: other.dispatchTime))
}
/// Returns a dispatch queue scheduler time calculated by advancing
@@ -104,8 +98,17 @@ extension DispatchQueue {
/// value of the conforming type.
public typealias Magnitude = Int
private var _nanoseconds: Int64
/// The value of this time interval in nanoseconds.
public var magnitude: Int
public var magnitude: Int {
get {
return Int(_nanoseconds)
}
set {
_nanoseconds = Int64(newValue)
}
}
/// A `DispatchTimeInterval` created with the value of this type
/// in nanoseconds.
@@ -113,8 +116,8 @@ extension DispatchQueue {
return .nanoseconds(magnitude)
}
private init(magnitude: Int) {
self.magnitude = magnitude
private init(magnitude: Int64) {
_nanoseconds = magnitude
}
/// Creates a dispatch queue time interval from the given
@@ -204,7 +207,7 @@ extension DispatchQueue {
}
public static func < (lhs: Stride, rhs: Stride) -> Bool {
return lhs.magnitude < rhs.magnitude
return lhs._nanoseconds < rhs._nanoseconds
}
public static func * (lhs: Stride, rhs: Stride) -> Stride {
@@ -212,43 +215,51 @@ extension DispatchQueue {
}
public static func + (lhs: Stride, rhs: Stride) -> Stride {
return Stride(magnitude: lhs.magnitude + rhs.magnitude)
return Stride(magnitude: lhs._nanoseconds + rhs._nanoseconds)
}
public static func - (lhs: Stride, rhs: Stride) -> Stride {
return Stride(magnitude: lhs.magnitude - rhs.magnitude)
return Stride(magnitude: lhs._nanoseconds - rhs._nanoseconds)
}
public static func -= (lhs: inout Stride, rhs: Stride) {
lhs.magnitude -= rhs.magnitude
lhs._nanoseconds -= rhs._nanoseconds
}
public static func *= (lhs: inout Stride, rhs: Stride) {
lhs.magnitude = 0
lhs._nanoseconds = 0
}
public static func += (lhs: inout Stride, rhs: Stride) {
lhs.magnitude += rhs.magnitude
lhs._nanoseconds += rhs._nanoseconds
}
public static func seconds(_ value: Double) -> Stride {
return Stride(magnitude: Int(value * 1_000_000_000))
let nanoseconds = value * 1_000_000_000
if nanoseconds >= Double(Int64.max) {
return Stride(magnitude: .max)
}
if nanoseconds <= Double(Int64.min) {
return Stride(magnitude: .min)
}
return Stride(magnitude: Int64(nanoseconds))
}
public static func seconds(_ value: Int) -> Stride {
return Stride(magnitude: clampedIntProduct(value, 1_000_000_000))
return Stride(magnitude: clampedIntProduct(Int64(value),
1_000_000_000))
}
public static func milliseconds(_ value: Int) -> Stride {
return Stride(magnitude: clampedIntProduct(value, 1_000_000))
return Stride(magnitude: clampedIntProduct(Int64(value), 1_000_000))
}
public static func microseconds(_ value: Int) -> Stride {
return Stride(magnitude: clampedIntProduct(value, 1_000))
return Stride(magnitude: clampedIntProduct(Int64(value), 1_000))
}
public static func nanoseconds(_ value: Int) -> Stride {
return Stride(magnitude: value)
return Stride(magnitude: Int64(value))
}
}
}
@@ -387,10 +398,10 @@ extension DispatchQueue: OpenCombine.Scheduler {
// This function is taken from swift-corelibs-libdispatch:
// https://github.com/apple/swift-corelibs-libdispatch/blob/c992dacf3ca114806e6ac9ffc9113b19255be9fe/src/swift/Time.swift#L134-L144
//
// Returns m1 * m2, clamped to the range [Int.min, Int.max].
// Returns m1 * m2, clamped to the range [Int64.min, Int64.max].
// Because of the way this function is used, we can always assume
// that m2 > 0.
private func clampedIntProduct(_ lhs: Int, _ rhs: Int) -> Int {
private func clampedIntProduct(_ lhs: Int64, _ rhs: Int64) -> Int64 {
assert(rhs > 0, "multiplier must be positive")
let (result, overflow) = lhs.multipliedReportingOverflow(by: rhs)
if overflow {
@@ -398,3 +409,31 @@ private func clampedIntProduct(_ lhs: Int, _ rhs: Int) -> Int {
}
return result
}
extension DispatchTime {
fileprivate func polyfillDistance(to other: DispatchTime) -> DispatchTimeInterval {
#if canImport(Darwin) && compiler(>=5.1)
if #available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) {
return distance(to: other)
}
#endif
let start = rawValue
let end = other.rawValue
if end >= start {
let result = end &- start
if result > UInt64(Int.max) {
return .never
} else {
return .nanoseconds(Int(result))
}
} else {
let result = start &- end
if result > UInt64(Int.max) {
return .never
} else {
return .nanoseconds(-Int(result))
}
}
}
}
@@ -5,7 +5,10 @@
// Created by Sergej Jaskiewicz on 28.10.2020.
//
#if canImport(CoreFoundation)
import CoreFoundation
#endif
import Foundation
/// Use CoreFoundation on Darwin, since some pure
@@ -93,6 +96,7 @@ internal struct Timer {
#endif
}
#if canImport(CoreFoundation)
fileprivate func getCFRunLoopTimer() -> CFRunLoopTimer? {
#if canImport(Darwin)
return underlyingTimer
@@ -113,6 +117,7 @@ internal struct Timer {
fatalError("unreachable")
#endif
}
#endif // canImport(CoreFoundation)
}
extension RunLoop {
@@ -138,6 +143,7 @@ extension RunLoop {
}
}
#if canImport(CoreFoundation)
extension RunLoop.Mode {
fileprivate func asCFRunLoopMode() -> CFRunLoopMode {
#if canImport(Darwin)
@@ -159,3 +165,4 @@ extension RunLoop.Mode {
#endif
}
}
#endif // canImport(CoreFoundation)
@@ -0,0 +1,14 @@
//
// Utils.swift
//
//
// Created by Sergej Jaskiewicz on 28.08.2021.
//
extension Optional {
internal mutating func take() -> Optional {
let taken = self
self = nil
return taken
}
}
@@ -200,13 +200,13 @@ extension Notification {
func cancel() {
lock.lock()
guard let center = self.center, let observation = self.observation else {
guard let center = self.center.take(),
let observation = self.observation.take()
else {
lock.unlock()
return
}
self.center = nil
self.object = nil
self.observation = nil
lock.unlock()
center.removeObserver(observation)
}
@@ -234,19 +234,16 @@ extension OperationQueue {
}
override func main() {
guard let action = self.action else { return }
self.action = nil
guard let action = self.action.take() else { return }
action()
guard let queue = self.queue,
let context = self.context
guard let queue = self.queue.take(),
let context = self.context.take()
else {
self.queue = nil
self.context = nil
return
}
self.queue = nil
self.context = nil
context.lock.lock()
if context.operation == nil {
@@ -5,7 +5,6 @@
// Created by Sergej Jaskiewicz on 13.12.2019.
//
import CoreFoundation
import Foundation
import OpenCombine
@@ -5,7 +5,6 @@
// Created by Sergej Jaskiewicz on 23.06.2020.
//
import CoreFoundation
import Foundation
import OpenCombine
@@ -203,11 +202,10 @@ extension Foundation.Timer {
func cancel() {
lock.lock()
if downstream == nil {
if downstream.take() == nil {
lock.unlock()
return
}
downstream = nil
lock.unlock()
parent?.disconnect(combineIdentifier)
}
+1 -1
View File
@@ -5,4 +5,4 @@ disabled_rules:
- explicit_acl
- explicit_top_level_acl
- explicit_enum_raw_value
- untyped_error_in_catch
@@ -0,0 +1,73 @@
//
// FutureConcurrencyTests.swift
//
//
// Created by Sergej Jaskiewicz on 12.12.2021.
//
import XCTest
#if canImport(_Concurrency) && compiler(>=5.5)
import _Concurrency
#endif
#if OPENCOMBINE_COMPATIBILITY_TEST
import Combine
#else
import OpenCombine
#endif
// swiftlint:disable:next line_length
#if !os(Windows) && !WASI && (canImport(_Concurrency) && compiler(>=5.5) || compiler(>=5.5.1)) // TEST_DISCOVERY_CONDITION
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
final class FutureConcurrencyTests: XCTestCase {
func testAsyncAwaitNonThrowingSuccess() async {
var promise: Future<Int, Never>.Promise?
let future = Future<Int, Never> { promise = $0 }
let task = Task {
await future.value
}
promise?(.success(42))
let value = await task.value
XCTAssertEqual(value, 42)
}
func testAsyncAwaitThrowingSuccess() async throws {
var promise: Future<Int, TestingError>.Promise?
let future = Future<Int, TestingError> { promise = $0 }
let task = Task {
try await future.value
}
promise?(.success(42))
let value = try await task.value
XCTAssertEqual(value, 42)
}
func testAsyncAwaitThrowingFailure() async throws {
var promise: Future<Int, TestingError>.Promise?
let future = Future<Int, TestingError> { promise = $0 }
let task = Task { try await future.value }
promise?(.failure(.oops))
do {
_ = try await task.value
XCTFail("Expected an error")
} catch let error as TestingError {
XCTAssertEqual(error, .oops)
} catch {
XCTFail("Unexpected error: \(error)")
}
}
}
#endif
File diff suppressed because it is too large Load Diff
@@ -314,7 +314,7 @@ final class CurrentValueSubjectTests: XCTestCase {
receiveSubscription: { subscription in
subscription.request(.unlimited)
},
receiveCompletion: { completion in
receiveCompletion: { _ in
cvs.send(completion: .failure("must not recurse"))
}
)
@@ -5,7 +5,7 @@
// Created by Sergej Jaskiewicz on 26.08.2019.
//
#if !WASI
#if !WASI // TEST_DISCOVERY_CONDITION
import Dispatch
import XCTest
@@ -31,23 +31,34 @@ final class DispatchQueueSchedulerTests: XCTestCase {
uptimeNanoseconds: DispatchTime.distantFuture.uptimeNanoseconds - 1024
)
)
let int64max = Scheduler.SchedulerTimeType(
DispatchTime(
uptimeNanoseconds: UInt64(Int.max)
)
)
XCTAssertEqual(time1.distance(to: time2), .nanoseconds(431))
XCTAssertEqual(time2.distance(to: time1), .nanoseconds(-431))
XCTAssertEqual(time1.distance(to: distantFuture), .nanoseconds(-10001))
XCTAssertEqual(distantFuture.distance(to: time1), .nanoseconds(10001))
XCTAssertEqual(time2.distance(to: distantFuture), .nanoseconds(-10432))
XCTAssertEqual(distantFuture.distance(to: time2), .nanoseconds(10432))
XCTAssertEqual(time1.distance(to: distantFuture), .nanoseconds(.max))
XCTAssertEqual(distantFuture.distance(to: time1), .nanoseconds(.max))
XCTAssertEqual(time2.distance(to: distantFuture), .nanoseconds(.max))
XCTAssertEqual(distantFuture.distance(to: time2), .nanoseconds(.max))
XCTAssertEqual(time1.distance(to: notSoDistantFuture), .nanoseconds(-11025))
XCTAssertEqual(notSoDistantFuture.distance(to: time1), .nanoseconds(11025))
XCTAssertEqual(time2.distance(to: notSoDistantFuture), .nanoseconds(-11456))
XCTAssertEqual(notSoDistantFuture.distance(to: time2), .nanoseconds(11456))
XCTAssertEqual(time1.distance(to: notSoDistantFuture), .nanoseconds(.max))
XCTAssertEqual(notSoDistantFuture.distance(to: time1), .nanoseconds(.max))
XCTAssertEqual(time2.distance(to: notSoDistantFuture), .nanoseconds(.max))
XCTAssertEqual(notSoDistantFuture.distance(to: time2), .nanoseconds(.max))
XCTAssertEqual(time1.distance(to: int64max), .nanoseconds(.max - 10000))
XCTAssertEqual(int64max.distance(to: time1), .nanoseconds(-(.max - 10000)))
XCTAssertEqual(time2.distance(to: int64max), .nanoseconds(.max - 10431))
XCTAssertEqual(int64max.distance(to: time2), .nanoseconds(-(.max - 10431)))
XCTAssertEqual(distantFuture.distance(to: distantFuture), .nanoseconds(0))
XCTAssertEqual(notSoDistantFuture.distance(to: notSoDistantFuture),
.nanoseconds(0))
XCTAssertEqual(int64max.distance(to: int64max), .nanoseconds(0))
}
func testSchedulerTimeTypeAdvanced() {
@@ -259,24 +270,22 @@ final class DispatchQueueSchedulerTests: XCTestCase {
XCTAssertEqual(Stride(exactly: 2 as UInt64)?.magnitude, 2_000_000_000)
}
func testStrideFromTooMuchSecondsCrashes() {
assertCrashes {
func testStrideFromTooMuchSeconds() {
#if arch(x86_64) || arch(arm64) || arch(s390x) || arch(powerpc64) || arch(powerpc64le)
// 64-bit platforms
XCTAssertGreaterThan(
Stride.seconds(Double(Int.max) / 1_000_000_000).magnitude,
.max
)
// 64-bit platforms
XCTAssertEqual(
Stride.seconds(Double(Int.max) / 1_000_000_000).magnitude,
.max
)
#elseif arch(i386) || arch(arm)
// 32-bit platforms
XCTAssertGreaterThan(
Stride.seconds(Double(Int.max) / 1_000_000_000 + 1).magnitude,
.max
)
// 32-bit platforms
XCTAssertEqual(
Stride.seconds(Double(Int.max) / 1_000_000_000).magnitude,
.max
)
#else
#error("This architecture isn't known. Add it to the 32-bit or 64-bit line.")
#endif
}
}
func testStrideComparable() {
@@ -443,7 +452,7 @@ final class DispatchQueueSchedulerTests: XCTestCase {
.encode(KeyedWrapper(value: stride))
let encodedString = String(decoding: encodedData, as: UTF8.self)
XCTAssertEqual(encodedString, #"{"value":{"magnitude":419872}}"#)
XCTAssertEqual(encodedString, #"{"value":{"_nanoseconds":419872}}"#)
let decodedStride = try decoder
.decode(KeyedWrapper<Stride>.self, from: encodedData)
@@ -486,11 +495,11 @@ final class DispatchQueueSchedulerTests: XCTestCase {
let main = expectation(description: "scheduled on main queue")
main.assertForOverFulfill = true
var didExecuteMainAction = false
let didExecuteMainAction = Atomic(false)
let didExecuteBackgroundAction = Atomic(false)
mainScheduler.schedule {
didExecuteMainAction = true
didExecuteMainAction.set(true)
main.fulfill()
}
@@ -501,12 +510,14 @@ final class DispatchQueueSchedulerTests: XCTestCase {
didExecuteBackgroundAction.set(true)
}
XCTAssertFalse(didExecuteMainAction, "action should be executed asynchronously")
XCTAssertFalse(didExecuteMainAction.value,
"action should be executed asynchronously")
// Wait for the background scheduler to execute the work.
XCTAssertEqual(group.wait(timeout: .now() + 5.0), .success)
XCTAssertFalse(didExecuteMainAction, "action should be executed asynchronously")
XCTAssertFalse(didExecuteMainAction.value,
"action should be executed asynchronously")
XCTAssertTrue(didExecuteBackgroundAction.value)
wait(for: [main], timeout: 0.1)
@@ -5,7 +5,7 @@
// Created by Sergej Jaskiewicz on 10.12.2019.
//
#if !WASI
#if !WASI // TEST_DISCOVERY_CONDITION
import Foundation
import XCTest
@@ -5,7 +5,7 @@
// Created by Sergej Jaskiewicz on 10.12.2019.
//
#if !WASI
#if !WASI // TEST_DISCOVERY_CONDITION
import Foundation
import XCTest
@@ -5,7 +5,7 @@
// Created by Sergej Jaskiewicz on 10.12.2019.
//
#if !WASI
#if !WASI // TEST_DISCOVERY_CONDITION
import Foundation
import XCTest
@@ -331,6 +331,7 @@ extension OperationQueueScheduler.SchedulerTimeType.Stride
@available(macOS 10.15, iOS 13.0, *)
extension OperationQueueScheduler.SchedulerTimeType: DateBackedSchedulerTimeType {}
@available(macOS 10.15, iOS 13.0, *)
extension OperationQueueScheduler: RunLoopLikeScheduler {}
private final class TestOperationQueue: OperationQueue {
@@ -5,7 +5,7 @@
// Created by Sergej Jaskiewicz on 14.12.2019.
//
#if !WASI
#if !WASI // TEST_DISCOVERY_CONDITION
import Foundation
import XCTest
@@ -599,12 +599,14 @@ private func makeScheduler(_ runLoop: RunLoop) -> RunLoopScheduler {
#endif
@available(macOS 10.15, iOS 13.0, *)
protocol DateBackedSchedulerTimeType: Strideable, Codable, Hashable {
init(_ date: Date)
var date: Date { get }
}
@available(macOS 10.15, iOS 13.0, *)
protocol TimeIntervalBackedSchedulerStride: SchedulerTimeIntervalConvertible,
Comparable,
SignedNumeric,
@@ -617,6 +619,7 @@ protocol TimeIntervalBackedSchedulerStride: SchedulerTimeIntervalConvertible,
var timeInterval: TimeInterval { get }
}
@available(macOS 10.15, iOS 13.0, *)
protocol RunLoopLikeScheduler: Scheduler
where SchedulerTimeType: DateBackedSchedulerTimeType,
SchedulerTimeType.Stride: TimeIntervalBackedSchedulerStride {
@@ -628,6 +631,7 @@ extension RunLoopScheduler.SchedulerTimeType.Stride: TimeIntervalBackedScheduler
@available(macOS 10.15, iOS 13.0, *)
extension RunLoopScheduler.SchedulerTimeType: DateBackedSchedulerTimeType {}
@available(macOS 10.15, iOS 13.0, *)
extension RunLoopScheduler: RunLoopLikeScheduler {}
#endif // !WASI
@@ -5,7 +5,7 @@
// Created by Sergej Jaskiewicz on 23.06.2020.
//
#if !WASI
#if !WASI // TEST_DISCOVERY_CONDITION
import Foundation
import XCTest
@@ -7,8 +7,6 @@
// swiftlint:disable multiline_arguments
#if !WASI
import Foundation
import XCTest
@@ -20,7 +18,7 @@ import FoundationNetworking
// swift-corelibs-foundation that were making these tests impossible to build.
//
// Those were fixed in https://github.com/apple/swift-corelibs-foundation/pull/2587.
#if canImport(Darwin) || swift(>=5.3) // TEST_DISCOVERY_CONDITION
#if canImport(Darwin) || swift(>=5.3) && !WASI // TEST_DISCOVERY_CONDITION
#if OPENCOMBINE_COMPATIBILITY_TEST
import Combine
@@ -53,13 +51,14 @@ final class URLSessionTests: XCTestCase {
private let unknownError = URLError(.unknown)
func testDataTaskPublisherFromURL() {
let publisher = makePublisher(TestURLSession(testDataTask: .init()), testURL)
let publisher = makePublisher(TestURLSession.withTestDataTask(.create()), testURL)
let expectedRequest = URLRequest(url: testURL)
XCTAssertEqual(publisher.request, expectedRequest)
}
func testDataTaskPublisherFromRequest() {
let publisher = makePublisher(TestURLSession(testDataTask: .init()), testRequest)
let publisher = makePublisher(TestURLSession.withTestDataTask(.create()),
testRequest)
XCTAssertEqual(publisher.request, testRequest)
}
@@ -126,8 +125,8 @@ final class URLSessionTests: XCTestCase {
}
func testRequesting() throws {
let dataTask = TestURLSessionDataTask()
let session = TestURLSession(testDataTask: dataTask)
let dataTask = TestURLSessionDataTask.create()
let session = TestURLSession.withTestDataTask(dataTask)
let publisher = makePublisher(session, testRequest)
var downstreamSubscription: Subscription?
let tracking = TrackingSubscriber(
@@ -166,8 +165,8 @@ final class URLSessionTests: XCTestCase {
}
func testCancelAlreadyCancelled() throws {
let dataTask = TestURLSessionDataTask()
let session = TestURLSession(testDataTask: dataTask)
let dataTask = TestURLSessionDataTask.create()
let session = TestURLSession.withTestDataTask(dataTask)
let publisher = makePublisher(session, testRequest)
var downstreamSubscription: Subscription?
let tracking = TrackingSubscriber(
@@ -192,8 +191,8 @@ final class URLSessionTests: XCTestCase {
}
func testCrashesOnZeroDemand() throws {
let dataTask = TestURLSessionDataTask()
let session = TestURLSession(testDataTask: dataTask)
let dataTask = TestURLSessionDataTask.create()
let session = TestURLSession.withTestDataTask(dataTask)
let publisher = makePublisher(session, testURL)
var downstreamSubscription: Subscription?
let tracking = TrackingSubscriber(
@@ -207,8 +206,8 @@ final class URLSessionTests: XCTestCase {
}
func testURLSessionSubscriptionReflection() throws {
let dataTask = TestURLSessionDataTask()
let session = TestURLSession(testDataTask: dataTask)
let dataTask = TestURLSessionDataTask.create()
let session = TestURLSession.withTestDataTask(dataTask)
let publisher = makePublisher(session, testURL)
try testSubscriptionReflection(
description: "DataTaskPublisher",
@@ -229,8 +228,8 @@ final class URLSessionTests: XCTestCase {
_ response: URLResponse?,
_ error: Error?,
expected: [TrackingSubscriber.Event]) {
let dataTask = TestURLSessionDataTask()
let session = TestURLSession(testDataTask: dataTask)
let dataTask = TestURLSessionDataTask.create()
let session = TestURLSession.withTestDataTask(dataTask)
let publisher = makePublisher(session, testRequest)
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.max(1)) })
publisher.subscribe(tracking)
@@ -292,16 +291,25 @@ private class TestURLSession: URLSession {
private(set) var history = [Event]()
private(set) var dataTaskCompletionHandlers: [(Data?, URLResponse?, Error?) -> Void]
private(set) var dataTaskCompletionHandlers =
[(Data?, URLResponse?, Error?) -> Void]()
private let testDataTask: TestURLSessionDataTask
private var testDataTask: TestURLSessionDataTask?
init(testDataTask: TestURLSessionDataTask) {
self.testDataTask = testDataTask
self.dataTaskCompletionHandlers = []
#if !canImport(Darwin)
super.init(configuration: .default)
static func withTestDataTask(
_ testDataTask: TestURLSessionDataTask
) -> TestURLSession {
// This dance is to avoid the deprecation warning for the URLSession
// default initializer. Believe me, I've tried to make it less ugly with
// no success.
#if canImport(Darwin)
let sessionClass = TestURLSession.self as NSObject.Type
let session = sessionClass.init() as! TestURLSession
#else
let session = TestURLSession(configuration: .default)
#endif
session.testDataTask = testDataTask
return session
}
// MARK: Testing
@@ -379,12 +387,12 @@ private class TestURLSession: URLSession {
override func dataTask(with request: URLRequest) -> URLSessionDataTask {
history.append(.dataTaskWithRequest(request))
return testDataTask
return testDataTask!
}
override func dataTask(with url: URL) -> URLSessionDataTask {
history.append(.dataTaskWithURL(url))
return testDataTask
return testDataTask!
}
override func dataTask(
@@ -393,7 +401,7 @@ private class TestURLSession: URLSession {
) -> URLSessionDataTask {
history.append(.dataTaskWithURLAndCompletion(url))
dataTaskCompletionHandlers.append(completionHandler)
return testDataTask
return testDataTask!
}
override func dataTask(
@@ -402,7 +410,7 @@ private class TestURLSession: URLSession {
) -> URLSessionDataTask {
history.append(.dataTaskWithRequestAndCompletion(request))
dataTaskCompletionHandlers.append(completionHandler)
return testDataTask
return testDataTask!
}
override func uploadTask(with request: URLRequest,
@@ -556,7 +564,18 @@ private final class TestURLSessionDataTask: URLSessionDataTask {
private(set) var history = [Event]()
override init() {}
static func create() -> TestURLSessionDataTask {
// This dance is to avoid the deprecation warning for the URLSessionDataTask
// default initializer. Believe me, I've tried to make it less ugly with
// no success.
#if canImport(Darwin)
let dataTaskClass = TestURLSessionDataTask.self as NSObject.Type
let dataTask = dataTaskClass.init() as! TestURLSessionDataTask
#else
let dataTask = TestURLSessionDataTask()
#endif
return dataTask
}
override var taskIdentifier: Int {
history.append(.taskIdentifier)
@@ -725,6 +744,4 @@ private func makePublisher(
}
#endif // OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
#endif // canImport(Darwin)
#endif // !WASI
#endif
@@ -0,0 +1,55 @@
//
// AutomaticallyFinish.swift
//
//
// Created by Sergej Jaskiewicz on 08.07.2021.
//
#if OPENCOMBINE_COMPATIBILITY_TEST
import Combine
#else
import OpenCombine
#endif
@available(macOS 10.15, iOS 13.0, *)
final class AutomaticallyFinish<Output, Failure: Error> {
let subscription: CustomSubscription
let publisher: CustomPublisherBase<Output, Failure>
init() {
subscription = .init()
publisher = .init(subscription: subscription)
}
deinit {
publisher.send(completion: .finished)
}
func notify(_ value: Output) {
_ = publisher.send(value)
}
func listen(receiveCompletion: @escaping (Subscribers.Completion<Failure>) -> Void,
receiveValue: @escaping (Output) -> Void) -> AnyCancellable {
return publisher.sink(receiveCompletion: receiveCompletion,
receiveValue: receiveValue)
}
}
@available(macOS 10.15, iOS 13.0, *)
extension AutomaticallyFinish: Publisher {
func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Failure == Failure, Downstream.Input == Output
{
publisher.subscribe(subscriber)
}
}
@available(macOS 10.15, iOS 13.0, *)
extension AutomaticallyFinish where Failure == Never {
func assign<Root>(to keyPath: ReferenceWritableKeyPath<Root, Output>,
on object: Root) -> AnyCancellable {
return publisher.assign(to: keyPath, on: object)
}
}
@@ -214,4 +214,19 @@ func unreachable() -> Never {
fatalError("unreachable")
}
func fromNever<T>(_ resultType: T.Type) -> (Never) -> T {
// This is to avoid the 'Will never be executed' warning.
//
// The first variant doesn't produce warnings in Swift 5.1, but doesn't compile
// with early Swift versions.
//
// The second variant compiles with all Swift versions,
// but produces a warning in Swift 5.1.
#if swift(>=5.1)
return { (_: Never) -> T in }
#else
return { switch $0 {} }
#endif
}
// swiftlint:enable generic_type_name
@@ -33,8 +33,28 @@ final class CustomSubscription: Subscription, CustomStringConvertible {
}
}
private struct State {
var cancelled: Bool
var history: [Event]
}
private let state = Atomic(State(cancelled: false, history: []))
/// The history of requests and cancellations of this subscription.
private(set) var history: [Event] = []
var history: [Event] {
return state.value.history
}
var cancelled: Bool {
get {
return state.value.cancelled
}
set {
state.do { state in
state.cancelled = newValue
}
}
}
var onRequest: ((Subscribers.Demand) -> Void)?
var onCancel: (() -> Void)?
@@ -63,16 +83,18 @@ final class CustomSubscription: Subscription, CustomStringConvertible {
}.last
}
var cancelled = false
func request(_ demand: Subscribers.Demand) {
history.append(.requested(demand))
state.do { state in
state.history.append(.requested(demand))
}
onRequest?(demand)
}
func cancel() {
history.append(.cancelled)
cancelled = true
state.do { state in
state.history.append(.cancelled)
state.cancelled = true
}
onCancel?()
}
@@ -11,38 +11,76 @@
import Darwin
#elseif canImport(Glibc)
import Glibc
#elseif os(Windows)
import WinSDK
#else
#error("How to do threads on this platform?")
#endif
#if canImport(Darwin)
private typealias ThreadPtr = UnsafeMutablePointer<pthread_t?>
#else
private typealias ThreadPtr = UnsafeMutablePointer<pthread_t>
#endif
// We could use Foundation's Thread, but it doesn't work on Linux for some
// reason.
func executeOnBackgroundThread<ResultType>(
_ body: () -> ResultType
) -> ResultType {
return withoutActuallyEscaping(body) { body in
typealias ThreadRoutine = () -> UnsafeMutableRawPointer
// We need this because @convention(c) closures can't capture generic params.
var typeErasedBody: () -> UnsafeMutableRawPointer = {
#if canImport(Darwin)
typealias ThreadHandle = UnsafeMutablePointer<pthread_t?>
#elseif canImport(Glibc)
typealias ThreadHandle = UnsafeMutablePointer<pthread_t>
#elseif os(Windows)
typealias ThreadHandle = HANDLE?
#endif
let typeErasedBody: ThreadRoutine = {
let resultPtr = UnsafeMutablePointer<ResultType>.allocate(capacity: 1)
resultPtr.initialize(to: body())
return UnsafeMutableRawPointer(resultPtr)
}
return withUnsafeMutablePointer(to: &typeErasedBody) { typeErasedBody in
let _backgroundThread = ThreadPtr.allocate(capacity: 1)
var _backgroundThread: ThreadHandle
defer { _backgroundThread.deallocate() }
#if os(Windows)
typealias ResultPtr = UnsafeMutablePointer<UnsafeMutableRawPointer>
typealias Context = (ThreadRoutine, ResultPtr)
var resultPtr: ResultPtr = .allocate(capacity: 1)
defer { resultPtr.deallocate() }
_backgroundThread = nil
var context: Context = (typeErasedBody, resultPtr)
#else
_backgroundThread = .allocate(capacity: 1)
defer { _backgroundThread.deallocate() }
var context = typeErasedBody
#endif
var status: Int32 = 0
return withUnsafeMutablePointer(to: &context) { context in
#if os(Windows)
_backgroundThread = CreateThread(
nil, // default security attributes
0, // use default stack size
{ context in
let (typeErasedBody, resultPtr) = context!
.assumingMemoryBound(to: Context.self)
.pointee
// We could use Foundation's Thread, but it doesn't work on Linux for some
// reason.
status = pthread_create(
resultPtr.initialize(to: typeErasedBody())
return 0
},
context,
0, // use default creation flags
nil // don't return thread identifier
)
precondition(_backgroundThread != nil, "Could not create a thread")
WaitForSingleObject(_backgroundThread!, INFINITE)
defer { resultPtr.pointee.deallocate() }
return resultPtr.pointee.assumingMemoryBound(to: ResultType.self).move()
#else
var status = pthread_create(
_backgroundThread,
nil,
{ context in
@@ -52,19 +90,17 @@ func executeOnBackgroundThread<ResultType>(
let context = context!
#endif
return context
.assumingMemoryBound(to: (() -> UnsafeMutableRawPointer).self)
.assumingMemoryBound(to: ThreadRoutine.self)
.pointee()
},
typeErasedBody
context
)
guard status == 0 else {
preconditionFailure("Could not create a background thread")
}
precondition(status == 0, "Could not create a thread")
#if canImport(Darwin)
guard let backgroundThread = _backgroundThread.pointee else {
preconditionFailure("Could not create a background thread")
preconditionFailure("Could not join thread")
}
#else
let backgroundThread = _backgroundThread.pointee
@@ -74,12 +110,13 @@ func executeOnBackgroundThread<ResultType>(
status = pthread_join(backgroundThread, &_resultPtr)
guard status == 0, let resultPtr = _resultPtr else {
preconditionFailure("Could not join threads")
preconditionFailure("Could not join thread")
}
defer { resultPtr.deallocate() }
return resultPtr.assumingMemoryBound(to: ResultType.self).move()
#endif
}
}
}
@@ -352,6 +352,7 @@ enum StringSubscription: Subscription,
ExpressibleByStringLiteral {
case string(String)
case contains(String)
case subscription(Subscription)
init(_ subscription: Subscription) {
@@ -364,7 +365,7 @@ enum StringSubscription: Subscription,
var description: String {
switch self {
case .string(let string):
case .string(let string), .contains(let string):
return string
case .subscription(let subscription):
return String(describing: subscription)
@@ -379,7 +380,7 @@ enum StringSubscription: Subscription,
switch self {
case .subscription(let subscription):
return subscription.combineIdentifier
case .string:
case .string, .contains:
fatalError("String has no combineIdentifier")
}
}
@@ -390,7 +391,7 @@ enum StringSubscription: Subscription,
var underlying: Subscription? {
switch self {
case .string:
case .string, .contains:
return nil
case .subscription(let underlying):
return underlying
@@ -401,6 +402,25 @@ enum StringSubscription: Subscription,
@available(macOS 10.15, iOS 13.0, *)
extension StringSubscription: Equatable {
static func == (lhs: StringSubscription, rhs: StringSubscription) -> Bool {
return lhs.description == rhs.description
// swiftlint:disable pattern_matching_keywords
switch (lhs, rhs) {
case (.contains(let pattern), .subscription(let subscription)),
(.subscription(let subscription), .contains(let pattern)):
return String(describing: subscription).contains(pattern)
case (.contains(let pattern), .string(let string)),
(.string(let string), .contains(let pattern)):
return string.contains(pattern)
case let (.subscription(lhs), .subscription(rhs)):
return String(describing: lhs) == String(describing: rhs)
case (.string(let string), .subscription(let subscription)),
(.subscription(let subscription), .string(let string)):
return String(describing: subscription) == string
case let (.string(lhs), .string(rhs)):
return lhs == rhs
case let (.contains(lhs), .contains(rhs)):
return lhs.contains(rhs) || rhs.contains(lhs)
}
// swiftlint:enable pattern_matching_keywords
}
}
@@ -357,9 +357,9 @@ final class VirtualTimeScheduler: Scheduler {
_now = time
}
func executeScheduledActions(until deadline: SchedulerTimeType = .nanoseconds(.max)) {
precondition(deadline >= _now)
while let (time, action) = workQueue.min(), time <= deadline {
func executeScheduledActions(until deadline: SchedulerTimeType? = nil) {
precondition(deadline.map { $0 >= _now } ?? true)
while let (time, action) = workQueue.min(), deadline.map({ time <= $0 }) ?? true {
workQueue.extractMin()
_now = max(time, _now)
action()
@@ -41,14 +41,21 @@ final class AllSatisfyTests: XCTestCase {
)
}
func testAllSatisfyUpstreamFinishesImmediately() {
ReduceTests.testUpstreamFinishesImmediately(
func testAllSatisfyUpstreamFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "AllSatisfy",
expectedResult: true,
{ $0.allSatisfy(shouldNotBeCalled()) }
)
}
func testAllSatisfyUpstreamFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "AllSatisfy",
{ $0.allSatisfy(shouldNotBeCalled()) }
)
}
func testAllSatisfyCancelAlreadyCancelled() throws {
try ReduceTests.testCancelAlreadyCancelled {
$0.allSatisfy(shouldNotBeCalled())
@@ -104,6 +111,7 @@ final class AllSatisfyTests: XCTestCase {
func testAllSatisfyLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.allSatisfy { _ in true } })
}
@@ -155,14 +163,21 @@ final class AllSatisfyTests: XCTestCase {
)
}
func testTryAllSatisfyUpstreamFinishesImmediately() {
ReduceTests.testUpstreamFinishesImmediately(
func testTryAllSatisfyUpstreamFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "TryAllSatisfy",
expectedResult: true,
{ $0.tryAllSatisfy(shouldNotBeCalled()) }
)
}
func testTryAllSatisfyUpstreamFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "TryAllSatisfy",
{ $0.tryAllSatisfy(shouldNotBeCalled()) }
)
}
func testTryAllSatisfyCancelAlreadyCancelled() throws {
try ReduceTests.testCancelAlreadyCancelled {
$0.tryAllSatisfy(shouldNotBeCalled())
@@ -224,6 +239,7 @@ final class AllSatisfyTests: XCTestCase {
func testTryAllSatisfyLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.tryAllSatisfy { _ in true } })
}
@@ -46,9 +46,11 @@ final class BreakpointTests: XCTestCase {
XCTAssertEqual(helper.subscription.history, [])
shouldStop = true
XCTAssertEqual(counter, 2)
#if !os(Windows)
assertCrashes {
helper.publisher.send(subscription: CustomSubscription())
}
#endif
}
func testReceiveValue() {
@@ -77,9 +79,11 @@ final class BreakpointTests: XCTestCase {
.subscription("CustomSubscription")])
XCTAssertEqual(helper.subscription.history, [])
XCTAssertEqual(counter, 2)
#if !os(Windows)
assertCrashes {
_ = helper.publisher.send(-1)
}
#endif
}
func testReceiveCompletion() {
@@ -107,9 +111,11 @@ final class BreakpointTests: XCTestCase {
.value(21),
.subscription("CustomSubscription")])
XCTAssertEqual(counter, 2)
#if !os(Windows)
assertCrashes {
helper.publisher.send(completion: .finished)
}
#endif
}
func testBreakpointOnError() throws {
@@ -139,9 +145,11 @@ final class BreakpointTests: XCTestCase {
XCTAssertEqual(helper.sut.receiveCompletion?(.finished), false)
XCTAssertEqual(helper.sut.receiveCompletion?(.failure(.oops)), true)
#if !os(Windows)
assertCrashes {
helper.publisher.send(completion: .failure(.oops))
}
#endif
}
func testCancelAlreadyCancelled() throws {
@@ -63,7 +63,7 @@ final class CatchTests: XCTestCase {
func testCatchReceiveValueBeforeSubscription() {
testReceiveValueBeforeSubscription(value: 1, expected: .crash) {
$0.catch { _ in Just(13) }
$0.catch(fromNever(Just<Int>.self))
}
}
@@ -71,7 +71,7 @@ final class CatchTests: XCTestCase {
testReceiveCompletionBeforeSubscription(
inputType: Int.self,
expected: .history([]),
{ $0.catch { _ in Just(13) } }
{ $0.catch(fromNever(Just<Int>.self)) }
)
}
@@ -220,7 +220,7 @@ final class CatchTests: XCTestCase {
func testTryCatchReceiveValueBeforeSubscription() {
testReceiveValueBeforeSubscription(value: 1, expected: .crash) {
$0.tryCatch { _ in Just(13) }
$0.tryCatch(fromNever(Just<Int>.self))
}
}
@@ -228,7 +228,7 @@ final class CatchTests: XCTestCase {
testReceiveCompletionBeforeSubscription(
inputType: Int.self,
expected: .history([]),
{ $0.tryCatch { _ in Just(13) } }
{ $0.tryCatch(fromNever(Just<Int>.self)) }
)
}

Some files were not shown because too many files have changed in this diff Show More