1 Commits

Author SHA1 Message Date
Sergej Jaskiewicz 26e86a9905 Implement CombineLatest 2021-06-21 19:09:04 +03:00
102 changed files with 2290 additions and 5917 deletions
+326
View File
@@ -0,0 +1,326 @@
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 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild build-for-testing \
-scheme OpenCombine-Package \
-sdk macosx \
-derivedDataPath DerivedData \
| tee xcodebuild_build-for-testing.log \
| xcpretty
- store_artifacts:
path: xcodebuild_build-for-testing.log
- run:
name: Testing on macOS with xcodebuild
command: |
set -o pipefail \
&& xcodebuild test-without-building \
-scheme OpenCombine-Package \
-sdk macosx \
-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 tests on macOS 11.4.0 (Xcode 12.5.0, Swift 5.4.0)":
macos:
xcode: "12.5.0"
environment:
SWIFT_VERSION: "5.4.0"
<<: *macOS_tests_steps
"Execute compatibility tests on iOS 14.5 (Xcode 12.5.0, Swift 5.4.0)":
macos:
xcode: "12.5.0"
environment:
SWIFT_VERSION: "5.4.0"
steps:
- checkout
- run:
name: Generating Xcode project
command: make generate-compatibility-xcodeproj
- run:
name: Building for testing on iOS 14.5 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild build-for-testing \
-scheme OpenCombine-Package \
-destination "platform=iOS Simulator,name=iPhone 12,OS=14.5" \
-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.5 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild test-without-building \
-scheme OpenCombine-Package \
-destination "platform=iOS Simulator,name=iPhone 12,OS=14.5" \
-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 compatibility tests on macOS 11.4.0 (Xcode 12.5.0, Swift 5.4.0)":
macos:
xcode: "12.5.0"
environment:
SWIFT_VERSION: "5.4.0"
steps:
- checkout
- run:
name: Testing against Combine on macOS 11.4.0
command: |
make test-compatibility
"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
"Execute tests on Ubuntu 18.04 (Swift 5.4)":
docker:
- image: swift:5.4-bionic
environment:
SWIFT_VERSION: "5.4"
<<: *ubuntu_tests_steps
"Run SwiftLint and Danger":
macos:
xcode: "11.4.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.4.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)"
- "Execute tests on macOS 11.4.0 (Xcode 12.5.0, Swift 5.4.0)"
"OpenCombine: execute compatibility tests":
jobs:
# - "Execute compatibility tests on iOS 14.5 (Xcode 12.5.0, Swift 5.4.0)"
- "Execute compatibility tests on macOS 11.4.0 (Xcode 12.5.0, Swift 5.4.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)"
- "Execute tests on Ubuntu 18.04 (Swift 5.4)"
"OpenCombine: run SwiftLint and Danger":
jobs:
- "Run SwiftLint and Danger"
"OpenCombine: validate podspec files":
jobs:
- "Run Pod spec lint"
-16
View File
@@ -1,16 +0,0 @@
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
@@ -1,19 +0,0 @@
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
@@ -1,130 +0,0 @@
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
@@ -1,26 +0,0 @@
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
@@ -1,57 +0,0 @@
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
+2 -34
View File
@@ -1,4 +1,4 @@
name: Wasm
name: SwiftWasm
on:
push:
@@ -7,42 +7,10 @@ on:
branches: [master]
jobs:
carton_wasmer_test_5_3:
name: "Execute tests on Wasm (Swift 5.3)"
carton_wasmer_test:
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
@@ -1,32 +0,0 @@
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
-1
View File
@@ -5,7 +5,6 @@ included:
child_config: Tests/.swiftlint.yml
disabled_rules:
- blanket_disable_command
- block_based_kvo
- class_delegate_protocol
- colon
-24
View File
@@ -1,27 +1,3 @@
# 0.14.0 (23 Apr 2023)
This release is compatible with Xcode 14.2 and Swift 5.7
### Additions
- Primary associated type support for `Publisher`, `Subscriber`, `ConnectablePublisher`, `Subject` and `Scheduler` protocols (#239)
### Bugfixes
- Fixed nullifying the reference to parent in `Future`'s conduit (#239)
# 0.13.0 (1 Feb 2022)
This release is compatible with Xcode 13.2.
### Additions
- Windows support (thank you @MaxDesiatov!)
- `Publishers.Throttle` (#195, thank you @stuaustin)
- `Publishers.PrefixUntilOutput` (#206)
- `Publishers.Zip` (#222, thank you @MaxDesiatov and @ArthurChi)
- `async`/`await` extensions: `Future.value` and `Publisher.values` (#219)
### Bugfixes
- Fixed reentrancy bugs in ` Subscribers.Sink` and `Subscribers.Assign` (#210)
- Fixed lifecycle bugs in `Publishers.Concatenate` (#210)
# 0.12.0 (29 Jan 2021)
This release adds a new `OpenCombineShim` product that will conditionally re-export either
-60
View File
@@ -1,60 +0,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 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
@@ -0,0 +1,76 @@
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(.all(directory: nil),
inline: true,
configFile: ".swiftlint.yml",
strict: true)
if danger.warnings.isEmpty, danger.fails.isEmpty {
markdown("LGTM")
}
+3
View File
@@ -0,0 +1,3 @@
source 'https://rubygems.org'
gem 'xcode-install'
+162
View File
@@ -0,0 +1,162 @@
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.14.0"
spec.version = "0.12.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/OpenCombine/OpenCombine/"
spec.homepage = "https://github.com/broadwaylamb/OpenCombine/"
spec.license = "MIT"
spec.authors = { "Sergej Jaskiewicz" => "jaskiewiczs@icloud.com" }
spec.source = { :git => "https://github.com/OpenCombine/OpenCombine.git", :tag => "#{spec.version}" }
spec.source = { :git => "https://github.com/broadwaylamb/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.14.0"
spec.version = "0.12.0"
spec.summary = "OpenCombine + Dispatch interoperability"
spec.description = <<-DESC
Extends `DispatchQueue` with conformance to the `Scheduler` protocol
DESC
spec.homepage = "https://github.com/OpenCombine/OpenCombine/"
spec.homepage = "https://github.com/broadwaylamb/OpenCombine/"
spec.license = "MIT"
spec.authors = { "Sergej Jaskiewicz" => "jaskiewiczs@icloud.com" }
spec.source = { :git => "https://github.com/OpenCombine/OpenCombine.git", :tag => "#{spec.version}" }
spec.source = { :git => "https://github.com/broadwaylamb/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.13.0'
spec.dependency "OpenCombine", '>= 0.10.2'
end
+4 -4
View File
@@ -1,17 +1,17 @@
Pod::Spec.new do |spec|
spec.name = "OpenCombineFoundation"
spec.version = "0.14.0"
spec.version = "0.12.0"
spec.summary = "OpenCombine + OpenCombineFoundation interoperability"
spec.description = <<-DESC
Adds publishers to Foundation types like NotificationCenter, URLSession etc.
DESC
spec.homepage = "https://github.com/OpenCombine/OpenCombine/"
spec.homepage = "https://github.com/broadwaylamb/OpenCombine/"
spec.license = "MIT"
spec.authors = { "Sergej Jaskiewicz" => "jaskiewiczs@icloud.com" }
spec.source = { :git => "https://github.com/OpenCombine/OpenCombine.git", :tag => "#{spec.version}" }
spec.source = { :git => "https://github.com/broadwaylamb/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.13.0'
spec.dependency "OpenCombine", '>= 0.10.2'
end
+9 -8
View File
@@ -1,4 +1,4 @@
// swift-tools-version:5.5
// swift-tools-version:5.3
import PackageDescription
@@ -6,14 +6,13 @@ import PackageDescription
// See: https://bugs.swift.org/browse/SR-13814
let supportedPlatforms: [Platform] = [
.macOS,
.macCatalyst,
.iOS,
.watchOS,
.tvOS,
.driverKit,
.linux,
.android,
.windows,
// Disable Windows because of https://bugs.swift.org/browse/SR-13817
// .windows,
.wasi,
]
@@ -34,8 +33,6 @@ let package = Package(
condition: .when(platforms: supportedPlatforms.except([.wasi])))
],
exclude: [
"RootProtocols.swift.gyb",
"Concurrency/Publisher+Concurrency.swift.gyb",
"Publishers/Publishers.Encode.swift.gyb",
"Publishers/Publishers.MapKeyPath.swift.gyb",
"Publishers/Publishers.Catch.swift.gyb"
@@ -76,13 +73,17 @@ let package = Package(
]
)
],
cxxLanguageStandard: .cxx17
cxxLanguageStandard: .cxx1z
)
// MARK: Helpers
extension Array where Element == Platform {
func except(_ exceptions: [Platform]) -> [Platform] {
return filter { !exceptions.contains($0) }
// See: https://bugs.swift.org/browse/SR-13813
let exceptionsDescriptions = exceptions.map(String.init(describing:))
return filter { platform in
!exceptionsDescriptions.contains(String(describing: platform))
}
}
}
-91
View File
@@ -1,91 +0,0 @@
// 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
@@ -1,86 +0,0 @@
// 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) }
}
}
+70 -19
View File
@@ -1,20 +1,14 @@
# 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)
[![Cocoapods](https://img.shields.io/cocoapods/v/OpenCombine?color=blue)](https://cocoapods.org/pods/OpenCombine)
![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20iOS%20%7C%20watchOS%20%7C%20tvOS%20%7C%20Wasm-lightgrey.svg)
![Cocoapods](https://img.shields.io/cocoapods/v/OpenCombine?color=blue)
[<img src="https://img.shields.io/badge/slack-OpenCombine-yellow.svg?logo=slack">](https://join.slack.com/t/opencombine/shared_invite/enQtNzE2MjE5NzkxODI0LTYxMjkzNDUxZWViZWI1Njc2YjBhODgxNjRjOTdkZTcxOGU2ZjJjZjYxMGI3NWZkN2RkNGFmZTUzNmU3MGE2ZWM)
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, 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)|
The main goal of this project is to provide a compatible, reliable and efficient implementation which can be used on Apple's operating systems before macOS 10.15 and iOS 13, as well as Linux and WebAssembly.
### Installation
`OpenCombine` contains three public targets: `OpenCombine`, `OpenCombineFoundation` and `OpenCombineDispatch` (the fourth one, `COpenCombineHelpers`, is considered private. Don't import it in your projects).
@@ -32,7 +26,7 @@ To add `OpenCombine` to your [SwiftPM](https://swift.org/package-manager/) packa
```swift
dependencies: [
.package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.14.0")
.package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.12.0")
],
targets: [
.target(
@@ -60,11 +54,72 @@ 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.14.0'
pod 'OpenCombineDispatch', '~> 0.14.0'
pod 'OpenCombineFoundation', '~> 0.14.0'
pod 'OpenCombine', '~> 0.12.0'
pod 'OpenCombineDispatch', '~> 0.12.0'
pod 'OpenCombineFoundation', '~> 0.12.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.
@@ -77,7 +132,3 @@ Currently, `opencombine_lldb.py` defines type summaries for these types:
- `Subscribers.Demand`
- That's all for now.
### Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md).
+102 -276
View File
@@ -2,165 +2,6 @@
// Please remove the corresponding piece from this file if you implement something,
// and complement this file as features are added in Apple's Combine
extension Publishers {
/// A publisher that receives and combines the latest elements from two publishers.
public struct CombineLatest<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 that receives and combines the latest elements from three publishers.
public struct CombineLatest3<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 that receives and combines the latest elements from four publishers.
public struct CombineLatest4<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 {
/// Subscribes to an additional publisher and publishes a tuple upon receiving output from either publisher.
///
/// The combined publisher passes through any requests to *all* upstream publishers. However, it still obeys the demand-fulfilling rule of only sending the request amount downstream. If the demand isnt `.unlimited`, it drops values from upstream publishers. It implements this by using a buffer size of 1 for each upstream, and holds the most recent value in each buffer.
/// All upstream publishers need to finish for this publisher to finsh. If an upstream publisher never publishes a value, this publisher never finishes.
/// If any of the combined publishers terminates with a failure, this publisher also fails.
/// - Parameters:
/// - other: Another publisher to combine with this one.
/// - Returns: A publisher that receives and combines elements from this and another publisher.
public func combineLatest<P>(_ other: P) -> Publishers.CombineLatest<Self, P> where P : Publisher, Self.Failure == P.Failure
/// Subscribes to an additional publisher and invokes a closure upon receiving output from either publisher.
///
/// The combined publisher passes through any requests to *all* upstream publishers. However, it still obeys the demand-fulfilling rule of only sending the request amount downstream. If the demand isnt `.unlimited`, it drops values from upstream publishers. It implements this by using a buffer size of 1 for each upstream, and holds the most recent value in each buffer.
/// All upstream publishers need to finish for this publisher to finsh. If an upstream publisher never publishes a value, this publisher never finishes.
/// If any of the combined publishers terminates with a failure, this publisher also fails.
/// - Parameters:
/// - other: Another publisher to combine with this one.
/// - transform: A closure that receives the most recent value from each publisher and returns a new value to publish.
/// - Returns: A publisher that receives and combines elements from this and another publisher.
public func combineLatest<P, T>(_ other: P, _ transform: @escaping (Self.Output, P.Output) -> T) -> Publishers.Map<Publishers.CombineLatest<Self, P>, T> where P : Publisher, Self.Failure == P.Failure
/// Subscribes to two additional publishers and publishes a tuple upon receiving output from any of the publishers.
///
/// The combined publisher passes through any requests to *all* upstream publishers. However, it still obeys the demand-fulfilling rule of only sending the request amount downstream. If the demand isnt `.unlimited`, it drops values from upstream publishers. It implements this by using a buffer size of 1 for each upstream, and holds the most recent value in each buffer.
/// All upstream publishers need to finish for this publisher to finish. If an upstream publisher never publishes a value, this publisher never finishes.
/// If any of the combined publishers terminates with a failure, this publisher also fails.
/// - Parameters:
/// - publisher1: A second publisher to combine with this one.
/// - publisher2: A third publisher to combine with this one.
/// - Returns: A publisher that receives and combines elements from this publisher and two other publishers.
public func combineLatest<P, Q>(_ publisher1: P, _ publisher2: Q) -> Publishers.CombineLatest3<Self, P, Q> where P : Publisher, Q : Publisher, Self.Failure == P.Failure, P.Failure == Q.Failure
/// Subscribes to two additional publishers and invokes a closure upon receiving output from any of the publishers.
///
/// The combined publisher passes through any requests to *all* upstream publishers. However, it still obeys the demand-fulfilling rule of only sending the request amount downstream. If the demand isnt `.unlimited`, it drops values from upstream publishers. It implements this by using a buffer size of 1 for each upstream, and holds the most recent value in each buffer.
/// All upstream publishers need to finish for this publisher to finish. If an upstream publisher never publishes a value, this publisher never finishes.
/// If any of the combined publishers terminates with a failure, this publisher also fails.
/// - Parameters:
/// - publisher1: A second publisher to combine with this one.
/// - publisher2: A third publisher to combine with this one.
/// - transform: A closure that receives the most recent value from each publisher and returns a new value to publish.
/// - Returns: A publisher that receives and combines elements from this publisher and two other publishers.
public func combineLatest<P, Q, T>(_ publisher1: P, _ publisher2: Q, _ transform: @escaping (Self.Output, P.Output, Q.Output) -> T) -> Publishers.Map<Publishers.CombineLatest3<Self, P, Q>, T> where P : Publisher, Q : Publisher, Self.Failure == P.Failure, P.Failure == Q.Failure
/// Subscribes to three additional publishers and publishes a tuple upon receiving output from any of the publishers.
///
/// The combined publisher passes through any requests to *all* upstream publishers. However, it still obeys the demand-fulfilling rule of only sending the request amount downstream. If the demand isnt `.unlimited`, it drops values from upstream publishers. It implements this by using a buffer size of 1 for each upstream, and holds the most recent value in each buffer.
/// All upstream publishers need to finish for this publisher to finish. If an upstream publisher never publishes a value, this publisher never finishes.
/// If any of the combined publishers terminates with a failure, this publisher also fails.
/// - Parameters:
/// - publisher1: A second publisher to combine with this one.
/// - publisher2: A third publisher to combine with this one.
/// - publisher3: A fourth publisher to combine with this one.
/// - Returns: A publisher that receives and combines elements from this publisher and three other publishers.
public func combineLatest<P, Q, R>(_ publisher1: P, _ publisher2: Q, _ publisher3: R) -> Publishers.CombineLatest4<Self, P, Q, R> where P : Publisher, Q : Publisher, R : Publisher, Self.Failure == P.Failure, P.Failure == Q.Failure, Q.Failure == R.Failure
/// Subscribes to three additional publishers and invokes a closure upon receiving output from any of the publishers.
///
/// The combined publisher passes through any requests to *all* upstream publishers. However, it still obeys the demand-fulfilling rule of only sending the request amount downstream. If the demand isnt `.unlimited`, it drops values from upstream publishers. It implements this by using a buffer size of 1 for each upstream, and holds the most recent value in each buffer.
/// All upstream publishers need to finish for this publisher to finish. If an upstream publisher never publishes a value, this publisher never finishes.
/// If any of the combined publishers terminates with a failure, this publisher also fails.
/// - Parameters:
/// - publisher1: A second publisher to combine with this one.
/// - publisher2: A third publisher to combine with this one.
/// - publisher3: A fourth publisher to combine with this one.
/// - transform: A closure that receives the most recent value from each publisher and returns a new value to publish.
/// - Returns: A publisher that receives and combines elements from this publisher and three other publishers.
public func combineLatest<P, Q, R, T>(_ publisher1: P, _ publisher2: Q, _ publisher3: R, _ transform: @escaping (Self.Output, P.Output, Q.Output, R.Output) -> T) -> Publishers.Map<Publishers.CombineLatest4<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 {
/// A strategy for collecting received elements.
@@ -636,31 +477,22 @@ extension Publisher {
extension Publishers {
/// A publisher that attempts to recreate its subscription to a failed upstream publisher.
public struct Retry<Upstream> : Publisher where Upstream : Publisher {
/// 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 = Upstream.Output
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 = Upstream.Failure
public typealias Failure = A.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
public let a: A
/// 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?
public let b: B
/// 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?)
public init(_ a: A, _ b: B)
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
///
@@ -668,58 +500,27 @@ extension Publishers {
/// - 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
public func receive<S>(subscriber: S) where S : Subscriber, B.Failure == S.Failure, S.Input == (A.Output, B.Output)
}
}
extension Publisher {
/// Attempts to recreate a failed subscription with the upstream publisher using a specified number of attempts to establish the connection.
///
/// After exceeding the specified number of retries, the publisher passes the failure to the downstream receiver.
/// - Parameter retries: The number of times to attempt to recreate the subscription.
/// - Returns: A publisher that attempts to recreate its subscription to a failed upstream publisher.
public func retry(_ retries: Int) -> Publishers.Retry<Self>
}
extension Publisher {
/// Attempts to recreate a failed subscription with the upstream publisher using a specified number of attempts to establish the connection.
///
/// After exceeding the specified number of retries, the publisher passes the failure to the downstream receiver.
/// - Parameter retries: The number of times to attempt to recreate the subscription.
/// - Returns: A publisher that attempts to recreate its subscription to a failed upstream publisher.
public func retry(_ retries: Int) -> Publishers.Retry<Self>
}
extension Publishers {
/// A publisher that publishes either the most-recent or first element published by the upstream publisher in a specified time interval.
public struct Throttle<Upstream, Context> : Publisher where Upstream : Publisher, Context : Scheduler {
/// 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 = Upstream.Output
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 = Upstream.Failure
public typealias Failure = A.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
public let a: A
/// The interval in which to find and emit the most recent element.
public let interval: Context.SchedulerTimeType.Stride
public let b: B
/// The scheduler on which to publish elements.
public let scheduler: Context
public let c: C
/// 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)
public init(_ a: A, _ b: B, _ c: C)
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
///
@@ -727,89 +528,114 @@ extension Publishers {
/// - 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
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 {
/// Publishes either the most-recent or first element published by the upstream
/// publisher in the specified time interval.
/// Combine elements from another publisher and deliver pairs of elements as tuples.
///
/// 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.
/// 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.
///
/// 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:
/// - 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.
///
/// 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).") }
/// )
/// 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.
///
/// // 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.
/// - 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:
/// - 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
}
/// - 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
extension Publishers.CombineLatest : Equatable where A : Equatable, B : Equatable {
/// Returns a Boolean value that indicates whether two publishers are equivalent.
/// 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:
/// - lhs: A combineLatest publisher to compare for equality.
/// - rhs: Another combineLatest publisher to compare for equality.
/// - Returns: `true` if the corresponding upstream publishers of each combineLatest publisher are equal, `false` otherwise.
public static func == (lhs: Publishers.CombineLatest<A, B>, rhs: Publishers.CombineLatest<A, B>) -> Bool
}
/// - 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
extension Publishers.CombineLatest3 : Equatable where A : Equatable, B : Equatable, C : Equatable {
/// Returns a Boolean value indicating whether two values are equal.
/// Combine elements from three other publishers and deliver groups of elements as tuples.
///
/// Equality is the inverse of inequality. For any values `a` and `b`,
/// `a == b` implies that `a != b` is `false`.
/// 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:
/// - lhs: A value to compare.
/// - rhs: Another value to compare.
public static func == (lhs: Publishers.CombineLatest3<A, B, C>, rhs: Publishers.CombineLatest3<A, B, C>) -> Bool
}
/// - 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
extension Publishers.CombineLatest4 : Equatable where A : Equatable, B : Equatable, C : Equatable, D : Equatable {
/// Returns a Boolean value indicating whether two values are equal.
/// Combine elements from three other publishers and deliver a transformed output.
///
/// Equality is the inverse of inequality. For any values `a` and `b`,
/// `a == b` implies that `a != b` is `false`.
/// 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:
/// - lhs: A value to compare.
/// - rhs: Another value to compare.
public static func == (lhs: Publishers.CombineLatest4<A, B, C, D>, rhs: Publishers.CombineLatest4<A, B, C, D>) -> Bool
/// - 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.Merge : Equatable where A : Equatable, B : Equatable {
@@ -10,31 +10,13 @@
#include <atomic>
#include <cstdlib>
#include <system_error>
#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
#include <pthread.h>
#include <signal.h>
#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 {
@@ -65,11 +47,10 @@ public:
virtual void unlock() = 0;
virtual void assertOwner() {}
virtual ~PlatformIndependentMutex() {}
virtual ~PlatformIndependentMutex() noexcept(false) {}
};
#if OPENCOMBINE_HAS_PTHREAD
class PThreadMutex final : PlatformIndependentMutex {
class PThreadMutex : PlatformIndependentMutex {
private:
pthread_mutex_t mutex_;
public:
@@ -85,16 +66,20 @@ public:
PThreadMutex(PThreadMutex&&) = delete;
PThreadMutex& operator=(PThreadMutex&&) = delete;
void lock() override {
void lock() override final {
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutex_lock(&mutex_));
}
void unlock() override {
void unlock() override final {
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutex_unlock(&mutex_));
}
~PThreadMutex() {
pthread_mutex_destroy(&mutex_);
// 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_));
}
protected:
class Attributes {
@@ -122,8 +107,12 @@ protected:
setType(PTHREAD_MUTEX_ERRORCHECK);
}
~Attributes() {
pthread_mutexattr_destroy(&attrs_);
~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_));
}
private:
void setType(int type) {
@@ -135,7 +124,21 @@ protected:
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutex_init(&mutex_, attributes.raw()));
}
};
#endif // OPENCOMBINE_HAS_PTHREAD
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;
};
#ifdef __APPLE__
@@ -164,60 +167,34 @@ public:
};
#endif // __APPLE__
template <typename Mu>
class GenericMutex final : PlatformIndependentMutex {
Mu mutex_;
public:
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
extern "C" {
uint64_t opencombine_next_combine_identifier(void) {
return next_combine_identifier.fetch_add(1);
}
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};
}
#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};
return {new PThreadMutex};
#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 StdRecursiveMutex};
return {new PThreadRecursiveMutex};
OPENCOMBINE_HANDLE_EXCEPTION_END
}
@@ -260,9 +237,7 @@ 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"
@@ -1,3 +0,0 @@
module COpenCombineHelpers {
header "COpenCombineHelpers.h"
}
@@ -1,24 +0,0 @@
//
// 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
@@ -1,136 +0,0 @@
//
// 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
@@ -1,397 +0,0 @@
//
//
// 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
@@ -1,249 +0,0 @@
${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
@@ -0,0 +1,24 @@
//
// 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
}
+10 -5
View File
@@ -108,7 +108,8 @@ public final class CurrentValueSubject<Output, Failure: Error>: Subject {
}
active = false
self.completion = completion
let downstreams = self.downstreams.take()
let downstreams = self.downstreams
self.downstreams.removeAll()
lock.unlock()
downstreams.forEach { conduit in
conduit.finish(completion: completion)
@@ -180,11 +181,13 @@ extension CurrentValueSubject {
override func finish(completion: Subscribers.Completion<Failure>) {
lock.lock()
guard let downstream = self.downstream.take() else {
guard let downstream = self.downstream else {
lock.unlock()
return
}
let parent = self.parent.take()
self.downstream = nil
let parent = self.parent
self.parent = nil
lock.unlock()
parent?.disassociate(self)
downstreamLock.lock()
@@ -224,11 +227,13 @@ extension CurrentValueSubject {
override func cancel() {
lock.lock()
if downstream.take() == nil {
if self.downstream == nil {
lock.unlock()
return
}
let parent = self.parent.take()
self.downstream = nil
let parent = self.parent
self.parent = nil
lock.unlock()
parent?.disassociate(self)
}
+52 -68
View File
@@ -43,7 +43,8 @@ public final class Future<Output, Failure: Error>: Publisher {
return
}
self.result = result
let downstreams = self.downstreams.take()
let downstreams = self.downstreams
self.downstreams.removeAll()
lock.unlock()
switch result {
case .success(let output):
@@ -86,32 +87,12 @@ extension Future {
CustomPlaygroundDisplayConvertible
where Downstream.Input == Output, Downstream.Failure == Failure
{
private enum State {
case active(Downstream, hasAnyDemand: Bool)
case terminal
var downstream: Downstream? {
switch self {
case .active(let downstream, hasAnyDemand: _):
return downstream
case .terminal:
return nil
}
}
fileprivate var parent: Future?
var hasAnyDemand: Bool {
switch self {
case .active(_, let hasAnyDemand):
return hasAnyDemand
case .terminal:
return false
}
}
}
fileprivate var downstream: Downstream?
private var parent: Future?
private var state: State
fileprivate var hasAnyDemand = false
private var lock = UnfairLock.allocate()
@@ -119,7 +100,7 @@ extension Future {
fileprivate init(parent: Future, downstream: Downstream) {
self.parent = parent
self.state = .active(downstream, hasAnyDemand: false)
self.downstream = downstream
}
deinit {
@@ -127,8 +108,21 @@ extension Future {
downstreamLock.deallocate()
}
fileprivate func lockedFulfill(downstream: Downstream,
result: Result<Output, Failure>) {
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()
switch result {
case .success(let output):
_ = downstream.receive(output)
@@ -136,27 +130,7 @@ 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)
}
@@ -176,37 +150,47 @@ extension Future {
override func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
lock.lock()
guard case .active(let downstream, hasAnyDemand: _) = state else {
guard let downstream = self.downstream, let parent = self.parent else {
lock.unlock()
return
}
state = .active(downstream, hasAnyDemand: true)
hasAnyDemand = true
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 {
parent.lock.lock()
guard let result = parent.result else {
parent.lock.unlock()
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()
switch state {
case .active:
state = .terminal
let parent = self.parent.take()
lock.unlock()
parent?.disassociate(self)
case .terminal:
if self.downstream == nil {
lock.unlock()
return
}
self.downstream = nil
let parent = self.parent
self.parent = nil
lock.unlock()
parent?.disassociate(self)
}
var description: String { return "Future" }
@@ -216,8 +200,8 @@ extension Future {
defer { lock.unlock() }
let children: [Mirror.Child] = [
("parent", parent as Any),
("downstream", state.downstream as Any),
("hasAnyDemand", state.hasAnyDemand),
("downstream", downstream as Any),
("hasAnyDemand", hasAnyDemand),
("subject", parent as Any)
]
return Mirror(self, children: children)
-426
View File
@@ -1,426 +0,0 @@
//
//
// 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
@@ -0,0 +1,205 @@
//
// AbstractCombineLatest.swift
//
//
// Created by Sergej Jaskiewicz on 10.12.2019.
//
internal class AbstractCombineLatest<Output, Failure, Downstream: Subscriber>
where Downstream.Input == Output, Downstream.Failure == Failure
{
private let downstream: Downstream
// TODO: The size of these arrays always stays the same.
// Maybe we can leverage ManagedBuffer/ManagedBufferPointer here
// to avoid additional allocations.
private var buffers: [Any?] // 0x78
private var subscriptions: [Subscription?] // 0x80
private var demand = Subscribers.Demand.none // 0x88
private var recursion = false // 0x90
private var finished = false // 0x98
private var errored = false // 0xA0
private var cancelled = false // 0xA8
private let upstreamCount: Int // 0xB0
private var finishCount = 0 // 0xB8
private let lock = UnfairLock.allocate() // 0xC0
private let downstreamLock = UnfairRecursiveLock.allocate() // 0xC8
internal init(downstream: Downstream, upstreamCount: Int) {
self.downstream = downstream
self.buffers = Array(repeating: nil, count: upstreamCount)
self.subscriptions = Array(repeating: nil, count: upstreamCount)
self.upstreamCount = upstreamCount
}
deinit {
lock.deallocate()
downstreamLock.deallocate()
}
// TODO: There should be more type-safe (and faster) way.
// E. g. what if we store `buffers` in subclasses?
internal func convert(values: [Any?]) -> Output {
abstractMethod()
}
fileprivate final func receive(subscription: Subscription, index: Int) {
lock.lock()
guard !cancelled && subscriptions[index] == nil else {
lock.unlock()
subscription.cancel()
return
}
subscriptions[index] = subscription
lock.unlock()
}
fileprivate final func receive(_ input: Any, index: Int) -> Subscribers.Demand {
lock.lock()
if cancelled || finished {
lock.unlock()
return .none
}
buffers[index] = input
guard !recursion && demand > 0 && buffers.allSatisfy({ $0 != nil }) else {
lock.unlock()
return .none
}
demand -= 1
recursion = true
lock.unlock()
downstreamLock.lock()
let newDemand = downstream.receive(convert(values: buffers))
downstreamLock.unlock()
lock.lock()
recursion = false
demand += newDemand
lock.unlock()
return .none
}
fileprivate final func receive(completion: Subscribers.Completion<Failure>,
index: Int) {
switch completion {
case .finished:
lock.lock()
if finished {
lock.unlock()
return
}
finishCount += 1
subscriptions[index] = nil
if finishCount == upstreamCount {
finished = true
buffers = Array(repeating: nil, count: upstreamCount)
lock.unlock()
downstreamLock.lock()
downstream.receive(completion: completion)
downstreamLock.unlock()
} else {
lock.unlock()
}
case .failure:
lock.lock()
finished = true
errored = true
let subscriptions = self.subscriptions
self.subscriptions = Array(repeating: nil, count: upstreamCount)
buffers = Array(repeating: nil, count: upstreamCount)
lock.unlock()
for (i, subscription) in subscriptions.enumerated() where i != index {
subscription?.cancel()
}
downstreamLock.lock()
downstream.receive(completion: completion)
downstreamLock.unlock()
}
}
}
extension AbstractCombineLatest: Subscription {
internal func request(_ demand: Subscribers.Demand) {
demand.assertNonZero() // TODO: Test this
lock.lock()
guard !cancelled && !finished else {
lock.unlock()
return
}
self.demand += demand
lock.unlock()
for subscription in subscriptions {
subscription?.request(demand)
}
}
internal func cancel() {
lock.lock()
cancelled = true
let subscriptions = self.subscriptions
self.subscriptions = Array(repeating: nil, count: upstreamCount)
buffers = Array(repeating: nil, count: upstreamCount)
lock.unlock()
for subscription in subscriptions {
subscription?.cancel()
}
}
}
extension AbstractCombineLatest: CustomStringConvertible {
internal var description: String { return "CombineLatest" }
}
extension AbstractCombineLatest: CustomReflectable {
internal var customMirror: Mirror {
lock.lock()
defer { lock.unlock() }
let children: [Mirror.Child] = [
("downstream", downstream),
("upstreamSubscriptions", subscriptions),
("demand", demand),
("buffers", buffers)
]
return Mirror(self, children: children)
}
}
extension AbstractCombineLatest: CustomPlaygroundDisplayConvertible {
internal final var playgroundDescription: Any { return description }
}
extension AbstractCombineLatest {
internal struct Side<Input>: Subscriber, CustomStringConvertible {
private let index: Int
private let combiner: AbstractCombineLatest
internal let combineIdentifier = CombineIdentifier()
internal init(index: Int, combiner: AbstractCombineLatest) {
self.index = index
self.combiner = combiner
}
internal func receive(subscription: Subscription) {
combiner.receive(subscription: subscription, index: index)
}
internal func receive(_ input: Input) -> Subscribers.Demand {
return combiner.receive(input, index: index)
}
internal func receive(completion: Subscribers.Completion<Failure>) {
combiner.receive(completion: completion, index: index)
}
internal var description: String { return "CombineLatest" }
}
}
@@ -11,12 +11,6 @@ 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 {
@@ -56,4 +50,8 @@ extension ConduitList {
self = .many(set)
}
}
internal mutating func removeAll() {
self = .empty
}
}
@@ -187,7 +187,8 @@ extension PublishedSubject {
return
}
self.downstream = nil
let parent = self.parent.take()
let parent = self.parent
self.parent = nil
lock.unlock()
parent?.disassociate(self)
}
-30
View File
@@ -1,30 +0,0 @@
//
// 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
}
}
+10 -5
View File
@@ -85,7 +85,8 @@ public final class PassthroughSubject<Output, Failure: Error>: Subject {
}
active = false
self.completion = completion
let downstreams = self.downstreams.take()
let downstreams = self.downstreams
self.downstreams.removeAll()
lock.unlock()
downstreams.forEach { conduit in
conduit.finish(completion: completion)
@@ -167,11 +168,13 @@ extension PassthroughSubject {
override func finish(completion: Subscribers.Completion<Failure>) {
lock.lock()
guard let downstream = self.downstream.take() else {
guard let downstream = self.downstream else {
lock.unlock()
return
}
let parent = self.parent.take()
self.downstream = nil
let parent = self.parent
self.parent = nil
lock.unlock()
parent?.disassociate(self)
downstreamLock.lock()
@@ -194,11 +197,13 @@ extension PassthroughSubject {
override func cancel() {
lock.lock()
if downstream.take() == nil {
if self.downstream == nil {
lock.unlock()
return
}
let parent = self.parent.take()
self.downstream = nil
let parent = self.parent
self.parent = nil
lock.unlock()
parent?.disassociate(self)
}
@@ -1,53 +0,0 @@
//
// 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
@@ -0,0 +1,112 @@
//
// 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)
}
}
@@ -0,0 +1,408 @@
//
//
// Auto-generated from GYB template. DO NOT EDIT!
//
//
//
//
// Publishers.CombineLatest.swift.gyb
//
//
// Created by Sergej Jaskiewicz on 10.12.2019.
//
// swiftlint:disable generic_type_name
// swiftlint:disable large_tuple
// MARK: - CombineLatest methods on Publisher
extension Publisher {
/// Subscribes to an additional publisher and publishes a tuple upon
/// receiving output from either publisher.
///
/// The combined publisher passes through any requests to *all* upstream publishers.
/// However, it still obeys the demand-fulfilling rule of only sending the request
/// amount downstream. If the demand isnt `.unlimited`, it drops values from upstream
/// publishers. It implements this by using a buffer size of 1 for each upstream, and
/// holds the most recent value in each buffer.
/// All upstream publishers need to finish for this publisher to finsh. If an upstream
/// publisher never publishes a value, this publisher never finishes.
/// If any of the combined publishers terminates with a failure, this publisher also
/// fails.
///
/// - Parameters:
/// - other: Another publisher to combine with this one.
/// - Returns: A publisher that receives and combines elements from this and another
/// publisher.
public func combineLatest<P: Publisher>(
_ other: P
) -> Publishers.CombineLatest<Self, P>
where Failure == P.Failure
{
return .init(self, other)
}
/// Subscribes to an additional publisher and invokes a closure
/// upon receiving output from either publisher.
///
/// The combined publisher passes through any requests to *all* upstream publishers.
/// However, it still obeys the demand-fulfilling rule of only sending the request
/// amount downstream. If the demand isnt `.unlimited`, it drops values from upstream
/// publishers. It implements this by using a buffer size of 1 for each upstream, and
/// holds the most recent value in each buffer.
/// All upstream publishers need to finish for this publisher to finsh. If an upstream
/// publisher never publishes a value, this publisher never finishes.
/// If any of the combined publishers terminates with a failure, this publisher also
/// fails.
///
/// - Parameters:
/// - other: Another publisher to combine with this one.
/// - transform: A closure that receives the most recent value from each publisher
/// and returns a new value to publish.
/// - Returns: A publisher that receives and combines elements from this and another
/// publisher.
public func combineLatest<P: Publisher, Result>(
_ other: P,
_ transform: @escaping (Output, P.Output) -> Result
) -> Publishers.Map<Publishers.CombineLatest<Self, P>, Result>
where Failure == P.Failure
{
return Publishers.CombineLatest(self, other).map {
transform($0, $1)
}
}
/// Subscribes to two additional publishers and publishes a tuple upon
/// receiving output from either publisher.
///
/// The combined publisher passes through any requests to *all* upstream publishers.
/// However, it still obeys the demand-fulfilling rule of only sending the request
/// amount downstream. If the demand isnt `.unlimited`, it drops values from upstream
/// publishers. It implements this by using a buffer size of 1 for each upstream, and
/// holds the most recent value in each buffer.
/// All upstream publishers need to finish for this publisher to finsh. If an upstream
/// publisher never publishes a value, this publisher never finishes.
/// If any of the combined publishers terminates with a failure, this publisher also
/// fails.
///
/// - Parameters:
/// - publisher1: A second publisher to combine with this one.
/// - publisher2: A third publisher to combine with this one.
/// - Returns: A publisher that receives and combines elements from this and another
/// publisher.
public func combineLatest<P: Publisher, Q: Publisher>(
_ publisher1: P,
_ publisher2: Q
) -> Publishers.CombineLatest3<Self, P, Q>
where Failure == P.Failure,
P.Failure == Q.Failure
{
return .init(self, publisher1, publisher2)
}
/// Subscribes to two additional publishers and invokes a closure
/// upon receiving output from either publisher.
///
/// The combined publisher passes through any requests to *all* upstream publishers.
/// However, it still obeys the demand-fulfilling rule of only sending the request
/// amount downstream. If the demand isnt `.unlimited`, it drops values from upstream
/// publishers. It implements this by using a buffer size of 1 for each upstream, and
/// holds the most recent value in each buffer.
/// All upstream publishers need to finish for this publisher to finsh. If an upstream
/// publisher never publishes a value, this publisher never finishes.
/// If any of the combined publishers terminates with a failure, this publisher also
/// fails.
///
/// - Parameters:
/// - publisher1: A second publisher to combine with this one.
/// - publisher2: A third publisher to combine with this one.
/// - transform: A closure that receives the most recent value from each publisher
/// and returns a new value to publish.
/// - Returns: A publisher that receives and combines elements from this and another
/// publisher.
public func combineLatest<P: Publisher, Q: Publisher, Result>(
_ publisher1: P,
_ publisher2: Q,
_ transform: @escaping (Output, P.Output, Q.Output) -> Result
) -> Publishers.Map<Publishers.CombineLatest3<Self, P, Q>, Result>
where Failure == P.Failure,
P.Failure == Q.Failure
{
return Publishers.CombineLatest3(self, publisher1, publisher2).map {
transform($0, $1, $2)
}
}
/// Subscribes to three additional publishers and publishes a tuple upon
/// receiving output from either publisher.
///
/// The combined publisher passes through any requests to *all* upstream publishers.
/// However, it still obeys the demand-fulfilling rule of only sending the request
/// amount downstream. If the demand isnt `.unlimited`, it drops values from upstream
/// publishers. It implements this by using a buffer size of 1 for each upstream, and
/// holds the most recent value in each buffer.
/// All upstream publishers need to finish for this publisher to finsh. If an upstream
/// publisher never publishes a value, this publisher never finishes.
/// If any of the combined publishers terminates with a failure, this publisher also
/// fails.
///
/// - Parameters:
/// - publisher1: A second publisher to combine with this one.
/// - publisher2: A third publisher to combine with this one.
/// - publisher3: A fourth publisher to combine with this one.
/// - Returns: A publisher that receives and combines elements from this and another
/// publisher.
public func combineLatest<P: Publisher, Q: Publisher, R: Publisher>(
_ publisher1: P,
_ publisher2: Q,
_ publisher3: R
) -> Publishers.CombineLatest4<Self, P, Q, R>
where Failure == P.Failure,
P.Failure == Q.Failure,
Q.Failure == R.Failure
{
return .init(self, publisher1, publisher2, publisher3)
}
/// Subscribes to three additional publishers and invokes a closure
/// upon receiving output from either publisher.
///
/// The combined publisher passes through any requests to *all* upstream publishers.
/// However, it still obeys the demand-fulfilling rule of only sending the request
/// amount downstream. If the demand isnt `.unlimited`, it drops values from upstream
/// publishers. It implements this by using a buffer size of 1 for each upstream, and
/// holds the most recent value in each buffer.
/// All upstream publishers need to finish for this publisher to finsh. If an upstream
/// publisher never publishes a value, this publisher never finishes.
/// If any of the combined publishers terminates with a failure, this publisher also
/// fails.
///
/// - Parameters:
/// - publisher1: A second publisher to combine with this one.
/// - publisher2: A third publisher to combine with this one.
/// - publisher3: A fourth publisher to combine with this one.
/// - transform: A closure that receives the most recent value from each publisher
/// and returns a new value to publish.
/// - Returns: A publisher that receives and combines elements from this and another
/// publisher.
public func combineLatest<P: Publisher, Q: Publisher, R: Publisher, Result>(
_ publisher1: P,
_ publisher2: Q,
_ publisher3: R,
_ transform: @escaping (Output, P.Output, Q.Output, R.Output) -> Result
) -> Publishers.Map<Publishers.CombineLatest4<Self, P, Q, R>, Result>
where Failure == P.Failure,
P.Failure == Q.Failure,
Q.Failure == R.Failure
{
return Publishers.CombineLatest4(self, publisher1, publisher2, publisher3).map {
transform($0, $1, $2, $3)
}
}
}
// MARK: - CombineLatest publishers
extension Publishers {
/// A publisher that receives and combines the latest elements from two
/// publishers.
public struct CombineLatest<A: Publisher, B: Publisher>
: Publisher
where A.Failure == B.Failure
{
public typealias Output = (A.Output, B.Output)
public typealias Failure = A.Failure
public let a: A
public let b: B
public init(
_ a: A,
_ b: B
) {
self.a = a
self.b = b
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Failure == Failure,
Downstream.Input == Output
{
typealias Inner = CombineLatest2Inner<A.Output,
B.Output,
Failure,
Downstream>
let inner = Inner(downstream: subscriber, upstreamCount: 2)
a.subscribe(Inner.Side(index: 0, combiner: inner))
b.subscribe(Inner.Side(index: 1, combiner: inner))
subscriber.receive(subscription: inner)
}
}
/// A publisher that receives and combines the latest elements from three
/// publishers.
public struct CombineLatest3<A: Publisher, B: Publisher, C: Publisher>
: Publisher
where A.Failure == B.Failure,
B.Failure == C.Failure
{
public typealias Output = (A.Output, B.Output, C.Output)
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
) {
self.a = a
self.b = b
self.c = c
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Failure == Failure,
Downstream.Input == Output
{
typealias Inner = CombineLatest3Inner<A.Output,
B.Output,
C.Output,
Failure,
Downstream>
let inner = Inner(downstream: subscriber, upstreamCount: 3)
a.subscribe(Inner.Side(index: 0, combiner: inner))
b.subscribe(Inner.Side(index: 1, combiner: inner))
c.subscribe(Inner.Side(index: 2, combiner: inner))
subscriber.receive(subscription: inner)
}
}
/// A publisher that receives and combines the latest elements from four
/// publishers.
public struct CombineLatest4<A: Publisher, B: Publisher, C: Publisher, D: Publisher>
: Publisher
where A.Failure == B.Failure,
B.Failure == C.Failure,
C.Failure == D.Failure
{
public typealias Output = (A.Output, B.Output, C.Output, D.Output)
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
) {
self.a = a
self.b = b
self.c = c
self.d = d
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Failure == Failure,
Downstream.Input == Output
{
typealias Inner = CombineLatest4Inner<A.Output,
B.Output,
C.Output,
D.Output,
Failure,
Downstream>
let inner = Inner(downstream: subscriber, upstreamCount: 4)
a.subscribe(Inner.Side(index: 0, combiner: inner))
b.subscribe(Inner.Side(index: 1, combiner: inner))
c.subscribe(Inner.Side(index: 2, combiner: inner))
d.subscribe(Inner.Side(index: 3, combiner: inner))
subscriber.receive(subscription: inner)
}
}
}
// MARK: - Equatable conformances
extension Publishers.CombineLatest: Equatable
where
A: Equatable,
B: Equatable {}
extension Publishers.CombineLatest3: Equatable
where
A: Equatable,
B: Equatable,
C: Equatable {}
extension Publishers.CombineLatest4: Equatable
where
A: Equatable,
B: Equatable,
C: Equatable,
D: Equatable {}
// MARK: - Inners
private final class CombineLatest2Inner<Input0,
Input1,
Failure,
Downstream: Subscriber>
: AbstractCombineLatest<(Input0, Input1), Failure, Downstream>
where Downstream.Input == (Input0, Input1),
Downstream.Failure == Failure
{
override func convert(values: [Any?]) -> (Input0, Input1) {
return (values[0] as! Input0,
values[1] as! Input1)
}
}
private final class CombineLatest3Inner<Input0,
Input1,
Input2,
Failure,
Downstream: Subscriber>
: AbstractCombineLatest<(Input0, Input1, Input2), Failure, Downstream>
where Downstream.Input == (Input0, Input1, Input2),
Downstream.Failure == Failure
{
override func convert(values: [Any?]) -> (Input0, Input1, Input2) {
return (values[0] as! Input0,
values[1] as! Input1,
values[2] as! Input2)
}
}
private final class CombineLatest4Inner<Input0,
Input1,
Input2,
Input3,
Failure,
Downstream: Subscriber>
: AbstractCombineLatest<(Input0, Input1, Input2, Input3), Failure, Downstream>
where Downstream.Input == (Input0, Input1, Input2, Input3),
Downstream.Failure == Failure
{
override func convert(values: [Any?]) -> (Input0, Input1, Input2, Input3) {
return (values[0] as! Input0,
values[1] as! Input1,
values[2] as! Input2,
values[3] as! Input3)
}
}
@@ -222,7 +222,8 @@ extension Publishers.Encode {
} catch {
lock.lock()
finished = true
let subscription = self.subscription.take()
let subscription = self.subscription
self.subscription = nil
lock.unlock()
subscription?.cancel()
downstream.receive(completion: .failure(error))
@@ -251,10 +252,11 @@ extension Publishers.Encode {
func cancel() {
lock.lock()
guard !finished, let subscription = self.subscription.take() else {
guard !finished, let subscription = self.subscription else {
lock.unlock()
return
}
self.subscription = nil
finished = true
lock.unlock()
subscription.cancel()
@@ -334,7 +336,8 @@ extension Publishers.Decode {
} catch {
lock.lock()
finished = true
let subscription = self.subscription.take()
let subscription = self.subscription
self.subscription = nil
lock.unlock()
subscription?.cancel()
downstream.receive(completion: .failure(error))
@@ -363,10 +366,11 @@ extension Publishers.Decode {
func cancel() {
lock.lock()
guard !finished, let subscription = self.subscription.take() else {
guard !finished, let subscription = self.subscription else {
lock.unlock()
return
}
self.subscription = nil
finished = true
lock.unlock()
subscription.cancel()
@@ -11,8 +11,6 @@
// Created by Sergej Jaskiewicz on 03/10/2019.
//
// swiftlint:disable large_tuple
extension Publisher {
/// Publishes the value of the key path.
///
@@ -33,7 +31,7 @@ extension Publisher {
/// .sink {
/// print ("Rolled: \($0)")
/// }
/// // Prints "Rolled: 4 (or some other random value).
/// // Prints "Rolled: 6 (or some other random value).
///
/// - Parameters:
/// - keyPath: The key path of a property on `Output`.
@@ -70,7 +68,7 @@ extension Publisher {
/// (total \(values.0 + values.1))
/// """)
/// }
/// // Prints "Rolled: 4, 1 (total: 5)" (or other random values).
/// // Prints "Rolled: 5, 3 (total: 8)" (or other random values).
///
/// - Parameters:
/// - keyPath0: The key path of a property on `Output`.
@@ -112,7 +110,7 @@ extension Publisher {
/// (total \(values.0 + values.1 + values.2))
/// """)
/// }
/// // Prints "Rolled: 3, 5, 4 (total: 12)" (or other random values).
/// // Prints "Rolled: 2, 4, 3 (total: 9)" (or other random values).
///
/// - Parameters:
/// - keyPath0: The key path of a property on `Output`.
+2 -1
View File
@@ -293,7 +293,8 @@ extension Just {
func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
guard let downstream = self.downstream.take() else { return }
guard let downstream = self.downstream else { return }
self.downstream = nil
_ = downstream.receive(value)
downstream.receive(completion: .finished)
}
@@ -122,7 +122,8 @@ extension Optional.OCombine {
func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
guard let downstream = self.downstream.take() else { return }
guard let downstream = self.downstream else { return }
self.downstream = nil
_ = 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 integrity checks that are active during
/// testing. However, it is important to note that, like its Swift counterpart
/// Use `assertNoFailure()` for internal sanity 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 during development and testing, _and_ in shipping versions of code.
/// triggered in both development/testing _and_ 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 integrity checks that are active during testing but
/// don't affect performance of shipping code.
/// Use this function for internal sanity checks that are active during testing but
/// do not impact performance of shipping code.
public struct AssertNoFailure<Upstream: Publisher>: Publisher {
public typealias Output = Upstream.Output
@@ -331,7 +331,9 @@ extension Publishers.Buffer {
private func lockedPop(_ demand: Subscribers.Demand) -> [Input] {
assert(demand > 0)
guard let max = demand.max else {
return values.take()
let poppedValues = self.values
self.values = []
return poppedValues
}
let poppedValues = Array(values.prefix(max))
@@ -128,7 +128,8 @@ extension Publishers.CollectByCount {
lock.unlock()
return .none
}
let output = self.buffer.take()
let output = self.buffer
self.buffer = []
lock.unlock()
return downstream.receive(output) * count
}
@@ -142,7 +143,8 @@ extension Publishers.CollectByCount {
if buffer.isEmpty {
lock.unlock()
} else {
let buffer = self.buffer.take()
let buffer = self.buffer
self.buffer = []
lock.unlock()
_ = downstream.receive(buffer)
}
@@ -166,9 +168,10 @@ extension Publishers.CollectByCount {
func cancel() {
lock.lock()
if let subscription = self.subscription.take() {
if let subscription = self.subscription {
buffer = []
finished = true
self.subscription = nil
lock.unlock()
subscription.cancel()
} else {
@@ -0,0 +1,268 @@
${template_header}
//
// Publishers.CombineLatest.swift.gyb
//
//
// Created by Sergej Jaskiewicz on 10.12.2019.
//
%{
from gyb_opencombine_support import (
suffix_variadic,
list_with_suffix_variadic,
indent
)
import string
instantiations = [(2, 'two', 'A second'),
(3, 'three', 'A third'),
(4, 'four', 'A fourth')]
def make_publisher_name(arity):
return suffix_variadic('CombineLatest', arity, arity - 1)
def make_upstream_types(arity, start=0):
return [str(c) for c in string.ascii_uppercase[start:(start + arity)]]
def make_upstream_generic_constraints(upstream_types, first_is_self=False):
format_string = '{0}Failure == {1}.Failure'
def format(i):
return format_string.format(upstream_types[i] + '.',
upstream_types[i + 1])
result = [format(i) for i in range(len(upstream_types) - 1)]
if first_is_self:
result.insert(0, format_string.format('', upstream_types[0]))
return result
def declare_combine_latest_method(arity, transform):
arg_count = arity - 1
declaration_format = """\
public func combineLatest<{}>(
{}
) -> {}
where {}\
"""
upstream_types = make_upstream_types(arg_count, 15)
method_generic_params = \
[upstream_type + ': Publisher' for upstream_type in upstream_types]
if transform:
method_generic_params.append('Result')
cs_method_generic_params = ', '.join(method_generic_params)
method_args = ['_ other: P'] if arg_count == 1 \
else ['_ publisher{}: {}'.format(i + 1, upstream_types[i]) \
for i in range(arg_count)]
if transform:
output_types = ['Output'] + ['{}.Output'.format(upstream_type) \
for upstream_type in upstream_types]
cs_output_types = ', '.join(output_types)
method_args \
.append('_ transform: @escaping ({}) -> Result'.format(cs_output_types))
cs_method_args = ',\n '.join(method_args)
publisher_generic_params = ['Self'] + upstream_types
cs_publisher_generic_params = ', '.join(publisher_generic_params)
publisher_name = 'Publishers.{}<{}>'.format(make_publisher_name(arity),
cs_publisher_generic_params)
if transform:
publisher_name = 'Publishers.Map<{}, Result>'.format(publisher_name)
generic_constraints = make_upstream_generic_constraints(upstream_types,
first_is_self=True)
cs_generic_constraints = ',\n '.join(generic_constraints)
declaration = declaration_format.format(cs_method_generic_params,
cs_method_args,
publisher_name,
cs_generic_constraints)
return indent(declaration, 4)
}%
// swiftlint:disable generic_type_name
// swiftlint:disable large_tuple
// MARK: - CombineLatest methods on Publisher
extension Publisher {
% for arity, _, _ in instantiations:
%
% argument_names = ['other'] \
% if arity == 2 else ['publisher{}'.format(i) for i in range(1, arity)]
% doc_cardinal = 'an additional publisher' if arity == 2 \
% else '{} additional publishers'.format(instantiations[arity - 3][1])
/// Subscribes to ${doc_cardinal} and publishes a tuple upon
/// receiving output from either publisher.
///
/// The combined publisher passes through any requests to *all* upstream publishers.
/// However, it still obeys the demand-fulfilling rule of only sending the request
/// amount downstream. If the demand isnt `.unlimited`, it drops values from upstream
/// publishers. It implements this by using a buffer size of 1 for each upstream, and
/// holds the most recent value in each buffer.
/// All upstream publishers need to finish for this publisher to finsh. If an upstream
/// publisher never publishes a value, this publisher never finishes.
/// If any of the combined publishers terminates with a failure, this publisher also
/// fails.
///
/// - Parameters:
% for i in range(arity - 1):
% param_doc = 'Another' if arity == 2 else instantiations[i][2]
/// - ${argument_names[i]}: ${param_doc} publisher to combine with this one.
% end
/// - Returns: A publisher that receives and combines elements from this and another
/// publisher.
${declare_combine_latest_method(arity, transform=False)}
{
return .init(self, ${', '.join(argument_names)})
}
/// Subscribes to ${doc_cardinal} and invokes a closure
/// upon receiving output from either publisher.
///
/// The combined publisher passes through any requests to *all* upstream publishers.
/// However, it still obeys the demand-fulfilling rule of only sending the request
/// amount downstream. If the demand isnt `.unlimited`, it drops values from upstream
/// publishers. It implements this by using a buffer size of 1 for each upstream, and
/// holds the most recent value in each buffer.
/// All upstream publishers need to finish for this publisher to finsh. If an upstream
/// publisher never publishes a value, this publisher never finishes.
/// If any of the combined publishers terminates with a failure, this publisher also
/// fails.
///
/// - Parameters:
% for i in range(arity - 1):
% param_doc = 'Another' if arity == 2 else instantiations[i][2]
/// - ${argument_names[i]}: ${param_doc} publisher to combine with this one.
% end
/// - transform: A closure that receives the most recent value from each publisher
/// and returns a new value to publish.
/// - Returns: A publisher that receives and combines elements from this and another
/// publisher.
${declare_combine_latest_method(arity, transform=True)}
{
% publisher_name = make_publisher_name(arity)
return Publishers.${publisher_name}(self, ${', '.join(argument_names)}).map {
transform(${', '.join(['${}'.format(i) for i in range(arity)])})
}
}
% end
}
// MARK: - CombineLatest publishers
extension Publishers {
% for arity, cardinal, _ in instantiations:
% publisher_name = make_publisher_name(arity)
% upstream_types = make_upstream_types(arity)
%
% upstream_generic_params = \
% [upstream_type + ': Publisher' for upstream_type in upstream_types]
%
% cs_upstream_generic_params = ', '.join(upstream_generic_params)
%
% output_types = [upstream_type + '.Output' for upstream_type in upstream_types]
%
% cs_output_types = ', '.join(output_types)
%
% upstream_generic_constraints = \
% make_upstream_generic_constraints(upstream_types)
%
% cs_upstream_generic_constraints = \
% ',\n '.join(upstream_generic_constraints)
%
% init_args = ['_ {}: {}'.format(upstream_type.lower(), upstream_type) \
% for upstream_type in upstream_types]
% cs_init_args = ',\n '.join(init_args)
%
% self_fields = [upstream_type.lower() for upstream_type in upstream_types]
/// A publisher that receives and combines the latest elements from ${cardinal}
/// publishers.
public struct ${publisher_name}<${cs_upstream_generic_params}>
: Publisher
where ${cs_upstream_generic_constraints}
{
public typealias Output = (${cs_output_types})
public typealias Failure = ${upstream_types[0]}.Failure
% for upstream_type in upstream_types:
public let ${upstream_type.lower()}: ${upstream_type}
% end
public init(
${cs_init_args}
) {
% for self_field in self_fields:
self.${self_field} = ${self_field}
% end
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Failure == Failure,
Downstream.Input == Output
{
% cs_indented_output_types = (',\n' + (50 * ' ')).join(output_types)
typealias Inner = CombineLatest${arity}Inner<${cs_indented_output_types},
Failure,
Downstream>
let inner = Inner(downstream: subscriber, upstreamCount: ${arity})
% for i in range(arity):
${self_fields[i]}.subscribe(Inner.Side(index: ${i}, combiner: inner))
% end
subscriber.receive(subscription: inner)
}
}
% end
}
// MARK: - Equatable conformances
% for arity, _, _ in instantiations:
%
% publisher_name = make_publisher_name(arity)
%
% upstream_types = make_upstream_types(arity)
%
% constraints = [upstream_type + ': Equatable' for upstream_type in upstream_types]
% cs_constraints = ',\n'.join(constraints)
% cs_constraints = indent(cs_constraints, 8)
%
extension Publishers.${publisher_name}: Equatable
where
${cs_constraints} {}
% end
// MARK: - Inners
% for arity, _, _ in instantiations:
%
% publisher_name = make_publisher_name(arity)
%
% upstream_types = make_upstream_types(arity)
%
% input_types = ['Input{}'.format(i) for i in range(arity)]
%
% converters = ['values[{}] as! {}'.format(i, input_types[i]) for i in range(arity)]
% output_type = '({})'.format(', '.join(input_types))
private final class CombineLatest${arity}Inner<${(',\n' + (40 * ' ')).join(input_types)},
Failure,
Downstream: Subscriber>
: AbstractCombineLatest<${output_type}, Failure, Downstream>
where Downstream.Input == ${output_type},
Downstream.Failure == Failure
{
override func convert(values: [Any?]) -> (${', '.join(input_types)}) {
return (${',\n '.join(converters)})
}
}
% end
@@ -233,7 +233,7 @@ extension Publishers.Concatenate {
private var suffixState = SubscriptionStatus.awaitingSubscription
private var suffix: Suffix?
private let suffix: Suffix
private var pending = Subscribers.Demand.none
@@ -266,18 +266,8 @@ extension Publishers.Concatenate {
prefixState.subscription ?? suffixState.subscription
prefixState = .terminal
suffixState = .terminal
// 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()
}
lock.unlock()
upstreamSubscription?.cancel()
}
var description: String { return "Concatenate" }
@@ -330,7 +320,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,7 +212,8 @@ extension Publishers.Debounce {
let generation = currentGeneration
currentValue = input
let due = scheduler.now.advanced(by: dueTime)
let previousCancellers = self.currentCancellers.take()
let previousCancellers = self.currentCancellers
currentCancellers.removeAll()
currentCancellers[generation] = .pending
lock.unlock()
let newCanceller = scheduler.schedule(after: due,
@@ -237,7 +238,8 @@ extension Publishers.Debounce {
return
}
state = .terminal
let previousCancellers = currentCancellers.take()
let previousCancellers = currentCancellers
currentCancellers.removeAll()
lock.unlock()
for canceller in previousCancellers.values {
canceller.cancel()
@@ -266,7 +268,8 @@ extension Publishers.Debounce {
return
}
state = .terminal
let previousCancellers = currentCancellers.take()
let previousCancellers = currentCancellers
currentCancellers.removeAll()
lock.unlock()
for canceller in previousCancellers.values {
canceller.cancel()
@@ -303,10 +306,11 @@ extension Publishers.Debounce {
return
}
guard let canceller = currentCancellers[generation].take() else {
guard let canceller = currentCancellers[generation] else {
lock.unlock()
return
}
currentCancellers[generation] = nil
let hasAnyDemand = downstreamDemand != .none
if hasAnyDemand {
@@ -139,7 +139,8 @@ extension Publishers.Drop {
func cancel() {
lock.lock()
let subscription = self.subscription.take()
let subscription = self.subscription
self.subscription = nil
lock.unlock()
subscription?.cancel()
}
@@ -204,7 +204,8 @@ extension Publishers.DropUntilOutput {
}
otherFinished = true
if let upstreamSubscription = self.upstreamSubscription.take() {
if let upstreamSubscription = self.upstreamSubscription {
self.upstreamSubscription = nil
lock.unlock()
upstreamSubscription.cancel()
} else {
@@ -228,8 +229,10 @@ extension Publishers.DropUntilOutput {
func cancel() {
lock.lock()
let upstreamSubscription = self.upstreamSubscription.take()
let otherSubscription = self.otherSubscription.take()
let upstreamSubscription = self.upstreamSubscription
let otherSubscription = self.otherSubscription
self.upstreamSubscription = nil
self.otherSubscription = nil
cancelled = true
lock.unlock()
@@ -221,7 +221,8 @@ extension Publishers.${instantiation} {
} catch {
lock.lock()
finished = true
let subscription = self.subscription.take()
let subscription = self.subscription
self.subscription = nil
lock.unlock()
subscription?.cancel()
downstream.receive(completion: .failure(error))
@@ -250,10 +251,11 @@ extension Publishers.${instantiation} {
func cancel() {
lock.lock()
guard !finished, let subscription = self.subscription.take() else {
guard !finished, let subscription = self.subscription 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 `Publisher` based on the received value.
/// The closure creates the new `Publishe`` based on the received value.
/// The new `Publisher` can emit more than one event, and successful completion of
/// the new `Publisher` does not complete the overall stream.
/// Failure of the new `Publisher` will fail the overall stream.
@@ -304,7 +304,8 @@ extension Publishers.FlatMap {
}
if demand == .unlimited {
downstreamDemand = .unlimited
let buffer = self.buffer.take()
let buffer = self.buffer
self.buffer = []
let subscriptions = self.subscriptions
lock.unlock()
downstreamLock.lock()
@@ -360,8 +361,10 @@ extension Publishers.FlatMap {
return
}
cancelledOrCompleted = true
let subscriptions = self.subscriptions.take()
let outerSubscription = self.outerSubscription.take()
let subscriptions = self.subscriptions
self.subscriptions = [:]
let outerSubscription = self.outerSubscription
self.outerSubscription = nil
lock.unlock()
for (_, subscription) in subscriptions {
subscription.cancel()
@@ -447,7 +450,8 @@ extension Publishers.FlatMap {
return
}
cancelledOrCompleted = true
let subscriptions = self.subscriptions.take()
let subscriptions = self.subscriptions
self.subscriptions = [:]
lock.unlock()
for (i, subscription) in subscriptions where i != index {
subscription.cancel()
@@ -139,12 +139,12 @@ extension Publishers.HandleEvents {
private var status = SubscriptionStatus.awaitingSubscription
private let lock = UnfairLock.allocate()
fileprivate var receiveSubscription: ((Subscription) -> Void)?
fileprivate var receiveOutput: ((Upstream.Output) -> Void)?
fileprivate var receiveCompletion:
public var receiveSubscription: ((Subscription) -> Void)?
public var receiveOutput: ((Upstream.Output) -> Void)?
public var receiveCompletion:
((Subscribers.Completion<Upstream.Failure>) -> Void)?
fileprivate var receiveCancel: (() -> Void)?
fileprivate var receiveRequest: ((Subscribers.Demand) -> Void)?
public var receiveCancel: (() -> Void)?
public var receiveRequest: ((Subscribers.Demand) -> Void)?
private let downstream: Downstream
init(_ events: Publishers.HandleEvents<Upstream>, downstream: Downstream) {
@@ -6,8 +6,6 @@ ${template_header}
// Created by Sergej Jaskiewicz on 03/10/2019.
//
// swiftlint:disable large_tuple
%{
from gyb_opencombine_support import (
suffix_variadic,
@@ -75,7 +75,9 @@ extension Publishers {
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Upstream.Output == Downstream.Input, Downstream.Failure == Failure
{
upstream.subscribe(Inner(downstream: subscriber, output: output))
let inner = Inner(downstream: subscriber, output: output)
upstream.subscribe(inner)
subscriber.receive(subscription: inner)
}
}
}
@@ -121,8 +123,11 @@ extension Publishers.ReplaceError {
return
}
status = .subscribed(subscription)
let pendingDemand = self.pendingDemand
lock.unlock()
downstream.receive(subscription: self)
if pendingDemand != .none {
subscription.request(pendingDemand)
}
}
func receive(_ input: Input) -> Subscribers.Demand {
@@ -145,7 +150,7 @@ extension Publishers.ReplaceError {
func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
guard case .subscribed = status, !terminated else {
guard case .subscribed = status else {
lock.unlock()
return
}
@@ -230,7 +230,8 @@ extension Publishers.SwitchToLatest {
return .none
}
if let currentInnerSubscription = self.currentInnerSubscription.take() {
if let currentInnerSubscription = self.currentInnerSubscription {
self.currentInnerSubscription = nil
lock.unlock()
currentInnerSubscription.cancel()
lock.lock()
@@ -271,7 +272,8 @@ extension Publishers.SwitchToLatest {
lock.unlock()
}
case .failure:
let currentInnerSubscription = self.currentInnerSubscription.take()
let currentInnerSubscription = self.currentInnerSubscription
self.currentInnerSubscription = nil
sentCompletion = true
lock.unlock()
currentInnerSubscription?.cancel()
@@ -296,8 +298,10 @@ extension Publishers.SwitchToLatest {
func cancel() {
lock.lock()
cancelled = true
let currentInnerSubscription = self.currentInnerSubscription.take()
let outerSubscription = self.outerSubscription.take()
let currentInnerSubscription = self.currentInnerSubscription
self.currentInnerSubscription = nil
let outerSubscription = self.outerSubscription
self.outerSubscription = nil
lock.unlock()
currentInnerSubscription?.cancel()
@@ -382,7 +386,8 @@ extension Publishers.SwitchToLatest {
return
}
cancelled = true
let outerSubscription = self.outerSubscription.take()
let outerSubscription = self.outerSubscription
self.outerSubscription = nil
sentCompletion = true
lock.unlock()
outerSubscription?.cancel()
@@ -292,8 +292,11 @@ extension Publishers.Throttle {
lastEmissionTime = scheduler.now
}
let pendingInput = self.pendingInput.take()
let pendingCompletion = self.pendingCompletion.take()
let pendingInput = self.pendingInput
let pendingCompletion = self.pendingCompletion
self.pendingInput = nil
self.pendingCompletion = nil
if pendingCompletion != nil {
state = .terminal
@@ -1,720 +0,0 @@
//
// 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,7 +136,8 @@ extension Result.OCombine {
func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
guard let downstream = self.downstream.take() else { return }
guard let downstream = self.downstream else { return }
self.downstream = nil
_ = downstream.receive(output)
downstream.receive(completion: .finished)
}
-220
View File
@@ -1,220 +0,0 @@
${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,6 +29,51 @@ 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
@@ -1,17 +0,0 @@
//
// 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
@@ -0,0 +1,45 @@
//
// 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
@@ -1,20 +0,0 @@
//
// 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
@@ -0,0 +1,76 @@
//
// 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,17 +112,8 @@ extension Subscribers {
lock.assertOwner()
#endif
status = .terminal
// 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()
}
object = nil
lock.unlock()
}
}
}
@@ -5,10 +5,6 @@
// 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
@@ -27,10 +23,6 @@ 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"
@@ -78,13 +70,4 @@ extension Subscribers.Completion {
return .failure(error)
}
}
internal var failure: Failure? {
switch self {
case .finished:
return nil
case .failure(let failure):
return failure
}
}
}
@@ -5,11 +5,7 @@
// Created by Sergej Jaskiewicz on 10.06.2019.
//
// swiftlint:disable attributes
#if canImport(_Concurrency) && compiler(>=5.5)
import _Concurrency
#endif
// swiftlint:disable shorthand_operator - because of false positives here
extension Subscribers {
@@ -470,7 +466,3 @@ extension Subscribers {
}
}
}
#if canImport(_Concurrency) && compiler(>=5.5) || compiler(>=5.5.1)
extension Subscribers.Demand: Sendable {}
#endif
@@ -73,22 +73,8 @@ extension Subscribers {
public func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
status = .terminal
let receiveCompletion = self.receiveCompletion
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()
}
terminateAndConsumeLock()
receiveCompletion(completion)
}
@@ -98,22 +84,19 @@ extension Subscribers {
lock.unlock()
return
}
status = .terminal
// 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()
}
terminateAndConsumeLock()
subscription.cancel()
}
private func terminateAndConsumeLock() {
#if DEBUG
lock.assertOwner()
#endif
status = .terminal
receiveValue = { _ in }
receiveCompletion = { _ in }
lock.unlock()
}
}
}
-1
View File
@@ -5,7 +5,6 @@
// Created by Sergej Jaskiewicz on 27.09.2020.
//
// swiftlint:disable:next type_name
public protocol _Introspection: AnyObject {
func willReceive<Upstream: Publisher, Downstream: Subscriber>(
@@ -50,7 +50,13 @@ 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 {
return Stride(dispatchTime.polyfillDistance(to: other.dispatchTime))
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))
)
}
/// Returns a dispatch queue scheduler time calculated by advancing
@@ -98,17 +104,8 @@ 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 {
get {
return Int(_nanoseconds)
}
set {
_nanoseconds = Int64(newValue)
}
}
public var magnitude: Int
/// A `DispatchTimeInterval` created with the value of this type
/// in nanoseconds.
@@ -116,8 +113,8 @@ extension DispatchQueue {
return .nanoseconds(magnitude)
}
private init(magnitude: Int64) {
_nanoseconds = magnitude
private init(magnitude: Int) {
self.magnitude = magnitude
}
/// Creates a dispatch queue time interval from the given
@@ -207,7 +204,7 @@ extension DispatchQueue {
}
public static func < (lhs: Stride, rhs: Stride) -> Bool {
return lhs._nanoseconds < rhs._nanoseconds
return lhs.magnitude < rhs.magnitude
}
public static func * (lhs: Stride, rhs: Stride) -> Stride {
@@ -215,51 +212,43 @@ extension DispatchQueue {
}
public static func + (lhs: Stride, rhs: Stride) -> Stride {
return Stride(magnitude: lhs._nanoseconds + rhs._nanoseconds)
return Stride(magnitude: lhs.magnitude + rhs.magnitude)
}
public static func - (lhs: Stride, rhs: Stride) -> Stride {
return Stride(magnitude: lhs._nanoseconds - rhs._nanoseconds)
return Stride(magnitude: lhs.magnitude - rhs.magnitude)
}
public static func -= (lhs: inout Stride, rhs: Stride) {
lhs._nanoseconds -= rhs._nanoseconds
lhs.magnitude -= rhs.magnitude
}
public static func *= (lhs: inout Stride, rhs: Stride) {
lhs._nanoseconds = 0
lhs.magnitude = 0
}
public static func += (lhs: inout Stride, rhs: Stride) {
lhs._nanoseconds += rhs._nanoseconds
lhs.magnitude += rhs.magnitude
}
public static func seconds(_ value: Double) -> Stride {
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))
return Stride(magnitude: Int(value * 1_000_000_000))
}
public static func seconds(_ value: Int) -> Stride {
return Stride(magnitude: clampedIntProduct(Int64(value),
1_000_000_000))
return Stride(magnitude: clampedIntProduct(value, 1_000_000_000))
}
public static func milliseconds(_ value: Int) -> Stride {
return Stride(magnitude: clampedIntProduct(Int64(value), 1_000_000))
return Stride(magnitude: clampedIntProduct(value, 1_000_000))
}
public static func microseconds(_ value: Int) -> Stride {
return Stride(magnitude: clampedIntProduct(Int64(value), 1_000))
return Stride(magnitude: clampedIntProduct(value, 1_000))
}
public static func nanoseconds(_ value: Int) -> Stride {
return Stride(magnitude: Int64(value))
return Stride(magnitude: value)
}
}
}
@@ -398,10 +387,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 [Int64.min, Int64.max].
// Returns m1 * m2, clamped to the range [Int.min, Int.max].
// Because of the way this function is used, we can always assume
// that m2 > 0.
private func clampedIntProduct(_ lhs: Int64, _ rhs: Int64) -> Int64 {
private func clampedIntProduct(_ lhs: Int, _ rhs: Int) -> Int {
assert(rhs > 0, "multiplier must be positive")
let (result, overflow) = lhs.multipliedReportingOverflow(by: rhs)
if overflow {
@@ -409,31 +398,3 @@ private func clampedIntProduct(_ lhs: Int64, _ rhs: Int64) -> Int64 {
}
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,10 +5,7 @@
// Created by Sergej Jaskiewicz on 28.10.2020.
//
#if canImport(CoreFoundation)
import CoreFoundation
#endif
import Foundation
/// Use CoreFoundation on Darwin, since some pure
@@ -96,7 +93,6 @@ internal struct Timer {
#endif
}
#if canImport(CoreFoundation)
fileprivate func getCFRunLoopTimer() -> CFRunLoopTimer? {
#if canImport(Darwin)
return underlyingTimer
@@ -117,7 +113,6 @@ internal struct Timer {
fatalError("unreachable")
#endif
}
#endif // canImport(CoreFoundation)
}
extension RunLoop {
@@ -143,7 +138,6 @@ extension RunLoop {
}
}
#if canImport(CoreFoundation)
extension RunLoop.Mode {
fileprivate func asCFRunLoopMode() -> CFRunLoopMode {
#if canImport(Darwin)
@@ -165,4 +159,3 @@ extension RunLoop.Mode {
#endif
}
}
#endif // canImport(CoreFoundation)
@@ -1,14 +0,0 @@
//
// 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.take(),
let observation = self.observation.take()
else {
guard let center = self.center, let observation = self.observation else {
lock.unlock()
return
}
self.center = nil
self.object = nil
self.observation = nil
lock.unlock()
center.removeObserver(observation)
}
@@ -234,16 +234,19 @@ extension OperationQueue {
}
override func main() {
guard let action = self.action.take() else { return }
guard let action = self.action else { return }
self.action = nil
action()
guard let queue = self.queue.take(),
let context = self.context.take()
guard let queue = self.queue,
let context = self.context
else {
self.queue = nil
self.context = nil
return
}
self.queue = nil
self.context = nil
context.lock.lock()
if context.operation == nil {
@@ -5,6 +5,7 @@
// Created by Sergej Jaskiewicz on 13.12.2019.
//
import CoreFoundation
import Foundation
import OpenCombine
@@ -5,6 +5,7 @@
// Created by Sergej Jaskiewicz on 23.06.2020.
//
import CoreFoundation
import Foundation
import OpenCombine
@@ -202,10 +203,11 @@ extension Foundation.Timer {
func cancel() {
lock.lock()
if downstream.take() == nil {
if downstream == nil {
lock.unlock()
return
}
downstream = nil
lock.unlock()
parent?.disconnect(combineIdentifier)
}
@@ -1,73 +0,0 @@
//
// 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: { _ in
receiveCompletion: { completion in
cvs.send(completion: .failure("must not recurse"))
}
)
@@ -5,7 +5,7 @@
// Created by Sergej Jaskiewicz on 26.08.2019.
//
#if !WASI // TEST_DISCOVERY_CONDITION
#if !WASI
import Dispatch
import XCTest
@@ -31,34 +31,23 @@ 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(.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: 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: 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(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(distantFuture.distance(to: distantFuture), .nanoseconds(0))
XCTAssertEqual(notSoDistantFuture.distance(to: notSoDistantFuture),
.nanoseconds(0))
XCTAssertEqual(int64max.distance(to: int64max), .nanoseconds(0))
}
func testSchedulerTimeTypeAdvanced() {
@@ -270,22 +259,24 @@ final class DispatchQueueSchedulerTests: XCTestCase {
XCTAssertEqual(Stride(exactly: 2 as UInt64)?.magnitude, 2_000_000_000)
}
func testStrideFromTooMuchSeconds() {
func testStrideFromTooMuchSecondsCrashes() {
assertCrashes {
#if arch(x86_64) || arch(arm64) || arch(s390x) || arch(powerpc64) || arch(powerpc64le)
// 64-bit platforms
XCTAssertEqual(
Stride.seconds(Double(Int.max) / 1_000_000_000).magnitude,
.max
)
// 64-bit platforms
XCTAssertGreaterThan(
Stride.seconds(Double(Int.max) / 1_000_000_000).magnitude,
.max
)
#elseif arch(i386) || arch(arm)
// 32-bit platforms
XCTAssertEqual(
Stride.seconds(Double(Int.max) / 1_000_000_000).magnitude,
.max
)
// 32-bit platforms
XCTAssertGreaterThan(
Stride.seconds(Double(Int.max) / 1_000_000_000 + 1).magnitude,
.max
)
#else
#error("This architecture isn't known. Add it to the 32-bit or 64-bit line.")
#endif
}
}
func testStrideComparable() {
@@ -452,7 +443,7 @@ final class DispatchQueueSchedulerTests: XCTestCase {
.encode(KeyedWrapper(value: stride))
let encodedString = String(decoding: encodedData, as: UTF8.self)
XCTAssertEqual(encodedString, #"{"value":{"_nanoseconds":419872}}"#)
XCTAssertEqual(encodedString, #"{"value":{"magnitude":419872}}"#)
let decodedStride = try decoder
.decode(KeyedWrapper<Stride>.self, from: encodedData)
@@ -495,11 +486,11 @@ final class DispatchQueueSchedulerTests: XCTestCase {
let main = expectation(description: "scheduled on main queue")
main.assertForOverFulfill = true
let didExecuteMainAction = Atomic(false)
var didExecuteMainAction = false
let didExecuteBackgroundAction = Atomic(false)
mainScheduler.schedule {
didExecuteMainAction.set(true)
didExecuteMainAction = true
main.fulfill()
}
@@ -510,14 +501,12 @@ final class DispatchQueueSchedulerTests: XCTestCase {
didExecuteBackgroundAction.set(true)
}
XCTAssertFalse(didExecuteMainAction.value,
"action should be executed asynchronously")
XCTAssertFalse(didExecuteMainAction, "action should be executed asynchronously")
// Wait for the background scheduler to execute the work.
XCTAssertEqual(group.wait(timeout: .now() + 5.0), .success)
XCTAssertFalse(didExecuteMainAction.value,
"action should be executed asynchronously")
XCTAssertFalse(didExecuteMainAction, "action should be executed asynchronously")
XCTAssertTrue(didExecuteBackgroundAction.value)
wait(for: [main], timeout: 0.1)
@@ -5,7 +5,7 @@
// Created by Sergej Jaskiewicz on 10.12.2019.
//
#if !WASI // TEST_DISCOVERY_CONDITION
#if !WASI
import Foundation
import XCTest
@@ -5,7 +5,7 @@
// Created by Sergej Jaskiewicz on 10.12.2019.
//
#if !WASI // TEST_DISCOVERY_CONDITION
#if !WASI
import Foundation
import XCTest
@@ -5,7 +5,7 @@
// Created by Sergej Jaskiewicz on 10.12.2019.
//
#if !WASI // TEST_DISCOVERY_CONDITION
#if !WASI
import Foundation
import XCTest
@@ -331,7 +331,6 @@ extension OperationQueueScheduler.SchedulerTimeType.Stride
@available(macOS 10.15, iOS 13.0, *)
extension OperationQueueScheduler.SchedulerTimeType: DateBackedSchedulerTimeType {}
@available(macOS 10.15, iOS 13.0, *)
extension OperationQueueScheduler: RunLoopLikeScheduler {}
private final class TestOperationQueue: OperationQueue {
@@ -5,7 +5,7 @@
// Created by Sergej Jaskiewicz on 14.12.2019.
//
#if !WASI // TEST_DISCOVERY_CONDITION
#if !WASI
import Foundation
import XCTest
@@ -599,14 +599,12 @@ private func makeScheduler(_ runLoop: RunLoop) -> RunLoopScheduler {
#endif
@available(macOS 10.15, iOS 13.0, *)
protocol DateBackedSchedulerTimeType: Strideable, Codable, Hashable {
init(_ date: Date)
var date: Date { get }
}
@available(macOS 10.15, iOS 13.0, *)
protocol TimeIntervalBackedSchedulerStride: SchedulerTimeIntervalConvertible,
Comparable,
SignedNumeric,
@@ -619,7 +617,6 @@ protocol TimeIntervalBackedSchedulerStride: SchedulerTimeIntervalConvertible,
var timeInterval: TimeInterval { get }
}
@available(macOS 10.15, iOS 13.0, *)
protocol RunLoopLikeScheduler: Scheduler
where SchedulerTimeType: DateBackedSchedulerTimeType,
SchedulerTimeType.Stride: TimeIntervalBackedSchedulerStride {
@@ -631,7 +628,6 @@ extension RunLoopScheduler.SchedulerTimeType.Stride: TimeIntervalBackedScheduler
@available(macOS 10.15, iOS 13.0, *)
extension RunLoopScheduler.SchedulerTimeType: DateBackedSchedulerTimeType {}
@available(macOS 10.15, iOS 13.0, *)
extension RunLoopScheduler: RunLoopLikeScheduler {}
#endif // !WASI
@@ -5,7 +5,7 @@
// Created by Sergej Jaskiewicz on 23.06.2020.
//
#if !WASI // TEST_DISCOVERY_CONDITION
#if !WASI
import Foundation
import XCTest
@@ -7,6 +7,8 @@
// swiftlint:disable multiline_arguments
#if !WASI
import Foundation
import XCTest
@@ -18,7 +20,7 @@ import FoundationNetworking
// swift-corelibs-foundation that were making these tests impossible to build.
//
// Those were fixed in https://github.com/apple/swift-corelibs-foundation/pull/2587.
#if canImport(Darwin) || swift(>=5.3) && !WASI // TEST_DISCOVERY_CONDITION
#if canImport(Darwin) || swift(>=5.3) // TEST_DISCOVERY_CONDITION
#if OPENCOMBINE_COMPATIBILITY_TEST
import Combine
@@ -744,4 +746,6 @@ private func makePublisher(
}
#endif // OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
#endif
#endif // canImport(Darwin)
#endif // !WASI
@@ -1,55 +0,0 @@
//
// AutomaticallyFinish.swift
//
//
// Created by Sergej Jaskiewicz on 08.07.2021.
//
#if OPENCOMBINE_COMPATIBILITY_TEST
import Combine
#else
import OpenCombine
#endif
@available(macOS 10.15, iOS 13.0, *)
final class AutomaticallyFinish<Output, Failure: Error> {
let subscription: CustomSubscription
let publisher: CustomPublisherBase<Output, Failure>
init() {
subscription = .init()
publisher = .init(subscription: subscription)
}
deinit {
publisher.send(completion: .finished)
}
func notify(_ value: Output) {
_ = publisher.send(value)
}
func listen(receiveCompletion: @escaping (Subscribers.Completion<Failure>) -> Void,
receiveValue: @escaping (Output) -> Void) -> AnyCancellable {
return publisher.sink(receiveCompletion: receiveCompletion,
receiveValue: receiveValue)
}
}
@available(macOS 10.15, iOS 13.0, *)
extension AutomaticallyFinish: Publisher {
func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Failure == Failure, Downstream.Input == Output
{
publisher.subscribe(subscriber)
}
}
@available(macOS 10.15, iOS 13.0, *)
extension AutomaticallyFinish where Failure == Never {
func assign<Root>(to keyPath: ReferenceWritableKeyPath<Root, Output>,
on object: Root) -> AnyCancellable {
return publisher.assign(to: keyPath, on: object)
}
}
@@ -33,28 +33,8 @@ final class CustomSubscription: Subscription, CustomStringConvertible {
}
}
private struct State {
var cancelled: Bool
var history: [Event]
}
private let state = Atomic(State(cancelled: false, history: []))
/// The history of requests and cancellations of this subscription.
var history: [Event] {
return state.value.history
}
var cancelled: Bool {
get {
return state.value.cancelled
}
set {
state.do { state in
state.cancelled = newValue
}
}
}
private(set) var history: [Event] = []
var onRequest: ((Subscribers.Demand) -> Void)?
var onCancel: (() -> Void)?
@@ -83,18 +63,16 @@ final class CustomSubscription: Subscription, CustomStringConvertible {
}.last
}
var cancelled = false
func request(_ demand: Subscribers.Demand) {
state.do { state in
state.history.append(.requested(demand))
}
history.append(.requested(demand))
onRequest?(demand)
}
func cancel() {
state.do { state in
state.history.append(.cancelled)
state.cancelled = true
}
history.append(.cancelled)
cancelled = true
onCancel?()
}
@@ -11,76 +11,38 @@
import Darwin
#elseif canImport(Glibc)
import Glibc
#elseif os(Windows)
import WinSDK
#else
#error("How to do threads on this platform?")
#endif
// We could use Foundation's Thread, but it doesn't work on Linux for some
// reason.
#if canImport(Darwin)
private typealias ThreadPtr = UnsafeMutablePointer<pthread_t?>
#else
private typealias ThreadPtr = UnsafeMutablePointer<pthread_t>
#endif
func executeOnBackgroundThread<ResultType>(
_ body: () -> ResultType
) -> ResultType {
return withoutActuallyEscaping(body) { body in
typealias ThreadRoutine = () -> UnsafeMutableRawPointer
#if canImport(Darwin)
typealias ThreadHandle = UnsafeMutablePointer<pthread_t?>
#elseif canImport(Glibc)
typealias ThreadHandle = UnsafeMutablePointer<pthread_t>
#elseif os(Windows)
typealias ThreadHandle = HANDLE?
#endif
let typeErasedBody: ThreadRoutine = {
// We need this because @convention(c) closures can't capture generic params.
var typeErasedBody: () -> UnsafeMutableRawPointer = {
let resultPtr = UnsafeMutablePointer<ResultType>.allocate(capacity: 1)
resultPtr.initialize(to: body())
return UnsafeMutableRawPointer(resultPtr)
}
var _backgroundThread: ThreadHandle
return withUnsafeMutablePointer(to: &typeErasedBody) { typeErasedBody in
let _backgroundThread = ThreadPtr.allocate(capacity: 1)
#if os(Windows)
typealias ResultPtr = UnsafeMutablePointer<UnsafeMutableRawPointer>
typealias Context = (ThreadRoutine, ResultPtr)
var resultPtr: ResultPtr = .allocate(capacity: 1)
defer { resultPtr.deallocate() }
_backgroundThread = nil
var context: Context = (typeErasedBody, resultPtr)
#else
_backgroundThread = .allocate(capacity: 1)
defer { _backgroundThread.deallocate() }
var context = typeErasedBody
#endif
defer { _backgroundThread.deallocate() }
return withUnsafeMutablePointer(to: &context) { context in
#if os(Windows)
_backgroundThread = CreateThread(
nil, // default security attributes
0, // use default stack size
{ context in
let (typeErasedBody, resultPtr) = context!
.assumingMemoryBound(to: Context.self)
.pointee
var status: Int32 = 0
resultPtr.initialize(to: typeErasedBody())
return 0
},
context,
0, // use default creation flags
nil // don't return thread identifier
)
precondition(_backgroundThread != nil, "Could not create a thread")
WaitForSingleObject(_backgroundThread!, INFINITE)
defer { resultPtr.pointee.deallocate() }
return resultPtr.pointee.assumingMemoryBound(to: ResultType.self).move()
#else
var status = pthread_create(
// We could use Foundation's Thread, but it doesn't work on Linux for some
// reason.
status = pthread_create(
_backgroundThread,
nil,
{ context in
@@ -90,17 +52,19 @@ func executeOnBackgroundThread<ResultType>(
let context = context!
#endif
return context
.assumingMemoryBound(to: ThreadRoutine.self)
.assumingMemoryBound(to: (() -> UnsafeMutableRawPointer).self)
.pointee()
},
context
typeErasedBody
)
precondition(status == 0, "Could not create a thread")
guard status == 0 else {
preconditionFailure("Could not create a background thread")
}
#if canImport(Darwin)
guard let backgroundThread = _backgroundThread.pointee else {
preconditionFailure("Could not join thread")
preconditionFailure("Could not create a background thread")
}
#else
let backgroundThread = _backgroundThread.pointee
@@ -110,13 +74,12 @@ func executeOnBackgroundThread<ResultType>(
status = pthread_join(backgroundThread, &_resultPtr)
guard status == 0, let resultPtr = _resultPtr else {
preconditionFailure("Could not join thread")
preconditionFailure("Could not join threads")
}
defer { resultPtr.deallocate() }
return resultPtr.assumingMemoryBound(to: ResultType.self).move()
#endif
}
}
}
@@ -46,11 +46,9 @@ final class BreakpointTests: XCTestCase {
XCTAssertEqual(helper.subscription.history, [])
shouldStop = true
XCTAssertEqual(counter, 2)
#if !os(Windows)
assertCrashes {
helper.publisher.send(subscription: CustomSubscription())
}
#endif
}
func testReceiveValue() {
@@ -79,11 +77,9 @@ final class BreakpointTests: XCTestCase {
.subscription("CustomSubscription")])
XCTAssertEqual(helper.subscription.history, [])
XCTAssertEqual(counter, 2)
#if !os(Windows)
assertCrashes {
_ = helper.publisher.send(-1)
}
#endif
}
func testReceiveCompletion() {
@@ -111,11 +107,9 @@ final class BreakpointTests: XCTestCase {
.value(21),
.subscription("CustomSubscription")])
XCTAssertEqual(counter, 2)
#if !os(Windows)
assertCrashes {
helper.publisher.send(completion: .finished)
}
#endif
}
func testBreakpointOnError() throws {
@@ -145,11 +139,9 @@ final class BreakpointTests: XCTestCase {
XCTAssertEqual(helper.sut.receiveCompletion?(.finished), false)
XCTAssertEqual(helper.sut.receiveCompletion?(.failure(.oops)), true)
#if !os(Windows)
assertCrashes {
helper.publisher.send(completion: .failure(.oops))
}
#endif
}
func testCancelAlreadyCancelled() throws {
@@ -377,60 +377,6 @@ final class ConcatenateTests: XCTestCase {
XCTAssertEqual(publisher.prepend(CollectionOfOne(42)).prefix.sequence.first, 42)
}
func testReleasesSuffixOnCancellation() throws {
var suffixIsDestroyed = false
var downstreamSubscription: Subscription?
do {
let prefix =
CustomPublisherBase<Int, Never>(subscription: CustomSubscription())
let suffix =
CustomPublisherBase<Int, Never>(subscription: CustomSubscription())
suffix.onDeinit = {
suffixIsDestroyed = true
}
let tracking = TrackingSubscriberBase<Int, Never>(
receiveSubscription: {
downstreamSubscription = $0
}
)
prefix.append(suffix).subscribe(tracking)
}
XCTAssertFalse(suffixIsDestroyed)
try XCTUnwrap(downstreamSubscription).cancel()
XCTAssertTrue(suffixIsDestroyed)
}
func testReceiveCompletionWhileCancelling() throws {
var downstreamSubscription: Subscription?
do {
let prefix =
CustomPublisherBase<Int, Never>(subscription: CustomSubscription())
let autofinish = AutomaticallyFinish<Int, Never>()
let tracking = TrackingSubscriberBase<Int, Never>(
receiveSubscription: {
downstreamSubscription = $0
}
)
prefix.append(autofinish).subscribe(tracking)
prefix.send(completion: .finished)
}
// autofinish is deallocated here, a completion is sent
try XCTUnwrap(downstreamSubscription).cancel()
}
func testAppendReceiveValueBeforeSubscription() {
testReceiveValueBeforeSubscription(
value: 12,
@@ -13,75 +13,22 @@ import Combine
import OpenCombine
#endif
// swiftlint:disable generic_type_name
/// See https://forums.swift.org/t/casting-from-any-to-optional/21883
private func dynamicCast<T>(_ value: Any, to: T.Type) -> T? {
if let value = value as? T {
return value
} else {
return nil
}
}
// swiftlint:enable generic_type_name
@available(macOS 10.15, iOS 13.0, *)
final class FutureTests: XCTestCase {
private typealias Sut = Future<Int, TestingError>
private func assertParent(of futureSubscription: Subscription, isNil: Bool) {
let parent: Mirror.Child
do {
parent = try XCTUnwrap(
Mirror(reflecting: futureSubscription)
.children
.first { $0.label == "parent" }
)
} catch {
XCTFail("Missing 'parent' property in \(futureSubscription)")
return
}
let parentAsSut: Sut?
do {
parentAsSut = try XCTUnwrap(dynamicCast(parent.value, to: Sut?.self))
} catch {
XCTFail("Unexpected type of the 'parent' property: \(parent.value)")
return
}
if isNil {
XCTAssertNil(parentAsSut)
} else {
XCTAssertNotNil(parentAsSut)
}
}
func testFutureSuccess() throws {
func testFutureSuccess() {
var promise: Sut.Promise?
let future = Sut { promise = $0 }
var downstreamSubscription: Subscription?
let subscriber = TrackingSubscriber(receiveSubscription: { subscription in
downstreamSubscription = subscription
subscription.request(.unlimited)
})
future.subscribe(subscriber)
let unwrappedDownstreamSubscription = try XCTUnwrap(downstreamSubscription)
self.assertParent(of: unwrappedDownstreamSubscription, isNil: false)
subscriber.onValue = { _ in
self.assertParent(of: unwrappedDownstreamSubscription, isNil: false)
}
promise?(.success(42))
self.assertParent(of: unwrappedDownstreamSubscription, isNil: true)
XCTAssertEqual(subscriber.history, [
.subscription("Future"),
.value(42),
@@ -89,15 +36,13 @@ final class FutureTests: XCTestCase {
])
}
func testFutureFailure() throws {
func testFutureFailure() {
var promise: Sut.Promise?
let future = Sut { promise = $0 }
var downstreamSubscription: Subscription?
let subscriber = TrackingSubscriber(
receiveSubscription: { subscription in
downstreamSubscription = subscription
subscription.request(.unlimited)
}, receiveValue: { _ in
XCTFail("no value should be returned")
@@ -106,19 +51,9 @@ final class FutureTests: XCTestCase {
)
future.subscribe(subscriber)
let unwrappedDownstreamSubscription = try XCTUnwrap(downstreamSubscription)
self.assertParent(of: unwrappedDownstreamSubscription, isNil: false)
subscriber.onFailure = { _ in
self.assertParent(of: unwrappedDownstreamSubscription, isNil: false)
}
let error = TestingError(description: "\(#function)")
promise?(.failure(error))
self.assertParent(of: unwrappedDownstreamSubscription, isNil: true)
XCTAssertEqual(subscriber.history, [
.subscription("Future"),
.completion(.failure(error))
@@ -296,7 +231,7 @@ final class FutureTests: XCTestCase {
XCTAssertTrue(hasStarted)
}
func testWaitsForDemandSuccess() throws {
func testWaitsForDemandSuccess() {
var promise: Sut.Promise?
let future = Sut { promise = $0 }
@@ -313,23 +248,13 @@ final class FutureTests: XCTestCase {
.subscription("Future")
])
let unwrappedDownstreamSubscription = try XCTUnwrap(downstreamSubscription)
self.assertParent(of: unwrappedDownstreamSubscription, isNil: false)
subscriber.onValue = { _ in
self.assertParent(of: unwrappedDownstreamSubscription, isNil: false)
}
unwrappedDownstreamSubscription.request(.max(1))
downstreamSubscription?.request(.max(1))
XCTAssertEqual(subscriber.history, [
.subscription("Future"),
.value(42),
.completion(.finished)
])
assertParent(of: unwrappedDownstreamSubscription, isNil: false)
}
func testReleasesEverythingOnTermination() {
@@ -390,17 +315,5 @@ final class FutureTests: XCTestCase {
playgroundDescription: "Future",
sut: Sut { _ in }
)
try testSubscriptionReflection(
description: "Future",
customMirror: expectedChildren(
("parent", "nil"),
("downstream", "nil"),
("hasAnyDemand", "false"),
("subject", "nil")
),
playgroundDescription: "Future",
sut: Sut { promise in promise(.failure(.oops)) }
)
}
}
@@ -68,7 +68,7 @@ final class ReplaceErrorTests: XCTestCase {
.completion(.finished)])
}
func testSendingErrorWithNoDemandThenFinish() throws {
func testSendingErrorWithNoDemandThenFinish() {
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .none,
@@ -77,12 +77,7 @@ final class ReplaceErrorTests: XCTestCase {
helper.publisher.send(completion: .failure(.oops))
helper.publisher.send(completion: .finished)
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceError")])
try XCTUnwrap(helper.downstreamSubscription).request(.max(1))
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceError"),
.value(42),
.completion(.finished)])
}
@@ -169,7 +164,7 @@ final class ReplaceErrorTests: XCTestCase {
replaceError.subscribe(tracking)
XCTAssertEqual(tracking.history, [])
XCTAssertEqual(tracking.history, [.subscription("ReplaceError")])
let subscription = CustomSubscription()
publisher.send(subscription: subscription)
@@ -230,7 +225,7 @@ final class ReplaceErrorTests: XCTestCase {
func testReplaceErrorReceiveValueBeforeSubscription() {
testReceiveValueBeforeSubscription(
value: 0,
expected: .history([], demand: .none),
expected: .history([.subscription("ReplaceError")], demand: .none),
{ $0.replaceError(with: 1) }
)
}
@@ -238,7 +233,7 @@ final class ReplaceErrorTests: XCTestCase {
func testReplaceErrorCompletionBeforeSubscription() {
testReceiveCompletionBeforeSubscription(
inputType: Int.self,
expected: .history([]),
expected: .history([.subscription("ReplaceError")]),
{ $0.replaceError(with: 1) }
)
}
@@ -1,743 +0,0 @@
//
// ZipTests.swift
//
// Created by Eric Patey on 28.08.20019.
//
import XCTest
#if OPENCOMBINE_COMPATIBILITY_TEST
import Combine
#else
import OpenCombine
#endif
@available(macOS 10.15, iOS 13.0, *)
final class ZipTests: XCTestCase {
static let arities = (2...4)
struct ChildInfo {
let subscription: CustomSubscription
let publisher: CustomPublisher
}
func testSendsExpectedValues() {
ZipTests.arities.forEach { arity in
let (children, zip) = getChildrenAndZipForArity(arity)
let downstreamSubscriber = TrackingSubscriber(receiveSubscription: {
$0.request(.unlimited)
})
zip.subscribe(downstreamSubscriber)
(0..<arity).forEach { XCTAssertEqual(children[$0].publisher.send(1), .none) }
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.value(arity)])
}
}
func testChildDemand() {
[Subscribers.Demand.unlimited, .max(1)].forEach { initialDemand in
let (children, zip) = getChildrenAndZipForArity(2)
var downstreamSubscription: Subscription?
let downstreamSubscriber = TrackingSubscriberBase<Int, TestingError>(
receiveSubscription: { downstreamSubscription = $0 })
zip.subscribe(downstreamSubscriber)
// Confirm initial demand
downstreamSubscription?.request(initialDemand)
(0..<2).forEach { XCTAssertEqual(children[$0].subscription.history,
[.requested(initialDemand)])
}
// Confirm no incremental demand
(0..<2).forEach { XCTAssertEqual(children[$0].publisher.send(1), .max(0)) }
// Confirm no additional subscription demand
(0..<2).forEach { XCTAssertEqual(children[$0].subscription.history,
[.requested(initialDemand)])
}
// Confirm value was sent
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.value(2)])
// Confirm subsequent demand
downstreamSubscription?.request(.max(2))
(0..<2).forEach { XCTAssertEqual(children[$0].subscription.history,
[.requested(initialDemand),
.requested(.max(2))])
}
}
}
func testDownstreamDemandRequestedWhileSendingValue() {
[Subscribers.Demand.unlimited, .max(10)].forEach { initialDemand in
let (children, zip) = getChildrenAndZipForArity(2)
var downstreamSubscription: Subscription?
let downstreamSubscriber = TrackingSubscriber(
receiveSubscription: {
downstreamSubscription = $0
$0.request(initialDemand)
},
receiveValue: { _ in
downstreamSubscription?.request(.max(666))
return Subscribers.Demand.none
}
)
zip.subscribe(downstreamSubscriber)
XCTAssertEqual(children[0].publisher.send(1), .none)
// Apple will use the result of .receive(_ input:) INSTEAD of sending
// .request to the subscription if a request is received WHILE processing
// the .receive.
// AppleRef: 001
XCTAssertEqual(children[1].publisher.send(1), .max(666))
XCTAssertEqual(children[0].subscription.history,
[.requested(initialDemand),
.requested(.max(666))])
XCTAssertEqual(children[1].subscription.history,
[.requested(initialDemand)])
}
}
func testUpstreamValueReceivedWhileSendingValue() {
let (children, zip) = getChildrenAndZipForArity(2)
let downstreamSubscriber = TrackingSubscriber(
receiveSubscription: { $0.request(.unlimited) },
receiveValue: { _ in
XCTAssertEqual(children[0].publisher.send(1), .none)
return Subscribers.Demand.none
}
)
zip.subscribe(downstreamSubscriber)
XCTAssertEqual(children[0].publisher.send(1), .none)
XCTAssertEqual(children[1].publisher.send(1), .none)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.value(2)])
}
func testUpstreamFinishReceivedWhileSendingValue() {
let (children, zip) = getChildrenAndZipForArity(2)
let downstreamSubscriber = TrackingSubscriber(
receiveSubscription: { $0.request(.unlimited) },
receiveValue: { _ in
children[0].publisher.send(completion: .finished)
return Subscribers.Demand.none
}
)
zip.subscribe(downstreamSubscriber)
XCTAssertEqual(children[0].publisher.send(1), .none)
XCTAssertEqual(children[0].publisher.send(1), .none)
XCTAssertEqual(children[1].publisher.send(1), .none)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.value(2)])
}
// NOTE about how/when Apple sends .finished on `Zip`.
//
// The documentation says:
// If either upstream publisher finishes successfuly or fails with an error,
// the zipped publisher does the same.
//
// This may be true for`.failed`, but it just isn't true for `.finished`.
// The combination of tests in here confirm Apple's actual behavior. An assesment
// is made to determine if it is possible for the `Zip` to send any more values
// downstream. Roughly speaking, if any children required to provide component
// values for the next value have finished, then it will be impossible for `Zip`
// to send any more values.
// This assessment is slightly complicated by the fact that `Zip` buffers _surplus_
// component values while waiting to complete the entire tuple.
//
// AppleRef: 002 The algorithm is currently further complicated by the fact that this
// assessment is not made continuously, but rather only when one of the child
// subcriptions sends a `.finished`. This means that Apple's behavior is inconsistent.
// Sometimes, the `Zip` remains alive even though no futher emissions are possible.
// Sometimes it finishes. Ugh.
//
// If I were in charge, `Zip` would finish as soon as it becomes impossible for it
// to send another value - regarless of what triggers that change in state.
func testZipCompletesOnlyAfterAllChildrenComplete() {
let upstreamSubscription = CustomSubscription()
let child1Publisher = CustomPublisher(subscription: upstreamSubscription)
let child2Publisher = CustomPublisher(subscription: upstreamSubscription)
let zip = child1Publisher.zip(child2Publisher) { $0 + $1 }
let downstreamSubscriber = TrackingSubscriberBase<Int, TestingError>(
receiveSubscription: { $0.request(.unlimited) })
zip.subscribe(downstreamSubscriber)
XCTAssertEqual(child1Publisher.send(100), .none)
XCTAssertEqual(child1Publisher.send(200), .none)
XCTAssertEqual(child1Publisher.send(300), .none)
XCTAssertEqual(child2Publisher.send(1), .none)
child1Publisher.send(completion: .finished)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.value(101)])
XCTAssertEqual(child2Publisher.send(2), .none)
XCTAssertEqual(child2Publisher.send(3), .none)
// This is so bogus. So, even though no further values are possible, Apple delays
// the completion. It seems to consider the fact that no more values are possible
// ONLY after one child sends a .finished
// Ref: 53EB
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.value(101),
.value(202),
.value(303),
.completion(.finished)])
child2Publisher.send(completion: .finished)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.value(101),
.value(202),
.value(303),
.completion(.finished)])
XCTAssertEqual(
upstreamSubscription.history,
[.requested(.unlimited), .requested(.unlimited), .cancelled]
)
}
func testUpstreamExceedsDemand() {
// Must use CustomPublisher if we want to force send a value beyond the demand
let child1Subscription = CustomSubscription()
let child1Publisher = CustomPublisher(subscription: child1Subscription)
let child2Subscription = CustomSubscription()
let child2Publisher = CustomPublisher(subscription: child2Subscription)
let zip = child1Publisher.zip(child2Publisher) { $0 + $1 }
var downstreamSubscription: Subscription?
let downstreamSubscriber = TrackingSubscriber(receiveSubscription: {
downstreamSubscription = $0
$0.request(.max(1))
})
zip.subscribe(downstreamSubscriber)
XCTAssertEqual(child1Publisher.send(100), .none)
XCTAssertEqual(child2Publisher.send(1), .none)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.value(101)])
XCTAssertEqual(child1Publisher.send(200), .none)
XCTAssertEqual(child1Publisher.send(300), .none)
XCTAssertEqual(child2Publisher.send(2), .none)
// Surplus is sent downstream despite demand of zero
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.value(101),
.value(202)])
XCTAssertEqual(child2Publisher.send(3), .none)
downstreamSubscription?.request(.max(1))
// Surplus is buffered for sending when demand resumes
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.value(101),
.value(202),
.value(303)])
}
private func getChildrenAndZipForArity(_ childCount: Int)
-> ([ChildInfo], AnyPublisher<Int, TestingError>)
{
var children = [ChildInfo]()
for _ in (0..<childCount) {
let subscription = CustomSubscription()
let publisher = CustomPublisher(subscription: subscription)
children.append(ChildInfo(subscription: subscription,
publisher: publisher))
}
let zip: AnyPublisher<Int, TestingError>
switch childCount {
case 2:
zip = AnyPublisher(children[0].publisher.zip(children[1].publisher)
{ $0 + $1 })
case 3:
zip = AnyPublisher(children[0].publisher
.zip(children[1].publisher,
children[2].publisher) { $0 + $1 + $2 })
case 4:
zip = AnyPublisher(children[0].publisher
.zip(children[1].publisher,
children[2].publisher,
children[3].publisher) { $0 + $1 + $2 + $3 })
default:
fatalError()
}
return (children, zip)
}
func testImmediateFinishWhenOneChildFinishesWithNoSurplus() {
ZipTests.arities.forEach { arity in
for childToFinish in (0..<arity) {
let description = "Zip\(arity) childToFinish=\(childToFinish)"
let (children, zip) = getChildrenAndZipForArity(arity)
let downstreamSubscriber = TrackingSubscriber(receiveSubscription: {
$0.request(.unlimited)
})
zip.subscribe(downstreamSubscriber)
children[childToFinish].publisher.send(completion: .finished)
XCTAssertEqual(downstreamSubscriber.history,
[.subscription("Zip"),
.completion(.finished)],
description)
for child in (0..<arity) {
if child == childToFinish {
XCTAssertEqual(children[child].subscription.history,
[.requested(.unlimited)],
description)
} else {
XCTAssertEqual(children[child].subscription.history,
[.requested(.unlimited),
.cancelled],
description)
}
}
}
}
}
// NOTE: This behavior betrays Apple's comments which say:
// If either upstream publisher finishes successfuly or fails with an error,
// the zipped publisher does the same.
// That appears to not be true for finishing successfully if the completing child
// has a surplus. Rather, the zip remains alive until it is impossible to deliver
// another result.
func testDelayedFinishWhenOneChildFinishesWithSurplus() {
ZipTests.arities.forEach { arity in
for childToSend in (0..<arity) {
for childToFinish in (0..<arity) {
let (children, zip) = getChildrenAndZipForArity(arity)
let downstreamSubscriber = TrackingSubscriber(receiveSubscription: {
$0.request(.unlimited)
})
zip.subscribe(downstreamSubscriber)
_ = children[childToSend].publisher.send(666)
children[childToFinish].publisher.send(completion: .finished)
if childToSend == childToFinish {
XCTAssertEqual(downstreamSubscriber.history,
[.subscription("Zip")])
// Finish the others
(0..<arity)
.filter { $0 != childToFinish }
.forEach( {
children[$0].publisher.send(completion: .finished)
})
XCTAssertEqual(downstreamSubscriber.history,
[.subscription("Zip"),
.completion(.finished)])
} else {
XCTAssertEqual(downstreamSubscriber.history,
[.subscription("Zip"),
.completion(.finished)])
}
}
}
}
}
func testBCancelledAfterAFailed() {
let child1Subscription = CustomSubscription()
let child1Publisher = CustomPublisher(subscription: child1Subscription)
let child2Subscription = CustomSubscription()
let child2Publisher = CustomPublisher(subscription: child2Subscription)
let zip = child1Publisher.zip(child2Publisher) { $0 + $1 }
let downstreamSubscriber = TrackingSubscriber(receiveSubscription: {
$0.request(.unlimited)
})
zip.subscribe(downstreamSubscriber)
child1Publisher.send(completion: .failure(.oops))
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.completion(.failure(.oops))])
XCTAssertEqual(child1Subscription.history, [.requested(.unlimited),
.cancelled])
XCTAssertEqual(child2Subscription.history, [.requested(.unlimited),
.cancelled])
}
func testAValueAfterAChildFinishedWithoutSurplus() {
let child1Publisher = PassthroughSubject<Int, TestingError>()
let child2Publisher = PassthroughSubject<Int, TestingError>()
let zip = child1Publisher.zip(child2Publisher) { $0 + $1 }
let downstreamSubscriber = TrackingSubscriber(
receiveSubscription: { $0.request(.unlimited) })
zip.subscribe(downstreamSubscriber)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip")])
child1Publisher.send(completion: .finished)
// This is strange and inconsistent. In other cases, zip doesn't complete
// until ALL children have completed
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.completion(.finished)])
child1Publisher.send(200)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.completion(.finished)])
child2Publisher.send(1)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.completion(.finished)])
child2Publisher.send(completion: .finished)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.completion(.finished)])
}
func testBValueAfterAChildFinishedWithoutSurplus() {
let child1Publisher = PassthroughSubject<Int, TestingError>()
let child2Publisher = PassthroughSubject<Int, TestingError>()
let zip = child1Publisher.zip(child2Publisher) { $0 + $1 }
let downstreamSubscriber = TrackingSubscriber(
receiveSubscription: { $0.request(.unlimited) })
zip.subscribe(downstreamSubscriber)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip")])
child1Publisher.send(completion: .finished)
// This is strange and inconsistent. In other cases, zip doesn't complete
// until ALL children have completed
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.completion(.finished)])
child2Publisher.send(1)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.completion(.finished)])
child2Publisher.send(completion: .finished)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.completion(.finished)])
}
func testAValueAfterAChildFinishedWithSurplus() {
let child1Publisher = PassthroughSubject<Int, TestingError>()
let child2Publisher = PassthroughSubject<Int, TestingError>()
let zip = child1Publisher.zip(child2Publisher) { $0 + $1 }
let downstreamSubscriber = TrackingSubscriber(
receiveSubscription: { $0.request(.unlimited) })
zip.subscribe(downstreamSubscriber)
child1Publisher.send(100)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip")])
child1Publisher.send(completion: .finished)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip")])
child1Publisher.send(200)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip")])
child2Publisher.send(1)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.value(101),
.completion(.finished)])
child2Publisher.send(completion: .finished)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.value(101),
.completion(.finished)])
}
func testBValueAfterAChildFinishedWithSurplus() {
let child1Publisher = PassthroughSubject<Int, TestingError>()
let child2Publisher = PassthroughSubject<Int, TestingError>()
let zip = child1Publisher.zip(child2Publisher) { $0 + $1 }
let downstreamSubscriber = TrackingSubscriber(
receiveSubscription: { $0.request(.unlimited) })
zip.subscribe(downstreamSubscriber)
child1Publisher.send(100)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip")])
child1Publisher.send(completion: .finished)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip")])
child2Publisher.send(1)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.value(101),
.completion(.finished)])
child2Publisher.send(completion: .finished)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.value(101),
.completion(.finished)])
}
func testValueAfterFailed() {
let child1Publisher = PassthroughSubject<Int, TestingError>()
let child2Publisher = PassthroughSubject<Int, TestingError>()
let zip = child1Publisher.zip(child2Publisher) { $0 + $1 }
let downstreamSubscriber = TrackingSubscriber(
receiveSubscription: { $0.request(.unlimited) })
zip.subscribe(downstreamSubscriber)
child1Publisher.send(100)
child1Publisher.send(completion: .failure(.oops))
child2Publisher.send(1)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.completion(.failure(.oops))])
}
func testFinishAfterFinished() {
let child1Publisher = PassthroughSubject<Int, TestingError>()
let child2Publisher = PassthroughSubject<Int, TestingError>()
let zip = child1Publisher.zip(child2Publisher) { $0 + $1 }
let downstreamSubscriber = TrackingSubscriber(
receiveSubscription: { $0.request(.unlimited) })
zip.subscribe(downstreamSubscriber)
child1Publisher.send(completion: .finished)
child2Publisher.send(completion: .finished)
child1Publisher.send(completion: .finished)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.completion(.finished)])
}
func testFinishAfterFailed() {
let child1Publisher = PassthroughSubject<Int, TestingError>()
let child2Publisher = PassthroughSubject<Int, TestingError>()
let zip = child1Publisher.zip(child2Publisher) { $0 + $1 }
let downstreamSubscriber = TrackingSubscriber(
receiveSubscription: { $0.request(.unlimited) })
zip.subscribe(downstreamSubscriber)
child1Publisher.send(completion: .failure(.oops))
child1Publisher.send(completion: .finished)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.completion(.failure(.oops))])
}
func testFailedAfterFinished() {
let child1Publisher = PassthroughSubject<Int, TestingError>()
let child2Publisher = PassthroughSubject<Int, TestingError>()
let zip = child1Publisher.zip(child2Publisher) { $0 + $1 }
let downstreamSubscriber = TrackingSubscriber(
receiveSubscription: { $0.request(.unlimited) })
zip.subscribe(downstreamSubscriber)
child1Publisher.send(completion: .finished)
child2Publisher.send(completion: .finished)
child1Publisher.send(completion: .failure(.oops))
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.completion(.finished)])
}
func testFailedAfterFailed() {
let child1Publisher = PassthroughSubject<Int, TestingError>()
let child2Publisher = PassthroughSubject<Int, TestingError>()
let zip = child1Publisher.zip(child2Publisher) { $0 + $1 }
let downstreamSubscriber = TrackingSubscriber(
receiveSubscription: { $0.request(.unlimited) })
zip.subscribe(downstreamSubscriber)
child1Publisher.send(completion: .failure(.oops))
child1Publisher.send(completion: .failure(.oops))
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.completion(.failure(.oops))])
}
func testZip2Lifecycle() throws {
let child2Publisher = PassthroughSubject<Int, TestingError>()
try testLifecycle(sendValue: 42,
cancellingSubscriptionReleasesSubscriber: false,
{ $0.zip(child2Publisher) })
}
func testZip3Lifecycle() throws {
let child2Publisher = PassthroughSubject<Int, TestingError>()
let child3Publisher = PassthroughSubject<Int, TestingError>()
try testLifecycle(sendValue: 42,
cancellingSubscriptionReleasesSubscriber: false,
{ $0.zip(child2Publisher, child3Publisher) })
}
func testZip4Lifecycle() throws {
let child2Publisher = PassthroughSubject<Int, TestingError>()
let child3Publisher = PassthroughSubject<Int, TestingError>()
let child4Publisher = PassthroughSubject<Int, TestingError>()
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
{ $0.zip(child2Publisher, child3Publisher, child4Publisher) })
}
func testZipReceiveSubscriptionTwice() throws {
let child2Publisher = PassthroughSubject<Int, TestingError>()
// Can't use `testReceiveSubscriptionTwice` helper here as `(Int, Int)` output
// can't be made `Equatable`.
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .none,
createSut: { $0.zip(child2Publisher) }
)
XCTAssertEqual(helper.subscription.history, [])
let secondSubscription = CustomSubscription()
try XCTUnwrap(helper.publisher.subscriber)
.receive(subscription: secondSubscription)
XCTAssertEqual(secondSubscription.history, [.cancelled])
try XCTUnwrap(helper.publisher.subscriber)
.receive(subscription: helper.subscription)
XCTAssertEqual(helper.subscription.history, [.cancelled])
try XCTUnwrap(helper.downstreamSubscription).cancel()
XCTAssertEqual(helper.subscription.history, [.cancelled, .cancelled])
let thirdSubscription = CustomSubscription()
try XCTUnwrap(helper.publisher.subscriber)
.receive(subscription: thirdSubscription)
}
func testNoDemandOnSubscriptionCrashes() {
ZipTests.arities.forEach { arity in
let (_, zip) = getChildrenAndZipForArity(arity)
let downstreamSubscriber = TrackingSubscriber(
receiveSubscription: { subscription in
self.assertCrashes { subscription.request(.none) }
}
)
zip.subscribe(downstreamSubscriber)
}
}
func testIncreasedDemand() throws {
ZipTests.arities.forEach { arity in
let (children, zip) = getChildrenAndZipForArity(arity)
let downstreamSubscriber = TrackingSubscriber(
receiveValue: { _ in
.max(1)
}
)
zip.subscribe(downstreamSubscriber)
(0..<arity).forEach {
let demand = children[$0].publisher.send(1)
if $0 == arity - 1 {
XCTAssertEqual(demand, .max(1))
} else {
XCTAssertEqual(demand, .none)
}
}
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.value(arity)])
}
}
func testZipCurrentValueSubject() throws {
let subject = CurrentValueSubject<Void, Never>(())
let zip = [42].publisher.zip(subject)
let downstreamSubscriber = TrackingSubscriberBase<(Int, ()), Never>(
receiveSubscription: { $0.request(.unlimited) })
zip.subscribe(downstreamSubscriber)
let history = downstreamSubscriber.history
XCTAssertEqual(history.count, 3)
// tuples aren't Equatable, so matching the elements one by one
switch history[0] {
case .subscription("Zip"):
break
default:
XCTFail("Failed to match the first subscription event in \(#function)")
}
switch history[1] {
case .value(let v):
if v.0 != 42 || v.1 != () {
XCTFail("Failed to match the value event in \(#function)")
}
default:
XCTFail("Failed to match the value event in \(#function)")
}
switch history[2] {
case .completion(.finished):
break
default:
XCTFail("Failed to match the completion event in \(#function)")
}
}
}
@@ -137,41 +137,4 @@ final class AssignTests: XCTestCase {
publisher.send(100)
XCTAssertEqual(object.value, 42)
}
func testReceiveCompletionWhileCancelling() {
let cancellable: AnyCancellable
do {
let object = ObjectToModify()
cancellable = object.autofinish.assign(to: \.value, on: object)
}
// autofinish is deallocated here, a completion is sent to the sink
cancellable.cancel()
}
func testReceiveCompletionWhileCompleting() {
let cancellable: AnyCancellable
let finish: () -> Void
do {
let object = ObjectToModify()
cancellable = object.autofinish.assign(to: \.value, on: object)
let underlyingPublisher = object.autofinish.publisher
finish = { underlyingPublisher.send(completion: .finished) }
}
finish() // autofinish is deallocated here, a completion is sent to the sink
cancellable.cancel()
}
}
@available(macOS 10.15, iOS 13.0, *)
final class ObjectToModify {
let autofinish = AutomaticallyFinish<Int, Never>()
var value = 0
}
@@ -210,82 +210,6 @@ final class SinkTests: XCTestCase {
releasesClosures: true)
}
func testReceiveCompletionWhileCancelling() {
// https://github.com/OpenCombine/OpenCombine/issues/208
let cancellable: AnyCancellable
do {
let autofinish = AutomaticallyFinish<Int, Never>()
cancellable = autofinish.listen(
receiveCompletion: { _ in },
receiveValue: { _ in
XCTFail("Should not be called")
_ = autofinish // capture
}
)
}
// autofinish is deallocated here, a completion is sent to the sink
cancellable.cancel()
}
func testReceiveCompletionWhileCancelling2() {
// https://github.com/OpenCombine/OpenCombine/issues/208
let cancellable: AnyCancellable
do {
let autofinish = AutomaticallyFinish<Int, Never>()
cancellable = autofinish.listen(
receiveCompletion: { _ in
XCTFail("Should not be called")
_ = autofinish // capture
},
receiveValue: { _ in }
)
}
// autofinish is deallocated here, a completion is sent to the sink
cancellable.cancel()
}
func testReceiveCompletionWhileCompleting() {
// https://github.com/OpenCombine/OpenCombine/issues/208
let cancellable: AnyCancellable
let finish: () -> Void
var receiveCompletionCalled = false
do {
let autofinish = AutomaticallyFinish<Int, Never>()
cancellable = autofinish.listen(
receiveCompletion: { _ in
receiveCompletionCalled = true
},
receiveValue: { _ in
XCTFail("Should not be called")
_ = autofinish // capture
}
)
let underlyingPublisher = autofinish.publisher
finish = { underlyingPublisher.send(completion: .finished) }
}
finish() // autofinish is deallocated here, a completion is sent to the sink
cancellable.cancel()
XCTAssertTrue(receiveCompletionCalled)
}
func testRecursiveCompletion() {
var recursionCounter = 10
var delayedSink: Sut?

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