85 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
Max Desiatov 28993ae57d Add CHANGELOG.md, bump version to 0.12.0 (#202)
* Add CHANGELOG.md, bump version to 0.12.0

* Mention the timer bug fix in `CHANGELOG.md`
2021-01-29 15:05:56 +00:00
Grigory Entin 3d61bf87e7 Fixed Timer(timeInterval:,repeats:,block:) not accounting timeInterval for the first fire date. (#196)
https://github.com/OpenCombine/OpenCombine/blob/master/Sources/OpenCombineFoundation/Helpers/Portability.swift#L58-L64

It looks like this was a typo/something overlooked, but basically, this `fire: Date()` breaks at least every timer publisher like `Timer.publish(every: timeInterval, on: .main, in: .default)`, as it basically results in the *first* event fired immediately vs in timeInterval. (Just in case, no, Combine does not fire that extra event).

* Fixed Timer(timeInterval:,repeats:,block:) not accounting timeInterval for the first fire date.

* Fixed Danger warning about line length.
2021-01-29 13:42:17 +00:00
Max Desiatov 911a4e1aa3 Add OpenCombineShim product for easier importing (#197) 2021-01-25 17:25:28 +03:00
Yuta Saito beb38dec0e Implementation for ObservableObject with Mirror (#201)
A temporary implementation until we implement the proper type metadata introspection.
2021-01-25 17:24:19 +03:00
Nomo Nomad 1fbf688897 Update README.md (#199) 2020-12-11 16:41:20 +03:00
Sergej Jaskiewicz 5436868053 Fix some lock acquiring in Publishers.FlatMap (#194) 2020-11-08 17:44:33 +03:00
Sergej Jaskiewicz 4977ca158f Update DispatchQueue scheduler to match iOS 14.2 behavior 2020-11-07 17:28:08 +03:00
Sergej Jaskiewicz 96214ac5f9 Run compatibility tests on iOS 14.2 2020-11-07 17:28:08 +03:00
Sergej Jaskiewicz 21fda909f5 Implement Publishers.Retry 2020-11-07 17:28:08 +03:00
Sergej Jaskiewicz 8438d09b82 Increase time intervals in OperationQueue tests
The test is sporadically failing on iOS 9.3.
2020-11-03 17:21:34 +03:00
Sergej Jaskiewicz 30a60b52cc Add missing availability annotations in tests
Fixes #192
2020-11-03 17:21:34 +03:00
Sergej Jaskiewicz a93ed143fb Add more supported platforms to Package.swift 2020-11-03 17:21:34 +03:00
Max Desiatov e054a884ef Add support for SwiftWasm with CI and tests (#191)
WebAssembly support for atomics and multi-threading isn't fully standardized yet, and it not supported in SwiftWasm at the moment. Because of this Dispatch is unavailable, and all Combine-related Foundation stuff is unavailable too. Tests related to this are disabled. Locking functions are replaced with no-op shims.
2020-11-02 22:02:39 +00:00
160 changed files with 9783 additions and 1755 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.1 (Xcode 12.1.0, Swift 5.3.0)":
macos:
xcode: "12.1.0"
environment:
SWIFT_VERSION: "5.3.0"
steps:
- checkout
- run:
name: Generating Xcode project
command: make generate-compatibility-xcodeproj
- run:
name: Building for testing on iOS 14.1 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild build-for-testing \
-scheme OpenCombine-Package \
-destination "platform=iOS Simulator,name=iPhone 11,OS=14.1" \
-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.1 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild test-without-building \
-scheme OpenCombine-Package \
-destination "platform=iOS Simulator,name=iPhone 11,OS=14.1" \
-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.1 (Xcode 12.1.0, Swift 5.3.0)"
"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
+48
View File
@@ -0,0 +1,48 @@
name: Wasm
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
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
+323
View File
@@ -0,0 +1,323 @@
# 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
Combine on Apple platforms, or OpenCombine on other platforms. Additionally, `ObservableObject`
protocol is now available and working on all platforms.
A bug with `Timer(timeInterval:repeats:block:)` firing immediately not accounting for the passed
`timeInterval` is fixed.
**Merged pull requests:**
- Fix `Timer(timeInterval:repeats:block:)` not accounting `timeInterval` ([#196](https://github.com/OpenCombine/OpenCombine/pull/196)) via [@grigorye](https://github.com/grigorye)
- Add `OpenCombineShim` product for easier importing ([#197](https://github.com/OpenCombine/OpenCombine/pull/197)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Implementation for `ObservableObject` with `Mirror` ([#201](https://github.com/OpenCombine/OpenCombine/pull/201)) via [@kateinoigakukun](https://github.com/kateinoigakukun)
# 0.11.0 (29 Oct 2020)
This release is compatible with Xcode 12.1.
### Additions
- `Publisher.assigned(to:)` method that accepts a `Published.Publisher`.
- New `Publisher.switchToLatest()` overloads.
- New `Publisher.flatMap(maxPublishers:_:)` overloads.
- `Optional.publisher` property.
- New `_Introspection` protocol that allows to track and explore the subscription graph and data flow.
### Bugfixes
- The project should now compile without warnings.
- The following entities have been updated to match the behavior of the newest Combine version:
- `Subscribers.Assign`
- `Publishers.Breakpoint`
- `Publishers.Buffer`
- `CombineIdentifier`
- `Publishers.CompactMap`
- `Publishers.Concatenate`
- `Publishers.Debounce`
- `Publishers.Delay`
- `DispatchQueue.SchedulerTimeType.Stride`
- `Publishers.Drop`
- `Publishers.Encode`
- `Publishers.Decode`
- `Publishers.Filter`
- `Publishers.HandleEvents`
- `Publishers.IgnoreOutput`
- `Publishers.MeasureInterval`
- `OperationQueue` scheduler
- `Published`
- `Publishers.ReceiveOn`
- `Publishers.ReplaceError`
- `RunLoop scheduler`
- `Publishers.Sequence`
- `Subscribers.Sink`
- `Publishers.SubscribeOn`
- `Publishers.Timeout`
- `Timer` publisher
### Known issues
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1 and later.
# 0.10.2 (23 Oct 2020)
### Bugfixes
- Fixed a crash caused by recursive acquisition of a non-recursive lock in SubbjectSubscriber (#186, thanks @stuaustin for the bug report)
### Known issues
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1 and later.
# 0.10.1 (4 Oct 2020)
### Bugfixes
- Fixed build errors on Linux with Swift 5.0 and Swift 5.3 toolchains (thanks, @adamleonard and @devmaximilian)
### Known issues
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1 and later.
# 0.10.0 (28 Jun 2020)
This release is compatible with Xcode 11.5.
### Additions
- `Timer.publish(every:tolerance:on:in:options:)` (#156, thank you @MaxDesiatov)
- `OperationQueue` scheduler (#165)
- `Publishers.Timeout` (#164)
- `Publishers.Debounce` (#133)
### Bugfixes
- `PassthroughSubject`, `CurrentValueSubject` and `Future` have been rewritten from scratch. They are now faster, more correct and no longer leak subscriptions (#170).
### Known issues
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1 and later.
# 0.9.0 (12 Jun 2020)
This release is compatible with Xcode 11.5.
### Additions
- The `Subscribers.Demand` struct can be nicely formatted in LLDB (#146, thank you @mayoff).
- `Publishers.SwitchToLatest` (#142).
- The `RunLoop` scheduler in `OpenCombineFoundation` (#131).
- `Publishers.Catch` and `Publishers.TryCatch` (#140).
### Bugfixes
- Worked around a [bug in the Swift compiler](https://bugs.swift.org/browse/SR-11680) when building the `COpenCombineHelpers` target (#145, thank you @mayoff).
- Improved documentation.
### Known issues
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1 and later.
# 0.8.0 (17 Jan 2020)
This release is compatible with Xcode 11.3.1.
### Additions
- `Publishers.ReplaceEmpty` (#122, thank you @spadafiva)
- `NotificationCenter.Publisher` (#84)
- `URLSession.DataTaskPublisher` (#127)
- `Publishers.DropUntilOutput` (#136)
- `Publishers.CollectByCount` (#137)
- `Publishers.AssertNoFailure` (#138)
- `Publishers.Buffer` (#143)
### Bugfixes
- Fixed integer overflows in `DispatchQueue.SchedulerTimeType.Stride` (#126, #130)
- Fixed the 'default will never be executed' warning on non-Darwin platforms (like Linux) (#129)
### Known issues
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1.
# 0.7.0 (10 Dec 2019)
This release is compatible with Xcode 11.2.1.
### Additions
- `Publishers.Delay` (#114)
- `Publishers.ReceiveOn` (#115)
- `Publishers.SubscribeOn` (#116)
- `Publishers.MeasureInterval` (#117)
- `Publishers.Breakpoint` (#118)
- `Publishers.HandleEvents` (#118)
- `Publishers.Concatenate` (#90)
### Known issues
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1.
# 0.6.0 (26 Nov 2019)
This release is compatible with Xcode 11.2.1.
### Thread safety
- `Publishers.IgnoreOutput` has been audited for thread safety (#88)
- `Publishers.DropWhile` and `Publishers.TryDropWhile` have been audited for thread safety (#87)
### Additions
- `Publishers.Output` (#91)
- `Record` (#100)
- `Publishers.RemoveDuplicates`, `Publishers.TryRemoveDuplicates` (#89)
- `Publishers.PrefixWhile`, `Publishers.TryPrefixWhile` (#89)
- `Future` (#107, thanks @MaxDesiatov!)
### Bugfixes
- The behavior of the `Publishers.Encode` and `Publishers.Decode` subscriptions is fixed (#112)
- The behavior of the `Publishers.IgnoreOutput` subscription is fixed (#88)
- The behavior of the `Publishers.Print` subscription is fixed (#92)
- The behavior of the `Publishers.ReplaceError` subscription is fixed (#89)
- The behavior of the `Publishers.Filter` and `Publishers.TryFilter` subscriptions is fixed (#89)
- The behavior of the `Publishers.CompactMap` and `Publishers.TryCompactMap` subscriptions is fixed (#89)
- The behavior of the `Publishers.Multicast` subscription is fixed (#110)
- `Publishers.FlatMap` is reimplemented from scratch. Its behavior is fixed in many ways, it now fully matches that of Combine (#89)
- `@Published` property wrapper is fixed! (#112)
- The behavior of `DispatchQueue.SchedulerTimeType` is fixed to match that of the latest SDKs (#96)
- OpenCombine is now usable on 32 bit platforms. Why? Because we can.
### Known issues
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1.
# 0.5.0 (17 Oct 2019)
This release is compatible with Xcode 11.1.
### Additions
- `Publishers.MapKeyPath` (#71)
- `Publishers.Reduce` (#76)
- `Publishers.TryReduce` (#76)
- `Publishers.Last` (#76)
- `Publishers.LastWhere` (#76)
- `Publishers.TryLastWhere` (#76)
- `Publishers.AllSatisfy` (#76)
- `Publishers.TryAllSatisfy` (#76)
- `Publishers.Contains` (#76)
- `Publishers.ContainsWhere` (#76)
- `Publishers.TryContainsWhere` (#76)
- `Publishers.Collect` (#76)
- `Publishers.Comparison` (#76)
- `Publishers.Drop` (#70, thank you @5sw!)
- `Publishers.Scan` (#83, thank you @epatey!)
- `Publishers.TryScan` (#83, thank you @epatey!)
### Bugfixes
- `Publishers.Print` doesn't print a redundant whitespace anymore.
### Known issues
- `@Published` property wrapper doesn't work yet
# 0.4.0 (8 Oct 2019)
This release is compatible with Xcode 11.1.
### Thread safety
- `SubjectSubscriber` (which is used when you subscribe a subject to a publisher) has been audited for thread-safety
- `Publishers.Multicast` has been audited for thread safety (#63)
- `Publishers.TryMap` has been audited for thread safety
- `Just` has been audited for thread safety
- `Optional.Publisher` has been audited for thread safety
- `Publishers.Sequence` has been audited for thread safety
- `Publishers.ReplaceError` has been audited for thread safety
- `Subscribers.Assign` has been audited for thread safety
- `Subscribers.Sink` has been audited for thread safety
### Bugfixes
- The semantics of `Publishers.Print`, `Publishers.TryMap` have been fixed
- Fix `iterator.next()` being called twice in `Publishers.Sequence` (#62)
- The default initializer of `CombineIdentifier` (the one that takes no arguments) is now much faster (#66, #69)
- When `Publishers.Sequence` subscription is cancelled while it emits values, the cancellation is respected (#73, thanks @5sw!)
### Additions
- `DispatchQueueScheduler` (#46)
- `Equatable` conformances for `First`, `ReplaceError`
- Added `eraseToAnyPublisher()` method (#59, thanks @evyasafhouzz for reporting!)
- `Publishers.MakeConnectable` (#61)
- `Publishers.Autoconnect` (#60)
- `Publishers.Share` (#60)
### Known issues
- `@Published` property wrapper doesn't work yet
# 0.3.0 (13 Sep 2019)
Among other things this release is compatible with Xcode 11.0 GM seed.
### Bugfixes
- Store newly send value in internal variable inside CurrentValueObject (#39, thanks @FranzBusch!)
### Additions
- `Filter`/`TryFilter` (#22, thanks @spadafiva!)
- `First`/`FirstWhere`/`TryFirstWhere` (#22, thanks again @spadafiva!)
- `CompactMap`/`TryCompacrMap` (#32)
- `IgnoreOutput` (#44, thanks @epatey!)
- `ReplaceError` (#50, thanks @vladiulianbogdan!)
- `FlatMap` (#45, thanks again @epatey!)
### Known issues
- `@Published` property wrapper doesn't work yet
# 0.2.0 (31 Jul 2019)
Updated for the newest Xcode 11.0 beta 5
# 0.1.0 (4 Jul 2019)
The first pre-pre-pre-alpha release is here!
Lots of stuff still unimplemented.
For now we have:
- `Just`
- `Publishers.Decode`
- `Publishers.DropWhile`
- `Publishers.Empty`
- `Publishers.Encode`
- `Publishers.Fail`
- `Publishers.Map`
- `Publishers.Multicast`
- `Publishers.Once`
- `Publishers.Optional`
- `Publishers.Print`
- `Publishers.Sequence`
- `Subscribers.Assign`
- `Subscribers.Completion`
- `Subscribers.Demand`
- `Subscribers.Sink`
- `AnyCancellable`
- `AnyPublisher`
- `AnySubject`
- `AnySubscriber`
- `Cancellable`
- `CombineIdentifier`
- `ConnectablePublisher`
- `CurrentValueSubject`
- `CustomCombineIdentifierConvertible`
- `ImmediateScheduler`
- `PassthroughSubject`
- `Publisher`
- `Result`
- `Scheduler`
- `Subject`
- `Subscriber`
- `Subscription`
+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.11.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.11.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.11.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
+73 -10
View File
@@ -1,25 +1,88 @@
// swift-tools-version:5.0
// swift-tools-version:5.5
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,
.macCatalyst,
.iOS,
.watchOS,
.tvOS,
.driverKit,
.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: ["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",
"COpenCombineHelpers"]),
.testTarget(name: "OpenCombineTests",
dependencies: ["OpenCombine",
"OpenCombineDispatch",
"OpenCombineFoundation"],
swiftSettings: [.unsafeFlags(["-enable-testing"])])
.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
cxxLanguageStandard: .cxx17
)
// MARK: Helpers
extension Array where Element == Platform {
func except(_ exceptions: [Platform]) -> [Platform] {
return filter { !exceptions.contains($0) }
}
}
+34
View File
@@ -0,0 +1,34 @@
// swift-tools-version:5.0
import PackageDescription
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: ["COpenCombineHelpers"]),
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
.target(name: "OpenCombineFoundation", dependencies: ["OpenCombine",
"COpenCombineHelpers"]),
.target(
name: "OpenCombineShim",
dependencies: [
"OpenCombine",
"OpenCombineDispatch",
"OpenCombineFoundation",
]
),
.testTarget(name: "OpenCombineTests",
dependencies: ["OpenCombine",
"OpenCombineDispatch",
"OpenCombineFoundation"],
swiftSettings: [.unsafeFlags(["-enable-testing"])])
],
cxxLanguageStandard: .cxx1z
)
+34
View File
@@ -0,0 +1,34 @@
// swift-tools-version:5.1
import PackageDescription
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: ["COpenCombineHelpers"]),
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
.target(name: "OpenCombineFoundation", dependencies: ["OpenCombine",
"COpenCombineHelpers"]),
.target(
name: "OpenCombineShim",
dependencies: [
"OpenCombine",
"OpenCombineDispatch",
"OpenCombineFoundation",
]
),
.testTarget(name: "OpenCombineTests",
dependencies: ["OpenCombine",
"OpenCombineDispatch",
"OpenCombineFoundation"],
swiftSettings: [.unsafeFlags(["-enable-testing"])])
],
cxxLanguageStandard: .cxx1z
)
+34
View File
@@ -0,0 +1,34 @@
// swift-tools-version:5.2
import PackageDescription
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: ["COpenCombineHelpers"]),
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
.target(name: "OpenCombineFoundation", dependencies: ["OpenCombine",
"COpenCombineHelpers"]),
.target(
name: "OpenCombineShim",
dependencies: [
"OpenCombine",
"OpenCombineDispatch",
"OpenCombineFoundation",
]
),
.testTarget(name: "OpenCombineTests",
dependencies: ["OpenCombine",
"OpenCombineDispatch",
"OpenCombineFoundation"],
swiftSettings: [.unsafeFlags(["-enable-testing"])])
],
cxxLanguageStandard: .cxx1z
)
+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) }
}
}
+35 -76
View File
@@ -1,37 +1,53 @@
# 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-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 Windows.
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).
OpenCombine itself does not have any dependencies. Not even Foundation or Dispatch. If you want to use OpenCombine with Dispatch (for example for using `DispatchQueue` as `Scheduler` for operators like `debounce`, `receive(on:)` etc.), you will need to import both `OpenCombine` and `OpenCombineDispatch`. The same applies to Foundation: if you want to use, for instance, `NotificationCenter` or `URLSession` publishers, you'll need to also import `OpenCombineFoundation`
OpenCombine itself does not have any dependencies. Not even Foundation or Dispatch. If you want to use OpenCombine with Dispatch (for example for using `DispatchQueue` as `Scheduler` for operators like `debounce`, `receive(on:)` etc.), you will need to import both `OpenCombine` and `OpenCombineDispatch`. The same applies to Foundation: if you want to use, for instance, `NotificationCenter` or `URLSession` publishers, you'll need to also import `OpenCombineFoundation`.
If you develop code for multiple platforms, you may find it more convenient to import the
`OpenCombineShim` module instead. It conditionally re-exports Combine on Apple platforms (if
available), and all OpenCombine modules on other platforms. You can import `OpenCombineShim` only
when using SwiftPM. It is not currently available for CocoaPods.
##### Swift Package Manager
###### Swift Package
To add `OpenCombine` to your [SPM](https://swift.org/package-manager/) package, add the `OpenCombine` package to the list of package and target dependencies in your `Package.swift` file.
To add `OpenCombine` to your [SwiftPM](https://swift.org/package-manager/) package, add the `OpenCombine` package to the list of package and target dependencies in your `Package.swift` file. `OpenCombineDispatch` and `OpenCombineFoundation` products are currently not supported on WebAssembly. If your project targets WebAssembly exclusively, you should omit them from the list of your dependencies. If it targets multiple platforms including WebAssembly, depend on them only on non-WebAssembly platforms with [conditional target dependencies](https://github.com/apple/swift-evolution/blob/main/proposals/0273-swiftpm-conditional-target-dependencies.md).
```swift
dependencies: [
.package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.11.0")
.package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.14.0")
],
targets: [
.target(name: "MyAwesomePackage", dependencies: ["OpenCombine",
"OpenCombineDispatch",
"OpenCombineFoundation"])
.target(
name: "MyAwesomePackage",
dependencies: [
"OpenCombine",
.product(name: "OpenCombineFoundation", package: "OpenCombine"),
.product(name: "OpenCombineDispatch", package: "OpenCombine")
]
),
]
```
###### Xcode
`OpenCombine` can also be added as a SPM dependency directly in your Xcode project *(requires Xcode 11 upwards)*.
`OpenCombine` can also be added as a SwiftPM dependency directly in your Xcode project *(requires Xcode 11 upwards)*.
To do so, open Xcode, use **File****Swift Packages****Add Package Dependency…**, enter the [repository URL](https://github.com/OpenCombine/OpenCombine.git), choose the latest available version, and activate the checkboxes:
@@ -44,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.11.0'
pod 'OpenCombineDispatch', '~> 0.11.0'
pod 'OpenCombineFoundation', '~> 0.11.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.
@@ -122,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).
-217
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.
@@ -816,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.
@@ -1106,19 +902,6 @@ extension Publishers.MergeMany : Equatable where Upstream : Equatable {
public static func == (lhs: Publishers.MergeMany<Upstream>, rhs: Publishers.MergeMany<Upstream>) -> Bool
}
extension Publishers.Retry : Equatable where Upstream : Equatable {
/// Returns a Boolean value indicating whether two values are equal.
///
/// Equality is the inverse of inequality. For any values `a` and `b`,
/// `a == b` implies that `a != b` is `false`.
///
/// - Parameters:
/// - lhs: A value to compare.
/// - rhs: Another value to compare.
public static func == (lhs: Publishers.Retry<Upstream>, rhs: Publishers.Retry<Upstream>) -> Bool
}
extension Publishers.Zip : 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.
@@ -9,6 +9,15 @@
import COpenCombineHelpers
#endif
#if WASI
private var __identifier: UInt64 = 0
internal func __nextCombineIdentifier() -> UInt64 {
defer { __identifier += 1 }
return __identifier
}
#endif // WASI
/// A unique identifier for identifying publisher streams.
///
/// To conform to `CustomCombineIdentifierConvertible` in a
@@ -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
}
}
+17
View File
@@ -9,5 +9,22 @@
import COpenCombineHelpers
#endif
#if WASI
internal struct __UnfairLock { // swiftlint:disable:this type_name
internal static func allocate() -> UnfairLock { return .init() }
internal func lock() {}
internal func unlock() {}
internal func assertOwner() {}
internal func deallocate() {}
}
internal struct __UnfairRecursiveLock { // swiftlint:disable:this type_name
internal static func allocate() -> UnfairRecursiveLock { return .init() }
internal func lock() {}
internal func unlock() {}
internal func deallocate() {}
}
#endif // WASI
internal typealias UnfairLock = __UnfairLock
internal typealias UnfairRecursiveLock = __UnfairRecursiveLock
@@ -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
}
}
+50 -18
View File
@@ -42,26 +42,58 @@ public protocol ObservableObject: AnyObject {
var objectWillChange: ObjectWillChangePublisher { get }
}
extension ObservableObject where ObjectWillChangePublisher == ObservableObjectPublisher {
// swiftlint:disable let_var_whitespace
#if swift(>=5.1)
/// A publisher that emits before the object has changed.
@available(*, unavailable, message: """
The default implementation of objectWillChange is not available yet. \
It's being worked on in \
https://github.com/broadwaylamb/OpenCombine/pull/97
""")
public var objectWillChange: ObservableObjectPublisher {
fatalError("unimplemented")
}
#else
public var objectWillChange: ObservableObjectPublisher {
return ObservableObjectPublisher()
}
#endif
// swiftlint:enable let_var_whitespace
private protocol _ObservableObjectProperty {
var objectWillChange: ObservableObjectPublisher? { get nonmutating set }
}
#if swift(>=5.1)
extension Published: _ObservableObjectProperty {}
extension ObservableObject where ObjectWillChangePublisher == ObservableObjectPublisher {
/// A publisher that emits before the object has changed.
public var objectWillChange: ObservableObjectPublisher {
var installedPublisher: ObservableObjectPublisher?
var reflection: Mirror? = Mirror(reflecting: self)
while let aClass = reflection {
for (_, property) in aClass.children {
guard let property = property as? _ObservableObjectProperty else {
// Visit other fields until we meet a @Published field
continue
}
// Now we know that the field is @Published.
if let alreadyInstalledPublisher = property.objectWillChange {
installedPublisher = alreadyInstalledPublisher
// Don't visit other fields, as all @Published fields
// already have a publisher installed.
break
}
// Okay, this field doesn't have a publisher installed.
// This means that other fields don't have it either
// (because we install it only once and fields can't be added at runtime).
var lazilyCreatedPublisher: ObjectWillChangePublisher {
if let publisher = installedPublisher {
return publisher
}
let publisher = ObservableObjectPublisher()
installedPublisher = publisher
return publisher
}
property.objectWillChange = lazilyCreatedPublisher
// Continue visiting other fields.
}
reflection = aClass.superclassMirror
}
return installedPublisher ?? ObservableObjectPublisher()
}
}
#endif
/// A publisher that publishes changes from observable objects.
public final class ObservableObjectPublisher: Publisher {
+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)
}
+24 -12
View File
@@ -107,8 +107,16 @@ public struct Published<Value> {
case value(Value)
case publisher(Publisher)
}
@propertyWrapper
private final class Box {
var wrappedValue: Storage
private var storage: Storage
init(wrappedValue: Storage) {
self.wrappedValue = wrappedValue
}
}
@Box private var storage: Storage
internal var objectWillChange: ObservableObjectPublisher? {
get {
@@ -119,8 +127,8 @@ public struct Published<Value> {
return publisher.subject.objectWillChange
}
}
set {
projectedValue.subject.objectWillChange = newValue
nonmutating set {
getPublisher().subject.objectWillChange = newValue
}
}
@@ -145,7 +153,7 @@ public struct Published<Value> {
///
/// - Parameter initialValue: The publisher's initial value.
public init(wrappedValue: Value) {
storage = .value(wrappedValue)
_storage = Box(wrappedValue: .value(wrappedValue))
}
/// The property for which this instance exposes a publisher.
@@ -153,14 +161,7 @@ public struct Published<Value> {
/// The `projectedValue` is the property accessed with the `$` operator.
public var projectedValue: Publisher {
mutating get {
switch storage {
case .value(let value):
let publisher = Publisher(value)
storage = .publisher(publisher)
return publisher
case .publisher(let publisher):
return publisher
}
return getPublisher()
}
set { // swiftlint:disable:this unused_setter_value
switch storage {
@@ -172,6 +173,17 @@ public struct Published<Value> {
}
}
/// Note: This method can mutate `storage`
internal func getPublisher() -> Publisher {
switch storage {
case .value(let value):
let publisher = Publisher(value)
storage = .publisher(publisher)
return publisher
case .publisher(let publisher):
return publisher
}
}
// swiftlint:disable let_var_whitespace
@available(*, unavailable, message: """
@Published is only available on properties of classes
@@ -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
@@ -5,6 +5,8 @@
// Created by Sergej Jaskiewicz on 03.12.2019.
//
#if !WASI
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
@@ -225,3 +227,5 @@ extension Publishers.Breakpoint {
var playgroundDescription: Any { return description }
}
}
#endif // !WASI
@@ -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.
@@ -157,17 +157,17 @@ extension Publishers {
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Child.Output == Downstream.Input, Upstream.Failure == Downstream.Failure
{
let inner = Inner(downstream: subscriber,
let outer = Outer(downstream: subscriber,
maxPublishers: maxPublishers,
map: transform)
subscriber.receive(subscription: inner)
upstream.subscribe(inner)
subscriber.receive(subscription: outer)
upstream.subscribe(outer)
}
}
}
extension Publishers.FlatMap {
private final class Inner<Downstream: Subscriber>
private final class Outer<Downstream: Subscriber>
: Subscriber,
Subscription,
CustomStringConvertible,
@@ -243,7 +243,7 @@ extension Publishers.FlatMap {
subscription.request(maxPublishers)
}
fileprivate func receive(_ input: Upstream.Output) -> Subscribers.Demand {
fileprivate func receive(_ input: Input) -> Subscribers.Demand {
lock.lock()
let cancelledOrCompleted = self.cancelledOrCompleted
lock.unlock()
@@ -260,9 +260,9 @@ extension Publishers.FlatMap {
return .none
}
fileprivate func receive(completion: Subscribers.Completion<Child.Failure>) {
outerSubscription = nil
fileprivate func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
outerSubscription = nil
outerFinished = true
switch completion {
case .finished:
@@ -272,6 +272,8 @@ extension Publishers.FlatMap {
let wasAlreadyCancelledOrCompleted = cancelledOrCompleted
cancelledOrCompleted = true
for (_, subscription) in subscriptions {
// Cancelling subscriptions with the lock acquired. Not good,
// but that's what Combine does. This code path is tested.
subscription.cancel()
}
subscriptions = [:]
@@ -302,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()
@@ -354,16 +355,19 @@ extension Publishers.FlatMap {
fileprivate func cancel() {
lock.lock()
if cancelledOrCompleted {
lock.unlock()
return
}
cancelledOrCompleted = true
let subscriptions = self.subscriptions
self.subscriptions = [:]
let subscriptions = self.subscriptions.take()
let outerSubscription = self.outerSubscription.take()
lock.unlock()
for (_, subscription) in subscriptions {
subscription.cancel()
}
// Combine doesn't acquire the lock here. Weird.
// Combine doesn't acquire outerLock here. Weird.
outerSubscription?.cancel()
outerSubscription = nil
}
// MARK: - Reflection
@@ -443,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()
@@ -471,9 +474,9 @@ extension Publishers.FlatMap {
private func releaseLockThenSendCompletionDownstreamIfNeeded(
outerFinished: Bool
) -> Bool {
#if DEBUG
#if DEBUG
lock.assertOwner() // Sanity check
#endif
#endif
if !cancelledOrCompleted && outerFinished && buffer.isEmpty &&
subscriptions.count + pendingSubscriptions == 0 {
cancelledOrCompleted = true
@@ -495,10 +498,10 @@ extension Publishers.FlatMap {
CustomReflectable,
CustomPlaygroundDisplayConvertible {
private let index: SubscriptionIndex
private let inner: Inner
private let inner: Outer
fileprivate let combineIdentifier = CombineIdentifier()
fileprivate init(index: SubscriptionIndex, inner: Inner) {
fileprivate init(index: SubscriptionIndex, inner: Outer) {
self.index = index
self.inner = inner
}
@@ -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
}
@@ -0,0 +1,274 @@
//
// Publishers.Retry.swift
//
//
// Created by Sergej Jaskiewicz on 28.06.2020.
//
extension Publisher {
/// Attempts to recreate a failed subscription with the upstream publisher up to
/// the number of times you specify.
///
/// Use `retry(_:)` to try connecting to an upstream publisher after a failed
/// connection attempt.
///
/// In the example below, a `URLSession.DataTaskPublisher` attempts to connect to
/// a remote URL. If the connection attempt succeeds, it publishes the remote
/// services HTML to the downstream publisher and completes normally. Otherwise,
/// the retry operator attempts to reestablish the connection. If after three attempts
/// the publisher still cant connect to the remote URL, the `catch(_:)` operator
/// replaces the error with a new publisher that publishes a connection timed out
/// HTML page. After the downstream subscriber receives the timed out message,
/// the stream completes normally.
///
/// struct WebSiteData: Codable {
/// var rawHTML: String
/// }
///
/// let myURL = URL(string: "https://www.example.com")
///
/// cancellable = URLSession.shared.dataTaskPublisher(for: myURL!)
/// .retry(3)
/// .map { page -> WebSiteData in
/// WebSiteData(rawHTML: String(decoding: page.data, as: UTF8.self))
/// }
/// .catch { error in
/// Just(
/// WebSiteData(
/// rawHTML: "<HTML>Unable to load page - timed out.</HTML>"
/// )
/// )
/// }
/// .sink(receiveCompletion: { print ("completion: \($0)") },
/// receiveValue: { print ("value: \($0)") })
///
/// // Prints: The HTML content from the remote URL upon a successful connection,
/// // or returns "<HTML>Unable to load page - timed out.</HTML>" if
/// // the number of retries exceeds the specified value.
///
/// 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> {
return .init(upstream: self, retries: retries)
}
}
extension Publishers {
/// A publisher that attempts to recreate its subscription to a failed upstream
/// publisher.
public struct Retry<Upstream: Publisher>: Publisher {
public typealias Output = Upstream.Output
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?) {
self.upstream = upstream
self.retries = retries
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == Output, Downstream.Failure == Failure
{
upstream.subscribe(Inner(parent: self, downstream: subscriber))
}
}
}
extension Publishers.Retry: Equatable where Upstream: Equatable {}
extension Publishers.Retry {
private final class Inner<Downstream: Subscriber>
: Subscriber,
Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Failure == Failure, Downstream.Input == Output
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private enum State {
case ready(Publishers.Retry<Upstream>, Downstream)
case terminal
}
private enum Chances {
case finite(Int)
case infinite
}
private let lock = UnfairLock.allocate()
private var state: State
private var upstreamSubscription: Subscription?
private var remaining: Chances
private var downstreamNeedsSubscription = true
private var downstreamDemand = Subscribers.Demand.none
private var completionRecursion = false
private var needsSubscribe = false
init(parent: Publishers.Retry<Upstream>, downstream: Downstream) {
state = .ready(parent, downstream)
remaining = parent.retries.map(Chances.finite) ?? .infinite
}
deinit {
lock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard case let .ready(_, downstream) = state, upstreamSubscription == nil
else {
lock.unlock()
subscription.cancel()
return
}
upstreamSubscription = subscription
let downstreamDemand = self.downstreamDemand
let downstreamNeedsSubscription = self.downstreamNeedsSubscription
self.downstreamNeedsSubscription = false
lock.unlock()
if downstreamNeedsSubscription {
downstream.receive(subscription: self)
}
if downstreamDemand != .none {
subscription.request(downstreamDemand)
}
}
func receive(_ input: Input) -> Subscribers.Demand {
lock.lock()
guard case let .ready(_, downstream) = state else {
lock.unlock()
return .none
}
downstreamDemand -= 1
lock.unlock()
let newDemand = downstream.receive(input)
if newDemand == .none { return .none }
lock.lock()
downstreamDemand += newDemand
if let upstreamSubscription = self.upstreamSubscription {
lock.unlock()
upstreamSubscription.request(newDemand)
} else {
lock.unlock()
}
return .none
}
func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
guard case let .ready(parent, downstream) = state else {
lock.unlock()
return
}
if case .failure = completion {
upstreamSubscription = nil
switch remaining {
case .finite(0):
break
case .finite(let attempts):
remaining = .finite(attempts - 1)
fallthrough
case .infinite:
if completionRecursion {
needsSubscribe = true
lock.unlock()
return
}
repeat {
completionRecursion = true
needsSubscribe = false
lock.unlock()
parent.upstream.subscribe(self)
lock.lock()
completionRecursion = false
} while needsSubscribe
lock.unlock()
return
}
}
state = .terminal
lock.unlock()
downstream.receive(completion: completion)
}
func request(_ demand: Subscribers.Demand) {
lock.lock()
guard case .ready = state else {
lock.unlock()
return
}
downstreamDemand += demand
if let upstreamSubscription = self.upstreamSubscription {
lock.unlock()
upstreamSubscription.request(demand)
} else {
lock.unlock()
}
}
func cancel() {
lock.lock()
guard case .ready = state else {
lock.unlock()
return
}
state = .terminal
if let upstreamSubscription = self.upstreamSubscription {
lock.unlock()
upstreamSubscription.cancel()
} else {
lock.unlock()
}
}
var description: String { return "Retry" }
var customMirror: Mirror {
return Mirror(self, children: EmptyCollection())
}
var playgroundDescription: Any { return description }
}
}
@@ -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
@@ -200,11 +203,11 @@ extension DispatchQueue {
/// - Parameter exactly: A binary integer representing a time interval.
public init?<Source: BinaryInteger>(exactly source: Source) {
guard let value = Int(exactly: source) else { return nil }
self = .nanoseconds(value)
self = .seconds(value)
}
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
@@ -60,7 +63,12 @@ internal struct Timer {
repeats: Bool,
block: @escaping (Timer) -> Void
) {
self.init(fire: Date(), interval: timeInterval, repeats: repeats, block: block)
self.init(
fire: Date() + timeInterval,
interval: timeInterval,
repeats: repeats,
block: block
)
}
internal var tolerance: TimeInterval {
@@ -88,6 +96,7 @@ internal struct Timer {
#endif
}
#if canImport(CoreFoundation)
fileprivate func getCFRunLoopTimer() -> CFRunLoopTimer? {
#if canImport(Darwin)
return underlyingTimer
@@ -108,6 +117,7 @@ internal struct Timer {
fatalError("unreachable")
#endif
}
#endif // canImport(CoreFoundation)
}
extension RunLoop {
@@ -133,6 +143,7 @@ extension RunLoop {
}
}
#if canImport(CoreFoundation)
extension RunLoop.Mode {
fileprivate func asCFRunLoopMode() -> CFRunLoopMode {
#if canImport(Darwin)
@@ -154,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)
}
@@ -0,0 +1,11 @@
#if canImport(Combine)
@_exported import Combine
#else
@_exported import OpenCombine
#if canImport(OpenCombineDispatch)
@_exported import OpenCombineDispatch
#endif
#if canImport(OpenCombineFoundation)
@_exported import OpenCombineFoundation
#endif
#endif
+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,6 +5,8 @@
// Created by Sergej Jaskiewicz on 26.08.2019.
//
#if !WASI // TEST_DISCOVERY_CONDITION
import Dispatch
import XCTest
@@ -29,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() {
@@ -254,27 +267,25 @@ final class DispatchQueueSchedulerTests: XCTestCase {
XCTAssertEqual((2 as Stride).magnitude, 2_000_000_000)
XCTAssertNil(Stride(exactly: UInt64.max))
XCTAssertEqual(Stride(exactly: 871 as UInt64)?.magnitude, 871)
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() {
@@ -441,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)
@@ -484,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()
}
@@ -499,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)
@@ -570,7 +583,7 @@ private typealias Scheduler = DispatchQueue.OCombine
private let mainScheduler = DispatchQueue.main.ocombine
private let backgroundScheduler = DispatchQueue.global(qos: .background).ocombine
#endif
#endif // OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
@available(macOS 10.15, iOS 13.0, *)
private typealias Stride = Scheduler.SchedulerTimeType.Stride
@@ -578,3 +591,5 @@ private typealias Stride = Scheduler.SchedulerTimeType.Stride
private struct KeyedWrapper<Value: Codable & Equatable>: Codable, Equatable {
let value: Value
}
#endif // !WASI
@@ -5,6 +5,8 @@
// Created by Sergej Jaskiewicz on 10.12.2019.
//
#if !WASI // TEST_DISCOVERY_CONDITION
import Foundation
import XCTest
@@ -61,3 +63,5 @@ final class JSONDecoderTests: XCTestCase {
cancellable.cancel()
}
}
#endif // !WASI
@@ -5,6 +5,8 @@
// Created by Sergej Jaskiewicz on 10.12.2019.
//
#if !WASI // TEST_DISCOVERY_CONDITION
import Foundation
import XCTest
@@ -65,3 +67,5 @@ final class JSONEncoderTests: XCTestCase {
cancellable.cancel()
}
}
#endif // !WASI
@@ -5,6 +5,8 @@
// Created by Sergej Jaskiewicz on 10.12.2019.
//
#if !WASI // TEST_DISCOVERY_CONDITION
import Foundation
import XCTest
@@ -630,4 +632,6 @@ extension Notification.Name {
self.init(rawValue: rawValue)
}
}
#endif
#endif // !canImport(Darwin) && swift(<5.1)
#endif // !WASI
@@ -5,6 +5,10 @@
// Created by Sergej Jaskiewicz on 14.06.2020.
//
// OperationQueue has serious bugs in swift-corelibs-foundation prior to Swift 5.3.
// (see https://github.com/apple/swift-corelibs-foundation/pull/2779)
#if canImport(Darwin) || swift(>=5.3) && !WASI // TEST_DISCOVERY_CONDITION
import Foundation
import XCTest
@@ -15,10 +19,6 @@ import OpenCombine
import OpenCombineFoundation
#endif
// OperationQueue has serious bugs in swift-corelibs-foundation prior to Swift 5.3.
// (see https://github.com/apple/swift-corelibs-foundation/pull/2779)
#if canImport(Darwin) || swift(>=5.3) // TEST_DISCOVERY_CONDITION
@available(macOS 10.15, iOS 13.0, *)
final class OperationQueueSchedulerTests: XCTestCase {
@@ -225,7 +225,7 @@ final class OperationQueueSchedulerTests: XCTestCase {
}
func testScheduleRepeatingWithRealQueue() {
let mainQueue = OperationQueue.main
let mainQueueScheduler = makeScheduler(OperationQueue.main)
let expectation10ticks = expectation(description: "10 ticks")
expectation10ticks.expectedFulfillmentCount = 10
@@ -234,13 +234,12 @@ final class OperationQueueSchedulerTests: XCTestCase {
let ticks = Atomic([TimeInterval]())
let desiredDelay: TimeInterval = 0.7
let desiredInterval: TimeInterval = 0.3
let desiredDelay: TimeInterval = 0.8
let desiredInterval: TimeInterval = 0.5
let cancellable = executeOnBackgroundThread { () -> Cancellable in
let scheduler = makeScheduler(mainQueue)
return scheduler
.schedule(after: scheduler.now.advanced(by: .init(desiredDelay)),
let cancellable = executeOnBackgroundThread {
mainQueueScheduler
.schedule(after: mainQueueScheduler.now.advanced(by: .init(desiredDelay)),
interval: .init(desiredInterval)) {
XCTAssertTrue(Thread.isMainThread)
ticks.append(Date().timeIntervalSinceReferenceDate)
@@ -256,7 +255,7 @@ final class OperationQueueSchedulerTests: XCTestCase {
RunLoop.main.run(until: Date() + 0.001)
XCTAssertEqual(ticks.count, 0)
wait(for: [expectation10ticks], timeout: 5)
wait(for: [expectation10ticks], timeout: 10)
if ticks.isEmpty {
XCTFail("The scheduler doesn't work")
@@ -332,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 {
@@ -471,4 +471,4 @@ private final class TestOperationQueue: OperationQueue {
#endif // canImport(Darwin)
}
#endif // canImport(Darwin) || swift(>=5.3)
#endif // canImport(Darwin) || swift(>=5.3) && !WASI
@@ -5,6 +5,10 @@
// Created by Sergej Jaskiewicz on 10.12.2019.
//
// PropertyListEncoder and PropertyListDecoder are unavailable in
// swift-corelibs-foundation prior to Swift 5.1.
#if canImport(Darwin) || swift(>=5.1) && !WASI // TEST_DISCOVERY_CONDITION
import Foundation
import XCTest
@@ -15,10 +19,6 @@ import OpenCombine
import OpenCombineFoundation
#endif
// PropertyListEncoder and PropertyListDecoder are unavailable in
// swift-corelibs-foundation prior to Swift 5.1.
#if canImport(Darwin) || swift(>=5.1) // TEST_DISCOVERY_CONDITION
@available(macOS 10.15, iOS 13.0, *)
final class PropertyListDecoderTests: XCTestCase {
func testSuccessfullyDecode() {
@@ -79,4 +79,4 @@ final class PropertyListDecoderTests: XCTestCase {
}
}
#endif // canImport(Darwin) || swift(>=5.1)
#endif // canImport(Darwin) || swift(>=5.1) && !WASI
@@ -5,6 +5,10 @@
// Created by Sergej Jaskiewicz on 10.12.2019.
//
// PropertyListEncoder and PropertyListDecoder are unavailable in
// swift-corelibs-foundation prior to Swift 5.1.
#if canImport(Darwin) || swift(>=5.1) && !WASI // TEST_DISCOVERY_CONDITION
import Foundation
import XCTest
@@ -15,10 +19,6 @@ import OpenCombine
import OpenCombineFoundation
#endif
// PropertyListEncoder and PropertyListDecoder are unavailable in
// swift-corelibs-foundation prior to Swift 5.1.
#if canImport(Darwin) || swift(>=5.1) // TEST_DISCOVERY_CONDITION
@available(macOS 10.15, iOS 13.0, *)
final class PropertyListEncoderTests: XCTestCase {
@@ -84,4 +84,4 @@ final class PropertyListEncoderTests: XCTestCase {
}
}
#endif // canImport(Darwin) || swift(>=5.1)
#endif // canImport(Darwin) || swift(>=5.1) && !WASI

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