Compare commits
65 Commits
combine-latest
...
0.14.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 8576f0d579 | |||
| 8f8ef5057d | |||
| ff31c43375 | |||
| 27d76e1fed | |||
| 5823523b72 | |||
| 6f7c3c4b47 | |||
| 4d9a7d6a5b | |||
| 9cf67e3637 | |||
| 3877609ba2 | |||
| 734e7e39cb | |||
| 2085bb7593 | |||
| 5b247a5a01 | |||
| 64f436c748 | |||
| b4e6313814 | |||
| a0cf895c8c | |||
| dec7d4a569 | |||
| fdc7550ff7 | |||
| 135dc9a8ab | |||
| 37a4fe400f | |||
| 7b466153a6 | |||
| baac42a0ad | |||
| fd05f5c8ff | |||
| 8bfdcd4295 | |||
| 90454807b4 | |||
| 77374fa820 | |||
| 8eda9d7e3d | |||
| 35cfe51c72 | |||
| 42c0fa02ae | |||
| 999a29cdf9 | |||
| 36edf4819b | |||
| dfac8a9da7 | |||
| 070ed94d18 | |||
| ea8938db72 | |||
| 4392b4610c | |||
| c96f2e300d | |||
| 94de7bae46 | |||
| ed1b06ba51 | |||
| 4b2c87a0bb | |||
| 0243fd063d | |||
| 4fed5e9a5a | |||
| 80a4915715 | |||
| 4716805f12 | |||
| 5490ff9be9 | |||
| c911862a24 | |||
| f823f7b18c | |||
| 02d1494ce9 | |||
| f69bf6af64 | |||
| 866d837cdf | |||
| 7d0a8cd6f8 | |||
| dfd3cdf890 | |||
| ef0288e075 | |||
| f219d6f6a5 | |||
| 710dfa2715 | |||
| 791625ff3b | |||
| 7e4cdde419 | |||
| 096e245d02 | |||
| 1879860f35 | |||
| ace5778817 | |||
| 12700a0500 | |||
| 6c8108f9dc | |||
| b27b2c31ce | |||
| 3d3adb564b | |||
| 925bee4af9 | |||
| adcee8c14d | |||
| 29126ac259 |
@@ -1,326 +0,0 @@
|
||||
macOS_tests_steps: &macOS_tests_steps
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Building and running tests in debug mode with coverage
|
||||
command: |
|
||||
make test-debug \
|
||||
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
|
||||
SWIFT_TEST_FLAGS="--enable-code-coverage --build-path .build-test-debug"
|
||||
xcrun llvm-cov show \
|
||||
-instr-profile=.build-test-debug/debug/codecov/default.profdata \
|
||||
.build-test-debug/debug/OpenCombinePackageTests.xctest/Contents/MacOS/OpenCombinePackageTests \
|
||||
> coverage.txt
|
||||
- run:
|
||||
name: Building and running tests in debug mode with TSan
|
||||
command: |
|
||||
make test-debug-sanitize-thread \
|
||||
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
|
||||
SWIFT_TEST_FLAGS="--build-path .build-test-debug-sanitize-thread"
|
||||
- run:
|
||||
name: Building and running tests in release mode
|
||||
command: |
|
||||
make test-release \
|
||||
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
|
||||
SWIFT_TEST_FLAGS="--build-path .build-test-release"
|
||||
- run:
|
||||
name: Generating Xcode project
|
||||
command: make generate-xcodeproj SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors"
|
||||
- run:
|
||||
name: Building for testing on macOS 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"
|
||||
@@ -0,0 +1,16 @@
|
||||
name: CocoaPods
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
validate_podspec:
|
||||
name: Run pod lib lint
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Run pod lib lint
|
||||
run: pod lib lint --allow-warnings --verbose
|
||||
@@ -0,0 +1,19 @@
|
||||
name: Compatibility tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
schedule:
|
||||
- cron: "0 9 * * 1" # Every Monday at 9:00 AM
|
||||
|
||||
jobs:
|
||||
compatibility_tests_macos:
|
||||
name: Execute compatibility tests
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Run tests against Apple's Combine
|
||||
run: make test-compatibility
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
name: macOS
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
# This job is not a part of the macos_test job because of
|
||||
# the 'This copy of libswiftCore.dylib requires an OS version prior to 10.14.4.' error.
|
||||
# We have to invoke install_name_tool and patch the test executable
|
||||
# to work around this error.
|
||||
#
|
||||
# Other combinations of Xcode and macOS versions don't lead to this error.
|
||||
swift_5_0_test:
|
||||
name: Execute tests (macos-10.15, 10.3)
|
||||
runs-on: macos-10.15
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: "10.3"
|
||||
- name: Swift version
|
||||
run: swift --version
|
||||
- name: Build and run tests in debug mode with coverage
|
||||
run: |
|
||||
swift build \
|
||||
--build-tests \
|
||||
-c debug \
|
||||
-Xswiftc -warnings-as-errors \
|
||||
-Xswiftc -profile-generate \
|
||||
-Xswiftc -profile-coverage-mapping \
|
||||
--build-path .build-test-debug
|
||||
install_name_tool \
|
||||
-rpath /Applications/Xcode_10.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx /usr/lib/swift \
|
||||
.build-test-debug/debug/OpenCombinePackageTests.xctest/Contents/MacOS/OpenCombinePackageTests
|
||||
install_name_tool \
|
||||
-add_rpath /Applications/Xcode_10.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx \
|
||||
.build-test-debug/debug/OpenCombinePackageTests.xctest/Contents/MacOS/OpenCombinePackageTests
|
||||
swift test \
|
||||
--skip-build \
|
||||
--enable-code-coverage \
|
||||
--build-path .build-test-debug
|
||||
xcrun llvm-cov show \
|
||||
-instr-profile=.build-test-debug/debug/codecov/default.profdata \
|
||||
.build-test-debug/debug/OpenCombinePackageTests.xctest/Contents/MacOS/OpenCombinePackageTests \
|
||||
> coverage.txt
|
||||
- name: Build and run tests in release mode
|
||||
run: |
|
||||
swift build \
|
||||
--build-tests \
|
||||
-c release \
|
||||
-Xswiftc -warnings-as-errors \
|
||||
-Xswiftc -profile-generate \
|
||||
-Xswiftc -profile-coverage-mapping \
|
||||
--build-path .build-test-release
|
||||
install_name_tool \
|
||||
-rpath /Applications/Xcode_10.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx /usr/lib/swift \
|
||||
.build-test-release/release/OpenCombinePackageTests.xctest/Contents/MacOS/OpenCombinePackageTests
|
||||
install_name_tool \
|
||||
-add_rpath /Applications/Xcode_10.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx \
|
||||
.build-test-release/release/OpenCombinePackageTests.xctest/Contents/MacOS/OpenCombinePackageTests
|
||||
swift test \
|
||||
--skip-build \
|
||||
-c release \
|
||||
--enable-code-coverage \
|
||||
--build-path .build-test-release
|
||||
- uses: codecov/codecov-action@v2
|
||||
with:
|
||||
verbose: true
|
||||
macos_test:
|
||||
name: Execute tests
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-10.15
|
||||
xcode-version: "11.3.1" # Swift 5.1.3
|
||||
- os: macos-10.15
|
||||
xcode-version: "11.7" # Swift 5.2.4
|
||||
- os: macos-11
|
||||
xcode-version: "12.4" # Swift 5.3.2
|
||||
- os: macos-11
|
||||
xcode-version: "12.5.1" # Swift 5.4.2
|
||||
- os: macos-11
|
||||
xcode-version: "13.2.1" # Swift 5.5.2
|
||||
- os: macos-12
|
||||
xcode-version: "13.4.1" # Swift 5.6.1
|
||||
- os: macos-12
|
||||
xcode-version: "14.2" # Swift 5.7.2
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: ${{ matrix.xcode-version }}
|
||||
- name: Swift version
|
||||
run: swift --version
|
||||
- name: Build and run tests in debug mode with coverage
|
||||
run: |
|
||||
swift test \
|
||||
-c debug \
|
||||
-Xswiftc -warnings-as-errors \
|
||||
--enable-code-coverage \
|
||||
--build-path .build-test-debug
|
||||
xcrun llvm-cov show \
|
||||
-instr-profile=.build-test-debug/debug/codecov/default.profdata \
|
||||
.build-test-debug/debug/OpenCombinePackageTests.xctest/Contents/MacOS/OpenCombinePackageTests \
|
||||
> coverage.txt
|
||||
- name: Build and run tests in debug mode with TSan
|
||||
if: ${{ matrix.xcode-version != '13.2.1' && matrix.xcode-version != '13.4.1' }} # https://bugs.swift.org/browse/SR-15444
|
||||
run: |
|
||||
swift test \
|
||||
-c debug \
|
||||
--sanitize thread \
|
||||
-Xswiftc -warnings-as-errors \
|
||||
--build-path .build-test-debug-sanitize-thread
|
||||
- name: Build and run tests in release mode
|
||||
run: |
|
||||
swift test \
|
||||
-c release \
|
||||
-Xswiftc -warnings-as-errors \
|
||||
--enable-code-coverage \
|
||||
--build-path .build-test-release
|
||||
- uses: codecov/codecov-action@v2
|
||||
with:
|
||||
verbose: true
|
||||
@@ -0,0 +1,26 @@
|
||||
name: SwiftLint
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/swiftlint.yml"
|
||||
- ".swiftlint.yml"
|
||||
- "**/*.swift"
|
||||
|
||||
jobs:
|
||||
SwiftLint:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
# Fetch current versions of files
|
||||
- name: Fetch base ref
|
||||
run: |
|
||||
git fetch --prune --no-tags --depth=1 origin +refs/heads/${{ github.base_ref }}:refs/heads/${{ github.base_ref }}
|
||||
# Diff pull request to current files, then SwiftLint changed files
|
||||
- name: GitHub Action for SwiftLint
|
||||
uses: mayk-it/action-swiftlint@3.2.2
|
||||
env:
|
||||
DIFF_BASE: ${{ github.base_ref }}
|
||||
DIFF_HEAD: HEAD
|
||||
with:
|
||||
args: --strict
|
||||
@@ -0,0 +1,57 @@
|
||||
name: Ubuntu
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
ubuntu_test:
|
||||
name: Execute tests on Ubuntu
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
swift_version: ["5.0", "5.1", "5.2", "5.3", "5.4", "5.5", "5.6", "5.7"]
|
||||
runs-on: ubuntu-latest
|
||||
container: swift:${{ matrix.swift_version }}-bionic
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Generating LinuxMain.swift
|
||||
if: >-
|
||||
${{ matrix.swift_version == '5.0' ||
|
||||
matrix.swift_version == '5.1' ||
|
||||
matrix.swift_version == '5.2' ||
|
||||
matrix.swift_version == '5.3' }}
|
||||
run: |
|
||||
apt update -y
|
||||
apt upgrade -y
|
||||
apt install -y python3.8
|
||||
python3.8 utils/discover_tests.py
|
||||
- name: Building and running tests in debug mode with coverage
|
||||
run: |
|
||||
swift test \
|
||||
-c debug \
|
||||
-Xswiftc -warnings-as-errors \
|
||||
--enable-code-coverage \
|
||||
--build-path .build-test-debug
|
||||
llvm-cov show \
|
||||
-instr-profile=.build-test-debug/debug/codecov/default.profdata \
|
||||
.build-test-debug/debug/OpenCombinePackageTests.xctest \
|
||||
> coverage.txt
|
||||
- name: Building and running tests in debug mode with TSan
|
||||
if: ${{ matrix.swift_version != '5.0' }} # There are false positives there
|
||||
run: |
|
||||
swift test \
|
||||
-c debug \
|
||||
--sanitize thread \
|
||||
--build-path .build-test-debug-sanitize-thread
|
||||
- name: Building and running tests in release mode
|
||||
run: |
|
||||
swift test \
|
||||
-c release \
|
||||
-Xswiftc -warnings-as-errors \
|
||||
--build-path .build-test-release
|
||||
- uses: codecov/codecov-action@v2
|
||||
with:
|
||||
verbose: true
|
||||
@@ -1,4 +1,4 @@
|
||||
name: SwiftWasm
|
||||
name: Wasm
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -7,10 +7,42 @@ on:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
carton_wasmer_test:
|
||||
carton_wasmer_test_5_3:
|
||||
name: "Execute tests on Wasm (Swift 5.3)"
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: swiftwasm/swiftwasm-action@v5.3
|
||||
|
||||
carton_wasmer_test_5_4:
|
||||
name: "Execute tests on Wasm (Swift 5.4)"
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: swiftwasm/swiftwasm-action@v5.4
|
||||
|
||||
carton_wasmer_test_5_5:
|
||||
name: "Execute tests on Wasm (Swift 5.5)"
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: swiftwasm/swiftwasm-action@v5.5
|
||||
|
||||
carton_wasmer_test_5_6:
|
||||
name: "Execute tests on Wasm (Swift 5.6)"
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: swiftwasm/swiftwasm-action@v5.6
|
||||
|
||||
carton_wasmer_test_5_7:
|
||||
name: "Execute tests on Wasm (Swift 5.7)"
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: swiftwasm/swiftwasm-action@v5.7
|
||||
@@ -0,0 +1,32 @@
|
||||
name: Windows
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
windows_test:
|
||||
name: Execute tests on Windows
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: windows-2019
|
||||
swift_version: "5.4.2"
|
||||
- os: windows-2019
|
||||
swift_version: "5.5.1"
|
||||
- os: windows-2019
|
||||
swift_version: "5.6.1"
|
||||
- os: windows-2019
|
||||
swift_version: "5.7.2"
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: compnerd/gha-setup-swift@main
|
||||
with:
|
||||
branch: swift-${{ matrix.swift_version }}-release
|
||||
tag: ${{ matrix.swift_version }}-RELEASE
|
||||
- name: Building and running tests in debug mode
|
||||
run: swift test
|
||||
@@ -5,6 +5,7 @@ included:
|
||||
child_config: Tests/.swiftlint.yml
|
||||
|
||||
disabled_rules:
|
||||
- blanket_disable_command
|
||||
- block_based_kvo
|
||||
- class_delegate_protocol
|
||||
- colon
|
||||
|
||||
@@ -1,3 +1,27 @@
|
||||
# 0.14.0 (23 Apr 2023)
|
||||
This release is compatible with Xcode 14.2 and Swift 5.7
|
||||
|
||||
### Additions
|
||||
- Primary associated type support for `Publisher`, `Subscriber`, `ConnectablePublisher`, `Subject` and `Scheduler` protocols (#239)
|
||||
|
||||
### Bugfixes
|
||||
- Fixed nullifying the reference to parent in `Future`'s conduit (#239)
|
||||
|
||||
# 0.13.0 (1 Feb 2022)
|
||||
This release is compatible with Xcode 13.2.
|
||||
|
||||
### Additions
|
||||
- Windows support (thank you @MaxDesiatov!)
|
||||
- `Publishers.Throttle` (#195, thank you @stuaustin)
|
||||
- `Publishers.PrefixUntilOutput` (#206)
|
||||
- `Publishers.Zip` (#222, thank you @MaxDesiatov and @ArthurChi)
|
||||
- `async`/`await` extensions: `Future.value` and `Publisher.values` (#219)
|
||||
|
||||
|
||||
### Bugfixes
|
||||
- Fixed reentrancy bugs in ` Subscribers.Sink` and `Subscribers.Assign` (#210)
|
||||
- Fixed lifecycle bugs in `Publishers.Concatenate` (#210)
|
||||
|
||||
# 0.12.0 (29 Jan 2021)
|
||||
|
||||
This release adds a new `OpenCombineShim` product that will conditionally re-export either
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# Contributing
|
||||
|
||||
In order to work on this project you will need Xcode 10.2 and Swift 5.0 or later.
|
||||
|
||||
Please refer to the [issue #1](https://github.com/OpenCombine/OpenCombine/issues/1) for the list of operators that remain unimplemented, as well as the [RemainingCombineInterface.swift](https://github.com/OpenCombine/OpenCombine/blob/master/RemainingCombineInterface.swift) file. The latter contains the generated interface of Apple's Combine from the latest Xcode version. When the functionality is implemented in OpenCombine, it should be removed from the RemainingCombineInterface.swift file.
|
||||
|
||||
You can refer to [this repo](https://github.com/OpenCombine/combine-interfaces) to observe Apple's Combine API and documentation changes between different Xcode (beta) versions.
|
||||
|
||||
You can run compatibility tests against Apple's Combine. In order to do that you will need either macOS 10.14 with iOS 13 simulator installed (since the only way we can get Apple's Combine on macOS 10.14 is using the simulator), or macOS 10.15 (Apple's Combine is bundled with the OS). Execute the following command from the root of the package:
|
||||
|
||||
```
|
||||
$ make test-compatibility
|
||||
```
|
||||
|
||||
Or enable the `-DOPENCOMBINE_COMPATIBILITY_TEST` compiler flag in Xcode's build settings. Note that on iOS only the latter will work.
|
||||
|
||||
> NOTE: Before starting to work on some feature, please consult the [GitHub project](https://github.com/OpenCombine/OpenCombine/projects/2) to make sure that nobody's already making progress on the same feature! If not, then please create a draft PR to indicate that you're beginning your work.
|
||||
|
||||
#### Releasing a new version
|
||||
|
||||
1. Create a new branch from master and call it `release/<major>.<minor>.<patch>`.
|
||||
1. Replace the usages of the old version in `README.md` with the new version (make sure to check the [Swift Package Manager](#swift-package-manager) and [CocoaPods](#cocoapods) sections).
|
||||
1. Bump the version in `OpenCombine.podspec`, `OpenCombineDispatch.podspec` and `OpenCombineFoundation.podspec`. In the latter two you will also need to set the `spec.dependency "OpenCombine"` property to the **previous** version. Why? Because otherwise the `pod lib lint` command that we run on our regular CI will fail when validating the `OpenCombineDispatch` and `OpenCombineFoundation` podspecs, since the dependencies are not yet in the trunk. If we set the dependencies to the previous version (which is already in the trunk), everything will be fine. This is purely to make the CI work. The clients will not experience any issues, since the version is specified as `>=`.
|
||||
1. Create a pull request to master for the release branch and make sure the CI passes.
|
||||
1. Merge the pull request.
|
||||
1. In the GitHub web interface on the [releases](https://github.com/OpenCombine/OpenCombine/releases) page, click the **Draft a new release** button.
|
||||
1. The **Tag version** and **Release title** fields should be filled with the version number.
|
||||
1. The description of the release should be consistent with the previous releases. It is a good practice to divide the description into several sections: additions, bugfixes, known issues etc. Also, be sure to mention the nicknames of the contributors of the new release.
|
||||
1. Publish the release.
|
||||
1. Switch to the master branch and pull the changes.
|
||||
1. Push the release to CocoaPods trunk. For that, execute the following commands:
|
||||
|
||||
```
|
||||
pod trunk push OpenCombine.podspec --verbose --allow-warnings
|
||||
pod trunk push OpenCombineDispatch.podspec --verbose --allow-warnings
|
||||
pod trunk push OpenCombineFoundation.podspec --verbose --allow-warnings
|
||||
```
|
||||
|
||||
Note that you need to be one of the owners of the pod for that.
|
||||
|
||||
#### GYB
|
||||
|
||||
Some publishers in OpenCombine (like `Publishers.MapKeyPath`, `Publishers.Merge`) exist in several
|
||||
different flavors in order to support several arities. For example, there are also `Publishers.MapKeyPath2`
|
||||
and `Publishers.MapKeyPath3`, which are very similar but different enough that Swift's type system
|
||||
can't help us here (because there's no support for variadic generics). Maintaining multiple instances of
|
||||
those generic types is tedious and error-prone (they can get out of sync), so we use the GYB tool for
|
||||
generating those instances from a template.
|
||||
|
||||
GYB is a Python script that evaluates Python code written inside a template file, so it's very flexible —
|
||||
templates can be arbitrarily complex. There is a good article about GYB on
|
||||
[NSHipster](https://nshipster.com/swift-gyb/).
|
||||
|
||||
GYB is part of the [Swift Open Source Project](https://github.com/apple/swift/blob/master/utils/gyb.py)
|
||||
and can be distributed under the same license as Swift itself.
|
||||
|
||||
GYB template files have the `.gyb` extension. Run `make gyb` to generate Swift code from those
|
||||
templates. The generated files are prefixed with `GENERATED-` and are checked into source control. Those
|
||||
files should never be edited directly. Instead, the `.gyb` template should be edited, and after that the files
|
||||
should be regenerated using `make gyb`.
|
||||
@@ -1,76 +0,0 @@
|
||||
import Danger
|
||||
import Foundation
|
||||
|
||||
extension StringProtocol {
|
||||
func dropSuffix<S: StringProtocol>(_ suffix: S) -> SubSequence {
|
||||
if hasSuffix(suffix) {
|
||||
return self[..<index(endIndex, offsetBy: -suffix.count)]
|
||||
} else {
|
||||
return self[...]
|
||||
}
|
||||
}
|
||||
|
||||
func directoryAndFileName() -> (SubSequence, SubSequence) {
|
||||
let lastPathSeparator = lastIndex(of: "/")
|
||||
if let lastPathSeparator = lastPathSeparator {
|
||||
return (self[..<lastPathSeparator], self[index(after: lastPathSeparator)...])
|
||||
} else {
|
||||
return (".", self[...])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let danger = Danger()
|
||||
|
||||
let allCreatedAndModified = danger.git.createdFiles + danger.git.modifiedFiles
|
||||
|
||||
do {
|
||||
// Fail if the committer modified a GYB template but forgot to run `make gyb`.
|
||||
|
||||
let modifiedTemplates = allCreatedAndModified.filter { $0.hasSuffix(".gyb") }
|
||||
|
||||
for modifiedTemplate in modifiedTemplates {
|
||||
let (directory, filename) = modifiedTemplate.directoryAndFileName()
|
||||
let generated = "\(directory)/GENERATED-\(filename.dropSuffix(".gyb"))"
|
||||
|
||||
if !allCreatedAndModified.contains(generated) {
|
||||
fail("""
|
||||
A template \(modifiedTemplate) was modified, but the file \(generated) \
|
||||
was not regenerated.
|
||||
|
||||
Run `make gyb` from the root of the project and commit the changes.
|
||||
""")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
// Fail if the committer modified a generated file.
|
||||
// A template should be modified instead.
|
||||
|
||||
for modifiedGeneratedFile in danger.git.modifiedFiles
|
||||
where modifiedGeneratedFile.contains("GENERATED-")
|
||||
{
|
||||
let template = modifiedGeneratedFile
|
||||
.replacingOccurrences(of: "GENERATED-", with: "") + ".gyb"
|
||||
|
||||
if !danger.git.modifiedFiles.contains(template) {
|
||||
fail("""
|
||||
A generated file \(modifiedGeneratedFile) was modified, but \
|
||||
the template it was generated from was not modified.
|
||||
|
||||
Please modify the template \(template) instead, \
|
||||
run `make gyb` from the root of the project and commit the changes.
|
||||
""")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SwiftLint.lint(.all(directory: nil),
|
||||
inline: true,
|
||||
configFile: ".swiftlint.yml",
|
||||
strict: true)
|
||||
|
||||
if danger.warnings.isEmpty, danger.fails.isEmpty {
|
||||
markdown("LGTM")
|
||||
}
|
||||
-162
@@ -1,162 +0,0 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.1)
|
||||
addressable (2.7.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
atomos (0.1.3)
|
||||
babosa (1.0.3)
|
||||
claide (1.0.3)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
commander-fastlane (4.4.6)
|
||||
highline (~> 1.7.2)
|
||||
declarative (0.0.10)
|
||||
declarative-option (0.1.0)
|
||||
digest-crc (0.4.1)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
dotenv (2.7.5)
|
||||
emoji_regex (1.0.1)
|
||||
excon (0.71.0)
|
||||
faraday (0.17.0)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
faraday-cookie_jar (0.0.6)
|
||||
faraday (>= 0.7.4)
|
||||
http-cookie (~> 1.0.0)
|
||||
faraday_middleware (0.13.1)
|
||||
faraday (>= 0.7.4, < 1.0)
|
||||
fastimage (2.1.7)
|
||||
fastlane (2.134.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.3, < 3.0.0)
|
||||
babosa (>= 1.0.2, < 2.0.0)
|
||||
bundler (>= 1.12.0, < 3.0.0)
|
||||
colored
|
||||
commander-fastlane (>= 4.4.6, < 5.0.0)
|
||||
dotenv (>= 2.1.1, < 3.0.0)
|
||||
emoji_regex (>= 0.1, < 2.0)
|
||||
excon (>= 0.45.0, < 1.0.0)
|
||||
faraday (~> 0.17)
|
||||
faraday-cookie_jar (~> 0.0.6)
|
||||
faraday_middleware (~> 0.13.1)
|
||||
fastimage (>= 2.1.0, < 3.0.0)
|
||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||
google-api-client (>= 0.21.2, < 0.24.0)
|
||||
google-cloud-storage (>= 1.15.0, < 2.0.0)
|
||||
highline (>= 1.7.2, < 2.0.0)
|
||||
json (< 3.0.0)
|
||||
jwt (~> 2.1.0)
|
||||
mini_magick (>= 4.9.4, < 5.0.0)
|
||||
multi_xml (~> 0.5)
|
||||
multipart-post (~> 2.0.0)
|
||||
plist (>= 3.1.0, < 4.0.0)
|
||||
public_suffix (~> 2.0.0)
|
||||
rubyzip (>= 1.3.0, < 2.0.0)
|
||||
security (= 0.1.3)
|
||||
simctl (~> 1.6.3)
|
||||
slack-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-table (>= 1.4.5, < 2.0.0)
|
||||
tty-screen (>= 0.6.3, < 1.0.0)
|
||||
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||
word_wrap (~> 1.0.0)
|
||||
xcodeproj (>= 1.8.1, < 2.0.0)
|
||||
xcpretty (~> 0.3.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3)
|
||||
gh_inspector (1.1.3)
|
||||
google-api-client (0.23.9)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.5, < 0.7.0)
|
||||
httpclient (>= 2.8.1, < 3.0)
|
||||
mime-types (~> 3.0)
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.0)
|
||||
signet (~> 0.9)
|
||||
google-cloud-core (1.3.2)
|
||||
google-cloud-env (~> 1.0)
|
||||
google-cloud-env (1.2.1)
|
||||
faraday (~> 0.11)
|
||||
google-cloud-storage (1.16.0)
|
||||
digest-crc (~> 0.4)
|
||||
google-api-client (~> 0.23)
|
||||
google-cloud-core (~> 1.2)
|
||||
googleauth (>= 0.6.2, < 0.10.0)
|
||||
googleauth (0.6.7)
|
||||
faraday (~> 0.12)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (~> 0.7)
|
||||
highline (1.7.10)
|
||||
http-cookie (1.0.3)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
json (2.3.1)
|
||||
jwt (2.1.0)
|
||||
memoist (0.16.1)
|
||||
mime-types (3.3)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2019.1009)
|
||||
mini_magick (4.9.5)
|
||||
multi_json (1.14.1)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.0.0)
|
||||
nanaimo (0.2.6)
|
||||
naturally (2.2.0)
|
||||
os (1.0.1)
|
||||
plist (3.5.0)
|
||||
public_suffix (2.0.5)
|
||||
representable (3.0.4)
|
||||
declarative (< 0.1.0)
|
||||
declarative-option (< 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
rouge (2.0.7)
|
||||
rubyzip (1.3.0)
|
||||
security (0.1.3)
|
||||
signet (0.11.0)
|
||||
addressable (~> 2.3)
|
||||
faraday (~> 0.9)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
multi_json (~> 1.10)
|
||||
simctl (1.6.6)
|
||||
CFPropertyList
|
||||
naturally
|
||||
slack-notifier (2.3.2)
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (1.8.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
tty-cursor (0.7.0)
|
||||
tty-screen (0.7.0)
|
||||
tty-spinner (0.9.1)
|
||||
tty-cursor (~> 0.7)
|
||||
uber (0.1.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.6)
|
||||
unicode-display_width (1.6.0)
|
||||
word_wrap (1.0.0)
|
||||
xcode-install (2.6.2)
|
||||
claide (>= 0.9.1, < 1.1.0)
|
||||
fastlane (>= 2.1.0, < 3.0.0)
|
||||
xcodeproj (1.13.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.2.6)
|
||||
xcpretty (0.3.0)
|
||||
rouge (~> 2.0.7)
|
||||
xcpretty-travis-formatter (1.0.0)
|
||||
xcpretty (~> 0.2, >= 0.0.7)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
xcode-install
|
||||
|
||||
BUNDLED WITH
|
||||
2.0.1
|
||||
+3
-3
@@ -1,17 +1,17 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = "OpenCombine"
|
||||
spec.version = "0.12.0"
|
||||
spec.version = "0.14.0"
|
||||
spec.summary = "Open source implementation of Apple's Combine framework for processing values over time."
|
||||
|
||||
spec.description = <<-DESC
|
||||
An open source implementation of Apple's Combine framework for processing values over time.
|
||||
DESC
|
||||
|
||||
spec.homepage = "https://github.com/broadwaylamb/OpenCombine/"
|
||||
spec.homepage = "https://github.com/OpenCombine/OpenCombine/"
|
||||
spec.license = "MIT"
|
||||
|
||||
spec.authors = { "Sergej Jaskiewicz" => "jaskiewiczs@icloud.com" }
|
||||
spec.source = { :git => "https://github.com/broadwaylamb/OpenCombine.git", :tag => "#{spec.version}" }
|
||||
spec.source = { :git => "https://github.com/OpenCombine/OpenCombine.git", :tag => "#{spec.version}" }
|
||||
|
||||
spec.swift_version = "5.0"
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = "OpenCombineDispatch"
|
||||
spec.version = "0.12.0"
|
||||
spec.version = "0.14.0"
|
||||
spec.summary = "OpenCombine + Dispatch interoperability"
|
||||
|
||||
spec.description = <<-DESC
|
||||
Extends `DispatchQueue` with conformance to the `Scheduler` protocol
|
||||
DESC
|
||||
|
||||
spec.homepage = "https://github.com/broadwaylamb/OpenCombine/"
|
||||
spec.homepage = "https://github.com/OpenCombine/OpenCombine/"
|
||||
spec.license = "MIT"
|
||||
|
||||
spec.authors = { "Sergej Jaskiewicz" => "jaskiewiczs@icloud.com" }
|
||||
spec.source = { :git => "https://github.com/broadwaylamb/OpenCombine.git", :tag => "#{spec.version}" }
|
||||
spec.source = { :git => "https://github.com/OpenCombine/OpenCombine.git", :tag => "#{spec.version}" }
|
||||
|
||||
spec.swift_version = "5.0"
|
||||
|
||||
@@ -21,5 +21,5 @@ Pod::Spec.new do |spec|
|
||||
spec.tvos.deployment_target = "9.0"
|
||||
|
||||
spec.source_files = "Sources/OpenCombineDispatch/**/*.swift"
|
||||
spec.dependency "OpenCombine", '>= 0.10.2'
|
||||
spec.dependency "OpenCombine", '>= 0.13.0'
|
||||
end
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = "OpenCombineFoundation"
|
||||
spec.version = "0.12.0"
|
||||
spec.version = "0.14.0"
|
||||
spec.summary = "OpenCombine + OpenCombineFoundation interoperability"
|
||||
|
||||
spec.description = <<-DESC
|
||||
Adds publishers to Foundation types like NotificationCenter, URLSession etc.
|
||||
DESC
|
||||
|
||||
spec.homepage = "https://github.com/broadwaylamb/OpenCombine/"
|
||||
spec.homepage = "https://github.com/OpenCombine/OpenCombine/"
|
||||
spec.license = "MIT"
|
||||
|
||||
spec.authors = { "Sergej Jaskiewicz" => "jaskiewiczs@icloud.com" }
|
||||
spec.source = { :git => "https://github.com/broadwaylamb/OpenCombine.git", :tag => "#{spec.version}" }
|
||||
spec.source = { :git => "https://github.com/OpenCombine/OpenCombine.git", :tag => "#{spec.version}" }
|
||||
|
||||
spec.swift_version = "5.0"
|
||||
|
||||
@@ -21,5 +21,5 @@ Pod::Spec.new do |spec|
|
||||
spec.tvos.deployment_target = "9.0"
|
||||
|
||||
spec.source_files = "Sources/OpenCombineFoundation/**/*.swift"
|
||||
spec.dependency "OpenCombine", '>= 0.10.2'
|
||||
spec.dependency "OpenCombine", '>= 0.13.0'
|
||||
end
|
||||
|
||||
+8
-9
@@ -1,4 +1,4 @@
|
||||
// swift-tools-version:5.3
|
||||
// swift-tools-version:5.5
|
||||
|
||||
import PackageDescription
|
||||
|
||||
@@ -6,13 +6,14 @@ import PackageDescription
|
||||
// See: https://bugs.swift.org/browse/SR-13814
|
||||
let supportedPlatforms: [Platform] = [
|
||||
.macOS,
|
||||
.macCatalyst,
|
||||
.iOS,
|
||||
.watchOS,
|
||||
.tvOS,
|
||||
.driverKit,
|
||||
.linux,
|
||||
.android,
|
||||
// Disable Windows because of https://bugs.swift.org/browse/SR-13817
|
||||
// .windows,
|
||||
.windows,
|
||||
.wasi,
|
||||
]
|
||||
|
||||
@@ -33,6 +34,8 @@ let package = Package(
|
||||
condition: .when(platforms: supportedPlatforms.except([.wasi])))
|
||||
],
|
||||
exclude: [
|
||||
"RootProtocols.swift.gyb",
|
||||
"Concurrency/Publisher+Concurrency.swift.gyb",
|
||||
"Publishers/Publishers.Encode.swift.gyb",
|
||||
"Publishers/Publishers.MapKeyPath.swift.gyb",
|
||||
"Publishers/Publishers.Catch.swift.gyb"
|
||||
@@ -73,17 +76,13 @@ let package = Package(
|
||||
]
|
||||
)
|
||||
],
|
||||
cxxLanguageStandard: .cxx1z
|
||||
cxxLanguageStandard: .cxx17
|
||||
)
|
||||
|
||||
// MARK: Helpers
|
||||
|
||||
extension Array where Element == Platform {
|
||||
func except(_ exceptions: [Platform]) -> [Platform] {
|
||||
// See: https://bugs.swift.org/browse/SR-13813
|
||||
let exceptionsDescriptions = exceptions.map(String.init(describing:))
|
||||
return filter { platform in
|
||||
!exceptionsDescriptions.contains(String(describing: platform))
|
||||
}
|
||||
return filter { !exceptions.contains($0) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
// swift-tools-version:5.3
|
||||
|
||||
import PackageDescription
|
||||
|
||||
// This list should be updated whenever SwiftPM adds support for a new platform.
|
||||
// See: https://bugs.swift.org/browse/SR-13814
|
||||
let supportedPlatforms: [Platform] = [
|
||||
.macOS,
|
||||
.iOS,
|
||||
.watchOS,
|
||||
.tvOS,
|
||||
.linux,
|
||||
.android,
|
||||
// Disable Windows because of https://bugs.swift.org/browse/SR-13817
|
||||
// .windows,
|
||||
.wasi,
|
||||
]
|
||||
|
||||
let package = Package(
|
||||
name: "OpenCombine",
|
||||
products: [
|
||||
.library(name: "OpenCombine", targets: ["OpenCombine"]),
|
||||
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
|
||||
.library(name: "OpenCombineFoundation", targets: ["OpenCombineFoundation"]),
|
||||
.library(name: "OpenCombineShim", targets: ["OpenCombineShim"]),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "COpenCombineHelpers"),
|
||||
.target(
|
||||
name: "OpenCombine",
|
||||
dependencies: [
|
||||
.target(name: "COpenCombineHelpers",
|
||||
condition: .when(platforms: supportedPlatforms.except([.wasi])))
|
||||
],
|
||||
exclude: [
|
||||
"RootProtocols.swift.gyb",
|
||||
"Concurrency/Publisher+Concurrency.swift.gyb",
|
||||
"Publishers/Publishers.Encode.swift.gyb",
|
||||
"Publishers/Publishers.MapKeyPath.swift.gyb",
|
||||
"Publishers/Publishers.Catch.swift.gyb"
|
||||
],
|
||||
swiftSettings: [.define("WASI", .when(platforms: [.wasi]))]
|
||||
),
|
||||
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
|
||||
.target(
|
||||
name: "OpenCombineFoundation",
|
||||
dependencies: [
|
||||
"OpenCombine",
|
||||
.target(name: "COpenCombineHelpers",
|
||||
condition: .when(platforms: supportedPlatforms.except([.wasi])))
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "OpenCombineShim",
|
||||
dependencies: [
|
||||
"OpenCombine",
|
||||
.target(name: "OpenCombineDispatch",
|
||||
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
|
||||
.target(name: "OpenCombineFoundation",
|
||||
condition: .when(platforms: supportedPlatforms.except([.wasi])))
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "OpenCombineTests",
|
||||
dependencies: [
|
||||
"OpenCombine",
|
||||
.target(name: "OpenCombineDispatch",
|
||||
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
|
||||
.target(name: "OpenCombineFoundation",
|
||||
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
|
||||
],
|
||||
swiftSettings: [
|
||||
.unsafeFlags(["-enable-testing"]),
|
||||
.define("WASI", .when(platforms: [.wasi]))
|
||||
]
|
||||
)
|
||||
],
|
||||
cxxLanguageStandard: .cxx1z
|
||||
)
|
||||
|
||||
// MARK: Helpers
|
||||
|
||||
extension Array where Element == Platform {
|
||||
func except(_ exceptions: [Platform]) -> [Platform] {
|
||||
// See: https://bugs.swift.org/browse/SR-13813
|
||||
let exceptionsDescriptions = exceptions.map(String.init(describing:))
|
||||
return filter { platform in
|
||||
!exceptionsDescriptions.contains(String(describing: platform))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// swift-tools-version:5.4
|
||||
|
||||
import PackageDescription
|
||||
|
||||
// This list should be updated whenever SwiftPM adds support for a new platform.
|
||||
// See: https://bugs.swift.org/browse/SR-13814
|
||||
let supportedPlatforms: [Platform] = [
|
||||
.macOS,
|
||||
.iOS,
|
||||
.watchOS,
|
||||
.tvOS,
|
||||
.linux,
|
||||
.android,
|
||||
.windows,
|
||||
.wasi,
|
||||
]
|
||||
|
||||
let package = Package(
|
||||
name: "OpenCombine",
|
||||
products: [
|
||||
.library(name: "OpenCombine", targets: ["OpenCombine"]),
|
||||
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
|
||||
.library(name: "OpenCombineFoundation", targets: ["OpenCombineFoundation"]),
|
||||
.library(name: "OpenCombineShim", targets: ["OpenCombineShim"]),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "COpenCombineHelpers"),
|
||||
.target(
|
||||
name: "OpenCombine",
|
||||
dependencies: [
|
||||
.target(name: "COpenCombineHelpers",
|
||||
condition: .when(platforms: supportedPlatforms.except([.wasi])))
|
||||
],
|
||||
exclude: [
|
||||
"RootProtocols.swift.gyb",
|
||||
"Concurrency/Publisher+Concurrency.swift.gyb",
|
||||
"Publishers/Publishers.Encode.swift.gyb",
|
||||
"Publishers/Publishers.MapKeyPath.swift.gyb",
|
||||
"Publishers/Publishers.Catch.swift.gyb"
|
||||
],
|
||||
swiftSettings: [.define("WASI", .when(platforms: [.wasi]))]
|
||||
),
|
||||
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
|
||||
.target(
|
||||
name: "OpenCombineFoundation",
|
||||
dependencies: [
|
||||
"OpenCombine",
|
||||
.target(name: "COpenCombineHelpers",
|
||||
condition: .when(platforms: supportedPlatforms.except([.wasi])))
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "OpenCombineShim",
|
||||
dependencies: [
|
||||
"OpenCombine",
|
||||
.target(name: "OpenCombineDispatch",
|
||||
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
|
||||
.target(name: "OpenCombineFoundation",
|
||||
condition: .when(platforms: supportedPlatforms.except([.wasi])))
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "OpenCombineTests",
|
||||
dependencies: [
|
||||
"OpenCombine",
|
||||
.target(name: "OpenCombineDispatch",
|
||||
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
|
||||
.target(name: "OpenCombineFoundation",
|
||||
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
|
||||
],
|
||||
swiftSettings: [
|
||||
.unsafeFlags(["-enable-testing"]),
|
||||
.define("WASI", .when(platforms: [.wasi]))
|
||||
]
|
||||
)
|
||||
],
|
||||
cxxLanguageStandard: .cxx17
|
||||
)
|
||||
|
||||
// MARK: Helpers
|
||||
|
||||
extension Array where Element == Platform {
|
||||
func except(_ exceptions: [Platform]) -> [Platform] {
|
||||
return filter { !exceptions.contains($0) }
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,20 @@
|
||||
# OpenCombine
|
||||
[](https://circleci.com/gh/OpenCombine/OpenCombine)
|
||||
[](https://codecov.io/gh/OpenCombine/OpenCombine)
|
||||

|
||||

|
||||

|
||||
[<img src="https://img.shields.io/badge/slack-OpenCombine-yellow.svg?logo=slack">](https://join.slack.com/t/opencombine/shared_invite/enQtNzE2MjE5NzkxODI0LTYxMjkzNDUxZWViZWI1Njc2YjBhODgxNjRjOTdkZTcxOGU2ZjJjZjYxMGI3NWZkN2RkNGFmZTUzNmU3MGE2ZWM)
|
||||
[](https://cocoapods.org/pods/OpenCombine)
|
||||
|
||||
Open-source implementation of Apple's [Combine](https://developer.apple.com/documentation/combine) framework for processing values over time.
|
||||
|
||||
The main goal of this project is to provide a compatible, reliable and efficient implementation which can be used on Apple's operating systems before macOS 10.15 and iOS 13, as well as Linux and WebAssembly.
|
||||
The main goal of this project is to provide a compatible, reliable and efficient implementation which can be used on Apple's operating systems before macOS 10.15 and iOS 13, as well as Linux, Windows and WebAssembly.
|
||||
|
||||
| **CI Status** |
|
||||
|---|
|
||||
|[](https://github.com/OpenCombine/OpenCombine/actions/workflows/compatibility_tests.yml)|
|
||||
|[](https://github.com/OpenCombine/OpenCombine/actions/workflows/macos.yml)|
|
||||
|[](https://github.com/OpenCombine/OpenCombine/actions/workflows/ubuntu.yml)|
|
||||
|[](https://github.com/OpenCombine/OpenCombine/actions/workflows/windows.yml)|
|
||||
|[](https://github.com/OpenCombine/OpenCombine/actions/workflows/wasm.yml)|
|
||||
|
||||
|
||||
### Installation
|
||||
`OpenCombine` contains three public targets: `OpenCombine`, `OpenCombineFoundation` and `OpenCombineDispatch` (the fourth one, `COpenCombineHelpers`, is considered private. Don't import it in your projects).
|
||||
@@ -26,7 +32,7 @@ To add `OpenCombine` to your [SwiftPM](https://swift.org/package-manager/) packa
|
||||
|
||||
```swift
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.12.0")
|
||||
.package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.14.0")
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
@@ -54,72 +60,11 @@ To do so, open Xcode, use **File** → **Swift Packages** → **Add Package Depe
|
||||
To add `OpenCombine` to a project using [CocoaPods](https://cocoapods.org/), add `OpenCombine` and `OpenCombineDispatch` to the list of target dependencies in your `Podfile`.
|
||||
|
||||
```ruby
|
||||
pod 'OpenCombine', '~> 0.12.0'
|
||||
pod 'OpenCombineDispatch', '~> 0.12.0'
|
||||
pod 'OpenCombineFoundation', '~> 0.12.0'
|
||||
pod 'OpenCombine', '~> 0.14.0'
|
||||
pod 'OpenCombineDispatch', '~> 0.14.0'
|
||||
pod 'OpenCombineFoundation', '~> 0.14.0'
|
||||
```
|
||||
|
||||
### Contributing
|
||||
|
||||
In order to work on this project you will need Xcode 10.2 and Swift 5.0 or later.
|
||||
|
||||
Please refer to the [issue #1](https://github.com/OpenCombine/OpenCombine/issues/1) for the list of operators that remain unimplemented, as well as the [RemainingCombineInterface.swift](https://github.com/OpenCombine/OpenCombine/blob/master/RemainingCombineInterface.swift) file. The latter contains the generated interface of Apple's Combine from the latest Xcode 11 version. When the functionality is implemented in OpenCombine, it should be removed from the RemainingCombineInterface.swift file.
|
||||
|
||||
You can refer to [this repo](https://github.com/OpenCombine/combine-interfaces) to observe Apple's Combine API and documentation changes between different Xcode (beta) versions.
|
||||
|
||||
You can run compatibility tests against Apple's Combine. In order to do that you will need either macOS 10.14 with iOS 13 simulator installed (since the only way we can get Apple's Combine on macOS 10.14 is using the simulator), or macOS 10.15 (Apple's Combine is bundled with the OS). Execute the following command from the root of the package:
|
||||
|
||||
```
|
||||
$ make test-compatibility
|
||||
```
|
||||
|
||||
Or enable the `-DOPENCOMBINE_COMPATIBILITY_TEST` compiler flag in Xcode's build settings. Note that on iOS only the latter will work.
|
||||
|
||||
> NOTE: Before starting to work on some feature, please consult the [GitHub project](https://github.com/OpenCombine/OpenCombine/projects/2) to make sure that nobody's already making progress on the same feature! If not, then please create a draft PR to indicate that you're beginning your work.
|
||||
|
||||
#### Releasing a new version
|
||||
|
||||
1. Create a new branch from master and call it `release/<major>.<minor>.<patch>`.
|
||||
1. Replace the usages of the old version in `README.md` with the new version (make sure to check the [Swift Package Manager](#swift-package-manager) and [CocoaPods](#cocoapods) sections).
|
||||
1. Bump the version in `OpenCombine.podspec`, `OpenCombineDispatch.podspec` and `OpenCombineFoundation.podspec`. In the latter two you will also need to set the `spec.dependency "OpenCombine"` property to the **previous** version. Why? Because otherwise the `pod lib lint` command that we run on our regular CI will fail when validating the `OpenCombineDispatch` and `OpenCombineFoundation` podspecs, since the dependencies are not yet in the trunk. If we set the dependencies to the previous version (which is already in the trunk), everything will be fine. This is purely to make the CI work. The clients will not experience any issues, since the version is specified as `>=`.
|
||||
1. Create a pull request to master for the release branch and make sure the CI passes.
|
||||
1. Merge the pull request.
|
||||
1. In the GitHub web interface on the [releases](https://github.com/OpenCombine/OpenCombine/releases) page, click the **Draft a new release** button.
|
||||
1. The **Tag version** and **Release title** fields should be filled with the version number.
|
||||
1. The description of the release should be consistent with the previous releases. It is a good practice to divide the description into several sections: additions, bugfixes, known issues etc. Also, be sure to mention the nicknames of the contributors of the new release.
|
||||
1. Publish the release.
|
||||
1. Switch to the master branch and pull the changes.
|
||||
1. Push the release to CocoaPods trunk. For that, execute the following commands:
|
||||
|
||||
```
|
||||
pod trunk push OpenCombine.podspec --verbose --allow-warnings
|
||||
pod trunk push OpenCombineDispatch.podspec --verbose --allow-warnings
|
||||
pod trunk push OpenCombineFoundation.podspec --verbose --allow-warnings
|
||||
```
|
||||
|
||||
Note that you need to be one of the owners of the pod for that.
|
||||
|
||||
#### GYB
|
||||
|
||||
Some publishers in OpenCombine (like `Publishers.MapKeyPath`, `Publishers.Merge`) exist in several
|
||||
different flavors in order to support several arities. For example, there are also `Publishers.MapKeyPath2`
|
||||
and `Publishers.MapKeyPath3`, which are very similar but different enough that Swift's type system
|
||||
can't help us here (because there's no support for variadic generics). Maintaining multiple instances of
|
||||
those generic types is tedious and error-prone (they can get out of sync), so we use the GYB tool for
|
||||
generating those instances from a template.
|
||||
|
||||
GYB is a Python script that evaluates Python code written inside a template file, so it's very flexible —
|
||||
templates can be arbitrarily complex. There is a good article about GYB on
|
||||
[NSHipster](https://nshipster.com/swift-gyb/).
|
||||
|
||||
GYB is part of the [Swift Open Source Project](https://github.com/apple/swift/blob/master/utils/gyb.py)
|
||||
and can be distributed under the same license as Swift itself.
|
||||
|
||||
GYB template files have the `.gyb` extension. Run `make gyb` to generate Swift code from those
|
||||
templates. The generated files are prefixed with `GENERATED-` and are checked into source control. Those
|
||||
files should never be edited directly. Instead, the `.gyb` template should be edited, and after that the files
|
||||
should be regenerated using `make gyb`.
|
||||
|
||||
#### Debugger Support
|
||||
|
||||
The file `opencombine_lldb.py` defines some `lldb` type summaries for easier debugging. These type summaries improve the way `lldb` and Xcode display some OpenCombine values.
|
||||
@@ -132,3 +77,7 @@ Currently, `opencombine_lldb.py` defines type summaries for these types:
|
||||
|
||||
- `Subscribers.Demand`
|
||||
- That's all for now.
|
||||
|
||||
### Contributing
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
+110
-132
@@ -636,22 +636,31 @@ extension Publisher {
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher created by applying the zip function to two upstream publishers.
|
||||
public struct Zip<A, B> : Publisher where A : Publisher, B : Publisher, A.Failure == B.Failure {
|
||||
/// A publisher that attempts to recreate its subscription to a failed upstream publisher.
|
||||
public struct Retry<Upstream> : Publisher where Upstream : Publisher {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = (A.Output, B.Output)
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = A.Failure
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
public let a: A
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
public let b: B
|
||||
/// 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 init(_ a: A, _ 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?)
|
||||
|
||||
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
|
||||
///
|
||||
@@ -659,142 +668,111 @@ 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, B.Failure == S.Failure, S.Input == (A.Output, B.Output)
|
||||
}
|
||||
|
||||
/// A publisher created by applying the zip function to three upstream publishers.
|
||||
public struct Zip3<A, B, C> : Publisher where A : Publisher, B : Publisher, C : Publisher, A.Failure == B.Failure, B.Failure == C.Failure {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = (A.Output, B.Output, C.Output)
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = A.Failure
|
||||
|
||||
public let a: A
|
||||
|
||||
public let b: B
|
||||
|
||||
public let c: C
|
||||
|
||||
public init(_ a: A, _ b: B, _ c: C)
|
||||
|
||||
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
|
||||
///
|
||||
/// - SeeAlso: `subscribe(_:)`
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<S>(subscriber: S) where S : Subscriber, C.Failure == S.Failure, S.Input == (A.Output, B.Output, C.Output)
|
||||
}
|
||||
|
||||
/// A publisher created by applying the zip function to four upstream publishers.
|
||||
public struct Zip4<A, B, C, D> : Publisher where A : Publisher, B : Publisher, C : Publisher, D : Publisher, A.Failure == B.Failure, B.Failure == C.Failure, C.Failure == D.Failure {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = (A.Output, B.Output, C.Output, D.Output)
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = A.Failure
|
||||
|
||||
public let a: A
|
||||
|
||||
public let b: B
|
||||
|
||||
public let c: C
|
||||
|
||||
public let d: D
|
||||
|
||||
public init(_ a: A, _ b: B, _ c: C, _ d: D)
|
||||
|
||||
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
|
||||
///
|
||||
/// - SeeAlso: `subscribe(_:)`
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<S>(subscriber: S) where S : Subscriber, D.Failure == S.Failure, S.Input == (A.Output, B.Output, C.Output, D.Output)
|
||||
public func receive<S>(subscriber: S) where S : Subscriber, Upstream.Failure == S.Failure, Upstream.Output == S.Input
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Combine elements from another publisher and deliver pairs of elements as tuples.
|
||||
/// Attempts to recreate a failed subscription with the upstream publisher using a specified number of attempts to establish the connection.
|
||||
///
|
||||
/// 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 won’t emit a tuple with event `b` until `P2` emits another event.
|
||||
/// If either upstream publisher finishes successfuly or fails with an error, the zipped publisher does the same.
|
||||
///
|
||||
/// - Parameter other: Another publisher.
|
||||
/// - Returns: A publisher that emits pairs of elements from the upstream publishers as tuples.
|
||||
public func zip<P>(_ other: P) -> Publishers.Zip<Self, P> where P : Publisher, Self.Failure == P.Failure
|
||||
/// 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>
|
||||
}
|
||||
|
||||
/// 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 won’t emit a tuple with event `b` until `P2` emits another event.
|
||||
/// If either upstream publisher finishes successfuly or fails with an error, the zipped publisher does the same.
|
||||
///
|
||||
/// - Parameter other: Another publisher.
|
||||
/// - transform: A closure that receives the most recent value from each publisher and returns a new value to publish.
|
||||
/// - Returns: A publisher that emits pairs of elements from the upstream publishers as tuples.
|
||||
public func zip<P, T>(_ other: P, _ transform: @escaping (Self.Output, P.Output) -> T) -> Publishers.Map<Publishers.Zip<Self, P>, T> where P : Publisher, Self.Failure == P.Failure
|
||||
extension Publisher {
|
||||
|
||||
/// Combine elements from two other publishers and deliver groups of elements as tuples.
|
||||
/// Attempts to recreate a failed subscription with the upstream publisher using a specified number of attempts to establish the connection.
|
||||
///
|
||||
/// 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 won’t 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.
|
||||
/// 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 {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The interval in which to find and emit the most recent element.
|
||||
public let interval: Context.SchedulerTimeType.Stride
|
||||
|
||||
/// The scheduler on which to publish elements.
|
||||
public let scheduler: Context
|
||||
|
||||
/// A Boolean value indicating whether to publish the most recent element.
|
||||
///
|
||||
/// If `false`, the publisher emits the first element received during the interval.
|
||||
public let latest: Bool
|
||||
|
||||
public init(upstream: Upstream, interval: Context.SchedulerTimeType.Stride, scheduler: Context, latest: Bool)
|
||||
|
||||
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
|
||||
///
|
||||
/// - SeeAlso: `subscribe(_:)`
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<S>(subscriber: S) where S : Subscriber, Upstream.Failure == S.Failure, Upstream.Output == S.Input
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Publishes either the most-recent or first element published by the upstream
|
||||
/// publisher in the specified time interval.
|
||||
///
|
||||
/// Use `throttle(for:scheduler:latest:`` to selectively republish elements from
|
||||
/// an upstream publisher during an interval you specify. Other elements received from
|
||||
/// the upstream in the throttling interval aren’t republished.
|
||||
///
|
||||
/// In the example below, a `Timer.TimerPublisher` produces elements on 3-second
|
||||
/// intervals; the `throttle(for:scheduler:latest:)` operator delivers the first
|
||||
/// event, then republishes only the latest event in the following ten second
|
||||
/// intervals:
|
||||
///
|
||||
/// cancellable = Timer.publish(every: 3.0, on: .main, in: .default)
|
||||
/// .autoconnect()
|
||||
/// .print("\(Date().description)")
|
||||
/// .throttle(for: 10.0, scheduler: RunLoop.main, latest: true)
|
||||
/// .sink(
|
||||
/// receiveCompletion: { print ("Completion: \($0).") },
|
||||
/// receiveValue: { print("Received Timestamp \($0).") }
|
||||
/// )
|
||||
///
|
||||
/// // Prints:
|
||||
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:26:57 +0000)
|
||||
/// // Received Timestamp 2020-03-19 18:26:57 +0000.
|
||||
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:27:00 +0000)
|
||||
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:27:03 +0000)
|
||||
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:27:06 +0000)
|
||||
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:27:09 +0000)
|
||||
/// // Received Timestamp 2020-03-19 18:27:09 +0000.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - publisher1: A second publisher.
|
||||
/// - publisher2: A third publisher.
|
||||
/// - Returns: A publisher that emits groups of elements from the upstream publishers as tuples.
|
||||
public func zip<P, Q>(_ publisher1: P, _ publisher2: Q) -> Publishers.Zip3<Self, P, Q> where P : Publisher, Q : Publisher, Self.Failure == P.Failure, P.Failure == Q.Failure
|
||||
|
||||
/// Combine elements from two other publishers and deliver a transformed output.
|
||||
///
|
||||
/// The returned publisher waits until all three publishers have emitted an event, then delivers the oldest unconsumed event from each publisher as a tuple to the subscriber.
|
||||
/// For example, if publisher `P1` emits elements `a` and `b`, and publisher `P2` emits elements `c` and `d`, and publisher `P3` emits the event `e`, the zip publisher emits the tuple `(a, c, e)`. It won’t emit a tuple with elements `b` or `d` until `P3` emits another event.
|
||||
/// If any upstream publisher finishes successfuly or fails with an error, the zipped publisher does the same.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - publisher1: A second publisher.
|
||||
/// - publisher2: A third publisher.
|
||||
/// - transform: A closure that receives the most recent value from each publisher and returns a new value to publish.
|
||||
/// - Returns: A publisher that emits groups of elements from the upstream publishers as tuples.
|
||||
public func zip<P, Q, T>(_ publisher1: P, _ publisher2: Q, _ transform: @escaping (Self.Output, P.Output, Q.Output) -> T) -> Publishers.Map<Publishers.Zip3<Self, P, Q>, T> where P : Publisher, Q : Publisher, Self.Failure == P.Failure, P.Failure == Q.Failure
|
||||
|
||||
/// Combine elements from three other publishers and deliver groups of elements as tuples.
|
||||
///
|
||||
/// The returned publisher waits until all four publishers have emitted an event, then delivers the oldest unconsumed event from each publisher as a tuple to the subscriber.
|
||||
/// For example, if publisher `P1` emits elements `a` and `b`, and publisher `P2` emits elements `c` and `d`, and publisher `P3` emits the elements `e` and `f`, and publisher `P4` emits the event `g`, the zip publisher emits the tuple `(a, c, e, g)`. It won’t emit a tuple with elements `b`, `d`, or `f` until `P4` emits another event.
|
||||
/// If any upstream publisher finishes successfuly or fails with an error, the zipped publisher does the same.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - publisher1: A second publisher.
|
||||
/// - publisher2: A third publisher.
|
||||
/// - publisher3: A fourth publisher.
|
||||
/// - Returns: A publisher that emits groups of elements from the upstream publishers as tuples.
|
||||
public func zip<P, Q, R>(_ publisher1: P, _ publisher2: Q, _ publisher3: R) -> Publishers.Zip4<Self, P, Q, R> where P : Publisher, Q : Publisher, R : Publisher, Self.Failure == P.Failure, P.Failure == Q.Failure, Q.Failure == R.Failure
|
||||
|
||||
/// Combine elements from three other publishers and deliver a transformed output.
|
||||
///
|
||||
/// The returned publisher waits until all four publishers have emitted an event, then delivers the oldest unconsumed event from each publisher as a tuple to the subscriber.
|
||||
/// For example, if publisher `P1` emits elements `a` and `b`, and publisher `P2` emits elements `c` and `d`, and publisher `P3` emits the elements `e` and `f`, and publisher `P4` emits the event `g`, the zip publisher emits the tuple `(a, c, e, g)`. It won’t emit a tuple with elements `b`, `d`, or `f` until `P4` emits another event.
|
||||
/// If any upstream publisher finishes successfuly or fails with an error, the zipped publisher does the same.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - publisher1: A second publisher.
|
||||
/// - publisher2: A third publisher.
|
||||
/// - publisher3: A fourth publisher.
|
||||
/// - transform: A closure that receives the most recent value from each publisher and returns a new value to publish.
|
||||
/// - Returns: A publisher that emits groups of elements from the upstream publishers as tuples.
|
||||
public func zip<P, Q, R, T>(_ publisher1: P, _ publisher2: Q, _ publisher3: R, _ transform: @escaping (Self.Output, P.Output, Q.Output, R.Output) -> T) -> Publishers.Map<Publishers.Zip4<Self, P, Q, R>, T> where P : Publisher, Q : Publisher, R : Publisher, Self.Failure == P.Failure, P.Failure == Q.Failure, Q.Failure == R.Failure
|
||||
/// - 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
|
||||
}
|
||||
|
||||
extension Publishers.CombineLatest : Equatable where A : Equatable, B : Equatable {
|
||||
|
||||
@@ -10,13 +10,31 @@
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <system_error>
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
|
||||
#if __has_include(<pthread.h>)
|
||||
# include <pthread.h>
|
||||
# define OPENCOMBINE_HAS_PTHREAD 1
|
||||
#else
|
||||
# define OPENCOMBINE_HAS_PTHREAD 0
|
||||
#endif
|
||||
|
||||
#if __has_include(<signal.h>)
|
||||
# include <signal.h>
|
||||
# define OPENCOMBINE_HAS_SIGNAL_HANDLING 1
|
||||
#else
|
||||
# define OPENCOMBINE_HAS_SIGNAL_HANDLING 0
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
# include <windows.h>
|
||||
#endif
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <os/lock.h>
|
||||
#endif // __APPLE__
|
||||
|
||||
#include <mutex>
|
||||
|
||||
// Throwing exceptions through language boundaries is undefined behavior,
|
||||
// so we must catch all of them in our extern "C" functions.
|
||||
#define OPENCOMBINE_HANDLE_EXCEPTION_BEGIN try {
|
||||
@@ -47,10 +65,11 @@ public:
|
||||
virtual void unlock() = 0;
|
||||
virtual void assertOwner() {}
|
||||
|
||||
virtual ~PlatformIndependentMutex() noexcept(false) {}
|
||||
virtual ~PlatformIndependentMutex() {}
|
||||
};
|
||||
|
||||
class PThreadMutex : PlatformIndependentMutex {
|
||||
#if OPENCOMBINE_HAS_PTHREAD
|
||||
class PThreadMutex final : PlatformIndependentMutex {
|
||||
private:
|
||||
pthread_mutex_t mutex_;
|
||||
public:
|
||||
@@ -66,20 +85,16 @@ public:
|
||||
PThreadMutex(PThreadMutex&&) = delete;
|
||||
PThreadMutex& operator=(PThreadMutex&&) = delete;
|
||||
|
||||
void lock() override final {
|
||||
void lock() override {
|
||||
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutex_lock(&mutex_));
|
||||
}
|
||||
|
||||
void unlock() override final {
|
||||
void unlock() override {
|
||||
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutex_unlock(&mutex_));
|
||||
}
|
||||
|
||||
~PThreadMutex() {
|
||||
// Yep, this destructor may throw. This is deliberate, since pthread_mutex_destroy
|
||||
// may fail.
|
||||
//
|
||||
// The altrenative is to just silently ignore the error, which is even worse.
|
||||
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutex_destroy(&mutex_));
|
||||
pthread_mutex_destroy(&mutex_);
|
||||
}
|
||||
protected:
|
||||
class Attributes {
|
||||
@@ -107,12 +122,8 @@ protected:
|
||||
setType(PTHREAD_MUTEX_ERRORCHECK);
|
||||
}
|
||||
|
||||
~Attributes() noexcept(false) {
|
||||
// Yep, this destructor may throw. This is deliberate,
|
||||
// since pthread_mutexattr_destroy may fail.
|
||||
//
|
||||
// The altrenative is to just silently ignore the error, which is even worse.
|
||||
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutexattr_destroy(&attrs_));
|
||||
~Attributes() {
|
||||
pthread_mutexattr_destroy(&attrs_);
|
||||
}
|
||||
private:
|
||||
void setType(int type) {
|
||||
@@ -124,21 +135,7 @@ protected:
|
||||
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutex_init(&mutex_, attributes.raw()));
|
||||
}
|
||||
};
|
||||
|
||||
class PThreadRecursiveMutex final : PThreadMutex {
|
||||
public:
|
||||
PThreadRecursiveMutex() {
|
||||
Attributes attrs;
|
||||
attrs.setRecursive();
|
||||
initialize(attrs);
|
||||
}
|
||||
|
||||
PThreadRecursiveMutex(const PThreadRecursiveMutex&) = delete;
|
||||
PThreadRecursiveMutex& operator=(const PThreadRecursiveMutex&) = delete;
|
||||
|
||||
PThreadRecursiveMutex(PThreadRecursiveMutex&&) = delete;
|
||||
PThreadRecursiveMutex& operator=(PThreadRecursiveMutex&&) = delete;
|
||||
};
|
||||
#endif // OPENCOMBINE_HAS_PTHREAD
|
||||
|
||||
#ifdef __APPLE__
|
||||
|
||||
@@ -167,9 +164,32 @@ public:
|
||||
};
|
||||
#endif // __APPLE__
|
||||
|
||||
} // end anonymous namespace
|
||||
template <typename Mu>
|
||||
class GenericMutex final : PlatformIndependentMutex {
|
||||
Mu mutex_;
|
||||
public:
|
||||
|
||||
extern "C" {
|
||||
GenericMutex() = default;
|
||||
|
||||
GenericMutex(const GenericMutex&) = delete;
|
||||
GenericMutex& operator=(const GenericMutex&) = delete;
|
||||
|
||||
GenericMutex(GenericMutex&&) = delete;
|
||||
GenericMutex& operator=(GenericMutex&&) = delete;
|
||||
|
||||
void lock() override {
|
||||
mutex_.lock();
|
||||
}
|
||||
|
||||
void unlock() override {
|
||||
mutex_.unlock();
|
||||
}
|
||||
};
|
||||
|
||||
using StdMutex = GenericMutex<std::mutex>;
|
||||
using StdRecursiveMutex = GenericMutex<std::recursive_mutex>;
|
||||
|
||||
} // end anonymous namespace
|
||||
|
||||
uint64_t opencombine_next_combine_identifier(void) {
|
||||
return next_combine_identifier.fetch_add(1);
|
||||
@@ -177,24 +197,27 @@ uint64_t opencombine_next_combine_identifier(void) {
|
||||
|
||||
OpenCombineUnfairLock opencombine_unfair_lock_alloc(void) {
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
|
||||
|
||||
#ifdef __APPLE__
|
||||
if (__builtin_available(macOS 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *)) {
|
||||
return {new OSUnfairLock};
|
||||
} else {
|
||||
return {new PThreadMutex};
|
||||
}
|
||||
#else
|
||||
#elif OPENCOMBINE_HAS_PTHREAD
|
||||
// When possible, use pthread mutex implementation, because it allows
|
||||
// setting the PTHREAD_MUTEX_ERRORCHECK attribute, which makes
|
||||
// recursive locking a hard error instead of UB.
|
||||
return {new PThreadMutex};
|
||||
#else
|
||||
return {new StdMutex};
|
||||
#endif
|
||||
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_END
|
||||
}
|
||||
|
||||
OpenCombineUnfairRecursiveLock opencombine_unfair_recursive_lock_alloc(void) {
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
|
||||
// TODO: Use os_unfair_recursive_lock on Darwin as soon as it becomes public API.
|
||||
return {new PThreadRecursiveMutex};
|
||||
return {new StdRecursiveMutex};
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_END
|
||||
}
|
||||
|
||||
@@ -237,7 +260,9 @@ void opencombine_unfair_recursive_lock_dealloc(OpenCombineUnfairRecursiveLock lo
|
||||
}
|
||||
|
||||
void opencombine_stop_in_debugger(void) {
|
||||
#if _WIN32
|
||||
DebugBreak();
|
||||
#elif OPENCOMBINE_HAS_SIGNAL_HANDLING
|
||||
raise(SIGTRAP);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// ConcurrencyHelpers.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 14.11.2022.
|
||||
//
|
||||
|
||||
#if canImport(_Concurrency) && compiler(>=5.5)
|
||||
import _Concurrency
|
||||
#endif
|
||||
|
||||
#if (canImport(_Concurrency) && compiler(>=5.5) || compiler(>=5.5.1)) && swift(<5.7)
|
||||
/// A polyfill for pre-5.7 Swift versions.
|
||||
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
||||
internal func withTaskCancellationHandler<T>( // swiftlint:disable:this generic_type_name
|
||||
operation: () async throws -> T,
|
||||
onCancel handler: @Sendable () -> Void
|
||||
) async rethrows -> T {
|
||||
return try await withTaskCancellationHandler(
|
||||
handler: handler,
|
||||
operation: operation
|
||||
)
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,136 @@
|
||||
//
|
||||
// Future+Concurrency.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 28.08.2021.
|
||||
//
|
||||
|
||||
#if canImport(_Concurrency) && compiler(>=5.5)
|
||||
import _Concurrency
|
||||
#endif
|
||||
|
||||
#if canImport(_Concurrency) && compiler(>=5.5) || compiler(>=5.5.1)
|
||||
extension Future where Failure == Never {
|
||||
|
||||
/// The published value of the future, delivered asynchronously.
|
||||
///
|
||||
/// This property subscribes to the `Future` and delivers the value asynchronously
|
||||
/// when the `Future` publishes it. Use this property when you want to use
|
||||
/// the `async`-`await` syntax with a `Future`.
|
||||
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
||||
public var value: Output {
|
||||
get async {
|
||||
await ContinuationSubscriber.withUnsafeSubscription(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Future {
|
||||
|
||||
/// The published value of the future or an error, delivered asynchronously.
|
||||
///
|
||||
/// This property subscribes to the `Future` and delivers the value asynchronously
|
||||
/// when the `Future` publishes it. If the `Future` terminates with an error,
|
||||
/// the awaiting caller receives the error instead. Use this property when you want
|
||||
/// to the `async`-`await` syntax with a `Future` whose `Failure` type is not `Never`.
|
||||
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
||||
public var value: Output {
|
||||
get async throws {
|
||||
try await ContinuationSubscriber.withUnsafeThrowingSubscription(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
||||
private final class ContinuationSubscriber<Input,
|
||||
UpstreamFailure: Error,
|
||||
ErrorOrNever: Error>
|
||||
: Subscriber
|
||||
{
|
||||
typealias Failure = UpstreamFailure
|
||||
|
||||
private var continuation: UnsafeContinuation<Input, ErrorOrNever>?
|
||||
private var subscription: Subscription?
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private init(_ continuation: UnsafeContinuation<Input, ErrorOrNever>) {
|
||||
self.continuation = continuation
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard self.subscription == nil else {
|
||||
assertionFailure("Unexpected state: received subscription twice")
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
self.subscription = subscription
|
||||
lock.unlock()
|
||||
subscription.request(.max(1))
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
if let continuation = self.continuation.take() {
|
||||
lock.unlock()
|
||||
continuation.resume(returning: input)
|
||||
} else {
|
||||
assertionFailure("Unexpected state: already completed")
|
||||
lock.unlock()
|
||||
}
|
||||
return .none
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
subscription = nil
|
||||
lock.unlock()
|
||||
completion.failure.map(handleFailure)
|
||||
}
|
||||
|
||||
private func handleFailure(_ error: Failure) {
|
||||
lock.lock()
|
||||
if let continuation = self.continuation.take() {
|
||||
lock.unlock()
|
||||
continuation.resume(throwing: error as! ErrorOrNever)
|
||||
} else {
|
||||
assertionFailure("Unexpected state: already completed")
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
||||
extension ContinuationSubscriber where ErrorOrNever == Error {
|
||||
fileprivate static func withUnsafeThrowingSubscription<Upstream: Publisher>(
|
||||
_ upstream: Upstream
|
||||
) async throws -> Input
|
||||
where Upstream.Output == Input,
|
||||
Upstream.Failure == UpstreamFailure
|
||||
{
|
||||
try await withUnsafeThrowingContinuation { continuation in
|
||||
upstream.subscribe(ContinuationSubscriber(continuation))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
||||
extension ContinuationSubscriber where UpstreamFailure == Never, ErrorOrNever == Never {
|
||||
fileprivate static func withUnsafeSubscription<Upstream: Publisher>(
|
||||
_ upstream: Upstream
|
||||
) async -> Input
|
||||
where Upstream.Output == Input,
|
||||
Upstream.Failure == Never
|
||||
{
|
||||
await withUnsafeContinuation { continuation in
|
||||
upstream.subscribe(ContinuationSubscriber(continuation))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,397 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ┃
|
||||
// ┃ Auto-generated from GYB template. DO NOT EDIT! ┃
|
||||
// ┃ ┃
|
||||
// ┃ ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
//
|
||||
// Publisher+Concurrency.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 28.08.2021.
|
||||
//
|
||||
|
||||
#if canImport(_Concurrency) && compiler(>=5.5)
|
||||
import _Concurrency
|
||||
#endif
|
||||
|
||||
#if canImport(_Concurrency) && compiler(>=5.5) || compiler(>=5.5.1)
|
||||
extension Publisher where Failure == Never {
|
||||
|
||||
/// The elements produced by the publisher, as an asynchronous sequence.
|
||||
///
|
||||
/// This property provides an `AsyncPublisher`, which allows you to use
|
||||
/// the Swift `async`-`await` syntax to receive the publisher's elements.
|
||||
/// Because `AsyncPublisher` conforms to `AsyncSequence`, you iterate over its
|
||||
/// elements with a `for`-`await`-`in` loop, rather than attaching a subscriber.
|
||||
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
||||
public var values: AsyncPublisher<Self> {
|
||||
return .init(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that exposes its elements as an asynchronous sequence.
|
||||
///
|
||||
/// `AsyncPublisher` conforms to `AsyncSequence`, which allows callers to receive
|
||||
/// values with the `for`-`await`-`in` syntax, rather than attaching a `Subscriber`.
|
||||
///
|
||||
/// Use the `values` property of the `Publisher` protocol to wrap an existing publisher
|
||||
/// with an instance of this type.
|
||||
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
||||
public struct AsyncPublisher<Upstream: Publisher>: AsyncSequence
|
||||
where Upstream.Failure == Never
|
||||
{
|
||||
|
||||
public typealias Element = Upstream.Output
|
||||
|
||||
/// The iterator that produces elements of the asynchronous publisher sequence.
|
||||
public struct Iterator: AsyncIteratorProtocol {
|
||||
|
||||
public typealias Element = Upstream.Output
|
||||
|
||||
fileprivate let inner: Inner
|
||||
|
||||
/// Produces the next element in the prefix sequence.
|
||||
///
|
||||
/// - Returns: The next published element, or `nil` if the publisher finishes
|
||||
/// normally.
|
||||
public mutating func next() async -> Element? {
|
||||
return await withTaskCancellationHandler(
|
||||
operation: { [inner] in await inner.next() },
|
||||
onCancel: { [inner] in inner.cancel() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The type of asynchronous iterator that produces elements of this
|
||||
/// asynchronous sequence.
|
||||
public typealias AsyncIterator = Iterator
|
||||
|
||||
private let publisher: Upstream
|
||||
|
||||
/// Creates a publisher that exposes elements received from an upstream publisher as
|
||||
/// a throwing asynchronous sequence.
|
||||
///
|
||||
/// - Parameter publisher: An upstream publisher. The asynchronous publisher converts
|
||||
/// elements received from this publisher into an asynchronous sequence.
|
||||
public init(_ publisher: Upstream) {
|
||||
self.publisher = publisher
|
||||
}
|
||||
|
||||
/// Creates the asynchronous iterator that produces elements of this asynchronous
|
||||
/// sequence.
|
||||
///
|
||||
/// - Returns: An instance of the `AsyncIterator` type used to produce elements of
|
||||
/// the asynchronous sequence.
|
||||
public func makeAsyncIterator() -> Iterator {
|
||||
let inner = Iterator.Inner()
|
||||
publisher.subscribe(inner)
|
||||
return Iterator(inner: inner)
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
||||
extension AsyncPublisher.Iterator {
|
||||
|
||||
fileprivate final class Inner: Subscriber, Cancellable {
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private enum State {
|
||||
case awaitingSubscription
|
||||
case subscribed(Subscription)
|
||||
case terminal
|
||||
}
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
private var pending: [UnsafeContinuation<Input?, Never>] = []
|
||||
private var state = State.awaitingSubscription
|
||||
private var pendingDemand = Subscribers.Demand.none
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .subscribed(subscription)
|
||||
let pendingDemand = self.pendingDemand
|
||||
self.pendingDemand = .none
|
||||
lock.unlock()
|
||||
if pendingDemand != .none {
|
||||
subscription.request(pendingDemand)
|
||||
}
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case .subscribed = state else {
|
||||
let pending = self.pending.take()
|
||||
lock.unlock()
|
||||
pending.resumeAllWithNil()
|
||||
return .none
|
||||
}
|
||||
precondition(!pending.isEmpty, "Received an output without requesting demand")
|
||||
let continuation = pending.removeFirst()
|
||||
lock.unlock()
|
||||
continuation.resume(returning: input)
|
||||
return .none
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
state = .terminal
|
||||
let pending = self.pending.take()
|
||||
lock.unlock()
|
||||
pending.resumeAllWithNil()
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
let pending = self.pending.take()
|
||||
guard case .subscribed(let subscription) = state else {
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
pending.resumeAllWithNil()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
pending.resumeAllWithNil()
|
||||
}
|
||||
|
||||
fileprivate func next() async -> Input? {
|
||||
return await withUnsafeContinuation { continuation in
|
||||
lock.lock()
|
||||
switch state {
|
||||
case .awaitingSubscription:
|
||||
pending.append(continuation)
|
||||
pendingDemand += 1
|
||||
lock.unlock()
|
||||
case .subscribed(let subscription):
|
||||
pending.append(continuation)
|
||||
lock.unlock()
|
||||
subscription.request(.max(1))
|
||||
case .terminal:
|
||||
lock.unlock()
|
||||
continuation.resume(returning: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
extension Publisher {
|
||||
|
||||
/// The elements produced by the publisher, as a throwing asynchronous sequence.
|
||||
///
|
||||
/// This property provides an `AsyncThrowingPublisher`, which allows you to use
|
||||
/// the Swift `async`-`await` syntax to receive the publisher's elements.
|
||||
/// Because `AsyncPublisher` conforms to `AsyncSequence`, you iterate over its
|
||||
/// elements with a `for`-`await`-`in` loop, rather than attaching a subscriber.
|
||||
/// If the publisher terminates with an error, the awaiting caller receives the error
|
||||
/// as a `throw`.
|
||||
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
||||
public var values: AsyncThrowingPublisher<Self> {
|
||||
return .init(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that exposes its elements as a throwing asynchronous sequence.
|
||||
///
|
||||
/// `AsyncThrowingPublisher` conforms to `AsyncSequence`, which allows callers to receive
|
||||
/// values with the `for`-`await`-`in` syntax, rather than attaching a `Subscriber`.
|
||||
/// If the upstream publisher terminates with an error, `AsyncThrowingPublisher` throws
|
||||
/// the error to the awaiting caller.
|
||||
///
|
||||
/// Use the `values` property of the `Publisher` protocol to wrap an existing publisher
|
||||
/// with an instance of this type.
|
||||
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
||||
public struct AsyncThrowingPublisher<Upstream: Publisher>: AsyncSequence
|
||||
{
|
||||
|
||||
public typealias Element = Upstream.Output
|
||||
|
||||
/// The iterator that produces elements of the asynchronous publisher sequence.
|
||||
public struct Iterator: AsyncIteratorProtocol {
|
||||
|
||||
public typealias Element = Upstream.Output
|
||||
|
||||
fileprivate let inner: Inner
|
||||
|
||||
/// Produces the next element in the prefix sequence.
|
||||
///
|
||||
/// - Returns: The next published element, or `nil` if the publisher finishes
|
||||
/// normally.
|
||||
/// If the publisher terminates with an error, the call point receives
|
||||
/// the error as a `throw`.
|
||||
public mutating func next() async throws -> Element? {
|
||||
return try await withTaskCancellationHandler(
|
||||
operation: { [inner] in try await inner.next() },
|
||||
onCancel: { [inner] in inner.cancel() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The type of asynchronous iterator that produces elements of this
|
||||
/// asynchronous sequence.
|
||||
public typealias AsyncIterator = Iterator
|
||||
|
||||
private let publisher: Upstream
|
||||
|
||||
/// Creates a publisher that exposes elements received from an upstream publisher as
|
||||
/// an asynchronous sequence.
|
||||
///
|
||||
/// - Parameter publisher: An upstream publisher. The asynchronous publisher converts
|
||||
/// elements received from this publisher into an asynchronous sequence.
|
||||
public init(_ publisher: Upstream) {
|
||||
self.publisher = publisher
|
||||
}
|
||||
|
||||
/// Creates the asynchronous iterator that produces elements of this asynchronous
|
||||
/// sequence.
|
||||
///
|
||||
/// - Returns: An instance of the `AsyncIterator` type used to produce elements of
|
||||
/// the asynchronous sequence.
|
||||
public func makeAsyncIterator() -> Iterator {
|
||||
let inner = Iterator.Inner()
|
||||
publisher.subscribe(inner)
|
||||
return Iterator(inner: inner)
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
||||
extension AsyncThrowingPublisher.Iterator {
|
||||
|
||||
fileprivate final class Inner: Subscriber, Cancellable {
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private enum State {
|
||||
case awaitingSubscription
|
||||
case subscribed(Subscription)
|
||||
case terminal(Error?)
|
||||
}
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
private var pending: [UnsafeContinuation<Input?, Error>] = []
|
||||
private var state = State.awaitingSubscription
|
||||
private var pendingDemand = Subscribers.Demand.none
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .subscribed(subscription)
|
||||
let pendingDemand = self.pendingDemand
|
||||
self.pendingDemand = .none
|
||||
lock.unlock()
|
||||
if pendingDemand != .none {
|
||||
subscription.request(pendingDemand)
|
||||
}
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case .subscribed = state else {
|
||||
let pending = self.pending.take()
|
||||
lock.unlock()
|
||||
pending.resumeAllWithNil()
|
||||
return .none
|
||||
}
|
||||
precondition(!pending.isEmpty, "Received an output without requesting demand")
|
||||
let continuation = pending.removeFirst()
|
||||
lock.unlock()
|
||||
continuation.resume(returning: input)
|
||||
return .none
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
switch state {
|
||||
case .awaitingSubscription, .subscribed:
|
||||
if let continuation = pending.first {
|
||||
state = .terminal(nil)
|
||||
let remaining = pending.take().dropFirst()
|
||||
lock.unlock()
|
||||
switch completion {
|
||||
case .finished:
|
||||
continuation.resume(returning: nil)
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
remaining.resumeAllWithNil()
|
||||
} else {
|
||||
state = .terminal(completion.failure)
|
||||
lock.unlock()
|
||||
}
|
||||
case .terminal:
|
||||
let pending = self.pending.take()
|
||||
lock.unlock()
|
||||
pending.resumeAllWithNil()
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
let pending = self.pending.take()
|
||||
guard case .subscribed(let subscription) = state else {
|
||||
state = .terminal(nil)
|
||||
lock.unlock()
|
||||
pending.resumeAllWithNil()
|
||||
return
|
||||
}
|
||||
state = .terminal(nil)
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
pending.resumeAllWithNil()
|
||||
}
|
||||
|
||||
fileprivate func next() async throws -> Input? {
|
||||
return try await withUnsafeThrowingContinuation { continuation in
|
||||
lock.lock()
|
||||
switch state {
|
||||
case .awaitingSubscription:
|
||||
pending.append(continuation)
|
||||
pendingDemand += 1
|
||||
lock.unlock()
|
||||
case .subscribed(let subscription):
|
||||
pending.append(continuation)
|
||||
lock.unlock()
|
||||
subscription.request(.max(1))
|
||||
case .terminal(nil):
|
||||
lock.unlock()
|
||||
continuation.resume(returning: nil)
|
||||
case .terminal(let error?):
|
||||
state = .terminal(nil)
|
||||
lock.unlock()
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
||||
extension Sequence {
|
||||
fileprivate func resumeAllWithNil<Output, Failure: Error>()
|
||||
where Element == UnsafeContinuation<Output?, Failure>
|
||||
{
|
||||
for continuation in self {
|
||||
continuation.resume(returning: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,249 @@
|
||||
${template_header}
|
||||
//
|
||||
// Publisher+Concurrency.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 28.08.2021.
|
||||
//
|
||||
|
||||
#if canImport(_Concurrency) && compiler(>=5.5)
|
||||
import _Concurrency
|
||||
#endif
|
||||
|
||||
#if canImport(_Concurrency) && compiler(>=5.5) || compiler(>=5.5.1)
|
||||
%{
|
||||
instantiations = [('AsyncPublisher', False), ('AsyncThrowingPublisher', True)]
|
||||
}%
|
||||
% for instantiation, throwing in instantiations:
|
||||
extension Publisher ${'' if throwing else 'where Failure == Never '}{
|
||||
|
||||
/// The elements produced by the publisher, as ${'a throwing' if throwing else 'an'} asynchronous sequence.
|
||||
///
|
||||
/// This property provides an `${instantiation}`, which allows you to use
|
||||
/// the Swift `async`-`await` syntax to receive the publisher's elements.
|
||||
/// Because `AsyncPublisher` conforms to `AsyncSequence`, you iterate over its
|
||||
/// elements with a `for`-`await`-`in` loop, rather than attaching a subscriber.
|
||||
% if throwing:
|
||||
/// If the publisher terminates with an error, the awaiting caller receives the error
|
||||
/// as a `throw`.
|
||||
% end
|
||||
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
||||
public var values: ${instantiation}<Self> {
|
||||
return .init(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that exposes its elements as ${'a throwing' if throwing else 'an'} asynchronous sequence.
|
||||
///
|
||||
/// `${instantiation}` conforms to `AsyncSequence`, which allows callers to receive
|
||||
/// values with the `for`-`await`-`in` syntax, rather than attaching a `Subscriber`.
|
||||
% if throwing:
|
||||
/// If the upstream publisher terminates with an error, `${instantiation}` throws
|
||||
/// the error to the awaiting caller.
|
||||
% end
|
||||
///
|
||||
/// Use the `values` property of the `Publisher` protocol to wrap an existing publisher
|
||||
/// with an instance of this type.
|
||||
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
||||
public struct ${instantiation}<Upstream: Publisher>: AsyncSequence
|
||||
% if not throwing:
|
||||
where Upstream.Failure == Never
|
||||
% end
|
||||
{
|
||||
|
||||
public typealias Element = Upstream.Output
|
||||
|
||||
/// The iterator that produces elements of the asynchronous publisher sequence.
|
||||
public struct Iterator: AsyncIteratorProtocol {
|
||||
|
||||
public typealias Element = Upstream.Output
|
||||
|
||||
fileprivate let inner: Inner
|
||||
|
||||
/// Produces the next element in the prefix sequence.
|
||||
///
|
||||
/// - Returns: The next published element, or `nil` if the publisher finishes
|
||||
/// normally.
|
||||
% if throwing:
|
||||
/// If the publisher terminates with an error, the call point receives
|
||||
/// the error as a `throw`.
|
||||
% end
|
||||
public mutating func next() async ${'throws ' if throwing else ''}-> Element? {
|
||||
return ${'try ' if throwing else ''}await withTaskCancellationHandler(
|
||||
operation: { [inner] in ${'try ' if throwing else ''}await inner.next() },
|
||||
onCancel: { [inner] in inner.cancel() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The type of asynchronous iterator that produces elements of this
|
||||
/// asynchronous sequence.
|
||||
public typealias AsyncIterator = Iterator
|
||||
|
||||
private let publisher: Upstream
|
||||
|
||||
/// Creates a publisher that exposes elements received from an upstream publisher as
|
||||
% if throwing:
|
||||
/// an asynchronous sequence.
|
||||
% else:
|
||||
/// a throwing asynchronous sequence.
|
||||
% end
|
||||
///
|
||||
/// - Parameter publisher: An upstream publisher. The asynchronous publisher converts
|
||||
/// elements received from this publisher into an asynchronous sequence.
|
||||
public init(_ publisher: Upstream) {
|
||||
self.publisher = publisher
|
||||
}
|
||||
|
||||
/// Creates the asynchronous iterator that produces elements of this asynchronous
|
||||
/// sequence.
|
||||
///
|
||||
/// - Returns: An instance of the `AsyncIterator` type used to produce elements of
|
||||
/// the asynchronous sequence.
|
||||
public func makeAsyncIterator() -> Iterator {
|
||||
let inner = Iterator.Inner()
|
||||
publisher.subscribe(inner)
|
||||
return Iterator(inner: inner)
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
||||
extension ${instantiation}.Iterator {
|
||||
|
||||
fileprivate final class Inner: Subscriber, Cancellable {
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private enum State {
|
||||
case awaitingSubscription
|
||||
case subscribed(Subscription)
|
||||
case terminal${'(Error?)' if throwing else ''}
|
||||
}
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
private var pending: [UnsafeContinuation<Input?, ${'Error' if throwing else 'Never'}>] = []
|
||||
private var state = State.awaitingSubscription
|
||||
private var pendingDemand = Subscribers.Demand.none
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .subscribed(subscription)
|
||||
let pendingDemand = self.pendingDemand
|
||||
self.pendingDemand = .none
|
||||
lock.unlock()
|
||||
if pendingDemand != .none {
|
||||
subscription.request(pendingDemand)
|
||||
}
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case .subscribed = state else {
|
||||
let pending = self.pending.take()
|
||||
lock.unlock()
|
||||
pending.resumeAllWithNil()
|
||||
return .none
|
||||
}
|
||||
precondition(!pending.isEmpty, "Received an output without requesting demand")
|
||||
let continuation = pending.removeFirst()
|
||||
lock.unlock()
|
||||
continuation.resume(returning: input)
|
||||
return .none
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
% if throwing:
|
||||
switch state {
|
||||
case .awaitingSubscription, .subscribed:
|
||||
if let continuation = pending.first {
|
||||
state = .terminal(nil)
|
||||
let remaining = pending.take().dropFirst()
|
||||
lock.unlock()
|
||||
switch completion {
|
||||
case .finished:
|
||||
continuation.resume(returning: nil)
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
remaining.resumeAllWithNil()
|
||||
} else {
|
||||
state = .terminal(completion.failure)
|
||||
lock.unlock()
|
||||
}
|
||||
case .terminal:
|
||||
let pending = self.pending.take()
|
||||
lock.unlock()
|
||||
pending.resumeAllWithNil()
|
||||
}
|
||||
% else:
|
||||
state = .terminal
|
||||
let pending = self.pending.take()
|
||||
lock.unlock()
|
||||
pending.resumeAllWithNil()
|
||||
% end
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
let pending = self.pending.take()
|
||||
guard case .subscribed(let subscription) = state else {
|
||||
state = .terminal${'(nil)' if throwing else ''}
|
||||
lock.unlock()
|
||||
pending.resumeAllWithNil()
|
||||
return
|
||||
}
|
||||
state = .terminal${'(nil)' if throwing else ''}
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
pending.resumeAllWithNil()
|
||||
}
|
||||
|
||||
fileprivate func next() async ${'throws ' if throwing else ''}-> Input? {
|
||||
return ${'try ' if throwing else ''}await withUnsafe${'Throwing' if throwing else ''}Continuation { continuation in
|
||||
lock.lock()
|
||||
switch state {
|
||||
case .awaitingSubscription:
|
||||
pending.append(continuation)
|
||||
pendingDemand += 1
|
||||
lock.unlock()
|
||||
case .subscribed(let subscription):
|
||||
pending.append(continuation)
|
||||
lock.unlock()
|
||||
subscription.request(.max(1))
|
||||
case .terminal${'(nil)' if throwing else ''}:
|
||||
lock.unlock()
|
||||
continuation.resume(returning: nil)
|
||||
% if throwing:
|
||||
case .terminal(let error?):
|
||||
state = .terminal(nil)
|
||||
lock.unlock()
|
||||
continuation.resume(throwing: error)
|
||||
% end
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
% end
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
||||
extension Sequence {
|
||||
fileprivate func resumeAllWithNil<Output, Failure: Error>()
|
||||
where Element == UnsafeContinuation<Output?, Failure>
|
||||
{
|
||||
for continuation in self {
|
||||
continuation.resume(returning: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,24 +0,0 @@
|
||||
//
|
||||
// ConnectablePublisher.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 14.06.2019.
|
||||
//
|
||||
|
||||
/// A publisher that provides an explicit means of connecting and canceling publication.
|
||||
///
|
||||
/// Use a `ConnectablePublisher` when you need to perform additional configuration or
|
||||
/// setup prior to producing any elements.
|
||||
///
|
||||
/// This publisher doesn’t 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
|
||||
}
|
||||
@@ -108,8 +108,7 @@ public final class CurrentValueSubject<Output, Failure: Error>: Subject {
|
||||
}
|
||||
active = false
|
||||
self.completion = completion
|
||||
let downstreams = self.downstreams
|
||||
self.downstreams.removeAll()
|
||||
let downstreams = self.downstreams.take()
|
||||
lock.unlock()
|
||||
downstreams.forEach { conduit in
|
||||
conduit.finish(completion: completion)
|
||||
@@ -181,13 +180,11 @@ extension CurrentValueSubject {
|
||||
|
||||
override func finish(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
guard let downstream = self.downstream else {
|
||||
guard let downstream = self.downstream.take() else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
self.downstream = nil
|
||||
let parent = self.parent
|
||||
self.parent = nil
|
||||
let parent = self.parent.take()
|
||||
lock.unlock()
|
||||
parent?.disassociate(self)
|
||||
downstreamLock.lock()
|
||||
@@ -227,13 +224,11 @@ extension CurrentValueSubject {
|
||||
|
||||
override func cancel() {
|
||||
lock.lock()
|
||||
if self.downstream == nil {
|
||||
if downstream.take() == nil {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
self.downstream = nil
|
||||
let parent = self.parent
|
||||
self.parent = nil
|
||||
let parent = self.parent.take()
|
||||
lock.unlock()
|
||||
parent?.disassociate(self)
|
||||
}
|
||||
|
||||
@@ -43,8 +43,7 @@ public final class Future<Output, Failure: Error>: Publisher {
|
||||
return
|
||||
}
|
||||
self.result = result
|
||||
let downstreams = self.downstreams
|
||||
self.downstreams.removeAll()
|
||||
let downstreams = self.downstreams.take()
|
||||
lock.unlock()
|
||||
switch result {
|
||||
case .success(let output):
|
||||
@@ -87,12 +86,32 @@ extension Future {
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure
|
||||
{
|
||||
private enum State {
|
||||
case active(Downstream, hasAnyDemand: Bool)
|
||||
case terminal
|
||||
|
||||
fileprivate var parent: Future?
|
||||
var downstream: Downstream? {
|
||||
switch self {
|
||||
case .active(let downstream, hasAnyDemand: _):
|
||||
return downstream
|
||||
case .terminal:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var downstream: Downstream?
|
||||
var hasAnyDemand: Bool {
|
||||
switch self {
|
||||
case .active(_, let hasAnyDemand):
|
||||
return hasAnyDemand
|
||||
case .terminal:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var hasAnyDemand = false
|
||||
private var parent: Future?
|
||||
|
||||
private var state: State
|
||||
|
||||
private var lock = UnfairLock.allocate()
|
||||
|
||||
@@ -100,7 +119,7 @@ extension Future {
|
||||
|
||||
fileprivate init(parent: Future, downstream: Downstream) {
|
||||
self.parent = parent
|
||||
self.downstream = downstream
|
||||
self.state = .active(downstream, hasAnyDemand: false)
|
||||
}
|
||||
|
||||
deinit {
|
||||
@@ -108,21 +127,8 @@ extension Future {
|
||||
downstreamLock.deallocate()
|
||||
}
|
||||
|
||||
fileprivate func fulfill(_ result: Result<Output, Failure>) {
|
||||
lock.lock()
|
||||
guard let downstream = self.downstream else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
let parent = self.parent
|
||||
if case .success = result, !hasAnyDemand {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
self.downstream = nil
|
||||
self.parent = nil
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
fileprivate func lockedFulfill(downstream: Downstream,
|
||||
result: Result<Output, Failure>) {
|
||||
switch result {
|
||||
case .success(let output):
|
||||
_ = downstream.receive(output)
|
||||
@@ -130,7 +136,27 @@ extension Future {
|
||||
case .failure(let error):
|
||||
downstream.receive(completion: .failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func fulfill(_ result: Result<Output, Failure>) {
|
||||
lock.lock()
|
||||
guard case let .active(downstream, hasAnyDemand) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
if case .success = result, !hasAnyDemand {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
lockedFulfill(downstream: downstream, result: result)
|
||||
downstreamLock.unlock()
|
||||
lock.lock()
|
||||
let parent = self.parent.take()
|
||||
lock.unlock()
|
||||
parent?.disassociate(self)
|
||||
}
|
||||
|
||||
@@ -150,47 +176,37 @@ extension Future {
|
||||
override func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
lock.lock()
|
||||
guard let downstream = self.downstream, let parent = self.parent else {
|
||||
guard case .active(let downstream, hasAnyDemand: _) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
hasAnyDemand = true
|
||||
state = .active(downstream, hasAnyDemand: true)
|
||||
|
||||
parent.lock.lock()
|
||||
guard let result = parent.result else {
|
||||
parent.lock.unlock()
|
||||
if let parent = parent, let result = parent.result {
|
||||
// If the promise is already resolved, send the result downstream
|
||||
// immediately
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
lockedFulfill(downstream: downstream, result: result)
|
||||
downstreamLock.unlock()
|
||||
parent.disassociate(self)
|
||||
} else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
parent.lock.unlock()
|
||||
self.downstream = nil
|
||||
self.parent = nil
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
switch result {
|
||||
case .success(let output):
|
||||
_ = downstream.receive(output)
|
||||
downstream.receive(completion: .finished)
|
||||
case .failure(let error):
|
||||
// This branch is not reachable under normal circumstances,
|
||||
// but may be reachable in case of a race condition.
|
||||
downstream.receive(completion: .failure(error))
|
||||
}
|
||||
downstreamLock.unlock()
|
||||
parent.disassociate(self)
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
lock.lock()
|
||||
if self.downstream == nil {
|
||||
switch state {
|
||||
case .active:
|
||||
state = .terminal
|
||||
let parent = self.parent.take()
|
||||
lock.unlock()
|
||||
parent?.disassociate(self)
|
||||
case .terminal:
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
self.downstream = nil
|
||||
let parent = self.parent
|
||||
self.parent = nil
|
||||
lock.unlock()
|
||||
parent?.disassociate(self)
|
||||
}
|
||||
|
||||
var description: String { return "Future" }
|
||||
@@ -200,8 +216,8 @@ extension Future {
|
||||
defer { lock.unlock() }
|
||||
let children: [Mirror.Child] = [
|
||||
("parent", parent as Any),
|
||||
("downstream", downstream as Any),
|
||||
("hasAnyDemand", hasAnyDemand),
|
||||
("downstream", state.downstream as Any),
|
||||
("hasAnyDemand", state.hasAnyDemand),
|
||||
("subject", parent as Any)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
|
||||
+426
@@ -0,0 +1,426 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ┃
|
||||
// ┃ Auto-generated from GYB template. DO NOT EDIT! ┃
|
||||
// ┃ ┃
|
||||
// ┃ ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
//
|
||||
// RootProtocols.swift
|
||||
// OpenCombine
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.06.2019.
|
||||
//
|
||||
|
||||
#if compiler(>=5.7)
|
||||
/// Declares that a type can transmit a sequence of values over time.
|
||||
///
|
||||
/// A publisher delivers elements to one or more `Subscriber` instances.
|
||||
/// The subscriber’s `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 subject’s 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 property’s 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 doesn’t 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 subscriber’s
|
||||
/// `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 publisher’s `subscribe(_:)`
|
||||
/// method. After making this call, the publisher invokes the subscriber’s
|
||||
/// `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 scheduler’s 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 subscriber’s `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 subject’s 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 property’s 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 doesn’t 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 subscriber’s
|
||||
/// `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 publisher’s `subscribe(_:)`
|
||||
/// method. After making this call, the publisher invokes the subscriber’s
|
||||
/// `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 scheduler’s definition of the current moment in time.
|
||||
var now: SchedulerTimeType { get }
|
||||
|
||||
/// The minimum tolerance allowed by the scheduler.
|
||||
var minimumTolerance: SchedulerTimeType.Stride { get }
|
||||
|
||||
/// Performs the action at the next possible opportunity.
|
||||
func schedule(options: SchedulerOptions?, _ action: @escaping () -> Void)
|
||||
|
||||
/// Performs the action at some time after the specified date.
|
||||
func schedule(after date: SchedulerTimeType,
|
||||
tolerance: SchedulerTimeType.Stride,
|
||||
options: SchedulerOptions?,
|
||||
_ action: @escaping () -> Void)
|
||||
|
||||
/// Performs the action at some time after the specified date, at the specified
|
||||
/// frequency, optionally taking into account tolerance if possible.
|
||||
func schedule(after date: SchedulerTimeType,
|
||||
interval: SchedulerTimeType.Stride,
|
||||
tolerance: SchedulerTimeType.Stride,
|
||||
options: SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) -> Cancellable
|
||||
}
|
||||
#endif
|
||||
@@ -11,6 +11,12 @@ internal enum ConduitList<Output, Failure: Error> {
|
||||
case many(Set<ConduitBase<Output, Failure>>)
|
||||
}
|
||||
|
||||
extension ConduitList: HasDefaultValue {
|
||||
init() {
|
||||
self = .empty
|
||||
}
|
||||
}
|
||||
|
||||
extension ConduitList {
|
||||
internal mutating func insert(_ conduit: ConduitBase<Output, Failure>) {
|
||||
switch self {
|
||||
@@ -50,8 +56,4 @@ extension ConduitList {
|
||||
self = .many(set)
|
||||
}
|
||||
}
|
||||
|
||||
internal mutating func removeAll() {
|
||||
self = .empty
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,8 +187,7 @@ extension PublishedSubject {
|
||||
return
|
||||
}
|
||||
self.downstream = nil
|
||||
let parent = self.parent
|
||||
self.parent = nil
|
||||
let parent = self.parent.take()
|
||||
lock.unlock()
|
||||
parent?.disassociate(self)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// Utils.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 28.08.2021.
|
||||
//
|
||||
|
||||
internal protocol HasDefaultValue {
|
||||
init()
|
||||
}
|
||||
|
||||
extension HasDefaultValue {
|
||||
|
||||
@inline(__always)
|
||||
internal mutating func take() -> Self {
|
||||
let taken = self
|
||||
self = .init()
|
||||
return taken
|
||||
}
|
||||
}
|
||||
|
||||
extension Array: HasDefaultValue {}
|
||||
|
||||
extension Dictionary: HasDefaultValue {}
|
||||
|
||||
extension Optional: HasDefaultValue {
|
||||
init() {
|
||||
self = nil
|
||||
}
|
||||
}
|
||||
@@ -85,8 +85,7 @@ public final class PassthroughSubject<Output, Failure: Error>: Subject {
|
||||
}
|
||||
active = false
|
||||
self.completion = completion
|
||||
let downstreams = self.downstreams
|
||||
self.downstreams.removeAll()
|
||||
let downstreams = self.downstreams.take()
|
||||
lock.unlock()
|
||||
downstreams.forEach { conduit in
|
||||
conduit.finish(completion: completion)
|
||||
@@ -168,13 +167,11 @@ extension PassthroughSubject {
|
||||
|
||||
override func finish(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
guard let downstream = self.downstream else {
|
||||
guard let downstream = self.downstream.take() else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
self.downstream = nil
|
||||
let parent = self.parent
|
||||
self.parent = nil
|
||||
let parent = self.parent.take()
|
||||
lock.unlock()
|
||||
parent?.disassociate(self)
|
||||
downstreamLock.lock()
|
||||
@@ -197,13 +194,11 @@ extension PassthroughSubject {
|
||||
|
||||
override func cancel() {
|
||||
lock.lock()
|
||||
if self.downstream == nil {
|
||||
if downstream.take() == nil {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
self.downstream = nil
|
||||
let parent = self.parent
|
||||
self.parent = nil
|
||||
let parent = self.parent.take()
|
||||
lock.unlock()
|
||||
parent?.disassociate(self)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// Publisher+Subscribe.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 23.04.2023.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Attaches the specified subscriber to this publisher.
|
||||
///
|
||||
/// Always call this function instead of `receive(subscriber:)`.
|
||||
/// Adopters of `Publisher` must implement `receive(subscriber:)`.
|
||||
/// The implementation of `subscribe(_:)` in this extension calls through to
|
||||
/// `receive(subscriber:)`.
|
||||
/// - SeeAlso: `receive(subscriber:)`
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`. After attaching,
|
||||
/// the subscriber can start to receive values.
|
||||
public func subscribe<Subscriber: OpenCombine.Subscriber>(_ subscriber: Subscriber)
|
||||
where Failure == Subscriber.Failure, Output == Subscriber.Input
|
||||
{
|
||||
if let hook = DebugHook.getGlobalHook() {
|
||||
if var marker = subscriber as? SubscriberTapMarker {
|
||||
let anySubscriber = marker.inner
|
||||
as! AnySubscriber<Subscriber.Input, Subscriber.Failure>
|
||||
hook.willReceive(publisher: self, subscriber: anySubscriber)
|
||||
receive(subscriber: subscriber)
|
||||
hook.didReceive(publisher: self, subscriber: anySubscriber)
|
||||
} else {
|
||||
let tap = SubscriberTap(subscriber: subscriber)
|
||||
hook.willReceive(publisher: self, subscriber: subscriber)
|
||||
receive(subscriber: tap)
|
||||
hook.didReceive(publisher: self, subscriber: subscriber)
|
||||
}
|
||||
} else {
|
||||
receive(subscriber: subscriber)
|
||||
}
|
||||
}
|
||||
|
||||
/// Attaches the specified subject to this publisher.
|
||||
///
|
||||
/// - Parameter subject: The subject to attach to this publisher.
|
||||
public func subscribe<Subject: OpenCombine.Subject>(
|
||||
_ subject: Subject
|
||||
) -> AnyCancellable
|
||||
where Failure == Subject.Failure, Output == Subject.Output
|
||||
{
|
||||
let subscriber = SubjectSubscriber(subject)
|
||||
self.subscribe(subscriber)
|
||||
return AnyCancellable(subscriber)
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
//
|
||||
// Publisher.swift
|
||||
// OpenCombine
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.06.2019.
|
||||
//
|
||||
|
||||
/// Declares that a type can transmit a sequence of values over time.
|
||||
///
|
||||
/// A publisher delivers elements to one or more `Subscriber` instances.
|
||||
/// The subscriber’s `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 subject’s 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 property’s value
|
||||
/// changes. See the `Published` type for an example of this approach.
|
||||
public protocol Publisher {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
associatedtype Output
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
associatedtype Failure: Error
|
||||
|
||||
/// Attaches the specified subscriber to this publisher.
|
||||
///
|
||||
/// Always call this function instead of `receive(subscriber:)`.
|
||||
/// Adopters of `Publisher` must implement `receive(subscriber:)`. The implementation
|
||||
/// of `subscribe(_:)` provided by `Publisher` calls through to
|
||||
/// `receive(subscriber:)`.
|
||||
///
|
||||
/// - Parameter subscriber: The subscriber to attach to this publisher. After
|
||||
/// attaching, the subscriber can start to receive values.
|
||||
func receive<Subscriber: OpenCombine.Subscriber>(subscriber: Subscriber)
|
||||
where Failure == Subscriber.Failure, Output == Subscriber.Input
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Attaches the specified subscriber to this publisher.
|
||||
///
|
||||
/// Always call this function instead of `receive(subscriber:)`.
|
||||
/// Adopters of `Publisher` must implement `receive(subscriber:)`.
|
||||
/// The implementation of `subscribe(_:)` in this extension calls through to
|
||||
/// `receive(subscriber:)`.
|
||||
/// - SeeAlso: `receive(subscriber:)`
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`. After attaching,
|
||||
/// the subscriber can start to receive values.
|
||||
public func subscribe<Subscriber: OpenCombine.Subscriber>(_ subscriber: Subscriber)
|
||||
where Failure == Subscriber.Failure, Output == Subscriber.Input
|
||||
{
|
||||
if let hook = DebugHook.getGlobalHook() {
|
||||
if var marker = subscriber as? SubscriberTapMarker {
|
||||
let anySubscriber = marker.inner
|
||||
as! AnySubscriber<Subscriber.Input, Subscriber.Failure>
|
||||
hook.willReceive(publisher: self, subscriber: anySubscriber)
|
||||
receive(subscriber: subscriber)
|
||||
hook.didReceive(publisher: self, subscriber: anySubscriber)
|
||||
} else {
|
||||
let tap = SubscriberTap(subscriber: subscriber)
|
||||
hook.willReceive(publisher: self, subscriber: subscriber)
|
||||
receive(subscriber: tap)
|
||||
hook.didReceive(publisher: self, subscriber: subscriber)
|
||||
}
|
||||
} else {
|
||||
receive(subscriber: subscriber)
|
||||
}
|
||||
}
|
||||
|
||||
/// Attaches the specified subject to this publisher.
|
||||
///
|
||||
/// - Parameter subject: The subject to attach to this publisher.
|
||||
public func subscribe<Subject: OpenCombine.Subject>(
|
||||
_ subject: Subject
|
||||
) -> AnyCancellable
|
||||
where Failure == Subject.Failure, Output == Subject.Output
|
||||
{
|
||||
let subscriber = SubjectSubscriber(subject)
|
||||
self.subscribe(subscriber)
|
||||
return AnyCancellable(subscriber)
|
||||
}
|
||||
}
|
||||
@@ -222,8 +222,7 @@ extension Publishers.Encode {
|
||||
} catch {
|
||||
lock.lock()
|
||||
finished = true
|
||||
let subscription = self.subscription
|
||||
self.subscription = nil
|
||||
let subscription = self.subscription.take()
|
||||
lock.unlock()
|
||||
subscription?.cancel()
|
||||
downstream.receive(completion: .failure(error))
|
||||
@@ -252,11 +251,10 @@ extension Publishers.Encode {
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard !finished, let subscription = self.subscription else {
|
||||
guard !finished, let subscription = self.subscription.take() else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
self.subscription = nil
|
||||
finished = true
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
@@ -336,8 +334,7 @@ extension Publishers.Decode {
|
||||
} catch {
|
||||
lock.lock()
|
||||
finished = true
|
||||
let subscription = self.subscription
|
||||
self.subscription = nil
|
||||
let subscription = self.subscription.take()
|
||||
lock.unlock()
|
||||
subscription?.cancel()
|
||||
downstream.receive(completion: .failure(error))
|
||||
@@ -366,11 +363,10 @@ extension Publishers.Decode {
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard !finished, let subscription = self.subscription else {
|
||||
guard !finished, let subscription = self.subscription.take() else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
self.subscription = nil
|
||||
finished = true
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
// Created by Sergej Jaskiewicz on 03/10/2019.
|
||||
//
|
||||
|
||||
// swiftlint:disable large_tuple
|
||||
|
||||
extension Publisher {
|
||||
/// Publishes the value of the key path.
|
||||
///
|
||||
@@ -31,7 +33,7 @@ extension Publisher {
|
||||
/// .sink {
|
||||
/// print ("Rolled: \($0)")
|
||||
/// }
|
||||
/// // Prints "Rolled: 6 (or some other random value).
|
||||
/// // Prints "Rolled: 4 (or some other random value).
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - keyPath: The key path of a property on `Output`.
|
||||
@@ -68,7 +70,7 @@ extension Publisher {
|
||||
/// (total \(values.0 + values.1))
|
||||
/// """)
|
||||
/// }
|
||||
/// // Prints "Rolled: 5, 3 (total: 8)" (or other random values).
|
||||
/// // Prints "Rolled: 4, 1 (total: 5)" (or other random values).
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - keyPath0: The key path of a property on `Output`.
|
||||
@@ -110,7 +112,7 @@ extension Publisher {
|
||||
/// (total \(values.0 + values.1 + values.2))
|
||||
/// """)
|
||||
/// }
|
||||
/// // Prints "Rolled: 2, 4, 3 (total: 9)" (or other random values).
|
||||
/// // Prints "Rolled: 3, 5, 4 (total: 12)" (or other random values).
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - keyPath0: The key path of a property on `Output`.
|
||||
|
||||
@@ -293,8 +293,7 @@ extension Just {
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
guard let downstream = self.downstream else { return }
|
||||
self.downstream = nil
|
||||
guard let downstream = self.downstream.take() else { return }
|
||||
_ = downstream.receive(value)
|
||||
downstream.receive(completion: .finished)
|
||||
}
|
||||
|
||||
@@ -122,8 +122,7 @@ extension Optional.OCombine {
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
guard let downstream = self.downstream else { return }
|
||||
self.downstream = nil
|
||||
guard let downstream = self.downstream.take() else { return }
|
||||
_ = downstream.receive(output)
|
||||
downstream.receive(completion: .finished)
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@ extension Publisher {
|
||||
/// Raises a fatal error when its upstream publisher fails, and otherwise republishes
|
||||
/// all received input.
|
||||
///
|
||||
/// Use `assertNoFailure()` for internal sanity checks that are active during testing.
|
||||
/// However, it is important to note that, like its Swift counterpart
|
||||
/// Use `assertNoFailure()` for internal integrity checks that are active during
|
||||
/// testing. However, it is important to note that, like its Swift counterpart
|
||||
/// `fatalError(_:)`, the `assertNoFailure()` operator asserts a fatal exception when
|
||||
/// triggered in both development/testing _and_ shipping versions of code.
|
||||
/// triggered during development and testing, _and_ in shipping versions of code.
|
||||
///
|
||||
/// In the example below, a `CurrentValueSubject` publishes the initial and second
|
||||
/// values successfully. The third value, containing a `genericSubjectError`, causes
|
||||
@@ -57,8 +57,8 @@ extension Publishers {
|
||||
/// A publisher that raises a fatal error upon receiving any failure, and otherwise
|
||||
/// republishes all received input.
|
||||
///
|
||||
/// Use this function for internal sanity checks that are active during testing but
|
||||
/// do not impact performance of shipping code.
|
||||
/// Use this function for internal integrity checks that are active during testing but
|
||||
/// don't affect performance of shipping code.
|
||||
public struct AssertNoFailure<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
@@ -331,9 +331,7 @@ extension Publishers.Buffer {
|
||||
private func lockedPop(_ demand: Subscribers.Demand) -> [Input] {
|
||||
assert(demand > 0)
|
||||
guard let max = demand.max else {
|
||||
let poppedValues = self.values
|
||||
self.values = []
|
||||
return poppedValues
|
||||
return values.take()
|
||||
}
|
||||
|
||||
let poppedValues = Array(values.prefix(max))
|
||||
|
||||
@@ -128,8 +128,7 @@ extension Publishers.CollectByCount {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
let output = self.buffer
|
||||
self.buffer = []
|
||||
let output = self.buffer.take()
|
||||
lock.unlock()
|
||||
return downstream.receive(output) * count
|
||||
}
|
||||
@@ -143,8 +142,7 @@ extension Publishers.CollectByCount {
|
||||
if buffer.isEmpty {
|
||||
lock.unlock()
|
||||
} else {
|
||||
let buffer = self.buffer
|
||||
self.buffer = []
|
||||
let buffer = self.buffer.take()
|
||||
lock.unlock()
|
||||
_ = downstream.receive(buffer)
|
||||
}
|
||||
@@ -168,10 +166,9 @@ extension Publishers.CollectByCount {
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
if let subscription = self.subscription {
|
||||
if let subscription = self.subscription.take() {
|
||||
buffer = []
|
||||
finished = true
|
||||
self.subscription = nil
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
} else {
|
||||
|
||||
@@ -233,7 +233,7 @@ extension Publishers.Concatenate {
|
||||
|
||||
private var suffixState = SubscriptionStatus.awaitingSubscription
|
||||
|
||||
private let suffix: Suffix
|
||||
private var suffix: Suffix?
|
||||
|
||||
private var pending = Subscribers.Demand.none
|
||||
|
||||
@@ -266,8 +266,18 @@ extension Publishers.Concatenate {
|
||||
prefixState.subscription ?? suffixState.subscription
|
||||
prefixState = .terminal
|
||||
suffixState = .terminal
|
||||
lock.unlock()
|
||||
upstreamSubscription?.cancel()
|
||||
|
||||
// We MUST release the object AFTER unlocking the lock,
|
||||
// since releasing it may trigger execution of arbitrary code,
|
||||
// for example, if the object has a deinit.
|
||||
// When the object deallocates, its deinit is called, and holding
|
||||
// the lock at that moment can lead to deadlocks.
|
||||
|
||||
withExtendedLifetime(suffix) {
|
||||
suffix = nil
|
||||
lock.unlock()
|
||||
upstreamSubscription?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
var description: String { return "Concatenate" }
|
||||
@@ -320,7 +330,7 @@ extension Publishers.Concatenate {
|
||||
lock.unlock()
|
||||
switch completion {
|
||||
case .finished:
|
||||
suffix.subscribe(SuffixSubscriber(inner: self))
|
||||
suffix?.subscribe(SuffixSubscriber(inner: self))
|
||||
case .failure:
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
@@ -212,8 +212,7 @@ extension Publishers.Debounce {
|
||||
let generation = currentGeneration
|
||||
currentValue = input
|
||||
let due = scheduler.now.advanced(by: dueTime)
|
||||
let previousCancellers = self.currentCancellers
|
||||
currentCancellers.removeAll()
|
||||
let previousCancellers = self.currentCancellers.take()
|
||||
currentCancellers[generation] = .pending
|
||||
lock.unlock()
|
||||
let newCanceller = scheduler.schedule(after: due,
|
||||
@@ -238,8 +237,7 @@ extension Publishers.Debounce {
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
let previousCancellers = currentCancellers
|
||||
currentCancellers.removeAll()
|
||||
let previousCancellers = currentCancellers.take()
|
||||
lock.unlock()
|
||||
for canceller in previousCancellers.values {
|
||||
canceller.cancel()
|
||||
@@ -268,8 +266,7 @@ extension Publishers.Debounce {
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
let previousCancellers = currentCancellers
|
||||
currentCancellers.removeAll()
|
||||
let previousCancellers = currentCancellers.take()
|
||||
lock.unlock()
|
||||
for canceller in previousCancellers.values {
|
||||
canceller.cancel()
|
||||
@@ -306,11 +303,10 @@ extension Publishers.Debounce {
|
||||
return
|
||||
}
|
||||
|
||||
guard let canceller = currentCancellers[generation] else {
|
||||
guard let canceller = currentCancellers[generation].take() else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
currentCancellers[generation] = nil
|
||||
|
||||
let hasAnyDemand = downstreamDemand != .none
|
||||
if hasAnyDemand {
|
||||
|
||||
@@ -139,8 +139,7 @@ extension Publishers.Drop {
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
let subscription = self.subscription
|
||||
self.subscription = nil
|
||||
let subscription = self.subscription.take()
|
||||
lock.unlock()
|
||||
subscription?.cancel()
|
||||
}
|
||||
|
||||
@@ -204,8 +204,7 @@ extension Publishers.DropUntilOutput {
|
||||
}
|
||||
|
||||
otherFinished = true
|
||||
if let upstreamSubscription = self.upstreamSubscription {
|
||||
self.upstreamSubscription = nil
|
||||
if let upstreamSubscription = self.upstreamSubscription.take() {
|
||||
lock.unlock()
|
||||
upstreamSubscription.cancel()
|
||||
} else {
|
||||
@@ -229,10 +228,8 @@ extension Publishers.DropUntilOutput {
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
let upstreamSubscription = self.upstreamSubscription
|
||||
let otherSubscription = self.otherSubscription
|
||||
self.upstreamSubscription = nil
|
||||
self.otherSubscription = nil
|
||||
let upstreamSubscription = self.upstreamSubscription.take()
|
||||
let otherSubscription = self.otherSubscription.take()
|
||||
cancelled = true
|
||||
lock.unlock()
|
||||
|
||||
|
||||
@@ -221,8 +221,7 @@ extension Publishers.${instantiation} {
|
||||
} catch {
|
||||
lock.lock()
|
||||
finished = true
|
||||
let subscription = self.subscription
|
||||
self.subscription = nil
|
||||
let subscription = self.subscription.take()
|
||||
lock.unlock()
|
||||
subscription?.cancel()
|
||||
downstream.receive(completion: .failure(error))
|
||||
@@ -251,11 +250,10 @@ extension Publishers.${instantiation} {
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard !finished, let subscription = self.subscription else {
|
||||
guard !finished, let subscription = self.subscription.take() else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
self.subscription = nil
|
||||
finished = true
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
|
||||
@@ -14,7 +14,7 @@ extension Publisher {
|
||||
/// the elements from one kind of publisher into a new publisher that is sent
|
||||
/// to subscribers. Use `flatMap(maxPublishers:_:)` when you want to create a new
|
||||
/// series of events for downstream subscribers based on the received value.
|
||||
/// The closure creates the new `Publishe`` based on the received value.
|
||||
/// The closure creates the new `Publisher` based on the received value.
|
||||
/// The new `Publisher` can emit more than one event, and successful completion of
|
||||
/// the new `Publisher` does not complete the overall stream.
|
||||
/// Failure of the new `Publisher` will fail the overall stream.
|
||||
@@ -304,8 +304,7 @@ extension Publishers.FlatMap {
|
||||
}
|
||||
if demand == .unlimited {
|
||||
downstreamDemand = .unlimited
|
||||
let buffer = self.buffer
|
||||
self.buffer = []
|
||||
let buffer = self.buffer.take()
|
||||
let subscriptions = self.subscriptions
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
@@ -361,10 +360,8 @@ extension Publishers.FlatMap {
|
||||
return
|
||||
}
|
||||
cancelledOrCompleted = true
|
||||
let subscriptions = self.subscriptions
|
||||
self.subscriptions = [:]
|
||||
let outerSubscription = self.outerSubscription
|
||||
self.outerSubscription = nil
|
||||
let subscriptions = self.subscriptions.take()
|
||||
let outerSubscription = self.outerSubscription.take()
|
||||
lock.unlock()
|
||||
for (_, subscription) in subscriptions {
|
||||
subscription.cancel()
|
||||
@@ -450,8 +447,7 @@ extension Publishers.FlatMap {
|
||||
return
|
||||
}
|
||||
cancelledOrCompleted = true
|
||||
let subscriptions = self.subscriptions
|
||||
self.subscriptions = [:]
|
||||
let subscriptions = self.subscriptions.take()
|
||||
lock.unlock()
|
||||
for (i, subscription) in subscriptions where i != index {
|
||||
subscription.cancel()
|
||||
|
||||
@@ -139,12 +139,12 @@ extension Publishers.HandleEvents {
|
||||
|
||||
private var status = SubscriptionStatus.awaitingSubscription
|
||||
private let lock = UnfairLock.allocate()
|
||||
public var receiveSubscription: ((Subscription) -> Void)?
|
||||
public var receiveOutput: ((Upstream.Output) -> Void)?
|
||||
public var receiveCompletion:
|
||||
fileprivate var receiveSubscription: ((Subscription) -> Void)?
|
||||
fileprivate var receiveOutput: ((Upstream.Output) -> Void)?
|
||||
fileprivate var receiveCompletion:
|
||||
((Subscribers.Completion<Upstream.Failure>) -> Void)?
|
||||
public var receiveCancel: (() -> Void)?
|
||||
public var receiveRequest: ((Subscribers.Demand) -> Void)?
|
||||
fileprivate var receiveCancel: (() -> Void)?
|
||||
fileprivate var receiveRequest: ((Subscribers.Demand) -> Void)?
|
||||
private let downstream: Downstream
|
||||
|
||||
init(_ events: Publishers.HandleEvents<Upstream>, downstream: Downstream) {
|
||||
|
||||
@@ -6,6 +6,8 @@ ${template_header}
|
||||
// Created by Sergej Jaskiewicz on 03/10/2019.
|
||||
//
|
||||
|
||||
// swiftlint:disable large_tuple
|
||||
|
||||
%{
|
||||
from gyb_opencombine_support import (
|
||||
suffix_variadic,
|
||||
|
||||
@@ -75,9 +75,7 @@ extension Publishers {
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Output == Downstream.Input, Downstream.Failure == Failure
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, output: output)
|
||||
upstream.subscribe(inner)
|
||||
subscriber.receive(subscription: inner)
|
||||
upstream.subscribe(Inner(downstream: subscriber, output: output))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,11 +121,8 @@ extension Publishers.ReplaceError {
|
||||
return
|
||||
}
|
||||
status = .subscribed(subscription)
|
||||
let pendingDemand = self.pendingDemand
|
||||
lock.unlock()
|
||||
if pendingDemand != .none {
|
||||
subscription.request(pendingDemand)
|
||||
}
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
@@ -150,7 +145,7 @@ extension Publishers.ReplaceError {
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
guard case .subscribed = status else {
|
||||
guard case .subscribed = status, !terminated else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -230,8 +230,7 @@ extension Publishers.SwitchToLatest {
|
||||
return .none
|
||||
}
|
||||
|
||||
if let currentInnerSubscription = self.currentInnerSubscription {
|
||||
self.currentInnerSubscription = nil
|
||||
if let currentInnerSubscription = self.currentInnerSubscription.take() {
|
||||
lock.unlock()
|
||||
currentInnerSubscription.cancel()
|
||||
lock.lock()
|
||||
@@ -272,8 +271,7 @@ extension Publishers.SwitchToLatest {
|
||||
lock.unlock()
|
||||
}
|
||||
case .failure:
|
||||
let currentInnerSubscription = self.currentInnerSubscription
|
||||
self.currentInnerSubscription = nil
|
||||
let currentInnerSubscription = self.currentInnerSubscription.take()
|
||||
sentCompletion = true
|
||||
lock.unlock()
|
||||
currentInnerSubscription?.cancel()
|
||||
@@ -298,10 +296,8 @@ extension Publishers.SwitchToLatest {
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
cancelled = true
|
||||
let currentInnerSubscription = self.currentInnerSubscription
|
||||
self.currentInnerSubscription = nil
|
||||
let outerSubscription = self.outerSubscription
|
||||
self.outerSubscription = nil
|
||||
let currentInnerSubscription = self.currentInnerSubscription.take()
|
||||
let outerSubscription = self.outerSubscription.take()
|
||||
lock.unlock()
|
||||
|
||||
currentInnerSubscription?.cancel()
|
||||
@@ -386,8 +382,7 @@ extension Publishers.SwitchToLatest {
|
||||
return
|
||||
}
|
||||
cancelled = true
|
||||
let outerSubscription = self.outerSubscription
|
||||
self.outerSubscription = nil
|
||||
let outerSubscription = self.outerSubscription.take()
|
||||
sentCompletion = true
|
||||
lock.unlock()
|
||||
outerSubscription?.cancel()
|
||||
|
||||
@@ -292,11 +292,8 @@ extension Publishers.Throttle {
|
||||
lastEmissionTime = scheduler.now
|
||||
}
|
||||
|
||||
let pendingInput = self.pendingInput
|
||||
let pendingCompletion = self.pendingCompletion
|
||||
|
||||
self.pendingInput = nil
|
||||
self.pendingCompletion = nil
|
||||
let pendingInput = self.pendingInput.take()
|
||||
let pendingCompletion = self.pendingCompletion.take()
|
||||
|
||||
if pendingCompletion != nil {
|
||||
state = .terminal
|
||||
|
||||
@@ -0,0 +1,720 @@
|
||||
//
|
||||
// Publishers.Zip.swift
|
||||
//
|
||||
// Created by Eric Patey on 29.08.2019.
|
||||
//
|
||||
|
||||
// swiftlint:disable large_tuple
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher created by applying the zip function to two upstream publishers.
|
||||
public struct Zip<UpstreamA: Publisher, UpstreamB: Publisher>: Publisher
|
||||
where UpstreamA.Failure == UpstreamB.Failure
|
||||
{
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = (UpstreamA.Output, UpstreamB.Output)
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = UpstreamA.Failure
|
||||
|
||||
public let a: UpstreamA
|
||||
|
||||
public let b: UpstreamB
|
||||
|
||||
public init(_ a: UpstreamA, _ b: UpstreamB) {
|
||||
self.a = a
|
||||
self.b = b
|
||||
}
|
||||
|
||||
/// This function is called to attach the specified `Subscriber` to this
|
||||
/// `Publisher` by `subscribe(_:)`
|
||||
///
|
||||
/// - SeeAlso: `subscribe(_:)`
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream) where
|
||||
UpstreamB.Failure == Downstream.Failure,
|
||||
Downstream.Input == (UpstreamA.Output, UpstreamB.Output)
|
||||
{
|
||||
_ = Inner<Downstream>(downstream: subscriber, a, b)
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher created by applying the zip function to three upstream publishers.
|
||||
public struct Zip3<UpstreamA: Publisher,
|
||||
UpstreamB: Publisher,
|
||||
UpstreamC: Publisher>
|
||||
: Publisher
|
||||
where UpstreamA.Failure == UpstreamB.Failure,
|
||||
UpstreamB.Failure == UpstreamC.Failure
|
||||
{
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = (UpstreamA.Output, UpstreamB.Output, UpstreamC.Output)
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = UpstreamA.Failure
|
||||
|
||||
public let a: UpstreamA
|
||||
|
||||
public let b: UpstreamB
|
||||
|
||||
public let c: UpstreamC
|
||||
|
||||
public init(_ a: UpstreamA, _ b: UpstreamB, _ c: UpstreamC) {
|
||||
self.a = a
|
||||
self.b = b
|
||||
self.c = c
|
||||
}
|
||||
|
||||
/// This function is called to attach the specified `Subscriber` to this
|
||||
/// `Publisher` by `subscribe(_:)`
|
||||
///
|
||||
/// - SeeAlso: `subscribe(_:)`
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<Downstream>(subscriber: Downstream)
|
||||
where Downstream: Subscriber,
|
||||
UpstreamC.Failure == Downstream.Failure,
|
||||
Downstream.Input == (UpstreamA.Output, UpstreamB.Output, UpstreamC.Output)
|
||||
{
|
||||
_ = Inner<Downstream>(downstream: subscriber, a, b, c)
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher created by applying the zip function to four upstream publishers.
|
||||
public struct Zip4<
|
||||
UpstreamA: Publisher,
|
||||
UpstreamB: Publisher,
|
||||
UpstreamC: Publisher,
|
||||
UpstreamD: Publisher
|
||||
>: Publisher where
|
||||
UpstreamA.Failure == UpstreamB.Failure,
|
||||
UpstreamB.Failure == UpstreamC.Failure,
|
||||
UpstreamC.Failure == UpstreamD.Failure
|
||||
{
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = (
|
||||
UpstreamA.Output,
|
||||
UpstreamB.Output,
|
||||
UpstreamC.Output,
|
||||
UpstreamD.Output)
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = UpstreamA.Failure
|
||||
|
||||
public let a: UpstreamA
|
||||
|
||||
public let b: UpstreamB
|
||||
|
||||
public let c: UpstreamC
|
||||
|
||||
public let d: UpstreamD
|
||||
|
||||
public init(_ a: UpstreamA, _ b: UpstreamB, _ c: UpstreamC, _ d: UpstreamD) {
|
||||
self.a = a
|
||||
self.b = b
|
||||
self.c = c
|
||||
self.d = d
|
||||
}
|
||||
|
||||
/// This function is called to attach the specified `Subscriber` to this
|
||||
/// `Publisher` by `subscribe(_:)`
|
||||
///
|
||||
/// - SeeAlso: `subscribe(_:)`
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where UpstreamD.Failure == Downstream.Failure,
|
||||
Downstream.Input == (
|
||||
UpstreamA.Output,
|
||||
UpstreamB.Output,
|
||||
UpstreamC.Output,
|
||||
UpstreamD.Output)
|
||||
{
|
||||
_ = Inner<Downstream>(downstream: subscriber, a, b, c, d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Combine elements from another publisher and deliver pairs of elements as tuples.
|
||||
///
|
||||
/// The returned publisher waits until both publishers have emitted an event, then
|
||||
/// delivers the oldest unconsumed event from each publisher together as a tuple to
|
||||
/// the subscriber.
|
||||
/// For example, if publisher `P1` emits elements `a` and `b`, and publisher `P2`
|
||||
/// emits event `c`, the zip publisher emits the tuple `(a, c)`. It won’t 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 won’t 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 won’t 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 won’t 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 won’t 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 won’t emit a tuple with elements `b`, `d`, or `f` until `P4`
|
||||
/// emits another event.
|
||||
/// If any upstream publisher finishes successfuly or fails with an error, the zipped
|
||||
/// publisher does the same.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - publisher1: A second publisher.
|
||||
/// - publisher2: A third publisher.
|
||||
/// - publisher3: A fourth publisher.
|
||||
/// - transform: A closure that receives the most recent value from each publisher
|
||||
/// and returns a new value to publish.
|
||||
/// - Returns: A publisher that emits groups of elements from the upstream publishers
|
||||
/// as tuples.
|
||||
public func zip<Other1, Other2, Other3, Result>(
|
||||
_ publisher1: Other1,
|
||||
_ publisher2: Other2,
|
||||
_ publisher3: Other3,
|
||||
_ transform: @escaping (Self.Output, Other1.Output, Other2.Output, Other3.Output)
|
||||
-> Result)
|
||||
-> Publishers.Map<Publishers.Zip4<Self, Other1, Other2, Other3>, Result>
|
||||
where Other1: Publisher,
|
||||
Other2: Publisher,
|
||||
Other3: Publisher,
|
||||
Self.Failure == Other1.Failure,
|
||||
Other1.Failure == Other2.Failure,
|
||||
Other2.Failure == Other3.Failure
|
||||
{
|
||||
return Publishers.Map(upstream: Publishers.Zip4(self,
|
||||
publisher1,
|
||||
publisher2,
|
||||
publisher3),
|
||||
transform: transform)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Zip {
|
||||
private class Inner<Downstream: Subscriber>: InnerBase<Downstream>
|
||||
where Downstream.Failure == Failure,
|
||||
Downstream.Input == (UpstreamA.Output, UpstreamB.Output)
|
||||
{
|
||||
private lazy var aSubscriber = ChildSubscriber<UpstreamA, Downstream>(self, 0)
|
||||
private lazy var bSubscriber = ChildSubscriber<UpstreamB, Downstream>(self, 1)
|
||||
|
||||
init(downstream: Downstream, _ a: UpstreamA, _ b: UpstreamB) {
|
||||
super.init(downstream: downstream)
|
||||
|
||||
a.subscribe(aSubscriber)
|
||||
b.subscribe(bSubscriber)
|
||||
}
|
||||
|
||||
override fileprivate var upstreamSubscriptions: [ChildSubscription] {
|
||||
return [aSubscriber, bSubscriber]
|
||||
}
|
||||
|
||||
override fileprivate func dequeueValue() -> Downstream.Input {
|
||||
return (aSubscriber.dequeueValue(), bSubscriber.dequeueValue())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Zip3 {
|
||||
private class Inner<Downstream: Subscriber>: InnerBase<Downstream>
|
||||
where Downstream.Failure == Failure,
|
||||
Downstream.Input == (UpstreamA.Output, UpstreamB.Output, UpstreamC.Output)
|
||||
{
|
||||
private lazy var aSubscriber = ChildSubscriber<UpstreamA, Downstream>(self, 0)
|
||||
private lazy var bSubscriber = ChildSubscriber<UpstreamB, Downstream>(self, 1)
|
||||
private lazy var cSubscriber = ChildSubscriber<UpstreamC, Downstream>(self, 2)
|
||||
|
||||
init(downstream: Downstream, _ a: UpstreamA, _ b: UpstreamB, _ c: UpstreamC) {
|
||||
super.init(downstream: downstream)
|
||||
|
||||
a.subscribe(aSubscriber)
|
||||
b.subscribe(bSubscriber)
|
||||
c.subscribe(cSubscriber)
|
||||
}
|
||||
|
||||
override fileprivate var upstreamSubscriptions: [ChildSubscription] {
|
||||
return [aSubscriber, bSubscriber, cSubscriber]
|
||||
}
|
||||
|
||||
override fileprivate func dequeueValue() -> Downstream.Input {
|
||||
return (aSubscriber.dequeueValue(),
|
||||
bSubscriber.dequeueValue(),
|
||||
cSubscriber.dequeueValue())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Zip4 {
|
||||
private class Inner<Downstream: Subscriber>: InnerBase<Downstream>
|
||||
where Downstream.Failure == Failure,
|
||||
Downstream.Input == (
|
||||
UpstreamA.Output,
|
||||
UpstreamB.Output,
|
||||
UpstreamC.Output,
|
||||
UpstreamD.Output)
|
||||
{
|
||||
private lazy var aSubscriber = ChildSubscriber<UpstreamA, Downstream>(self, 0)
|
||||
private lazy var bSubscriber = ChildSubscriber<UpstreamB, Downstream>(self, 1)
|
||||
private lazy var cSubscriber = ChildSubscriber<UpstreamC, Downstream>(self, 2)
|
||||
private lazy var dSubscriber = ChildSubscriber<UpstreamD, Downstream>(self, 3)
|
||||
|
||||
init(downstream: Downstream,
|
||||
_ a: UpstreamA,
|
||||
_ b: UpstreamB,
|
||||
_ c: UpstreamC,
|
||||
_ d: UpstreamD)
|
||||
{
|
||||
super.init(downstream: downstream)
|
||||
|
||||
a.subscribe(aSubscriber)
|
||||
b.subscribe(bSubscriber)
|
||||
c.subscribe(cSubscriber)
|
||||
d.subscribe(dSubscriber)
|
||||
}
|
||||
|
||||
override fileprivate var upstreamSubscriptions: [ChildSubscription] {
|
||||
return [aSubscriber, bSubscriber, cSubscriber, dSubscriber]
|
||||
}
|
||||
|
||||
override fileprivate func dequeueValue() -> Downstream.Input {
|
||||
return (aSubscriber.dequeueValue(),
|
||||
bSubscriber.dequeueValue(),
|
||||
cSubscriber.dequeueValue(),
|
||||
dSubscriber.dequeueValue())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class InnerBase<Downstream: Subscriber>: CustomStringConvertible {
|
||||
let description = "Zip"
|
||||
|
||||
private let lock = UnfairRecursiveLock.allocate()
|
||||
|
||||
private let downstream: Downstream
|
||||
private var downstreamDemand = Subscribers.Demand.none
|
||||
private var valueIsBeingProcessed = false
|
||||
private var value: Downstream.Input?
|
||||
private var isFinished = false
|
||||
|
||||
// The following two pieces of state are a hacky implementation of subtle Apple
|
||||
// concurrency behaviors. Specifically, when Zip is processing an upstream child value
|
||||
// and sending a resulting value downstream, multiple behaviors are changed.
|
||||
// 1. If a downstream demand request comes in during this period, the demand request
|
||||
// for that specific triggering upstream child will be communiated via the result
|
||||
// of `.receive(_ input:)` INSTEAD of a later `.request(_ demand:)` call.
|
||||
// (AppleRef: 001)
|
||||
// 2. If an upstream `.finished` comes in during this time period, the "finished
|
||||
// asssessment check" (AppleRef: 002) is skipped.
|
||||
// If an upstream value is being processed when a downstream demand request comes in,
|
||||
// the demand for that specfic upstream child will be communiated via the result
|
||||
// of `.receive(_ input:)` INSTEAD of a later `.request(_ demand:)` call.
|
||||
private final var processingValueForChild: ChildSubscription?
|
||||
private final var demandReceivedWhileProcessing: Subscribers.Demand?
|
||||
|
||||
init(downstream: Downstream) {
|
||||
self.downstream = downstream
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
fileprivate var upstreamSubscriptions: [ChildSubscription] {
|
||||
abstractMethod()
|
||||
}
|
||||
|
||||
fileprivate func dequeueValue() -> Downstream.Input {
|
||||
abstractMethod()
|
||||
}
|
||||
|
||||
fileprivate final func receivedSubscription(for child: ChildSubscription) {
|
||||
lock.lock()
|
||||
child.state = .active
|
||||
let sendSubscriptionDownstream = upstreamSubscriptions
|
||||
.filter { $0.state == .waitingForSubscription }
|
||||
.isEmpty
|
||||
lock.unlock()
|
||||
|
||||
if sendSubscriptionDownstream {
|
||||
self.sendSubscriptionDownstream()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate final func receivedChildValue(
|
||||
child: ChildSubscription,
|
||||
_ lockedStoreValue: () -> Void
|
||||
) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
lockedStoreValue()
|
||||
defer {
|
||||
checkShouldFinish()
|
||||
lock.unlock()
|
||||
}
|
||||
if let dequeuedValue = maybeDequeueValue() {
|
||||
value = dequeuedValue
|
||||
assert(processingValueForChild == nil)
|
||||
processingValueForChild = child
|
||||
valueIsBeingProcessed = true
|
||||
return processValue() ?? .none
|
||||
} else {
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate final func receivedCompletion(
|
||||
_ completion: Subscribers.Completion<Downstream.Failure>,
|
||||
forChild child: ChildSubscription)
|
||||
{
|
||||
switch completion {
|
||||
case .failure:
|
||||
downstream.receive(completion: completion)
|
||||
lock.lock()
|
||||
child.state = .failed
|
||||
let subscriptionsToCancel = upstreamSubscriptions
|
||||
lock.unlock()
|
||||
subscriptionsToCancel.forEach { $0.cancel() }
|
||||
case .finished:
|
||||
lock.lock()
|
||||
child.state = .finished
|
||||
if !valueIsBeingProcessed {
|
||||
valueIsBeingProcessed = true
|
||||
if processingValueForChild == nil &&
|
||||
!areMoreValuesPossible &&
|
||||
!isFinished {
|
||||
sendFinishDownstream()
|
||||
} else {
|
||||
processValue()
|
||||
}
|
||||
}
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
private func checkShouldFinish() {
|
||||
if processingValueForChild == nil && upstreamSubscriptions.shouldFinish() {
|
||||
sendFinishDownstream()
|
||||
isFinished = true
|
||||
}
|
||||
}
|
||||
|
||||
private func maybeDequeueValue() -> Downstream.Input? {
|
||||
return hasCompleteValueAvailable ? dequeueValue() : nil
|
||||
}
|
||||
|
||||
private func sendSubscriptionDownstream() {
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
private var hasCompleteValueAvailable: Bool {
|
||||
return upstreamSubscriptions.allSatisfy { $0.hasValue }
|
||||
}
|
||||
|
||||
private var areMoreValuesPossible: Bool {
|
||||
// More values are possible if all children are (active || have surplus)
|
||||
return upstreamSubscriptions
|
||||
.allSatisfy { $0.state == .active || $0.hasValue }
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func processValue() -> Subscribers.Demand? {
|
||||
assert(valueIsBeingProcessed)
|
||||
|
||||
lock.lock()
|
||||
defer {
|
||||
valueIsBeingProcessed = false
|
||||
processingValueForChild = nil
|
||||
demandReceivedWhileProcessing = nil
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
if let value = self.value {
|
||||
if downstreamDemand != .none {
|
||||
downstreamDemand -= 1
|
||||
}
|
||||
let newDemand = downstream.receive(value)
|
||||
if newDemand != .none {
|
||||
downstreamDemand += newDemand
|
||||
demandReceivedWhileProcessing = newDemand
|
||||
}
|
||||
self.value = nil
|
||||
}
|
||||
|
||||
return demandReceivedWhileProcessing
|
||||
}
|
||||
|
||||
private func sendRequestUpstream(demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
let subscriptionsToRequest = upstreamSubscriptions
|
||||
.filter { $0.childIndex != processingValueForChild?.childIndex }
|
||||
lock.unlock()
|
||||
subscriptionsToRequest.forEach { $0.request(demand) }
|
||||
}
|
||||
|
||||
private func sendFinishDownstream() {
|
||||
downstream.receive(completion: .finished)
|
||||
lock.lock()
|
||||
let activeChildren = upstreamSubscriptions.filter { $0.state == .active }
|
||||
lock.unlock()
|
||||
activeChildren.forEach { $0.cancel() }
|
||||
}
|
||||
}
|
||||
|
||||
extension InnerBase: Subscription {
|
||||
fileprivate final func request(_ demand: Subscribers.Demand) {
|
||||
guard demand != .none else {
|
||||
fatalError()
|
||||
}
|
||||
lock.lock()
|
||||
downstreamDemand += demand
|
||||
sendRequestUpstream(demand: demand)
|
||||
if valueIsBeingProcessed {
|
||||
demandReceivedWhileProcessing = demand
|
||||
} else {
|
||||
valueIsBeingProcessed = true
|
||||
processValue()
|
||||
}
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
fileprivate final func cancel() {
|
||||
lock.lock()
|
||||
let subscriptionsToCancel = upstreamSubscriptions
|
||||
lock.unlock()
|
||||
subscriptionsToCancel.forEach { $0.cancel() }
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == ChildSubscription {
|
||||
func shouldFinish() -> Bool {
|
||||
for subscription in self
|
||||
where subscription.state == .finished && !subscription.hasValue{
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private enum ChildState {
|
||||
case waitingForSubscription
|
||||
case active
|
||||
case finished
|
||||
case failed
|
||||
case canceled
|
||||
}
|
||||
|
||||
// Note that it's critical that this protocol not have any associated types - specifically
|
||||
// note that it does not refer to `Upstream`.
|
||||
// This allows `InnerBase` to do most of the heavy lifting without regard to the
|
||||
// upstream publisher's value type.
|
||||
private protocol ChildSubscription: AnyObject, Subscription {
|
||||
var state: ChildState { get set }
|
||||
var childIndex: Int { get }
|
||||
var hasValue: Bool { get }
|
||||
}
|
||||
|
||||
private final class ChildSubscriber<Upstream: Publisher, Downstream: Subscriber>
|
||||
where Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
fileprivate final var state: ChildState = .waitingForSubscription
|
||||
fileprivate final var upstreamSubscription: Subscription?
|
||||
private var values = [Upstream.Output]()
|
||||
private unowned let parent: InnerBase<Downstream>
|
||||
fileprivate let childIndex: Int
|
||||
|
||||
init(_ parent: InnerBase<Downstream>, _ childIndex: Int) {
|
||||
self.parent = parent
|
||||
self.childIndex = childIndex
|
||||
}
|
||||
|
||||
fileprivate final func dequeueValue() -> Upstream.Output {
|
||||
return values.remove(at: 0)
|
||||
}
|
||||
}
|
||||
|
||||
extension ChildSubscriber: ChildSubscription {
|
||||
fileprivate final var hasValue: Bool {
|
||||
return !values.isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
extension ChildSubscriber: Subscription {
|
||||
fileprivate final func request(_ demand: Subscribers.Demand) {
|
||||
upstreamSubscription?.request(demand)
|
||||
}
|
||||
}
|
||||
|
||||
extension ChildSubscriber: Cancellable {
|
||||
fileprivate final func cancel() {
|
||||
upstreamSubscription?.cancel()
|
||||
upstreamSubscription = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension ChildSubscriber: Subscriber {
|
||||
fileprivate final func receive(subscription: Subscription) {
|
||||
if upstreamSubscription == nil {
|
||||
upstreamSubscription = subscription
|
||||
parent.receivedSubscription(for: self)
|
||||
} else {
|
||||
subscription.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate final func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return parent.receivedChildValue(child: self) { values.append(input) }
|
||||
}
|
||||
|
||||
fileprivate final func receive(completion: Subscribers.Completion<Failure>) {
|
||||
parent.receivedCompletion(completion, forChild: self)
|
||||
}
|
||||
}
|
||||
@@ -136,8 +136,7 @@ extension Result.OCombine {
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
guard let downstream = self.downstream else { return }
|
||||
self.downstream = nil
|
||||
guard let downstream = self.downstream.take() else { return }
|
||||
_ = downstream.receive(output)
|
||||
downstream.receive(completion: .finished)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
${template_header}
|
||||
//
|
||||
// RootProtocols.swift
|
||||
// OpenCombine
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.06.2019.
|
||||
//
|
||||
|
||||
%{
|
||||
variants = [(True, '#if compiler(>=5.7)'), (False, '#else')]
|
||||
}%
|
||||
% for primary_associated_types_supported, guard in variants:
|
||||
${guard}
|
||||
/// Declares that a type can transmit a sequence of values over time.
|
||||
///
|
||||
/// A publisher delivers elements to one or more `Subscriber` instances.
|
||||
/// The subscriber’s `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 subject’s 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 property’s 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 doesn’t 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 subscriber’s
|
||||
/// `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 publisher’s `subscribe(_:)`
|
||||
/// method. After making this call, the publisher invokes the subscriber’s
|
||||
/// `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 scheduler’s 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
|
||||
@@ -29,51 +29,6 @@ public protocol SchedulerTimeIntervalConvertible {
|
||||
static func nanoseconds(_ ns: Int) -> Self
|
||||
}
|
||||
|
||||
/// A protocol that defines when and how to execute a closure.
|
||||
///
|
||||
/// You can use a scheduler to execute code as soon as possible, or after a future date.
|
||||
/// Individual scheduler implementations use whatever time-keeping system makes sense
|
||||
/// for them. Schedulers express this as their `SchedulerTimeType`. Since this type
|
||||
/// conforms to `SchedulerTimeIntervalConvertible`, you can always express these times
|
||||
/// with the convenience functions like `.milliseconds(500)`. Schedulers can accept
|
||||
/// options to control how they execute the actions passed to them. These options may
|
||||
/// control factors like which threads or dispatch queues execute the actions.
|
||||
public protocol Scheduler {
|
||||
|
||||
/// Describes an instant in time for this scheduler.
|
||||
associatedtype SchedulerTimeType: Strideable
|
||||
where SchedulerTimeType.Stride: SchedulerTimeIntervalConvertible
|
||||
|
||||
/// A type that defines options accepted by the scheduler.
|
||||
///
|
||||
/// This type is freely definable by each `Scheduler`. Typically, operations that
|
||||
/// take a `Scheduler` parameter will also take `SchedulerOptions`.
|
||||
associatedtype SchedulerOptions
|
||||
|
||||
/// This scheduler’s 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 scheduler’s
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// Subject+Void.swift
|
||||
// OpenCombine
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.06.2019.
|
||||
//
|
||||
|
||||
extension Subject where Output == Void {
|
||||
|
||||
/// Sends a void value to the subscriber.
|
||||
///
|
||||
/// Use `Void` inputs and outputs when you want to signal that an event has occurred,
|
||||
/// but don’t need to send the event itself.
|
||||
public func send() {
|
||||
send(())
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
//
|
||||
// Subject.swift
|
||||
// OpenCombine
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.06.2019.
|
||||
//
|
||||
|
||||
/// A publisher that exposes a method for outside callers to publish elements.
|
||||
///
|
||||
/// A subject is a publisher that you can use to ”inject” values into a stream, by calling
|
||||
/// its `send()` method. This can be useful for adapting existing imperative code to the
|
||||
/// Combine model.
|
||||
public protocol Subject: AnyObject, Publisher {
|
||||
|
||||
/// Sends a value to the subscriber.
|
||||
///
|
||||
/// - Parameter value: The value to send.
|
||||
func send(_ value: Output)
|
||||
|
||||
/// Sends a completion signal to the subscriber.
|
||||
///
|
||||
/// - Parameter completion: A `Completion` instance which indicates whether publishing
|
||||
/// has finished normally or failed with an error.
|
||||
func send(completion: Subscribers.Completion<Failure>)
|
||||
|
||||
/// Sends a subscription to the subscriber.
|
||||
///
|
||||
/// This call provides the `Subject` an opportunity to establish demand for any new
|
||||
/// upstream subscriptions.
|
||||
///
|
||||
/// - Parameter subscription: The subscription instance through which the subscriber
|
||||
/// can request elements.
|
||||
func send(subscription: Subscription)
|
||||
}
|
||||
|
||||
extension Subject where Output == Void {
|
||||
|
||||
/// Sends a void value to the subscriber.
|
||||
///
|
||||
/// Use `Void` inputs and outputs when you want to signal that an event has occurred,
|
||||
/// but don’t need to send the event itself.
|
||||
public func send() {
|
||||
send(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// Subscriber+Void.swift
|
||||
// OpenCombine
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.06.2019.
|
||||
//
|
||||
|
||||
extension Subscriber where Input == Void {
|
||||
|
||||
/// Tells the subscriber that a publisher of void elements is ready to receive further
|
||||
/// requests.
|
||||
///
|
||||
/// Use `Void` inputs and outputs when you want to signal that an event has occurred,
|
||||
/// but don’t 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(())
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
//
|
||||
// Subscriber.swift
|
||||
// OpenCombine
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.06.2019.
|
||||
//
|
||||
|
||||
/// A protocol that declares a type that can receive input from a publisher.
|
||||
///
|
||||
/// A `Subscriber` instance receives a stream of elements from a `Publisher`, along with
|
||||
/// life cycle events describing changes to their relationship. A given subscriber’s
|
||||
/// `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 publisher’s `subscribe(_:)`
|
||||
/// method. After making this call, the publisher invokes the subscriber’s
|
||||
/// `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 don’t need to send the event itself.
|
||||
/// - Returns: A `Subscribers.Demand` instance indicating how many more elements
|
||||
/// the subscriber expects to receive.
|
||||
public func receive() -> Subscribers.Demand {
|
||||
return receive(())
|
||||
}
|
||||
}
|
||||
@@ -112,8 +112,17 @@ extension Subscribers {
|
||||
lock.assertOwner()
|
||||
#endif
|
||||
status = .terminal
|
||||
object = nil
|
||||
lock.unlock()
|
||||
|
||||
// We MUST release the object AFTER unlocking the lock,
|
||||
// since releasing it may trigger execution of arbitrary code,
|
||||
// for example, if the object has a deinit.
|
||||
// When the object deallocates, its deinit is called, and holding
|
||||
// the lock at that moment can lead to deadlocks.
|
||||
|
||||
withExtendedLifetime(object) {
|
||||
object = nil
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
#if canImport(_Concurrency) && compiler(>=5.5)
|
||||
import _Concurrency
|
||||
#endif
|
||||
|
||||
extension Subscribers {
|
||||
|
||||
/// A signal that a publisher doesn’t produce additional elements, either due to
|
||||
@@ -23,6 +27,10 @@ extension Subscribers.Completion: Equatable where Failure: Equatable {}
|
||||
|
||||
extension Subscribers.Completion: Hashable where Failure: Hashable {}
|
||||
|
||||
#if canImport(_Concurrency) && compiler(>=5.5) || compiler(>=5.5.1)
|
||||
extension Subscribers.Completion: Sendable {}
|
||||
#endif
|
||||
|
||||
extension Subscribers.Completion {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case success = "success"
|
||||
@@ -70,4 +78,13 @@ extension Subscribers.Completion {
|
||||
return .failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
internal var failure: Failure? {
|
||||
switch self {
|
||||
case .finished:
|
||||
return nil
|
||||
case .failure(let failure):
|
||||
return failure
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
// Created by Sergej Jaskiewicz on 10.06.2019.
|
||||
//
|
||||
|
||||
// swiftlint:disable shorthand_operator - because of false positives here
|
||||
// swiftlint:disable attributes
|
||||
|
||||
#if canImport(_Concurrency) && compiler(>=5.5)
|
||||
import _Concurrency
|
||||
#endif
|
||||
|
||||
extension Subscribers {
|
||||
|
||||
@@ -466,3 +470,7 @@ extension Subscribers {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(_Concurrency) && compiler(>=5.5) || compiler(>=5.5.1)
|
||||
extension Subscribers.Demand: Sendable {}
|
||||
#endif
|
||||
|
||||
@@ -73,8 +73,22 @@ extension Subscribers {
|
||||
|
||||
public func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
status = .terminal
|
||||
let receiveCompletion = self.receiveCompletion
|
||||
terminateAndConsumeLock()
|
||||
self.receiveCompletion = { _ in }
|
||||
|
||||
// We MUST release the closures AFTER unlocking the lock,
|
||||
// since releasing a closure may trigger execution of arbitrary code,
|
||||
// for example, if the closure captures an object with a deinit.
|
||||
// When closure deallocates, the object's deinit is called, and holding
|
||||
// the lock at that moment can lead to deadlocks.
|
||||
// See https://github.com/OpenCombine/OpenCombine/issues/208
|
||||
|
||||
withExtendedLifetime(receiveValue) {
|
||||
receiveValue = { _ in }
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
receiveCompletion(completion)
|
||||
}
|
||||
|
||||
@@ -84,18 +98,21 @@ extension Subscribers {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
terminateAndConsumeLock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
private func terminateAndConsumeLock() {
|
||||
#if DEBUG
|
||||
lock.assertOwner()
|
||||
#endif
|
||||
status = .terminal
|
||||
receiveValue = { _ in }
|
||||
receiveCompletion = { _ in }
|
||||
lock.unlock()
|
||||
|
||||
// We MUST release the closures AFTER unlocking the lock,
|
||||
// since releasing a closure may trigger execution of arbitrary code,
|
||||
// for example, if the closure captures an object with a deinit.
|
||||
// When closure deallocates, the object's deinit is called, and holding
|
||||
// the lock at that moment can lead to deadlocks.
|
||||
// See https://github.com/OpenCombine/OpenCombine/issues/208
|
||||
|
||||
withExtendedLifetime((receiveValue, receiveCompletion)) {
|
||||
receiveCompletion = { _ in }
|
||||
receiveValue = { _ in }
|
||||
lock.unlock()
|
||||
}
|
||||
subscription.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// Created by Sergej Jaskiewicz on 27.09.2020.
|
||||
//
|
||||
|
||||
// swiftlint:disable:next type_name
|
||||
public protocol _Introspection: AnyObject {
|
||||
|
||||
func willReceive<Upstream: Publisher, Downstream: Subscriber>(
|
||||
|
||||
@@ -50,13 +50,7 @@ extension DispatchQueue {
|
||||
/// - Parameter other: Another dispatch queue time.
|
||||
/// - Returns: The time interval between this time and the provided time.
|
||||
public func distance(to other: SchedulerTimeType) -> Stride {
|
||||
let start = dispatchTime.rawValue
|
||||
let end = other.dispatchTime.rawValue
|
||||
return .nanoseconds(
|
||||
end >= start
|
||||
? Int(Int64(bitPattern: end) - Int64(bitPattern: start))
|
||||
: -Int(Int64(bitPattern: start) - Int64(bitPattern: end))
|
||||
)
|
||||
return Stride(dispatchTime.polyfillDistance(to: other.dispatchTime))
|
||||
}
|
||||
|
||||
/// Returns a dispatch queue scheduler time calculated by advancing
|
||||
@@ -104,8 +98,17 @@ extension DispatchQueue {
|
||||
/// value of the conforming type.
|
||||
public typealias Magnitude = Int
|
||||
|
||||
private var _nanoseconds: Int64
|
||||
|
||||
/// The value of this time interval in nanoseconds.
|
||||
public var magnitude: Int
|
||||
public var magnitude: Int {
|
||||
get {
|
||||
return Int(_nanoseconds)
|
||||
}
|
||||
set {
|
||||
_nanoseconds = Int64(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
/// A `DispatchTimeInterval` created with the value of this type
|
||||
/// in nanoseconds.
|
||||
@@ -113,8 +116,8 @@ extension DispatchQueue {
|
||||
return .nanoseconds(magnitude)
|
||||
}
|
||||
|
||||
private init(magnitude: Int) {
|
||||
self.magnitude = magnitude
|
||||
private init(magnitude: Int64) {
|
||||
_nanoseconds = magnitude
|
||||
}
|
||||
|
||||
/// Creates a dispatch queue time interval from the given
|
||||
@@ -204,7 +207,7 @@ extension DispatchQueue {
|
||||
}
|
||||
|
||||
public static func < (lhs: Stride, rhs: Stride) -> Bool {
|
||||
return lhs.magnitude < rhs.magnitude
|
||||
return lhs._nanoseconds < rhs._nanoseconds
|
||||
}
|
||||
|
||||
public static func * (lhs: Stride, rhs: Stride) -> Stride {
|
||||
@@ -212,43 +215,51 @@ extension DispatchQueue {
|
||||
}
|
||||
|
||||
public static func + (lhs: Stride, rhs: Stride) -> Stride {
|
||||
return Stride(magnitude: lhs.magnitude + rhs.magnitude)
|
||||
return Stride(magnitude: lhs._nanoseconds + rhs._nanoseconds)
|
||||
}
|
||||
|
||||
public static func - (lhs: Stride, rhs: Stride) -> Stride {
|
||||
return Stride(magnitude: lhs.magnitude - rhs.magnitude)
|
||||
return Stride(magnitude: lhs._nanoseconds - rhs._nanoseconds)
|
||||
}
|
||||
|
||||
public static func -= (lhs: inout Stride, rhs: Stride) {
|
||||
lhs.magnitude -= rhs.magnitude
|
||||
lhs._nanoseconds -= rhs._nanoseconds
|
||||
}
|
||||
|
||||
public static func *= (lhs: inout Stride, rhs: Stride) {
|
||||
lhs.magnitude = 0
|
||||
lhs._nanoseconds = 0
|
||||
}
|
||||
|
||||
public static func += (lhs: inout Stride, rhs: Stride) {
|
||||
lhs.magnitude += rhs.magnitude
|
||||
lhs._nanoseconds += rhs._nanoseconds
|
||||
}
|
||||
|
||||
public static func seconds(_ value: Double) -> Stride {
|
||||
return Stride(magnitude: Int(value * 1_000_000_000))
|
||||
let nanoseconds = value * 1_000_000_000
|
||||
if nanoseconds >= Double(Int64.max) {
|
||||
return Stride(magnitude: .max)
|
||||
}
|
||||
if nanoseconds <= Double(Int64.min) {
|
||||
return Stride(magnitude: .min)
|
||||
}
|
||||
return Stride(magnitude: Int64(nanoseconds))
|
||||
}
|
||||
|
||||
public static func seconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: clampedIntProduct(value, 1_000_000_000))
|
||||
return Stride(magnitude: clampedIntProduct(Int64(value),
|
||||
1_000_000_000))
|
||||
}
|
||||
|
||||
public static func milliseconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: clampedIntProduct(value, 1_000_000))
|
||||
return Stride(magnitude: clampedIntProduct(Int64(value), 1_000_000))
|
||||
}
|
||||
|
||||
public static func microseconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: clampedIntProduct(value, 1_000))
|
||||
return Stride(magnitude: clampedIntProduct(Int64(value), 1_000))
|
||||
}
|
||||
|
||||
public static func nanoseconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: value)
|
||||
return Stride(magnitude: Int64(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -387,10 +398,10 @@ extension DispatchQueue: OpenCombine.Scheduler {
|
||||
// This function is taken from swift-corelibs-libdispatch:
|
||||
// https://github.com/apple/swift-corelibs-libdispatch/blob/c992dacf3ca114806e6ac9ffc9113b19255be9fe/src/swift/Time.swift#L134-L144
|
||||
//
|
||||
// Returns m1 * m2, clamped to the range [Int.min, Int.max].
|
||||
// Returns m1 * m2, clamped to the range [Int64.min, Int64.max].
|
||||
// Because of the way this function is used, we can always assume
|
||||
// that m2 > 0.
|
||||
private func clampedIntProduct(_ lhs: Int, _ rhs: Int) -> Int {
|
||||
private func clampedIntProduct(_ lhs: Int64, _ rhs: Int64) -> Int64 {
|
||||
assert(rhs > 0, "multiplier must be positive")
|
||||
let (result, overflow) = lhs.multipliedReportingOverflow(by: rhs)
|
||||
if overflow {
|
||||
@@ -398,3 +409,31 @@ private func clampedIntProduct(_ lhs: Int, _ rhs: Int) -> Int {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
extension DispatchTime {
|
||||
|
||||
fileprivate func polyfillDistance(to other: DispatchTime) -> DispatchTimeInterval {
|
||||
#if canImport(Darwin) && compiler(>=5.1)
|
||||
if #available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) {
|
||||
return distance(to: other)
|
||||
}
|
||||
#endif
|
||||
let start = rawValue
|
||||
let end = other.rawValue
|
||||
if end >= start {
|
||||
let result = end &- start
|
||||
if result > UInt64(Int.max) {
|
||||
return .never
|
||||
} else {
|
||||
return .nanoseconds(Int(result))
|
||||
}
|
||||
} else {
|
||||
let result = start &- end
|
||||
if result > UInt64(Int.max) {
|
||||
return .never
|
||||
} else {
|
||||
return .nanoseconds(-Int(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
// Created by Sergej Jaskiewicz on 28.10.2020.
|
||||
//
|
||||
|
||||
#if canImport(CoreFoundation)
|
||||
import CoreFoundation
|
||||
#endif
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Use CoreFoundation on Darwin, since some pure
|
||||
@@ -93,6 +96,7 @@ internal struct Timer {
|
||||
#endif
|
||||
}
|
||||
|
||||
#if canImport(CoreFoundation)
|
||||
fileprivate func getCFRunLoopTimer() -> CFRunLoopTimer? {
|
||||
#if canImport(Darwin)
|
||||
return underlyingTimer
|
||||
@@ -113,6 +117,7 @@ internal struct Timer {
|
||||
fatalError("unreachable")
|
||||
#endif
|
||||
}
|
||||
#endif // canImport(CoreFoundation)
|
||||
}
|
||||
|
||||
extension RunLoop {
|
||||
@@ -138,6 +143,7 @@ extension RunLoop {
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(CoreFoundation)
|
||||
extension RunLoop.Mode {
|
||||
fileprivate func asCFRunLoopMode() -> CFRunLoopMode {
|
||||
#if canImport(Darwin)
|
||||
@@ -159,3 +165,4 @@ extension RunLoop.Mode {
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif // canImport(CoreFoundation)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// Utils.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 28.08.2021.
|
||||
//
|
||||
|
||||
extension Optional {
|
||||
internal mutating func take() -> Optional {
|
||||
let taken = self
|
||||
self = nil
|
||||
return taken
|
||||
}
|
||||
}
|
||||
@@ -200,13 +200,13 @@ extension Notification {
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard let center = self.center, let observation = self.observation else {
|
||||
guard let center = self.center.take(),
|
||||
let observation = self.observation.take()
|
||||
else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
self.center = nil
|
||||
self.object = nil
|
||||
self.observation = nil
|
||||
lock.unlock()
|
||||
center.removeObserver(observation)
|
||||
}
|
||||
|
||||
@@ -234,19 +234,16 @@ extension OperationQueue {
|
||||
}
|
||||
|
||||
override func main() {
|
||||
guard let action = self.action else { return }
|
||||
self.action = nil
|
||||
guard let action = self.action.take() else { return }
|
||||
action()
|
||||
|
||||
guard let queue = self.queue,
|
||||
let context = self.context
|
||||
guard let queue = self.queue.take(),
|
||||
let context = self.context.take()
|
||||
else {
|
||||
self.queue = nil
|
||||
self.context = nil
|
||||
return
|
||||
}
|
||||
self.queue = nil
|
||||
self.context = nil
|
||||
|
||||
context.lock.lock()
|
||||
if context.operation == nil {
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 13.12.2019.
|
||||
//
|
||||
|
||||
import CoreFoundation
|
||||
import Foundation
|
||||
import OpenCombine
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 23.06.2020.
|
||||
//
|
||||
|
||||
import CoreFoundation
|
||||
import Foundation
|
||||
import OpenCombine
|
||||
|
||||
@@ -203,11 +202,10 @@ extension Foundation.Timer {
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
if downstream == nil {
|
||||
if downstream.take() == nil {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
downstream = nil
|
||||
lock.unlock()
|
||||
parent?.disconnect(combineIdentifier)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
//
|
||||
// FutureConcurrencyTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 12.12.2021.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if canImport(_Concurrency) && compiler(>=5.5)
|
||||
import _Concurrency
|
||||
#endif
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
// swiftlint:disable:next line_length
|
||||
#if !os(Windows) && !WASI && (canImport(_Concurrency) && compiler(>=5.5) || compiler(>=5.5.1)) // TEST_DISCOVERY_CONDITION
|
||||
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
||||
final class FutureConcurrencyTests: XCTestCase {
|
||||
|
||||
func testAsyncAwaitNonThrowingSuccess() async {
|
||||
var promise: Future<Int, Never>.Promise?
|
||||
let future = Future<Int, Never> { promise = $0 }
|
||||
|
||||
let task = Task {
|
||||
await future.value
|
||||
}
|
||||
|
||||
promise?(.success(42))
|
||||
|
||||
let value = await task.value
|
||||
|
||||
XCTAssertEqual(value, 42)
|
||||
}
|
||||
|
||||
func testAsyncAwaitThrowingSuccess() async throws {
|
||||
var promise: Future<Int, TestingError>.Promise?
|
||||
let future = Future<Int, TestingError> { promise = $0 }
|
||||
|
||||
let task = Task {
|
||||
try await future.value
|
||||
}
|
||||
|
||||
promise?(.success(42))
|
||||
|
||||
let value = try await task.value
|
||||
|
||||
XCTAssertEqual(value, 42)
|
||||
}
|
||||
|
||||
func testAsyncAwaitThrowingFailure() async throws {
|
||||
var promise: Future<Int, TestingError>.Promise?
|
||||
let future = Future<Int, TestingError> { promise = $0 }
|
||||
|
||||
let task = Task { try await future.value }
|
||||
|
||||
promise?(.failure(.oops))
|
||||
|
||||
do {
|
||||
_ = try await task.value
|
||||
XCTFail("Expected an error")
|
||||
} catch let error as TestingError {
|
||||
XCTAssertEqual(error, .oops)
|
||||
} catch {
|
||||
XCTFail("Unexpected error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
@@ -314,7 +314,7 @@ final class CurrentValueSubjectTests: XCTestCase {
|
||||
receiveSubscription: { subscription in
|
||||
subscription.request(.unlimited)
|
||||
},
|
||||
receiveCompletion: { completion in
|
||||
receiveCompletion: { _ in
|
||||
cvs.send(completion: .failure("must not recurse"))
|
||||
}
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// Created by Sergej Jaskiewicz on 26.08.2019.
|
||||
//
|
||||
|
||||
#if !WASI
|
||||
#if !WASI // TEST_DISCOVERY_CONDITION
|
||||
|
||||
import Dispatch
|
||||
import XCTest
|
||||
@@ -31,23 +31,34 @@ final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
uptimeNanoseconds: DispatchTime.distantFuture.uptimeNanoseconds - 1024
|
||||
)
|
||||
)
|
||||
let int64max = Scheduler.SchedulerTimeType(
|
||||
DispatchTime(
|
||||
uptimeNanoseconds: UInt64(Int.max)
|
||||
)
|
||||
)
|
||||
|
||||
XCTAssertEqual(time1.distance(to: time2), .nanoseconds(431))
|
||||
XCTAssertEqual(time2.distance(to: time1), .nanoseconds(-431))
|
||||
|
||||
XCTAssertEqual(time1.distance(to: distantFuture), .nanoseconds(-10001))
|
||||
XCTAssertEqual(distantFuture.distance(to: time1), .nanoseconds(10001))
|
||||
XCTAssertEqual(time2.distance(to: distantFuture), .nanoseconds(-10432))
|
||||
XCTAssertEqual(distantFuture.distance(to: time2), .nanoseconds(10432))
|
||||
XCTAssertEqual(time1.distance(to: distantFuture), .nanoseconds(.max))
|
||||
XCTAssertEqual(distantFuture.distance(to: time1), .nanoseconds(.max))
|
||||
XCTAssertEqual(time2.distance(to: distantFuture), .nanoseconds(.max))
|
||||
XCTAssertEqual(distantFuture.distance(to: time2), .nanoseconds(.max))
|
||||
|
||||
XCTAssertEqual(time1.distance(to: notSoDistantFuture), .nanoseconds(-11025))
|
||||
XCTAssertEqual(notSoDistantFuture.distance(to: time1), .nanoseconds(11025))
|
||||
XCTAssertEqual(time2.distance(to: notSoDistantFuture), .nanoseconds(-11456))
|
||||
XCTAssertEqual(notSoDistantFuture.distance(to: time2), .nanoseconds(11456))
|
||||
XCTAssertEqual(time1.distance(to: notSoDistantFuture), .nanoseconds(.max))
|
||||
XCTAssertEqual(notSoDistantFuture.distance(to: time1), .nanoseconds(.max))
|
||||
XCTAssertEqual(time2.distance(to: notSoDistantFuture), .nanoseconds(.max))
|
||||
XCTAssertEqual(notSoDistantFuture.distance(to: time2), .nanoseconds(.max))
|
||||
|
||||
XCTAssertEqual(time1.distance(to: int64max), .nanoseconds(.max - 10000))
|
||||
XCTAssertEqual(int64max.distance(to: time1), .nanoseconds(-(.max - 10000)))
|
||||
XCTAssertEqual(time2.distance(to: int64max), .nanoseconds(.max - 10431))
|
||||
XCTAssertEqual(int64max.distance(to: time2), .nanoseconds(-(.max - 10431)))
|
||||
|
||||
XCTAssertEqual(distantFuture.distance(to: distantFuture), .nanoseconds(0))
|
||||
XCTAssertEqual(notSoDistantFuture.distance(to: notSoDistantFuture),
|
||||
.nanoseconds(0))
|
||||
XCTAssertEqual(int64max.distance(to: int64max), .nanoseconds(0))
|
||||
}
|
||||
|
||||
func testSchedulerTimeTypeAdvanced() {
|
||||
@@ -259,24 +270,22 @@ final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
XCTAssertEqual(Stride(exactly: 2 as UInt64)?.magnitude, 2_000_000_000)
|
||||
}
|
||||
|
||||
func testStrideFromTooMuchSecondsCrashes() {
|
||||
assertCrashes {
|
||||
func testStrideFromTooMuchSeconds() {
|
||||
#if arch(x86_64) || arch(arm64) || arch(s390x) || arch(powerpc64) || arch(powerpc64le)
|
||||
// 64-bit platforms
|
||||
XCTAssertGreaterThan(
|
||||
Stride.seconds(Double(Int.max) / 1_000_000_000).magnitude,
|
||||
.max
|
||||
)
|
||||
// 64-bit platforms
|
||||
XCTAssertEqual(
|
||||
Stride.seconds(Double(Int.max) / 1_000_000_000).magnitude,
|
||||
.max
|
||||
)
|
||||
#elseif arch(i386) || arch(arm)
|
||||
// 32-bit platforms
|
||||
XCTAssertGreaterThan(
|
||||
Stride.seconds(Double(Int.max) / 1_000_000_000 + 1).magnitude,
|
||||
.max
|
||||
)
|
||||
// 32-bit platforms
|
||||
XCTAssertEqual(
|
||||
Stride.seconds(Double(Int.max) / 1_000_000_000).magnitude,
|
||||
.max
|
||||
)
|
||||
#else
|
||||
#error("This architecture isn't known. Add it to the 32-bit or 64-bit line.")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
func testStrideComparable() {
|
||||
@@ -443,7 +452,7 @@ final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
.encode(KeyedWrapper(value: stride))
|
||||
let encodedString = String(decoding: encodedData, as: UTF8.self)
|
||||
|
||||
XCTAssertEqual(encodedString, #"{"value":{"magnitude":419872}}"#)
|
||||
XCTAssertEqual(encodedString, #"{"value":{"_nanoseconds":419872}}"#)
|
||||
|
||||
let decodedStride = try decoder
|
||||
.decode(KeyedWrapper<Stride>.self, from: encodedData)
|
||||
@@ -486,11 +495,11 @@ final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
let main = expectation(description: "scheduled on main queue")
|
||||
main.assertForOverFulfill = true
|
||||
|
||||
var didExecuteMainAction = false
|
||||
let didExecuteMainAction = Atomic(false)
|
||||
let didExecuteBackgroundAction = Atomic(false)
|
||||
|
||||
mainScheduler.schedule {
|
||||
didExecuteMainAction = true
|
||||
didExecuteMainAction.set(true)
|
||||
main.fulfill()
|
||||
}
|
||||
|
||||
@@ -501,12 +510,14 @@ final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
didExecuteBackgroundAction.set(true)
|
||||
}
|
||||
|
||||
XCTAssertFalse(didExecuteMainAction, "action should be executed asynchronously")
|
||||
XCTAssertFalse(didExecuteMainAction.value,
|
||||
"action should be executed asynchronously")
|
||||
|
||||
// Wait for the background scheduler to execute the work.
|
||||
XCTAssertEqual(group.wait(timeout: .now() + 5.0), .success)
|
||||
|
||||
XCTAssertFalse(didExecuteMainAction, "action should be executed asynchronously")
|
||||
XCTAssertFalse(didExecuteMainAction.value,
|
||||
"action should be executed asynchronously")
|
||||
XCTAssertTrue(didExecuteBackgroundAction.value)
|
||||
|
||||
wait(for: [main], timeout: 0.1)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
#if !WASI
|
||||
#if !WASI // TEST_DISCOVERY_CONDITION
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
#if !WASI
|
||||
#if !WASI // TEST_DISCOVERY_CONDITION
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
#if !WASI
|
||||
#if !WASI // TEST_DISCOVERY_CONDITION
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@@ -331,6 +331,7 @@ extension OperationQueueScheduler.SchedulerTimeType.Stride
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension OperationQueueScheduler.SchedulerTimeType: DateBackedSchedulerTimeType {}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension OperationQueueScheduler: RunLoopLikeScheduler {}
|
||||
|
||||
private final class TestOperationQueue: OperationQueue {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// Created by Sergej Jaskiewicz on 14.12.2019.
|
||||
//
|
||||
|
||||
#if !WASI
|
||||
#if !WASI // TEST_DISCOVERY_CONDITION
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
@@ -599,12 +599,14 @@ private func makeScheduler(_ runLoop: RunLoop) -> RunLoopScheduler {
|
||||
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
protocol DateBackedSchedulerTimeType: Strideable, Codable, Hashable {
|
||||
init(_ date: Date)
|
||||
|
||||
var date: Date { get }
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
protocol TimeIntervalBackedSchedulerStride: SchedulerTimeIntervalConvertible,
|
||||
Comparable,
|
||||
SignedNumeric,
|
||||
@@ -617,6 +619,7 @@ protocol TimeIntervalBackedSchedulerStride: SchedulerTimeIntervalConvertible,
|
||||
var timeInterval: TimeInterval { get }
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
protocol RunLoopLikeScheduler: Scheduler
|
||||
where SchedulerTimeType: DateBackedSchedulerTimeType,
|
||||
SchedulerTimeType.Stride: TimeIntervalBackedSchedulerStride {
|
||||
@@ -628,6 +631,7 @@ extension RunLoopScheduler.SchedulerTimeType.Stride: TimeIntervalBackedScheduler
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension RunLoopScheduler.SchedulerTimeType: DateBackedSchedulerTimeType {}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension RunLoopScheduler: RunLoopLikeScheduler {}
|
||||
|
||||
#endif // !WASI
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// Created by Sergej Jaskiewicz on 23.06.2020.
|
||||
//
|
||||
|
||||
#if !WASI
|
||||
#if !WASI // TEST_DISCOVERY_CONDITION
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
|
||||
// swiftlint:disable multiline_arguments
|
||||
|
||||
#if !WASI
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@@ -20,7 +18,7 @@ import FoundationNetworking
|
||||
// swift-corelibs-foundation that were making these tests impossible to build.
|
||||
//
|
||||
// Those were fixed in https://github.com/apple/swift-corelibs-foundation/pull/2587.
|
||||
#if canImport(Darwin) || swift(>=5.3) // TEST_DISCOVERY_CONDITION
|
||||
#if canImport(Darwin) || swift(>=5.3) && !WASI // TEST_DISCOVERY_CONDITION
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
@@ -746,6 +744,4 @@ private func makePublisher(
|
||||
}
|
||||
#endif // OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
|
||||
|
||||
#endif // canImport(Darwin)
|
||||
|
||||
#endif // !WASI
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
//
|
||||
// AutomaticallyFinish.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 08.07.2021.
|
||||
//
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class AutomaticallyFinish<Output, Failure: Error> {
|
||||
|
||||
let subscription: CustomSubscription
|
||||
let publisher: CustomPublisherBase<Output, Failure>
|
||||
|
||||
init() {
|
||||
subscription = .init()
|
||||
publisher = .init(subscription: subscription)
|
||||
}
|
||||
|
||||
deinit {
|
||||
publisher.send(completion: .finished)
|
||||
}
|
||||
|
||||
func notify(_ value: Output) {
|
||||
_ = publisher.send(value)
|
||||
}
|
||||
|
||||
func listen(receiveCompletion: @escaping (Subscribers.Completion<Failure>) -> Void,
|
||||
receiveValue: @escaping (Output) -> Void) -> AnyCancellable {
|
||||
return publisher.sink(receiveCompletion: receiveCompletion,
|
||||
receiveValue: receiveValue)
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension AutomaticallyFinish: Publisher {
|
||||
func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == Failure, Downstream.Input == Output
|
||||
{
|
||||
publisher.subscribe(subscriber)
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension AutomaticallyFinish where Failure == Never {
|
||||
func assign<Root>(to keyPath: ReferenceWritableKeyPath<Root, Output>,
|
||||
on object: Root) -> AnyCancellable {
|
||||
return publisher.assign(to: keyPath, on: object)
|
||||
}
|
||||
}
|
||||
@@ -33,8 +33,28 @@ final class CustomSubscription: Subscription, CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
private struct State {
|
||||
var cancelled: Bool
|
||||
var history: [Event]
|
||||
}
|
||||
|
||||
private let state = Atomic(State(cancelled: false, history: []))
|
||||
|
||||
/// The history of requests and cancellations of this subscription.
|
||||
private(set) var history: [Event] = []
|
||||
var history: [Event] {
|
||||
return state.value.history
|
||||
}
|
||||
|
||||
var cancelled: Bool {
|
||||
get {
|
||||
return state.value.cancelled
|
||||
}
|
||||
set {
|
||||
state.do { state in
|
||||
state.cancelled = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var onRequest: ((Subscribers.Demand) -> Void)?
|
||||
var onCancel: (() -> Void)?
|
||||
@@ -63,16 +83,18 @@ final class CustomSubscription: Subscription, CustomStringConvertible {
|
||||
}.last
|
||||
}
|
||||
|
||||
var cancelled = false
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
history.append(.requested(demand))
|
||||
state.do { state in
|
||||
state.history.append(.requested(demand))
|
||||
}
|
||||
onRequest?(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
history.append(.cancelled)
|
||||
cancelled = true
|
||||
state.do { state in
|
||||
state.history.append(.cancelled)
|
||||
state.cancelled = true
|
||||
}
|
||||
onCancel?()
|
||||
}
|
||||
|
||||
|
||||
@@ -11,38 +11,76 @@
|
||||
import Darwin
|
||||
#elseif canImport(Glibc)
|
||||
import Glibc
|
||||
#elseif os(Windows)
|
||||
import WinSDK
|
||||
#else
|
||||
#error("How to do threads on this platform?")
|
||||
#endif
|
||||
|
||||
#if canImport(Darwin)
|
||||
private typealias ThreadPtr = UnsafeMutablePointer<pthread_t?>
|
||||
#else
|
||||
private typealias ThreadPtr = UnsafeMutablePointer<pthread_t>
|
||||
#endif
|
||||
// We could use Foundation's Thread, but it doesn't work on Linux for some
|
||||
// reason.
|
||||
|
||||
func executeOnBackgroundThread<ResultType>(
|
||||
_ body: () -> ResultType
|
||||
) -> ResultType {
|
||||
return withoutActuallyEscaping(body) { body in
|
||||
typealias ThreadRoutine = () -> UnsafeMutableRawPointer
|
||||
|
||||
// We need this because @convention(c) closures can't capture generic params.
|
||||
var typeErasedBody: () -> UnsafeMutableRawPointer = {
|
||||
#if canImport(Darwin)
|
||||
typealias ThreadHandle = UnsafeMutablePointer<pthread_t?>
|
||||
#elseif canImport(Glibc)
|
||||
typealias ThreadHandle = UnsafeMutablePointer<pthread_t>
|
||||
#elseif os(Windows)
|
||||
typealias ThreadHandle = HANDLE?
|
||||
#endif
|
||||
|
||||
let typeErasedBody: ThreadRoutine = {
|
||||
let resultPtr = UnsafeMutablePointer<ResultType>.allocate(capacity: 1)
|
||||
resultPtr.initialize(to: body())
|
||||
return UnsafeMutableRawPointer(resultPtr)
|
||||
}
|
||||
|
||||
return withUnsafeMutablePointer(to: &typeErasedBody) { typeErasedBody in
|
||||
let _backgroundThread = ThreadPtr.allocate(capacity: 1)
|
||||
var _backgroundThread: ThreadHandle
|
||||
|
||||
defer { _backgroundThread.deallocate() }
|
||||
#if os(Windows)
|
||||
typealias ResultPtr = UnsafeMutablePointer<UnsafeMutableRawPointer>
|
||||
typealias Context = (ThreadRoutine, ResultPtr)
|
||||
var resultPtr: ResultPtr = .allocate(capacity: 1)
|
||||
defer { resultPtr.deallocate() }
|
||||
_backgroundThread = nil
|
||||
var context: Context = (typeErasedBody, resultPtr)
|
||||
#else
|
||||
_backgroundThread = .allocate(capacity: 1)
|
||||
defer { _backgroundThread.deallocate() }
|
||||
var context = typeErasedBody
|
||||
#endif
|
||||
|
||||
var status: Int32 = 0
|
||||
return withUnsafeMutablePointer(to: &context) { context in
|
||||
#if os(Windows)
|
||||
_backgroundThread = CreateThread(
|
||||
nil, // default security attributes
|
||||
0, // use default stack size
|
||||
{ context in
|
||||
let (typeErasedBody, resultPtr) = context!
|
||||
.assumingMemoryBound(to: Context.self)
|
||||
.pointee
|
||||
|
||||
// We could use Foundation's Thread, but it doesn't work on Linux for some
|
||||
// reason.
|
||||
status = pthread_create(
|
||||
resultPtr.initialize(to: typeErasedBody())
|
||||
return 0
|
||||
},
|
||||
context,
|
||||
0, // use default creation flags
|
||||
nil // don't return thread identifier
|
||||
)
|
||||
precondition(_backgroundThread != nil, "Could not create a thread")
|
||||
|
||||
WaitForSingleObject(_backgroundThread!, INFINITE)
|
||||
|
||||
defer { resultPtr.pointee.deallocate() }
|
||||
|
||||
return resultPtr.pointee.assumingMemoryBound(to: ResultType.self).move()
|
||||
#else
|
||||
var status = pthread_create(
|
||||
_backgroundThread,
|
||||
nil,
|
||||
{ context in
|
||||
@@ -52,19 +90,17 @@ func executeOnBackgroundThread<ResultType>(
|
||||
let context = context!
|
||||
#endif
|
||||
return context
|
||||
.assumingMemoryBound(to: (() -> UnsafeMutableRawPointer).self)
|
||||
.assumingMemoryBound(to: ThreadRoutine.self)
|
||||
.pointee()
|
||||
},
|
||||
typeErasedBody
|
||||
context
|
||||
)
|
||||
|
||||
guard status == 0 else {
|
||||
preconditionFailure("Could not create a background thread")
|
||||
}
|
||||
precondition(status == 0, "Could not create a thread")
|
||||
|
||||
#if canImport(Darwin)
|
||||
guard let backgroundThread = _backgroundThread.pointee else {
|
||||
preconditionFailure("Could not create a background thread")
|
||||
preconditionFailure("Could not join thread")
|
||||
}
|
||||
#else
|
||||
let backgroundThread = _backgroundThread.pointee
|
||||
@@ -74,12 +110,13 @@ func executeOnBackgroundThread<ResultType>(
|
||||
status = pthread_join(backgroundThread, &_resultPtr)
|
||||
|
||||
guard status == 0, let resultPtr = _resultPtr else {
|
||||
preconditionFailure("Could not join threads")
|
||||
preconditionFailure("Could not join thread")
|
||||
}
|
||||
|
||||
defer { resultPtr.deallocate() }
|
||||
|
||||
return resultPtr.assumingMemoryBound(to: ResultType.self).move()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,9 +46,11 @@ final class BreakpointTests: XCTestCase {
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
shouldStop = true
|
||||
XCTAssertEqual(counter, 2)
|
||||
#if !os(Windows)
|
||||
assertCrashes {
|
||||
helper.publisher.send(subscription: CustomSubscription())
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
func testReceiveValue() {
|
||||
@@ -77,9 +79,11 @@ final class BreakpointTests: XCTestCase {
|
||||
.subscription("CustomSubscription")])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(counter, 2)
|
||||
#if !os(Windows)
|
||||
assertCrashes {
|
||||
_ = helper.publisher.send(-1)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
func testReceiveCompletion() {
|
||||
@@ -107,9 +111,11 @@ final class BreakpointTests: XCTestCase {
|
||||
.value(21),
|
||||
.subscription("CustomSubscription")])
|
||||
XCTAssertEqual(counter, 2)
|
||||
#if !os(Windows)
|
||||
assertCrashes {
|
||||
helper.publisher.send(completion: .finished)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
func testBreakpointOnError() throws {
|
||||
@@ -139,9 +145,11 @@ final class BreakpointTests: XCTestCase {
|
||||
XCTAssertEqual(helper.sut.receiveCompletion?(.finished), false)
|
||||
XCTAssertEqual(helper.sut.receiveCompletion?(.failure(.oops)), true)
|
||||
|
||||
#if !os(Windows)
|
||||
assertCrashes {
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
|
||||
@@ -377,6 +377,60 @@ 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,22 +13,75 @@ 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>
|
||||
|
||||
func testFutureSuccess() {
|
||||
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 {
|
||||
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),
|
||||
@@ -36,13 +89,15 @@ final class FutureTests: XCTestCase {
|
||||
])
|
||||
}
|
||||
|
||||
func testFutureFailure() {
|
||||
func testFutureFailure() throws {
|
||||
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")
|
||||
@@ -51,9 +106,19 @@ 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))
|
||||
@@ -231,7 +296,7 @@ final class FutureTests: XCTestCase {
|
||||
XCTAssertTrue(hasStarted)
|
||||
}
|
||||
|
||||
func testWaitsForDemandSuccess() {
|
||||
func testWaitsForDemandSuccess() throws {
|
||||
var promise: Sut.Promise?
|
||||
|
||||
let future = Sut { promise = $0 }
|
||||
@@ -248,13 +313,23 @@ final class FutureTests: XCTestCase {
|
||||
.subscription("Future")
|
||||
])
|
||||
|
||||
downstreamSubscription?.request(.max(1))
|
||||
let unwrappedDownstreamSubscription = try XCTUnwrap(downstreamSubscription)
|
||||
|
||||
self.assertParent(of: unwrappedDownstreamSubscription, isNil: false)
|
||||
|
||||
subscriber.onValue = { _ in
|
||||
self.assertParent(of: unwrappedDownstreamSubscription, isNil: false)
|
||||
}
|
||||
|
||||
unwrappedDownstreamSubscription.request(.max(1))
|
||||
|
||||
XCTAssertEqual(subscriber.history, [
|
||||
.subscription("Future"),
|
||||
.value(42),
|
||||
.completion(.finished)
|
||||
])
|
||||
|
||||
assertParent(of: unwrappedDownstreamSubscription, isNil: false)
|
||||
}
|
||||
|
||||
func testReleasesEverythingOnTermination() {
|
||||
@@ -315,5 +390,17 @@ 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() {
|
||||
func testSendingErrorWithNoDemandThenFinish() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
@@ -77,7 +77,12 @@ 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)])
|
||||
}
|
||||
|
||||
@@ -164,7 +169,7 @@ final class ReplaceErrorTests: XCTestCase {
|
||||
|
||||
replaceError.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("ReplaceError")])
|
||||
XCTAssertEqual(tracking.history, [])
|
||||
|
||||
let subscription = CustomSubscription()
|
||||
publisher.send(subscription: subscription)
|
||||
@@ -225,7 +230,7 @@ final class ReplaceErrorTests: XCTestCase {
|
||||
func testReplaceErrorReceiveValueBeforeSubscription() {
|
||||
testReceiveValueBeforeSubscription(
|
||||
value: 0,
|
||||
expected: .history([.subscription("ReplaceError")], demand: .none),
|
||||
expected: .history([], demand: .none),
|
||||
{ $0.replaceError(with: 1) }
|
||||
)
|
||||
}
|
||||
@@ -233,7 +238,7 @@ final class ReplaceErrorTests: XCTestCase {
|
||||
func testReplaceErrorCompletionBeforeSubscription() {
|
||||
testReceiveCompletionBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([.subscription("ReplaceError")]),
|
||||
expected: .history([]),
|
||||
{ $0.replaceError(with: 1) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,743 @@
|
||||
//
|
||||
// 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,4 +137,41 @@ 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,6 +210,82 @@ 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?
|
||||
|
||||
+19
-7
@@ -15,16 +15,25 @@ import re
|
||||
from pathlib import Path
|
||||
from argparse import ArgumentParser
|
||||
|
||||
class Test:
|
||||
def __init__(self, name, is_async):
|
||||
self.name = name
|
||||
self.is_async = is_async
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
TEST_METHOD_PATTERN = \
|
||||
re.compile(r"func\s*(test\w+)\s*\(\s*\)\s*(?:throws\s*)?{")
|
||||
re.compile(r"func\s*(test\w+)\s*\(\s*\)\s*(async\s*)?(?:throws\s*)?{")
|
||||
TEST_DISCOVERY_CONDITION_PATTERN = \
|
||||
re.compile(r"^#if (.+)\s*\/\/\s*TEST_DISCOVERY_CONDITION\s*$", flags=re.MULTILINE)
|
||||
|
||||
def extract_test_names(test_file):
|
||||
contents = test_file.read_text()
|
||||
test_names = [match[1] for match in TEST_METHOD_PATTERN.finditer(contents)]
|
||||
tests = [Test(match[1], match[2] is not None) for match in TEST_METHOD_PATTERN.finditer(contents)]
|
||||
condition = TEST_DISCOVERY_CONDITION_PATTERN.search(contents)
|
||||
return (test_names, condition[1] if condition else None)
|
||||
return (tests, condition[1] if condition else None)
|
||||
|
||||
def generate_linuxmain(workdir):
|
||||
workdir = Path(workdir)
|
||||
@@ -41,14 +50,17 @@ var tests = [XCTestCaseEntry]()
|
||||
|
||||
""")
|
||||
for test_file in test_files:
|
||||
(test_names, condition) = extract_test_names(test_file)
|
||||
if not test_names:
|
||||
(tests, condition) = extract_test_names(test_file)
|
||||
if not tests:
|
||||
continue
|
||||
if condition:
|
||||
linuxmain.write(f"#if {condition}\n")
|
||||
linuxmain.write(f"let allTests_{test_file.stem} = [\n")
|
||||
for test_name in test_names:
|
||||
linuxmain.write(f" (\"{test_name}\", {test_file.stem}.{test_name}),\n")
|
||||
for test in tests:
|
||||
if test.is_async:
|
||||
linuxmain.write(f" (\"{test}\", asyncTest({test_file.stem}.{test})),\n")
|
||||
else:
|
||||
linuxmain.write(f" (\"{test}\", {test_file.stem}.{test}),\n")
|
||||
linuxmain.write("]\n")
|
||||
linuxmain.write(f"tests.append(testCase(allTests_{test_file.stem}))\n")
|
||||
if condition:
|
||||
|
||||
Reference in New Issue
Block a user