Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| bab8e08d2f | |||
| 4060ee9f57 | |||
| 5996772433 | |||
| cd45c77fac | |||
| e618d179fe | |||
| 4fa5f48c19 | |||
| 28993ae57d | |||
| 3d61bf87e7 | |||
| 911a4e1aa3 | |||
| beb38dec0e | |||
| 1fbf688897 | |||
| 5436868053 | |||
| 4977ca158f | |||
| 96214ac5f9 | |||
| 21fda909f5 | |||
| 8438d09b82 | |||
| 30a60b52cc | |||
| a93ed143fb | |||
| e054a884ef |
@@ -1,297 +0,0 @@
|
||||
macOS_tests_steps: &macOS_tests_steps
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Building and running tests in debug mode with coverage
|
||||
command: |
|
||||
make test-debug \
|
||||
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
|
||||
SWIFT_TEST_FLAGS="--enable-code-coverage --build-path .build-test-debug"
|
||||
xcrun llvm-cov show \
|
||||
-instr-profile=.build-test-debug/debug/codecov/default.profdata \
|
||||
.build-test-debug/debug/OpenCombinePackageTests.xctest/Contents/MacOS/OpenCombinePackageTests \
|
||||
> coverage.txt
|
||||
- run:
|
||||
name: Building and running tests in debug mode with TSan
|
||||
command: |
|
||||
make test-debug-sanitize-thread \
|
||||
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
|
||||
SWIFT_TEST_FLAGS="--build-path .build-test-debug-sanitize-thread"
|
||||
- run:
|
||||
name: Building and running tests in release mode
|
||||
command: |
|
||||
make test-release \
|
||||
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
|
||||
SWIFT_TEST_FLAGS="--build-path .build-test-release"
|
||||
- run:
|
||||
name: Generating Xcode project
|
||||
command: make generate-xcodeproj SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors"
|
||||
- run:
|
||||
name: Building for testing on macOS 10.15.0 with xcodebuild
|
||||
command: |
|
||||
set -o pipefail \
|
||||
&& xcodebuild build-for-testing \
|
||||
-scheme OpenCombine-Package \
|
||||
-sdk macosx10.15 \
|
||||
-derivedDataPath DerivedData \
|
||||
| tee xcodebuild_build-for-testing.log \
|
||||
| xcpretty
|
||||
- store_artifacts:
|
||||
path: xcodebuild_build-for-testing.log
|
||||
- run:
|
||||
name: Testing on macOS 10.15.0 with xcodebuild
|
||||
command: |
|
||||
set -o pipefail \
|
||||
&& xcodebuild test-without-building \
|
||||
-scheme OpenCombine-Package \
|
||||
-sdk macosx10.15 \
|
||||
-derivedDataPath DerivedData \
|
||||
| tee xcodebuild_test-without-building.log \
|
||||
| xcpretty --report junit -o build/reports/results.xml
|
||||
- store_artifacts:
|
||||
path: xcodebuild_test-without-building.log
|
||||
- store_test_results:
|
||||
path: build/reports
|
||||
- run:
|
||||
name: Uploading code coverage
|
||||
command: |
|
||||
bash <(curl -s https://codecov.io/bash) -D DerivedData
|
||||
|
||||
ubuntu_tests_steps: &ubuntu_tests_steps
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Installing dependencies
|
||||
command: |
|
||||
apt update -y
|
||||
apt upgrade -y
|
||||
apt install -y curl python3.8
|
||||
- run:
|
||||
name: "Generating LinuxMain.swift"
|
||||
command: python3.8 utils/discover_tests.py
|
||||
- run:
|
||||
name: Building and running tests in debug mode with coverage
|
||||
command: |
|
||||
make test-debug \
|
||||
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
|
||||
SWIFT_TEST_FLAGS="--enable-code-coverage \
|
||||
--disable-index-store \
|
||||
--build-path .build-test-debug"
|
||||
llvm-cov show \
|
||||
-instr-profile=.build-test-debug/debug/codecov/default.profdata \
|
||||
.build-test-debug/debug/OpenCombinePackageTests.xctest \
|
||||
> coverage.txt
|
||||
- run:
|
||||
name: Building and running tests in debug mode with TSan
|
||||
command: |
|
||||
make test-debug-sanitize-thread \
|
||||
SWIFT_TEST_FLAGS="--disable-index-store \
|
||||
--build-path .build-test-debug-sanitize-thread" \
|
||||
- run:
|
||||
name: Building and running tests in release mode
|
||||
command: |
|
||||
make test-release \
|
||||
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
|
||||
SWIFT_TEST_FLAGS="--build-path .build-test-release"
|
||||
- run:
|
||||
name: Uploading code coverage
|
||||
command: |
|
||||
bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
version: 2
|
||||
jobs:
|
||||
"Execute tests on macOS 10.15.0 (Xcode 11.3.0, Swift 5.1.3)":
|
||||
macos:
|
||||
xcode: "11.3.0"
|
||||
environment:
|
||||
SWIFT_VERSION: "5.1.3"
|
||||
<<: *macOS_tests_steps
|
||||
|
||||
"Execute tests on macOS 10.15.0 (Xcode 12.1.0, Swift 5.3.0)":
|
||||
macos:
|
||||
xcode: "12.1.0"
|
||||
environment:
|
||||
SWIFT_VERSION: "5.3.0"
|
||||
<<: *macOS_tests_steps
|
||||
|
||||
"Execute compatibility tests on iOS 14.1 (Xcode 12.1.0, Swift 5.3.0)":
|
||||
macos:
|
||||
xcode: "12.1.0"
|
||||
environment:
|
||||
SWIFT_VERSION: "5.3.0"
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Generating Xcode project
|
||||
command: make generate-compatibility-xcodeproj
|
||||
- run:
|
||||
name: Building for testing on iOS 14.1 with xcodebuild
|
||||
command: |
|
||||
set -o pipefail \
|
||||
&& xcodebuild build-for-testing \
|
||||
-scheme OpenCombine-Package \
|
||||
-destination "platform=iOS Simulator,name=iPhone 11,OS=14.1" \
|
||||
-derivedDataPath DerivedData \
|
||||
| tee xcodebuild_build-for-testing.log \
|
||||
| xcpretty
|
||||
- store_artifacts:
|
||||
path: xcodebuild_build-for-testing.log
|
||||
- run:
|
||||
name: Testing against Combine on iOS 14.1 with xcodebuild
|
||||
command: |
|
||||
set -o pipefail \
|
||||
&& xcodebuild test-without-building \
|
||||
-scheme OpenCombine-Package \
|
||||
-destination "platform=iOS Simulator,name=iPhone 11,OS=14.1" \
|
||||
-derivedDataPath DerivedData \
|
||||
| tee xcodebuild_test-without-building.log \
|
||||
| xcpretty --report junit -o build/reports/results.xml
|
||||
- store_artifacts:
|
||||
path: xcodebuild_test-without-building.log
|
||||
- store_test_results:
|
||||
path: build/reports
|
||||
|
||||
"Execute tests on iOS 9.3 (Xcode 10.2.1, Swift 5.0.1)":
|
||||
macos:
|
||||
xcode: "10.2.1"
|
||||
environment:
|
||||
BUNDLE_PATH: .bundle # path to install gems and use for caching
|
||||
SWIFT_VERSION: "5.0.1"
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Installing gem dependencies
|
||||
command: bundle install && bundle clean
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v1-simulator-cache-{{ arch }}
|
||||
- run:
|
||||
# CircleCI doesn't have an iOS 9 simulator, so we need to install it manually.
|
||||
name: Installing iOS 9 simulator
|
||||
command: |
|
||||
bundle exec xcversion simulators --install="iOS 9.3"
|
||||
bundle exec xcversion simulators
|
||||
xcrun simctl list
|
||||
- save_cache:
|
||||
key: v1-simulator-cache-{{ arch }}
|
||||
paths:
|
||||
- ~/Library/Caches/XcodeInstall
|
||||
- run:
|
||||
name: Generating Xcode project
|
||||
command: |
|
||||
make generate-xcodeproj SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors"
|
||||
xcodebuild -scheme OpenCombine-Package -showdestinations
|
||||
- run:
|
||||
name: Building for testing on iOS 9.3 with xcodebuild
|
||||
command: |
|
||||
set -o pipefail \
|
||||
&& xcodebuild build-for-testing \
|
||||
-scheme OpenCombine-Package \
|
||||
-destination "platform=iOS Simulator,name=iPhone 4s,OS=9.3" \
|
||||
-derivedDataPath DerivedData \
|
||||
| tee xcodebuild_build-for-testing.log \
|
||||
| xcpretty
|
||||
- store_artifacts:
|
||||
path: xcodebuild_build-for-testing.log
|
||||
- run:
|
||||
name: Testing on iOS 9.3 with xcodebuild
|
||||
command: |
|
||||
set -o pipefail \
|
||||
&& xcodebuild test-without-building \
|
||||
-scheme OpenCombine-Package \
|
||||
-destination "platform=iOS Simulator,name=iPhone 4s,OS=9.3" \
|
||||
-derivedDataPath DerivedData \
|
||||
| tee xcodebuild_test-without-building.log \
|
||||
| xcpretty --report junit -o build/reports/results.xml
|
||||
- store_artifacts:
|
||||
path: xcodebuild_test-without-building.log
|
||||
- store_test_results:
|
||||
path: build/reports
|
||||
- run:
|
||||
name: Uploading code coverage
|
||||
command: |
|
||||
bash <(curl -s https://codecov.io/bash) -D DerivedData
|
||||
|
||||
"Execute tests on Ubuntu 18.04 (Swift 5.0)":
|
||||
docker:
|
||||
- image: swift:5.0-bionic
|
||||
environment:
|
||||
SWIFT_VERSION: "5.0"
|
||||
<<: *ubuntu_tests_steps
|
||||
|
||||
"Execute tests on Ubuntu 18.04 (Swift 5.1)":
|
||||
docker:
|
||||
- image: swift:5.1-bionic
|
||||
environment:
|
||||
SWIFT_VERSION: "5.1"
|
||||
<<: *ubuntu_tests_steps
|
||||
|
||||
"Execute tests on Ubuntu 18.04 (Swift 5.2)":
|
||||
docker:
|
||||
- image: swift:5.2-bionic
|
||||
environment:
|
||||
SWIFT_VERSION: "5.2"
|
||||
<<: *ubuntu_tests_steps
|
||||
|
||||
"Execute tests on Ubuntu 18.04 (Swift 5.3)":
|
||||
docker:
|
||||
- image: swift:5.3-bionic
|
||||
environment:
|
||||
SWIFT_VERSION: "5.3"
|
||||
<<: *ubuntu_tests_steps
|
||||
|
||||
"Run SwiftLint and Danger":
|
||||
macos:
|
||||
xcode: "11.3.0"
|
||||
environment:
|
||||
HOMEBREW_NO_AUTO_UPDATE: "1"
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Install SwiftLint
|
||||
command: |
|
||||
brew install swiftlint
|
||||
- run:
|
||||
name: Install danger-swift
|
||||
command: |
|
||||
brew install danger/tap/danger-swift
|
||||
- run:
|
||||
name: Run danger-swift
|
||||
command: danger-swift ci
|
||||
|
||||
"Run Pod spec lint":
|
||||
macos:
|
||||
xcode: "11.3.0"
|
||||
environment:
|
||||
HOMEBREW_NO_AUTO_UPDATE: "1"
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Pod lib lint
|
||||
command: |
|
||||
pod lib lint --allow-warnings --verbose
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
"OpenCombine: execute tests on macOS":
|
||||
jobs:
|
||||
- "Execute tests on macOS 10.15.0 (Xcode 11.3.0, Swift 5.1.3)"
|
||||
- "Execute tests on macOS 10.15.0 (Xcode 12.1.0, Swift 5.3.0)"
|
||||
"OpenCombine: execute compatibility tests":
|
||||
jobs:
|
||||
- "Execute compatibility tests on iOS 14.1 (Xcode 12.1.0, Swift 5.3.0)"
|
||||
"OpenCombine: execute tests on iOS":
|
||||
jobs:
|
||||
- "Execute tests on iOS 9.3 (Xcode 10.2.1, Swift 5.0.1)"
|
||||
"OpenCombine: execute tests on Linux":
|
||||
jobs:
|
||||
- "Execute tests on Ubuntu 18.04 (Swift 5.0)"
|
||||
- "Execute tests on Ubuntu 18.04 (Swift 5.1)"
|
||||
- "Execute tests on Ubuntu 18.04 (Swift 5.2)"
|
||||
- "Execute tests on Ubuntu 18.04 (Swift 5.3)"
|
||||
"OpenCombine: run SwiftLint and Danger":
|
||||
jobs:
|
||||
- "Run SwiftLint and Danger"
|
||||
"OpenCombine: validate podspec files":
|
||||
jobs:
|
||||
- "Run Pod spec lint"
|
||||
@@ -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,28 @@
|
||||
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
|
||||
# Attempt to run compatibility tests on macOS.
|
||||
# If they fail, run on iOS.
|
||||
run: |
|
||||
make test-compatibility \
|
||||
|| (set -o pipefail \
|
||||
&& xcodebuild test \
|
||||
-scheme OpenCombine-Package \
|
||||
-destination "name=iPhone 13" \
|
||||
-xcconfig Combine-Compatibility.xcconfig \
|
||||
| tee xcodebuild_test.log \
|
||||
| xcpretty)
|
||||
@@ -0,0 +1,126 @@
|
||||
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.3.1
|
||||
- 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
|
||||
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' }} # 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"]
|
||||
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
|
||||
@@ -0,0 +1,33 @@
|
||||
name: Wasm
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
carton_wasmer_test_5_3:
|
||||
name: "Execute tests on Wasm (Swift 5.3)"
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: swiftwasm/swiftwasm-action@v5.3
|
||||
|
||||
carton_wasmer_test_5_4:
|
||||
name: "Execute tests on Wasm (Swift 5.4)"
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: swiftwasm/swiftwasm-action@v5.4
|
||||
|
||||
carton_wasmer_test_5_5:
|
||||
name: "Execute tests on Wasm (Swift 5.5)"
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: swiftwasm/swiftwasm-action@v5.5
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
name: Windows
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
windows_test:
|
||||
name: Execute tests on Windows
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
swift_version: ["5.4.2", "5.5.1"]
|
||||
runs-on: windows-2019
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: MaxDesiatov/swift-windows-action@v1
|
||||
with:
|
||||
swift-version: ${{ matrix.swift_version }}
|
||||
@@ -2,6 +2,8 @@ included:
|
||||
- Sources
|
||||
- Tests
|
||||
|
||||
child_config: Tests/.swiftlint.yml
|
||||
|
||||
disabled_rules:
|
||||
- block_based_kvo
|
||||
- class_delegate_protocol
|
||||
|
||||
+299
@@ -0,0 +1,299 @@
|
||||
# 0.12.0 (29 Jan 2021)
|
||||
|
||||
This release adds a new `OpenCombineShim` product that will conditionally re-export either
|
||||
Combine on Apple platforms, or OpenCombine on other platforms. Additionally, `ObservableObject`
|
||||
protocol is now available and working on all platforms.
|
||||
|
||||
A bug with `Timer(timeInterval:repeats:block:)` firing immediately not accounting for the passed
|
||||
`timeInterval` is fixed.
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Fix `Timer(timeInterval:repeats:block:)` not accounting `timeInterval` ([#196](https://github.com/OpenCombine/OpenCombine/pull/196)) via [@grigorye](https://github.com/grigorye)
|
||||
- Add `OpenCombineShim` product for easier importing ([#197](https://github.com/OpenCombine/OpenCombine/pull/197)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Implementation for `ObservableObject` with `Mirror` ([#201](https://github.com/OpenCombine/OpenCombine/pull/201)) via [@kateinoigakukun](https://github.com/kateinoigakukun)
|
||||
|
||||
# 0.11.0 (29 Oct 2020)
|
||||
|
||||
This release is compatible with Xcode 12.1.
|
||||
|
||||
### Additions
|
||||
- `Publisher.assigned(to:)` method that accepts a `Published.Publisher`.
|
||||
- New `Publisher.switchToLatest()` overloads.
|
||||
- New `Publisher.flatMap(maxPublishers:_:)` overloads.
|
||||
- `Optional.publisher` property.
|
||||
- New `_Introspection` protocol that allows to track and explore the subscription graph and data flow.
|
||||
|
||||
### Bugfixes
|
||||
- The project should now compile without warnings.
|
||||
- The following entities have been updated to match the behavior of the newest Combine version:
|
||||
- `Subscribers.Assign`
|
||||
- `Publishers.Breakpoint`
|
||||
- `Publishers.Buffer`
|
||||
- `CombineIdentifier`
|
||||
- `Publishers.CompactMap`
|
||||
- `Publishers.Concatenate`
|
||||
- `Publishers.Debounce`
|
||||
- `Publishers.Delay`
|
||||
- `DispatchQueue.SchedulerTimeType.Stride`
|
||||
- `Publishers.Drop`
|
||||
- `Publishers.Encode`
|
||||
- `Publishers.Decode`
|
||||
- `Publishers.Filter`
|
||||
- `Publishers.HandleEvents`
|
||||
- `Publishers.IgnoreOutput`
|
||||
- `Publishers.MeasureInterval`
|
||||
- `OperationQueue` scheduler
|
||||
- `Published`
|
||||
- `Publishers.ReceiveOn`
|
||||
- `Publishers.ReplaceError`
|
||||
- `RunLoop scheduler`
|
||||
- `Publishers.Sequence`
|
||||
- `Subscribers.Sink`
|
||||
- `Publishers.SubscribeOn`
|
||||
- `Publishers.Timeout`
|
||||
- `Timer` publisher
|
||||
|
||||
### Known issues
|
||||
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1 and later.
|
||||
|
||||
# 0.10.2 (23 Oct 2020)
|
||||
|
||||
### Bugfixes
|
||||
- Fixed a crash caused by recursive acquisition of a non-recursive lock in SubbjectSubscriber (#186, thanks @stuaustin for the bug report)
|
||||
|
||||
### Known issues
|
||||
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1 and later.
|
||||
|
||||
# 0.10.1 (4 Oct 2020)
|
||||
|
||||
### Bugfixes
|
||||
- Fixed build errors on Linux with Swift 5.0 and Swift 5.3 toolchains (thanks, @adamleonard and @devmaximilian)
|
||||
|
||||
### Known issues
|
||||
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1 and later.
|
||||
|
||||
# 0.10.0 (28 Jun 2020)
|
||||
|
||||
This release is compatible with Xcode 11.5.
|
||||
|
||||
### Additions
|
||||
- `Timer.publish(every:tolerance:on:in:options:)` (#156, thank you @MaxDesiatov)
|
||||
- `OperationQueue` scheduler (#165)
|
||||
- `Publishers.Timeout` (#164)
|
||||
- `Publishers.Debounce` (#133)
|
||||
|
||||
### Bugfixes
|
||||
- `PassthroughSubject`, `CurrentValueSubject` and `Future` have been rewritten from scratch. They are now faster, more correct and no longer leak subscriptions (#170).
|
||||
|
||||
### Known issues
|
||||
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1 and later.
|
||||
|
||||
# 0.9.0 (12 Jun 2020)
|
||||
|
||||
This release is compatible with Xcode 11.5.
|
||||
|
||||
### Additions
|
||||
- The `Subscribers.Demand` struct can be nicely formatted in LLDB (#146, thank you @mayoff).
|
||||
- `Publishers.SwitchToLatest` (#142).
|
||||
- The `RunLoop` scheduler in `OpenCombineFoundation` (#131).
|
||||
- `Publishers.Catch` and `Publishers.TryCatch` (#140).
|
||||
|
||||
### Bugfixes
|
||||
- Worked around a [bug in the Swift compiler](https://bugs.swift.org/browse/SR-11680) when building the `COpenCombineHelpers` target (#145, thank you @mayoff).
|
||||
- Improved documentation.
|
||||
|
||||
### Known issues
|
||||
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1 and later.
|
||||
|
||||
# 0.8.0 (17 Jan 2020)
|
||||
|
||||
This release is compatible with Xcode 11.3.1.
|
||||
|
||||
### Additions
|
||||
- `Publishers.ReplaceEmpty` (#122, thank you @spadafiva)
|
||||
- `NotificationCenter.Publisher` (#84)
|
||||
- `URLSession.DataTaskPublisher` (#127)
|
||||
- `Publishers.DropUntilOutput` (#136)
|
||||
- `Publishers.CollectByCount` (#137)
|
||||
- `Publishers.AssertNoFailure` (#138)
|
||||
- `Publishers.Buffer` (#143)
|
||||
|
||||
### Bugfixes
|
||||
- Fixed integer overflows in `DispatchQueue.SchedulerTimeType.Stride` (#126, #130)
|
||||
- Fixed the 'default will never be executed' warning on non-Darwin platforms (like Linux) (#129)
|
||||
|
||||
### Known issues
|
||||
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1.
|
||||
|
||||
# 0.7.0 (10 Dec 2019)
|
||||
|
||||
This release is compatible with Xcode 11.2.1.
|
||||
|
||||
### Additions
|
||||
- `Publishers.Delay` (#114)
|
||||
- `Publishers.ReceiveOn` (#115)
|
||||
- `Publishers.SubscribeOn` (#116)
|
||||
- `Publishers.MeasureInterval` (#117)
|
||||
- `Publishers.Breakpoint` (#118)
|
||||
- `Publishers.HandleEvents` (#118)
|
||||
- `Publishers.Concatenate` (#90)
|
||||
|
||||
### Known issues
|
||||
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1.
|
||||
|
||||
# 0.6.0 (26 Nov 2019)
|
||||
|
||||
This release is compatible with Xcode 11.2.1.
|
||||
|
||||
### Thread safety
|
||||
- `Publishers.IgnoreOutput` has been audited for thread safety (#88)
|
||||
- `Publishers.DropWhile` and `Publishers.TryDropWhile` have been audited for thread safety (#87)
|
||||
|
||||
### Additions
|
||||
- `Publishers.Output` (#91)
|
||||
- `Record` (#100)
|
||||
- `Publishers.RemoveDuplicates`, `Publishers.TryRemoveDuplicates` (#89)
|
||||
- `Publishers.PrefixWhile`, `Publishers.TryPrefixWhile` (#89)
|
||||
- `Future` (#107, thanks @MaxDesiatov!)
|
||||
|
||||
### Bugfixes
|
||||
- The behavior of the `Publishers.Encode` and `Publishers.Decode` subscriptions is fixed (#112)
|
||||
- The behavior of the `Publishers.IgnoreOutput` subscription is fixed (#88)
|
||||
- The behavior of the `Publishers.Print` subscription is fixed (#92)
|
||||
- The behavior of the `Publishers.ReplaceError` subscription is fixed (#89)
|
||||
- The behavior of the `Publishers.Filter` and `Publishers.TryFilter` subscriptions is fixed (#89)
|
||||
- The behavior of the `Publishers.CompactMap` and `Publishers.TryCompactMap` subscriptions is fixed (#89)
|
||||
- The behavior of the `Publishers.Multicast` subscription is fixed (#110)
|
||||
- `Publishers.FlatMap` is reimplemented from scratch. Its behavior is fixed in many ways, it now fully matches that of Combine (#89)
|
||||
- `@Published` property wrapper is fixed! (#112)
|
||||
- The behavior of `DispatchQueue.SchedulerTimeType` is fixed to match that of the latest SDKs (#96)
|
||||
- OpenCombine is now usable on 32 bit platforms. Why? Because we can.
|
||||
|
||||
|
||||
### Known issues
|
||||
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1.
|
||||
|
||||
# 0.5.0 (17 Oct 2019)
|
||||
|
||||
This release is compatible with Xcode 11.1.
|
||||
|
||||
### Additions
|
||||
- `Publishers.MapKeyPath` (#71)
|
||||
- `Publishers.Reduce` (#76)
|
||||
- `Publishers.TryReduce` (#76)
|
||||
- `Publishers.Last` (#76)
|
||||
- `Publishers.LastWhere` (#76)
|
||||
- `Publishers.TryLastWhere` (#76)
|
||||
- `Publishers.AllSatisfy` (#76)
|
||||
- `Publishers.TryAllSatisfy` (#76)
|
||||
- `Publishers.Contains` (#76)
|
||||
- `Publishers.ContainsWhere` (#76)
|
||||
- `Publishers.TryContainsWhere` (#76)
|
||||
- `Publishers.Collect` (#76)
|
||||
- `Publishers.Comparison` (#76)
|
||||
- `Publishers.Drop` (#70, thank you @5sw!)
|
||||
- `Publishers.Scan` (#83, thank you @epatey!)
|
||||
- `Publishers.TryScan` (#83, thank you @epatey!)
|
||||
|
||||
### Bugfixes
|
||||
- `Publishers.Print` doesn't print a redundant whitespace anymore.
|
||||
|
||||
### Known issues
|
||||
- `@Published` property wrapper doesn't work yet
|
||||
|
||||
# 0.4.0 (8 Oct 2019)
|
||||
|
||||
This release is compatible with Xcode 11.1.
|
||||
|
||||
### Thread safety
|
||||
- `SubjectSubscriber` (which is used when you subscribe a subject to a publisher) has been audited for thread-safety
|
||||
- `Publishers.Multicast` has been audited for thread safety (#63)
|
||||
- `Publishers.TryMap` has been audited for thread safety
|
||||
- `Just` has been audited for thread safety
|
||||
- `Optional.Publisher` has been audited for thread safety
|
||||
- `Publishers.Sequence` has been audited for thread safety
|
||||
- `Publishers.ReplaceError` has been audited for thread safety
|
||||
- `Subscribers.Assign` has been audited for thread safety
|
||||
- `Subscribers.Sink` has been audited for thread safety
|
||||
|
||||
### Bugfixes
|
||||
- The semantics of `Publishers.Print`, `Publishers.TryMap` have been fixed
|
||||
- Fix `iterator.next()` being called twice in `Publishers.Sequence` (#62)
|
||||
- The default initializer of `CombineIdentifier` (the one that takes no arguments) is now much faster (#66, #69)
|
||||
- When `Publishers.Sequence` subscription is cancelled while it emits values, the cancellation is respected (#73, thanks @5sw!)
|
||||
|
||||
### Additions
|
||||
- `DispatchQueueScheduler` (#46)
|
||||
- `Equatable` conformances for `First`, `ReplaceError`
|
||||
- Added `eraseToAnyPublisher()` method (#59, thanks @evyasafhouzz for reporting!)
|
||||
- `Publishers.MakeConnectable` (#61)
|
||||
- `Publishers.Autoconnect` (#60)
|
||||
- `Publishers.Share` (#60)
|
||||
|
||||
### Known issues
|
||||
- `@Published` property wrapper doesn't work yet
|
||||
|
||||
# 0.3.0 (13 Sep 2019)
|
||||
|
||||
Among other things this release is compatible with Xcode 11.0 GM seed.
|
||||
|
||||
### Bugfixes
|
||||
- Store newly send value in internal variable inside CurrentValueObject (#39, thanks @FranzBusch!)
|
||||
|
||||
### Additions
|
||||
- `Filter`/`TryFilter` (#22, thanks @spadafiva!)
|
||||
- `First`/`FirstWhere`/`TryFirstWhere` (#22, thanks again @spadafiva!)
|
||||
- `CompactMap`/`TryCompacrMap` (#32)
|
||||
- `IgnoreOutput` (#44, thanks @epatey!)
|
||||
- `ReplaceError` (#50, thanks @vladiulianbogdan!)
|
||||
- `FlatMap` (#45, thanks again @epatey!)
|
||||
|
||||
### Known issues
|
||||
- `@Published` property wrapper doesn't work yet
|
||||
|
||||
# 0.2.0 (31 Jul 2019)
|
||||
|
||||
Updated for the newest Xcode 11.0 beta 5
|
||||
|
||||
# 0.1.0 (4 Jul 2019)
|
||||
|
||||
The first pre-pre-pre-alpha release is here!
|
||||
|
||||
Lots of stuff still unimplemented.
|
||||
|
||||
For now we have:
|
||||
|
||||
- `Just`
|
||||
- `Publishers.Decode`
|
||||
- `Publishers.DropWhile`
|
||||
- `Publishers.Empty`
|
||||
- `Publishers.Encode`
|
||||
- `Publishers.Fail`
|
||||
- `Publishers.Map`
|
||||
- `Publishers.Multicast`
|
||||
- `Publishers.Once`
|
||||
- `Publishers.Optional`
|
||||
- `Publishers.Print`
|
||||
- `Publishers.Sequence`
|
||||
- `Subscribers.Assign`
|
||||
- `Subscribers.Completion`
|
||||
- `Subscribers.Demand`
|
||||
- `Subscribers.Sink`
|
||||
- `AnyCancellable`
|
||||
- `AnyPublisher`
|
||||
- `AnySubject`
|
||||
- `AnySubscriber`
|
||||
- `Cancellable`
|
||||
- `CombineIdentifier`
|
||||
- `ConnectablePublisher`
|
||||
- `CurrentValueSubject`
|
||||
- `CustomCombineIdentifierConvertible`
|
||||
- `ImmediateScheduler`
|
||||
- `PassthroughSubject`
|
||||
- `Publisher`
|
||||
- `Result`
|
||||
- `Scheduler`
|
||||
- `Subject`
|
||||
- `Subscriber`
|
||||
- `Subscription`
|
||||
+3
-3
@@ -66,10 +66,10 @@ do {
|
||||
}
|
||||
}
|
||||
|
||||
SwiftLint.lint(inline: true,
|
||||
SwiftLint.lint(.all(directory: nil),
|
||||
inline: true,
|
||||
configFile: ".swiftlint.yml",
|
||||
strict: true,
|
||||
lintAllFiles: true)
|
||||
strict: true)
|
||||
|
||||
if danger.warnings.isEmpty, danger.fails.isEmpty {
|
||||
markdown("LGTM")
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.1)
|
||||
addressable (2.7.0)
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
atomos (0.1.3)
|
||||
babosa (1.0.3)
|
||||
|
||||
+3
-3
@@ -1,17 +1,17 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = "OpenCombine"
|
||||
spec.version = "0.11.0"
|
||||
spec.version = "0.13.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.11.0"
|
||||
spec.version = "0.13.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.12.0'
|
||||
end
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = "OpenCombineFoundation"
|
||||
spec.version = "0.11.0"
|
||||
spec.version = "0.13.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.12.0'
|
||||
end
|
||||
|
||||
+72
-10
@@ -1,25 +1,87 @@
|
||||
// swift-tools-version:5.0
|
||||
// swift-tools-version:5.5
|
||||
|
||||
import PackageDescription
|
||||
|
||||
// This list should be updated whenever SwiftPM adds support for a new platform.
|
||||
// See: https://bugs.swift.org/browse/SR-13814
|
||||
let supportedPlatforms: [Platform] = [
|
||||
.macOS,
|
||||
.macCatalyst,
|
||||
.iOS,
|
||||
.watchOS,
|
||||
.tvOS,
|
||||
.driverKit,
|
||||
.linux,
|
||||
.android,
|
||||
.windows,
|
||||
.wasi,
|
||||
]
|
||||
|
||||
let package = Package(
|
||||
name: "OpenCombine",
|
||||
products: [
|
||||
.library(name: "OpenCombine", targets: ["OpenCombine"]),
|
||||
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
|
||||
.library(name: "OpenCombineFoundation", targets: ["OpenCombineFoundation"]),
|
||||
.library(name: "OpenCombineShim", targets: ["OpenCombineShim"]),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "COpenCombineHelpers"),
|
||||
.target(name: "OpenCombine", dependencies: ["COpenCombineHelpers"]),
|
||||
.target(
|
||||
name: "OpenCombine",
|
||||
dependencies: [
|
||||
.target(name: "COpenCombineHelpers",
|
||||
condition: .when(platforms: supportedPlatforms.except([.wasi])))
|
||||
],
|
||||
exclude: [
|
||||
"Concurrency/Publisher+Concurrency.swift.gyb",
|
||||
"Publishers/Publishers.Encode.swift.gyb",
|
||||
"Publishers/Publishers.MapKeyPath.swift.gyb",
|
||||
"Publishers/Publishers.Catch.swift.gyb"
|
||||
],
|
||||
swiftSettings: [.define("WASI", .when(platforms: [.wasi]))]
|
||||
),
|
||||
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
|
||||
.target(name: "OpenCombineFoundation", dependencies: ["OpenCombine",
|
||||
"COpenCombineHelpers"]),
|
||||
.testTarget(name: "OpenCombineTests",
|
||||
dependencies: ["OpenCombine",
|
||||
"OpenCombineDispatch",
|
||||
"OpenCombineFoundation"],
|
||||
swiftSettings: [.unsafeFlags(["-enable-testing"])])
|
||||
.target(
|
||||
name: "OpenCombineFoundation",
|
||||
dependencies: [
|
||||
"OpenCombine",
|
||||
.target(name: "COpenCombineHelpers",
|
||||
condition: .when(platforms: supportedPlatforms.except([.wasi])))
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "OpenCombineShim",
|
||||
dependencies: [
|
||||
"OpenCombine",
|
||||
.target(name: "OpenCombineDispatch",
|
||||
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
|
||||
.target(name: "OpenCombineFoundation",
|
||||
condition: .when(platforms: supportedPlatforms.except([.wasi])))
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "OpenCombineTests",
|
||||
dependencies: [
|
||||
"OpenCombine",
|
||||
.target(name: "OpenCombineDispatch",
|
||||
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
|
||||
.target(name: "OpenCombineFoundation",
|
||||
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
|
||||
],
|
||||
swiftSettings: [
|
||||
.unsafeFlags(["-enable-testing"]),
|
||||
.define("WASI", .when(platforms: [.wasi]))
|
||||
]
|
||||
)
|
||||
],
|
||||
cxxLanguageStandard: .cxx1z
|
||||
cxxLanguageStandard: .cxx17
|
||||
)
|
||||
|
||||
// MARK: Helpers
|
||||
|
||||
extension Array where Element == Platform {
|
||||
func except(_ exceptions: [Platform]) -> [Platform] {
|
||||
return filter { !exceptions.contains($0) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// swift-tools-version:5.0
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "OpenCombine",
|
||||
products: [
|
||||
.library(name: "OpenCombine", targets: ["OpenCombine"]),
|
||||
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
|
||||
.library(name: "OpenCombineFoundation", targets: ["OpenCombineFoundation"]),
|
||||
.library(name: "OpenCombineShim", targets: ["OpenCombineShim"]),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "COpenCombineHelpers"),
|
||||
.target(name: "OpenCombine", dependencies: ["COpenCombineHelpers"]),
|
||||
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
|
||||
.target(name: "OpenCombineFoundation", dependencies: ["OpenCombine",
|
||||
"COpenCombineHelpers"]),
|
||||
.target(
|
||||
name: "OpenCombineShim",
|
||||
dependencies: [
|
||||
"OpenCombine",
|
||||
"OpenCombineDispatch",
|
||||
"OpenCombineFoundation",
|
||||
]
|
||||
),
|
||||
.testTarget(name: "OpenCombineTests",
|
||||
dependencies: ["OpenCombine",
|
||||
"OpenCombineDispatch",
|
||||
"OpenCombineFoundation"],
|
||||
swiftSettings: [.unsafeFlags(["-enable-testing"])])
|
||||
],
|
||||
cxxLanguageStandard: .cxx1z
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
// swift-tools-version:5.1
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "OpenCombine",
|
||||
products: [
|
||||
.library(name: "OpenCombine", targets: ["OpenCombine"]),
|
||||
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
|
||||
.library(name: "OpenCombineFoundation", targets: ["OpenCombineFoundation"]),
|
||||
.library(name: "OpenCombineShim", targets: ["OpenCombineShim"]),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "COpenCombineHelpers"),
|
||||
.target(name: "OpenCombine", dependencies: ["COpenCombineHelpers"]),
|
||||
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
|
||||
.target(name: "OpenCombineFoundation", dependencies: ["OpenCombine",
|
||||
"COpenCombineHelpers"]),
|
||||
.target(
|
||||
name: "OpenCombineShim",
|
||||
dependencies: [
|
||||
"OpenCombine",
|
||||
"OpenCombineDispatch",
|
||||
"OpenCombineFoundation",
|
||||
]
|
||||
),
|
||||
.testTarget(name: "OpenCombineTests",
|
||||
dependencies: ["OpenCombine",
|
||||
"OpenCombineDispatch",
|
||||
"OpenCombineFoundation"],
|
||||
swiftSettings: [.unsafeFlags(["-enable-testing"])])
|
||||
],
|
||||
cxxLanguageStandard: .cxx1z
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
// swift-tools-version:5.2
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "OpenCombine",
|
||||
products: [
|
||||
.library(name: "OpenCombine", targets: ["OpenCombine"]),
|
||||
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
|
||||
.library(name: "OpenCombineFoundation", targets: ["OpenCombineFoundation"]),
|
||||
.library(name: "OpenCombineShim", targets: ["OpenCombineShim"]),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "COpenCombineHelpers"),
|
||||
.target(name: "OpenCombine", dependencies: ["COpenCombineHelpers"]),
|
||||
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
|
||||
.target(name: "OpenCombineFoundation", dependencies: ["OpenCombine",
|
||||
"COpenCombineHelpers"]),
|
||||
.target(
|
||||
name: "OpenCombineShim",
|
||||
dependencies: [
|
||||
"OpenCombine",
|
||||
"OpenCombineDispatch",
|
||||
"OpenCombineFoundation",
|
||||
]
|
||||
),
|
||||
.testTarget(name: "OpenCombineTests",
|
||||
dependencies: ["OpenCombine",
|
||||
"OpenCombineDispatch",
|
||||
"OpenCombineFoundation"],
|
||||
swiftSettings: [.unsafeFlags(["-enable-testing"])])
|
||||
],
|
||||
cxxLanguageStandard: .cxx1z
|
||||
)
|
||||
@@ -0,0 +1,90 @@
|
||||
// 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: [
|
||||
"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,85 @@
|
||||
// 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: [
|
||||
"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,37 +1,54 @@
|
||||
# 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)
|
||||
[<img src="https://img.shields.io/badge/slack-OpenCombine-yellow.svg?logo=slack">](https://join.slack.com/t/opencombine/shared_invite/zt-96rr6cyf-0Hk5_hY8nM5zta6M56Jfzg)
|
||||
|
||||
Open-source implementation of Apple's [Combine](https://developer.apple.com/documentation/combine) framework for processing values over time.
|
||||
|
||||
The main goal of this project is to provide a compatible, reliable and efficient implementation which can be used on Apple's operating systems before macOS 10.15 and iOS 13, as well as Linux and Windows.
|
||||
The main goal of this project is to provide a compatible, reliable and efficient implementation which can be used on Apple's operating systems before macOS 10.15 and iOS 13, as well as Linux, Windows and WebAssembly.
|
||||
|
||||
| **CI Status** |
|
||||
|---|
|
||||
|[](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).
|
||||
|
||||
OpenCombine itself does not have any dependencies. Not even Foundation or Dispatch. If you want to use OpenCombine with Dispatch (for example for using `DispatchQueue` as `Scheduler` for operators like `debounce`, `receive(on:)` etc.), you will need to import both `OpenCombine` and `OpenCombineDispatch`. The same applies to Foundation: if you want to use, for instance, `NotificationCenter` or `URLSession` publishers, you'll need to also import `OpenCombineFoundation`
|
||||
OpenCombine itself does not have any dependencies. Not even Foundation or Dispatch. If you want to use OpenCombine with Dispatch (for example for using `DispatchQueue` as `Scheduler` for operators like `debounce`, `receive(on:)` etc.), you will need to import both `OpenCombine` and `OpenCombineDispatch`. The same applies to Foundation: if you want to use, for instance, `NotificationCenter` or `URLSession` publishers, you'll need to also import `OpenCombineFoundation`.
|
||||
|
||||
If you develop code for multiple platforms, you may find it more convenient to import the
|
||||
`OpenCombineShim` module instead. It conditionally re-exports Combine on Apple platforms (if
|
||||
available), and all OpenCombine modules on other platforms. You can import `OpenCombineShim` only
|
||||
when using SwiftPM. It is not currently available for CocoaPods.
|
||||
|
||||
##### Swift Package Manager
|
||||
###### Swift Package
|
||||
To add `OpenCombine` to your [SPM](https://swift.org/package-manager/) package, add the `OpenCombine` package to the list of package and target dependencies in your `Package.swift` file.
|
||||
To add `OpenCombine` to your [SwiftPM](https://swift.org/package-manager/) package, add the `OpenCombine` package to the list of package and target dependencies in your `Package.swift` file. `OpenCombineDispatch` and `OpenCombineFoundation` products are currently not supported on WebAssembly. If your project targets WebAssembly exclusively, you should omit them from the list of your dependencies. If it targets multiple platforms including WebAssembly, depend on them only on non-WebAssembly platforms with [conditional target dependencies](https://github.com/apple/swift-evolution/blob/main/proposals/0273-swiftpm-conditional-target-dependencies.md).
|
||||
|
||||
```swift
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.11.0")
|
||||
.package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.13.0")
|
||||
],
|
||||
targets: [
|
||||
.target(name: "MyAwesomePackage", dependencies: ["OpenCombine",
|
||||
"OpenCombineDispatch",
|
||||
"OpenCombineFoundation"])
|
||||
.target(
|
||||
name: "MyAwesomePackage",
|
||||
dependencies: [
|
||||
"OpenCombine",
|
||||
.product(name: "OpenCombineFoundation", package: "OpenCombine"),
|
||||
.product(name: "OpenCombineDispatch", package: "OpenCombine")
|
||||
]
|
||||
),
|
||||
]
|
||||
```
|
||||
|
||||
###### Xcode
|
||||
`OpenCombine` can also be added as a SPM dependency directly in your Xcode project *(requires Xcode 11 upwards)*.
|
||||
`OpenCombine` can also be added as a SwiftPM dependency directly in your Xcode project *(requires Xcode 11 upwards)*.
|
||||
|
||||
To do so, open Xcode, use **File** → **Swift Packages** → **Add Package Dependency…**, enter the [repository URL](https://github.com/OpenCombine/OpenCombine.git), choose the latest available version, and activate the checkboxes:
|
||||
|
||||
@@ -44,9 +61,9 @@ To do so, open Xcode, use **File** → **Swift Packages** → **Add Package Depe
|
||||
To add `OpenCombine` to a project using [CocoaPods](https://cocoapods.org/), add `OpenCombine` and `OpenCombineDispatch` to the list of target dependencies in your `Podfile`.
|
||||
|
||||
```ruby
|
||||
pod 'OpenCombine', '~> 0.11.0'
|
||||
pod 'OpenCombineDispatch', '~> 0.11.0'
|
||||
pod 'OpenCombineFoundation', '~> 0.11.0'
|
||||
pod 'OpenCombine', '~> 0.13.0'
|
||||
pod 'OpenCombineDispatch', '~> 0.13.0'
|
||||
pod 'OpenCombineFoundation', '~> 0.13.0'
|
||||
```
|
||||
|
||||
### Contributing
|
||||
|
||||
@@ -244,47 +244,6 @@ extension Publisher {
|
||||
public func collect<S>(_ strategy: Publishers.TimeGroupingStrategy<S>, options: S.SchedulerOptions? = nil) -> Publishers.CollectByTime<Self, S> where S : Scheduler
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
public struct PrefixUntilOutput<Upstream, Other> : Publisher where Upstream : Publisher, Other : Publisher {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// Another publisher, whose first output causes this publisher to finish.
|
||||
public let other: Other
|
||||
|
||||
public init(upstream: Upstream, other: Other)
|
||||
|
||||
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
|
||||
///
|
||||
/// - SeeAlso: `subscribe(_:)`
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<S>(subscriber: S) where S : Subscriber, Upstream.Failure == S.Failure, Upstream.Output == S.Input
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Republishes elements until another publisher emits an element.
|
||||
///
|
||||
/// After the second publisher publishes an element, the publisher returned by this method finishes.
|
||||
///
|
||||
/// - Parameter publisher: A second publisher.
|
||||
/// - Returns: A publisher that republishes elements until the second publisher publishes an element.
|
||||
public func prefix<P>(untilOutputFrom publisher: P) -> Publishers.PrefixUntilOutput<Self, P> where P : Publisher
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher created by applying the merge function to two upstream publishers.
|
||||
@@ -816,169 +775,6 @@ extension Publisher {
|
||||
public func throttle<S>(for interval: S.SchedulerTimeType.Stride, scheduler: S, latest: Bool) -> Publishers.Throttle<Self, S> where S : Scheduler
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher created by applying the zip function to two upstream publishers.
|
||||
public struct Zip<A, B> : Publisher where A : Publisher, B : Publisher, A.Failure == B.Failure {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = (A.Output, B.Output)
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = A.Failure
|
||||
|
||||
public let a: A
|
||||
|
||||
public let b: B
|
||||
|
||||
public init(_ a: A, _ b: B)
|
||||
|
||||
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
|
||||
///
|
||||
/// - SeeAlso: `subscribe(_:)`
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<S>(subscriber: S) where S : Subscriber, B.Failure == S.Failure, S.Input == (A.Output, B.Output)
|
||||
}
|
||||
|
||||
/// A publisher created by applying the zip function to three upstream publishers.
|
||||
public struct Zip3<A, B, C> : Publisher where A : Publisher, B : Publisher, C : Publisher, A.Failure == B.Failure, B.Failure == C.Failure {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = (A.Output, B.Output, C.Output)
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = A.Failure
|
||||
|
||||
public let a: A
|
||||
|
||||
public let b: B
|
||||
|
||||
public let c: C
|
||||
|
||||
public init(_ a: A, _ b: B, _ c: C)
|
||||
|
||||
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
|
||||
///
|
||||
/// - SeeAlso: `subscribe(_:)`
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<S>(subscriber: S) where S : Subscriber, C.Failure == S.Failure, S.Input == (A.Output, B.Output, C.Output)
|
||||
}
|
||||
|
||||
/// A publisher created by applying the zip function to four upstream publishers.
|
||||
public struct Zip4<A, B, C, D> : Publisher where A : Publisher, B : Publisher, C : Publisher, D : Publisher, A.Failure == B.Failure, B.Failure == C.Failure, C.Failure == D.Failure {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = (A.Output, B.Output, C.Output, D.Output)
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = A.Failure
|
||||
|
||||
public let a: A
|
||||
|
||||
public let b: B
|
||||
|
||||
public let c: C
|
||||
|
||||
public let d: D
|
||||
|
||||
public init(_ a: A, _ b: B, _ c: C, _ d: D)
|
||||
|
||||
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
|
||||
///
|
||||
/// - SeeAlso: `subscribe(_:)`
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<S>(subscriber: S) where S : Subscriber, D.Failure == S.Failure, S.Input == (A.Output, B.Output, C.Output, D.Output)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Combine elements from another publisher and deliver pairs of elements as tuples.
|
||||
///
|
||||
/// The returned publisher waits until both publishers have emitted an event, then delivers the oldest unconsumed event from each publisher together as a tuple to the subscriber.
|
||||
/// For example, if publisher `P1` emits elements `a` and `b`, and publisher `P2` emits event `c`, the zip publisher emits the tuple `(a, c)`. It 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
|
||||
|
||||
/// 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
|
||||
|
||||
/// 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<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
|
||||
}
|
||||
|
||||
extension Publishers.CombineLatest : Equatable where A : Equatable, B : Equatable {
|
||||
|
||||
/// Returns a Boolean value that indicates whether two publishers are equivalent.
|
||||
@@ -1106,19 +902,6 @@ extension Publishers.MergeMany : Equatable where Upstream : Equatable {
|
||||
public static func == (lhs: Publishers.MergeMany<Upstream>, rhs: Publishers.MergeMany<Upstream>) -> Bool
|
||||
}
|
||||
|
||||
extension Publishers.Retry : Equatable where Upstream : Equatable {
|
||||
|
||||
/// Returns a Boolean value indicating whether two values are equal.
|
||||
///
|
||||
/// Equality is the inverse of inequality. For any values `a` and `b`,
|
||||
/// `a == b` implies that `a != b` is `false`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - lhs: A value to compare.
|
||||
/// - rhs: Another value to compare.
|
||||
public static func == (lhs: Publishers.Retry<Upstream>, rhs: Publishers.Retry<Upstream>) -> Bool
|
||||
}
|
||||
|
||||
extension Publishers.Zip : Equatable where A : Equatable, B : Equatable {
|
||||
|
||||
/// Returns a Boolean value that indicates whether two publishers are equivalent.
|
||||
|
||||
@@ -10,13 +10,31 @@
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <system_error>
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
|
||||
#if __has_include(<pthread.h>)
|
||||
# include <pthread.h>
|
||||
# define OPENCOMBINE_HAS_PTHREAD 1
|
||||
#else
|
||||
# define OPENCOMBINE_HAS_PTHREAD 0
|
||||
#endif
|
||||
|
||||
#if __has_include(<signal.h>)
|
||||
# include <signal.h>
|
||||
# define OPENCOMBINE_HAS_SIGNAL_HANDLING 1
|
||||
#else
|
||||
# define OPENCOMBINE_HAS_SIGNAL_HANDLING 0
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
# include <windows.h>
|
||||
#endif
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <os/lock.h>
|
||||
#endif // __APPLE__
|
||||
|
||||
#include <mutex>
|
||||
|
||||
// Throwing exceptions through language boundaries is undefined behavior,
|
||||
// so we must catch all of them in our extern "C" functions.
|
||||
#define OPENCOMBINE_HANDLE_EXCEPTION_BEGIN try {
|
||||
@@ -47,10 +65,11 @@ public:
|
||||
virtual void unlock() = 0;
|
||||
virtual void assertOwner() {}
|
||||
|
||||
virtual ~PlatformIndependentMutex() noexcept(false) {}
|
||||
virtual ~PlatformIndependentMutex() {}
|
||||
};
|
||||
|
||||
class PThreadMutex : PlatformIndependentMutex {
|
||||
#if OPENCOMBINE_HAS_PTHREAD
|
||||
class PThreadMutex final : PlatformIndependentMutex {
|
||||
private:
|
||||
pthread_mutex_t mutex_;
|
||||
public:
|
||||
@@ -66,20 +85,16 @@ public:
|
||||
PThreadMutex(PThreadMutex&&) = delete;
|
||||
PThreadMutex& operator=(PThreadMutex&&) = delete;
|
||||
|
||||
void lock() override final {
|
||||
void lock() override {
|
||||
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutex_lock(&mutex_));
|
||||
}
|
||||
|
||||
void unlock() override final {
|
||||
void unlock() override {
|
||||
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutex_unlock(&mutex_));
|
||||
}
|
||||
|
||||
~PThreadMutex() {
|
||||
// Yep, this destructor may throw. This is deliberate, since pthread_mutex_destroy
|
||||
// may fail.
|
||||
//
|
||||
// The altrenative is to just silently ignore the error, which is even worse.
|
||||
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutex_destroy(&mutex_));
|
||||
pthread_mutex_destroy(&mutex_);
|
||||
}
|
||||
protected:
|
||||
class Attributes {
|
||||
@@ -107,12 +122,8 @@ protected:
|
||||
setType(PTHREAD_MUTEX_ERRORCHECK);
|
||||
}
|
||||
|
||||
~Attributes() noexcept(false) {
|
||||
// Yep, this destructor may throw. This is deliberate,
|
||||
// since pthread_mutexattr_destroy may fail.
|
||||
//
|
||||
// The altrenative is to just silently ignore the error, which is even worse.
|
||||
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutexattr_destroy(&attrs_));
|
||||
~Attributes() {
|
||||
pthread_mutexattr_destroy(&attrs_);
|
||||
}
|
||||
private:
|
||||
void setType(int type) {
|
||||
@@ -124,21 +135,7 @@ protected:
|
||||
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutex_init(&mutex_, attributes.raw()));
|
||||
}
|
||||
};
|
||||
|
||||
class PThreadRecursiveMutex final : PThreadMutex {
|
||||
public:
|
||||
PThreadRecursiveMutex() {
|
||||
Attributes attrs;
|
||||
attrs.setRecursive();
|
||||
initialize(attrs);
|
||||
}
|
||||
|
||||
PThreadRecursiveMutex(const PThreadRecursiveMutex&) = delete;
|
||||
PThreadRecursiveMutex& operator=(const PThreadRecursiveMutex&) = delete;
|
||||
|
||||
PThreadRecursiveMutex(PThreadRecursiveMutex&&) = delete;
|
||||
PThreadRecursiveMutex& operator=(PThreadRecursiveMutex&&) = delete;
|
||||
};
|
||||
#endif // OPENCOMBINE_HAS_PTHREAD
|
||||
|
||||
#ifdef __APPLE__
|
||||
|
||||
@@ -167,9 +164,32 @@ public:
|
||||
};
|
||||
#endif // __APPLE__
|
||||
|
||||
} // end anonymous namespace
|
||||
template <typename Mu>
|
||||
class GenericMutex final : PlatformIndependentMutex {
|
||||
Mu mutex_;
|
||||
public:
|
||||
|
||||
extern "C" {
|
||||
GenericMutex() = default;
|
||||
|
||||
GenericMutex(const GenericMutex&) = delete;
|
||||
GenericMutex& operator=(const GenericMutex&) = delete;
|
||||
|
||||
GenericMutex(GenericMutex&&) = delete;
|
||||
GenericMutex& operator=(GenericMutex&&) = delete;
|
||||
|
||||
void lock() override {
|
||||
mutex_.lock();
|
||||
}
|
||||
|
||||
void unlock() override {
|
||||
mutex_.unlock();
|
||||
}
|
||||
};
|
||||
|
||||
using StdMutex = GenericMutex<std::mutex>;
|
||||
using StdRecursiveMutex = GenericMutex<std::recursive_mutex>;
|
||||
|
||||
} // end anonymous namespace
|
||||
|
||||
uint64_t opencombine_next_combine_identifier(void) {
|
||||
return next_combine_identifier.fetch_add(1);
|
||||
@@ -177,24 +197,27 @@ uint64_t opencombine_next_combine_identifier(void) {
|
||||
|
||||
OpenCombineUnfairLock opencombine_unfair_lock_alloc(void) {
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
|
||||
|
||||
#ifdef __APPLE__
|
||||
if (__builtin_available(macOS 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *)) {
|
||||
return {new OSUnfairLock};
|
||||
} else {
|
||||
return {new PThreadMutex};
|
||||
}
|
||||
#else
|
||||
#elif OPENCOMBINE_HAS_PTHREAD
|
||||
// When possible, use pthread mutex implementation, because it allows
|
||||
// setting the PTHREAD_MUTEX_ERRORCHECK attribute, which makes
|
||||
// recursive locking a hard error instead of UB.
|
||||
return {new PThreadMutex};
|
||||
#else
|
||||
return {new StdMutex};
|
||||
#endif
|
||||
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_END
|
||||
}
|
||||
|
||||
OpenCombineUnfairRecursiveLock opencombine_unfair_recursive_lock_alloc(void) {
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
|
||||
// TODO: Use os_unfair_recursive_lock on Darwin as soon as it becomes public API.
|
||||
return {new PThreadRecursiveMutex};
|
||||
return {new StdRecursiveMutex};
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_END
|
||||
}
|
||||
|
||||
@@ -237,7 +260,9 @@ void opencombine_unfair_recursive_lock_dealloc(OpenCombineUnfairRecursiveLock lo
|
||||
}
|
||||
|
||||
void opencombine_stop_in_debugger(void) {
|
||||
#if _WIN32
|
||||
DebugBreak();
|
||||
#elif OPENCOMBINE_HAS_SIGNAL_HANDLING
|
||||
raise(SIGTRAP);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
@@ -9,7 +9,7 @@ extension Publisher {
|
||||
|
||||
/// Wraps this publisher with a type eraser.
|
||||
///
|
||||
/// Use `eraseToAnyPublisher()` to expose an instance of `AnyPublishe`` to
|
||||
/// Use `eraseToAnyPublisher()` to expose an instance of `AnyPublisher`` to
|
||||
/// the downstream subscriber, rather than this publisher’s actual type.
|
||||
/// This form of _type erasure_ preserves abstraction across API boundaries, such as
|
||||
/// different modules.
|
||||
|
||||
@@ -9,6 +9,15 @@
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
#if WASI
|
||||
private var __identifier: UInt64 = 0
|
||||
|
||||
internal func __nextCombineIdentifier() -> UInt64 {
|
||||
defer { __identifier += 1 }
|
||||
return __identifier
|
||||
}
|
||||
#endif // WASI
|
||||
|
||||
/// A unique identifier for identifying publisher streams.
|
||||
///
|
||||
/// To conform to `CustomCombineIdentifierConvertible` in a
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
//
|
||||
// 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 {
|
||||
|
||||
@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 {
|
||||
|
||||
@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,331 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ┃
|
||||
// ┃ 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 {
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
||||
public var values: AsyncPublisher<Self> {
|
||||
return .init(self)
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
|
||||
public struct Iterator: AsyncIteratorProtocol {
|
||||
|
||||
public typealias Element = Upstream.Output
|
||||
|
||||
fileprivate let inner: Inner
|
||||
|
||||
public mutating func next() async -> Element? {
|
||||
return await withTaskCancellationHandler(
|
||||
handler: { [inner] in inner.cancel() },
|
||||
operation: { [inner] in await inner.next() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public typealias AsyncIterator = Iterator
|
||||
|
||||
private let publisher: Upstream
|
||||
|
||||
public init(_ publisher: Upstream) {
|
||||
self.publisher = publisher
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
||||
public var values: AsyncThrowingPublisher<Self> {
|
||||
return .init(self)
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
||||
public struct AsyncThrowingPublisher<Upstream: Publisher>: AsyncSequence
|
||||
{
|
||||
|
||||
public typealias Element = Upstream.Output
|
||||
|
||||
public struct Iterator: AsyncIteratorProtocol {
|
||||
|
||||
public typealias Element = Upstream.Output
|
||||
|
||||
fileprivate let inner: Inner
|
||||
|
||||
public mutating func next() async throws -> Element? {
|
||||
return try await withTaskCancellationHandler(
|
||||
handler: { [inner] in inner.cancel() },
|
||||
operation: { [inner] in try await inner.next() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public typealias AsyncIterator = Iterator
|
||||
|
||||
private let publisher: Upstream
|
||||
|
||||
public init(_ publisher: Upstream) {
|
||||
self.publisher = publisher
|
||||
}
|
||||
|
||||
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,203 @@
|
||||
${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 '}{
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
||||
public var values: ${instantiation}<Self> {
|
||||
return .init(self)
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
|
||||
public struct Iterator: AsyncIteratorProtocol {
|
||||
|
||||
public typealias Element = Upstream.Output
|
||||
|
||||
fileprivate let inner: Inner
|
||||
|
||||
public mutating func next() async ${'throws ' if throwing else ''}-> Element? {
|
||||
return ${'try ' if throwing else ''}await withTaskCancellationHandler(
|
||||
handler: { [inner] in inner.cancel() },
|
||||
operation: { [inner] in ${'try ' if throwing else ''}await inner.next() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public typealias AsyncIterator = Iterator
|
||||
|
||||
private let publisher: Upstream
|
||||
|
||||
public init(_ publisher: Upstream) {
|
||||
self.publisher = publisher
|
||||
}
|
||||
|
||||
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
|
||||
@@ -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,6 +136,24 @@ 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)
|
||||
let parent = self.parent.take()
|
||||
downstreamLock.unlock()
|
||||
parent?.disassociate(self)
|
||||
}
|
||||
@@ -150,47 +174,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 +214,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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,5 +9,22 @@
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
#if WASI
|
||||
internal struct __UnfairLock { // swiftlint:disable:this type_name
|
||||
internal static func allocate() -> UnfairLock { return .init() }
|
||||
internal func lock() {}
|
||||
internal func unlock() {}
|
||||
internal func assertOwner() {}
|
||||
internal func deallocate() {}
|
||||
}
|
||||
|
||||
internal struct __UnfairRecursiveLock { // swiftlint:disable:this type_name
|
||||
internal static func allocate() -> UnfairRecursiveLock { return .init() }
|
||||
internal func lock() {}
|
||||
internal func unlock() {}
|
||||
internal func deallocate() {}
|
||||
}
|
||||
#endif // WASI
|
||||
|
||||
internal typealias UnfairLock = __UnfairLock
|
||||
internal typealias UnfairRecursiveLock = __UnfairRecursiveLock
|
||||
|
||||
@@ -187,8 +187,7 @@ extension PublishedSubject {
|
||||
return
|
||||
}
|
||||
self.downstream = nil
|
||||
let parent = self.parent
|
||||
self.parent = nil
|
||||
let parent = self.parent.take()
|
||||
lock.unlock()
|
||||
parent?.disassociate(self)
|
||||
}
|
||||
|
||||
@@ -48,8 +48,6 @@ internal class ReduceProducer<Downstream: Subscriber,
|
||||
|
||||
private var upstreamCompleted = false
|
||||
|
||||
private var empty = true
|
||||
|
||||
internal init(downstream: Downstream, initial: Output?, reduce: Reducer) {
|
||||
self.downstream = downstream
|
||||
self.initial = initial
|
||||
@@ -100,7 +98,9 @@ internal class ReduceProducer<Downstream: Subscriber,
|
||||
return
|
||||
}
|
||||
upstreamCompleted = true
|
||||
self.completed = downstreamRequested || empty
|
||||
if downstreamRequested {
|
||||
self.completed = true
|
||||
}
|
||||
let completed = self.completed
|
||||
let result = self.result
|
||||
lock.unlock()
|
||||
@@ -157,7 +157,6 @@ extension ReduceProducer: Subscriber {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
empty = false
|
||||
lock.unlock()
|
||||
|
||||
// Combine doesn't hold the lock when calling `receive(newValue:)`.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -42,26 +42,58 @@ public protocol ObservableObject: AnyObject {
|
||||
var objectWillChange: ObjectWillChangePublisher { get }
|
||||
}
|
||||
|
||||
extension ObservableObject where ObjectWillChangePublisher == ObservableObjectPublisher {
|
||||
// swiftlint:disable let_var_whitespace
|
||||
#if swift(>=5.1)
|
||||
/// A publisher that emits before the object has changed.
|
||||
@available(*, unavailable, message: """
|
||||
The default implementation of objectWillChange is not available yet. \
|
||||
It's being worked on in \
|
||||
https://github.com/broadwaylamb/OpenCombine/pull/97
|
||||
""")
|
||||
public var objectWillChange: ObservableObjectPublisher {
|
||||
fatalError("unimplemented")
|
||||
}
|
||||
#else
|
||||
public var objectWillChange: ObservableObjectPublisher {
|
||||
return ObservableObjectPublisher()
|
||||
}
|
||||
#endif
|
||||
// swiftlint:enable let_var_whitespace
|
||||
private protocol _ObservableObjectProperty {
|
||||
var objectWillChange: ObservableObjectPublisher? { get nonmutating set }
|
||||
}
|
||||
|
||||
#if swift(>=5.1)
|
||||
extension Published: _ObservableObjectProperty {}
|
||||
|
||||
extension ObservableObject where ObjectWillChangePublisher == ObservableObjectPublisher {
|
||||
|
||||
/// A publisher that emits before the object has changed.
|
||||
public var objectWillChange: ObservableObjectPublisher {
|
||||
var installedPublisher: ObservableObjectPublisher?
|
||||
var reflection: Mirror? = Mirror(reflecting: self)
|
||||
while let aClass = reflection {
|
||||
for (_, property) in aClass.children {
|
||||
guard let property = property as? _ObservableObjectProperty else {
|
||||
// Visit other fields until we meet a @Published field
|
||||
continue
|
||||
}
|
||||
|
||||
// Now we know that the field is @Published.
|
||||
if let alreadyInstalledPublisher = property.objectWillChange {
|
||||
installedPublisher = alreadyInstalledPublisher
|
||||
// Don't visit other fields, as all @Published fields
|
||||
// already have a publisher installed.
|
||||
break
|
||||
}
|
||||
|
||||
// Okay, this field doesn't have a publisher installed.
|
||||
// This means that other fields don't have it either
|
||||
// (because we install it only once and fields can't be added at runtime).
|
||||
var lazilyCreatedPublisher: ObjectWillChangePublisher {
|
||||
if let publisher = installedPublisher {
|
||||
return publisher
|
||||
}
|
||||
let publisher = ObservableObjectPublisher()
|
||||
installedPublisher = publisher
|
||||
return publisher
|
||||
}
|
||||
|
||||
property.objectWillChange = lazilyCreatedPublisher
|
||||
|
||||
// Continue visiting other fields.
|
||||
}
|
||||
reflection = aClass.superclassMirror
|
||||
}
|
||||
return installedPublisher ?? ObservableObjectPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/// A publisher that publishes changes from observable objects.
|
||||
public final class ObservableObjectPublisher: Publisher {
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -107,8 +107,16 @@ public struct Published<Value> {
|
||||
case value(Value)
|
||||
case publisher(Publisher)
|
||||
}
|
||||
@propertyWrapper
|
||||
private final class Box {
|
||||
var wrappedValue: Storage
|
||||
|
||||
private var storage: Storage
|
||||
init(wrappedValue: Storage) {
|
||||
self.wrappedValue = wrappedValue
|
||||
}
|
||||
}
|
||||
|
||||
@Box private var storage: Storage
|
||||
|
||||
internal var objectWillChange: ObservableObjectPublisher? {
|
||||
get {
|
||||
@@ -119,8 +127,8 @@ public struct Published<Value> {
|
||||
return publisher.subject.objectWillChange
|
||||
}
|
||||
}
|
||||
set {
|
||||
projectedValue.subject.objectWillChange = newValue
|
||||
nonmutating set {
|
||||
getPublisher().subject.objectWillChange = newValue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +153,7 @@ public struct Published<Value> {
|
||||
///
|
||||
/// - Parameter initialValue: The publisher's initial value.
|
||||
public init(wrappedValue: Value) {
|
||||
storage = .value(wrappedValue)
|
||||
_storage = Box(wrappedValue: .value(wrappedValue))
|
||||
}
|
||||
|
||||
/// The property for which this instance exposes a publisher.
|
||||
@@ -153,14 +161,7 @@ public struct Published<Value> {
|
||||
/// The `projectedValue` is the property accessed with the `$` operator.
|
||||
public var projectedValue: Publisher {
|
||||
mutating get {
|
||||
switch storage {
|
||||
case .value(let value):
|
||||
let publisher = Publisher(value)
|
||||
storage = .publisher(publisher)
|
||||
return publisher
|
||||
case .publisher(let publisher):
|
||||
return publisher
|
||||
}
|
||||
return getPublisher()
|
||||
}
|
||||
set { // swiftlint:disable:this unused_setter_value
|
||||
switch storage {
|
||||
@@ -172,6 +173,17 @@ public struct Published<Value> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Note: This method can mutate `storage`
|
||||
internal func getPublisher() -> Publisher {
|
||||
switch storage {
|
||||
case .value(let value):
|
||||
let publisher = Publisher(value)
|
||||
storage = .publisher(publisher)
|
||||
return publisher
|
||||
case .publisher(let publisher):
|
||||
return publisher
|
||||
}
|
||||
}
|
||||
// swiftlint:disable let_var_whitespace
|
||||
@available(*, unavailable, message: """
|
||||
@Published is only available on properties of classes
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Sergej Jaskiewicz on 03.12.2019.
|
||||
//
|
||||
|
||||
#if !WASI
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
@@ -225,3 +227,5 @@ extension Publishers.Breakpoint {
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
#endif // !WASI
|
||||
|
||||
@@ -331,9 +331,7 @@ extension Publishers.Buffer {
|
||||
private func lockedPop(_ demand: Subscribers.Demand) -> [Input] {
|
||||
assert(demand > 0)
|
||||
guard let max = demand.max else {
|
||||
let poppedValues = self.values
|
||||
self.values = []
|
||||
return poppedValues
|
||||
return values.take()
|
||||
}
|
||||
|
||||
let poppedValues = Array(values.prefix(max))
|
||||
|
||||
@@ -128,8 +128,7 @@ extension Publishers.CollectByCount {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
let output = self.buffer
|
||||
self.buffer = []
|
||||
let output = self.buffer.take()
|
||||
lock.unlock()
|
||||
return downstream.receive(output) * count
|
||||
}
|
||||
@@ -143,8 +142,7 @@ extension Publishers.CollectByCount {
|
||||
if buffer.isEmpty {
|
||||
lock.unlock()
|
||||
} else {
|
||||
let buffer = self.buffer
|
||||
self.buffer = []
|
||||
let buffer = self.buffer.take()
|
||||
lock.unlock()
|
||||
_ = downstream.receive(buffer)
|
||||
}
|
||||
@@ -168,10 +166,9 @@ extension Publishers.CollectByCount {
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
if let subscription = self.subscription {
|
||||
if let subscription = self.subscription.take() {
|
||||
buffer = []
|
||||
finished = true
|
||||
self.subscription = nil
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
} else {
|
||||
|
||||
@@ -233,7 +233,7 @@ extension Publishers.Concatenate {
|
||||
|
||||
private var suffixState = SubscriptionStatus.awaitingSubscription
|
||||
|
||||
private let suffix: Suffix
|
||||
private var suffix: Suffix?
|
||||
|
||||
private var pending = Subscribers.Demand.none
|
||||
|
||||
@@ -266,8 +266,18 @@ extension Publishers.Concatenate {
|
||||
prefixState.subscription ?? suffixState.subscription
|
||||
prefixState = .terminal
|
||||
suffixState = .terminal
|
||||
lock.unlock()
|
||||
upstreamSubscription?.cancel()
|
||||
|
||||
// We MUST release the object AFTER unlocking the lock,
|
||||
// since releasing it may trigger execution of arbitrary code,
|
||||
// for example, if the object has a deinit.
|
||||
// When the object deallocates, its deinit is called, and holding
|
||||
// the lock at that moment can lead to deadlocks.
|
||||
|
||||
withExtendedLifetime(suffix) {
|
||||
suffix = nil
|
||||
lock.unlock()
|
||||
upstreamSubscription?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
var description: String { return "Concatenate" }
|
||||
@@ -320,7 +330,7 @@ extension Publishers.Concatenate {
|
||||
lock.unlock()
|
||||
switch completion {
|
||||
case .finished:
|
||||
suffix.subscribe(SuffixSubscriber(inner: self))
|
||||
suffix?.subscribe(SuffixSubscriber(inner: self))
|
||||
case .failure:
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
@@ -212,8 +212,7 @@ extension Publishers.Debounce {
|
||||
let generation = currentGeneration
|
||||
currentValue = input
|
||||
let due = scheduler.now.advanced(by: dueTime)
|
||||
let previousCancellers = self.currentCancellers
|
||||
currentCancellers.removeAll()
|
||||
let previousCancellers = self.currentCancellers.take()
|
||||
currentCancellers[generation] = .pending
|
||||
lock.unlock()
|
||||
let newCanceller = scheduler.schedule(after: due,
|
||||
@@ -238,8 +237,7 @@ extension Publishers.Debounce {
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
let previousCancellers = currentCancellers
|
||||
currentCancellers.removeAll()
|
||||
let previousCancellers = currentCancellers.take()
|
||||
lock.unlock()
|
||||
for canceller in previousCancellers.values {
|
||||
canceller.cancel()
|
||||
@@ -268,8 +266,7 @@ extension Publishers.Debounce {
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
let previousCancellers = currentCancellers
|
||||
currentCancellers.removeAll()
|
||||
let previousCancellers = currentCancellers.take()
|
||||
lock.unlock()
|
||||
for canceller in previousCancellers.values {
|
||||
canceller.cancel()
|
||||
@@ -306,11 +303,10 @@ extension Publishers.Debounce {
|
||||
return
|
||||
}
|
||||
|
||||
guard let canceller = currentCancellers[generation] else {
|
||||
guard let canceller = currentCancellers[generation].take() else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
currentCancellers[generation] = nil
|
||||
|
||||
let hasAnyDemand = downstreamDemand != .none
|
||||
if hasAnyDemand {
|
||||
|
||||
@@ -139,8 +139,7 @@ extension Publishers.Drop {
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
let subscription = self.subscription
|
||||
self.subscription = nil
|
||||
let subscription = self.subscription.take()
|
||||
lock.unlock()
|
||||
subscription?.cancel()
|
||||
}
|
||||
|
||||
@@ -204,8 +204,7 @@ extension Publishers.DropUntilOutput {
|
||||
}
|
||||
|
||||
otherFinished = true
|
||||
if let upstreamSubscription = self.upstreamSubscription {
|
||||
self.upstreamSubscription = nil
|
||||
if let upstreamSubscription = self.upstreamSubscription.take() {
|
||||
lock.unlock()
|
||||
upstreamSubscription.cancel()
|
||||
} else {
|
||||
@@ -229,10 +228,8 @@ extension Publishers.DropUntilOutput {
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
let upstreamSubscription = self.upstreamSubscription
|
||||
let otherSubscription = self.otherSubscription
|
||||
self.upstreamSubscription = nil
|
||||
self.otherSubscription = nil
|
||||
let upstreamSubscription = self.upstreamSubscription.take()
|
||||
let otherSubscription = self.otherSubscription.take()
|
||||
cancelled = true
|
||||
lock.unlock()
|
||||
|
||||
|
||||
@@ -221,8 +221,7 @@ extension Publishers.${instantiation} {
|
||||
} catch {
|
||||
lock.lock()
|
||||
finished = true
|
||||
let subscription = self.subscription
|
||||
self.subscription = nil
|
||||
let subscription = self.subscription.take()
|
||||
lock.unlock()
|
||||
subscription?.cancel()
|
||||
downstream.receive(completion: .failure(error))
|
||||
@@ -251,11 +250,10 @@ extension Publishers.${instantiation} {
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard !finished, let subscription = self.subscription else {
|
||||
guard !finished, let subscription = self.subscription.take() else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
self.subscription = nil
|
||||
finished = true
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
|
||||
@@ -14,7 +14,7 @@ extension Publisher {
|
||||
/// the elements from one kind of publisher into a new publisher that is sent
|
||||
/// to subscribers. Use `flatMap(maxPublishers:_:)` when you want to create a new
|
||||
/// series of events for downstream subscribers based on the received value.
|
||||
/// The closure creates the new `Publishe`` based on the received value.
|
||||
/// The closure creates the new `Publisher` based on the received value.
|
||||
/// The new `Publisher` can emit more than one event, and successful completion of
|
||||
/// the new `Publisher` does not complete the overall stream.
|
||||
/// Failure of the new `Publisher` will fail the overall stream.
|
||||
@@ -157,17 +157,17 @@ extension Publishers {
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Child.Output == Downstream.Input, Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
let inner = Inner(downstream: subscriber,
|
||||
let outer = Outer(downstream: subscriber,
|
||||
maxPublishers: maxPublishers,
|
||||
map: transform)
|
||||
subscriber.receive(subscription: inner)
|
||||
upstream.subscribe(inner)
|
||||
subscriber.receive(subscription: outer)
|
||||
upstream.subscribe(outer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.FlatMap {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
private final class Outer<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
@@ -243,7 +243,7 @@ extension Publishers.FlatMap {
|
||||
subscription.request(maxPublishers)
|
||||
}
|
||||
|
||||
fileprivate func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
fileprivate func receive(_ input: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
let cancelledOrCompleted = self.cancelledOrCompleted
|
||||
lock.unlock()
|
||||
@@ -260,9 +260,9 @@ extension Publishers.FlatMap {
|
||||
return .none
|
||||
}
|
||||
|
||||
fileprivate func receive(completion: Subscribers.Completion<Child.Failure>) {
|
||||
outerSubscription = nil
|
||||
fileprivate func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
outerSubscription = nil
|
||||
outerFinished = true
|
||||
switch completion {
|
||||
case .finished:
|
||||
@@ -272,6 +272,8 @@ extension Publishers.FlatMap {
|
||||
let wasAlreadyCancelledOrCompleted = cancelledOrCompleted
|
||||
cancelledOrCompleted = true
|
||||
for (_, subscription) in subscriptions {
|
||||
// Cancelling subscriptions with the lock acquired. Not good,
|
||||
// but that's what Combine does. This code path is tested.
|
||||
subscription.cancel()
|
||||
}
|
||||
subscriptions = [:]
|
||||
@@ -302,8 +304,7 @@ extension Publishers.FlatMap {
|
||||
}
|
||||
if demand == .unlimited {
|
||||
downstreamDemand = .unlimited
|
||||
let buffer = self.buffer
|
||||
self.buffer = []
|
||||
let buffer = self.buffer.take()
|
||||
let subscriptions = self.subscriptions
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
@@ -354,16 +355,19 @@ extension Publishers.FlatMap {
|
||||
|
||||
fileprivate func cancel() {
|
||||
lock.lock()
|
||||
if cancelledOrCompleted {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
cancelledOrCompleted = true
|
||||
let subscriptions = self.subscriptions
|
||||
self.subscriptions = [:]
|
||||
let subscriptions = self.subscriptions.take()
|
||||
let outerSubscription = self.outerSubscription.take()
|
||||
lock.unlock()
|
||||
for (_, subscription) in subscriptions {
|
||||
subscription.cancel()
|
||||
}
|
||||
// Combine doesn't acquire the lock here. Weird.
|
||||
// Combine doesn't acquire outerLock here. Weird.
|
||||
outerSubscription?.cancel()
|
||||
outerSubscription = nil
|
||||
}
|
||||
|
||||
// MARK: - Reflection
|
||||
@@ -443,8 +447,7 @@ extension Publishers.FlatMap {
|
||||
return
|
||||
}
|
||||
cancelledOrCompleted = true
|
||||
let subscriptions = self.subscriptions
|
||||
self.subscriptions = [:]
|
||||
let subscriptions = self.subscriptions.take()
|
||||
lock.unlock()
|
||||
for (i, subscription) in subscriptions where i != index {
|
||||
subscription.cancel()
|
||||
@@ -471,9 +474,9 @@ extension Publishers.FlatMap {
|
||||
private func releaseLockThenSendCompletionDownstreamIfNeeded(
|
||||
outerFinished: Bool
|
||||
) -> Bool {
|
||||
#if DEBUG
|
||||
#if DEBUG
|
||||
lock.assertOwner() // Sanity check
|
||||
#endif
|
||||
#endif
|
||||
if !cancelledOrCompleted && outerFinished && buffer.isEmpty &&
|
||||
subscriptions.count + pendingSubscriptions == 0 {
|
||||
cancelledOrCompleted = true
|
||||
@@ -495,10 +498,10 @@ extension Publishers.FlatMap {
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible {
|
||||
private let index: SubscriptionIndex
|
||||
private let inner: Inner
|
||||
private let inner: Outer
|
||||
fileprivate let combineIdentifier = CombineIdentifier()
|
||||
|
||||
fileprivate init(index: SubscriptionIndex, inner: Inner) {
|
||||
fileprivate init(index: SubscriptionIndex, inner: Outer) {
|
||||
self.index = index
|
||||
self.inner = inner
|
||||
}
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
//
|
||||
// Publishers.PrefixUntilOutput.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 08.11.2020.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Republishes elements until another publisher emits an element.
|
||||
///
|
||||
/// After the second publisher publishes an element, the publisher returned by this
|
||||
/// method finishes.
|
||||
///
|
||||
/// - Parameter publisher: A second publisher.
|
||||
/// - Returns: A publisher that republishes elements until the second publisher
|
||||
/// publishes an element.
|
||||
public func prefix<Other: Publisher>(
|
||||
untilOutputFrom publisher: Other
|
||||
) -> Publishers.PrefixUntilOutput<Self, Other> {
|
||||
return .init(upstream: self, other: publisher)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
public struct PrefixUntilOutput<Upstream: Publisher, Other: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// Another publisher, whose first output causes this publisher to finish.
|
||||
public let other: Other
|
||||
|
||||
public init(upstream: Upstream, other: Other) {
|
||||
self.upstream = upstream
|
||||
self.other = other
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == Failure, Downstream.Input == Output
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, trigger: other))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.PrefixUntilOutput {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private struct Termination: Subscriber {
|
||||
|
||||
let inner: Inner
|
||||
|
||||
var combineIdentifier: CombineIdentifier {
|
||||
return inner.combineIdentifier
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
inner.terminationReceive(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Other.Output) -> Subscribers.Demand {
|
||||
return inner.terminationReceive(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Other.Failure>) {
|
||||
inner.terminationReceive(completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
private var termination: Termination?
|
||||
private var prefixState = SubscriptionStatus.awaitingSubscription
|
||||
private var terminationState = SubscriptionStatus.awaitingSubscription
|
||||
private var triggered = false
|
||||
private let lock = UnfairLock.allocate()
|
||||
private let downstream: Downstream
|
||||
|
||||
init(downstream: Downstream, trigger: Other) {
|
||||
self.downstream = downstream
|
||||
let termination = Termination(inner: self)
|
||||
self.termination = termination
|
||||
trigger.subscribe(termination)
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = prefixState else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
prefixState = triggered ? .terminal : .subscribed(subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case .subscribed = prefixState else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
lock.unlock()
|
||||
return downstream.receive(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
let prefixState = self.prefixState
|
||||
let terminationSubscription = terminationState.subscription
|
||||
self.prefixState = .terminal
|
||||
terminationState = .terminal
|
||||
termination = nil
|
||||
lock.unlock()
|
||||
terminationSubscription?.cancel()
|
||||
if case .subscribed = prefixState {
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = prefixState else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
let prefixSubscription = prefixState.subscription
|
||||
let terminationSubscription = terminationState.subscription
|
||||
prefixState = .terminal
|
||||
terminationState = .terminal
|
||||
lock.unlock()
|
||||
prefixSubscription?.cancel()
|
||||
terminationSubscription?.cancel()
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func terminationReceive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = terminationState else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
terminationState = .subscribed(subscription)
|
||||
lock.unlock()
|
||||
subscription.request(.max(1))
|
||||
}
|
||||
|
||||
private func terminationReceive(_ input: Other.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case .subscribed = terminationState else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
let prefixSubscription = prefixState.subscription
|
||||
prefixState = .terminal
|
||||
terminationState = .terminal
|
||||
termination = nil
|
||||
triggered = true
|
||||
lock.unlock()
|
||||
prefixSubscription?.cancel()
|
||||
downstream.receive(completion: .finished)
|
||||
return .none
|
||||
}
|
||||
|
||||
private func terminationReceive(
|
||||
completion: Subscribers.Completion<Other.Failure>
|
||||
) {
|
||||
lock.lock()
|
||||
terminationState = .terminal
|
||||
termination = nil
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,9 +75,7 @@ extension Publishers {
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Output == Downstream.Input, Downstream.Failure == Failure
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, output: output)
|
||||
upstream.subscribe(inner)
|
||||
subscriber.receive(subscription: inner)
|
||||
upstream.subscribe(Inner(downstream: subscriber, output: output))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,11 +121,8 @@ extension Publishers.ReplaceError {
|
||||
return
|
||||
}
|
||||
status = .subscribed(subscription)
|
||||
let pendingDemand = self.pendingDemand
|
||||
lock.unlock()
|
||||
if pendingDemand != .none {
|
||||
subscription.request(pendingDemand)
|
||||
}
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
@@ -150,7 +145,7 @@ extension Publishers.ReplaceError {
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
guard case .subscribed = status else {
|
||||
guard case .subscribed = status, !terminated else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
//
|
||||
// Publishers.Retry.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 28.06.2020.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Attempts to recreate a failed subscription with the upstream publisher up to
|
||||
/// the number of times you specify.
|
||||
///
|
||||
/// Use `retry(_:)` to try connecting to an upstream publisher after a failed
|
||||
/// connection attempt.
|
||||
///
|
||||
/// In the example below, a `URLSession.DataTaskPublisher` attempts to connect to
|
||||
/// a remote URL. If the connection attempt succeeds, it publishes the remote
|
||||
/// service’s HTML to the downstream publisher and completes normally. Otherwise,
|
||||
/// the retry operator attempts to reestablish the connection. If after three attempts
|
||||
/// the publisher still can’t connect to the remote URL, the `catch(_:)` operator
|
||||
/// replaces the error with a new publisher that publishes a “connection timed out”
|
||||
/// HTML page. After the downstream subscriber receives the timed out message,
|
||||
/// the stream completes normally.
|
||||
///
|
||||
/// struct WebSiteData: Codable {
|
||||
/// var rawHTML: String
|
||||
/// }
|
||||
///
|
||||
/// let myURL = URL(string: "https://www.example.com")
|
||||
///
|
||||
/// cancellable = URLSession.shared.dataTaskPublisher(for: myURL!)
|
||||
/// .retry(3)
|
||||
/// .map { page -> WebSiteData in
|
||||
/// WebSiteData(rawHTML: String(decoding: page.data, as: UTF8.self))
|
||||
/// }
|
||||
/// .catch { error in
|
||||
/// Just(
|
||||
/// WebSiteData(
|
||||
/// rawHTML: "<HTML>Unable to load page - timed out.</HTML>"
|
||||
/// )
|
||||
/// )
|
||||
/// }
|
||||
/// .sink(receiveCompletion: { print ("completion: \($0)") },
|
||||
/// receiveValue: { print ("value: \($0)") })
|
||||
///
|
||||
/// // Prints: The HTML content from the remote URL upon a successful connection,
|
||||
/// // or returns "<HTML>Unable to load page - timed out.</HTML>" if
|
||||
/// // the number of retries exceeds the specified value.
|
||||
///
|
||||
/// After exceeding the specified number of retries, the publisher passes the failure
|
||||
/// to the downstream receiver.
|
||||
/// - Parameter retries: The number of times to attempt to recreate the subscription.
|
||||
/// - Returns: A publisher that attempts to recreate its subscription to a failed
|
||||
/// upstream publisher.
|
||||
public func retry(_ retries: Int) -> Publishers.Retry<Self> {
|
||||
return .init(upstream: self, retries: retries)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that attempts to recreate its subscription to a failed upstream
|
||||
/// publisher.
|
||||
public struct Retry<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The maximum number of retry attempts to perform.
|
||||
///
|
||||
/// If `nil`, this publisher attempts to reconnect with the upstream publisher
|
||||
/// an unlimited number of times.
|
||||
public let retries: Int?
|
||||
|
||||
/// Creates a publisher that attempts to recreate its subscription to a failed
|
||||
/// upstream publisher.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - upstream: The publisher from which this publisher receives its elements.
|
||||
/// - retries: The maximum number of retry attempts to perform. If `nil`, this
|
||||
/// publisher attempts to reconnect with the upstream publisher an unlimited
|
||||
/// number of times.
|
||||
public init(upstream: Upstream, retries: Int?) {
|
||||
self.upstream = upstream
|
||||
self.retries = retries
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure
|
||||
{
|
||||
upstream.subscribe(Inner(parent: self, downstream: subscriber))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Retry: Equatable where Upstream: Equatable {}
|
||||
|
||||
extension Publishers.Retry {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Failure == Failure, Downstream.Input == Output
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private enum State {
|
||||
case ready(Publishers.Retry<Upstream>, Downstream)
|
||||
case terminal
|
||||
}
|
||||
|
||||
private enum Chances {
|
||||
case finite(Int)
|
||||
case infinite
|
||||
}
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private var state: State
|
||||
|
||||
private var upstreamSubscription: Subscription?
|
||||
|
||||
private var remaining: Chances
|
||||
|
||||
private var downstreamNeedsSubscription = true
|
||||
|
||||
private var downstreamDemand = Subscribers.Demand.none
|
||||
|
||||
private var completionRecursion = false
|
||||
|
||||
private var needsSubscribe = false
|
||||
|
||||
init(parent: Publishers.Retry<Upstream>, downstream: Downstream) {
|
||||
state = .ready(parent, downstream)
|
||||
remaining = parent.retries.map(Chances.finite) ?? .infinite
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case let .ready(_, downstream) = state, upstreamSubscription == nil
|
||||
else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
upstreamSubscription = subscription
|
||||
let downstreamDemand = self.downstreamDemand
|
||||
let downstreamNeedsSubscription = self.downstreamNeedsSubscription
|
||||
self.downstreamNeedsSubscription = false
|
||||
lock.unlock()
|
||||
if downstreamNeedsSubscription {
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
if downstreamDemand != .none {
|
||||
subscription.request(downstreamDemand)
|
||||
}
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case let .ready(_, downstream) = state else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
downstreamDemand -= 1
|
||||
lock.unlock()
|
||||
|
||||
let newDemand = downstream.receive(input)
|
||||
|
||||
if newDemand == .none { return .none }
|
||||
|
||||
lock.lock()
|
||||
downstreamDemand += newDemand
|
||||
|
||||
if let upstreamSubscription = self.upstreamSubscription {
|
||||
lock.unlock()
|
||||
upstreamSubscription.request(newDemand)
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
return .none
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
guard case let .ready(parent, downstream) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
|
||||
if case .failure = completion {
|
||||
upstreamSubscription = nil
|
||||
switch remaining {
|
||||
case .finite(0):
|
||||
break
|
||||
case .finite(let attempts):
|
||||
remaining = .finite(attempts - 1)
|
||||
fallthrough
|
||||
case .infinite:
|
||||
if completionRecursion {
|
||||
needsSubscribe = true
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
repeat {
|
||||
completionRecursion = true
|
||||
needsSubscribe = false
|
||||
lock.unlock()
|
||||
parent.upstream.subscribe(self)
|
||||
lock.lock()
|
||||
completionRecursion = false
|
||||
} while needsSubscribe
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard case .ready = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
downstreamDemand += demand
|
||||
if let upstreamSubscription = self.upstreamSubscription {
|
||||
lock.unlock()
|
||||
upstreamSubscription.request(demand)
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case .ready = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
if let upstreamSubscription = self.upstreamSubscription {
|
||||
lock.unlock()
|
||||
upstreamSubscription.cancel()
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
var description: String { return "Retry" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -230,8 +230,7 @@ extension Publishers.SwitchToLatest {
|
||||
return .none
|
||||
}
|
||||
|
||||
if let currentInnerSubscription = self.currentInnerSubscription {
|
||||
self.currentInnerSubscription = nil
|
||||
if let currentInnerSubscription = self.currentInnerSubscription.take() {
|
||||
lock.unlock()
|
||||
currentInnerSubscription.cancel()
|
||||
lock.lock()
|
||||
@@ -272,8 +271,7 @@ extension Publishers.SwitchToLatest {
|
||||
lock.unlock()
|
||||
}
|
||||
case .failure:
|
||||
let currentInnerSubscription = self.currentInnerSubscription
|
||||
self.currentInnerSubscription = nil
|
||||
let currentInnerSubscription = self.currentInnerSubscription.take()
|
||||
sentCompletion = true
|
||||
lock.unlock()
|
||||
currentInnerSubscription?.cancel()
|
||||
@@ -298,10 +296,8 @@ extension Publishers.SwitchToLatest {
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
cancelled = true
|
||||
let currentInnerSubscription = self.currentInnerSubscription
|
||||
self.currentInnerSubscription = nil
|
||||
let outerSubscription = self.outerSubscription
|
||||
self.outerSubscription = nil
|
||||
let currentInnerSubscription = self.currentInnerSubscription.take()
|
||||
let outerSubscription = self.outerSubscription.take()
|
||||
lock.unlock()
|
||||
|
||||
currentInnerSubscription?.cancel()
|
||||
@@ -386,8 +382,7 @@ extension Publishers.SwitchToLatest {
|
||||
return
|
||||
}
|
||||
cancelled = true
|
||||
let outerSubscription = self.outerSubscription
|
||||
self.outerSubscription = nil
|
||||
let outerSubscription = self.outerSubscription.take()
|
||||
sentCompletion = true
|
||||
lock.unlock()
|
||||
outerSubscription?.cancel()
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
//
|
||||
// Publishers.Throttle.swift
|
||||
//
|
||||
//
|
||||
// Created by Stuart Austin on 14/11/2020.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
// swiftlint:disable generic_type_name line_length
|
||||
|
||||
/// Publishes either the most-recent or first element published by the upstream
|
||||
/// publisher in the specified time interval.
|
||||
///
|
||||
/// Use `throttle(for:scheduler:latest:`` to selectively republish elements from
|
||||
/// an upstream publisher during an interval you specify. Other elements received from
|
||||
/// the upstream in the throttling interval 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:
|
||||
/// - interval: The interval at which to find and emit either the most recent or
|
||||
/// the first element, expressed in the time system of the scheduler.
|
||||
/// - scheduler: The scheduler on which to publish elements.
|
||||
/// - latest: A Boolean value that indicates whether to publish the most recent
|
||||
/// element. If `false`, the publisher emits the first element received during
|
||||
/// the interval.
|
||||
/// - Returns: A publisher that emits either the most-recent or first element received
|
||||
/// during the specified interval.
|
||||
public func throttle<S>(for interval: S.SchedulerTimeType.Stride,
|
||||
scheduler: S,
|
||||
latest: Bool) -> Publishers.Throttle<Self, S>
|
||||
where S: Scheduler
|
||||
{
|
||||
return .init(upstream: self,
|
||||
interval: interval,
|
||||
scheduler: scheduler,
|
||||
latest: latest)
|
||||
}
|
||||
|
||||
// swiftlint:enable generic_type_name line_length
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that publishes either the most-recent or first element published by
|
||||
/// the upstream publisher in a specified time interval.
|
||||
public struct Throttle<Upstream, Context>: Publisher
|
||||
where Upstream: Publisher, Context: Scheduler
|
||||
{
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The interval in which to find and emit the most recent element.
|
||||
public let interval: Context.SchedulerTimeType.Stride
|
||||
|
||||
/// The scheduler on which to publish elements.
|
||||
public let scheduler: Context
|
||||
|
||||
/// A Boolean value indicating whether to publish the most recent element.
|
||||
///
|
||||
/// If `false`, the publisher emits the first element received during
|
||||
/// the interval.
|
||||
public let latest: Bool
|
||||
|
||||
public init(upstream: Upstream,
|
||||
interval: Context.SchedulerTimeType.Stride,
|
||||
scheduler: Context,
|
||||
latest: Bool) {
|
||||
|
||||
self.upstream = upstream
|
||||
self.interval = interval
|
||||
self.scheduler = scheduler
|
||||
self.latest = latest
|
||||
}
|
||||
|
||||
// swiftlint:disable generic_type_name
|
||||
|
||||
/// Attaches the specified subscriber to this publisher.
|
||||
///
|
||||
/// Implementations of ``Publisher`` must implement this method.
|
||||
///
|
||||
/// The provided implementation of ``Publisher/subscribe(_:)-4u8kn``calls
|
||||
/// this method.
|
||||
///
|
||||
/// - Parameter subscriber: The subscriber to attach to this ``Publisher``,
|
||||
/// after which it can receive values.
|
||||
public func receive<S>(subscriber: S)
|
||||
where S: Subscriber, Upstream.Failure == S.Failure, Upstream.Output == S.Input
|
||||
{
|
||||
let inner = Inner(interval: interval,
|
||||
scheduler: scheduler,
|
||||
latest: latest,
|
||||
downstream: subscriber)
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
|
||||
// swiftlint:enable generic_type_name
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Throttle {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private enum State {
|
||||
case awaitingSubscription(Downstream)
|
||||
case subscribed(Subscription, Downstream)
|
||||
case pendingTerminal(Subscription, Downstream)
|
||||
case terminal
|
||||
}
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
private let interval: Context.SchedulerTimeType.Stride
|
||||
private let scheduler: Context
|
||||
private let latest: Bool
|
||||
private var state: State
|
||||
private let downstreamLock = UnfairRecursiveLock.allocate()
|
||||
|
||||
private var lastEmissionTime: Context.SchedulerTimeType?
|
||||
|
||||
private var pendingInput: Input?
|
||||
private var pendingCompletion: Subscribers.Completion<Failure>?
|
||||
|
||||
private var demand: Subscribers.Demand = .none
|
||||
|
||||
private var lastTime: Context.SchedulerTimeType
|
||||
|
||||
init(interval: Context.SchedulerTimeType.Stride,
|
||||
scheduler: Context,
|
||||
latest: Bool,
|
||||
downstream: Downstream) {
|
||||
self.state = .awaitingSubscription(downstream)
|
||||
self.interval = interval
|
||||
self.scheduler = scheduler
|
||||
self.latest = latest
|
||||
|
||||
self.lastTime = scheduler.now
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
downstreamLock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case let .awaitingSubscription(downstream) = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
self.lastTime = scheduler.now
|
||||
|
||||
state = .subscribed(subscription, downstream)
|
||||
lock.unlock()
|
||||
|
||||
subscription.request(.unlimited)
|
||||
|
||||
downstreamLock.lock()
|
||||
downstream.receive(subscription: self)
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case .subscribed = state else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
|
||||
let lastTime = scheduler.now
|
||||
self.lastTime = lastTime
|
||||
|
||||
guard demand > .none else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
|
||||
let hasScheduledOutput = (pendingInput != nil || pendingCompletion != nil)
|
||||
|
||||
if hasScheduledOutput && latest {
|
||||
pendingInput = input
|
||||
lock.unlock()
|
||||
} else if !hasScheduledOutput {
|
||||
let minimumEmissionTime =
|
||||
lastEmissionTime.map { $0.advanced(by: interval) }
|
||||
|
||||
let emissionTime =
|
||||
minimumEmissionTime.map { Swift.max(lastTime, $0) } ?? lastTime
|
||||
|
||||
demand -= 1
|
||||
|
||||
pendingInput = input
|
||||
lock.unlock()
|
||||
|
||||
let action: () -> Void = { [weak self] in
|
||||
self?.scheduledEmission()
|
||||
}
|
||||
|
||||
if emissionTime == lastTime {
|
||||
scheduler.schedule(action)
|
||||
} else {
|
||||
scheduler.schedule(after: emissionTime, action)
|
||||
}
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
return .none
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription, downstream) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
let lastTime = scheduler.now
|
||||
self.lastTime = lastTime
|
||||
state = .pendingTerminal(subscription, downstream)
|
||||
|
||||
let hasScheduledOutput = (pendingInput != nil || pendingCompletion != nil)
|
||||
|
||||
if hasScheduledOutput && pendingCompletion == nil {
|
||||
pendingCompletion = completion
|
||||
lock.unlock()
|
||||
} else if !hasScheduledOutput {
|
||||
pendingCompletion = completion
|
||||
lock.unlock()
|
||||
|
||||
scheduler.schedule { [weak self] in
|
||||
self?.scheduledEmission()
|
||||
}
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduledEmission() {
|
||||
lock.lock()
|
||||
|
||||
let downstream: Downstream
|
||||
|
||||
switch state {
|
||||
case .awaitingSubscription, .terminal:
|
||||
lock.unlock()
|
||||
return
|
||||
case let .subscribed(_, foundDownstream),
|
||||
let .pendingTerminal(_, foundDownstream):
|
||||
downstream = foundDownstream
|
||||
}
|
||||
|
||||
if self.pendingInput != nil && self.pendingCompletion == nil {
|
||||
lastEmissionTime = scheduler.now
|
||||
}
|
||||
|
||||
let pendingInput = self.pendingInput.take()
|
||||
let pendingCompletion = self.pendingCompletion.take()
|
||||
|
||||
if pendingCompletion != nil {
|
||||
state = .terminal
|
||||
}
|
||||
|
||||
lock.unlock()
|
||||
|
||||
downstreamLock.lock()
|
||||
|
||||
let newDemand: Subscribers.Demand
|
||||
if let input = pendingInput {
|
||||
newDemand = downstream.receive(input)
|
||||
} else {
|
||||
newDemand = .none
|
||||
}
|
||||
|
||||
if let completion = pendingCompletion {
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
downstreamLock.unlock()
|
||||
|
||||
guard newDemand > 0 else { return }
|
||||
self.lock.lock()
|
||||
demand += newDemand
|
||||
self.lock.unlock()
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
guard demand > 0 else { return }
|
||||
lock.lock()
|
||||
guard case .subscribed = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
self.demand += demand
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
|
||||
let subscription: Subscription?
|
||||
switch state {
|
||||
case let .subscribed(existingSubscription, _),
|
||||
let .pendingTerminal(existingSubscription, _):
|
||||
subscription = existingSubscription
|
||||
case .awaitingSubscription, .terminal:
|
||||
subscription = nil
|
||||
}
|
||||
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
|
||||
subscription?.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "Throttle" }
|
||||
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,720 @@
|
||||
//
|
||||
// Publishers.Zip.swift
|
||||
//
|
||||
// Created by Eric Patey on 29.08.2019.
|
||||
//
|
||||
|
||||
#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,
|
||||
// swiftlint:disable:next large_tuple
|
||||
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,
|
||||
// swiftlint:disable:next large_tuple
|
||||
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 }
|
||||
}
|
||||
|
||||
fileprivate 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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
|
||||
// swiftlint:disable shorthand_operator - because of false positives here
|
||||
|
||||
#if canImport(_Concurrency) && compiler(>=5.5)
|
||||
import _Concurrency
|
||||
#endif
|
||||
|
||||
extension Subscribers {
|
||||
|
||||
/// A requested number of items, sent to a publisher from a subscriber through
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,8 +104,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 +122,8 @@ extension DispatchQueue {
|
||||
return .nanoseconds(magnitude)
|
||||
}
|
||||
|
||||
private init(magnitude: Int) {
|
||||
self.magnitude = magnitude
|
||||
private init(magnitude: Int64) {
|
||||
_nanoseconds = magnitude
|
||||
}
|
||||
|
||||
/// Creates a dispatch queue time interval from the given
|
||||
@@ -200,11 +209,11 @@ extension DispatchQueue {
|
||||
/// - Parameter exactly: A binary integer representing a time interval.
|
||||
public init?<Source: BinaryInteger>(exactly source: Source) {
|
||||
guard let value = Int(exactly: source) else { return nil }
|
||||
self = .nanoseconds(value)
|
||||
self = .seconds(value)
|
||||
}
|
||||
|
||||
public static func < (lhs: Stride, rhs: Stride) -> Bool {
|
||||
return lhs.magnitude < rhs.magnitude
|
||||
return lhs._nanoseconds < rhs._nanoseconds
|
||||
}
|
||||
|
||||
public static func * (lhs: Stride, rhs: Stride) -> Stride {
|
||||
@@ -212,43 +221,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 +404,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 {
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
// Created by Sergej Jaskiewicz on 28.10.2020.
|
||||
//
|
||||
|
||||
#if canImport(CoreFoundation)
|
||||
import CoreFoundation
|
||||
#endif
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Use CoreFoundation on Darwin, since some pure
|
||||
@@ -60,7 +63,12 @@ internal struct Timer {
|
||||
repeats: Bool,
|
||||
block: @escaping (Timer) -> Void
|
||||
) {
|
||||
self.init(fire: Date(), interval: timeInterval, repeats: repeats, block: block)
|
||||
self.init(
|
||||
fire: Date() + timeInterval,
|
||||
interval: timeInterval,
|
||||
repeats: repeats,
|
||||
block: block
|
||||
)
|
||||
}
|
||||
|
||||
internal var tolerance: TimeInterval {
|
||||
@@ -88,6 +96,7 @@ internal struct Timer {
|
||||
#endif
|
||||
}
|
||||
|
||||
#if canImport(CoreFoundation)
|
||||
fileprivate func getCFRunLoopTimer() -> CFRunLoopTimer? {
|
||||
#if canImport(Darwin)
|
||||
return underlyingTimer
|
||||
@@ -108,6 +117,7 @@ internal struct Timer {
|
||||
fatalError("unreachable")
|
||||
#endif
|
||||
}
|
||||
#endif // canImport(CoreFoundation)
|
||||
}
|
||||
|
||||
extension RunLoop {
|
||||
@@ -133,6 +143,7 @@ extension RunLoop {
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(CoreFoundation)
|
||||
extension RunLoop.Mode {
|
||||
fileprivate func asCFRunLoopMode() -> CFRunLoopMode {
|
||||
#if canImport(Darwin)
|
||||
@@ -154,3 +165,4 @@ extension RunLoop.Mode {
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif // canImport(CoreFoundation)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// Utils.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 28.08.2021.
|
||||
//
|
||||
|
||||
extension Optional {
|
||||
internal mutating func take() -> Optional {
|
||||
let taken = self
|
||||
self = nil
|
||||
return taken
|
||||
}
|
||||
}
|
||||
@@ -200,13 +200,13 @@ extension Notification {
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard let center = self.center, let observation = self.observation else {
|
||||
guard let center = self.center.take(),
|
||||
let observation = self.observation.take()
|
||||
else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
self.center = nil
|
||||
self.object = nil
|
||||
self.observation = nil
|
||||
lock.unlock()
|
||||
center.removeObserver(observation)
|
||||
}
|
||||
|
||||
@@ -234,19 +234,16 @@ extension OperationQueue {
|
||||
}
|
||||
|
||||
override func main() {
|
||||
guard let action = self.action else { return }
|
||||
self.action = nil
|
||||
guard let action = self.action.take() else { return }
|
||||
action()
|
||||
|
||||
guard let queue = self.queue,
|
||||
let context = self.context
|
||||
guard let queue = self.queue.take(),
|
||||
let context = self.context.take()
|
||||
else {
|
||||
self.queue = nil
|
||||
self.context = nil
|
||||
return
|
||||
}
|
||||
self.queue = nil
|
||||
self.context = nil
|
||||
|
||||
context.lock.lock()
|
||||
if context.operation == nil {
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 13.12.2019.
|
||||
//
|
||||
|
||||
import CoreFoundation
|
||||
import Foundation
|
||||
import OpenCombine
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 23.06.2020.
|
||||
//
|
||||
|
||||
import CoreFoundation
|
||||
import Foundation
|
||||
import OpenCombine
|
||||
|
||||
@@ -203,11 +202,10 @@ extension Foundation.Timer {
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
if downstream == nil {
|
||||
if downstream.take() == nil {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
downstream = nil
|
||||
lock.unlock()
|
||||
parent?.disconnect(combineIdentifier)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
#if canImport(Combine)
|
||||
@_exported import Combine
|
||||
#else
|
||||
@_exported import OpenCombine
|
||||
#if canImport(OpenCombineDispatch)
|
||||
@_exported import OpenCombineDispatch
|
||||
#endif
|
||||
#if canImport(OpenCombineFoundation)
|
||||
@_exported import OpenCombineFoundation
|
||||
#endif
|
||||
#endif
|
||||
@@ -5,4 +5,4 @@ disabled_rules:
|
||||
- explicit_acl
|
||||
- explicit_top_level_acl
|
||||
- explicit_enum_raw_value
|
||||
|
||||
- untyped_error_in_catch
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
//
|
||||
// FutureConcurrencyTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 12.12.2021.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if canImport(_Concurrency) && compiler(>=5.5)
|
||||
import _Concurrency
|
||||
#endif
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
// swiftlint:disable:next line_length
|
||||
#if !os(Windows) && !WASI && (canImport(_Concurrency) && compiler(>=5.5) || compiler(>=5.5.1)) // TEST_DISCOVERY_CONDITION
|
||||
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
||||
final class FutureConcurrencyTests: XCTestCase {
|
||||
|
||||
func testAsyncAwaitNonThrowingSuccess() async {
|
||||
var promise: Future<Int, Never>.Promise?
|
||||
let future = Future<Int, Never> { promise = $0 }
|
||||
|
||||
let task = Task {
|
||||
await future.value
|
||||
}
|
||||
|
||||
promise?(.success(42))
|
||||
|
||||
let value = await task.value
|
||||
|
||||
XCTAssertEqual(value, 42)
|
||||
}
|
||||
|
||||
func testAsyncAwaitThrowingSuccess() async throws {
|
||||
var promise: Future<Int, TestingError>.Promise?
|
||||
let future = Future<Int, TestingError> { promise = $0 }
|
||||
|
||||
let task = Task {
|
||||
try await future.value
|
||||
}
|
||||
|
||||
promise?(.success(42))
|
||||
|
||||
let value = try await task.value
|
||||
|
||||
XCTAssertEqual(value, 42)
|
||||
}
|
||||
|
||||
func testAsyncAwaitThrowingFailure() async throws {
|
||||
var promise: Future<Int, TestingError>.Promise?
|
||||
let future = Future<Int, TestingError> { promise = $0 }
|
||||
|
||||
let task = Task { try await future.value }
|
||||
|
||||
promise?(.failure(.oops))
|
||||
|
||||
do {
|
||||
_ = try await task.value
|
||||
XCTFail("Expected an error")
|
||||
} catch let error as TestingError {
|
||||
XCTAssertEqual(error, .oops)
|
||||
} catch {
|
||||
XCTFail("Unexpected error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,8 @@
|
||||
// Created by Sergej Jaskiewicz on 26.08.2019.
|
||||
//
|
||||
|
||||
#if !WASI // TEST_DISCOVERY_CONDITION
|
||||
|
||||
import Dispatch
|
||||
import XCTest
|
||||
|
||||
@@ -254,27 +256,25 @@ final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
XCTAssertEqual((2 as Stride).magnitude, 2_000_000_000)
|
||||
|
||||
XCTAssertNil(Stride(exactly: UInt64.max))
|
||||
XCTAssertEqual(Stride(exactly: 871 as UInt64)?.magnitude, 871)
|
||||
XCTAssertEqual(Stride(exactly: 2 as UInt64)?.magnitude, 2_000_000_000)
|
||||
}
|
||||
|
||||
func testStrideFromTooMuchSecondsCrashes() {
|
||||
assertCrashes {
|
||||
func testStrideFromTooMuchSeconds() {
|
||||
#if arch(x86_64) || arch(arm64) || arch(s390x) || arch(powerpc64) || arch(powerpc64le)
|
||||
// 64-bit platforms
|
||||
XCTAssertGreaterThan(
|
||||
Stride.seconds(Double(Int.max) / 1_000_000_000).magnitude,
|
||||
.max
|
||||
)
|
||||
// 64-bit platforms
|
||||
XCTAssertEqual(
|
||||
Stride.seconds(Double(Int.max) / 1_000_000_000).magnitude,
|
||||
.max
|
||||
)
|
||||
#elseif arch(i386) || arch(arm)
|
||||
// 32-bit platforms
|
||||
XCTAssertGreaterThan(
|
||||
Stride.seconds(Double(Int.max) / 1_000_000_000 + 1).magnitude,
|
||||
.max
|
||||
)
|
||||
// 32-bit platforms
|
||||
XCTAssertEqual(
|
||||
Stride.seconds(Double(Int.max) / 1_000_000_000).magnitude,
|
||||
.max
|
||||
)
|
||||
#else
|
||||
#error("This architecture isn't known. Add it to the 32-bit or 64-bit line.")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
func testStrideComparable() {
|
||||
@@ -441,7 +441,7 @@ final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
.encode(KeyedWrapper(value: stride))
|
||||
let encodedString = String(decoding: encodedData, as: UTF8.self)
|
||||
|
||||
XCTAssertEqual(encodedString, #"{"value":{"magnitude":419872}}"#)
|
||||
XCTAssertEqual(encodedString, #"{"value":{"_nanoseconds":419872}}"#)
|
||||
|
||||
let decodedStride = try decoder
|
||||
.decode(KeyedWrapper<Stride>.self, from: encodedData)
|
||||
@@ -484,11 +484,11 @@ final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
let main = expectation(description: "scheduled on main queue")
|
||||
main.assertForOverFulfill = true
|
||||
|
||||
var didExecuteMainAction = false
|
||||
let didExecuteMainAction = Atomic(false)
|
||||
let didExecuteBackgroundAction = Atomic(false)
|
||||
|
||||
mainScheduler.schedule {
|
||||
didExecuteMainAction = true
|
||||
didExecuteMainAction.set(true)
|
||||
main.fulfill()
|
||||
}
|
||||
|
||||
@@ -499,12 +499,14 @@ final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
didExecuteBackgroundAction.set(true)
|
||||
}
|
||||
|
||||
XCTAssertFalse(didExecuteMainAction, "action should be executed asynchronously")
|
||||
XCTAssertFalse(didExecuteMainAction.value,
|
||||
"action should be executed asynchronously")
|
||||
|
||||
// Wait for the background scheduler to execute the work.
|
||||
XCTAssertEqual(group.wait(timeout: .now() + 5.0), .success)
|
||||
|
||||
XCTAssertFalse(didExecuteMainAction, "action should be executed asynchronously")
|
||||
XCTAssertFalse(didExecuteMainAction.value,
|
||||
"action should be executed asynchronously")
|
||||
XCTAssertTrue(didExecuteBackgroundAction.value)
|
||||
|
||||
wait(for: [main], timeout: 0.1)
|
||||
@@ -570,7 +572,7 @@ private typealias Scheduler = DispatchQueue.OCombine
|
||||
private let mainScheduler = DispatchQueue.main.ocombine
|
||||
private let backgroundScheduler = DispatchQueue.global(qos: .background).ocombine
|
||||
|
||||
#endif
|
||||
#endif // OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
@@ -578,3 +580,5 @@ private typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
private struct KeyedWrapper<Value: Codable & Equatable>: Codable, Equatable {
|
||||
let value: Value
|
||||
}
|
||||
|
||||
#endif // !WASI
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
#if !WASI // TEST_DISCOVERY_CONDITION
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@@ -61,3 +63,5 @@ final class JSONDecoderTests: XCTestCase {
|
||||
cancellable.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
#endif // !WASI
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
#if !WASI // TEST_DISCOVERY_CONDITION
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@@ -65,3 +67,5 @@ final class JSONEncoderTests: XCTestCase {
|
||||
cancellable.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
#endif // !WASI
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
#if !WASI // TEST_DISCOVERY_CONDITION
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@@ -630,4 +632,6 @@ extension Notification.Name {
|
||||
self.init(rawValue: rawValue)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif // !canImport(Darwin) && swift(<5.1)
|
||||
|
||||
#endif // !WASI
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
// Created by Sergej Jaskiewicz on 14.06.2020.
|
||||
//
|
||||
|
||||
// OperationQueue has serious bugs in swift-corelibs-foundation prior to Swift 5.3.
|
||||
// (see https://github.com/apple/swift-corelibs-foundation/pull/2779)
|
||||
#if canImport(Darwin) || swift(>=5.3) && !WASI // TEST_DISCOVERY_CONDITION
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@@ -15,10 +19,6 @@ import OpenCombine
|
||||
import OpenCombineFoundation
|
||||
#endif
|
||||
|
||||
// OperationQueue has serious bugs in swift-corelibs-foundation prior to Swift 5.3.
|
||||
// (see https://github.com/apple/swift-corelibs-foundation/pull/2779)
|
||||
#if canImport(Darwin) || swift(>=5.3) // TEST_DISCOVERY_CONDITION
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class OperationQueueSchedulerTests: XCTestCase {
|
||||
|
||||
@@ -225,7 +225,7 @@ final class OperationQueueSchedulerTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testScheduleRepeatingWithRealQueue() {
|
||||
let mainQueue = OperationQueue.main
|
||||
let mainQueueScheduler = makeScheduler(OperationQueue.main)
|
||||
|
||||
let expectation10ticks = expectation(description: "10 ticks")
|
||||
expectation10ticks.expectedFulfillmentCount = 10
|
||||
@@ -234,13 +234,12 @@ final class OperationQueueSchedulerTests: XCTestCase {
|
||||
|
||||
let ticks = Atomic([TimeInterval]())
|
||||
|
||||
let desiredDelay: TimeInterval = 0.7
|
||||
let desiredInterval: TimeInterval = 0.3
|
||||
let desiredDelay: TimeInterval = 0.8
|
||||
let desiredInterval: TimeInterval = 0.5
|
||||
|
||||
let cancellable = executeOnBackgroundThread { () -> Cancellable in
|
||||
let scheduler = makeScheduler(mainQueue)
|
||||
return scheduler
|
||||
.schedule(after: scheduler.now.advanced(by: .init(desiredDelay)),
|
||||
let cancellable = executeOnBackgroundThread {
|
||||
mainQueueScheduler
|
||||
.schedule(after: mainQueueScheduler.now.advanced(by: .init(desiredDelay)),
|
||||
interval: .init(desiredInterval)) {
|
||||
XCTAssertTrue(Thread.isMainThread)
|
||||
ticks.append(Date().timeIntervalSinceReferenceDate)
|
||||
@@ -256,7 +255,7 @@ final class OperationQueueSchedulerTests: XCTestCase {
|
||||
RunLoop.main.run(until: Date() + 0.001)
|
||||
XCTAssertEqual(ticks.count, 0)
|
||||
|
||||
wait(for: [expectation10ticks], timeout: 5)
|
||||
wait(for: [expectation10ticks], timeout: 10)
|
||||
|
||||
if ticks.isEmpty {
|
||||
XCTFail("The scheduler doesn't work")
|
||||
@@ -332,6 +331,7 @@ extension OperationQueueScheduler.SchedulerTimeType.Stride
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension OperationQueueScheduler.SchedulerTimeType: DateBackedSchedulerTimeType {}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension OperationQueueScheduler: RunLoopLikeScheduler {}
|
||||
|
||||
private final class TestOperationQueue: OperationQueue {
|
||||
@@ -471,4 +471,4 @@ private final class TestOperationQueue: OperationQueue {
|
||||
#endif // canImport(Darwin)
|
||||
}
|
||||
|
||||
#endif // canImport(Darwin) || swift(>=5.3)
|
||||
#endif // canImport(Darwin) || swift(>=5.3) && !WASI
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
// PropertyListEncoder and PropertyListDecoder are unavailable in
|
||||
// swift-corelibs-foundation prior to Swift 5.1.
|
||||
#if canImport(Darwin) || swift(>=5.1) && !WASI // TEST_DISCOVERY_CONDITION
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@@ -15,10 +19,6 @@ import OpenCombine
|
||||
import OpenCombineFoundation
|
||||
#endif
|
||||
|
||||
// PropertyListEncoder and PropertyListDecoder are unavailable in
|
||||
// swift-corelibs-foundation prior to Swift 5.1.
|
||||
#if canImport(Darwin) || swift(>=5.1) // TEST_DISCOVERY_CONDITION
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class PropertyListDecoderTests: XCTestCase {
|
||||
func testSuccessfullyDecode() {
|
||||
@@ -79,4 +79,4 @@ final class PropertyListDecoderTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
#endif // canImport(Darwin) || swift(>=5.1)
|
||||
#endif // canImport(Darwin) || swift(>=5.1) && !WASI
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
// PropertyListEncoder and PropertyListDecoder are unavailable in
|
||||
// swift-corelibs-foundation prior to Swift 5.1.
|
||||
#if canImport(Darwin) || swift(>=5.1) && !WASI // TEST_DISCOVERY_CONDITION
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@@ -15,10 +19,6 @@ import OpenCombine
|
||||
import OpenCombineFoundation
|
||||
#endif
|
||||
|
||||
// PropertyListEncoder and PropertyListDecoder are unavailable in
|
||||
// swift-corelibs-foundation prior to Swift 5.1.
|
||||
#if canImport(Darwin) || swift(>=5.1) // TEST_DISCOVERY_CONDITION
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class PropertyListEncoderTests: XCTestCase {
|
||||
|
||||
@@ -84,4 +84,4 @@ final class PropertyListEncoderTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
#endif // canImport(Darwin) || swift(>=5.1)
|
||||
#endif // canImport(Darwin) || swift(>=5.1) && !WASI
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Sergej Jaskiewicz on 14.12.2019.
|
||||
//
|
||||
|
||||
#if !WASI // TEST_DISCOVERY_CONDITION
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@@ -597,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,
|
||||
@@ -615,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 {
|
||||
@@ -626,4 +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,6 +5,8 @@
|
||||
// Created by Sergej Jaskiewicz on 23.06.2020.
|
||||
//
|
||||
|
||||
#if !WASI // TEST_DISCOVERY_CONDITION
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@@ -214,3 +216,5 @@ private typealias TimerPublisher = Timer.TimerPublisher
|
||||
#else
|
||||
private typealias TimerPublisher = Timer.OCombine.TimerPublisher
|
||||
#endif
|
||||
|
||||
#endif // !WASI
|
||||
|
||||
@@ -18,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
|
||||
@@ -51,13 +51,14 @@ final class URLSessionTests: XCTestCase {
|
||||
private let unknownError = URLError(.unknown)
|
||||
|
||||
func testDataTaskPublisherFromURL() {
|
||||
let publisher = makePublisher(TestURLSession(testDataTask: .init()), testURL)
|
||||
let publisher = makePublisher(TestURLSession.withTestDataTask(.create()), testURL)
|
||||
let expectedRequest = URLRequest(url: testURL)
|
||||
XCTAssertEqual(publisher.request, expectedRequest)
|
||||
}
|
||||
|
||||
func testDataTaskPublisherFromRequest() {
|
||||
let publisher = makePublisher(TestURLSession(testDataTask: .init()), testRequest)
|
||||
let publisher = makePublisher(TestURLSession.withTestDataTask(.create()),
|
||||
testRequest)
|
||||
XCTAssertEqual(publisher.request, testRequest)
|
||||
}
|
||||
|
||||
@@ -124,8 +125,8 @@ final class URLSessionTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testRequesting() throws {
|
||||
let dataTask = TestURLSessionDataTask()
|
||||
let session = TestURLSession(testDataTask: dataTask)
|
||||
let dataTask = TestURLSessionDataTask.create()
|
||||
let session = TestURLSession.withTestDataTask(dataTask)
|
||||
let publisher = makePublisher(session, testRequest)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
@@ -164,8 +165,8 @@ final class URLSessionTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let dataTask = TestURLSessionDataTask()
|
||||
let session = TestURLSession(testDataTask: dataTask)
|
||||
let dataTask = TestURLSessionDataTask.create()
|
||||
let session = TestURLSession.withTestDataTask(dataTask)
|
||||
let publisher = makePublisher(session, testRequest)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
@@ -190,8 +191,8 @@ final class URLSessionTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testCrashesOnZeroDemand() throws {
|
||||
let dataTask = TestURLSessionDataTask()
|
||||
let session = TestURLSession(testDataTask: dataTask)
|
||||
let dataTask = TestURLSessionDataTask.create()
|
||||
let session = TestURLSession.withTestDataTask(dataTask)
|
||||
let publisher = makePublisher(session, testURL)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
@@ -205,8 +206,8 @@ final class URLSessionTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testURLSessionSubscriptionReflection() throws {
|
||||
let dataTask = TestURLSessionDataTask()
|
||||
let session = TestURLSession(testDataTask: dataTask)
|
||||
let dataTask = TestURLSessionDataTask.create()
|
||||
let session = TestURLSession.withTestDataTask(dataTask)
|
||||
let publisher = makePublisher(session, testURL)
|
||||
try testSubscriptionReflection(
|
||||
description: "DataTaskPublisher",
|
||||
@@ -227,8 +228,8 @@ final class URLSessionTests: XCTestCase {
|
||||
_ response: URLResponse?,
|
||||
_ error: Error?,
|
||||
expected: [TrackingSubscriber.Event]) {
|
||||
let dataTask = TestURLSessionDataTask()
|
||||
let session = TestURLSession(testDataTask: dataTask)
|
||||
let dataTask = TestURLSessionDataTask.create()
|
||||
let session = TestURLSession.withTestDataTask(dataTask)
|
||||
let publisher = makePublisher(session, testRequest)
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.max(1)) })
|
||||
publisher.subscribe(tracking)
|
||||
@@ -290,16 +291,25 @@ private class TestURLSession: URLSession {
|
||||
|
||||
private(set) var history = [Event]()
|
||||
|
||||
private(set) var dataTaskCompletionHandlers: [(Data?, URLResponse?, Error?) -> Void]
|
||||
private(set) var dataTaskCompletionHandlers =
|
||||
[(Data?, URLResponse?, Error?) -> Void]()
|
||||
|
||||
private let testDataTask: TestURLSessionDataTask
|
||||
private var testDataTask: TestURLSessionDataTask?
|
||||
|
||||
init(testDataTask: TestURLSessionDataTask) {
|
||||
self.testDataTask = testDataTask
|
||||
self.dataTaskCompletionHandlers = []
|
||||
#if !canImport(Darwin)
|
||||
super.init(configuration: .default)
|
||||
static func withTestDataTask(
|
||||
_ testDataTask: TestURLSessionDataTask
|
||||
) -> TestURLSession {
|
||||
// This dance is to avoid the deprecation warning for the URLSession
|
||||
// default initializer. Believe me, I've tried to make it less ugly with
|
||||
// no success.
|
||||
#if canImport(Darwin)
|
||||
let sessionClass = TestURLSession.self as NSObject.Type
|
||||
let session = sessionClass.init() as! TestURLSession
|
||||
#else
|
||||
let session = TestURLSession(configuration: .default)
|
||||
#endif
|
||||
session.testDataTask = testDataTask
|
||||
return session
|
||||
}
|
||||
|
||||
// MARK: Testing
|
||||
@@ -377,12 +387,12 @@ private class TestURLSession: URLSession {
|
||||
|
||||
override func dataTask(with request: URLRequest) -> URLSessionDataTask {
|
||||
history.append(.dataTaskWithRequest(request))
|
||||
return testDataTask
|
||||
return testDataTask!
|
||||
}
|
||||
|
||||
override func dataTask(with url: URL) -> URLSessionDataTask {
|
||||
history.append(.dataTaskWithURL(url))
|
||||
return testDataTask
|
||||
return testDataTask!
|
||||
}
|
||||
|
||||
override func dataTask(
|
||||
@@ -391,7 +401,7 @@ private class TestURLSession: URLSession {
|
||||
) -> URLSessionDataTask {
|
||||
history.append(.dataTaskWithURLAndCompletion(url))
|
||||
dataTaskCompletionHandlers.append(completionHandler)
|
||||
return testDataTask
|
||||
return testDataTask!
|
||||
}
|
||||
|
||||
override func dataTask(
|
||||
@@ -400,7 +410,7 @@ private class TestURLSession: URLSession {
|
||||
) -> URLSessionDataTask {
|
||||
history.append(.dataTaskWithRequestAndCompletion(request))
|
||||
dataTaskCompletionHandlers.append(completionHandler)
|
||||
return testDataTask
|
||||
return testDataTask!
|
||||
}
|
||||
|
||||
override func uploadTask(with request: URLRequest,
|
||||
@@ -554,7 +564,18 @@ private final class TestURLSessionDataTask: URLSessionDataTask {
|
||||
|
||||
private(set) var history = [Event]()
|
||||
|
||||
override init() {}
|
||||
static func create() -> TestURLSessionDataTask {
|
||||
// This dance is to avoid the deprecation warning for the URLSessionDataTask
|
||||
// default initializer. Believe me, I've tried to make it less ugly with
|
||||
// no success.
|
||||
#if canImport(Darwin)
|
||||
let dataTaskClass = TestURLSessionDataTask.self as NSObject.Type
|
||||
let dataTask = dataTaskClass.init() as! TestURLSessionDataTask
|
||||
#else
|
||||
let dataTask = TestURLSessionDataTask()
|
||||
#endif
|
||||
return dataTask
|
||||
}
|
||||
|
||||
override var taskIdentifier: Int {
|
||||
history.append(.taskIdentifier)
|
||||
@@ -721,6 +742,6 @@ private func makePublisher(
|
||||
) -> URLSession.OCombine.DataTaskPublisher {
|
||||
return session.ocombine.dataTaskPublisher(for: request)
|
||||
}
|
||||
#endif
|
||||
#endif // OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
|
||||
|
||||
#endif // canImport(Darwin)
|
||||
#endif
|
||||
|
||||
@@ -29,7 +29,7 @@ extension XCTest {
|
||||
// Taken from swift-corelibs-foundation and slightly modified for OpenCombine
|
||||
@available(macOS 10.13, iOS 8.0, *)
|
||||
func assertCrashes(within block: () throws -> Void) rethrows {
|
||||
#if !Xcode && !os(iOS) && !os(watchOS) && !os(tvOS)
|
||||
#if !Xcode && !os(iOS) && !os(watchOS) && !os(tvOS) && !WASI
|
||||
let childProcessEnvVariable = "OPENCOMBINE_TEST_PERFORM_ASSERT_CRASHES_BLOCKS"
|
||||
let childProcessEnvVariableOnValue = "YES"
|
||||
|
||||
@@ -82,6 +82,6 @@ extension XCTest {
|
||||
printDiagnostics()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif // !Xcode && !os(iOS) && !os(watchOS) && !os(tvOS) && !WASI
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -98,9 +98,14 @@ extension XCTest {
|
||||
}
|
||||
}
|
||||
|
||||
enum CancelBeforeSubscriptionBehavior {
|
||||
case crash
|
||||
case history([CustomSubscription.Event])
|
||||
}
|
||||
|
||||
func testCancelBeforeSubscription<Value, Operator: Publisher>(
|
||||
inputType: Value.Type,
|
||||
shouldCrash: Bool,
|
||||
expected: CancelBeforeSubscriptionBehavior,
|
||||
_ makeOperator: (CustomConnectablePublisherBase<Value, Never>) -> Operator
|
||||
) {
|
||||
|
||||
@@ -109,17 +114,23 @@ extension XCTest {
|
||||
let tracking = TrackingSubscriberBase<Operator.Output, Operator.Failure>()
|
||||
operatorPublisher.subscribe(tracking)
|
||||
|
||||
guard let subscription = publisher.erasedSubscriber as? Subscription else {
|
||||
guard let downstreamSubscription = publisher.erasedSubscriber as? Subscription
|
||||
else {
|
||||
XCTFail("The subscriber must also be a subscription")
|
||||
return
|
||||
}
|
||||
|
||||
if shouldCrash {
|
||||
switch expected {
|
||||
case .crash:
|
||||
assertCrashes {
|
||||
subscription.cancel()
|
||||
downstreamSubscription.cancel()
|
||||
}
|
||||
} else {
|
||||
subscription.cancel()
|
||||
case let .history(history):
|
||||
downstreamSubscription.cancel()
|
||||
|
||||
let subscription = CustomSubscription()
|
||||
publisher.send(subscription: subscription)
|
||||
XCTAssertEqual(subscription.history, history)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,4 +214,19 @@ func unreachable() -> Never {
|
||||
fatalError("unreachable")
|
||||
}
|
||||
|
||||
func fromNever<T>(_ resultType: T.Type) -> (Never) -> T {
|
||||
// This is to avoid the 'Will never be executed' warning.
|
||||
//
|
||||
// The first variant doesn't produce warnings in Swift 5.1, but doesn't compile
|
||||
// with early Swift versions.
|
||||
//
|
||||
// The second variant compiles with all Swift versions,
|
||||
// but produces a warning in Swift 5.1.
|
||||
#if swift(>=5.1)
|
||||
return { (_: Never) -> T in }
|
||||
#else
|
||||
return { switch $0 {} }
|
||||
#endif
|
||||
}
|
||||
|
||||
// swiftlint:enable generic_type_name
|
||||
|
||||
@@ -33,8 +33,28 @@ final class CustomSubscription: Subscription, CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
private struct State {
|
||||
var cancelled: Bool
|
||||
var history: [Event]
|
||||
}
|
||||
|
||||
private let state = Atomic(State(cancelled: false, history: []))
|
||||
|
||||
/// The history of requests and cancellations of this subscription.
|
||||
private(set) var history: [Event] = []
|
||||
var history: [Event] {
|
||||
return state.value.history
|
||||
}
|
||||
|
||||
var cancelled: Bool {
|
||||
get {
|
||||
return state.value.cancelled
|
||||
}
|
||||
set {
|
||||
state.do { state in
|
||||
state.cancelled = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var onRequest: ((Subscribers.Demand) -> Void)?
|
||||
var onCancel: (() -> Void)?
|
||||
@@ -63,16 +83,18 @@ final class CustomSubscription: Subscription, CustomStringConvertible {
|
||||
}.last
|
||||
}
|
||||
|
||||
var cancelled = false
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
history.append(.requested(demand))
|
||||
state.do { state in
|
||||
state.history.append(.requested(demand))
|
||||
}
|
||||
onRequest?(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
history.append(.cancelled)
|
||||
cancelled = true
|
||||
state.do { state in
|
||||
state.history.append(.cancelled)
|
||||
state.cancelled = true
|
||||
}
|
||||
onCancel?()
|
||||
}
|
||||
|
||||
|
||||
@@ -5,42 +5,82 @@
|
||||
// Created by Sergej Jaskiewicz on 04.02.2020.
|
||||
//
|
||||
|
||||
#if !WASI
|
||||
|
||||
#if canImport(Darwin)
|
||||
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
|
||||
@@ -50,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
|
||||
@@ -72,12 +110,15 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // !WASI
|
||||
|
||||
@@ -5,20 +5,34 @@
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
#if !WASI
|
||||
import Dispatch
|
||||
#endif
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
func race(times: Int = 100, _ bodies: () -> Void...) {
|
||||
#if WASI
|
||||
for body in bodies {
|
||||
for _ in 0..<times {
|
||||
body()
|
||||
}
|
||||
}
|
||||
#else
|
||||
DispatchQueue.concurrentPerform(iterations: bodies.count) {
|
||||
for _ in 0..<times {
|
||||
bodies[$0]()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
final class Atomic<Value> {
|
||||
#if !WASI
|
||||
let lock = NSLock()
|
||||
#endif
|
||||
|
||||
private var _value: Value
|
||||
|
||||
init(_ initialValue: Value) {
|
||||
@@ -26,20 +40,29 @@ final class Atomic<Value> {
|
||||
}
|
||||
|
||||
var value: Value {
|
||||
#if !WASI
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
#endif
|
||||
|
||||
return _value
|
||||
}
|
||||
|
||||
func set(_ newValue: Value) {
|
||||
#if !WASI
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
#endif
|
||||
|
||||
_value = newValue
|
||||
}
|
||||
|
||||
func `do`(_ body: (inout Value) throws -> Void) rethrows {
|
||||
#if !WASI
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
#endif
|
||||
|
||||
try body(&_value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,6 +352,7 @@ enum StringSubscription: Subscription,
|
||||
ExpressibleByStringLiteral {
|
||||
|
||||
case string(String)
|
||||
case contains(String)
|
||||
case subscription(Subscription)
|
||||
|
||||
init(_ subscription: Subscription) {
|
||||
@@ -364,7 +365,7 @@ enum StringSubscription: Subscription,
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .string(let string):
|
||||
case .string(let string), .contains(let string):
|
||||
return string
|
||||
case .subscription(let subscription):
|
||||
return String(describing: subscription)
|
||||
@@ -379,7 +380,7 @@ enum StringSubscription: Subscription,
|
||||
switch self {
|
||||
case .subscription(let subscription):
|
||||
return subscription.combineIdentifier
|
||||
case .string:
|
||||
case .string, .contains:
|
||||
fatalError("String has no combineIdentifier")
|
||||
}
|
||||
}
|
||||
@@ -390,7 +391,7 @@ enum StringSubscription: Subscription,
|
||||
|
||||
var underlying: Subscription? {
|
||||
switch self {
|
||||
case .string:
|
||||
case .string, .contains:
|
||||
return nil
|
||||
case .subscription(let underlying):
|
||||
return underlying
|
||||
@@ -401,6 +402,25 @@ enum StringSubscription: Subscription,
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension StringSubscription: Equatable {
|
||||
static func == (lhs: StringSubscription, rhs: StringSubscription) -> Bool {
|
||||
return lhs.description == rhs.description
|
||||
// swiftlint:disable pattern_matching_keywords
|
||||
switch (lhs, rhs) {
|
||||
case (.contains(let pattern), .subscription(let subscription)),
|
||||
(.subscription(let subscription), .contains(let pattern)):
|
||||
return String(describing: subscription).contains(pattern)
|
||||
case (.contains(let pattern), .string(let string)),
|
||||
(.string(let string), .contains(let pattern)):
|
||||
return string.contains(pattern)
|
||||
case let (.subscription(lhs), .subscription(rhs)):
|
||||
return String(describing: lhs) == String(describing: rhs)
|
||||
case (.string(let string), .subscription(let subscription)),
|
||||
(.subscription(let subscription), .string(let string)):
|
||||
return String(describing: subscription) == string
|
||||
case let (.string(lhs), .string(rhs)):
|
||||
return lhs == rhs
|
||||
case let (.contains(lhs), .contains(rhs)):
|
||||
return lhs.contains(rhs) || rhs.contains(lhs)
|
||||
}
|
||||
|
||||
// swiftlint:enable pattern_matching_keywords
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,9 +357,9 @@ final class VirtualTimeScheduler: Scheduler {
|
||||
_now = time
|
||||
}
|
||||
|
||||
func executeScheduledActions(until deadline: SchedulerTimeType = .nanoseconds(.max)) {
|
||||
precondition(deadline >= _now)
|
||||
while let (time, action) = workQueue.min(), time <= deadline {
|
||||
func executeScheduledActions(until deadline: SchedulerTimeType? = nil) {
|
||||
precondition(deadline.map { $0 >= _now } ?? true)
|
||||
while let (time, action) = workQueue.min(), deadline.map({ time <= $0 }) ?? true {
|
||||
workQueue.extractMin()
|
||||
_now = max(time, _now)
|
||||
action()
|
||||
|
||||
@@ -0,0 +1,424 @@
|
||||
//
|
||||
// ObservableObjectTests.swift
|
||||
//
|
||||
//
|
||||
// Created by kateinoigakukun on 2020/12/22.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if swift(>=5.1)
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private typealias Published = Combine.Published
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private typealias ObservableObject = Combine.ObservableObject
|
||||
#else
|
||||
import OpenCombine
|
||||
|
||||
private typealias Published = OpenCombine.Published
|
||||
|
||||
private typealias ObservableObject = OpenCombine.ObservableObject
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class ObservableObjectTests: XCTestCase {
|
||||
var disposeBag = [AnyCancellable]()
|
||||
|
||||
override func tearDown() {
|
||||
disposeBag = []
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testBasicBehavior() {
|
||||
let testObject = TestObject()
|
||||
var downstreamSubscription1: Subscription?
|
||||
let tracking1 = TrackingSubscriberBase<Void, Never>(
|
||||
receiveSubscription: { downstreamSubscription1 = $0 }
|
||||
)
|
||||
|
||||
testObject.objectWillChange.subscribe(tracking1)
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher")])
|
||||
downstreamSubscription1?.request(.max(2))
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher")])
|
||||
testObject.state1 += 1
|
||||
testObject.state1 += 2
|
||||
testObject.state1 += 3
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal,
|
||||
.signal,
|
||||
.signal])
|
||||
testObject.state2 += 1
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal])
|
||||
downstreamSubscription1?.request(.max(10))
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal])
|
||||
|
||||
let tracking2 = TrackingSubscriberBase<Void, Never>(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
testObject.objectWillChange.subscribe(tracking2)
|
||||
tracking2.assertHistoryEqual([.subscription("ObservableObjectPublisher")])
|
||||
|
||||
testObject.state1 = 42
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal])
|
||||
tracking2.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal])
|
||||
|
||||
downstreamSubscription1?.cancel()
|
||||
testObject.state1 = -1
|
||||
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal])
|
||||
tracking2.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.value(()),
|
||||
.value(())])
|
||||
}
|
||||
|
||||
// TODO: `objectWillChange` should return the same `ObservableObjectPublisher`
|
||||
// every time for Combine compatibility
|
||||
//
|
||||
// func testNoFields() {
|
||||
// let observableObject = NoFields()
|
||||
// let publisher1 = observableObject.objectWillChange
|
||||
// let publisher2 = observableObject.objectWillChange
|
||||
// XCTAssert(publisher1 === publisher2)
|
||||
// }
|
||||
|
||||
// func testNoPublishedFields() {
|
||||
// let observableObject = NoPublishedFields()
|
||||
// let publisher1 = observableObject.objectWillChange
|
||||
// let publisher2 = observableObject.objectWillChange
|
||||
// XCTAssert(publisher1 === publisher2)
|
||||
// }
|
||||
|
||||
func testPublishedFieldIsConstant() {
|
||||
let observableObject = PublishedFieldIsConstant()
|
||||
|
||||
let publisher1 = observableObject.objectWillChange
|
||||
let publisher2 = observableObject.objectWillChange
|
||||
|
||||
XCTAssert(publisher1 === publisher2,
|
||||
"""
|
||||
Even if the Published field is a constant, a publisher \
|
||||
should be installed there.
|
||||
""")
|
||||
}
|
||||
|
||||
func testDerivedClassWithPublishedField() {
|
||||
let observableObject = ObservedDerivedWithObservedBase()
|
||||
|
||||
var counter = 0
|
||||
|
||||
observableObject.objectWillChange.sink {
|
||||
counter += 1
|
||||
}.store(in: &disposeBag)
|
||||
|
||||
XCTAssertEqual(observableObject.publishedValue0, 0)
|
||||
XCTAssertEqual(observableObject.simpleValue, "what")
|
||||
XCTAssertEqual(observableObject.subclassPublished0, 0)
|
||||
XCTAssertEqual(observableObject.subclassPublished1, 1)
|
||||
XCTAssertEqual(observableObject.subclassPublished2, 2)
|
||||
|
||||
observableObject.publishedValue0 += 5
|
||||
|
||||
XCTAssertEqual(counter, 1)
|
||||
XCTAssertEqual(observableObject.publishedValue0, 5)
|
||||
|
||||
Published<String>[_enclosingInstance: observableObject,
|
||||
wrapped: \.simpleValue,
|
||||
storage: \.publishedValue1] += "???"
|
||||
|
||||
XCTAssertEqual(counter, 2)
|
||||
XCTAssertEqual(observableObject.simpleValue, "what")
|
||||
|
||||
observableObject.subclassPublished0 += 3
|
||||
|
||||
XCTAssertEqual(counter, 3)
|
||||
XCTAssertEqual(observableObject.subclassPublished0, 3)
|
||||
|
||||
observableObject.subclassPublished1 += 3
|
||||
|
||||
XCTAssertEqual(counter, 4)
|
||||
XCTAssertEqual(observableObject.subclassPublished1, 4)
|
||||
|
||||
observableObject.subclassPublished2 += 3
|
||||
|
||||
XCTAssertEqual(counter, 5)
|
||||
XCTAssertEqual(observableObject.subclassPublished1, 4)
|
||||
}
|
||||
|
||||
func testObjCClassSubclass() {
|
||||
let observableObject = ObjCClassSubclass()
|
||||
let publisher1 = observableObject.objectWillChange
|
||||
let publisher2 = observableObject.objectWillChange
|
||||
XCTAssert(publisher1 === publisher2)
|
||||
}
|
||||
|
||||
func testResilientClassSubclass() {
|
||||
let observableObject = ResilientClassSubclass()
|
||||
let publisher1 = observableObject.objectWillChange
|
||||
let publisher2 = observableObject.objectWillChange
|
||||
|
||||
XCTAssert(publisher1 === publisher2)
|
||||
}
|
||||
|
||||
func testResilientClassSubclass2() {
|
||||
let observableObject = ResilientClassSubclass2()
|
||||
let publisher1 = observableObject.objectWillChange
|
||||
let publisher2 = observableObject.objectWillChange
|
||||
|
||||
XCTAssert(publisher1 === publisher2)
|
||||
}
|
||||
|
||||
func testGenericClass() {
|
||||
let observableObject = GenericClass(123, true)
|
||||
|
||||
var counter = 0
|
||||
|
||||
observableObject.objectWillChange.sink { counter += 1 }.store(in: &disposeBag)
|
||||
XCTAssertEqual(counter, 0)
|
||||
XCTAssertEqual(observableObject.value1, 123)
|
||||
XCTAssertEqual(observableObject.value2, true)
|
||||
|
||||
observableObject.value1 += 1
|
||||
|
||||
XCTAssertEqual(counter, 1)
|
||||
XCTAssertEqual(observableObject.value1, 124)
|
||||
|
||||
observableObject.value2.toggle()
|
||||
|
||||
XCTAssertEqual(counter, 2)
|
||||
XCTAssertEqual(observableObject.value2, false)
|
||||
}
|
||||
|
||||
func testGenericSubclassOfResilientClass() {
|
||||
let observableObject = ResilientClassGenericSubclass("hello", true)
|
||||
|
||||
var counter = 0
|
||||
|
||||
observableObject.objectWillChange.sink { counter += 1 }.store(in: &disposeBag)
|
||||
XCTAssertEqual(counter, 0)
|
||||
XCTAssertEqual(observableObject.value1, "hello")
|
||||
XCTAssertEqual(observableObject.value2, true)
|
||||
|
||||
observableObject.value1 += "!"
|
||||
|
||||
XCTAssertEqual(counter, 1)
|
||||
XCTAssertEqual(observableObject.value1, "hello!")
|
||||
|
||||
observableObject.value2.toggle()
|
||||
|
||||
XCTAssertEqual(counter, 2)
|
||||
XCTAssertEqual(observableObject.value2, false)
|
||||
}
|
||||
|
||||
func testGenericSubclassOfResilientClass2() {
|
||||
let observableObject = ResilientClassGenericSubclass2("hello", true)
|
||||
|
||||
var counter = 0
|
||||
|
||||
observableObject.objectWillChange.sink { counter += 1 }.store(in: &disposeBag)
|
||||
XCTAssertEqual(counter, 0)
|
||||
XCTAssertEqual(observableObject.value1, "hello")
|
||||
XCTAssertEqual(observableObject.value2, true)
|
||||
|
||||
observableObject.value1 += "!"
|
||||
|
||||
XCTAssertEqual(counter, 1)
|
||||
XCTAssertEqual(observableObject.value1, "hello!")
|
||||
|
||||
observableObject.value2.toggle()
|
||||
|
||||
XCTAssertEqual(counter, 2)
|
||||
XCTAssertEqual(observableObject.value2, false)
|
||||
|
||||
observableObject.value3.toggle()
|
||||
|
||||
XCTAssertEqual(counter, 3)
|
||||
XCTAssertEqual(observableObject.value3, true)
|
||||
}
|
||||
|
||||
func testObservableDerivedWithNonObservableBase() {
|
||||
let observableObject = ObservedDerivedWithNonObservedBase()
|
||||
var counter = 0
|
||||
observableObject.objectWillChange.sink { counter += 1 }.store(in: &disposeBag)
|
||||
|
||||
XCTAssertEqual(counter, 0)
|
||||
XCTAssertEqual(observableObject.nonObservedBaseValue0, 10)
|
||||
XCTAssertEqual(observableObject.nonObservedBaseValue1, .pi)
|
||||
XCTAssertEqual(observableObject.observedDerivedValue2,
|
||||
"Asuka is obviously the best girl.")
|
||||
XCTAssertEqual(observableObject.observedDerivedValue3, 255)
|
||||
|
||||
observableObject.nonObservedBaseValue0 -= 1
|
||||
XCTAssertEqual(counter, 1)
|
||||
XCTAssertEqual(observableObject.nonObservedBaseValue0, 9)
|
||||
|
||||
observableObject.nonObservedBaseValue1 *= 2
|
||||
XCTAssertEqual(counter, 2)
|
||||
XCTAssertEqual(observableObject.nonObservedBaseValue1, 2 * .pi)
|
||||
|
||||
observableObject.observedDerivedValue2 = "Nevermind."
|
||||
XCTAssertEqual(counter, 3)
|
||||
XCTAssertEqual(observableObject.observedDerivedValue2, "Nevermind.")
|
||||
|
||||
observableObject.observedDerivedValue3 &+= 1
|
||||
XCTAssertEqual(counter, 4)
|
||||
XCTAssertEqual(observableObject.observedDerivedValue3, 0)
|
||||
}
|
||||
|
||||
func testNSObjectSubclass() {
|
||||
let observableObject = NSObjectSubclass()
|
||||
var counter = 0
|
||||
observableObject.objectWillChange.sink { counter += 1 }.store(in: &disposeBag)
|
||||
|
||||
XCTAssertEqual(counter, 0)
|
||||
XCTAssertEqual(observableObject.value0, 0)
|
||||
XCTAssertEqual(observableObject.value1, 42)
|
||||
|
||||
observableObject.value0 += 1
|
||||
XCTAssertEqual(counter, 1)
|
||||
XCTAssertEqual(observableObject.value0, 1)
|
||||
|
||||
observableObject.value1 += 1
|
||||
XCTAssertEqual(counter, 2)
|
||||
XCTAssertEqual(observableObject.value1, 43)
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private final class NoFields: ObservableObject {}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private final class NoPublishedFields: ObservableObject {
|
||||
var field = NoFields()
|
||||
var int = 0
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private final class PublishedFieldIsConstant: ObservableObject {
|
||||
let publishedValue = Published(initialValue: 42)
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private class ObservedBase: ObservableObject {
|
||||
@Published var publishedValue0 = 0
|
||||
var publishedValue1 = Published(initialValue: "Hello!")
|
||||
let publishedValue2 = Published(initialValue: 42)
|
||||
var simpleValue = "what"
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private final class ObservedDerivedWithObservedBase: ObservedBase {
|
||||
@Published var subclassPublished0 = 0
|
||||
@Published var subclassPublished1 = 1
|
||||
@Published var subclassPublished2 = 2
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension NSNumber: ObservableObject {}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private final class ObjCClassSubclass: NSObject, ObservableObject {
|
||||
@Published var published = 10
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private class ResilientClassSubclass: JSONDecoder, ObservableObject {
|
||||
@Published var published0 = 10
|
||||
@Published var published1 = "hello!"
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private final class ResilientClassSubclass2: ResilientClassSubclass {
|
||||
@Published var published3 = true
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension JSONEncoder: ObservableObject {}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private final class GenericClass<Value1, Value2>: ObservableObject {
|
||||
@Published var value1: Value1
|
||||
@Published var value2: Value2
|
||||
|
||||
init(_ value1: Value1, _ value2: Value2) {
|
||||
self.value1 = value1
|
||||
self.value2 = value2
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private class NonObservedBase {
|
||||
@Published var nonObservedBaseValue0 = 10
|
||||
@Published var nonObservedBaseValue1 = Double.pi
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private class ObservedDerivedWithNonObservedBase: NonObservedBase, ObservableObject {
|
||||
@Published var observedDerivedValue2 = "Asuka is obviously the best girl."
|
||||
@Published var observedDerivedValue3: UInt8 = 255
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private class NSObjectSubclass: NSObject, ObservableObject {
|
||||
@Published var value0 = 0
|
||||
@Published var value1: UInt8 = 42
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private class ResilientClassGenericSubclass<Value1, Value2>
|
||||
: JSONDecoder,
|
||||
ObservableObject
|
||||
{
|
||||
@Published var value1: Value1
|
||||
@Published var value2: Value2
|
||||
|
||||
init(_ value1: Value1, _ value2: Value2) {
|
||||
self.value1 = value1
|
||||
self.value2 = value2
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private final class ResilientClassGenericSubclass2<Value1, Value2>
|
||||
: ResilientClassGenericSubclass<Value1, Value2>
|
||||
{
|
||||
@Published var value3 = false
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private final class TestObject: ObservableObject {
|
||||
@Published var state1: Int
|
||||
@Published var state2: Int
|
||||
var nonPublished: Int
|
||||
|
||||
init(_ initialValue: Int = 0) {
|
||||
_state1 = Published(initialValue: initialValue)
|
||||
_state2 = Published(initialValue: initialValue)
|
||||
nonPublished = initialValue
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -110,6 +110,7 @@ final class PublishedTests: XCTestCase {
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher")])
|
||||
}
|
||||
|
||||
@available(macOS 11.0, iOS 14.0, *)
|
||||
func testAssignToPublished() throws {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisherBase<Int, Never>(subscription: subscription)
|
||||
@@ -148,6 +149,7 @@ final class PublishedTests: XCTestCase {
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
@available(macOS 11.0, iOS 14.0, *)
|
||||
func testAssignToPublishedFinish() {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisherBase<Int, Never>(subscription: subscription)
|
||||
@@ -289,6 +291,7 @@ final class PublishedTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
@available(macOS 11.0, iOS 14.0, *)
|
||||
func testProjectedValueSetter() {
|
||||
let testObject1 = TestObject(1)
|
||||
let testObject2 = TestObject(2)
|
||||
|
||||
@@ -41,14 +41,21 @@ final class AllSatisfyTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
func testAllSatisfyUpstreamFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(
|
||||
func testAllSatisfyUpstreamFinishesImmediatelyWithDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
|
||||
expectedSubscription: "AllSatisfy",
|
||||
expectedResult: true,
|
||||
{ $0.allSatisfy(shouldNotBeCalled()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testAllSatisfyUpstreamFinishesImmediatelyWithoutDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
|
||||
expectedSubscription: "AllSatisfy",
|
||||
{ $0.allSatisfy(shouldNotBeCalled()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testAllSatisfyCancelAlreadyCancelled() throws {
|
||||
try ReduceTests.testCancelAlreadyCancelled {
|
||||
$0.allSatisfy(shouldNotBeCalled())
|
||||
@@ -97,13 +104,14 @@ final class AllSatisfyTests: XCTestCase {
|
||||
|
||||
func testAllSatisfyCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.allSatisfy(shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
func testAllSatisfyLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: false,
|
||||
{ $0.allSatisfy { _ in true } })
|
||||
}
|
||||
|
||||
@@ -155,14 +163,21 @@ final class AllSatisfyTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
func testTryAllSatisfyUpstreamFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(
|
||||
func testTryAllSatisfyUpstreamFinishesImmediatelyWithDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
|
||||
expectedSubscription: "TryAllSatisfy",
|
||||
expectedResult: true,
|
||||
{ $0.tryAllSatisfy(shouldNotBeCalled()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryAllSatisfyUpstreamFinishesImmediatelyWithoutDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
|
||||
expectedSubscription: "TryAllSatisfy",
|
||||
{ $0.tryAllSatisfy(shouldNotBeCalled()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryAllSatisfyCancelAlreadyCancelled() throws {
|
||||
try ReduceTests.testCancelAlreadyCancelled {
|
||||
$0.tryAllSatisfy(shouldNotBeCalled())
|
||||
@@ -217,13 +232,14 @@ final class AllSatisfyTests: XCTestCase {
|
||||
|
||||
func testTryAllSatisfyCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.tryAllSatisfy(shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
func testTryAllSatisfyLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: false,
|
||||
{ $0.tryAllSatisfy { _ in true } })
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Sergej Jaskiewicz on 03.12.2019.
|
||||
//
|
||||
|
||||
#if !WASI
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
@@ -44,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() {
|
||||
@@ -75,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() {
|
||||
@@ -105,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 {
|
||||
@@ -137,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 {
|
||||
@@ -170,3 +180,5 @@ final class BreakpointTests: XCTestCase {
|
||||
{ $0.breakpointOnError() })
|
||||
}
|
||||
}
|
||||
|
||||
#endif // !WASI
|
||||
|
||||
@@ -918,10 +918,23 @@ final class BufferTests: XCTestCase {
|
||||
prefetch: Publishers.PrefetchStrategy,
|
||||
whenFull: Publishers.BufferingStrategy<Never>
|
||||
) {
|
||||
testCancelBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
{ $0.buffer(size: 2, prefetch: prefetch, whenFull: whenFull) }
|
||||
)
|
||||
switch prefetch {
|
||||
case .byRequest:
|
||||
testCancelBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.buffer(size: 2, prefetch: prefetch, whenFull: whenFull) }
|
||||
)
|
||||
case .keepFull:
|
||||
testCancelBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([.requested(.max(2))]),
|
||||
{ $0.buffer(size: 2, prefetch: prefetch, whenFull: whenFull) }
|
||||
)
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
@unknown default:
|
||||
unreachable()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ final class CatchTests: XCTestCase {
|
||||
|
||||
func testCatchReceiveValueBeforeSubscription() {
|
||||
testReceiveValueBeforeSubscription(value: 1, expected: .crash) {
|
||||
$0.catch { _ in Just(13) }
|
||||
$0.catch(fromNever(Just<Int>.self))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ final class CatchTests: XCTestCase {
|
||||
testReceiveCompletionBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([]),
|
||||
{ $0.catch { _ in Just(13) } }
|
||||
{ $0.catch(fromNever(Just<Int>.self)) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -220,7 +220,7 @@ final class CatchTests: XCTestCase {
|
||||
|
||||
func testTryCatchReceiveValueBeforeSubscription() {
|
||||
testReceiveValueBeforeSubscription(value: 1, expected: .crash) {
|
||||
$0.tryCatch { _ in Just(13) }
|
||||
$0.tryCatch(fromNever(Just<Int>.self))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,7 +228,7 @@ final class CatchTests: XCTestCase {
|
||||
testReceiveCompletionBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([]),
|
||||
{ $0.tryCatch { _ in Just(13) } }
|
||||
{ $0.tryCatch(fromNever(Just<Int>.self)) }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -197,7 +197,7 @@ final class CollectByCountTests: XCTestCase {
|
||||
|
||||
func testCollectByCountCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([]),
|
||||
{ $0.collect(19) })
|
||||
}
|
||||
|
||||
|
||||
@@ -27,10 +27,19 @@ final class CollectTests: XCTestCase {
|
||||
{ $0.collect() })
|
||||
}
|
||||
|
||||
func testtestUpstreamFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Collect",
|
||||
expectedResult: [Int](),
|
||||
{ $0.collect() })
|
||||
func testUpstreamFinishesImmediatelyWithDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
|
||||
expectedSubscription: "Collect",
|
||||
expectedResult: [Int](),
|
||||
{ $0.collect() }
|
||||
)
|
||||
}
|
||||
|
||||
func testUpstreamFinishesImmediatelyWithoutDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
|
||||
expectedSubscription: "Collect",
|
||||
{ $0.collect() }
|
||||
)
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
@@ -70,13 +79,14 @@ final class CollectTests: XCTestCase {
|
||||
|
||||
func testCollectCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.collect() })
|
||||
}
|
||||
|
||||
func testCollectLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: false,
|
||||
{ $0.collect() })
|
||||
}
|
||||
|
||||
|
||||
@@ -336,7 +336,7 @@ final class CompactMapTests: XCTestCase {
|
||||
|
||||
func testTryCompactMapCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.cancelled]),
|
||||
{ $0.tryCompactMap(shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user