Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1496bab272 | |||
| 1ebbdb8ea9 | |||
| f861335dc3 | |||
| 769c3c818f | |||
| 910d21da4c | |||
| 6e20956d6d | |||
| e453879d75 | |||
| 98f6b6b337 | |||
| 74b739d74e | |||
| bcba9a19d4 | |||
| 486e166462 | |||
| c6536cf8d3 | |||
| cf41c25cf7 | |||
| b4557fb311 | |||
| f8e6e66ab4 | |||
| 95b42abce3 | |||
| 899a04bb3f | |||
| 5f9a700689 | |||
| a300fd09d3 | |||
| 5973f86c6e | |||
| 1b5afdba26 | |||
| 51d5d1e71d | |||
| a8bc5cc046 | |||
| 86d6170dc9 | |||
| 171131d768 | |||
| d6b4fb4115 | |||
| 014b82b99d | |||
| 7c5a76cf2b | |||
| 668c292245 | |||
| 981ab4fa09 | |||
| 8cf71e0122 | |||
| 130125cb66 | |||
| 7b3ceae666 | |||
| 56202b1663 | |||
| d0a02de7c5 | |||
| c8058edc5f | |||
| 8b685f78a3 | |||
| 0816abe33c | |||
| b6c7560f4c | |||
| 7a9e8b22d2 | |||
| 26f9acd75a | |||
| 4c42b434ca | |||
| 8afe945325 | |||
| eb879213ef | |||
| e402fb3980 | |||
| 4f96378c02 | |||
| 243f3d200e | |||
| f1fb5552b5 | |||
| bfd875ccba | |||
| 27da28f378 | |||
| cba3a69e74 | |||
| d634a76c39 | |||
| b5e31a43ef | |||
| 1bb3583a36 | |||
| 449b8eef48 | |||
| d4bdd83a00 | |||
| f1cc94adff | |||
| 5e1e10a780 | |||
| 5b7358111c | |||
| 5f90c4c85f | |||
| 0dec30fcad | |||
| a5ec9723e2 | |||
| 25ac4dfa5f | |||
| f5645f605b | |||
| ffef3ac76c | |||
| 36a9f1999c | |||
| f069f9b9fa | |||
| 55bdbba0f9 | |||
| 1290545c49 | |||
| aed074af43 | |||
| 11ec7c89e6 |
@@ -0,0 +1,274 @@
|
||||
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"
|
||||
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
|
||||
|
||||
"Execute compatibility tests on iOS 13.3 (Xcode 11.3.0, Swift 5.1.3)":
|
||||
macos:
|
||||
xcode: "11.3.0"
|
||||
environment:
|
||||
SWIFT_VERSION: "5.1.3"
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Generating Xcode project
|
||||
command: make generate-compatibility-xcodeproj
|
||||
- run:
|
||||
name: Building for testing on iOS 13.3 with xcodebuild
|
||||
command: |
|
||||
set -o pipefail \
|
||||
&& xcodebuild build-for-testing \
|
||||
-scheme OpenCombine-Package \
|
||||
-destination "platform=iOS Simulator,name=iPhone 11,OS=13.3" \
|
||||
-derivedDataPath DerivedData \
|
||||
| tee xcodebuild_build-for-testing.log \
|
||||
| xcpretty
|
||||
- store_artifacts:
|
||||
path: xcodebuild_build-for-testing.log
|
||||
- run:
|
||||
name: Testing against Combine on iOS 13.3 with xcodebuild
|
||||
command: |
|
||||
set -o pipefail \
|
||||
&& xcodebuild test-without-building \
|
||||
-scheme OpenCombine-Package \
|
||||
-destination "platform=iOS Simulator,name=iPhone 11,OS=13.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
|
||||
|
||||
"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.1.1)":
|
||||
docker:
|
||||
- image: swift:5.1.1-bionic
|
||||
environment:
|
||||
SWIFT_VERSION: "5.1.1"
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Installing dependencies
|
||||
command: |
|
||||
apt update -y
|
||||
apt upgrade -y
|
||||
apt install -y curl
|
||||
- run:
|
||||
name: Building and running tests in debug mode with coverage
|
||||
command: | # We need to run the test command twice because of https://bugs.swift.org/browse/SR-10783
|
||||
make test-debug \
|
||||
SWIFT_TEST_FLAGS="--enable-test-discovery \
|
||||
--enable-index-store \
|
||||
--enable-code-coverage \
|
||||
--build-path .build-test-debug" \
|
||||
> /dev/null 2>&1 \
|
||||
|| true
|
||||
make test-debug \
|
||||
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
|
||||
SWIFT_TEST_FLAGS="--enable-test-discovery \
|
||||
--enable-index-store \
|
||||
--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
|
||||
- run:
|
||||
name: Building and running tests in debug mode with TSan
|
||||
command: | # We need to run the test command twice because of https://bugs.swift.org/browse/SR-10783
|
||||
make test-debug-sanitize-thread \
|
||||
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
|
||||
SWIFT_TEST_FLAGS="--enable-test-discovery \
|
||||
--enable-index-store \
|
||||
--build-path .build-test-debug-sanitize-thread" \
|
||||
> /dev/null 2>&1 \
|
||||
|| true
|
||||
make test-debug-sanitize-thread \
|
||||
SWIFT_TEST_FLAGS="--enable-test-discovery \
|
||||
--enable-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="--enable-test-discovery \
|
||||
--enable-index-store \
|
||||
--build-path .build-test-release"
|
||||
- run:
|
||||
name: Uploading code coverage
|
||||
command: |
|
||||
bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
"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)"
|
||||
"OpenCombine: execute compatibility tests":
|
||||
jobs:
|
||||
- "Execute compatibility tests on iOS 13.3 (Xcode 11.3.0, Swift 5.1.3)"
|
||||
"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.1.1)"
|
||||
"OpenCombine: run SwiftLint and Danger":
|
||||
jobs:
|
||||
- "Run SwiftLint and Danger"
|
||||
"OpenCombine: validate podspec files":
|
||||
jobs:
|
||||
- "Run Pod spec lint"
|
||||
@@ -153,3 +153,6 @@ dmypy.json
|
||||
.pyre/
|
||||
|
||||
# End of https://www.gitignore.io/api/Python
|
||||
|
||||
.bundle/
|
||||
node_modules/
|
||||
|
||||
+2
-1
@@ -16,6 +16,8 @@ disabled_rules:
|
||||
- identifier_name
|
||||
- nesting
|
||||
- notification_center_detachment
|
||||
- no_fallthrough_only
|
||||
- no_space_in_method_call
|
||||
- redundant_string_enum_value
|
||||
- todo
|
||||
- trailing_comma
|
||||
@@ -59,7 +61,6 @@ opt_in_rules:
|
||||
- unneeded_parentheses_in_closure_argument
|
||||
- untyped_error_in_catch
|
||||
- unused_import
|
||||
- unused_private_declaration
|
||||
- vertical_parameter_alignment_on_call
|
||||
- vertical_whitespace_closing_braces
|
||||
- yoda_condition
|
||||
|
||||
-90
@@ -1,90 +0,0 @@
|
||||
language: generic
|
||||
|
||||
addons:
|
||||
homebrew:
|
||||
taps:
|
||||
- danger/tap
|
||||
packages:
|
||||
- swiftlint
|
||||
- danger-swift
|
||||
update: true
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- .build
|
||||
- ~/.danger-swift
|
||||
- ~/.swiftenv
|
||||
- ~/Library/Caches/Homebrew
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- name: "Ubuntu 16.04 | Swift 5.1.1 | Tests"
|
||||
os: linux
|
||||
dist: xenial
|
||||
sudo: required
|
||||
env: SWIFT_VERSION="5.1.1" OPENCOMBINE_TEST="YES"
|
||||
- name: "macOS 10.14 | Swift 5.0 | Tests"
|
||||
os: osx
|
||||
osx_image: xcode10.2
|
||||
env: SWIFT_VERSION="5.0" CODE_COVERAGE="YES" OPENCOMBINE_TEST="YES"
|
||||
- name: "iOS 13.1 | Swift 5.1.1 | Compatibility Tests"
|
||||
os: osx
|
||||
osx_image: xcode11.1
|
||||
env: SWIFT_VERSION="5.1.1" OPENCOMBINE_COMPATIBILITY_TEST="YES"
|
||||
- name: "macOS 10.14 | Swift 5.0 | Code Quality"
|
||||
os: osx
|
||||
osx_image: xcode10.2
|
||||
env: SWIFT_VERSION="5.0" RUN_DANGER="YES" SWIFT_LINT="YES"
|
||||
|
||||
before_cache:
|
||||
- brew cleanup
|
||||
|
||||
before_install:
|
||||
- if [[ $TRAVIS_OS_NAME == "linux" ]]; then
|
||||
cat /proc/cpuinfo;
|
||||
fi
|
||||
install:
|
||||
- if [[ $TRAVIS_OS_NAME == "linux" ]]; then
|
||||
eval "$(curl -sL https://swiftenv.fuller.li/install.sh)";
|
||||
fi
|
||||
- if [[ $CODE_COVERAGE == "YES" ]]; then
|
||||
gem install xcpretty;
|
||||
fi
|
||||
script:
|
||||
- if [[ $OPENCOMBINE_TEST == "YES" ]]; then
|
||||
if [[ $TRAVIS_OS_NAME == "linux" ]]; then
|
||||
make SWIFT_TEST_FLAGS="--enable-test-discovery --enable-index-store" test-debug;
|
||||
else
|
||||
make test-debug;
|
||||
fi
|
||||
fi
|
||||
- if [[ $OPENCOMBINE_TEST == "YES" ]]; then
|
||||
if [[ $TRAVIS_OS_NAME == "linux" ]]; then
|
||||
make SWIFT_TEST_FLAGS="--enable-test-discovery --enable-index-store" test-debug-sanitize-thread;
|
||||
else
|
||||
make test-debug-sanitize-thread;
|
||||
fi
|
||||
fi
|
||||
- if [[ $OPENCOMBINE_TEST == "YES" ]]; then
|
||||
if [[ $TRAVIS_OS_NAME == "linux" ]]; then
|
||||
make SWIFT_TEST_FLAGS="--enable-test-discovery --enable-index-store" test-release;
|
||||
else
|
||||
make test-release;
|
||||
fi
|
||||
fi
|
||||
- if [[ $OPENCOMBINE_COMPATIBILITY_TEST == "YES" ]]; then
|
||||
make generate-compatibility-xcodeproj;
|
||||
set -o pipefail && xcodebuild -scheme OpenCombine-Package -sdk iphonesimulator13.1 -destination "platform=iOS Simulator,name=iPhone 11,OS=13.1" build test | xcpretty;
|
||||
fi
|
||||
- if [[ $SWIFT_LINT == "YES" ]]; then
|
||||
swiftlint lint --strict --reporter "emoji";
|
||||
fi
|
||||
- if [[ $RUN_DANGER == "YES" ]]; then
|
||||
danger-swift ci;
|
||||
fi
|
||||
after_success:
|
||||
- if [[ $CODE_COVERAGE == "YES" ]]; then
|
||||
make generate-xcodeproj;
|
||||
xcodebuild -scheme OpenCombine-Package build test | xcpretty;
|
||||
bash <(curl -s https://codecov.io/bash);
|
||||
fi
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.1)
|
||||
addressable (2.7.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
atomos (0.1.3)
|
||||
babosa (1.0.3)
|
||||
claide (1.0.3)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
commander-fastlane (4.4.6)
|
||||
highline (~> 1.7.2)
|
||||
declarative (0.0.10)
|
||||
declarative-option (0.1.0)
|
||||
digest-crc (0.4.1)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
dotenv (2.7.5)
|
||||
emoji_regex (1.0.1)
|
||||
excon (0.71.0)
|
||||
faraday (0.17.0)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
faraday-cookie_jar (0.0.6)
|
||||
faraday (>= 0.7.4)
|
||||
http-cookie (~> 1.0.0)
|
||||
faraday_middleware (0.13.1)
|
||||
faraday (>= 0.7.4, < 1.0)
|
||||
fastimage (2.1.7)
|
||||
fastlane (2.134.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.3, < 3.0.0)
|
||||
babosa (>= 1.0.2, < 2.0.0)
|
||||
bundler (>= 1.12.0, < 3.0.0)
|
||||
colored
|
||||
commander-fastlane (>= 4.4.6, < 5.0.0)
|
||||
dotenv (>= 2.1.1, < 3.0.0)
|
||||
emoji_regex (>= 0.1, < 2.0)
|
||||
excon (>= 0.45.0, < 1.0.0)
|
||||
faraday (~> 0.17)
|
||||
faraday-cookie_jar (~> 0.0.6)
|
||||
faraday_middleware (~> 0.13.1)
|
||||
fastimage (>= 2.1.0, < 3.0.0)
|
||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||
google-api-client (>= 0.21.2, < 0.24.0)
|
||||
google-cloud-storage (>= 1.15.0, < 2.0.0)
|
||||
highline (>= 1.7.2, < 2.0.0)
|
||||
json (< 3.0.0)
|
||||
jwt (~> 2.1.0)
|
||||
mini_magick (>= 4.9.4, < 5.0.0)
|
||||
multi_xml (~> 0.5)
|
||||
multipart-post (~> 2.0.0)
|
||||
plist (>= 3.1.0, < 4.0.0)
|
||||
public_suffix (~> 2.0.0)
|
||||
rubyzip (>= 1.3.0, < 2.0.0)
|
||||
security (= 0.1.3)
|
||||
simctl (~> 1.6.3)
|
||||
slack-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-table (>= 1.4.5, < 2.0.0)
|
||||
tty-screen (>= 0.6.3, < 1.0.0)
|
||||
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||
word_wrap (~> 1.0.0)
|
||||
xcodeproj (>= 1.8.1, < 2.0.0)
|
||||
xcpretty (~> 0.3.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3)
|
||||
gh_inspector (1.1.3)
|
||||
google-api-client (0.23.9)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.5, < 0.7.0)
|
||||
httpclient (>= 2.8.1, < 3.0)
|
||||
mime-types (~> 3.0)
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.0)
|
||||
signet (~> 0.9)
|
||||
google-cloud-core (1.3.2)
|
||||
google-cloud-env (~> 1.0)
|
||||
google-cloud-env (1.2.1)
|
||||
faraday (~> 0.11)
|
||||
google-cloud-storage (1.16.0)
|
||||
digest-crc (~> 0.4)
|
||||
google-api-client (~> 0.23)
|
||||
google-cloud-core (~> 1.2)
|
||||
googleauth (>= 0.6.2, < 0.10.0)
|
||||
googleauth (0.6.7)
|
||||
faraday (~> 0.12)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (~> 0.7)
|
||||
highline (1.7.10)
|
||||
http-cookie (1.0.3)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
json (2.2.0)
|
||||
jwt (2.1.0)
|
||||
memoist (0.16.1)
|
||||
mime-types (3.3)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2019.1009)
|
||||
mini_magick (4.9.5)
|
||||
multi_json (1.14.1)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.0.0)
|
||||
nanaimo (0.2.6)
|
||||
naturally (2.2.0)
|
||||
os (1.0.1)
|
||||
plist (3.5.0)
|
||||
public_suffix (2.0.5)
|
||||
representable (3.0.4)
|
||||
declarative (< 0.1.0)
|
||||
declarative-option (< 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
rouge (2.0.7)
|
||||
rubyzip (1.3.0)
|
||||
security (0.1.3)
|
||||
signet (0.11.0)
|
||||
addressable (~> 2.3)
|
||||
faraday (~> 0.9)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
multi_json (~> 1.10)
|
||||
simctl (1.6.6)
|
||||
CFPropertyList
|
||||
naturally
|
||||
slack-notifier (2.3.2)
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (1.8.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
tty-cursor (0.7.0)
|
||||
tty-screen (0.7.0)
|
||||
tty-spinner (0.9.1)
|
||||
tty-cursor (~> 0.7)
|
||||
uber (0.1.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.6)
|
||||
unicode-display_width (1.6.0)
|
||||
word_wrap (1.0.0)
|
||||
xcode-install (2.6.2)
|
||||
claide (>= 0.9.1, < 1.1.0)
|
||||
fastlane (>= 2.1.0, < 3.0.0)
|
||||
xcodeproj (1.13.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.2.6)
|
||||
xcpretty (0.3.0)
|
||||
rouge (~> 2.0.7)
|
||||
xcpretty-travis-formatter (1.0.0)
|
||||
xcpretty (~> 0.2, >= 0.0.7)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
xcode-install
|
||||
|
||||
BUNDLED WITH
|
||||
2.0.1
|
||||
@@ -1,32 +1,34 @@
|
||||
SWIFT_EXE=swift
|
||||
SWIFT_TEST_FLAGS=
|
||||
SWIFT_BUILD_FLAGS=-Xcc -Wunguarded-availability
|
||||
|
||||
debug:
|
||||
swift build -c debug
|
||||
$(SWIFT_EXE) build -c debug $(SWIFT_BUILD_FLAGS)
|
||||
|
||||
release:
|
||||
swift build -c release
|
||||
$(SWIFT_EXE) build -c release $(SWIFT_BUILD_FLAGS)
|
||||
|
||||
test-debug:
|
||||
swift test -c debug $(SWIFT_TEST_FLAGS)
|
||||
$(SWIFT_EXE) test -c debug $(SWIFT_BUILD_FLAGS) $(SWIFT_TEST_FLAGS)
|
||||
|
||||
test-debug-sanitize-thread:
|
||||
swift test -c debug --sanitize thread $(SWIFT_TEST_FLAGS)
|
||||
$(SWIFT_EXE) test -c debug --sanitize thread $(SWIFT_BUILD_FLAGS) $(SWIFT_TEST_FLAGS)
|
||||
|
||||
test-release:
|
||||
swift test -c release $(SWIFT_TEST_FLAGS)
|
||||
$(SWIFT_EXE) test -c release $(SWIFT_BUILD_FLAGS) $(SWIFT_TEST_FLAGS)
|
||||
|
||||
swift-version:
|
||||
swift -version
|
||||
$(SWIFT_EXE) -version
|
||||
|
||||
test-compatibility:
|
||||
swift test -Xswiftc -DOPENCOMBINE_COMPATIBILITY_TEST
|
||||
$(SWIFT_EXE) test -Xswiftc -DOPENCOMBINE_COMPATIBILITY_TEST
|
||||
|
||||
generate-compatibility-xcodeproj:
|
||||
swift package generate-xcodeproj --xcconfig-overrides Combine-Compatibility.xcconfig; \
|
||||
$(SWIFT_EXE) package generate-xcodeproj --xcconfig-overrides Combine-Compatibility.xcconfig; \
|
||||
open OpenCombine.xcodeproj
|
||||
|
||||
generate-xcodeproj:
|
||||
swift package generate-xcodeproj --enable-code-coverage
|
||||
$(SWIFT_EXE) package $(SWIFT_BUILD_FLAGS) generate-xcodeproj --enable-code-coverage
|
||||
|
||||
gyb:
|
||||
$(shell ./utils/recursively_gyb.sh)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = "OpenCombine"
|
||||
spec.version = "0.8.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.license = "MIT"
|
||||
|
||||
spec.authors = { "Sergej Jaskiewicz" => "jaskiewiczs@icloud.com" }
|
||||
spec.source = { :git => "https://github.com/broadwaylamb/OpenCombine.git", :tag => "#{spec.version}" }
|
||||
|
||||
spec.swift_version = "5.0"
|
||||
|
||||
spec.osx.deployment_target = "10.10"
|
||||
spec.ios.deployment_target = "8.0"
|
||||
spec.watchos.deployment_target = "2.0"
|
||||
spec.tvos.deployment_target = "9.0"
|
||||
|
||||
spec.source_files = "Sources/COpenCombineHelpers/**/*.{h,cpp}", "Sources/OpenCombine/**/*.swift"
|
||||
spec.public_header_files = "Sources/COpenCombineHelpers/include/*.h"
|
||||
|
||||
spec.libraries = "c++"
|
||||
end
|
||||
@@ -0,0 +1,25 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = "OpenCombineDispatch"
|
||||
spec.version = "0.8.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.license = "MIT"
|
||||
|
||||
spec.authors = { "Sergej Jaskiewicz" => "jaskiewiczs@icloud.com" }
|
||||
spec.source = { :git => "https://github.com/broadwaylamb/OpenCombine.git", :tag => "#{spec.version}" }
|
||||
|
||||
spec.swift_version = "5.0"
|
||||
|
||||
spec.osx.deployment_target = "10.10"
|
||||
spec.ios.deployment_target = "8.0"
|
||||
spec.watchos.deployment_target = "2.0"
|
||||
spec.tvos.deployment_target = "9.0"
|
||||
|
||||
spec.source_files = "Sources/OpenCombineDispatch/**/*.swift"
|
||||
spec.dependency "OpenCombine", '>= 0.7'
|
||||
end
|
||||
@@ -0,0 +1,25 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = "OpenCombineFoundation"
|
||||
spec.version = "0.7.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.license = "MIT"
|
||||
|
||||
spec.authors = { "Sergej Jaskiewicz" => "jaskiewiczs@icloud.com" }
|
||||
spec.source = { :git => "https://github.com/broadwaylamb/OpenCombine.git", :tag => "#{spec.version}" }
|
||||
|
||||
spec.swift_version = "5.0"
|
||||
|
||||
spec.osx.deployment_target = "10.10"
|
||||
spec.ios.deployment_target = "8.0"
|
||||
spec.watchos.deployment_target = "2.0"
|
||||
spec.tvos.deployment_target = "9.0"
|
||||
|
||||
spec.source_files = "Sources/OpenCombineFoundation/**/*.swift"
|
||||
spec.dependency "OpenCombine", '>= 0.7'
|
||||
end
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "GottaGoFast",
|
||||
"repositoryURL": "https://github.com/broadwaylamb/GottaGoFast.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "85e1f5104fb1ede87d6f31acc0445555007c474c",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Yams",
|
||||
"repositoryURL": "https://github.com/jpsim/Yams.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "c947a306d2e80ecb2c0859047b35c73b8e1ca27f",
|
||||
"version": "2.0.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
+4
-4
@@ -7,18 +7,18 @@ let package = Package(
|
||||
products: [
|
||||
.library(name: "OpenCombine", targets: ["OpenCombine"]),
|
||||
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/broadwaylamb/GottaGoFast.git", from: "0.1.0")
|
||||
.library(name: "OpenCombineFoundation", targets: ["OpenCombineFoundation"]),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "COpenCombineHelpers"),
|
||||
.target(name: "OpenCombine", dependencies: ["COpenCombineHelpers"]),
|
||||
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
|
||||
.target(name: "OpenCombineFoundation", dependencies: ["OpenCombine",
|
||||
"COpenCombineHelpers"]),
|
||||
.testTarget(name: "OpenCombineTests",
|
||||
dependencies: ["OpenCombine",
|
||||
"OpenCombineDispatch",
|
||||
"GottaGoFast"],
|
||||
"OpenCombineFoundation"],
|
||||
swiftSettings: [.unsafeFlags(["-enable-testing"])])
|
||||
],
|
||||
cxxLanguageStandard: .cxx1z
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# OpenCombine
|
||||
[](https://travis-ci.org/broadwaylamb/OpenCombine)
|
||||
[](https://circleci.com/gh/broadwaylamb/OpenCombine/tree/master)
|
||||
[](https://codecov.io/gh/broadwaylamb/OpenCombine)
|
||||

|
||||

|
||||

|
||||
[<img src="https://img.shields.io/badge/slack-OpenCombine-yellow.svg?logo=slack">](https://join.slack.com/t/opencombine/shared_invite/enQtNzE2MjE5NzkxODI0LTYxMjkzNDUxZWViZWI1Njc2YjBhODgxNjRjOTdkZTcxOGU2ZjJjZjYxMGI3NWZkN2RkNGFmZTUzNmU3MGE2ZWM)
|
||||
|
||||
Open-source implementation of Apple's [Combine](https://developer.apple.com/documentation/combine) framework for processing values over time.
|
||||
@@ -11,6 +12,45 @@ The main goal of this project is to provide a compatible, reliable and efficient
|
||||
|
||||
The project is in early development.
|
||||
|
||||
### 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`
|
||||
|
||||
##### 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.
|
||||
|
||||
```swift
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/broadwaylamb/OpenCombine.git", from: "0.8.0")
|
||||
],
|
||||
targets: [
|
||||
.target(name: "MyAwesomePackage", dependencies: ["OpenCombine",
|
||||
"OpenCombineDispatch",
|
||||
"OpenCombineFoundation"])
|
||||
]
|
||||
```
|
||||
|
||||
###### Xcode
|
||||
`OpenCombine` can also be added as a SPM 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/broadwaylamb/OpenCombine.git), choose the latest available version, and activate the checkboxes:
|
||||
|
||||
<p align="center">
|
||||
<img alt="Select the OpenCombine and OpenCombineDispatch targets"
|
||||
src="https://user-images.githubusercontent.com/16309982/67618468-bd379f80-f7f8-11e9-917f-e76e878a1aee.png" width="70%">
|
||||
</p>
|
||||
|
||||
##### CocoaPods
|
||||
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.8'
|
||||
pod 'OpenCombineDispatch', '~> 0.8'
|
||||
pod 'OpenCombineFoundation', '~> 0.8'
|
||||
```
|
||||
|
||||
### Contributing
|
||||
|
||||
In order to work on this project you will need Xcode 10.2 and Swift 5.0 or later.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,163 @@
|
||||
// From /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/lib/swift/Foundation.swiftmodule/x86_64.swiftinterface
|
||||
// swift-interface-format-version: 1.0
|
||||
// swift-compiler-version: Apple Swift version 5.1.1 (swiftlang-1100.8.275.1 clang-1100.0.32.1)
|
||||
// swift-module-flags: -target x86_64-apple-macosx10.15 -enable-objc-interop -autolink-force-load -enable-library-evolution -module-link-name swiftFoundation -swift-version 5 -O -enforce-exclusivity=unchecked -module-name Foundation
|
||||
|
||||
public typealias Published = Combine.Published
|
||||
|
||||
public typealias ObservableObject = Combine.ObservableObject
|
||||
|
||||
public protocol _KeyValueCodingAndObservingPublishing {
|
||||
}
|
||||
|
||||
extension NSObject : Foundation._KeyValueCodingAndObservingPublishing {
|
||||
}
|
||||
|
||||
extension _KeyValueCodingAndObservingPublishing where Self : ObjectiveC.NSObject {
|
||||
public func publisher<Value>(for keyPath: Swift.KeyPath<Self, Value>, options: Foundation.NSKeyValueObservingOptions = [.initial, .new]) -> ObjectiveC.NSObject.KeyValueObservingPublisher<Self, Value>
|
||||
}
|
||||
|
||||
extension NSObject.KeyValueObservingPublisher {
|
||||
public func didChange() -> Combine.Publishers.Map<ObjectiveC.NSObject.KeyValueObservingPublisher<Subject, Value>, Swift.Void>
|
||||
}
|
||||
|
||||
extension NSObject {
|
||||
public struct KeyValueObservingPublisher<Subject, Value> : Swift.Equatable where Subject : ObjectiveC.NSObject {
|
||||
public let object: Subject
|
||||
public let keyPath: Swift.KeyPath<Subject, Value>
|
||||
public let options: Foundation.NSKeyValueObservingOptions
|
||||
public init(object: Subject, keyPath: Swift.KeyPath<Subject, Value>, options: Foundation.NSKeyValueObservingOptions)
|
||||
public static func == (lhs: ObjectiveC.NSObject.KeyValueObservingPublisher<Subject, Value>, rhs: ObjectiveC.NSObject.KeyValueObservingPublisher<Subject, Value>) -> Swift.Bool
|
||||
}
|
||||
}
|
||||
|
||||
extension NSObject.KeyValueObservingPublisher : Combine.Publisher {
|
||||
public typealias Output = Value
|
||||
public typealias Failure = Swift.Never
|
||||
public func receive<S>(subscriber: S) where Value == S.Input, S : Combine.Subscriber, S.Failure == ObjectiveC.NSObject.KeyValueObservingPublisher<Subject, Value>.Failure
|
||||
}
|
||||
|
||||
extension Timer {
|
||||
public static func publish(every interval: Foundation.TimeInterval, tolerance: Foundation.TimeInterval? = nil, on runLoop: Foundation.RunLoop, in mode: Foundation.RunLoop.Mode, options: Foundation.RunLoop.SchedulerOptions? = nil) -> Foundation.Timer.TimerPublisher
|
||||
final public class TimerPublisher : Combine.ConnectablePublisher {
|
||||
public typealias Output = Foundation.Date
|
||||
public typealias Failure = Swift.Never
|
||||
final public let interval: Foundation.TimeInterval
|
||||
final public let tolerance: Foundation.TimeInterval?
|
||||
final public let runLoop: Foundation.RunLoop
|
||||
final public let mode: Foundation.RunLoop.Mode
|
||||
final public let options: Foundation.RunLoop.SchedulerOptions?
|
||||
public init(interval: Foundation.TimeInterval, tolerance: Foundation.TimeInterval? = nil, runLoop: Foundation.RunLoop, mode: Foundation.RunLoop.Mode, options: Foundation.RunLoop.SchedulerOptions? = nil)
|
||||
final public func receive<S>(subscriber: S) where S : Combine.Subscriber, S.Failure == Foundation.Timer.TimerPublisher.Failure, S.Input == Foundation.Timer.TimerPublisher.Output
|
||||
final public func connect() -> Combine.Cancellable
|
||||
@objc deinit
|
||||
}
|
||||
}
|
||||
|
||||
extension OperationQueue : Combine.Scheduler {
|
||||
public struct SchedulerTimeType : Swift.Strideable, Swift.Codable, Swift.Hashable {
|
||||
public var date: Foundation.Date
|
||||
public init(_ date: Foundation.Date)
|
||||
public func distance(to other: Foundation.OperationQueue.SchedulerTimeType) -> Foundation.OperationQueue.SchedulerTimeType.Stride
|
||||
public func advanced(by n: Foundation.OperationQueue.SchedulerTimeType.Stride) -> Foundation.OperationQueue.SchedulerTimeType
|
||||
public struct Stride : Swift.ExpressibleByFloatLiteral, Swift.Comparable, Swift.SignedNumeric, Swift.Codable, Combine.SchedulerTimeIntervalConvertible {
|
||||
public typealias FloatLiteralType = Foundation.TimeInterval
|
||||
public typealias IntegerLiteralType = Foundation.TimeInterval
|
||||
public typealias Magnitude = Foundation.TimeInterval
|
||||
public var magnitude: Foundation.TimeInterval
|
||||
public var timeInterval: Foundation.TimeInterval {
|
||||
get
|
||||
}
|
||||
public init(integerLiteral value: Foundation.TimeInterval)
|
||||
public init(floatLiteral value: Foundation.TimeInterval)
|
||||
public init(_ timeInterval: Foundation.TimeInterval)
|
||||
public init?<T>(exactly source: T) where T : Swift.BinaryInteger
|
||||
public static func < (lhs: Foundation.OperationQueue.SchedulerTimeType.Stride, rhs: Foundation.OperationQueue.SchedulerTimeType.Stride) -> Swift.Bool
|
||||
public static func * (lhs: Foundation.OperationQueue.SchedulerTimeType.Stride, rhs: Foundation.OperationQueue.SchedulerTimeType.Stride) -> Foundation.OperationQueue.SchedulerTimeType.Stride
|
||||
public static func + (lhs: Foundation.OperationQueue.SchedulerTimeType.Stride, rhs: Foundation.OperationQueue.SchedulerTimeType.Stride) -> Foundation.OperationQueue.SchedulerTimeType.Stride
|
||||
public static func - (lhs: Foundation.OperationQueue.SchedulerTimeType.Stride, rhs: Foundation.OperationQueue.SchedulerTimeType.Stride) -> Foundation.OperationQueue.SchedulerTimeType.Stride
|
||||
public static func *= (lhs: inout Foundation.OperationQueue.SchedulerTimeType.Stride, rhs: Foundation.OperationQueue.SchedulerTimeType.Stride)
|
||||
public static func += (lhs: inout Foundation.OperationQueue.SchedulerTimeType.Stride, rhs: Foundation.OperationQueue.SchedulerTimeType.Stride)
|
||||
public static func -= (lhs: inout Foundation.OperationQueue.SchedulerTimeType.Stride, rhs: Foundation.OperationQueue.SchedulerTimeType.Stride)
|
||||
public static func seconds(_ s: Swift.Int) -> Foundation.OperationQueue.SchedulerTimeType.Stride
|
||||
public static func seconds(_ s: Swift.Double) -> Foundation.OperationQueue.SchedulerTimeType.Stride
|
||||
public static func milliseconds(_ ms: Swift.Int) -> Foundation.OperationQueue.SchedulerTimeType.Stride
|
||||
public static func microseconds(_ us: Swift.Int) -> Foundation.OperationQueue.SchedulerTimeType.Stride
|
||||
public static func nanoseconds(_ ns: Swift.Int) -> Foundation.OperationQueue.SchedulerTimeType.Stride
|
||||
public init(from decoder: Swift.Decoder) throws
|
||||
public func encode(to encoder: Swift.Encoder) throws
|
||||
public static func == (a: Foundation.OperationQueue.SchedulerTimeType.Stride, b: Foundation.OperationQueue.SchedulerTimeType.Stride) -> Swift.Bool
|
||||
}
|
||||
public init(from decoder: Swift.Decoder) throws
|
||||
public func encode(to encoder: Swift.Encoder) throws
|
||||
public var hashValue: Swift.Int {
|
||||
get
|
||||
}
|
||||
public func hash(into hasher: inout Swift.Hasher)
|
||||
}
|
||||
public struct SchedulerOptions {
|
||||
}
|
||||
public func schedule(options: Foundation.OperationQueue.SchedulerOptions?, _ action: @escaping () -> Swift.Void)
|
||||
public func schedule(after date: Foundation.OperationQueue.SchedulerTimeType, tolerance: Foundation.OperationQueue.SchedulerTimeType.Stride, options: Foundation.OperationQueue.SchedulerOptions?, _ action: @escaping () -> Swift.Void)
|
||||
public func schedule(after date: Foundation.OperationQueue.SchedulerTimeType, interval: Foundation.OperationQueue.SchedulerTimeType.Stride, tolerance: Foundation.OperationQueue.SchedulerTimeType.Stride, options: Foundation.OperationQueue.SchedulerOptions?, _ action: @escaping () -> Swift.Void) -> Combine.Cancellable
|
||||
public var now: Foundation.OperationQueue.SchedulerTimeType {
|
||||
get
|
||||
}
|
||||
public var minimumTolerance: Foundation.OperationQueue.SchedulerTimeType.Stride {
|
||||
get
|
||||
}
|
||||
}
|
||||
|
||||
extension RunLoop : Combine.Scheduler {
|
||||
public struct SchedulerTimeType : Swift.Strideable, Swift.Codable, Swift.Hashable {
|
||||
public var date: Foundation.Date
|
||||
public init(_ date: Foundation.Date)
|
||||
public func distance(to other: Foundation.RunLoop.SchedulerTimeType) -> Foundation.RunLoop.SchedulerTimeType.Stride
|
||||
public func advanced(by n: Foundation.RunLoop.SchedulerTimeType.Stride) -> Foundation.RunLoop.SchedulerTimeType
|
||||
public struct Stride : Swift.ExpressibleByFloatLiteral, Swift.Comparable, Swift.SignedNumeric, Swift.Codable, Combine.SchedulerTimeIntervalConvertible {
|
||||
public typealias FloatLiteralType = Foundation.TimeInterval
|
||||
public typealias IntegerLiteralType = Foundation.TimeInterval
|
||||
public typealias Magnitude = Foundation.TimeInterval
|
||||
public var magnitude: Foundation.TimeInterval
|
||||
public var timeInterval: Foundation.TimeInterval {
|
||||
get
|
||||
}
|
||||
public init(integerLiteral value: Foundation.TimeInterval)
|
||||
public init(floatLiteral value: Foundation.TimeInterval)
|
||||
public init(_ timeInterval: Foundation.TimeInterval)
|
||||
public init?<T>(exactly source: T) where T : Swift.BinaryInteger
|
||||
public static func < (lhs: Foundation.RunLoop.SchedulerTimeType.Stride, rhs: Foundation.RunLoop.SchedulerTimeType.Stride) -> Swift.Bool
|
||||
public static func * (lhs: Foundation.RunLoop.SchedulerTimeType.Stride, rhs: Foundation.RunLoop.SchedulerTimeType.Stride) -> Foundation.RunLoop.SchedulerTimeType.Stride
|
||||
public static func + (lhs: Foundation.RunLoop.SchedulerTimeType.Stride, rhs: Foundation.RunLoop.SchedulerTimeType.Stride) -> Foundation.RunLoop.SchedulerTimeType.Stride
|
||||
public static func - (lhs: Foundation.RunLoop.SchedulerTimeType.Stride, rhs: Foundation.RunLoop.SchedulerTimeType.Stride) -> Foundation.RunLoop.SchedulerTimeType.Stride
|
||||
public static func *= (lhs: inout Foundation.RunLoop.SchedulerTimeType.Stride, rhs: Foundation.RunLoop.SchedulerTimeType.Stride)
|
||||
public static func += (lhs: inout Foundation.RunLoop.SchedulerTimeType.Stride, rhs: Foundation.RunLoop.SchedulerTimeType.Stride)
|
||||
public static func -= (lhs: inout Foundation.RunLoop.SchedulerTimeType.Stride, rhs: Foundation.RunLoop.SchedulerTimeType.Stride)
|
||||
public static func seconds(_ s: Swift.Int) -> Foundation.RunLoop.SchedulerTimeType.Stride
|
||||
public static func seconds(_ s: Swift.Double) -> Foundation.RunLoop.SchedulerTimeType.Stride
|
||||
public static func milliseconds(_ ms: Swift.Int) -> Foundation.RunLoop.SchedulerTimeType.Stride
|
||||
public static func microseconds(_ us: Swift.Int) -> Foundation.RunLoop.SchedulerTimeType.Stride
|
||||
public static func nanoseconds(_ ns: Swift.Int) -> Foundation.RunLoop.SchedulerTimeType.Stride
|
||||
public init(from decoder: Swift.Decoder) throws
|
||||
public func encode(to encoder: Swift.Encoder) throws
|
||||
public static func == (a: Foundation.RunLoop.SchedulerTimeType.Stride, b: Foundation.RunLoop.SchedulerTimeType.Stride) -> Swift.Bool
|
||||
}
|
||||
public init(from decoder: Swift.Decoder) throws
|
||||
public func encode(to encoder: Swift.Encoder) throws
|
||||
public var hashValue: Swift.Int {
|
||||
get
|
||||
}
|
||||
public func hash(into hasher: inout Swift.Hasher)
|
||||
}
|
||||
public struct SchedulerOptions {
|
||||
}
|
||||
public func schedule(options: Foundation.RunLoop.SchedulerOptions?, _ action: @escaping () -> Swift.Void)
|
||||
public func schedule(after date: Foundation.RunLoop.SchedulerTimeType, tolerance: Foundation.RunLoop.SchedulerTimeType.Stride, options: Foundation.RunLoop.SchedulerOptions?, _ action: @escaping () -> Swift.Void)
|
||||
public func schedule(after date: Foundation.RunLoop.SchedulerTimeType, interval: Foundation.RunLoop.SchedulerTimeType.Stride, tolerance: Foundation.RunLoop.SchedulerTimeType.Stride, options: Foundation.RunLoop.SchedulerOptions?, _ action: @escaping () -> Swift.Void) -> Combine.Cancellable
|
||||
public var now: Foundation.RunLoop.SchedulerTimeType {
|
||||
get
|
||||
}
|
||||
public var minimumTolerance: Foundation.RunLoop.SchedulerTimeType.Stride {
|
||||
get
|
||||
}
|
||||
}
|
||||
@@ -8,55 +8,149 @@
|
||||
#include "COpenCombineHelpers.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <cstdlib>
|
||||
#include <system_error>
|
||||
#include <pthread.h>
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <os/lock.h>
|
||||
#endif // __APPLE__
|
||||
|
||||
// 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 {
|
||||
|
||||
#define OPENCOMBINE_HANDLE_EXCEPTION_END } catch (...) { abort(); }
|
||||
// std::terminate will print the type and the error message of the in-flight exception.
|
||||
#define OPENCOMBINE_HANDLE_EXCEPTION_END } catch (...) { std::terminate(); }
|
||||
|
||||
// See 'double expansion trick'
|
||||
#define OPENCOMBINE_STRINGIFY(value) #value
|
||||
#define OPENCOMBINE_STRINGIFY_(value) OPENCOMBINE_STRINGIFY(value)
|
||||
#define OPENCOMBINE_STRING_LINE_NUMBER OPENCOMBINE_STRINGIFY_(__LINE__)
|
||||
|
||||
// Throw an exception if the argument is non-zero with filename and line where the error
|
||||
// occured.
|
||||
#define OPENCOMBINE_HANDLE_PTHREAD_CALL(errc) \
|
||||
if ((errc) != 0) { \
|
||||
const char* what = __FILE__ ":" OPENCOMBINE_STRING_LINE_NUMBER ": " #errc; \
|
||||
throw std::system_error((errc), std::system_category(), what); \
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
static std::atomic<uint64_t> next_combine_identifier;
|
||||
std::atomic<uint64_t> next_combine_identifier;
|
||||
|
||||
class PlatformIndependentMutex {
|
||||
public:
|
||||
virtual void lock() = 0;
|
||||
virtual void unlock() = 0;
|
||||
virtual void assertOwner() {}
|
||||
|
||||
virtual ~PlatformIndependentMutex() {}
|
||||
virtual ~PlatformIndependentMutex() noexcept(false) {}
|
||||
};
|
||||
|
||||
template <typename Mutex>
|
||||
class GenericMutex final : PlatformIndependentMutex {
|
||||
Mutex mutex_;
|
||||
class PThreadMutex : PlatformIndependentMutex {
|
||||
private:
|
||||
pthread_mutex_t mutex_;
|
||||
public:
|
||||
void lock() override {
|
||||
mutex_.lock();
|
||||
PThreadMutex() {
|
||||
Attributes attrs;
|
||||
attrs.setErrorCheck();
|
||||
initialize(attrs);
|
||||
}
|
||||
|
||||
void unlock() override {
|
||||
mutex_.unlock();
|
||||
PThreadMutex(const PThreadMutex&) = delete;
|
||||
PThreadMutex& operator=(const PThreadMutex&) = delete;
|
||||
|
||||
PThreadMutex(PThreadMutex&&) = delete;
|
||||
PThreadMutex& operator=(PThreadMutex&&) = delete;
|
||||
|
||||
void lock() override final {
|
||||
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutex_lock(&mutex_));
|
||||
}
|
||||
|
||||
void unlock() override final {
|
||||
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_));
|
||||
}
|
||||
protected:
|
||||
class Attributes {
|
||||
pthread_mutexattr_t attrs_;
|
||||
public:
|
||||
Attributes() {
|
||||
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutexattr_init(&attrs_));
|
||||
}
|
||||
|
||||
Attributes(const Attributes&) = delete;
|
||||
Attributes& operator=(const Attributes&) = delete;
|
||||
|
||||
Attributes(Attributes&&) = delete;
|
||||
Attributes& operator=(Attributes&&) = delete;
|
||||
|
||||
const pthread_mutexattr_t* raw() const noexcept {
|
||||
return &attrs_;
|
||||
}
|
||||
|
||||
void setRecursive() {
|
||||
setType(PTHREAD_MUTEX_RECURSIVE);
|
||||
}
|
||||
|
||||
void setErrorCheck() {
|
||||
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_));
|
||||
}
|
||||
private:
|
||||
void setType(int type) {
|
||||
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutexattr_settype(&attrs_, type));
|
||||
}
|
||||
};
|
||||
|
||||
void initialize(const Attributes& attributes) {
|
||||
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;
|
||||
};
|
||||
|
||||
#ifdef __APPLE__
|
||||
bool isOSUnfairLockAvailable() {
|
||||
// We're linking weakly, so if we're back-deploying, this will be null.
|
||||
return os_unfair_lock_lock != nullptr;
|
||||
}
|
||||
|
||||
template <>
|
||||
class GenericMutex<os_unfair_lock> final : PlatformIndependentMutex {
|
||||
class OS_UNFAIR_LOCK_AVAILABILITY OSUnfairLock final : PlatformIndependentMutex {
|
||||
os_unfair_lock mutex_ = OS_UNFAIR_LOCK_INIT;
|
||||
public:
|
||||
GenericMutex() = default;
|
||||
GenericMutex(const GenericMutex&) = delete;
|
||||
GenericMutex& operator=(const GenericMutex&) = delete;
|
||||
OSUnfairLock() = default;
|
||||
|
||||
OSUnfairLock(const OSUnfairLock&) = delete;
|
||||
OSUnfairLock& operator=(const OSUnfairLock&) = delete;
|
||||
|
||||
OSUnfairLock(OSUnfairLock&&) = delete;
|
||||
OSUnfairLock& operator=(OSUnfairLock&&) = delete;
|
||||
|
||||
void lock() override {
|
||||
os_unfair_lock_lock(&mutex_);
|
||||
@@ -65,6 +159,10 @@ public:
|
||||
void unlock() override {
|
||||
os_unfair_lock_unlock(&mutex_);
|
||||
}
|
||||
|
||||
void assertOwner() override {
|
||||
os_unfair_lock_assert_owner(&mutex_);
|
||||
}
|
||||
};
|
||||
#endif // __APPLE__
|
||||
|
||||
@@ -80,13 +178,13 @@ OpenCombineUnfairLock opencombine_unfair_lock_alloc(void) {
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
|
||||
|
||||
#ifdef __APPLE__
|
||||
if (isOSUnfairLockAvailable()) {
|
||||
return {new GenericMutex<os_unfair_lock>};
|
||||
if (__builtin_available(macOS 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *)) {
|
||||
return {new OSUnfairLock};
|
||||
} else {
|
||||
return {new GenericMutex<std::mutex>};
|
||||
return {new PThreadMutex};
|
||||
}
|
||||
#else
|
||||
return {new GenericMutex<std::mutex>};
|
||||
return {new PThreadMutex};
|
||||
#endif
|
||||
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_END
|
||||
@@ -95,7 +193,7 @@ OpenCombineUnfairLock opencombine_unfair_lock_alloc(void) {
|
||||
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 GenericMutex<std::recursive_mutex>};
|
||||
return {new PThreadRecursiveMutex};
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_END
|
||||
}
|
||||
|
||||
@@ -111,6 +209,12 @@ void opencombine_unfair_lock_unlock(OpenCombineUnfairLock mutex) {
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_END
|
||||
}
|
||||
|
||||
void opencombine_unfair_lock_assert_owner(OpenCombineUnfairLock mutex) {
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
|
||||
static_cast<PlatformIndependentMutex*>(mutex.opaque)->assertOwner();
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_END
|
||||
}
|
||||
|
||||
void opencombine_unfair_recursive_lock_lock(OpenCombineUnfairRecursiveLock lock) {
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
|
||||
static_cast<PlatformIndependentMutex*>(lock.opaque)->lock();
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#define COPENCOMBINEHELPERS_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <signal.h>
|
||||
|
||||
#if __has_attribute(swift_name)
|
||||
# define OPENCOMBINE_SWIFT_NAME(_name) __attribute__((swift_name(#_name)))
|
||||
@@ -16,6 +17,12 @@
|
||||
# define OPENCOMBINE_SWIFT_NAME(_name)
|
||||
#endif
|
||||
|
||||
#if __has_attribute(always_inline)
|
||||
# define OPENCOMBINE_ALWAYS_INLINE __attribute__((always_inline))
|
||||
#else
|
||||
# define OPENCOMBINE_ALWAYS_INLINE
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
@@ -23,47 +30,59 @@ extern "C" {
|
||||
#pragma mark - CombineIdentifier
|
||||
|
||||
uint64_t opencombine_next_combine_identifier(void)
|
||||
OPENCOMBINE_SWIFT_NAME(nextCombineIdentifier());
|
||||
OPENCOMBINE_SWIFT_NAME(__nextCombineIdentifier());
|
||||
|
||||
#pragma mark - OpenCombineUnfairLock
|
||||
|
||||
/// A wrapper around an opaque pointer for type safety in Swift.
|
||||
typedef struct OpenCombineUnfairLock {
|
||||
void* _Nonnull opaque;
|
||||
} OPENCOMBINE_SWIFT_NAME(UnfairLock) OpenCombineUnfairLock;
|
||||
} OPENCOMBINE_SWIFT_NAME(__UnfairLock) OpenCombineUnfairLock;
|
||||
|
||||
/// Allocates a lock object. The allocated object must be destroyed by calling
|
||||
/// the destroy() method.
|
||||
OpenCombineUnfairLock opencombine_unfair_lock_alloc(void)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairLock.allocate());
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairLock.allocate());
|
||||
|
||||
void opencombine_unfair_lock_lock(OpenCombineUnfairLock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairLock.lock(self:));
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairLock.lock(self:));
|
||||
|
||||
void opencombine_unfair_lock_unlock(OpenCombineUnfairLock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairLock.unlock(self:));
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairLock.unlock(self:));
|
||||
|
||||
void opencombine_unfair_lock_assert_owner(OpenCombineUnfairLock mutex)
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairLock.assertOwner(self:));
|
||||
|
||||
void opencombine_unfair_lock_dealloc(OpenCombineUnfairLock lock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairLock.deallocate(self:));
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairLock.deallocate(self:));
|
||||
|
||||
#pragma mark - OpenCombineUnfairRecursiveLock
|
||||
|
||||
/// A wrapper around an opaque pointer for type safety in Swift.
|
||||
typedef struct OpenCombineUnfairRecursiveLock {
|
||||
void* _Nonnull opaque;
|
||||
} OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock) OpenCombineUnfairRecursiveLock;
|
||||
} OPENCOMBINE_SWIFT_NAME(__UnfairRecursiveLock) OpenCombineUnfairRecursiveLock;
|
||||
|
||||
OpenCombineUnfairRecursiveLock opencombine_unfair_recursive_lock_alloc(void)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock.allocate());
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairRecursiveLock.allocate());
|
||||
|
||||
void opencombine_unfair_recursive_lock_lock(OpenCombineUnfairRecursiveLock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock.lock(self:));
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairRecursiveLock.lock(self:));
|
||||
|
||||
void opencombine_unfair_recursive_lock_unlock(OpenCombineUnfairRecursiveLock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock.unlock(self:));
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairRecursiveLock.unlock(self:));
|
||||
|
||||
void opencombine_unfair_recursive_lock_dealloc(OpenCombineUnfairRecursiveLock lock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock.deallocate(self:));
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairRecursiveLock.deallocate(self:));
|
||||
|
||||
#pragma mark - Breakpoint
|
||||
|
||||
OPENCOMBINE_ALWAYS_INLINE
|
||||
inline void opencombine_stop_in_debugger(void) OPENCOMBINE_SWIFT_NAME(__stopInDebugger());
|
||||
|
||||
void opencombine_stop_in_debugger(void) {
|
||||
raise(SIGTRAP);
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
|
||||
@@ -5,21 +5,23 @@
|
||||
// Created by Sergej Jaskiewicz on 10.06.2019.
|
||||
//
|
||||
|
||||
import func COpenCombineHelpers.nextCombineIdentifier
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
public struct CombineIdentifier: Hashable, CustomStringConvertible {
|
||||
|
||||
private let id: UInt64
|
||||
private let value: UInt64
|
||||
|
||||
public init() {
|
||||
self.id = nextCombineIdentifier()
|
||||
value = __nextCombineIdentifier()
|
||||
}
|
||||
|
||||
public init(_ obj: AnyObject) {
|
||||
id = UInt64(UInt(bitPattern: ObjectIdentifier(obj)))
|
||||
value = UInt64(UInt(bitPattern: ObjectIdentifier(obj)))
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
return "0x\(String(id, radix: 16))"
|
||||
return "0x\(String(value, radix: 16))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
/// A subject that wraps a single value and publishes a new element whenever the value
|
||||
/// changes.
|
||||
public final class CurrentValueSubject<Output, Failure: Error>: Subject {
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
//
|
||||
// Future.swift
|
||||
//
|
||||
//
|
||||
// Created by Max Desiatov on 24/11/2019.
|
||||
//
|
||||
|
||||
/// A publisher that eventually produces one value and then finishes or fails.
|
||||
public final class Future<Output, Failure>: Publisher where Failure: Error {
|
||||
|
||||
public typealias Promise = (Result<Output, Failure>) -> Void
|
||||
|
||||
private let _lock = UnfairRecursiveLock.allocate()
|
||||
private var _subscriptions: [Conduit] = []
|
||||
|
||||
private var result: Result<Output, Failure>?
|
||||
|
||||
public init(
|
||||
_ attemptToFulfill: @escaping (@escaping Promise) -> Void
|
||||
) {
|
||||
attemptToFulfill { result in
|
||||
self._lock.do {
|
||||
guard self.result == nil else { return }
|
||||
self.result = result
|
||||
self._publish(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
_lock.deallocate()
|
||||
}
|
||||
|
||||
/// 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 Output == Downstream.Input, Failure == Downstream.Failure {
|
||||
let subscription = Conduit(parent: self,
|
||||
downstream: AnySubscriber(subscriber))
|
||||
|
||||
_subscriptions.append(subscription)
|
||||
subscriber.receive(subscription: subscription)
|
||||
}
|
||||
|
||||
private func _acknowledgeDownstreamDemand() {
|
||||
_lock.do {
|
||||
guard let result = result else { return }
|
||||
_publish(result)
|
||||
}
|
||||
}
|
||||
|
||||
private func _publish(_ result: Result<Output, Failure>) {
|
||||
for subscription in self._subscriptions where !subscription._isCompleted {
|
||||
switch result {
|
||||
case let .success(output) where subscription._demand > 0:
|
||||
subscription._demand -= 1
|
||||
subscription._demand += subscription._downstream?.receive(output) ?? .none
|
||||
subscription._receive(completion: .finished)
|
||||
case let .failure(error):
|
||||
subscription._receive(completion: .failure(error))
|
||||
|
||||
// nothing to do if no demand
|
||||
default: ()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Future {
|
||||
|
||||
fileprivate final class Conduit: Subscription {
|
||||
|
||||
fileprivate var _parent: Future<Output, Failure>?
|
||||
|
||||
fileprivate var _downstream: AnySubscriber<Output, Failure>?
|
||||
|
||||
fileprivate var _demand: Subscribers.Demand = .none
|
||||
|
||||
fileprivate var _isCompleted: Bool {
|
||||
return _parent == nil
|
||||
}
|
||||
|
||||
fileprivate init(parent: Future<Output, Failure>,
|
||||
downstream: AnySubscriber<Output, Failure>) {
|
||||
_parent = parent
|
||||
_downstream = downstream
|
||||
}
|
||||
|
||||
fileprivate func _receive(completion: Subscribers.Completion<Failure>) {
|
||||
if !_isCompleted {
|
||||
_parent = nil
|
||||
_downstream?.receive(completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
_parent?._lock.do {
|
||||
_demand += demand
|
||||
}
|
||||
_parent?._acknowledgeDownstreamDemand()
|
||||
}
|
||||
|
||||
fileprivate func cancel() {
|
||||
_parent = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Future.Conduit: CustomStringConvertible {
|
||||
fileprivate var description: String { return "Future" }
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
//
|
||||
// FilterProducer.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 23.10.2019.
|
||||
//
|
||||
|
||||
/// A helper class that acts like both subscriber and subscription.
|
||||
///
|
||||
/// Filter-like operators send an instance of their `Inner` class that is subclass
|
||||
/// of this class to the upstream publisher (as subscriber) and
|
||||
/// to the downstream subcriber (as subscription).
|
||||
///
|
||||
/// Filter-like operators include `Publishers.Filter`,
|
||||
/// `Publishers.RemoveDuplicates`, `Publishers.PrefixWhile` and more.
|
||||
///
|
||||
/// Subclasses must override the `receive(newValue:)` and `description`.
|
||||
internal class FilterProducer<Downstream: Subscriber,
|
||||
Input,
|
||||
Output,
|
||||
UpstreamFailure: Error,
|
||||
Filter>
|
||||
: CustomStringConvertible,
|
||||
CustomReflectable
|
||||
where Downstream.Input == Output
|
||||
{
|
||||
// NOTE: This class has been audited for thread safety
|
||||
|
||||
// MARK: - State
|
||||
|
||||
private enum State {
|
||||
case awaitingSubscription
|
||||
case connected(Subscription)
|
||||
case completed
|
||||
}
|
||||
|
||||
internal final let filter: Filter
|
||||
|
||||
internal final let downstream: Downstream
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private var state = State.awaitingSubscription
|
||||
|
||||
internal init(downstream: Downstream, filter: Filter) {
|
||||
self.downstream = downstream
|
||||
self.filter = filter
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
// MARK: - Abstract methods
|
||||
|
||||
internal func receive(
|
||||
newValue: Input
|
||||
) -> PartialCompletion<Output?, Downstream.Failure> {
|
||||
abstractMethod()
|
||||
}
|
||||
|
||||
internal var description: String {
|
||||
abstractMethod()
|
||||
}
|
||||
|
||||
// MARK: - CustomReflectable
|
||||
|
||||
internal var customMirror: Mirror {
|
||||
let children = CollectionOfOne<Mirror.Child>(("downstream", downstream))
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
}
|
||||
|
||||
extension FilterProducer: Subscriber {
|
||||
|
||||
internal func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .connected(subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
internal func receive(_ input: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
switch state {
|
||||
case .awaitingSubscription:
|
||||
lock.unlock()
|
||||
fatalError("Invalid state: Received value before receiving subscription")
|
||||
case .completed:
|
||||
lock.unlock()
|
||||
case let .connected(subscription):
|
||||
lock.unlock()
|
||||
switch receive(newValue: input) {
|
||||
case let .continue(output?):
|
||||
return downstream.receive(output)
|
||||
case .continue(nil):
|
||||
return .max(1)
|
||||
case .finished:
|
||||
lock.lock()
|
||||
state = .completed
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
downstream.receive(completion: .finished)
|
||||
case let .failure(error):
|
||||
lock.lock()
|
||||
state = .completed
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
downstream.receive(completion: .failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
return .none
|
||||
}
|
||||
|
||||
internal func receive(completion: Subscribers.Completion<UpstreamFailure>) {
|
||||
lock.lock()
|
||||
switch state {
|
||||
case .awaitingSubscription:
|
||||
lock.unlock()
|
||||
fatalError("Invalid state: Received completion before receiving subscription")
|
||||
case .completed:
|
||||
lock.unlock()
|
||||
return
|
||||
case .connected:
|
||||
state = .completed
|
||||
lock.unlock()
|
||||
switch completion {
|
||||
case .finished:
|
||||
downstream.receive(completion: .finished)
|
||||
case let .failure(failure):
|
||||
downstream.receive(completion: .failure(failure as! Downstream.Failure))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FilterProducer: Subscription {
|
||||
|
||||
internal func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
lock.lock()
|
||||
switch state {
|
||||
case .awaitingSubscription:
|
||||
lock.unlock()
|
||||
fatalError("Invalid state: Received request before sending subscription")
|
||||
case .completed:
|
||||
lock.unlock()
|
||||
return
|
||||
case let .connected(subscription):
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
}
|
||||
|
||||
internal func cancel() {
|
||||
lock.lock()
|
||||
guard case let .connected(subscription) = state else {
|
||||
state = .completed
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .completed
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
extension FilterProducer: CustomPlaygroundDisplayConvertible {
|
||||
internal var playgroundDescription: Any { return description }
|
||||
}
|
||||
@@ -5,17 +5,12 @@
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
extension UnfairLock {
|
||||
|
||||
@inlinable
|
||||
internal func `do`<Result>(_ body: () throws -> Result) rethrows -> Result {
|
||||
lock()
|
||||
defer { unlock() }
|
||||
return try body()
|
||||
}
|
||||
}
|
||||
internal typealias UnfairLock = __UnfairLock
|
||||
internal typealias UnfairRecursiveLock = __UnfairRecursiveLock
|
||||
|
||||
extension UnfairRecursiveLock {
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 22.09.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
/// A helper class that acts like both subscriber and subscription.
|
||||
///
|
||||
/// Reduce-like operators send an instance of their `Inner` class that is subclass
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 16/09/2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
// NOTE: This class has been audited for thread safety.
|
||||
internal final class SubjectSubscriber<Downstream: Subject>
|
||||
: Subscriber,
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
|
||||
/// A scheduler for performing synchronous actions.
|
||||
///
|
||||
/// You can use this scheduler for immediate actions. If you attempt to schedule
|
||||
/// actions after a specific date, the scheduler produces a fatal error.
|
||||
/// You can only use this scheduler for immediate actions. If you attempt to schedule
|
||||
/// actions after a specific date, this scheduler ignores the date and performs
|
||||
/// them immediately.
|
||||
public struct ImmediateScheduler: Scheduler {
|
||||
|
||||
/// The time type used by the immediate scheduler.
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
//
|
||||
// ObservableObject.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 08/09/2019.
|
||||
//
|
||||
|
||||
/// A type of object with a publisher that emits before the object has changed.
|
||||
///
|
||||
/// By default an `ObservableObject` will synthesize an `objectWillChange`
|
||||
/// publisher that emits before any of its `@Published` properties changes:
|
||||
///
|
||||
/// class Contact : ObservableObject {
|
||||
/// @Published var name: String
|
||||
/// @Published var age: Int
|
||||
///
|
||||
/// init(name: String, age: Int) {
|
||||
/// self.name = name
|
||||
/// self.age = age
|
||||
/// }
|
||||
///
|
||||
/// func haveBirthday() -> Int {
|
||||
/// age += 1
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let john = Contact(name: "John Appleseed", age: 24)
|
||||
/// john.objectWillChange.sink { _ in print("will change") }
|
||||
/// print(john.haveBirthday)
|
||||
/// // Prints "will change"
|
||||
/// // Prints "25"
|
||||
///
|
||||
public protocol ObservableObject: AnyObject {
|
||||
|
||||
/// The type of publisher that emits before the object has changed.
|
||||
associatedtype ObjectWillChangePublisher: Publisher = ObservableObjectPublisher
|
||||
where ObjectWillChangePublisher.Failure == Never
|
||||
|
||||
/// A publisher that emits before the object has changed.
|
||||
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
|
||||
}
|
||||
|
||||
/// The default publisher of an `ObservableObject`.
|
||||
public final class ObservableObjectPublisher: Publisher {
|
||||
|
||||
public typealias Output = Void
|
||||
|
||||
public typealias Failure = Never
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private var connections = Set<Conduit>()
|
||||
|
||||
// TODO: Combine needs this for some reason
|
||||
private var identifier: ObjectIdentifier?
|
||||
|
||||
public init() {}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Void, Downstream.Failure == Never
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, parent: self)
|
||||
lock.lock()
|
||||
connections.insert(inner)
|
||||
lock.unlock()
|
||||
subscriber.receive(subscription: inner)
|
||||
}
|
||||
|
||||
public func send() {
|
||||
lock.lock()
|
||||
let connections = self.connections
|
||||
lock.unlock()
|
||||
for connection in connections {
|
||||
connection.send()
|
||||
}
|
||||
}
|
||||
|
||||
private func remove(_ conduit: Conduit) {
|
||||
lock.lock()
|
||||
connections.remove(conduit)
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
extension ObservableObjectPublisher {
|
||||
private class Conduit: Hashable {
|
||||
|
||||
fileprivate func send() {
|
||||
abstractMethod()
|
||||
}
|
||||
|
||||
fileprivate static func == (lhs: Conduit, rhs: Conduit) -> Bool {
|
||||
return lhs === rhs
|
||||
}
|
||||
|
||||
fileprivate func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(ObjectIdentifier(self))
|
||||
}
|
||||
}
|
||||
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Conduit,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Void, Downstream.Failure == Never
|
||||
{
|
||||
private enum State {
|
||||
case initialized
|
||||
case active
|
||||
case terminal
|
||||
}
|
||||
|
||||
private weak var parent: ObservableObjectPublisher?
|
||||
private let downstream: Downstream
|
||||
private let downstreamLock = UnfairRecursiveLock.allocate()
|
||||
private let lock = UnfairLock.allocate()
|
||||
private var state = State.initialized
|
||||
|
||||
init(downstream: Downstream, parent: ObservableObjectPublisher) {
|
||||
self.parent = parent
|
||||
self.downstream = downstream
|
||||
}
|
||||
|
||||
deinit {
|
||||
downstreamLock.deallocate()
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
override func send() {
|
||||
lock.lock()
|
||||
let state = self.state
|
||||
lock.unlock()
|
||||
if state == .active {
|
||||
downstreamLock.lock()
|
||||
_ = downstream.receive()
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
if state == .initialized {
|
||||
state = .active
|
||||
}
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
parent?.remove(self)
|
||||
}
|
||||
|
||||
var description: String { return "ObservableObjectPublisher" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children = CollectionOfOne<Mirror.Child>(("downstream", downstream))
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any {
|
||||
return description
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
/// A subject that passes along values and completion.
|
||||
///
|
||||
/// Use a `PassthroughSubject` in unit tests when you want a publisher than can publish
|
||||
@@ -120,7 +118,7 @@ extension PassthroughSubject {
|
||||
}
|
||||
|
||||
fileprivate func request(_ demand: Subscribers.Demand) {
|
||||
precondition(demand > 0, "demand must not be zero")
|
||||
demand.assertNonZero()
|
||||
_parent?._lock.do {
|
||||
_demand += demand
|
||||
}
|
||||
|
||||
@@ -14,15 +14,18 @@
|
||||
/// of the property first.
|
||||
/// Note that the `@Published` property is class-constrained.
|
||||
/// Use it with properties of classes, not with non-class types like structures.
|
||||
@propertyWrapper public struct Published<Value> {
|
||||
@available(swift, introduced: 5.1)
|
||||
@propertyWrapper
|
||||
public struct Published<Value> {
|
||||
|
||||
/// Initialize the storage of the Published
|
||||
/// property as well as the corresponding `Publisher`.
|
||||
/// Initialize the storage of the `Published` property as well as the corresponding
|
||||
/// `Publisher`.
|
||||
public init(initialValue: Value) {
|
||||
value = initialValue
|
||||
self.init(wrappedValue: initialValue)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
/// Initialize the storage of the `Published` property as well as the corresponding
|
||||
/// `Publisher`.
|
||||
public init(wrappedValue: Value) {
|
||||
value = wrappedValue
|
||||
}
|
||||
@@ -30,21 +33,10 @@
|
||||
/// A publisher for properties marked with the `@Published` attribute.
|
||||
public struct Publisher: OpenCombine.Publisher {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Value
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Never
|
||||
|
||||
/// 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 Downstream.Input == Value, Downstream.Failure == Never
|
||||
{
|
||||
@@ -60,8 +52,12 @@
|
||||
|
||||
private var value: Value
|
||||
|
||||
/// The property that can be accessed with the
|
||||
/// `$` syntax and allows access to the `Publisher`
|
||||
private var publisher: Publisher?
|
||||
|
||||
internal var objectWillChange: ObservableObjectPublisher?
|
||||
|
||||
/// The property that can be accessed with the `$` syntax and allows access to
|
||||
/// the `Publisher`
|
||||
public var projectedValue: Publisher {
|
||||
mutating get {
|
||||
if let publisher = publisher {
|
||||
@@ -73,28 +69,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable, message:
|
||||
"@Published is only available on properties of classes")
|
||||
|
||||
// swiftlint:disable let_var_whitespace
|
||||
@available(*, unavailable, message: """
|
||||
@Published is only available on properties of classes
|
||||
""")
|
||||
public var wrappedValue: Value {
|
||||
get { value }
|
||||
set {
|
||||
value = newValue
|
||||
publisher?.subject.value = newValue
|
||||
}
|
||||
get { fatalError() }
|
||||
set { fatalError() } // swiftlint:disable:this unused_setter_value
|
||||
}
|
||||
// swiftlint:enable let_var_whitespace
|
||||
|
||||
private var publisher: Publisher?
|
||||
|
||||
@available(*, unavailable, message:
|
||||
"This subscript is unavailable in OpenCombine yet")
|
||||
public static subscript<EnclosingSelf: AnyObject>(
|
||||
_enclosingInstance object: EnclosingSelf,
|
||||
wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Value>,
|
||||
storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Published<Value>>
|
||||
) -> Value {
|
||||
get { fatalError() }
|
||||
set { fatalError() }
|
||||
get {
|
||||
return object[keyPath: storageKeyPath].value
|
||||
}
|
||||
set {
|
||||
object[keyPath: storageKeyPath].objectWillChange?.send()
|
||||
object[keyPath: storageKeyPath].publisher?.subject.send(newValue)
|
||||
object[keyPath: storageKeyPath].value = newValue
|
||||
}
|
||||
// TODO: Benchmark and explore a possibility to use _modify
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#else
|
||||
|
||||
@available(swift, introduced: 5.1)
|
||||
public typealias Published = Never
|
||||
|
||||
#endif // swift(>=5.1)
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ┃
|
||||
// ┃ Auto-generated from GYB template. DO NOT EDIT! ┃
|
||||
// ┃ ┃
|
||||
// ┃ ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
//
|
||||
// Publishers.Encode.swift.gyb
|
||||
//
|
||||
//
|
||||
// Created by Joseph Spadafora on 6/22/19.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Encodes the output from upstream using a specified `TopLevelEncoder`.
|
||||
/// For example, use `JSONEncoder`.
|
||||
public func encode<Coder: TopLevelEncoder>(
|
||||
encoder: Coder
|
||||
) -> Publishers.Encode<Self, Coder> {
|
||||
return .init(upstream: self, encoder: encoder)
|
||||
}
|
||||
|
||||
/// Decodes the output from upstream using a specified `TopLevelDecoder`.
|
||||
/// For example, use `JSONDecoder`.
|
||||
public func decode<Item: Decodable, Coder: TopLevelDecoder>(
|
||||
type: Item.Type,
|
||||
decoder: Coder
|
||||
) -> Publishers.Decode<Self, Item, Coder> where Output == Coder.Input {
|
||||
return .init(upstream: self, decoder: decoder)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
public struct Encode<Upstream: Publisher, Coder: TopLevelEncoder>: Publisher
|
||||
where Upstream.Output: Encodable
|
||||
{
|
||||
public typealias Failure = Error
|
||||
|
||||
public typealias Output = Coder.Output
|
||||
|
||||
public let upstream: Upstream
|
||||
|
||||
private let _encode: (Upstream.Output) throws -> Output
|
||||
|
||||
public init(upstream: Upstream, encoder: Coder) {
|
||||
self.upstream = upstream
|
||||
self._encode = encoder.encode
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, encode: _encode))
|
||||
}
|
||||
}
|
||||
|
||||
public struct Decode<Upstream: Publisher, Output: Decodable, Coder: TopLevelDecoder>
|
||||
: Publisher
|
||||
where Upstream.Output == Coder.Input
|
||||
{
|
||||
public typealias Failure = Error
|
||||
|
||||
public let upstream: Upstream
|
||||
|
||||
private let _decode: (Upstream.Output) throws -> Output
|
||||
|
||||
public init(upstream: Upstream, decoder: Coder) {
|
||||
self.upstream = upstream
|
||||
self._decode = { try decoder.decode(Output.self, from: $0) }
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, decode: _decode))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Encode {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Output, Downstream.Failure == Error
|
||||
{
|
||||
// NOTE: This class has been audited for thread safety.
|
||||
// Combine doesn't use any locking here.
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let encode: (Upstream.Output) throws -> Output
|
||||
|
||||
private var finished = false
|
||||
|
||||
private var subscription: Subscription?
|
||||
|
||||
fileprivate init(
|
||||
downstream: Downstream,
|
||||
encode: @escaping (Upstream.Output) throws -> Output
|
||||
) {
|
||||
self.downstream = downstream
|
||||
self.encode = encode
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
if finished || self.subscription != nil {
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
self.subscription = subscription
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
if finished { return .none }
|
||||
do {
|
||||
return try downstream.receive(encode(input))
|
||||
} catch {
|
||||
finished = true
|
||||
subscription?.cancel()
|
||||
subscription = nil
|
||||
downstream.receive(completion: .failure(error))
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
if finished { return }
|
||||
finished = true
|
||||
subscription = nil
|
||||
downstream.receive(completion: completion.eraseError())
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
subscription?.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
guard let subscription = self.subscription, !finished else { return }
|
||||
subscription.cancel()
|
||||
self.subscription = nil
|
||||
finished = true
|
||||
}
|
||||
|
||||
var description: String { return "Encode" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("downstream", downstream),
|
||||
("finished", finished),
|
||||
("upstreamSubscription", subscription as Any)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Decode {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Output, Downstream.Failure == Error
|
||||
{
|
||||
// NOTE: This class has been audited for thread safety.
|
||||
// Combine doesn't use any locking here.
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let decode: (Upstream.Output) throws -> Output
|
||||
|
||||
private var finished = false
|
||||
|
||||
private var subscription: Subscription?
|
||||
|
||||
fileprivate init(
|
||||
downstream: Downstream,
|
||||
decode: @escaping (Upstream.Output) throws -> Output
|
||||
) {
|
||||
self.downstream = downstream
|
||||
self.decode = decode
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
if finished || self.subscription != nil {
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
self.subscription = subscription
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
if finished { return .none }
|
||||
do {
|
||||
return try downstream.receive(decode(input))
|
||||
} catch {
|
||||
finished = true
|
||||
subscription?.cancel()
|
||||
subscription = nil
|
||||
downstream.receive(completion: .failure(error))
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
if finished { return }
|
||||
finished = true
|
||||
subscription = nil
|
||||
downstream.receive(completion: completion.eraseError())
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
subscription?.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
guard let subscription = self.subscription, !finished else { return }
|
||||
subscription.cancel()
|
||||
self.subscription = nil
|
||||
finished = true
|
||||
}
|
||||
|
||||
var description: String { return "Decode" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("downstream", downstream),
|
||||
("finished", finished),
|
||||
("upstreamSubscription", subscription as Any)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -249,6 +249,26 @@ extension Just {
|
||||
) -> Result<ElementOfResult, Error>.OCombine.Publisher {
|
||||
return .init(Result { try nextPartialResult(initialResult, output) })
|
||||
}
|
||||
|
||||
public func prepend(_ elements: Output...) -> Publishers.Sequence<[Output], Never> {
|
||||
return prepend(elements)
|
||||
}
|
||||
|
||||
public func prepend<Elements: Sequence>(
|
||||
_ elements: Elements
|
||||
) -> Publishers.Sequence<[Output], Never> where Output == Elements.Element {
|
||||
return .init(sequence: elements + [output])
|
||||
}
|
||||
|
||||
public func append(_ elements: Output...) -> Publishers.Sequence<[Output], Never> {
|
||||
return append(elements)
|
||||
}
|
||||
|
||||
public func append<Elements: Sequence>(
|
||||
_ elements: Elements
|
||||
) -> Publishers.Sequence<[Output], Never> where Output == Elements.Element {
|
||||
return .init(sequence: [output] + elements)
|
||||
}
|
||||
}
|
||||
|
||||
extension Just {
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
//
|
||||
// OperatorSubscription.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 26.06.2019.
|
||||
//
|
||||
|
||||
internal class OperatorSubscription<Downstream: Subscriber>: CustomReflectable {
|
||||
internal var downstream: Downstream?
|
||||
internal var upstreamSubscription: Subscription?
|
||||
|
||||
internal var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
internal init(downstream: Downstream) {
|
||||
self.downstream = downstream
|
||||
}
|
||||
|
||||
internal func cancel() {
|
||||
upstreamSubscription?.cancel()
|
||||
upstreamSubscription = nil
|
||||
downstream = nil
|
||||
}
|
||||
}
|
||||
@@ -239,7 +239,9 @@ extension Optional.OCombine.Publisher {
|
||||
|
||||
// I don't know why, but Combine has this precondition
|
||||
precondition(range.upperBound < .max - 1)
|
||||
return .init(output.flatMap { range.contains(0) ? $0 : nil })
|
||||
return .init(
|
||||
output.flatMap { (range.lowerBound == 0 && range.upperBound != 0) ? $0 : nil }
|
||||
)
|
||||
}
|
||||
|
||||
public func prefix(_ maxLength: Int) -> Optional<Output>.OCombine.Publisher {
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
//
|
||||
// Publishers.AssertNoFailure.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 25.12.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Raises a fatal error when its upstream publisher fails, and otherwise republishes
|
||||
/// all received input.
|
||||
///
|
||||
/// Use this function for internal sanity checks that are active during testing but
|
||||
/// do not impact performance of shipping code.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - prefix: A string used at the beginning of the fatal error message.
|
||||
/// - file: A filename used in the error message. This defaults to `#file`.
|
||||
/// - line: A line number used in the error message. This defaults to `#line`.
|
||||
/// - Returns: A publisher that raises a fatal error when its upstream publisher
|
||||
/// fails.
|
||||
public func assertNoFailure(_ prefix: String = "",
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line) -> Publishers.AssertNoFailure<Self> {
|
||||
return .init(upstream: self, prefix: prefix, file: file, line: line)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that raises a fatal error upon receiving any failure, and otherwise
|
||||
/// republishes all received input.
|
||||
///
|
||||
/// Use this function for internal sanity checks that are active during testing but
|
||||
/// do not impact performance of shipping code.
|
||||
public struct AssertNoFailure<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Never
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The string used at the beginning of the fatal error message.
|
||||
public let prefix: String
|
||||
|
||||
/// The filename used in the error message.
|
||||
public let file: StaticString
|
||||
|
||||
/// The line number used in the error message.
|
||||
public let line: UInt
|
||||
|
||||
public init(upstream: Upstream, prefix: String, file: StaticString, line: UInt) {
|
||||
self.upstream = upstream
|
||||
self.prefix = prefix
|
||||
self.file = file
|
||||
self.line = line
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Never
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber,
|
||||
prefix: prefix,
|
||||
file: file,
|
||||
line: line))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.AssertNoFailure {
|
||||
private struct Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Never
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let prefix: String
|
||||
|
||||
private let file: StaticString
|
||||
|
||||
private let line: UInt
|
||||
|
||||
let combineIdentifier = CombineIdentifier()
|
||||
|
||||
init(downstream: Downstream, prefix: String, file: StaticString, line: UInt) {
|
||||
self.downstream = downstream
|
||||
self.prefix = prefix
|
||||
self.file = file
|
||||
self.line = line
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
downstream.receive(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return downstream.receive(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
downstream.receive(completion: .finished)
|
||||
case .failure(let error):
|
||||
let prefix = self.prefix.isEmpty ? "" : self.prefix + ": "
|
||||
fatalError("\(prefix)\(error)", file: file, line: line)
|
||||
}
|
||||
}
|
||||
|
||||
var description: String { return "AssertNoFailure" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("file", file),
|
||||
("line", line),
|
||||
("prefix", prefix)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 18/09/2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension ConnectablePublisher {
|
||||
|
||||
/// Automates the process of connecting or disconnecting from this connectable
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
//
|
||||
// Publishers.Breakpoint.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 03.12.2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Raises a debugger signal when a provided closure needs to stop the process in
|
||||
/// the debugger.
|
||||
///
|
||||
/// When any of the provided closures returns `true`, this publisher raises
|
||||
/// the `SIGTRAP` signal to stop the process in the debugger.
|
||||
/// Otherwise, this publisher passes through values and completions as-is.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - receiveSubscription: A closure that executes when when the publisher receives
|
||||
/// a subscription. Return `true` from this closure to raise `SIGTRAP`, or `false`
|
||||
/// to continue.
|
||||
/// - receiveOutput: A closure that executes when when the publisher receives
|
||||
/// a value. Return `true` from this closure to raise `SIGTRAP`, or `false`
|
||||
/// to continue.
|
||||
/// - receiveCompletion: A closure that executes when when the publisher receives
|
||||
/// a completion. Return `true` from this closure to raise `SIGTRAP`, or `false`
|
||||
/// to continue.
|
||||
/// - Returns: A publisher that raises a debugger signal when one of the provided
|
||||
/// closures returns `true`.
|
||||
public func breakpoint(
|
||||
receiveSubscription: ((Subscription) -> Bool)? = nil,
|
||||
receiveOutput: ((Output) -> Bool)? = nil,
|
||||
receiveCompletion: ((Subscribers.Completion<Failure>) -> Bool)? = nil
|
||||
) -> Publishers.Breakpoint<Self> {
|
||||
return .init(upstream: self,
|
||||
receiveSubscription: receiveSubscription,
|
||||
receiveOutput: receiveOutput,
|
||||
receiveCompletion: receiveCompletion)
|
||||
}
|
||||
|
||||
/// Raises a debugger signal upon receiving a failure.
|
||||
///
|
||||
/// When the upstream publisher fails with an error, this publisher raises
|
||||
/// the `SIGTRAP` signal, which stops the process in the debugger.
|
||||
/// Otherwise, this publisher passes through values and completions as-is.
|
||||
///
|
||||
/// - Returns: A publisher that raises a debugger signal upon receiving a failure.
|
||||
public func breakpointOnError() -> Publishers.Breakpoint<Self> {
|
||||
return breakpoint { completion in
|
||||
switch completion {
|
||||
case .finished:
|
||||
return false
|
||||
case .failure:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that raises a debugger signal when a provided closure needs to stop
|
||||
/// the process in the debugger.
|
||||
///
|
||||
/// When any of the provided closures returns `true`, this publisher raises
|
||||
/// the `SIGTRAP` signal to stop the process in the debugger.
|
||||
/// Otherwise, this publisher passes through values and completions as-is.
|
||||
public struct Breakpoint<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
|
||||
|
||||
/// A closure that executes when the publisher receives a subscription, and can
|
||||
/// raise a debugger signal by returning a `true` Boolean value.
|
||||
public let receiveSubscription: ((Subscription) -> Bool)?
|
||||
|
||||
/// A closure that executes when the publisher receives output from the upstream
|
||||
/// publisher, and can raise a debugger signal by returning a `true` Boolean
|
||||
/// value.
|
||||
public let receiveOutput: ((Upstream.Output) -> Bool)?
|
||||
|
||||
/// A closure that executes when the publisher receives completion, and can raise
|
||||
/// a debugger signal by returning a `true` Boolean value.
|
||||
public let receiveCompletion:
|
||||
((Subscribers.Completion<Upstream.Failure>) -> Bool)?
|
||||
|
||||
/// Creates a breakpoint publisher with the provided upstream publisher and
|
||||
/// breakpoint-raising closures.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - upstream: The publisher from which this publisher receives elements.
|
||||
/// - receiveSubscription: A closure that executes when the publisher receives
|
||||
/// a subscription, and can raise a debugger signal by returning a `true`
|
||||
/// Boolean value.
|
||||
/// - receiveOutput: A closure that executes when the publisher receives output
|
||||
/// from the upstream publisher, and can raise a debugger signal by returning
|
||||
/// a `true` Boolean value.
|
||||
/// - receiveCompletion: A closure that executes when the publisher receives
|
||||
/// completion, and can raise a debugger signal by returning a `true` Boolean
|
||||
/// value.
|
||||
public init(
|
||||
upstream: Upstream,
|
||||
receiveSubscription: ((Subscription) -> Bool)? = nil,
|
||||
receiveOutput: ((Upstream.Output) -> Bool)? = nil,
|
||||
receiveCompletion: ((Subscribers.Completion<Failure>) -> Bool)? = nil
|
||||
) {
|
||||
self.upstream = upstream
|
||||
self.receiveSubscription = receiveSubscription
|
||||
self.receiveOutput = receiveOutput
|
||||
self.receiveCompletion = receiveCompletion
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
upstream.subscribe(Inner(self, downstream: subscriber))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Breakpoint {
|
||||
private struct Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
private let breakpoint: Publishers.Breakpoint<Upstream>
|
||||
|
||||
let combineIdentifier = CombineIdentifier()
|
||||
|
||||
init(_ breakpoint: Publishers.Breakpoint<Upstream>,
|
||||
downstream: Downstream) {
|
||||
self.downstream = downstream
|
||||
self.breakpoint = breakpoint
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
if breakpoint.receiveSubscription?(subscription) == true {
|
||||
__stopInDebugger()
|
||||
}
|
||||
downstream.receive(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
if breakpoint.receiveOutput?(input) == true {
|
||||
__stopInDebugger()
|
||||
}
|
||||
return downstream.receive(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
if breakpoint.receiveCompletion?(completion) == true {
|
||||
__stopInDebugger()
|
||||
}
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
var description: String { return "Breakpoint" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children = CollectionOfOne<Mirror.Child>(
|
||||
("upstream", breakpoint.upstream)
|
||||
)
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
//
|
||||
// Publishers.Buffer.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 08.01.2020.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Buffers elements received from an upstream publisher.
|
||||
/// - Parameter size: The maximum number of elements to store.
|
||||
/// - Parameter prefetch: The strategy for initially populating the buffer.
|
||||
/// - Parameter whenFull: The action to take when the buffer becomes full.
|
||||
public func buffer(
|
||||
size: Int,
|
||||
prefetch: Publishers.PrefetchStrategy,
|
||||
whenFull: Publishers.BufferingStrategy<Failure>
|
||||
) -> Publishers.Buffer<Self> {
|
||||
return .init(upstream: self,
|
||||
size: size,
|
||||
prefetch: prefetch,
|
||||
whenFull: whenFull)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A strategy for filling a buffer.
|
||||
///
|
||||
/// * keepFull: A strategy to fill the buffer at subscription time, and keep it full
|
||||
/// thereafter.
|
||||
/// * byRequest: A strategy that avoids prefetching and instead performs requests
|
||||
/// on demand.
|
||||
public enum PrefetchStrategy {
|
||||
|
||||
/// A strategy to fill the buffer at subscription time, and keep it full
|
||||
/// thereafter.
|
||||
case keepFull
|
||||
|
||||
/// A strategy that avoids prefetching and instead performs requests
|
||||
/// on demand.
|
||||
case byRequest
|
||||
}
|
||||
|
||||
/// A strategy for handling exhaustion of a buffer’s capacity.
|
||||
///
|
||||
/// * dropNewest: When full, discard the newly-received element without buffering it.
|
||||
/// * dropOldest: When full, remove the least recently-received element from the
|
||||
/// buffer.
|
||||
/// * customError: When full, execute the closure to provide a custom error.
|
||||
public enum BufferingStrategy<Failure: Error> {
|
||||
|
||||
/// When full, discard the newly-received element without buffering it.
|
||||
case dropNewest
|
||||
|
||||
/// When full, remove the least recently-received element from the buffer.
|
||||
case dropOldest
|
||||
|
||||
/// When full, execute the closure to provide a custom error.
|
||||
case customError(() -> Failure)
|
||||
}
|
||||
|
||||
/// A publisher that buffers elements received from an upstream publisher.
|
||||
public struct Buffer<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 elements to store.
|
||||
public let size: Int
|
||||
|
||||
/// The strategy for initially populating the buffer.
|
||||
public let prefetch: Publishers.PrefetchStrategy
|
||||
|
||||
/// The action to take when the buffer becomes full.
|
||||
public let whenFull: Publishers.BufferingStrategy<Failure>
|
||||
|
||||
/// Creates a publisher that buffers elements received from an upstream publisher.
|
||||
/// - Parameter upstream: The publisher from which this publisher receives
|
||||
/// elements.
|
||||
/// - Parameter size: The maximum number of elements to store.
|
||||
/// - Parameter prefetch: The strategy for initially populating the buffer.
|
||||
/// - Parameter whenFull: The action to take when the buffer becomes full.
|
||||
public init(upstream: Upstream,
|
||||
size: Int,
|
||||
prefetch: Publishers.PrefetchStrategy,
|
||||
whenFull: Publishers.BufferingStrategy<Failure>) {
|
||||
self.upstream = upstream
|
||||
self.size = size
|
||||
self.prefetch = prefetch
|
||||
self.whenFull = whenFull
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, buffer: self))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.PrefetchStrategy: Equatable {}
|
||||
|
||||
extension Publishers.PrefetchStrategy: Hashable {}
|
||||
|
||||
extension Publishers.Buffer {
|
||||
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 ready(Publishers.Buffer<Upstream>, Downstream)
|
||||
case subscribed(Publishers.Buffer<Upstream>, Downstream, Subscription)
|
||||
case terminal
|
||||
}
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private var recursion = false
|
||||
|
||||
private var state: State
|
||||
|
||||
private var downstreamDemand = Subscribers.Demand.none
|
||||
|
||||
// TODO: Use a deque here?
|
||||
// Need to measure performance with large buffers and `dropOldest` strategy.
|
||||
private var values = [Input]()
|
||||
|
||||
private var upstreamFailed = false
|
||||
|
||||
private var terminal: Subscribers.Completion<Failure>?
|
||||
|
||||
init(downstream: Downstream, buffer: Publishers.Buffer<Upstream>) {
|
||||
state = .ready(buffer, downstream)
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case let .ready(buffer, downstream) = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .subscribed(buffer, downstream, subscription)
|
||||
lock.unlock()
|
||||
|
||||
let upstreamDemand: Subscribers.Demand
|
||||
switch buffer.prefetch {
|
||||
case .keepFull:
|
||||
upstreamDemand = .max(buffer.size)
|
||||
case .byRequest:
|
||||
upstreamDemand = .unlimited
|
||||
}
|
||||
subscription.request(upstreamDemand)
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case let .subscribed(buffer, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
switch terminal {
|
||||
case nil, .finished?:
|
||||
if values.count >= buffer.size {
|
||||
switch buffer.whenFull {
|
||||
case .dropNewest:
|
||||
lock.unlock()
|
||||
return drain()
|
||||
case .dropOldest:
|
||||
values.removeFirst()
|
||||
case let .customError(makeError):
|
||||
terminal = .failure(makeError())
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
values.append(input)
|
||||
lock.unlock()
|
||||
return drain()
|
||||
case .failure?:
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
lock.lock()
|
||||
guard case .subscribed = state, terminal == nil else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
terminal = completion
|
||||
lock.unlock()
|
||||
_ = drain()
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
downstreamDemand += demand
|
||||
let recursion = self.recursion
|
||||
lock.unlock()
|
||||
if recursion {
|
||||
return
|
||||
}
|
||||
|
||||
// Request the number of items just enough to fill the buffer.
|
||||
subscription.request(drain() + demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
values = []
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
private func drain() -> Subscribers.Demand {
|
||||
var upstreamDemand = Subscribers.Demand.none
|
||||
lock.lock()
|
||||
while true {
|
||||
guard case let .subscribed(buffer, downstream, _) = state,
|
||||
downstreamDemand > 0 else {
|
||||
lock.unlock()
|
||||
return upstreamDemand
|
||||
}
|
||||
|
||||
if values.isEmpty {
|
||||
if let completion = terminal {
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
return upstreamDemand
|
||||
}
|
||||
|
||||
let poppedValues = lockedPop(downstreamDemand)
|
||||
assert(poppedValues.count > 0,
|
||||
"""
|
||||
We check that the buffer is not empty and downstreamDemand is \
|
||||
nonzero, how can this be triggered?
|
||||
""")
|
||||
|
||||
// This should not crash because `lockedPop(_:)` returns at most
|
||||
// `downstreamDemand` items.
|
||||
downstreamDemand -= poppedValues.count
|
||||
|
||||
recursion = true
|
||||
lock.unlock()
|
||||
|
||||
var newDownstreamDemand = Subscribers.Demand.none
|
||||
var additionalUpstreamDemand = 0
|
||||
|
||||
for value in poppedValues {
|
||||
newDownstreamDemand += downstream.receive(value)
|
||||
additionalUpstreamDemand += 1
|
||||
}
|
||||
|
||||
if buffer.prefetch == .keepFull {
|
||||
upstreamDemand += additionalUpstreamDemand
|
||||
}
|
||||
|
||||
lock.lock()
|
||||
recursion = false
|
||||
downstreamDemand += newDownstreamDemand
|
||||
}
|
||||
}
|
||||
|
||||
private func lockedPop(_ demand: Subscribers.Demand) -> [Input] {
|
||||
assert(demand > 0)
|
||||
guard let max = demand.max else {
|
||||
let poppedValues = self.values
|
||||
self.values = []
|
||||
return poppedValues
|
||||
}
|
||||
|
||||
let poppedValues = Array(values.prefix(max))
|
||||
values.removeFirst(poppedValues.count)
|
||||
return poppedValues
|
||||
}
|
||||
|
||||
var description: String { return "Buffer" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("values", values),
|
||||
("state", state),
|
||||
("downstreamDemand", downstreamDemand),
|
||||
("terminal", terminal as Any)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
//
|
||||
// Publishers.CollectByCount.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 24.12.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Collects up to the specified number of elements, and then emits a single array of
|
||||
/// the collection.
|
||||
///
|
||||
/// If the upstream publisher finishes before filling the buffer, this publisher sends
|
||||
/// an array of all the items it has received. This may be fewer than `count`
|
||||
/// elements.
|
||||
/// If the upstream publisher fails with an error, this publisher forwards the error
|
||||
/// to the downstream receiver instead of sending its output.
|
||||
/// Note: When this publisher receives a request for `.max(n)` elements, it requests
|
||||
/// `.max(count * n)` from the upstream publisher.
|
||||
///
|
||||
/// - Parameter count: The maximum number of received elements to buffer before
|
||||
/// publishing.
|
||||
/// - Returns: A publisher that collects up to the specified number of elements, and
|
||||
/// then publishes them as an array.
|
||||
public func collect(_ count: Int) -> Publishers.CollectByCount<Self> {
|
||||
return .init(upstream: self, count: count)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that buffers a maximum number of items.
|
||||
public struct CollectByCount<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 received elements to buffer before publishing.
|
||||
public let count: Int
|
||||
|
||||
public init(upstream: Upstream, count: Int) {
|
||||
self.upstream = upstream
|
||||
self.count = count
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == Failure, Downstream.Input == Output
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, count: count))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.CollectByCount: Equatable where Upstream: Equatable {}
|
||||
|
||||
extension Publishers.CollectByCount {
|
||||
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 let downstream: Downstream
|
||||
|
||||
private let count: Int
|
||||
|
||||
private var buffer: [Input] = []
|
||||
|
||||
private var subscription: Subscription?
|
||||
|
||||
private var finished = false
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
init(downstream: Downstream, count: Int) {
|
||||
self.downstream = downstream
|
||||
self.count = count
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
if finished || self.subscription != nil {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
self.subscription = subscription
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
if subscription == nil {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
buffer.append(input)
|
||||
guard buffer.count == count else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
let output = self.buffer
|
||||
self.buffer = []
|
||||
lock.unlock()
|
||||
return downstream.receive(output) * count
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
lock.lock()
|
||||
subscription = nil
|
||||
finished = true
|
||||
switch completion {
|
||||
case .finished:
|
||||
if buffer.isEmpty {
|
||||
lock.unlock()
|
||||
} else {
|
||||
let buffer = self.buffer
|
||||
self.buffer = []
|
||||
lock.unlock()
|
||||
_ = downstream.receive(buffer)
|
||||
}
|
||||
case .failure:
|
||||
buffer = []
|
||||
lock.unlock()
|
||||
}
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
lock.lock()
|
||||
if let subscription = self.subscription {
|
||||
lock.unlock()
|
||||
subscription.request(demand * count)
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
if let subscription = self.subscription {
|
||||
buffer = []
|
||||
finished = true
|
||||
self.subscription = nil
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
var description: String { return "CollectByCount" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
let children: [Mirror.Child] = [
|
||||
("downstream", downstream),
|
||||
("upstreamSubscription", subscription as Any),
|
||||
("buffer", buffer),
|
||||
("count", count)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -5,65 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 11.07.2019.
|
||||
//
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that republishes all non-`nil` results of calling a closure
|
||||
/// with each received element.
|
||||
public struct CompactMap<Upstream: Publisher, Output>: Publisher {
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A closure that receives values from the upstream publisher
|
||||
/// and returns optional values.
|
||||
public let transform: (Upstream.Output) -> Output?
|
||||
|
||||
public init(upstream: Upstream,
|
||||
transform: @escaping (Upstream.Output) -> Output?) {
|
||||
self.upstream = upstream
|
||||
self.transform = transform
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, transform: catching(transform))
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that republishes all non-`nil` results of calling an error-throwing
|
||||
/// closure with each received element.
|
||||
public struct TryCompactMap<Upstream: Publisher, Output>: Publisher {
|
||||
|
||||
public typealias Failure = Error
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// An error-throwing closure that receives values from the upstream publisher
|
||||
/// and returns optional values.
|
||||
///
|
||||
/// If this closure throws an error, the publisher fails.
|
||||
public let transform: (Upstream.Output) throws -> Output?
|
||||
|
||||
public init(upstream: Upstream,
|
||||
transform: @escaping (Upstream.Output) throws -> Output?) {
|
||||
self.upstream = upstream
|
||||
self.transform = transform
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, transform: catching(transform))
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Calls a closure with each received element and publishes any returned
|
||||
@@ -123,89 +64,105 @@ extension Publishers.TryCompactMap {
|
||||
}
|
||||
}
|
||||
|
||||
private class _CompactMap<Upstream: Publisher, Downstream: Subscriber>
|
||||
: OperatorSubscription<Downstream>,
|
||||
Subscription
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
typealias Transform = (Input) -> Result<Downstream.Input?, Downstream.Failure>
|
||||
extension Publishers {
|
||||
|
||||
fileprivate var _transform: Transform?
|
||||
/// A publisher that republishes all non-`nil` results of calling a closure
|
||||
/// with each received element.
|
||||
public struct CompactMap<Upstream: Publisher, Output>: Publisher {
|
||||
|
||||
var _isCompleted: Bool {
|
||||
return _transform == nil
|
||||
}
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
init(downstream: Downstream, transform: @escaping Transform) {
|
||||
_transform = transform
|
||||
super.init(downstream: downstream)
|
||||
}
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
/// A closure that receives values from the upstream publisher
|
||||
/// and returns optional values.
|
||||
public let transform: (Upstream.Output) -> Output?
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
guard let transform = _transform else { return .none }
|
||||
public init(upstream: Upstream,
|
||||
transform: @escaping (Upstream.Output) -> Output?) {
|
||||
self.upstream = upstream
|
||||
self.transform = transform
|
||||
}
|
||||
|
||||
switch transform(input) {
|
||||
case .success(let output?):
|
||||
return downstream.receive(output)
|
||||
case .success(nil):
|
||||
return .max(1)
|
||||
case .failure(let error):
|
||||
downstream.receive(completion: .failure(error))
|
||||
_transform = nil
|
||||
return .none
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, filter: transform))
|
||||
}
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
guard !_isCompleted else { return }
|
||||
upstreamSubscription?.request(demand)
|
||||
}
|
||||
/// A publisher that republishes all non-`nil` results of calling an error-throwing
|
||||
/// closure with each received element.
|
||||
public struct TryCompactMap<Upstream: Publisher, Output>: Publisher {
|
||||
|
||||
override func cancel() {
|
||||
_transform = nil
|
||||
upstreamSubscription?.cancel()
|
||||
upstreamSubscription = nil
|
||||
public typealias Failure = Error
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// An error-throwing closure that receives values from the upstream publisher
|
||||
/// and returns optional values.
|
||||
///
|
||||
/// If this closure throws an error, the publisher fails.
|
||||
public let transform: (Upstream.Output) throws -> Output?
|
||||
|
||||
public init(upstream: Upstream,
|
||||
transform: @escaping (Upstream.Output) throws -> Output?) {
|
||||
self.upstream = upstream
|
||||
self.transform = transform
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, filter: transform))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.CompactMap {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: _CompactMap<Upstream, Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible
|
||||
where Downstream.Failure == Upstream.Failure
|
||||
: FilterProducer<Downstream,
|
||||
Upstream.Output,
|
||||
Output,
|
||||
Upstream.Failure,
|
||||
(Upstream.Output) -> Output?>
|
||||
where Downstream.Failure == Upstream.Failure, Downstream.Input == Output
|
||||
{
|
||||
var description: String { return "CompactMap" }
|
||||
// NOTE: This class has been audited for thread safety
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
if !_isCompleted {
|
||||
_transform = nil
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Output?, Downstream.Failure> {
|
||||
return .continue(filter(newValue))
|
||||
}
|
||||
|
||||
override var description: String { return "CompactMap" }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryCompactMap {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: _CompactMap<Upstream, Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible
|
||||
where Downstream.Failure == Error
|
||||
: FilterProducer<Downstream,
|
||||
Upstream.Output,
|
||||
Output,
|
||||
Upstream.Failure,
|
||||
(Upstream.Output) throws -> Output?>
|
||||
where Downstream.Failure == Error, Downstream.Input == Output
|
||||
{
|
||||
var description: String { return "TryCompactMap" }
|
||||
// NOTE: This class has been audited for thread safety
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
if !_isCompleted {
|
||||
_transform = nil
|
||||
downstream.receive(completion: completion.eraseError())
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Output?, Error> {
|
||||
do {
|
||||
return try .continue(filter(newValue))
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
override var description: String { return "TryCompactMap" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ extension Publisher {
|
||||
/// - Returns: A publisher that publishes the maximum value received from the upstream
|
||||
/// publisher, after the upstream publisher finishes.
|
||||
public func tryMax(
|
||||
by areInIncreasingOrder: @escaping (Self.Output, Self.Output) throws -> Bool
|
||||
by areInIncreasingOrder: @escaping (Output, Output) throws -> Bool
|
||||
) -> Publishers.TryComparison<Self> {
|
||||
return .init(upstream: self, areInIncreasingOrder: areInIncreasingOrder)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
//
|
||||
// Publishers.Concatenate.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 24.10.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Prefixes a `Publisher`'s output with the specified sequence.
|
||||
///
|
||||
/// - Parameter elements: The elements to publish before this publisher’s elements.
|
||||
/// - Returns: A publisher that prefixes the specified elements prior to this
|
||||
/// publisher’s elements.
|
||||
public func prepend(
|
||||
_ elements: Output...
|
||||
) -> Publishers.Concatenate<Publishers.Sequence<[Output], Failure>, Self> {
|
||||
return prepend(elements)
|
||||
}
|
||||
|
||||
/// Prefixes a `Publisher`'s output with the specified sequence.
|
||||
///
|
||||
/// - Parameter elements: A sequence of elements to publish before this publisher’s
|
||||
/// elements.
|
||||
/// - Returns: A publisher that prefixes the sequence of elements prior to this
|
||||
/// publisher’s elements.
|
||||
public func prepend<Elements: Sequence>(
|
||||
_ elements: Elements
|
||||
) -> Publishers.Concatenate<Publishers.Sequence<Elements, Failure>, Self>
|
||||
where Output == Elements.Element
|
||||
{
|
||||
return prepend(.init(sequence: elements))
|
||||
}
|
||||
|
||||
/// Prefixes this publisher’s output with the elements emitted by the given publisher.
|
||||
///
|
||||
/// The resulting publisher doesn’t emit any elements until the prefixing publisher
|
||||
/// finishes.
|
||||
///
|
||||
/// - Parameter publisher: The prefixing publisher.
|
||||
/// - Returns: A publisher that prefixes the prefixing publisher’s elements prior to
|
||||
/// this publisher’s elements.
|
||||
public func prepend<Prefix: Publisher>(
|
||||
_ publisher: Prefix
|
||||
) -> Publishers.Concatenate<Prefix, Self>
|
||||
where Failure == Prefix.Failure, Output == Prefix.Output
|
||||
{
|
||||
return .init(prefix: publisher, suffix: self)
|
||||
}
|
||||
|
||||
/// Append a `Publisher`'s output with the specified sequence.
|
||||
public func append(
|
||||
_ elements: Output...
|
||||
) -> Publishers.Concatenate<Self, Publishers.Sequence<[Output], Failure>> {
|
||||
return append(elements)
|
||||
}
|
||||
|
||||
/// Appends a `Publisher`'s output with the specified sequence.
|
||||
public func append<Elements: Sequence>(
|
||||
_ elements: Elements
|
||||
) -> Publishers.Concatenate<Self, Publishers.Sequence<Elements, Failure>>
|
||||
where Output == Elements.Element
|
||||
{
|
||||
return append(.init(sequence: elements))
|
||||
}
|
||||
|
||||
/// Appends this publisher’s output with the elements emitted by the given publisher.
|
||||
///
|
||||
/// This operator produces no elements until this publisher finishes. It then produces
|
||||
/// this publisher’s elements, followed by the given publisher’s elements.
|
||||
/// If this publisher fails with an error, the prefixing publisher does not publish
|
||||
/// the provided publisher’s elements.
|
||||
///
|
||||
/// - Parameter publisher: The appending publisher.
|
||||
/// - Returns: A publisher that appends the appending publisher’s elements after this
|
||||
/// publisher’s elements.
|
||||
public func append<Suffix: Publisher>(
|
||||
_ publisher: Suffix
|
||||
) -> Publishers.Concatenate<Self, Suffix>
|
||||
where Suffix.Failure == Failure, Suffix.Output == Output
|
||||
{
|
||||
return .init(prefix: self, suffix: publisher)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that emits all of one publisher’s elements before those from anothe
|
||||
/// publisher.
|
||||
public struct Concatenate<Prefix: Publisher, Suffix: Publisher>: Publisher
|
||||
where Prefix.Failure == Suffix.Failure, Prefix.Output == Suffix.Output
|
||||
{
|
||||
public typealias Output = Suffix.Output
|
||||
|
||||
public typealias Failure = Suffix.Failure
|
||||
|
||||
/// The publisher to republish, in its entirety, before republishing elements from
|
||||
/// `suffix`.
|
||||
public let prefix: Prefix
|
||||
|
||||
/// The publisher to republish only after `prefix` finishes.
|
||||
public let suffix: Suffix
|
||||
|
||||
public init(prefix: Prefix, suffix: Suffix) {
|
||||
self.prefix = prefix
|
||||
self.suffix = suffix
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Suffix.Failure == Downstream.Failure, Suffix.Output == Downstream.Input
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, suffix: suffix)
|
||||
prefix.subscribe(inner)
|
||||
subscriber.receive(subscription: inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Concatenate: Equatable where Prefix: Equatable, Suffix: Equatable {}
|
||||
|
||||
extension Publishers.Concatenate {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Suffix.Output, Downstream.Failure == Suffix.Failure
|
||||
{
|
||||
typealias Input = Suffix.Output
|
||||
|
||||
typealias Failure = Suffix.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let suffix: Suffix
|
||||
|
||||
private var prefixFinished = false
|
||||
|
||||
private var demand = Subscribers.Demand.none
|
||||
|
||||
private var upstream: Subscription?
|
||||
|
||||
private var expectedSubscriptions = 2
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private let downstreamLock = UnfairRecursiveLock.allocate()
|
||||
|
||||
fileprivate init(downstream: Downstream, suffix: Suffix) {
|
||||
self.downstream = downstream
|
||||
self.suffix = suffix
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
downstreamLock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard upstream == nil, expectedSubscriptions > 0 else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
upstream = subscription
|
||||
expectedSubscriptions -= 1
|
||||
let demand = self.demand
|
||||
lock.unlock()
|
||||
if demand > 0 {
|
||||
subscription.request(demand)
|
||||
}
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
demand -= 1
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
let newDemand = downstream.receive(input)
|
||||
downstreamLock.unlock()
|
||||
lock.lock()
|
||||
demand += newDemand
|
||||
lock.unlock()
|
||||
return newDemand
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
// Reading prefixFinished should be locked. Combine doesn't lock here.
|
||||
if prefixFinished {
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
return
|
||||
}
|
||||
|
||||
guard case .finished = completion else {
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
return
|
||||
}
|
||||
|
||||
prefixFinished = true // Should be locked as well?
|
||||
lock.lock()
|
||||
upstream = nil
|
||||
lock.unlock()
|
||||
suffix.subscribe(self)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
self.demand += demand
|
||||
guard let subscription = upstream else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard let subscription = upstream else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
upstream = nil
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "Concatenate" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("downstream", downstream),
|
||||
("upstreamSubscription", upstream as Any),
|
||||
("suffix", suffix),
|
||||
("demand", demand)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
//
|
||||
// Publishers.Decode.swift
|
||||
//
|
||||
//
|
||||
// Created by Joseph Spadafora on 6/21/19.
|
||||
//
|
||||
|
||||
extension Publishers {
|
||||
|
||||
public struct Decode<Upstream, Output, Coder>: Publisher
|
||||
where Upstream: Publisher,
|
||||
Output: Decodable,
|
||||
Coder: TopLevelDecoder,
|
||||
Upstream.Output == Coder.Input
|
||||
{
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Error
|
||||
|
||||
public let upstream: Upstream
|
||||
|
||||
private let _decoder: Coder
|
||||
|
||||
public init(upstream: Upstream, decoder: Coder) {
|
||||
self.upstream = upstream
|
||||
self._decoder = decoder
|
||||
}
|
||||
|
||||
/// 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 Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
let decodeSubscriber = _Decode<Upstream, Downstream, Coder>(
|
||||
downstream: subscriber,
|
||||
decoder: _decoder
|
||||
)
|
||||
upstream.subscribe(decodeSubscriber)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class _Decode<Upstream: Publisher,
|
||||
Downstream: Subscriber,
|
||||
Coder: TopLevelDecoder>
|
||||
: OperatorSubscription<Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible,
|
||||
Subscription
|
||||
where Downstream.Input: Decodable,
|
||||
Coder.Input == Upstream.Output,
|
||||
Downstream.Failure == Error {
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
typealias Output = Downstream.Input
|
||||
|
||||
private let _decoder: Coder
|
||||
|
||||
var description: String { return "Decode" }
|
||||
|
||||
init(downstream: Downstream, decoder: Coder) {
|
||||
self._decoder = decoder
|
||||
super.init(downstream: downstream)
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
do {
|
||||
let value = try _decoder.decode(Downstream.Input.self, from: input)
|
||||
return downstream.receive(value)
|
||||
} catch {
|
||||
downstream.receive(completion: .failure(error))
|
||||
cancel()
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
downstream.receive(completion: completion.eraseError())
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
upstreamSubscription?.request(demand)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
public func decode<Item: Decodable,
|
||||
Coder: TopLevelDecoder>(
|
||||
type: Item.Type,
|
||||
decoder: Coder
|
||||
) -> Publishers.Decode<Self, Item, Coder>
|
||||
where Self.Output == Coder.Input
|
||||
{
|
||||
return Publishers.Decode(upstream: self, decoder: decoder)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
//
|
||||
// Publishers.Delay.swift
|
||||
// OpenCombine
|
||||
//
|
||||
// Created by Евгений Богомолов on 07/09/2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Delays delivery of all output to the downstream receiver by a specified amount
|
||||
/// of time on a particular scheduler.
|
||||
///
|
||||
/// The delay affects the delivery of elements and completion, but not of the original
|
||||
/// subscription.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - interval: The amount of time to delay.
|
||||
/// - tolerance: The allowed tolerance in firing delayed events.
|
||||
/// - scheduler: The scheduler to deliver the delayed events.
|
||||
/// - Returns: A publisher that delays delivery of elements and completion to
|
||||
/// the downstream receiver.
|
||||
public func delay<Context: Scheduler>(
|
||||
for interval: Context.SchedulerTimeType.Stride,
|
||||
tolerance: Context.SchedulerTimeType.Stride? = nil,
|
||||
scheduler: Context,
|
||||
options: Context.SchedulerOptions? = nil
|
||||
) -> Publishers.Delay<Self, Context> {
|
||||
return .init(upstream: self,
|
||||
interval: interval,
|
||||
tolerance: tolerance ?? scheduler.minimumTolerance,
|
||||
scheduler: scheduler,
|
||||
options: options)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that delays delivery of elements and completion
|
||||
/// to the downstream receiver.
|
||||
public struct Delay<Upstream: Publisher, Context: Scheduler>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher that this publisher receives elements from.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The amount of time to delay.
|
||||
public let interval: Context.SchedulerTimeType.Stride
|
||||
|
||||
/// The allowed tolerance in firing delayed events.
|
||||
public let tolerance: Context.SchedulerTimeType.Stride
|
||||
|
||||
/// The scheduler to deliver the delayed events.
|
||||
public let scheduler: Context
|
||||
|
||||
public let options: Context.SchedulerOptions?
|
||||
|
||||
public init(upstream: Upstream,
|
||||
interval: Context.SchedulerTimeType.Stride,
|
||||
tolerance: Context.SchedulerTimeType.Stride,
|
||||
scheduler: Context,
|
||||
options: Context.SchedulerOptions? = nil)
|
||||
{
|
||||
self.upstream = upstream
|
||||
self.interval = interval
|
||||
self.tolerance = tolerance
|
||||
self.scheduler = scheduler
|
||||
self.options = options
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
upstream.subscribe(Inner(self, downstream: subscriber))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Delay {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
// NOTE: This class has been audited for thread safety
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
fileprivate typealias Delay = Publishers.Delay<Upstream, Context>
|
||||
|
||||
private enum State {
|
||||
case ready(Delay, Downstream)
|
||||
case subscribed(Delay, Downstream, Subscription)
|
||||
case terminal
|
||||
}
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
private var state: State
|
||||
private let downstreamLock = UnfairRecursiveLock.allocate()
|
||||
|
||||
fileprivate init(_ publisher: Delay, downstream: Downstream) {
|
||||
state = .ready(publisher, downstream)
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
downstreamLock.deallocate()
|
||||
}
|
||||
|
||||
private func schedule(_ delay: Delay, work: @escaping () -> Void) {
|
||||
delay
|
||||
.scheduler
|
||||
.schedule(after: delay.scheduler.now.advanced(by: delay.interval),
|
||||
tolerance: delay.tolerance,
|
||||
options: delay.options,
|
||||
work)
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case let .ready(delay, downstream) = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .subscribed(delay, downstream, subscription)
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
downstream.receive(subscription: self)
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case let .subscribed(delay, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
lock.unlock()
|
||||
schedule(delay) {
|
||||
self.scheduledReceive(input, downstream: downstream)
|
||||
}
|
||||
return .none
|
||||
}
|
||||
|
||||
private func scheduledReceive(_ input: Upstream.Output, downstream: Downstream) {
|
||||
downstreamLock.lock()
|
||||
let newDemand = downstream.receive(input)
|
||||
downstreamLock.unlock()
|
||||
guard newDemand > 0 else {
|
||||
return
|
||||
}
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(newDemand)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(delay, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
schedule(delay) {
|
||||
self.scheduledReceive(completion: completion, downstream: downstream)
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduledReceive(completion: Subscribers.Completion<Failure>,
|
||||
downstream: Downstream) {
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sven Weidauer on 03.10.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
/// Omits the specified number of elements before republishing subsequent elements.
|
||||
///
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
//
|
||||
// Publishers.DropUntilOutput.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 24.12.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Ignores elements from the upstream publisher until it receives an element from
|
||||
/// a second publisher.
|
||||
///
|
||||
/// This publisher requests a single value from the upstream publisher, and it ignores
|
||||
/// (drops) all elements from that publisher until the upstream publisher produces
|
||||
/// a value. After the `other` publisher produces an element, this publisher cancels
|
||||
/// its subscription to the `other` publisher, and allows events from the `upstream`
|
||||
/// publisher to pass through.
|
||||
/// After this publisher receives a subscription from the upstream publisher, it
|
||||
/// passes through backpressure requests from downstream to the upstream publisher.
|
||||
/// If the upstream publisher acts on those requests before the other publisher
|
||||
/// produces an item, this publisher drops the elements it receives from the upstream
|
||||
/// publisher.
|
||||
///
|
||||
/// - Parameter publisher: A publisher to monitor for its first emitted element.
|
||||
/// - Returns: A publisher that drops elements from the upstream publisher until the
|
||||
/// `other` publisher produces a value.
|
||||
public func drop<Other: Publisher>(
|
||||
untilOutputFrom publisher: Other
|
||||
) -> Publishers.DropUntilOutput<Self, Other> where Failure == Other.Failure {
|
||||
return .init(upstream: self, other: publisher)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that ignores elements from the upstream publisher until it receives
|
||||
/// an element from second publisher.
|
||||
public struct DropUntilOutput<Upstream: Publisher, Other: Publisher>: Publisher
|
||||
where Upstream.Failure == Other.Failure
|
||||
{
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher that this publisher receives elements from.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A publisher to monitor for its first emitted element.
|
||||
public let other: Other
|
||||
|
||||
/// Creates a publisher that ignores elements from the upstream publisher until
|
||||
/// it receives an element from another publisher.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - upstream: A publisher to drop elements from while waiting for another
|
||||
/// publisher to emit elements.
|
||||
/// - other: A publisher to monitor for its first emitted element.
|
||||
public init(upstream: Upstream, other: Other) {
|
||||
self.upstream = upstream
|
||||
self.other = other
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Output == Downstream.Input,
|
||||
Other.Failure == Downstream.Failure
|
||||
{
|
||||
let inner = Inner(downstream: subscriber)
|
||||
other.subscribe(Inner.OtherSubscriber(inner: inner))
|
||||
upstream.subscribe(inner)
|
||||
subscriber.receive(subscription: inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.DropUntilOutput: Equatable
|
||||
where Upstream: Equatable, Other: Equatable {}
|
||||
|
||||
extension Publishers.DropUntilOutput {
|
||||
fileprivate 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 let downstream: Downstream
|
||||
|
||||
private var triggered = false
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private let downstreamLock = UnfairRecursiveLock.allocate()
|
||||
|
||||
private var upstreamSubscription: Subscription?
|
||||
|
||||
private var pendingDemand = Subscribers.Demand.none
|
||||
|
||||
private var otherSubscription: Subscription?
|
||||
|
||||
private var otherFinished = false
|
||||
|
||||
private var cancelled = false
|
||||
|
||||
init(downstream: Downstream) {
|
||||
self.downstream = downstream
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
downstreamLock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard upstreamSubscription == nil && !cancelled else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
upstreamSubscription = subscription
|
||||
if pendingDemand > 0 {
|
||||
lock.unlock()
|
||||
subscription.request(pendingDemand)
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
if !triggered || cancelled {
|
||||
pendingDemand -= 1
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
let newDemand = downstream.receive(input)
|
||||
downstreamLock.unlock()
|
||||
return newDemand
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
if cancelled {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
cancelled = true
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
|
||||
private func receiveOther(subscription: Subscription) {
|
||||
// Combine doesn't lock here
|
||||
guard otherSubscription == nil else {
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
otherSubscription = subscription
|
||||
subscription.request(.max(1))
|
||||
}
|
||||
|
||||
private func receiveOther(_ input: Other.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
triggered = true
|
||||
otherSubscription = nil
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
|
||||
private func receiveOther(completion: Subscribers.Completion<Other.Failure>) {
|
||||
lock.lock()
|
||||
if triggered {
|
||||
otherSubscription = nil
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
|
||||
otherFinished = true
|
||||
if let upstreamSubscription = self.upstreamSubscription {
|
||||
self.upstreamSubscription = nil
|
||||
lock.unlock()
|
||||
upstreamSubscription.cancel()
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
pendingDemand += demand
|
||||
if let subscription = upstreamSubscription {
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
let upstreamSubscription = self.upstreamSubscription
|
||||
let otherSubscription = self.otherSubscription
|
||||
self.upstreamSubscription = nil
|
||||
self.otherSubscription = nil
|
||||
cancelled = true
|
||||
lock.unlock()
|
||||
|
||||
upstreamSubscription?.cancel()
|
||||
otherSubscription?.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "DropUntilOutput" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.DropUntilOutput.Inner {
|
||||
fileprivate struct OtherSubscriber
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
let inner: Publishers.DropUntilOutput<Upstream, Other>.Inner<Downstream>
|
||||
|
||||
var combineIdentifier: CombineIdentifier {
|
||||
return inner.combineIdentifier
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
inner.receiveOther(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Other.Output) -> Subscribers.Demand {
|
||||
return inner.receiveOther(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Other.Failure>) {
|
||||
inner.receiveOther(completion: completion)
|
||||
}
|
||||
|
||||
var description: String { return "DropUntilOutput" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,40 @@
|
||||
// Created by Sergej Jaskiewicz on 16.06.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Omits elements from the upstream publisher until a given closure returns false,
|
||||
/// before republishing all remaining elements.
|
||||
///
|
||||
/// - Parameter predicate: A closure that takes an element as a parameter and returns
|
||||
/// a Boolean value indicating whether to drop the element from the publisher’s
|
||||
/// output.
|
||||
/// - Returns: A publisher that skips over elements until the provided closure returns
|
||||
/// `false`.
|
||||
public func drop(
|
||||
while predicate: @escaping (Output) -> Bool
|
||||
) -> Publishers.DropWhile<Self> {
|
||||
return .init(upstream: self, predicate: predicate)
|
||||
}
|
||||
|
||||
/// Omits elements from the upstream publisher until an error-throwing closure returns
|
||||
/// false, before republishing all remaining elements.
|
||||
///
|
||||
/// If the predicate closure throws, the publisher fails with an error.
|
||||
///
|
||||
/// - Parameter predicate: A closure that takes an element as a parameter and returns
|
||||
/// a Boolean value indicating whether to drop the element from the publisher’s
|
||||
/// output.
|
||||
/// - Returns: A publisher that skips over elements until the provided closure returns
|
||||
/// `false`, and then republishes all remaining elements. If the predicate closure
|
||||
/// throws, the publisher fails with an error.
|
||||
public func tryDrop(
|
||||
while predicate: @escaping (Output) throws -> Bool
|
||||
) -> Publishers.TryDropWhile<Self> {
|
||||
return .init(upstream: self, predicate: predicate)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that omits elements from an upstream publisher until a given closure
|
||||
@@ -29,8 +63,7 @@ extension Publishers {
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, predicate: catching(predicate))
|
||||
upstream.subscribe(inner)
|
||||
upstream.subscribe(Inner(downstream: subscriber, predicate: predicate))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,148 +89,249 @@ extension Publishers {
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Downstream.Failure == Error
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, predicate: catching(predicate))
|
||||
upstream.subscribe(inner)
|
||||
upstream.subscribe(Inner(downstream: subscriber, predicate: predicate))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class _DropWhile<Upstream: Publisher, Downstream: Subscriber>
|
||||
: OperatorSubscription<Downstream>,
|
||||
Subscription
|
||||
where Upstream.Output == Downstream.Input
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
typealias Predicate = (Input) -> Result<Bool, Downstream.Failure>
|
||||
|
||||
/// The predicate is reset to `nil` as soon as it returns `false`.
|
||||
var predicate: Predicate?
|
||||
var isCompleted = false
|
||||
|
||||
init(downstream: Downstream, predicate: @escaping Predicate) {
|
||||
self.predicate = predicate
|
||||
super.init(downstream: downstream)
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
|
||||
guard upstreamSubscription != nil else {
|
||||
return .none
|
||||
}
|
||||
|
||||
guard let predicate = self.predicate else {
|
||||
return downstream.receive(input)
|
||||
}
|
||||
|
||||
// NOTE: until the predicate returns false, we will ask the upstream publisher
|
||||
// for elements one by one.
|
||||
//
|
||||
// However, IF the downstream requests anything, we accumulate this demand in the
|
||||
// `demand` property so that later we can provide the downstream with the correct
|
||||
// amount of values.
|
||||
//
|
||||
// As soon as the predicate returns false, we switch to the mode where
|
||||
// we just forward all the requests from the downstream to the upstream.
|
||||
switch predicate(input) {
|
||||
case .success(true):
|
||||
return .max(1)
|
||||
case .success(false):
|
||||
// Okay, we hit the first element not satisfying the predicate,
|
||||
// from now on we just republish the values to the downstream.
|
||||
self.predicate = nil
|
||||
return downstream.receive(input)
|
||||
case .failure(let error):
|
||||
downstream.receive(completion: .failure(error))
|
||||
isCompleted = true
|
||||
cancel()
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
upstreamSubscription?.request(demand)
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
upstreamSubscription?.cancel()
|
||||
upstreamSubscription = nil
|
||||
isCompleted = true
|
||||
// Don't zero out downstream, that's what Combine does (probably a bug)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.DropWhile {
|
||||
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: _DropWhile<Upstream, Downstream>,
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
Subscriber
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Upstream.Output == Downstream.Input, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
var description: String { return "DropWhile" }
|
||||
// NOTE: This class has been audited for thread safety.
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private var status = SubscriptionStatus.awaitingSubscription
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private var predicate: ((Input) -> Bool)?
|
||||
|
||||
private var dropping = true
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
fileprivate init(downstream: Downstream, predicate: @escaping (Input) -> Bool) {
|
||||
self.downstream = downstream
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = status else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
status = .subscribed(subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case .subscribed = status, let shouldDrop = predicate else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
let dropping = self.dropping
|
||||
lock.unlock()
|
||||
|
||||
if dropping {
|
||||
if shouldDrop(input) {
|
||||
return .max(1)
|
||||
} else {
|
||||
lock.lock()
|
||||
self.dropping = false
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
return downstream.receive(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
guard !isCompleted else { return }
|
||||
lock.lock()
|
||||
guard case .subscribed = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
status = .terminal
|
||||
predicate = nil
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
isCompleted = true
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
status = .terminal
|
||||
predicate = nil
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "DropWhile" }
|
||||
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryDropWhile {
|
||||
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: _DropWhile<Upstream, Downstream>,
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
Subscriber
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Upstream.Output == Downstream.Input, Downstream.Failure == Error
|
||||
{
|
||||
var description: String { return "TryDropWhile" }
|
||||
// NOTE: This class has been audited for thread safety.
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private var status = SubscriptionStatus.awaitingSubscription
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private var predicate: ((Input) throws -> Bool)?
|
||||
|
||||
private var dropping = true
|
||||
|
||||
private var finished = false
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
fileprivate init(downstream: Downstream,
|
||||
predicate: @escaping (Input) throws -> Bool) {
|
||||
self.downstream = downstream
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = status else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
status = .subscribed(subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status,
|
||||
let shouldDrop = predicate else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
let dropping = self.dropping
|
||||
lock.unlock()
|
||||
|
||||
if dropping {
|
||||
do {
|
||||
if try shouldDrop(input) {
|
||||
return .max(1)
|
||||
} else {
|
||||
lock.lock()
|
||||
self.dropping = false
|
||||
lock.unlock()
|
||||
}
|
||||
} catch {
|
||||
lock.lock()
|
||||
status = .terminal
|
||||
predicate = nil
|
||||
finished = true
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
downstream.receive(completion: .failure(error))
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
return downstream.receive(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
guard !isCompleted else { return }
|
||||
downstream.receive(completion: completion.eraseError())
|
||||
isCompleted = true
|
||||
lock.lock()
|
||||
guard case .subscribed = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
status = .terminal
|
||||
let wasFinished = finished
|
||||
finished = true
|
||||
lock.unlock()
|
||||
|
||||
if !wasFinished {
|
||||
downstream.receive(completion: completion.eraseError())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Omits elements from the upstream publisher until a given closure returns false,
|
||||
/// before republishing all remaining elements.
|
||||
///
|
||||
/// - Parameter predicate: A closure that takes an element as a parameter and returns
|
||||
/// a Boolean value indicating whether to drop the element from the publisher’s
|
||||
/// output.
|
||||
/// - Returns: A publisher that skips over elements until the provided closure returns
|
||||
/// `false`.
|
||||
public func drop(
|
||||
while predicate: @escaping (Output) -> Bool
|
||||
) -> Publishers.DropWhile<Self> {
|
||||
return Publishers.DropWhile(upstream: self, predicate: predicate)
|
||||
}
|
||||
|
||||
/// Omits elements from the upstream publisher until an error-throwing closure returns
|
||||
/// false, before republishing all remaining elements.
|
||||
///
|
||||
/// If the predicate closure throws, the publisher fails with an error.
|
||||
///
|
||||
/// - Parameter predicate: A closure that takes an element as a parameter and returns
|
||||
/// a Boolean value indicating whether to drop the element from the publisher’s
|
||||
/// output.
|
||||
/// - Returns: A publisher that skips over elements until the provided closure returns
|
||||
/// `false`, and then republishes all remaining elements. If the predicate closure
|
||||
/// throws, the publisher fails with an error.
|
||||
public func tryDrop(
|
||||
while predicate: @escaping (Output) throws -> Bool
|
||||
) -> Publishers.TryDropWhile<Self> {
|
||||
return Publishers.TryDropWhile(upstream: self, predicate: predicate)
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
status = .terminal
|
||||
predicate = nil
|
||||
finished = true
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "TryDropWhile" }
|
||||
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
//
|
||||
// Publishers.Encode.swift
|
||||
//
|
||||
//
|
||||
// Created by Joseph Spadafora on 6/22/19.
|
||||
//
|
||||
|
||||
extension Publishers {
|
||||
|
||||
public struct Encode<Upstream, Coder>: Publisher
|
||||
where Upstream: Publisher,
|
||||
Coder: TopLevelEncoder,
|
||||
Upstream.Output: Encodable
|
||||
{
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Error
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Coder.Output
|
||||
|
||||
public let upstream: Upstream
|
||||
|
||||
private let encoder: Coder
|
||||
|
||||
public init(upstream: Upstream, encoder: Coder) {
|
||||
self.upstream = upstream
|
||||
self.encoder = encoder
|
||||
}
|
||||
|
||||
/// 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 Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
let encodeSubscriber = _Encode<Upstream, Downstream, Coder>(
|
||||
downstream: subscriber,
|
||||
encoder: encoder
|
||||
)
|
||||
upstream.subscribe(encodeSubscriber)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class _Encode<Upstream: Publisher,
|
||||
Downstream: Subscriber,
|
||||
Coder: TopLevelEncoder>
|
||||
: OperatorSubscription<Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible,
|
||||
Subscription
|
||||
where Coder.Output == Downstream.Input,
|
||||
Upstream.Output: Encodable,
|
||||
Downstream.Failure == Error {
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
typealias Output = Downstream.Input
|
||||
|
||||
private let _encoder: Coder
|
||||
|
||||
var description: String { return "Encode" }
|
||||
|
||||
init(downstream: Downstream, encoder: Coder) {
|
||||
self._encoder = encoder
|
||||
super.init(downstream: downstream)
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
do {
|
||||
let value = try _encoder.encode(input)
|
||||
return downstream.receive(value)
|
||||
} catch {
|
||||
downstream.receive(completion: .failure(error))
|
||||
cancel()
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
downstream.receive(completion: completion.eraseError())
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
upstreamSubscription?.request(demand)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
public func encode<Coder>(
|
||||
encoder: Coder
|
||||
) -> Publishers.Encode<Self, Coder>
|
||||
where Coder: TopLevelEncoder
|
||||
{
|
||||
return Publishers.Encode(upstream: self, encoder: encoder)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
${template_header}
|
||||
//
|
||||
// Publishers.Encode.swift.gyb
|
||||
//
|
||||
//
|
||||
// Created by Joseph Spadafora on 6/22/19.
|
||||
//
|
||||
|
||||
%{
|
||||
instantiations = ['Encode', 'Decode']
|
||||
}%
|
||||
extension Publisher {
|
||||
|
||||
/// Encodes the output from upstream using a specified `TopLevelEncoder`.
|
||||
/// For example, use `JSONEncoder`.
|
||||
public func encode<Coder: TopLevelEncoder>(
|
||||
encoder: Coder
|
||||
) -> Publishers.Encode<Self, Coder> {
|
||||
return .init(upstream: self, encoder: encoder)
|
||||
}
|
||||
|
||||
/// Decodes the output from upstream using a specified `TopLevelDecoder`.
|
||||
/// For example, use `JSONDecoder`.
|
||||
public func decode<Item: Decodable, Coder: TopLevelDecoder>(
|
||||
type: Item.Type,
|
||||
decoder: Coder
|
||||
) -> Publishers.Decode<Self, Item, Coder> where Output == Coder.Input {
|
||||
return .init(upstream: self, decoder: decoder)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
public struct Encode<Upstream: Publisher, Coder: TopLevelEncoder>: Publisher
|
||||
where Upstream.Output: Encodable
|
||||
{
|
||||
public typealias Failure = Error
|
||||
|
||||
public typealias Output = Coder.Output
|
||||
|
||||
public let upstream: Upstream
|
||||
|
||||
private let _encode: (Upstream.Output) throws -> Output
|
||||
|
||||
public init(upstream: Upstream, encoder: Coder) {
|
||||
self.upstream = upstream
|
||||
self._encode = encoder.encode
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, encode: _encode))
|
||||
}
|
||||
}
|
||||
|
||||
public struct Decode<Upstream: Publisher, Output: Decodable, Coder: TopLevelDecoder>
|
||||
: Publisher
|
||||
where Upstream.Output == Coder.Input
|
||||
{
|
||||
public typealias Failure = Error
|
||||
|
||||
public let upstream: Upstream
|
||||
|
||||
private let _decode: (Upstream.Output) throws -> Output
|
||||
|
||||
public init(upstream: Upstream, decoder: Coder) {
|
||||
self.upstream = upstream
|
||||
self._decode = { try decoder.decode(Output.self, from: $0) }
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, decode: _decode))
|
||||
}
|
||||
}
|
||||
}
|
||||
% for instantiation in instantiations:
|
||||
|
||||
extension Publishers.${instantiation} {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Output, Downstream.Failure == Error
|
||||
{
|
||||
// NOTE: This class has been audited for thread safety.
|
||||
// Combine doesn't use any locking here.
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let ${instantiation.lower()}: (Upstream.Output) throws -> Output
|
||||
|
||||
private var finished = false
|
||||
|
||||
private var subscription: Subscription?
|
||||
|
||||
fileprivate init(
|
||||
downstream: Downstream,
|
||||
${instantiation.lower()}: @escaping (Upstream.Output) throws -> Output
|
||||
) {
|
||||
self.downstream = downstream
|
||||
self.${instantiation.lower()} = ${instantiation.lower()}
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
if finished || self.subscription != nil {
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
self.subscription = subscription
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
if finished { return .none }
|
||||
do {
|
||||
return try downstream.receive(${instantiation.lower()}(input))
|
||||
} catch {
|
||||
finished = true
|
||||
subscription?.cancel()
|
||||
subscription = nil
|
||||
downstream.receive(completion: .failure(error))
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
if finished { return }
|
||||
finished = true
|
||||
subscription = nil
|
||||
downstream.receive(completion: completion.eraseError())
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
subscription?.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
guard let subscription = self.subscription, !finished else { return }
|
||||
subscription.cancel()
|
||||
self.subscription = nil
|
||||
finished = true
|
||||
}
|
||||
|
||||
var description: String { return "${instantiation}" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("downstream", downstream),
|
||||
("finished", finished),
|
||||
("upstreamSubscription", subscription as Any)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
% end
|
||||
@@ -97,8 +97,7 @@ extension Publishers {
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
let filter = Inner(downstream: subscriber, isIncluded: catching(isIncluded))
|
||||
upstream.receive(subscriber: filter)
|
||||
upstream.subscribe(Inner(downstream: subscriber, filter: isIncluded))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,92 +136,53 @@ extension Publishers {
|
||||
where Upstream.Output == Downstream.Input,
|
||||
Downstream.Failure == Failure
|
||||
{
|
||||
let filter = Inner(downstream: subscriber, isIncluded: catching(isIncluded))
|
||||
upstream.receive(subscriber: filter)
|
||||
upstream.subscribe(Inner(downstream: subscriber, filter: isIncluded))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class _Filter<Upstream: Publisher, Downstream: Subscriber>
|
||||
: OperatorSubscription<Downstream>,
|
||||
Subscription
|
||||
where Upstream.Output == Downstream.Input
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
typealias Predicate = (Input) -> Result<Bool, Downstream.Failure>
|
||||
|
||||
private var _isIncluded: Predicate?
|
||||
|
||||
var isFinished: Bool {
|
||||
return _isIncluded == nil
|
||||
}
|
||||
|
||||
init(downstream: Downstream, isIncluded: @escaping Predicate) {
|
||||
_isIncluded = isIncluded
|
||||
super.init(downstream: downstream)
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
guard let isIncluded = _isIncluded else { return .none }
|
||||
switch isIncluded(input) {
|
||||
case .success(let isIncluded):
|
||||
return isIncluded ? downstream.receive(input) : .max(1)
|
||||
case .failure(let error):
|
||||
downstream.receive(completion: .failure(error))
|
||||
cancel()
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
guard !isFinished else { return }
|
||||
upstreamSubscription?.request(demand)
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
_isIncluded = nil
|
||||
upstreamSubscription?.cancel()
|
||||
upstreamSubscription = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Filter {
|
||||
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: _Filter<Upstream, Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible
|
||||
where Upstream.Output == Downstream.Input,
|
||||
Upstream.Failure == Downstream.Failure {
|
||||
: FilterProducer<Downstream,
|
||||
Upstream.Output,
|
||||
Upstream.Output,
|
||||
Upstream.Failure,
|
||||
(Upstream.Output) -> Bool>
|
||||
where Upstream.Output == Downstream.Input, Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
// NOTE: This class has been audited for thread safety
|
||||
|
||||
var description: String { return "Filter" }
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
guard !isFinished else { return }
|
||||
downstream.receive(completion: completion)
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Upstream.Output?, Downstream.Failure> {
|
||||
return filter(newValue) ? .continue(newValue) : .continue(nil)
|
||||
}
|
||||
|
||||
override var description: String { return "Filter" }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryFilter {
|
||||
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: _Filter<Upstream, Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible
|
||||
where Upstream.Output == Downstream.Input, Downstream.Failure == Error {
|
||||
: FilterProducer<Downstream,
|
||||
Upstream.Output,
|
||||
Upstream.Output,
|
||||
Upstream.Failure,
|
||||
(Upstream.Output) throws -> Bool>
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Error
|
||||
{
|
||||
// NOTE: This class has been audited for thread safety
|
||||
|
||||
var description: String { return "TryFilter" }
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
guard !isFinished else { return }
|
||||
downstream.receive(completion: completion.eraseError())
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Upstream.Output?, Error> {
|
||||
do {
|
||||
return try filter(newValue) ? .continue(newValue) : .continue(nil)
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
override var description: String { return "TryFilter" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,7 @@
|
||||
// Created by Eric Patey on 16.08.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Transforms all elements from an upstream publisher into a new or existing
|
||||
/// publisher.
|
||||
///
|
||||
@@ -25,23 +22,18 @@ extension Publisher {
|
||||
_ transform: @escaping (Output) -> Child
|
||||
) -> Publishers.FlatMap<Child, Self>
|
||||
where Result == Child.Output, Failure == Child.Failure {
|
||||
return Publishers.FlatMap(upstream: self,
|
||||
maxPublishers: maxPublishers,
|
||||
transform: transform)
|
||||
return .init(upstream: self,
|
||||
maxPublishers: maxPublishers,
|
||||
transform: transform)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
public struct FlatMap<Child: Publisher, Upstream: Publisher>: Publisher
|
||||
where Child.Failure == Upstream.Failure
|
||||
{
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Child.Output
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
public let upstream: Upstream
|
||||
@@ -56,363 +48,378 @@ extension Publishers {
|
||||
self.maxPublishers = maxPublishers
|
||||
self.transform = transform
|
||||
}
|
||||
|
||||
/// 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 Child.Output == Downstream.Input, Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
let inner = Inner(downstream: subscriber,
|
||||
maxPublishers: maxPublishers,
|
||||
transform: transform)
|
||||
map: transform)
|
||||
subscriber.receive(subscription: inner)
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.FlatMap {
|
||||
|
||||
fileprivate final class Inner<Downstream: Subscriber>
|
||||
: CustomStringConvertible,
|
||||
Cancellable
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Child.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private typealias PendingValue = (
|
||||
value: Downstream.Input,
|
||||
// If the value was buffered at the time it became available, and the child's
|
||||
// demand was left at `.none` we keep track of the child in `pausedChild` so
|
||||
// that we can demand some more of it after sending this value.
|
||||
pausedChild: ChildSubscriber?
|
||||
)
|
||||
private typealias SubscriptionIndex = Int
|
||||
|
||||
/// All requests to this subscription should be made with the `outerLock`
|
||||
/// acquired.
|
||||
private var outerSubscription: Subscription?
|
||||
|
||||
// Must be recursive lock. Probably a bug in Combine.
|
||||
/// The lock for requesting from `outerSubscription`.
|
||||
private let outerLock = UnfairLock.allocate()
|
||||
|
||||
/// The lock for modifying the state. All mutable state here should be
|
||||
/// read and modified with this lock acquired.
|
||||
/// The only exception is the `downstreamRecursive` field, which is guarded
|
||||
/// by the `downstreamLock`.
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
/// All the calls to the downstream subscriber should be made with this lock
|
||||
/// acquired.
|
||||
private let downstreamLock = UnfairRecursiveLock.allocate()
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private var downstreamDemand = Subscribers.Demand.none
|
||||
|
||||
/// This variable is set to `true` whenever we call `downstream.receive(_:)`,
|
||||
/// and then set back to `false`.
|
||||
private var downstreamRecursive = false
|
||||
|
||||
private var innerRecursive = false
|
||||
private var subscriptions = [SubscriptionIndex : Subscription]()
|
||||
private var nextInnerIndex: SubscriptionIndex = 0
|
||||
private var pendingSubscriptions = 0
|
||||
private var buffer = [(SubscriptionIndex, Child.Output)]()
|
||||
private let maxPublishers: Subscribers.Demand
|
||||
private let transform: (Input) -> Child
|
||||
|
||||
// Locking rules for this class.
|
||||
// - All mutable state must only be accessed while `lock` is held.
|
||||
// - In order to avoid any deadlock potential, it is absolutely forbidden to have
|
||||
// any sort of call out from this class while the lock is held. This is why
|
||||
// the draining of the work queue uses a relatively complex pattern.
|
||||
private var downstream: Downstream?
|
||||
private var childSubscribers = Set<ChildSubscriber>()
|
||||
private var downstreamDemand = Subscribers.Demand.unlimited
|
||||
private var valuesToSend = [PendingValue]()
|
||||
private var queueIsBeingProcessed = false
|
||||
private var sendFinishedAfterDrainingQueue = false
|
||||
private var upstreamSubscription: Subscription?
|
||||
|
||||
var description: String { return "FlatMap" }
|
||||
private let map: (Input) -> Child
|
||||
private var cancelledOrCompleted = false
|
||||
private var outerFinished = false
|
||||
|
||||
init(downstream: Downstream,
|
||||
maxPublishers: Subscribers.Demand,
|
||||
transform: @escaping (Upstream.Output) -> Child) {
|
||||
map: @escaping (Upstream.Output) -> Child) {
|
||||
self.downstream = downstream
|
||||
self.maxPublishers = maxPublishers
|
||||
self.transform = transform
|
||||
self.map = map
|
||||
}
|
||||
|
||||
deinit {
|
||||
outerLock.deallocate()
|
||||
lock.deallocate()
|
||||
downstreamLock.deallocate()
|
||||
}
|
||||
|
||||
final func cancel() {
|
||||
// MARK: - Subscriber
|
||||
|
||||
let (upstreamToCancel, childrenToCancel) = lock
|
||||
.do { () -> (Subscription?, Set<ChildSubscriber>) in
|
||||
let upstreamToCancel = upstreamSubscription
|
||||
upstreamSubscription = nil
|
||||
return (upstreamToCancel, lockedDeactivateAndReturnChildrenToCancel())
|
||||
fileprivate func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard outerSubscription == nil, !cancelledOrCompleted else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
outerSubscription = subscription
|
||||
lock.unlock()
|
||||
subscription.request(maxPublishers)
|
||||
}
|
||||
|
||||
fileprivate func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
let cancelledOrCompleted = self.cancelledOrCompleted
|
||||
lock.unlock()
|
||||
if cancelledOrCompleted {
|
||||
return .none
|
||||
}
|
||||
let child = map(input)
|
||||
lock.lock()
|
||||
let innerIndex = nextInnerIndex
|
||||
nextInnerIndex += 1
|
||||
pendingSubscriptions += 1
|
||||
lock.unlock()
|
||||
child.subscribe(Side(index: innerIndex, inner: self))
|
||||
return .none
|
||||
}
|
||||
|
||||
fileprivate func receive(completion: Subscribers.Completion<Child.Failure>) {
|
||||
outerSubscription = nil
|
||||
lock.lock()
|
||||
outerFinished = true
|
||||
switch completion {
|
||||
case .finished:
|
||||
releaseLockThenSendCompletionDownstreamIfNeeded(outerFinished: true)
|
||||
return
|
||||
case .failure:
|
||||
let wasAlreadyCancelledOrCompleted = cancelledOrCompleted
|
||||
cancelledOrCompleted = true
|
||||
for (_, subscription) in subscriptions {
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
upstreamToCancel?.cancel()
|
||||
cancelChildren(childrenToCancel)
|
||||
subscriptions = [:]
|
||||
lock.unlock()
|
||||
if wasAlreadyCancelledOrCompleted {
|
||||
return
|
||||
}
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Private implementation
|
||||
extension Publishers.FlatMap.Inner {
|
||||
// MARK: - Subscription
|
||||
|
||||
private func deactivate() {
|
||||
cancelChildren(lock.do(lockedDeactivateAndReturnChildrenToCancel))
|
||||
}
|
||||
|
||||
// Must be called with lock held.
|
||||
private func lockedDeactivateAndReturnChildrenToCancel() -> Set<ChildSubscriber> {
|
||||
downstream = nil
|
||||
downstreamDemand = .none
|
||||
let result = childSubscribers
|
||||
childSubscribers.removeAll()
|
||||
upstreamSubscription = nil
|
||||
return result
|
||||
}
|
||||
|
||||
private func cancelChildren(_ childrenToCancel: Set<ChildSubscriber>) {
|
||||
childrenToCancel.forEach { $0.cancel() }
|
||||
}
|
||||
|
||||
/// In a thread-safe way, this function performs the passed in work with the lock held
|
||||
/// and then checks to see if either upstream or any of the child subscriptions remain
|
||||
/// active. If there are no remaining active subscriptions, it enqueues the sending
|
||||
/// of `.finished` downstream using the processing queue.
|
||||
/// - Parameter lockedWork: block to be formed with the lock held.
|
||||
private final func maybeSendFinishedAfterExecutingWork(lockedWork: () -> Void) {
|
||||
let shouldProcessQueue: Bool = lock.do {
|
||||
lockedWork()
|
||||
if childSubscribers.isEmpty && upstreamSubscription == nil {
|
||||
sendFinishedAfterDrainingQueue = true
|
||||
if !queueIsBeingProcessed {
|
||||
queueIsBeingProcessed = true
|
||||
return true
|
||||
fileprivate func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
if downstreamRecursive {
|
||||
// downstreamRecursive being true means that downstreamLock
|
||||
// is already acquired.
|
||||
downstreamDemand += demand
|
||||
return
|
||||
}
|
||||
lock.lock()
|
||||
if cancelledOrCompleted {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
if demand == .unlimited {
|
||||
downstreamDemand = .unlimited
|
||||
let buffer = self.buffer
|
||||
self.buffer = []
|
||||
let subscriptions = self.subscriptions
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
downstreamRecursive = true
|
||||
for (_, childOutput) in buffer {
|
||||
_ = downstream.receive(childOutput)
|
||||
}
|
||||
downstreamRecursive = false
|
||||
downstreamLock.unlock()
|
||||
for (_, subscription) in subscriptions {
|
||||
subscription.request(.unlimited)
|
||||
}
|
||||
lock.lock()
|
||||
} else {
|
||||
downstreamDemand += demand
|
||||
while !buffer.isEmpty && downstreamDemand > 0 {
|
||||
// FIXME: This has quadratic complexity.
|
||||
// This is what Combine does.
|
||||
// Can we improve perfomance by using e. g. Deque instead of Array?
|
||||
// Or array's cache locality makes this solution more efficient?
|
||||
// Must benchmark before optimizing!
|
||||
//
|
||||
// https://www.cocoawithlove.com/blog/2016/09/22/deque.html
|
||||
let (index, value) = buffer.removeFirst()
|
||||
downstreamDemand -= 1
|
||||
let subscription = subscriptions[index]
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
downstreamRecursive = true
|
||||
let additionalDemand = downstream.receive(value)
|
||||
downstreamRecursive = false
|
||||
downstreamLock.unlock()
|
||||
if additionalDemand != .none {
|
||||
lock.lock()
|
||||
downstreamDemand += additionalDemand
|
||||
lock.unlock()
|
||||
}
|
||||
if let subscription = subscription {
|
||||
innerRecursive = true
|
||||
subscription.request(.max(1))
|
||||
innerRecursive = false
|
||||
}
|
||||
lock.lock()
|
||||
}
|
||||
}
|
||||
releaseLockThenSendCompletionDownstreamIfNeeded(outerFinished: outerFinished)
|
||||
}
|
||||
|
||||
fileprivate func cancel() {
|
||||
lock.lock()
|
||||
cancelledOrCompleted = true
|
||||
let subscriptions = self.subscriptions
|
||||
self.subscriptions = [:]
|
||||
lock.unlock()
|
||||
for (_, subscription) in subscriptions {
|
||||
subscription.cancel()
|
||||
}
|
||||
// Combine doesn't acquire the lock here. Weird.
|
||||
outerSubscription?.cancel()
|
||||
outerSubscription = nil
|
||||
}
|
||||
|
||||
// MARK: - Reflection
|
||||
|
||||
fileprivate var description: String { return "FlatMap" }
|
||||
|
||||
fileprivate var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
fileprivate var playgroundDescription: Any { return description }
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func receiveInner(subscription: Subscription,
|
||||
_ index: SubscriptionIndex) {
|
||||
lock.lock()
|
||||
pendingSubscriptions -= 1
|
||||
subscriptions[index] = subscription
|
||||
|
||||
let demand = downstreamDemand == .unlimited
|
||||
? Subscribers.Demand.unlimited
|
||||
: .max(1)
|
||||
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
private func receiveInner(_ input: Child.Output,
|
||||
_ index: SubscriptionIndex) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
if downstreamDemand == .unlimited {
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
downstreamRecursive = true
|
||||
_ = downstream.receive(input)
|
||||
downstreamRecursive = false
|
||||
downstreamLock.unlock()
|
||||
return .none
|
||||
}
|
||||
if downstreamDemand == .none || innerRecursive {
|
||||
buffer.append((index, input))
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
downstreamDemand -= 1
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
downstreamRecursive = true
|
||||
let newDemand = downstream.receive(input)
|
||||
downstreamRecursive = false
|
||||
downstreamLock.unlock()
|
||||
if newDemand > 0 {
|
||||
lock.lock()
|
||||
downstreamDemand += newDemand
|
||||
lock.unlock()
|
||||
}
|
||||
return .max(1)
|
||||
}
|
||||
|
||||
private func receiveInner(completion: Subscribers.Completion<Child.Failure>,
|
||||
_ index: SubscriptionIndex) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
lock.lock()
|
||||
subscriptions.removeValue(forKey: index)
|
||||
let downstreamCompleted = releaseLockThenSendCompletionDownstreamIfNeeded(
|
||||
outerFinished: outerFinished
|
||||
)
|
||||
if !downstreamCompleted {
|
||||
requestOneMorePublisher()
|
||||
}
|
||||
case .failure:
|
||||
lock.lock()
|
||||
if cancelledOrCompleted {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
cancelledOrCompleted = true
|
||||
let subscriptions = self.subscriptions
|
||||
self.subscriptions = [:]
|
||||
lock.unlock()
|
||||
for (i, subscription) in subscriptions where i != index {
|
||||
subscription.cancel()
|
||||
}
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
private func requestOneMorePublisher() {
|
||||
if maxPublishers != .unlimited {
|
||||
outerLock.lock()
|
||||
outerSubscription?.request(.max(1))
|
||||
outerLock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
/// - Precondition: `lock` is acquired
|
||||
/// - Postcondition: `lock` is released
|
||||
///
|
||||
/// - Returns: `true` if a completion was sent downstream
|
||||
@discardableResult
|
||||
private func releaseLockThenSendCompletionDownstreamIfNeeded(
|
||||
outerFinished: Bool
|
||||
) -> Bool {
|
||||
#if DEBUG
|
||||
lock.assertOwner() // Sanity check
|
||||
#endif
|
||||
if !cancelledOrCompleted && outerFinished && buffer.isEmpty &&
|
||||
subscriptions.count + pendingSubscriptions == 0 {
|
||||
cancelledOrCompleted = true
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: .finished)
|
||||
downstreamLock.unlock()
|
||||
return true
|
||||
}
|
||||
|
||||
lock.unlock()
|
||||
return false
|
||||
}
|
||||
|
||||
if shouldProcessQueue {
|
||||
processQueue()
|
||||
}
|
||||
}
|
||||
// MARK: - Side
|
||||
|
||||
private func receivedCompletion(_ completion: Subscribers.Completion<Failure>,
|
||||
fromChild child: ChildSubscriber) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
removeActiveSubscription(forChild: child)
|
||||
case .failure:
|
||||
downstream?.receive(completion: completion)
|
||||
deactivate()
|
||||
}
|
||||
}
|
||||
private struct Side: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible {
|
||||
private let index: SubscriptionIndex
|
||||
private let inner: Inner
|
||||
fileprivate let combineIdentifier = CombineIdentifier()
|
||||
|
||||
private func removeActiveSubscription(forChild child: ChildSubscriber) {
|
||||
maybeSendFinishedAfterExecutingWork { childSubscribers.remove(child) }
|
||||
}
|
||||
|
||||
private func receivedValue(_ value: Child.Output,
|
||||
fromChild child: ChildSubscriber) -> Subscribers.Demand {
|
||||
// When receiving a value from a child, we need to determine what additional
|
||||
// demand to return to the child. Apple's logic for this determination is as
|
||||
// follows:
|
||||
// - If we are in `.unlimited` mode, we always request `.none` additional
|
||||
// else
|
||||
// - If there is a surplus relative to the demand, we request `.none`
|
||||
// else
|
||||
// - There is not yet a surplus, so request `.max(1)` more from the child
|
||||
|
||||
let (surplusAvailable, processTheQueue): (Bool, Bool) = lock.do {
|
||||
// If we already have enough values to satisfy the demand, we "buffer" this
|
||||
// child value establishing a surplus.
|
||||
if downstreamDemand <= valuesToSend.count {
|
||||
valuesToSend.append((value, child))
|
||||
return (surplusAvailable: true, processTheQueue: false)
|
||||
} else {
|
||||
valuesToSend.append((value, nil))
|
||||
if queueIsBeingProcessed {
|
||||
return (surplusAvailable: false, processTheQueue: false)
|
||||
}
|
||||
queueIsBeingProcessed = true
|
||||
return (surplusAvailable: false, processTheQueue: true)
|
||||
}
|
||||
}
|
||||
|
||||
let demandResult = surplusAvailable || demandForChild() == .unlimited
|
||||
? Subscribers.Demand.none
|
||||
: .max(1)
|
||||
|
||||
if processTheQueue {
|
||||
processQueue()
|
||||
}
|
||||
|
||||
return demandResult
|
||||
}
|
||||
|
||||
private func demandForChild() -> Subscribers.Demand {
|
||||
return downstreamDemand == .unlimited ? .unlimited : .max(1)
|
||||
}
|
||||
|
||||
private enum QueueWorkStatus {
|
||||
case noWork
|
||||
case sendFinish
|
||||
case sendValues(values: ArraySlice<PendingValue>)
|
||||
}
|
||||
|
||||
private func processQueue() {
|
||||
assert(queueIsBeingProcessed)
|
||||
|
||||
// We loop processing the queue in case somebody put stuff on the queue while we
|
||||
// were sending values with the lock unlocked.
|
||||
while true {
|
||||
let work: QueueWorkStatus = lock.do {
|
||||
if downstreamDemand == .none || valuesToSend.isEmpty {
|
||||
if sendFinishedAfterDrainingQueue && valuesToSend.isEmpty {
|
||||
return .sendFinish
|
||||
} else {
|
||||
queueIsBeingProcessed = false
|
||||
return .noWork
|
||||
}
|
||||
}
|
||||
|
||||
let countToSend = min(valuesToSend.count, downstreamDemand.max ?? .max)
|
||||
let result = valuesToSend[0..<countToSend]
|
||||
// TODO: Consider an alternative storage to avoid O(n) removeFirst
|
||||
valuesToSend.removeFirst(countToSend)
|
||||
downstreamDemand -= countToSend
|
||||
return .sendValues(values: result)
|
||||
fileprivate init(index: SubscriptionIndex, inner: Inner) {
|
||||
self.index = index
|
||||
self.inner = inner
|
||||
}
|
||||
|
||||
guard let downstream = downstream else { return }
|
||||
|
||||
switch work {
|
||||
case .noWork:
|
||||
return
|
||||
case .sendFinish:
|
||||
downstream.receive(completion: .finished)
|
||||
deactivate()
|
||||
return
|
||||
case .sendValues(let values):
|
||||
var newDemand = Subscribers.Demand.none
|
||||
values.forEach {
|
||||
newDemand += downstream.receive($0.value)
|
||||
// pausedChild is present only if the value was buffered and the
|
||||
// child's demand was left at `.none`. In that case, once we send the
|
||||
// buffered value, we need to tell the child to get another value.
|
||||
$0.pausedChild?.request(.max(1))
|
||||
}
|
||||
|
||||
if newDemand != .none {
|
||||
lock.do { downstreamDemand += newDemand }
|
||||
}
|
||||
fileprivate func receive(subscription: Subscription) {
|
||||
inner.receiveInner(subscription: subscription, index)
|
||||
}
|
||||
|
||||
fileprivate func receive(_ input: Child.Output) -> Subscribers.Demand {
|
||||
return inner.receiveInner(input, index)
|
||||
}
|
||||
|
||||
fileprivate func receive(completion: Subscribers.Completion<Child.Failure>) {
|
||||
inner.receiveInner(completion: completion, index)
|
||||
}
|
||||
|
||||
fileprivate var description: String { return "FlatMap" }
|
||||
|
||||
fileprivate var customMirror: Mirror {
|
||||
let children = CollectionOfOne<Mirror.Child>(
|
||||
("parentSubscription", inner.combineIdentifier)
|
||||
)
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
fileprivate var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This `Subscriber` implementation is for `FlatMap`'s upstream subscription
|
||||
extension Publishers.FlatMap.Inner: Subscriber {
|
||||
|
||||
fileprivate func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
downstream?.receive(subscription: self)
|
||||
subscription.request(maxPublishers)
|
||||
}
|
||||
|
||||
/// Receive a new value from the upstream subscription. A new child subscription
|
||||
/// will be made on the `Child` that the input value is transformed into.
|
||||
/// - Parameter input: a value to be transformed by `transform`
|
||||
fileprivate func receive(_ input: Input) -> Subscribers.Demand {
|
||||
let newChildSubscriber = ChildSubscriber(parent: self)
|
||||
|
||||
lock.do { _ = childSubscribers.insert(newChildSubscriber) }
|
||||
|
||||
self.transform(input).subscribe(newChildSubscriber)
|
||||
|
||||
return .none
|
||||
}
|
||||
|
||||
fileprivate func receive(completion: Subscribers.Completion<Failure>) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
maybeSendFinishedAfterExecutingWork { upstreamSubscription = nil }
|
||||
case .failure:
|
||||
downstream?.receive(completion: completion)
|
||||
deactivate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Inner is the `Subscription` for `Downstream`
|
||||
extension Publishers.FlatMap.Inner: Subscription {
|
||||
fileprivate func request(_ demand: Subscribers.Demand) {
|
||||
let (drainTheQueue, becameUnlimited) = lock.do { () -> (Bool, Bool) in
|
||||
let becameUnlimited = demand == .unlimited && downstreamDemand != .unlimited
|
||||
downstreamDemand = demand
|
||||
defer { queueIsBeingProcessed = true }
|
||||
return (!queueIsBeingProcessed, becameUnlimited)
|
||||
}
|
||||
|
||||
if becameUnlimited {
|
||||
// TODO: This code isn't yet thread safe. The correct change is to do this
|
||||
// through the queue just like sending values and finished. Finished is
|
||||
// done through the queue as a bit of a hack. The right design is to have
|
||||
// an enum of actions on the queue. That enum will include (send value,
|
||||
// send finished, set child demand).
|
||||
let newChildDemand = demandForChild()
|
||||
childSubscribers.forEach { $0.request(newChildDemand) }
|
||||
}
|
||||
|
||||
if drainTheQueue {
|
||||
processQueue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.FlatMap.Inner {
|
||||
/// ChildSubscriber is needed to help implement the backpressure/demand strategy.
|
||||
/// Specifically, a custom subscriber is needed to manage the demand of the child
|
||||
/// subscription:
|
||||
/// - Send .max(1) request when the subscription is received
|
||||
/// - Send .max(1) request when downstream subscriber demands more and a previously
|
||||
/// buffered value from the child was sent. (When the value was buffered, the
|
||||
/// child's demand reached .none - effectively pausing the child.)
|
||||
fileprivate final class ChildSubscriber: Hashable {
|
||||
internal typealias Input = Downstream.Input
|
||||
internal typealias Failure = Downstream.Failure
|
||||
|
||||
private var _upstreamSubscription: Subscription?
|
||||
private unowned let _parent: Publishers.FlatMap<Child, Upstream>.Inner<Downstream>
|
||||
|
||||
init(parent: Publishers.FlatMap<Child, Upstream>.Inner<Downstream>) {
|
||||
_parent = parent
|
||||
}
|
||||
|
||||
fileprivate func request(_ demand: Subscribers.Demand) {
|
||||
_upstreamSubscription?.request(demand)
|
||||
}
|
||||
|
||||
public static func == (lhs: ChildSubscriber, rhs: ChildSubscriber) -> Bool {
|
||||
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(ObjectIdentifier(self))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.FlatMap.Inner.ChildSubscriber: Cancellable {
|
||||
fileprivate func cancel() {
|
||||
_upstreamSubscription?.cancel()
|
||||
_upstreamSubscription = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.FlatMap.Inner.ChildSubscriber: Subscriber {
|
||||
|
||||
fileprivate func receive(subscription: Subscription) {
|
||||
if _upstreamSubscription == nil {
|
||||
_upstreamSubscription = subscription
|
||||
subscription.request(_parent.demandForChild())
|
||||
} else {
|
||||
assertionFailure()
|
||||
subscription.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return _parent.receivedValue(input, fromChild: self)
|
||||
}
|
||||
|
||||
fileprivate func receive(completion: Subscribers.Completion<Failure>) {
|
||||
_parent.receivedCompletion(completion, fromChild: self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
//
|
||||
// Publishers.HandleEvents.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 03.12.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Performs the specified closures when publisher events occur.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - receiveSubscription: A closure that executes when the publisher receives
|
||||
/// the subscription from the upstream publisher. Defaults to `nil`.
|
||||
/// - receiveOutput: A closure that executes when the publisher receives a value
|
||||
/// from the upstream publisher. Defaults to `nil`.
|
||||
/// - receiveCompletion: A closure that executes when the publisher receives
|
||||
/// the completion from the upstream publisher. Defaults to `nil`.
|
||||
/// - receiveCancel: A closure that executes when the downstream receiver cancels
|
||||
/// publishing. Defaults to `nil`.
|
||||
/// - receiveRequest: A closure that executes when the publisher receives a request
|
||||
/// for more elements. Defaults to `nil`.
|
||||
/// - Returns: A publisher that performs the specified closures when publisher events
|
||||
/// occur.
|
||||
public func handleEvents(
|
||||
receiveSubscription: ((Subscription) -> Void)? = nil,
|
||||
receiveOutput: ((Output) -> Void)? = nil,
|
||||
receiveCompletion: ((Subscribers.Completion<Failure>) -> Void)? = nil,
|
||||
receiveCancel: (() -> Void)? = nil,
|
||||
receiveRequest: ((Subscribers.Demand) -> Void)? = nil
|
||||
) -> Publishers.HandleEvents<Self> {
|
||||
return .init(upstream: self,
|
||||
receiveSubscription: receiveSubscription,
|
||||
receiveOutput: receiveOutput,
|
||||
receiveCompletion: receiveCompletion,
|
||||
receiveCancel: receiveCancel,
|
||||
receiveRequest: receiveRequest)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that performs the specified closures when publisher events occur.
|
||||
public struct HandleEvents<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
|
||||
|
||||
/// A closure that executes when the publisher receives the subscription from
|
||||
/// the upstream publisher.
|
||||
public var receiveSubscription: ((Subscription) -> Void)?
|
||||
|
||||
/// A closure that executes when the publisher receives a value from the upstream
|
||||
/// publisher.
|
||||
public var receiveOutput: ((Upstream.Output) -> Void)?
|
||||
|
||||
/// A closure that executes when the publisher receives the completion from
|
||||
/// the upstream publisher.
|
||||
public var receiveCompletion:
|
||||
((Subscribers.Completion<Upstream.Failure>) -> Void)?
|
||||
|
||||
/// A closure that executes when the downstream receiver cancels publishing.
|
||||
public var receiveCancel: (() -> Void)?
|
||||
|
||||
/// A closure that executes when the publisher receives a request for more
|
||||
/// elements.
|
||||
public var receiveRequest: ((Subscribers.Demand) -> Void)?
|
||||
|
||||
public init(
|
||||
upstream: Upstream,
|
||||
receiveSubscription: ((Subscription) -> Void)? = nil,
|
||||
receiveOutput: ((Output) -> Void)? = nil,
|
||||
receiveCompletion: ((Subscribers.Completion<Failure>) -> Void)? = nil,
|
||||
receiveCancel: (() -> Void)? = nil,
|
||||
receiveRequest: ((Subscribers.Demand) -> Void)?
|
||||
) {
|
||||
self.upstream = upstream
|
||||
self.receiveSubscription = receiveSubscription
|
||||
self.receiveOutput = receiveOutput
|
||||
self.receiveCompletion = receiveCompletion
|
||||
self.receiveCancel = receiveCancel
|
||||
self.receiveRequest = receiveRequest
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
let inner = Inner(self, downstream: subscriber)
|
||||
subscriber.receive(subscription: inner)
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.HandleEvents {
|
||||
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 var status = SubscriptionStatus.awaitingSubscription
|
||||
private var pendingDemand = Subscribers.Demand.none
|
||||
private let lock = UnfairLock.allocate()
|
||||
private var events: Publishers.HandleEvents<Upstream>?
|
||||
private let downstream: Downstream
|
||||
|
||||
init(_ events: Publishers.HandleEvents<Upstream>, downstream: Downstream) {
|
||||
self.events = events
|
||||
self.downstream = downstream
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
events?.receiveSubscription?(subscription)
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = status else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
status = .subscribed(subscription)
|
||||
let pendingDemand = self.pendingDemand
|
||||
self.pendingDemand = .none
|
||||
lock.unlock()
|
||||
if pendingDemand > 0 {
|
||||
subscription.request(pendingDemand)
|
||||
}
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
events?.receiveOutput?(input)
|
||||
let newDemand = downstream.receive(input)
|
||||
if newDemand > 0 {
|
||||
events?.receiveRequest?(newDemand)
|
||||
}
|
||||
return newDemand
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
events?.receiveCompletion?(completion)
|
||||
lock.lock()
|
||||
events = nil
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
events?.receiveRequest?(demand)
|
||||
lock.lock()
|
||||
if case let .subscribed(subscription) = status {
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
return
|
||||
}
|
||||
pendingDemand += demand
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
events?.receiveCancel?()
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
events = nil
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "HandleEvents" }
|
||||
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ extension Publisher {
|
||||
/// The output type of this publisher is `Never`.
|
||||
/// - Returns: A publisher that ignores all upstream elements.
|
||||
public func ignoreOutput() -> Publishers.IgnoreOutput<Self> {
|
||||
return Publishers.IgnoreOutput(upstream: self)
|
||||
return .init(upstream: self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,12 +21,8 @@ extension Publishers {
|
||||
/// state (finish or failed).
|
||||
public struct IgnoreOutput<Upstream: Publisher>: Publisher {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Never
|
||||
|
||||
/// 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.
|
||||
@@ -36,39 +32,52 @@ extension Publishers {
|
||||
self.upstream = upstream
|
||||
}
|
||||
|
||||
/// 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 Downstream.Failure == Upstream.Failure, Downstream.Input == Never {
|
||||
let inner = Inner<Downstream>(downstream: subscriber)
|
||||
upstream.subscribe(inner)
|
||||
where Downstream.Failure == Upstream.Failure, Downstream.Input == Never
|
||||
{
|
||||
upstream.subscribe(Inner<Downstream>(downstream: subscriber))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.IgnoreOutput {
|
||||
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: OperatorSubscription<Downstream>,
|
||||
Subscriber,
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
Subscription
|
||||
where Downstream.Input == Never,
|
||||
Downstream.Failure == Upstream.Failure
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Never, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
// NOTE: This class has been audited for thread safety
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
typealias Output = Never
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
var description: String { return "IgnoreOutput" }
|
||||
private let downstream: Downstream
|
||||
|
||||
private var status = SubscriptionStatus.awaitingSubscription
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
fileprivate init(downstream: Downstream) {
|
||||
self.downstream = downstream
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = status else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
status = .subscribed(subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
subscription.request(.unlimited)
|
||||
}
|
||||
@@ -78,6 +87,13 @@ extension Publishers.IgnoreOutput {
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
guard case .subscribed = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
@@ -85,6 +101,31 @@ extension Publishers.IgnoreOutput {
|
||||
// ignore and requests from downstream since we'll never send
|
||||
// any values
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "IgnoreOutput" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
let children: [Mirror.Child] = [
|
||||
("downstream", downstream),
|
||||
("status", status)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Anton Nazarov on 25.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Transforms all elements from the upstream publisher with a provided closure.
|
||||
@@ -32,7 +30,7 @@ extension Publisher {
|
||||
/// - Returns: A publisher that uses the provided closure to map elements from
|
||||
/// the upstream publisher to new elements that it then publishes.
|
||||
public func tryMap<Result>(
|
||||
_ transform: @escaping (Self.Output) throws -> Result
|
||||
_ transform: @escaping (Output) throws -> Result
|
||||
) -> Publishers.TryMap<Self, Result> {
|
||||
return Publishers.TryMap(upstream: self, transform: transform)
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ extension Publisher {
|
||||
/// - Returns: A publisher that replaces any upstream failure with a
|
||||
/// new error produced by the `transform` closure.
|
||||
public func mapError<NewFailure: Error>(
|
||||
_ transform: @escaping (Self.Failure) -> NewFailure
|
||||
_ transform: @escaping (Failure) -> NewFailure
|
||||
) -> Publishers.MapError<Self, NewFailure>
|
||||
{
|
||||
return Publishers.MapError(upstream: self, transform)
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
//
|
||||
// Publishers.MeasureInterval.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 03.12.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Measures and emits the time interval between events received from an upstream
|
||||
/// publisher.
|
||||
///
|
||||
/// The output type of the returned scheduler is the time interval of the provided
|
||||
/// scheduler.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - scheduler: The scheduler on which to deliver elements.
|
||||
/// - options: Options that customize the delivery of elements.
|
||||
/// - Returns: A publisher that emits elements representing the time interval between
|
||||
/// the elements it receives.
|
||||
public func measureInterval<Context: Scheduler>(
|
||||
using scheduler: Context,
|
||||
options: Context.SchedulerOptions? = nil
|
||||
) -> Publishers.MeasureInterval<Self, Context> {
|
||||
return .init(upstream: self, scheduler: scheduler)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that measures and emits the time interval between events received from
|
||||
/// an upstream publisher.
|
||||
public struct MeasureInterval<Upstream: Publisher, Context: Scheduler>: Publisher {
|
||||
|
||||
public typealias Output = Context.SchedulerTimeType.Stride
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The scheduler on which to deliver elements.
|
||||
public let scheduler: Context
|
||||
|
||||
public init(upstream: Upstream, scheduler: Context) {
|
||||
self.upstream = upstream
|
||||
self.scheduler = scheduler
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Downstream.Input == Context.SchedulerTimeType.Stride
|
||||
{
|
||||
upstream.subscribe(Inner(self, downstream: subscriber))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.MeasureInterval {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Context.SchedulerTimeType.Stride,
|
||||
Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
// NOTE: This class has been audited for thread safety
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
typealias MeasureInterval = Publishers.MeasureInterval<Upstream, Context>
|
||||
|
||||
private enum State {
|
||||
case ready(MeasureInterval, Downstream)
|
||||
case subscribed(MeasureInterval, Downstream, Subscription)
|
||||
case terminal
|
||||
}
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private var state: State
|
||||
|
||||
private var last: Context.SchedulerTimeType?
|
||||
|
||||
init(_ measureInterval: MeasureInterval, downstream: Downstream) {
|
||||
state = .ready(measureInterval, downstream)
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case let .ready(measureInterval, downstream) = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .subscribed(measureInterval, downstream, subscription)
|
||||
last = measureInterval.scheduler.now
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case let .subscribed(measureInterval, downstream, subscription) = state,
|
||||
let previousTime = last else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
let now = measureInterval.scheduler.now
|
||||
last = now
|
||||
lock.unlock()
|
||||
let newDemand = downstream.receive(previousTime.distance(to: now))
|
||||
if newDemand > 0 {
|
||||
subscription.request(newDemand)
|
||||
}
|
||||
return .none
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
last = nil
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
last = nil
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "MeasureInterval" }
|
||||
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 14.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Applies a closure to create a subject that delivers elements to subscribers.
|
||||
@@ -159,6 +157,7 @@ extension Publishers.Multicast {
|
||||
lock.lock()
|
||||
guard case let .ready(upstream, downstream) = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .subscribed(upstream: upstream,
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
//
|
||||
// Publishers.Output.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 24.10.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Republishes elements up to the specified maximum count.
|
||||
///
|
||||
/// - Parameter maxLength: The maximum number of elements to republish.
|
||||
/// - Returns: A publisher that publishes up to the specified number of elements
|
||||
/// before completing.
|
||||
public func prefix(_ maxLength: Int) -> Publishers.Output<Self> {
|
||||
return output(in: ..<maxLength)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Publishes a specific element, indicated by its index in the sequence of published
|
||||
/// elements.
|
||||
///
|
||||
/// If the publisher completes normally or with an error before publishing
|
||||
/// the specified element, then the publisher doesn’t produce any elements.
|
||||
///
|
||||
/// - Parameter index: The index that indicates the element to publish.
|
||||
/// - Returns: A publisher that publishes a specific indexed element.
|
||||
public func output(at index: Int) -> Publishers.Output<Self> {
|
||||
return output(in: index...index)
|
||||
}
|
||||
|
||||
/// Publishes elements specified by their range in the sequence of published elements.
|
||||
///
|
||||
/// After all elements are published, the publisher finishes normally.
|
||||
/// If the publisher completes normally or with an error before producing all
|
||||
/// the elements in the range, it doesn’t publish the remaining elements.
|
||||
///
|
||||
/// - Parameter range: A range that indicates which elements to publish.
|
||||
/// - Returns: A publisher that publishes elements specified by a range.
|
||||
public func output<Range: RangeExpression>(in range: Range) -> Publishers.Output<Self>
|
||||
where Range.Bound == Int
|
||||
{
|
||||
return .init(upstream: self, range: range.relative(to: 0 ..< .max))
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that publishes elements specified by a range in the sequence of
|
||||
/// published elements.
|
||||
public struct Output<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher that this publisher receives elements from.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The range of elements to publish.
|
||||
public let range: CountableRange<Int>
|
||||
|
||||
/// Creates a publisher that publishes elements specified by a range.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - upstream: The publisher that this publisher receives elements from.
|
||||
/// - range: The range of elements to publish.
|
||||
public init(upstream: Upstream, range: CountableRange<Int>) {
|
||||
precondition(range.lowerBound >= 0, "lowerBound must not be negative")
|
||||
precondition(range.upperBound >= 0, "upperBound must not be negative")
|
||||
self.upstream = upstream
|
||||
self.range = range
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, range: range))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Output: Equatable where Upstream: Equatable {}
|
||||
|
||||
extension Publishers.Output {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
// NOTE: This class has been audited for thread safety
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private var status = SubscriptionStatus.awaitingSubscription
|
||||
|
||||
private var remainingUntilStart: Int
|
||||
|
||||
private var remainingCount: Int
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
fileprivate init(downstream: Downstream, range: CountableRange<Int>) {
|
||||
self.downstream = downstream
|
||||
self.remainingUntilStart = range.lowerBound
|
||||
self.remainingCount = range.count
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = status else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
status = .subscribed(subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
if remainingUntilStart > 0 {
|
||||
remainingUntilStart -= 1
|
||||
return .max(1)
|
||||
}
|
||||
|
||||
let newDemand: Subscribers.Demand
|
||||
if remainingCount > 0 {
|
||||
remainingCount -= 1
|
||||
newDemand = downstream.receive(input)
|
||||
} else {
|
||||
newDemand = .none
|
||||
cancelUpstreamAndFinish()
|
||||
}
|
||||
return newDemand
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
lock.lock()
|
||||
guard case .subscribed = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "Output" }
|
||||
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func cancelUpstreamAndFinish() {
|
||||
assert(remainingCount == 0)
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
downstream.receive(completion: .finished)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
//
|
||||
// Publishers.PrefixWhile.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 24.10.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Republishes elements while a predicate closure indicates publishing should
|
||||
/// continue.
|
||||
///
|
||||
/// The publisher finishes when the closure returns `false`.
|
||||
///
|
||||
/// - Parameter predicate: A closure that takes an element as its parameter and
|
||||
/// returns a Boolean value indicating whether publishing should continue.
|
||||
/// - Returns: A publisher that passes through elements until the predicate indicates
|
||||
/// publishing should finish.
|
||||
public func prefix(
|
||||
while predicate: @escaping (Output) -> Bool
|
||||
) -> Publishers.PrefixWhile<Self> {
|
||||
return .init(upstream: self, predicate: predicate)
|
||||
}
|
||||
|
||||
/// Republishes elements while a error-throwing predicate closure indicates publishing
|
||||
/// should continue.
|
||||
///
|
||||
/// The publisher finishes when the closure returns `false`. If the closure throws,
|
||||
/// the publisher fails with the thrown error.
|
||||
///
|
||||
/// - Parameter predicate: A closure that takes an element as its parameter and
|
||||
/// returns a Boolean value indicating whether publishing should continue.
|
||||
/// - Returns: A publisher that passes through elements until the predicate throws or
|
||||
/// indicates publishing should finish.
|
||||
public func tryPrefix(
|
||||
while predicate: @escaping (Output) throws -> Bool
|
||||
) -> Publishers.TryPrefixWhile<Self> {
|
||||
return .init(upstream: self, predicate: predicate)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that republishes elements while a predicate closure indicates
|
||||
/// publishing should continue.
|
||||
public struct PrefixWhile<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 closure that determines whether whether publishing should continue.
|
||||
public let predicate: (Upstream.Output) -> Bool
|
||||
|
||||
public init(upstream: Upstream, predicate: @escaping (Upstream.Output) -> Bool) {
|
||||
self.upstream = upstream
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, filter: predicate))
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that republishes elements while an error-throwing predicate closure
|
||||
/// indicates publishing should continue.
|
||||
public struct TryPrefixWhile<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Error
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The error-throwing closure that determines whether publishing should continue.
|
||||
public let predicate: (Upstream.Output) throws -> Bool
|
||||
|
||||
public init(upstream: Upstream,
|
||||
predicate: @escaping (Upstream.Output) throws -> Bool) {
|
||||
self.upstream = upstream
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Output == Downstream.Input, Downstream.Failure == Error
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, filter: predicate))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.PrefixWhile {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: FilterProducer<Downstream,
|
||||
Upstream.Output,
|
||||
Upstream.Output,
|
||||
Upstream.Failure,
|
||||
(Upstream.Output) -> Bool>
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
override func receive(
|
||||
newValue: Input
|
||||
) -> PartialCompletion<Upstream.Output?, Downstream.Failure> {
|
||||
return filter(newValue) ? .continue(newValue) : .finished
|
||||
}
|
||||
|
||||
override var description: String { return "PrefixWhile" }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryPrefixWhile {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: FilterProducer<Downstream,
|
||||
Upstream.Output,
|
||||
Upstream.Output,
|
||||
Upstream.Failure,
|
||||
(Upstream.Output) throws -> Bool>
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Error
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
override func receive(
|
||||
newValue: Input
|
||||
) -> PartialCompletion<Upstream.Output?, Downstream.Failure> {
|
||||
do {
|
||||
return try filter(newValue) ? .continue(newValue) : .finished
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
override var description: String { return "TryPrefixWhile" }
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,18 @@
|
||||
// Created by Sergej Jaskiewicz on 16.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
extension Publisher {
|
||||
|
||||
/// Prints log messages for all publishing events.
|
||||
///
|
||||
/// - Parameter prefix: A string with which to prefix all log messages. Defaults to
|
||||
/// an empty string.
|
||||
/// - Returns: A publisher that prints log messages for all publishing events.
|
||||
public func print(_ prefix: String = "",
|
||||
to stream: TextOutputStream? = nil) -> Publishers.Print<Self> {
|
||||
return .init(upstream: self, prefix: prefix, to: stream)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
@@ -54,24 +65,12 @@ extension Publishers {
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Prints log messages for all publishing events.
|
||||
///
|
||||
/// - Parameter prefix: A string with which to prefix all log messages. Defaults to
|
||||
/// an empty string.
|
||||
/// - Returns: A publisher that prints log messages for all publishing events.
|
||||
public func print(_ prefix: String = "",
|
||||
to stream: TextOutputStream? = nil) -> Publishers.Print<Self> {
|
||||
return Publishers.Print(upstream: self, prefix: prefix, to: stream)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Print {
|
||||
private final class Inner<Downstream: Subscriber>: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
typealias Input = Downstream.Input
|
||||
typealias Failure = Downstream.Failure
|
||||
@@ -89,7 +88,7 @@ extension Publishers.Print {
|
||||
private var downstream: Downstream
|
||||
private let prefix: String
|
||||
private var stream: PrintTarget?
|
||||
private var subscription: Subscription?
|
||||
private var status = SubscriptionStatus.awaitingSubscription
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
init(downstream: Downstream, prefix: String, stream: TextOutputStream?) {
|
||||
@@ -104,9 +103,14 @@ extension Publishers.Print {
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
log("\(prefix)receive subscription: (\(subscription))")
|
||||
lock.do {
|
||||
self.subscription = subscription
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = status else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
status = .subscribed(subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
@@ -130,6 +134,9 @@ extension Publishers.Print {
|
||||
case .failure(let error):
|
||||
log("\(prefix)receive error: (\(error))")
|
||||
}
|
||||
lock.lock()
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
@@ -139,19 +146,35 @@ extension Publishers.Print {
|
||||
} else {
|
||||
log("\(prefix)request unlimited")
|
||||
}
|
||||
subscription?.request(demand)
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
log("\(prefix)receive cancel")
|
||||
subscription?.cancel()
|
||||
subscription = nil
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "Print" }
|
||||
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func log(_ text: String) {
|
||||
if var stream = stream {
|
||||
Swift.print(text, to: &stream)
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
//
|
||||
// Publishers.ReceiveOn.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 02.12.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
/// Specifies the scheduler on which to receive elements from the publisher.
|
||||
///
|
||||
/// You use the `receive(on:options:)` operator to receive results on a specific
|
||||
/// scheduler, such as performing UI work on the main run loop.
|
||||
/// In contrast with `subscribe(on:options:)`, which affects upstream messages,
|
||||
/// `receive(on:options:)` changes the execution context of downstream messages.
|
||||
/// In the following example, requests to `jsonPublisher` are performed on
|
||||
/// `backgroundQueue`, but elements received from it are performed on `RunLoop.main`.
|
||||
///
|
||||
/// // Some publisher.
|
||||
/// let jsonPublisher = MyJSONLoaderPublisher()
|
||||
///
|
||||
/// // Some subscriber that updates the UI.
|
||||
/// let labelUpdater = MyLabelUpdateSubscriber()
|
||||
///
|
||||
/// jsonPublisher
|
||||
/// .subscribe(on: backgroundQueue)
|
||||
/// .receiveOn(on: RunLoop.main)
|
||||
/// .subscribe(labelUpdater)
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - scheduler: The scheduler the publisher is to use for element delivery.
|
||||
/// - options: Scheduler options that customize the element delivery.
|
||||
/// - Returns: A publisher that delivers elements using the specified scheduler.
|
||||
public func receive<Context: Scheduler>(
|
||||
on scheduler: Context,
|
||||
options: Context.SchedulerOptions? = nil
|
||||
) -> Publishers.ReceiveOn<Self, Context> {
|
||||
return .init(upstream: self, scheduler: scheduler, options: options)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that delivers elements to its downstream subscriber on a specific
|
||||
/// scheduler.
|
||||
public struct ReceiveOn<Upstream: Publisher, Context: Scheduler>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The scheduler the publisher is to use for element delivery.
|
||||
public let scheduler: Context
|
||||
|
||||
/// Scheduler options that customize the delivery of elements.
|
||||
public let options: Context.SchedulerOptions?
|
||||
|
||||
public init(upstream: Upstream,
|
||||
scheduler: Context,
|
||||
options: Context.SchedulerOptions?) {
|
||||
self.upstream = upstream
|
||||
self.scheduler = scheduler
|
||||
self.options = options
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
upstream.subscribe(Inner(self, downstream: subscriber))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.ReceiveOn {
|
||||
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
|
||||
|
||||
typealias ReceiveOn = Publishers.ReceiveOn<Upstream, Context>
|
||||
|
||||
private enum State {
|
||||
case ready(ReceiveOn, Downstream)
|
||||
case subscribed(ReceiveOn, Downstream, Subscription)
|
||||
case terminal
|
||||
}
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
private var state: State
|
||||
private let downstreamLock = UnfairRecursiveLock.allocate()
|
||||
|
||||
init(_ receiveOn: ReceiveOn, downstream: Downstream) {
|
||||
state = .ready(receiveOn, downstream)
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
downstreamLock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case let .ready(receiveOn, downstream) = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .subscribed(receiveOn, downstream, subscription)
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
downstream.receive(subscription: self)
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case let .subscribed(receiveOn, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
lock.unlock()
|
||||
receiveOn.scheduler.schedule(options: receiveOn.options) { [weak self] in
|
||||
self?.scheduledReceive(input, downstream: downstream)
|
||||
}
|
||||
return .none
|
||||
}
|
||||
|
||||
private func scheduledReceive(_ input: Upstream.Output, downstream: Downstream) {
|
||||
downstreamLock.lock()
|
||||
let newDemand = downstream.receive(input)
|
||||
downstreamLock.unlock()
|
||||
guard newDemand > 0 else {
|
||||
return
|
||||
}
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(newDemand)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(receiveOn, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
receiveOn.scheduler.schedule(options: receiveOn.options) { [weak self] in
|
||||
self?.scheduledReceive(completion: completion, downstream: downstream)
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduledReceive(completion: Subscribers.Completion<Failure>,
|
||||
downstream: Downstream) {
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "ReceiveOn" }
|
||||
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
//
|
||||
// Publishers.RemoveDuplicates.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 24.10.2019.
|
||||
//
|
||||
|
||||
extension Publisher where Output: Equatable {
|
||||
|
||||
/// Publishes only elements that don’t match the previous element.
|
||||
///
|
||||
/// - Returns: A publisher that consumes — rather than publishes — duplicate elements.
|
||||
public func removeDuplicates() -> Publishers.RemoveDuplicates<Self> {
|
||||
return removeDuplicates(by: ==)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Publishes only elements that don’t match the previous element, as evaluated by
|
||||
/// a provided closure.
|
||||
///
|
||||
/// - Parameter predicate: A closure to evaluate whether two elements are equivalent,
|
||||
/// for purposes of filtering. Return `true` from this closure to indicate that
|
||||
/// the second element is a duplicate of the first.
|
||||
public func removeDuplicates(
|
||||
by predicate: @escaping (Output, Output) -> Bool
|
||||
) -> Publishers.RemoveDuplicates<Self> {
|
||||
return .init(upstream: self, predicate: predicate)
|
||||
}
|
||||
|
||||
/// Publishes only elements that don’t match the previous element, as evaluated by
|
||||
/// a provided error-throwing closure.
|
||||
///
|
||||
/// - Parameter predicate: A closure to evaluate whether two elements are equivalent,
|
||||
/// for purposes of filtering. Return `true` from this closure to indicate that
|
||||
/// the second element is a duplicate of the first. If this closure throws an error,
|
||||
/// the publisher terminates with the thrown error.
|
||||
public func tryRemoveDuplicates(
|
||||
by predicate: @escaping (Output, Output) throws -> Bool
|
||||
) -> Publishers.TryRemoveDuplicates<Self> {
|
||||
return .init(upstream: self, predicate: predicate)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that publishes only elements that don’t match the previous element.
|
||||
public struct RemoveDuplicates<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
|
||||
|
||||
/// A closure to evaluate whether two elements are equivalent,
|
||||
/// for purposes of filtering.
|
||||
public let predicate: (Output, Output) -> Bool
|
||||
|
||||
/// Creates a publisher that publishes only elements that don’t match the previou
|
||||
/// element, as evaluated by a provided closure.
|
||||
///
|
||||
/// - Parameter upstream: The publisher from which this publisher receives
|
||||
/// elements.
|
||||
/// - Parameter predicate: A closure to evaluate whether two elements are
|
||||
/// equivalent, for purposes of filtering. Return `true` from this closure
|
||||
/// to indicate that the second element is a duplicate of the first.
|
||||
public init(upstream: Upstream, predicate: @escaping (Output, Output) -> Bool) {
|
||||
self.upstream = upstream
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, filter: predicate))
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that publishes only elements that don’t match the previous element,
|
||||
/// as evaluated by a provided error-throwing closure.
|
||||
public struct TryRemoveDuplicates<Upstream: Publisher>: Publisher{
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Error
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// An error-throwing closure to evaluate whether two elements are equivalent,
|
||||
/// for purposes of filtering.
|
||||
public let predicate: (Output, Output) throws -> Bool
|
||||
|
||||
/// Creates a publisher that publishes only elements that don’t match the previous
|
||||
/// element, as evaluated by a provided error-throwing closure.
|
||||
///
|
||||
/// - Parameter upstream: The publisher from which this publisher receives
|
||||
/// elements.
|
||||
/// - Parameter predicate: An error-throwing closure to evaluate whether two
|
||||
/// elements are equivalent, for purposes of filtering. Return `true` from this
|
||||
/// closure to indicate that the second element is a duplicate of the first.
|
||||
/// If this closure throws an error, the publisher terminates
|
||||
/// with the thrown error.
|
||||
public init(upstream: Upstream,
|
||||
predicate: @escaping (Output, Output) throws -> Bool) {
|
||||
self.upstream = upstream
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Output == Downstream.Input, Downstream.Failure == Error
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, filter: predicate))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.RemoveDuplicates {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: FilterProducer<Downstream,
|
||||
Upstream.Output,
|
||||
Upstream.Output,
|
||||
Upstream.Failure,
|
||||
(Output, Output) -> Bool>
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
// NOTE: This class has been audited for thread-safety
|
||||
|
||||
private var last: Upstream.Output?
|
||||
|
||||
override func receive(
|
||||
newValue: Input
|
||||
) -> PartialCompletion<Upstream.Output?, Downstream.Failure> {
|
||||
let last = self.last
|
||||
self.last = newValue
|
||||
return last.map {
|
||||
filter($0, newValue) ? .continue(nil) : .continue(newValue)
|
||||
} ?? .continue(newValue)
|
||||
}
|
||||
|
||||
override var description: String { return "RemoveDuplicates" }
|
||||
|
||||
override var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("downstream", downstream),
|
||||
("last", last as Any)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryRemoveDuplicates {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: FilterProducer<Downstream,
|
||||
Upstream.Output,
|
||||
Upstream.Output,
|
||||
Upstream.Failure,
|
||||
(Output, Output) throws -> Bool>
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Error
|
||||
{
|
||||
// NOTE: This class has been audited for thread-safety
|
||||
|
||||
private var last: Upstream.Output?
|
||||
|
||||
override func receive(
|
||||
newValue: Input
|
||||
) -> PartialCompletion<Upstream.Output?, Downstream.Failure> {
|
||||
let last = self.last
|
||||
self.last = newValue
|
||||
return last.map {
|
||||
do {
|
||||
return try filter($0, newValue)
|
||||
? .continue(nil)
|
||||
: .continue(newValue)
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
} ?? .continue(newValue)
|
||||
}
|
||||
|
||||
override var description: String { return "TryRemoveDuplicates" }
|
||||
|
||||
override var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("downstream", downstream),
|
||||
("last", last as Any)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
//
|
||||
// Publishers.ReplaceEmpty.swift
|
||||
// OpenCombine
|
||||
//
|
||||
// Created by Joe Spadafora on 12/10/19.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Replaces an empty stream with the provided element.
|
||||
///
|
||||
/// If the upstream publisher finishes without producing any elements,
|
||||
/// this publisher emits the provided element, then finishes normally.
|
||||
/// - Parameter output: An element to emit when the upstream publisher
|
||||
/// finishes without emitting any elements.
|
||||
/// - Returns: A publisher that replaces an empty stream with
|
||||
/// the provided output element.
|
||||
public func replaceEmpty(with output: Output) -> Publishers.ReplaceEmpty<Self> {
|
||||
return .init(upstream: self, output: output)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that replaces an empty stream with a provided element.
|
||||
public struct ReplaceEmpty<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The element to deliver when the upstream publisher finishes
|
||||
/// without delivering any elements.
|
||||
public let output: Upstream.Output
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
public init(upstream: Upstream, output: Output) {
|
||||
self.upstream = upstream
|
||||
self.output = output
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, output: output)
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.ReplaceEmpty: Equatable
|
||||
where Upstream: Equatable, Upstream.Output: Equatable {}
|
||||
|
||||
extension Publishers.ReplaceEmpty {
|
||||
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let output: Output
|
||||
private let downstream: Downstream
|
||||
|
||||
private var receivedUpstream = false
|
||||
private var lock = UnfairLock.allocate()
|
||||
private var downstreamRequested = false
|
||||
private var finishedWithoutUpstream = false
|
||||
|
||||
private var status = SubscriptionStatus.awaitingSubscription
|
||||
|
||||
fileprivate init(downstream: Downstream, output: Output) {
|
||||
self.downstream = downstream
|
||||
self.output = output
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = status else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
status = .subscribed(subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
subscription.request(.unlimited)
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case .subscribed = status else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
receivedUpstream = true
|
||||
lock.unlock()
|
||||
return downstream.receive(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
lock.lock()
|
||||
guard case .subscribed = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
status = .terminal
|
||||
if receivedUpstream {
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
return
|
||||
}
|
||||
switch completion {
|
||||
case .finished:
|
||||
if downstreamRequested {
|
||||
lock.unlock()
|
||||
_ = downstream.receive(output)
|
||||
downstream.receive(completion: completion)
|
||||
return
|
||||
}
|
||||
finishedWithoutUpstream = true
|
||||
lock.unlock()
|
||||
case .failure:
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
lock.lock()
|
||||
downstreamRequested = true
|
||||
if finishedWithoutUpstream {
|
||||
lock.unlock()
|
||||
_ = downstream.receive(output)
|
||||
downstream.receive(completion: .finished)
|
||||
return
|
||||
}
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "ReplaceEmpty" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Bogdan Vlad on 8/29/19.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
/// Replaces any errors in the stream with the provided element.
|
||||
///
|
||||
@@ -54,7 +52,9 @@ extension Publishers {
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Output == Downstream.Input, Downstream.Failure == Failure
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, output: output))
|
||||
let inner = Inner(downstream: subscriber, output: output)
|
||||
upstream.subscribe(inner)
|
||||
subscriber.receive(subscription: inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,8 +102,11 @@ extension Publishers.ReplaceError {
|
||||
return
|
||||
}
|
||||
status = .subscribed(subscription)
|
||||
let pendingDemand = self.pendingDemand
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
if pendingDemand > 0 {
|
||||
subscription.request(pendingDemand)
|
||||
}
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
// Created by Eric Patey on 26.08.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Transforms elements from the upstream publisher by providing the current element
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 19.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that publishes a given sequence of elements.
|
||||
@@ -107,7 +105,7 @@ extension Publishers.Sequence {
|
||||
|
||||
// Combine calls next() while the lock is held.
|
||||
// It is possible to engineer a custom Sequence that would cause
|
||||
// a dedlock here, but it would be something insane.
|
||||
// a deadlock here, but it would be something insane.
|
||||
let next = iterator.next()
|
||||
recursion = true
|
||||
lock.unlock()
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
//
|
||||
// Publishers.SubscribeOn.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 02.12.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Specifies the scheduler on which to perform subscribe, cancel, and request
|
||||
/// operations.
|
||||
///
|
||||
/// In contrast with `receive(on:options:)`, which affects downstream messages,
|
||||
/// `subscribe(on:)` changes the execution context of upstream messages.
|
||||
/// In the following example, requests to `jsonPublisher` are performed on
|
||||
/// `backgroundQueue`, but elements received from it are performed on `RunLoop.main`.
|
||||
///
|
||||
/// let ioPerformingPublisher == // Some publisher.
|
||||
/// let uiUpdatingSubscriber == // Some subscriber that updates the UI.
|
||||
///
|
||||
/// ioPerformingPublisher
|
||||
/// .subscribe(on: backgroundQueue)
|
||||
/// .receiveOn(on: RunLoop.main)
|
||||
/// .subscribe(uiUpdatingSubscriber)
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - scheduler: The scheduler on which to receive upstream messages.
|
||||
/// - options: Options that customize the delivery of elements.
|
||||
/// - Returns: A publisher which performs upstream operations on the specified
|
||||
/// scheduler.
|
||||
public func subscribe<Context: Scheduler>(
|
||||
on scheduler: Context,
|
||||
options: Context.SchedulerOptions? = nil
|
||||
) -> Publishers.SubscribeOn<Self, Context> {
|
||||
return .init(upstream: self, scheduler: scheduler, options: options)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that receives elements from an upstream publisher on a specific
|
||||
/// scheduler.
|
||||
public struct SubscribeOn<Upstream: Publisher, Context: Scheduler>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The scheduler the publisher should use to receive elements.
|
||||
public let scheduler: Context
|
||||
|
||||
/// Scheduler options that customize the delivery of elements.
|
||||
public let options: Context.SchedulerOptions?
|
||||
|
||||
public init(upstream: Upstream,
|
||||
scheduler: Context,
|
||||
options: Context.SchedulerOptions?) {
|
||||
self.upstream = upstream
|
||||
self.scheduler = scheduler
|
||||
self.options = options
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
scheduler.schedule(options: options) {
|
||||
self.upstream.subscribe(Inner(self, downstream: subscriber))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.SubscribeOn {
|
||||
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
|
||||
|
||||
typealias SubscribeOn = Publishers.SubscribeOn<Upstream, Context>
|
||||
|
||||
private enum State {
|
||||
case ready(SubscribeOn, Downstream)
|
||||
case subscribed(SubscribeOn, Downstream, Subscription)
|
||||
case terminal
|
||||
}
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
private var state: State
|
||||
private let upstreamLock = UnfairLock.allocate()
|
||||
|
||||
init(_ subscribeOn: SubscribeOn, downstream: Downstream) {
|
||||
state = .ready(subscribeOn, downstream)
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
upstreamLock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case let .ready(subscribeOn, downstream) = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .subscribed(subscribeOn, downstream, subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
lock.unlock()
|
||||
return downstream.receive(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscribeOn, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscribeOn.scheduler.schedule(options: subscribeOn.options) { [weak self] in
|
||||
self?.scheduledRequest(demand, subscription: subscription)
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduledRequest(_ demand: Subscribers.Demand,
|
||||
subscription: Subscription) {
|
||||
upstreamLock.lock()
|
||||
subscription.request(demand)
|
||||
upstreamLock.unlock()
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscribeOn, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
subscribeOn.scheduler.schedule(options: subscribeOn.options) { [weak self] in
|
||||
self?.scheduledCancel(subscription)
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduledCancel(_ subscription: Subscription) {
|
||||
upstreamLock.lock()
|
||||
subscription.cancel()
|
||||
upstreamLock.unlock()
|
||||
}
|
||||
|
||||
var description: String { return "SubscribeOn" }
|
||||
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
//
|
||||
// Record.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 12.11.2019.
|
||||
//
|
||||
|
||||
/// A publisher that allows for recording a series of inputs and a completion for later
|
||||
/// playback to each subscriber.
|
||||
public struct Record<Output, Failure: Error>: Publisher {
|
||||
|
||||
/// The recorded output and completion.
|
||||
public let recording: Recording
|
||||
|
||||
/// Interactively record a series of outputs and a completion.
|
||||
public init(record: (inout Recording) -> Void) {
|
||||
var recording = Recording()
|
||||
record(&recording)
|
||||
self.init(recording: recording)
|
||||
}
|
||||
|
||||
/// Initialize with a recording.
|
||||
public init(recording: Recording) {
|
||||
self.recording = recording
|
||||
}
|
||||
|
||||
/// Set up a complete recording with the specified output and completion.
|
||||
public init(output: [Output], completion: Subscribers.Completion<Failure>) {
|
||||
self.init(recording: Recording(output: output, completion: completion))
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Failure == Downstream.Failure
|
||||
{
|
||||
if recording.output.isEmpty {
|
||||
subscriber.receive(subscription: Subscriptions.empty)
|
||||
subscriber.receive(completion: recording.completion)
|
||||
} else {
|
||||
let inner = Inner(downstream: subscriber,
|
||||
sequence: recording.output,
|
||||
completion: recording.completion)
|
||||
subscriber.receive(subscription: inner)
|
||||
}
|
||||
}
|
||||
|
||||
/// A recorded set of `Output` and a `Subscribers.Completion`.
|
||||
public struct Recording {
|
||||
|
||||
public typealias Input = Output
|
||||
|
||||
private enum State {
|
||||
case input
|
||||
case complete
|
||||
}
|
||||
|
||||
private var state: State
|
||||
|
||||
/// The output which will be sent to a `Subscriber`.
|
||||
public private(set) var output: [Output]
|
||||
|
||||
/// The completion which will be sent to a `Subscriber`.
|
||||
public private(set) var completion: Subscribers.Completion<Failure>
|
||||
|
||||
/// Set up a recording in a state ready to receive output.
|
||||
public init() {
|
||||
state = .input
|
||||
output = []
|
||||
completion = .finished
|
||||
}
|
||||
|
||||
/// Set up a complete recording with the specified output and completion.
|
||||
public init(output: [Output],
|
||||
completion: Subscribers.Completion<Failure> = .finished) {
|
||||
self.state = .complete
|
||||
self.output = output
|
||||
self.completion = completion
|
||||
}
|
||||
|
||||
/// Add an output to the recording.
|
||||
///
|
||||
/// A `fatalError` will be raised if output is added after adding completion.
|
||||
public mutating func receive(_ input: Input) {
|
||||
precondition(state == .input,
|
||||
"Receiving values after completion is not allowed")
|
||||
output.append(input)
|
||||
}
|
||||
|
||||
/// Add a completion to the recording.
|
||||
///
|
||||
/// A `fatalError` will be raised if more than one completion is added.
|
||||
public mutating func receive(completion: Subscribers.Completion<Failure>) {
|
||||
precondition(state == .input,
|
||||
"Receiving completion more than once is not allowed")
|
||||
self.completion = completion
|
||||
self.state = .complete
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Record: Codable where Output: Codable, Failure: Codable {}
|
||||
|
||||
extension Record.Recording: Codable where Output: Codable, Failure: Codable {
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case output = "output"
|
||||
case completion = "completion"
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let output = try container.decode([Output].self, forKey: .output)
|
||||
let completion = try container.decode(Subscribers.Completion<Failure>.self,
|
||||
forKey: .completion)
|
||||
self.init(output: output, completion: completion)
|
||||
}
|
||||
|
||||
public func encode(into encoder: Encoder) throws {
|
||||
try encode(to: encoder)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(output, forKey: .output)
|
||||
try container.encode(completion, forKey: .completion)
|
||||
}
|
||||
}
|
||||
|
||||
extension Record {
|
||||
|
||||
// This class is almost the same as Publishers.Sequence.Inner
|
||||
// despite some small details.
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure
|
||||
{
|
||||
// NOTE: This class has been audited for thread-safety
|
||||
|
||||
private var sequence: [Output]?
|
||||
private let completion: Subscribers.Completion<Failure>
|
||||
private var downstream: Downstream?
|
||||
private var iterator: IndexingIterator<[Output]>
|
||||
private var next: Output?
|
||||
private var pendingDemand = Subscribers.Demand.none
|
||||
private var recursion = false
|
||||
private var lock = UnfairLock.allocate()
|
||||
|
||||
fileprivate init(downstream: Downstream,
|
||||
sequence: [Output],
|
||||
completion: Subscribers.Completion<Failure>) {
|
||||
self.sequence = sequence
|
||||
self.completion = completion
|
||||
self.downstream = downstream
|
||||
self.iterator = sequence.makeIterator()
|
||||
next = iterator.next()
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
var description: String {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
return sequence.map { $0.description } ?? "Cancelled Events"
|
||||
}
|
||||
|
||||
var customMirror: Mirror {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
let children: [Mirror.Child] = [
|
||||
("sequence", sequence ?? [Output]()),
|
||||
("completion", completion)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard downstream != nil else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
pendingDemand += demand
|
||||
if recursion {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
|
||||
while let downstream = self.downstream, pendingDemand > 0 {
|
||||
if let current = self.next {
|
||||
pendingDemand -= 1
|
||||
let next = iterator.next()
|
||||
recursion = true
|
||||
lock.unlock()
|
||||
let additionalDemand = downstream.receive(current)
|
||||
lock.lock()
|
||||
recursion = false
|
||||
pendingDemand += additionalDemand
|
||||
self.next = next
|
||||
}
|
||||
|
||||
if next == nil {
|
||||
self.downstream = nil
|
||||
self.sequence = nil
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
downstream = nil
|
||||
sequence = nil
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,19 +44,3 @@ extension Result where Failure == Never {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An overload of `catching` that takes a non-thowing function and returns
|
||||
/// a function that returns an always succeeding `Result.`
|
||||
internal func catching<Input, Output, Failure: Error>(
|
||||
_ transform: @escaping (Input) -> Output
|
||||
) -> (Input) -> Result<Output, Failure> {
|
||||
return { input in .success(transform(input)) }
|
||||
}
|
||||
|
||||
/// Takes a function that may throw an error and returns a function that doesn't throw
|
||||
/// an error but returns `Result`.
|
||||
internal func catching<Input, Output>(
|
||||
_ transform: @escaping (Input) throws -> Output
|
||||
) -> (Input) -> Result<Output, Error> {
|
||||
return { input in Result { try transform(input) } }
|
||||
}
|
||||
|
||||
@@ -45,18 +45,3 @@ extension Subscriber where Input == Void {
|
||||
return receive(())
|
||||
}
|
||||
}
|
||||
|
||||
extension Optional where Wrapped: Subscriber {
|
||||
|
||||
internal func receive(subscription: Subscription) {
|
||||
self?.receive(subscription: subscription)
|
||||
}
|
||||
|
||||
internal func receive(_ input: Wrapped.Input) -> Subscribers.Demand {
|
||||
return self?.receive(input) ?? .none
|
||||
}
|
||||
|
||||
internal func receive(completion: Subscribers.Completion<Wrapped.Failure>) {
|
||||
self?.receive(completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ extension Subscribers {
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher where Self.Failure == Never {
|
||||
extension Publisher where Failure == Never {
|
||||
|
||||
/// Assigns each element from a Publisher to a property on an object.
|
||||
///
|
||||
|
||||
@@ -330,6 +330,7 @@ extension Subscribers {
|
||||
|
||||
/// Returns `true` if `lhs` and `rhs` are not equal. `.unlimited` is not equal to
|
||||
/// any integer.
|
||||
@inlinable
|
||||
public static func != (lhs: Demand, rhs: Int) -> Bool {
|
||||
if lhs == .unlimited {
|
||||
return true
|
||||
@@ -340,6 +341,7 @@ extension Subscribers {
|
||||
|
||||
/// Returns `true` if `lhs` and `rhs` are equal. `.unlimited` is not equal to any
|
||||
/// integer.
|
||||
@inlinable
|
||||
public static func == (lhs: Int, rhs: Demand) -> Bool {
|
||||
if rhs == .unlimited {
|
||||
return false
|
||||
@@ -350,6 +352,7 @@ extension Subscribers {
|
||||
|
||||
/// Returns `true` if `lhs` and `rhs` are not equal. `.unlimited` is not equal to
|
||||
/// any integer.
|
||||
@inlinable
|
||||
public static func != (lhs: Int, rhs: Demand) -> Bool {
|
||||
if rhs == .unlimited {
|
||||
return true
|
||||
@@ -358,8 +361,13 @@ extension Subscribers {
|
||||
}
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public static func == (lhs: Demand, rhs: Demand) -> Bool {
|
||||
return lhs.rawValue == rhs.rawValue
|
||||
}
|
||||
|
||||
/// Returns the number of requested values, or `nil` if `.unlimited`.
|
||||
public var max: Int? {
|
||||
@inlinable public var max: Int? {
|
||||
if self == .unlimited {
|
||||
return nil
|
||||
} else {
|
||||
|
||||
@@ -13,22 +13,29 @@ extension Subscriptions {
|
||||
///
|
||||
/// Use the empty subscription when you need a `Subscription` that ignores requests
|
||||
/// and cancellation.
|
||||
public static var empty: Subscription { return EmptySubscription.shared }
|
||||
public static let empty: Subscription = _EmptySubscription.singleton
|
||||
}
|
||||
|
||||
private final class EmptySubscription: Subscription,
|
||||
extension Subscriptions {
|
||||
private struct _EmptySubscription: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable
|
||||
{
|
||||
private init() {}
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
let combineIdentifier = CombineIdentifier()
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {}
|
||||
private init() {}
|
||||
|
||||
func cancel() {}
|
||||
func request(_ demand: Subscribers.Demand) {}
|
||||
|
||||
fileprivate static let shared = EmptySubscription()
|
||||
func cancel() {}
|
||||
|
||||
var description: String { return "Empty" }
|
||||
fileprivate static let singleton = _EmptySubscription()
|
||||
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
var description: String { return "Empty" }
|
||||
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,8 +50,12 @@ extension DispatchQueue {
|
||||
/// - Parameter other: Another dispatch queue time.
|
||||
/// - Returns: The time interval between this time and the provided time.
|
||||
public func distance(to other: SchedulerTimeType) -> Stride {
|
||||
let start = dispatchTime.rawValue
|
||||
let end = other.dispatchTime.rawValue
|
||||
return .nanoseconds(
|
||||
Int(other.dispatchTime.rawValue - dispatchTime.rawValue)
|
||||
end >= start
|
||||
? Int(Int64(bitPattern: end) - Int64(bitPattern: start))
|
||||
: -Int(Int64(bitPattern: start) - Int64(bitPattern: end))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -62,7 +66,9 @@ extension DispatchQueue {
|
||||
/// - Returns: A dispatch queue time advanced by the given
|
||||
/// interval from this instance’s time.
|
||||
public func advanced(by stride: Stride) -> SchedulerTimeType {
|
||||
return .init(dispatchTime + stride.timeInterval)
|
||||
return stride.magnitude == .max
|
||||
? .init(.distantFuture)
|
||||
: .init(dispatchTime + stride.timeInterval)
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
@@ -125,13 +131,52 @@ extension DispatchQueue {
|
||||
self = .microseconds(microseconds)
|
||||
case .nanoseconds(let nanoseconds):
|
||||
self = .nanoseconds(nanoseconds)
|
||||
// This dance is to avoid the warning 'default will never be executed'
|
||||
// on non-Darwin platforms.
|
||||
// There really shouldn't be a warning.
|
||||
// See https://forums.swift.org/t/unknown-default-produces-a-warning-on-linux-with-non-frozen-enum/31687
|
||||
//
|
||||
// Thanks to Jeremy David Giesbrecht for suggesting this workaround.
|
||||
#if canImport(Darwin)
|
||||
case .never:
|
||||
fallthrough
|
||||
@unknown default:
|
||||
self = .nanoseconds(.max)
|
||||
@unknown default:
|
||||
self.init(__guessFromUnknown: timeInterval)
|
||||
#else
|
||||
default:
|
||||
if case .never = timeInterval {
|
||||
self = .nanoseconds(.max)
|
||||
} else {
|
||||
self.init(__guessFromUnknown: timeInterval)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public // testable
|
||||
init(__guessFromUnknown timeInterval: DispatchTimeInterval) {
|
||||
// Let's take some reference time,
|
||||
// add `timeInterval` to it, take the `rawValue` from the result
|
||||
// and subtract the `rawValue` of the reference time.
|
||||
//
|
||||
// We won't be able to provide the exact implementation though,
|
||||
// because something will definitely overflow.
|
||||
//
|
||||
// However, we can try to support as wide a range of values
|
||||
// as possible.
|
||||
//
|
||||
// By trial and error I got that the `rawValue` of `UInt64.max / 13`
|
||||
// gives us propably the widest range of supported values:
|
||||
// from `Int.min / 6.5` to `Int.max / 2.889` nanoseconds.
|
||||
// That's with Int being 64 bits. Since here only UInt64 can overflow,
|
||||
// when Int is 32 bits, we don't have this issue.
|
||||
// It should be more than enough.
|
||||
|
||||
let referenceTime = DispatchTime(uptimeNanoseconds: .max / 13)
|
||||
self = SchedulerTimeType(referenceTime)
|
||||
.distance(to: SchedulerTimeType(referenceTime + timeInterval))
|
||||
}
|
||||
|
||||
/// Creates a dispatch queue time interval from a floating-point
|
||||
/// seconds value.
|
||||
///
|
||||
@@ -163,50 +208,43 @@ extension DispatchQueue {
|
||||
}
|
||||
|
||||
public static func * (lhs: Stride, rhs: Stride) -> Stride {
|
||||
// A bug in Combine, should be nanoseconds (FB7189676)
|
||||
return .seconds(lhs.magnitude * rhs.magnitude)
|
||||
return Stride(magnitude: lhs.magnitude * rhs.magnitude)
|
||||
}
|
||||
|
||||
public static func + (lhs: Stride, rhs: Stride) -> Stride {
|
||||
// A bug in Combine, should be nanoseconds (FB7189676)
|
||||
return .seconds(lhs.magnitude + rhs.magnitude)
|
||||
return Stride(magnitude: lhs.magnitude + rhs.magnitude)
|
||||
}
|
||||
|
||||
public static func - (lhs: Stride, rhs: Stride) -> Stride {
|
||||
// A bug in Combine, should be nanoseconds (FB7189676)
|
||||
return .seconds(lhs.magnitude - rhs.magnitude)
|
||||
return Stride(magnitude: lhs.magnitude - rhs.magnitude)
|
||||
}
|
||||
|
||||
// swiftlint:disable shorthand_operator
|
||||
|
||||
public static func -= (lhs: inout Stride, rhs: Stride) {
|
||||
lhs = lhs - rhs
|
||||
lhs.magnitude -= rhs.magnitude
|
||||
}
|
||||
|
||||
public static func *= (lhs: inout Stride, rhs: Stride) {
|
||||
lhs = lhs * rhs
|
||||
lhs.magnitude *= rhs.magnitude
|
||||
}
|
||||
|
||||
public static func += (lhs: inout Stride, rhs: Stride) {
|
||||
lhs = lhs + rhs
|
||||
lhs.magnitude += rhs.magnitude
|
||||
}
|
||||
|
||||
// swiftlint:enable shorthand_operator
|
||||
|
||||
public static func seconds(_ value: Double) -> Stride {
|
||||
return Stride(magnitude: Int(value * 1_000_000_000))
|
||||
}
|
||||
|
||||
public static func seconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: value * 1_000_000_000)
|
||||
return Stride(magnitude: clampedIntProduct(value, 1_000_000_000))
|
||||
}
|
||||
|
||||
public static func milliseconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: value * 1_000_000)
|
||||
return Stride(magnitude: clampedIntProduct(value, 1_000_000))
|
||||
}
|
||||
|
||||
public static func microseconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: value * 1_000)
|
||||
return Stride(magnitude: clampedIntProduct(value, 1_000))
|
||||
}
|
||||
|
||||
public static func nanoseconds(_ value: Int) -> Stride {
|
||||
@@ -343,3 +381,18 @@ extension DispatchQueue: OpenCombine.Scheduler {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// This function is taken from swift-corlibs-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].
|
||||
// Because of the way this function is used, we can always assume
|
||||
// that m2 > 0.
|
||||
private func clampedIntProduct(_ lhs: Int, _ rhs: Int) -> Int {
|
||||
assert(rhs > 0, "multiplier must be positive")
|
||||
let (result, overflow) = lhs.multipliedReportingOverflow(by: rhs)
|
||||
if overflow {
|
||||
return lhs > 0 ? .max : .min
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// Locking.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
import OpenCombine
|
||||
|
||||
internal typealias UnfairLock = __UnfairLock
|
||||
internal typealias UnfairRecursiveLock = __UnfairRecursiveLock
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// Violations.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 13.12.2019.
|
||||
//
|
||||
|
||||
import OpenCombine
|
||||
|
||||
extension Subscribers.Demand {
|
||||
internal func assertNonZero(file: StaticString = #file,
|
||||
line: UInt = #line) {
|
||||
if self == .none {
|
||||
fatalError("API Violation: demand must not be zero", file: file, line: line)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// JSONEncoder.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.10.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import OpenCombine
|
||||
|
||||
extension JSONEncoder: TopLevelEncoder {
|
||||
public typealias Output = Data
|
||||
}
|
||||
|
||||
extension JSONDecoder: TopLevelDecoder {
|
||||
public typealias Input = Data
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
//
|
||||
// NotificationCenter.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.10.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import OpenCombine
|
||||
|
||||
extension NotificationCenter {
|
||||
|
||||
/// A namespace for disambiguation when both OpenCombine and Foundation are imported.
|
||||
///
|
||||
/// Foundation extends `NotificationCenter` with new methods and nested types.
|
||||
/// If you import both OpenCombine and Foundation, you will not be able
|
||||
/// to write `NotificationCenter.Publisher`,
|
||||
/// because Swift is unable to understand which `Publisher`
|
||||
/// you're referring to — the one declared in Foundation or in OpenCombine.
|
||||
///
|
||||
/// So you have to write `NotificationCenter.OCombine.Publisher`.
|
||||
///
|
||||
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
|
||||
///
|
||||
/// You can omit this whenever Combine is not available (e. g. on Linux).
|
||||
public struct OCombine {
|
||||
|
||||
public let center: NotificationCenter
|
||||
|
||||
public init(_ center: NotificationCenter) {
|
||||
self.center = center
|
||||
}
|
||||
|
||||
/// A publisher that emits elements when broadcasting notifications.
|
||||
public struct Publisher: OpenCombine.Publisher {
|
||||
|
||||
public typealias Output = Notification
|
||||
|
||||
public typealias Failure = Never
|
||||
|
||||
/// The notification center this publisher uses as a source.
|
||||
public let center: NotificationCenter
|
||||
|
||||
/// The name of notifications published by this publisher.
|
||||
public let name: Notification.Name
|
||||
|
||||
/// The object posting the named notfication.
|
||||
public let object: AnyObject?
|
||||
|
||||
/// Creates a publisher that emits events when broadcasting notifications.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - center: The notification center to publish notifications for.
|
||||
/// - name: The name of the notification to publish.
|
||||
/// - object: The object posting the named notfication. If `nil`,
|
||||
/// the publisher emits elements for any object producing a notification
|
||||
/// with the given name.
|
||||
public init(center: NotificationCenter,
|
||||
name: Notification.Name,
|
||||
object: AnyObject? = nil) {
|
||||
self.center = center
|
||||
self.name = name
|
||||
self.object = object
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == Never, Downstream.Input == Notification
|
||||
{
|
||||
let subscription = Notification.Subscription(center: center,
|
||||
name: name,
|
||||
object: object,
|
||||
downstream: subscriber)
|
||||
subscriber.receive(subscription: subscription)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a publisher that emits events when broadcasting notifications.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - name: The name of the notification to publish.
|
||||
/// - object: The object posting the named notfication. If `nil`, the publisher
|
||||
/// emits elements for any object producing a notification with the given
|
||||
/// name.
|
||||
/// - Returns: A publisher that emits events when broadcasting notifications.
|
||||
public func publisher(for name: Notification.Name,
|
||||
object: AnyObject? = nil) -> Publisher {
|
||||
return .init(center: center, name: name, object: object)
|
||||
}
|
||||
}
|
||||
|
||||
#if !canImport(Combine)
|
||||
/// A publisher that emits elements when broadcasting notifications.
|
||||
public typealias Publisher = OCombine.Publisher
|
||||
#endif
|
||||
}
|
||||
|
||||
extension NotificationCenter {
|
||||
|
||||
/// A namespace for disambiguation when both OpenCombine and Foundation are imported.
|
||||
///
|
||||
/// Foundation extends `NotificationCenter` with new methods and nested types.
|
||||
/// If you import both OpenCombine and Foundation, you will not be able
|
||||
/// to write `NotificationCenter.default.publisher(for: name)`,
|
||||
/// because Swift is unable to understand which `publisher` method
|
||||
/// you're referring to — the one declared in Foundation or in OpenCombine.
|
||||
///
|
||||
/// So you have to write `NotificationCenter.default.ocombine.publisher(for: name)`.
|
||||
///
|
||||
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
|
||||
///
|
||||
/// You can omit this whenever Combine is not available (e. g. on Linux).
|
||||
public var ocombine: OCombine { return .init(self) }
|
||||
|
||||
#if !canImport(Combine)
|
||||
/// Returns a publisher that emits events when broadcasting notifications.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - name: The name of the notification to publish.
|
||||
/// - object: The object posting the named notfication. If `nil`, the publisher
|
||||
/// emits elements for any object producing a notification with the given name.
|
||||
/// - Returns: A publisher that emits events when broadcasting notifications.
|
||||
public func publisher(for name: Notification.Name,
|
||||
object: AnyObject? = nil) -> OCombine.Publisher {
|
||||
return ocombine.publisher(for: name, object: object)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
extension NotificationCenter.OCombine.Publisher: Equatable {
|
||||
public static func == (lhs: NotificationCenter.OCombine.Publisher,
|
||||
rhs: NotificationCenter.OCombine.Publisher) -> Bool {
|
||||
return lhs.center == rhs.center &&
|
||||
lhs.name == rhs.name &&
|
||||
lhs.object === rhs.object
|
||||
}
|
||||
}
|
||||
|
||||
extension Notification {
|
||||
fileprivate final class Subscription<Downstream: Subscriber>
|
||||
: OpenCombine.Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Notification, Downstream.Failure == Never
|
||||
{
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
fileprivate let downstreamLock = UnfairRecursiveLock.allocate()
|
||||
|
||||
fileprivate var demand = Subscribers.Demand.none
|
||||
|
||||
private var center: NotificationCenter?
|
||||
|
||||
private let name: Name
|
||||
|
||||
private var object: AnyObject?
|
||||
|
||||
private var observation: AnyObject?
|
||||
|
||||
fileprivate init(center: NotificationCenter,
|
||||
name: Notification.Name,
|
||||
object: AnyObject?,
|
||||
downstream: Downstream) {
|
||||
self.center = center
|
||||
self.name = name
|
||||
self.object = object
|
||||
self.observation = center
|
||||
.addObserver(forName: name, object: object, queue: nil) { [weak self] in
|
||||
self?.didReceiveNotification($0, downstream: downstream)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
downstreamLock.deallocate()
|
||||
}
|
||||
|
||||
private func didReceiveNotification(_ notification: Notification,
|
||||
downstream: Downstream) {
|
||||
lock.lock()
|
||||
guard demand > 0 else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
demand -= 1
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
let newDemand = downstream.receive(notification)
|
||||
downstreamLock.unlock()
|
||||
lock.lock()
|
||||
demand += newDemand
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
self.demand += demand
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard let center = self.center, let observation = self.observation else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
self.center = nil
|
||||
self.object = nil
|
||||
self.observation = nil
|
||||
lock.unlock()
|
||||
center.removeObserver(observation)
|
||||
}
|
||||
|
||||
fileprivate var description: String { return "NotificationCenter Observer" }
|
||||
|
||||
fileprivate var customMirror: Mirror {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
let children: [Mirror.Child] = [
|
||||
("center", center as Any),
|
||||
("name", name),
|
||||
("object", object as Any),
|
||||
("demand", demand)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
fileprivate var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// PropertyListEncoder.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import OpenCombine
|
||||
|
||||
extension PropertyListEncoder: TopLevelEncoder {
|
||||
public typealias Output = Data
|
||||
}
|
||||
|
||||
extension PropertyListDecoder: TopLevelDecoder {
|
||||
public typealias Input = Data
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
//
|
||||
// URLSession.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 13.12.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
import OpenCombine
|
||||
|
||||
extension URLSession {
|
||||
|
||||
/// A namespace for disambiguation when both OpenCombine and Foundation are imported.
|
||||
///
|
||||
/// Foundation extends `URLSession` with new methods and nested types.
|
||||
/// If you import both OpenCombine and Foundation, you will not be able
|
||||
/// to write `URLSession.DataTaskPublisher`,
|
||||
/// because Swift is unable to understand which `DataTaskPublisher`
|
||||
/// you're referring to — the one declared in Foundation or in OpenCombine.
|
||||
///
|
||||
/// So you have to write `URLSession.OCombine.DataTaskPublisher`.
|
||||
///
|
||||
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
|
||||
///
|
||||
/// You can omit this whenever Combine is not available (e. g. on Linux).
|
||||
public struct OCombine {
|
||||
|
||||
public let session: URLSession
|
||||
|
||||
public init(_ session: URLSession) {
|
||||
self.session = session
|
||||
}
|
||||
|
||||
public struct DataTaskPublisher: Publisher {
|
||||
|
||||
public typealias Output = (data: Data, response: URLResponse)
|
||||
|
||||
public typealias Failure = URLError
|
||||
|
||||
public let request: URLRequest
|
||||
|
||||
public let session: URLSession
|
||||
|
||||
public init(request: URLRequest, session: URLSession) {
|
||||
self.request = request
|
||||
self.session = session
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == Failure, Downstream.Input == Output
|
||||
{
|
||||
let subscription = Inner(parent: self, downstream: subscriber)
|
||||
subscriber.receive(subscription: subscription)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a publisher that wraps a URL session data task for a given URL.
|
||||
///
|
||||
/// The publisher publishes data when the task completes, or terminates if
|
||||
/// the task fails with an error.
|
||||
///
|
||||
/// - Parameter url: The URL for which to create a data task.
|
||||
/// - Returns: A publisher that wraps a data task for the URL.
|
||||
public func dataTaskPublisher(for url: URL) -> DataTaskPublisher {
|
||||
return dataTaskPublisher(for: URLRequest(url: url))
|
||||
}
|
||||
|
||||
/// Returns a publisher that wraps a URL session data task for a given
|
||||
/// URL request.
|
||||
///
|
||||
/// The publisher publishes data when the task completes, or terminates if
|
||||
/// the task fails with an error.
|
||||
///
|
||||
/// - Parameter request: The URL request for which to create a data task.
|
||||
/// - Returns: A publisher that wraps a data task for the URL request.
|
||||
public func dataTaskPublisher(for request: URLRequest) -> DataTaskPublisher {
|
||||
return .init(request: request, session: session)
|
||||
}
|
||||
}
|
||||
|
||||
#if !canImport(Combine)
|
||||
public typealias DataTaskPublisher = OCombine.DataTaskPublisher
|
||||
#endif
|
||||
}
|
||||
|
||||
extension URLSession {
|
||||
|
||||
/// A namespace for disambiguation when both OpenCombine and Foundation are imported.
|
||||
///
|
||||
/// Foundation extends `URLSession` with new methods and nested types.
|
||||
/// If you import both OpenCombine and Foundation, you will not be able
|
||||
/// to write `URLSession.shared.dataTaskPublisher(for: url)`,
|
||||
/// because Swift is unable to understand which `dataTaskPublisher` method
|
||||
/// you're referring to — the one declared in Foundation or in OpenCombine.
|
||||
///
|
||||
/// So you have to write `URLSession.shared.ocombine.dataTaskPublisher(for: url)`.
|
||||
///
|
||||
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
|
||||
///
|
||||
/// You can omit this whenever Combine is not available (e. g. on Linux).
|
||||
public var ocombine: OCombine { return .init(self) }
|
||||
|
||||
#if !canImport(Combine)
|
||||
/// Returns a publisher that wraps a URL session data task for a given URL.
|
||||
///
|
||||
/// The publisher publishes data when the task completes, or terminates if the task
|
||||
/// fails with an error.
|
||||
///
|
||||
/// - Parameter url: The URL for which to create a data task.
|
||||
/// - Returns: A publisher that wraps a data task for the URL.
|
||||
public func dataTaskPublisher(for url: URL) -> DataTaskPublisher {
|
||||
return ocombine.dataTaskPublisher(for: url)
|
||||
}
|
||||
|
||||
/// Returns a publisher that wraps a URL session data task for a given URL request.
|
||||
///
|
||||
/// The publisher publishes data when the task completes, or terminates if the task
|
||||
/// fails with an error.
|
||||
///
|
||||
/// - Parameter request: The URL request for which to create a data task.
|
||||
/// - Returns: A publisher that wraps a data task for the URL request.
|
||||
public func dataTaskPublisher(for request: URLRequest) -> DataTaskPublisher {
|
||||
return ocombine.dataTaskPublisher(for: request)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
extension URLSession.OCombine.DataTaskPublisher {
|
||||
private class Inner<Downstream: Subscriber>
|
||||
: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == (data: Data, response: URLResponse),
|
||||
Downstream.Failure == URLError
|
||||
{
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private var parent: URLSession.OCombine.DataTaskPublisher?
|
||||
|
||||
private var downstream: Downstream?
|
||||
|
||||
private var demand = Subscribers.Demand.none
|
||||
|
||||
private var task: URLSessionDataTask?
|
||||
|
||||
fileprivate init(parent: URLSession.OCombine.DataTaskPublisher,
|
||||
downstream: Downstream) {
|
||||
self.parent = parent
|
||||
self.downstream = downstream
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
lock.lock()
|
||||
guard let parent = self.parent else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
if self.task == nil {
|
||||
task = parent.session.dataTask(with: parent.request,
|
||||
completionHandler: handleResponse)
|
||||
}
|
||||
self.demand += demand
|
||||
let task = self.task
|
||||
lock.unlock()
|
||||
task?.resume()
|
||||
}
|
||||
|
||||
private func handleResponse(data: Data?, response: URLResponse?, error: Error?) {
|
||||
lock.lock()
|
||||
guard demand > 0, parent != nil, let downstream = self.downstream else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lockedTerminate()
|
||||
lock.unlock()
|
||||
switch (data, response, error) {
|
||||
case let (data, response?, nil):
|
||||
_ = downstream.receive((data ?? Data(), response))
|
||||
downstream.receive(completion: .finished)
|
||||
case let (_, _, error as URLError):
|
||||
downstream.receive(completion: .failure(error))
|
||||
default:
|
||||
downstream.receive(completion: .failure(URLError(.unknown)))
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard parent != nil else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
let task = self.task
|
||||
lockedTerminate()
|
||||
lock.unlock()
|
||||
task?.cancel()
|
||||
}
|
||||
|
||||
private func lockedTerminate() {
|
||||
parent = nil
|
||||
downstream = nil
|
||||
demand = .none
|
||||
task = nil
|
||||
}
|
||||
|
||||
var description: String { return "DataTaskPublisher" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
let children: [Mirror.Child] = [
|
||||
("task", task as Any),
|
||||
("downstream", downstream as Any),
|
||||
("parent", parent as Any),
|
||||
("demand", demand)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@
|
||||
// Created by Sergej Jaskiewicz on 13.06.2019.
|
||||
//
|
||||
|
||||
import GottaGoFast
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
@@ -15,7 +14,7 @@ import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class CombineIdentifierTests: PerformanceTestCase {
|
||||
final class CombineIdentifierTests: XCTestCase {
|
||||
|
||||
func testDefaultInitialized() {
|
||||
let id1 = CombineIdentifier()
|
||||
@@ -43,11 +42,9 @@ final class CombineIdentifierTests: PerformanceTestCase {
|
||||
"0x\(String(UInt(bitPattern: ObjectIdentifier(c1)), radix: 16))")
|
||||
}
|
||||
|
||||
func testDefaultInitializedPerformance() throws {
|
||||
try benchmark(allowFailure: isDebug, executionCount: 500) {
|
||||
for _ in 0..<2000 {
|
||||
blackHole(CombineIdentifier())
|
||||
}
|
||||
}
|
||||
func testUsesUInt64UnderTheHood() {
|
||||
let mirror = Mirror(reflecting: CombineIdentifier())
|
||||
XCTAssertEqual(mirror.children.count, 1)
|
||||
XCTAssertNotNil(mirror.descendant("value") as? UInt64)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,19 +23,34 @@ final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
func testSchedulerTimeTypeDistance() {
|
||||
let time1 = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10000))
|
||||
let time2 = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10431))
|
||||
let distantFuture = Scheduler.SchedulerTimeType(.distantFuture)
|
||||
let notSoDistantFuture = Scheduler.SchedulerTimeType(
|
||||
DispatchTime(
|
||||
uptimeNanoseconds: DispatchTime.distantFuture.uptimeNanoseconds - 1024
|
||||
)
|
||||
)
|
||||
|
||||
XCTAssertEqual(time1.distance(to: time2), .nanoseconds(431))
|
||||
XCTAssertEqual(time2.distance(to: time1), .nanoseconds(-431))
|
||||
|
||||
// A bug in Combine (FB7127210), caused by overflow on subtraction.
|
||||
// It should not crash. When they fix it, this test will fail and we'll know
|
||||
// that we need to update our implementation.
|
||||
assertCrashes {
|
||||
_ = time2.distance(to: time1)
|
||||
}
|
||||
XCTAssertEqual(time1.distance(to: distantFuture), .nanoseconds(-10001))
|
||||
XCTAssertEqual(distantFuture.distance(to: time1), .nanoseconds(10001))
|
||||
XCTAssertEqual(time2.distance(to: distantFuture), .nanoseconds(-10432))
|
||||
XCTAssertEqual(distantFuture.distance(to: time2), .nanoseconds(10432))
|
||||
|
||||
XCTAssertEqual(time1.distance(to: notSoDistantFuture), .nanoseconds(-11025))
|
||||
XCTAssertEqual(notSoDistantFuture.distance(to: time1), .nanoseconds(11025))
|
||||
XCTAssertEqual(time2.distance(to: notSoDistantFuture), .nanoseconds(-11456))
|
||||
XCTAssertEqual(notSoDistantFuture.distance(to: time2), .nanoseconds(11456))
|
||||
|
||||
XCTAssertEqual(distantFuture.distance(to: distantFuture), .nanoseconds(0))
|
||||
XCTAssertEqual(notSoDistantFuture.distance(to: notSoDistantFuture),
|
||||
.nanoseconds(0))
|
||||
}
|
||||
|
||||
func testSchedulerTimeTypeAdvanced() {
|
||||
let time = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10000))
|
||||
let beginningOfTime = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 1))
|
||||
let stride1 = Scheduler.SchedulerTimeType.Stride.nanoseconds(431)
|
||||
let stride2 = Scheduler.SchedulerTimeType.Stride.nanoseconds(-220)
|
||||
|
||||
@@ -44,6 +59,12 @@ final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(time.advanced(by: stride2),
|
||||
Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 9780)))
|
||||
|
||||
XCTAssertEqual(time.advanced(by: .nanoseconds(.max)).dispatchTime,
|
||||
.distantFuture)
|
||||
|
||||
XCTAssertEqual(beginningOfTime.advanced(by: .nanoseconds(-1000)).dispatchTime,
|
||||
DispatchTime(uptimeNanoseconds: 1))
|
||||
}
|
||||
|
||||
func testSchedulerTimeTypeEquatable() {
|
||||
@@ -58,10 +79,7 @@ final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
XCTAssertEqual(time1, time2)
|
||||
XCTAssertEqual(time2, time1)
|
||||
XCTAssertNotEqual(time1, time3)
|
||||
|
||||
assertCrashes {
|
||||
XCTAssertNotEqual(time3, time1)
|
||||
}
|
||||
XCTAssertNotEqual(time3, time1)
|
||||
}
|
||||
|
||||
func testSchedulerTimeTypeHashable() {
|
||||
@@ -93,73 +111,187 @@ final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
// MARK: - Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
func testStrideToDispatchTimeInterval() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
switch (Stride.seconds(12).timeInterval,
|
||||
Stride.milliseconds(34).timeInterval,
|
||||
Stride.microseconds(56).timeInterval,
|
||||
Stride.nanoseconds(78).timeInterval) {
|
||||
case (.nanoseconds(12000000000),
|
||||
.nanoseconds(34000000),
|
||||
.nanoseconds(56000),
|
||||
.nanoseconds(78)):
|
||||
switch (Stride.seconds(2).timeInterval,
|
||||
Stride.milliseconds(2).timeInterval,
|
||||
Stride.microseconds(2).timeInterval,
|
||||
Stride.nanoseconds(2).timeInterval,
|
||||
Stride.nanoseconds(.max).timeInterval) {
|
||||
case (.nanoseconds(2_000_000_000),
|
||||
.nanoseconds(2_000_000),
|
||||
.nanoseconds(2_000),
|
||||
.nanoseconds(2),
|
||||
.nanoseconds(.max)):
|
||||
break // pass
|
||||
case let intervals:
|
||||
XCTFail("Unexpected DispatchTimeInterval: \(intervals)")
|
||||
}
|
||||
}
|
||||
|
||||
func testStrideFromDispatchTimeInterval() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
func testStrideFromDispatchTimeInterval() throws {
|
||||
XCTAssertEqual(Stride(.seconds(2)).magnitude, 2_000_000_000)
|
||||
XCTAssertEqual(Stride(.milliseconds(2)).magnitude, 2_000_000)
|
||||
XCTAssertEqual(Stride(.microseconds(2)).magnitude, 2_000)
|
||||
XCTAssertEqual(Stride(.nanoseconds(2)).magnitude, 2)
|
||||
|
||||
XCTAssertEqual(Stride(.seconds(12)).magnitude, 12000000000)
|
||||
XCTAssertEqual(Stride(.milliseconds(34)).magnitude, 34000000)
|
||||
XCTAssertEqual(Stride(.microseconds(56)).magnitude, 56000)
|
||||
XCTAssertEqual(Stride(.nanoseconds(78)).magnitude, 78)
|
||||
XCTAssertEqual(Stride(.never).magnitude, .max)
|
||||
XCTAssertEqual(Stride(.nanoseconds(.max)).magnitude, .max)
|
||||
XCTAssertEqual(Stride(.nanoseconds(.min)).magnitude, .min)
|
||||
XCTAssertEqual(Stride(.microseconds(.max)).magnitude, .max)
|
||||
XCTAssertEqual(Stride(.microseconds(.min)).magnitude, .min)
|
||||
XCTAssertEqual(Stride(.milliseconds(.max)).magnitude, .max)
|
||||
XCTAssertEqual(Stride(.milliseconds(.min)).magnitude, .min)
|
||||
XCTAssertEqual(Stride(.seconds(.max)).magnitude, .max)
|
||||
XCTAssertEqual(Stride(.seconds(.min)).magnitude, .min)
|
||||
}
|
||||
|
||||
func testStrideFromUnknownDispatchTimeIntervalCase() {
|
||||
// Here we're testing out internal API that is not present in Combine.
|
||||
// Although we prefer only testing public APIs, this case is special.
|
||||
let makeStride: (DispatchTimeInterval) -> Stride
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
makeStride = Stride.init(_:)
|
||||
#else
|
||||
makeStride = Stride.init(__guessFromUnknown:)
|
||||
#endif
|
||||
|
||||
#if arch(x86_64) || arch(arm64) || arch(s390x) || arch(powerpc64) || arch(powerpc64le)
|
||||
// 64-bit platforms
|
||||
let minNanoseconds = -0x13B13B13B13B13B0 // Int64.min / 6.5
|
||||
let maxNanoseconds = 0x2C4EC4EC4EC4EC4D // Int64.max / 2.889
|
||||
#elseif arch(i386) || arch(arm)
|
||||
// 32-bit platforms
|
||||
let minNanoseconds = Int.min + 1
|
||||
let maxNanoseconds = Int.max
|
||||
#else
|
||||
#error("This architecture isn't known. Add it to the 32-bit or 64-bit line.")
|
||||
#endif
|
||||
XCTAssertEqual(makeStride(.nanoseconds(minNanoseconds)).magnitude, minNanoseconds)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(-128)).magnitude, -128)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(-57)).magnitude, -57)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(-33)).magnitude, -33)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(-17)).magnitude, -17)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(-8)).magnitude, -8)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(-3)).magnitude, -3)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(-1)).magnitude, -1)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(0)).magnitude, 0)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(1)).magnitude, 1)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(3)).magnitude, 3)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(8)).magnitude, 8)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(17)).magnitude, 17)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(33)).magnitude, 33)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(57)).magnitude, 57)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(128)).magnitude, 128)
|
||||
XCTAssertEqual(makeStride(.nanoseconds(maxNanoseconds)).magnitude, maxNanoseconds)
|
||||
|
||||
XCTAssertEqual(makeStride(.microseconds(-128)).magnitude, -128_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(-57)).magnitude, -57_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(-33)).magnitude, -33_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(-17)).magnitude, -17_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(-8)).magnitude, -8_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(-3)).magnitude, -3_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(-1)).magnitude, -1_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(0)).magnitude, 0)
|
||||
XCTAssertEqual(makeStride(.microseconds(1)).magnitude, 1_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(3)).magnitude, 3_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(8)).magnitude, 8_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(17)).magnitude, 17_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(33)).magnitude, 33_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(57)).magnitude, 57_000)
|
||||
XCTAssertEqual(makeStride(.microseconds(128)).magnitude, 128_000)
|
||||
|
||||
XCTAssertEqual(makeStride(.milliseconds(-128)).magnitude, -128_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(-57)).magnitude, -57_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(-33)).magnitude, -33_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(-17)).magnitude, -17_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(-8)).magnitude, -8_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(-3)).magnitude, -3_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(-1)).magnitude, -1_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(0)).magnitude, 0)
|
||||
XCTAssertEqual(makeStride(.milliseconds(1)).magnitude, 1_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(3)).magnitude, 3_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(8)).magnitude, 8_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(17)).magnitude, 17_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(33)).magnitude, 33_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(57)).magnitude, 57_000_000)
|
||||
XCTAssertEqual(makeStride(.milliseconds(128)).magnitude, 128_000_000)
|
||||
|
||||
XCTAssertEqual(makeStride(.seconds(-2)).magnitude, -2_000_000_000)
|
||||
XCTAssertEqual(makeStride(.seconds(-1)).magnitude, -1_000_000_000)
|
||||
XCTAssertEqual(makeStride(.seconds(0)).magnitude, 0)
|
||||
XCTAssertEqual(makeStride(.seconds(1)).magnitude, 1_000_000_000)
|
||||
XCTAssertEqual(makeStride(.seconds(2)).magnitude, 2_000_000_000)
|
||||
}
|
||||
|
||||
func testStrideFromNumericValue() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
XCTAssertEqual(Stride.seconds(1.2).magnitude, 1_200_000_000)
|
||||
XCTAssertEqual(Stride.seconds(2).magnitude, 2_000_000_000)
|
||||
XCTAssertEqual(Stride.milliseconds(2).magnitude, 2_000_000)
|
||||
XCTAssertEqual(Stride.microseconds(2).magnitude, 2_000)
|
||||
XCTAssertEqual(Stride.nanoseconds(2).magnitude, 2)
|
||||
|
||||
XCTAssertEqual(Stride.seconds(12.756).magnitude, 12756000000)
|
||||
XCTAssertEqual(Stride.seconds(34).magnitude, 34000000000)
|
||||
XCTAssertEqual(Stride.milliseconds(56).magnitude, 56000000)
|
||||
XCTAssertEqual(Stride.microseconds(78).magnitude, 78000)
|
||||
XCTAssertEqual(Stride.nanoseconds(90).magnitude, 90)
|
||||
#if arch(x86_64) || arch(arm64) || arch(s390x) || arch(powerpc64) || arch(powerpc64le)
|
||||
// 64-bit platforms
|
||||
XCTAssertEqual(
|
||||
Stride.seconds(Double(Int.max) / 1_000_000_000 - 1).magnitude,
|
||||
9223372035854776320
|
||||
)
|
||||
#elseif arch(i386) || arch(arm)
|
||||
// 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
|
||||
|
||||
XCTAssertEqual((12.756 as Stride).magnitude, 12756000000)
|
||||
XCTAssertEqual((34 as Stride).magnitude, 34000000000)
|
||||
XCTAssertEqual(Stride.seconds(.max).magnitude, .max)
|
||||
XCTAssertEqual(Stride.milliseconds(.max).magnitude, .max)
|
||||
XCTAssertEqual(Stride.microseconds(.max).magnitude, .max)
|
||||
XCTAssertEqual(Stride.nanoseconds(.max).magnitude, .max)
|
||||
|
||||
XCTAssertEqual((1.2 as Stride).magnitude, 1_200_000_000)
|
||||
XCTAssertEqual((2 as Stride).magnitude, 2_000_000_000)
|
||||
|
||||
XCTAssertNil(Stride(exactly: UInt64.max))
|
||||
XCTAssertEqual(Stride(exactly: 871 as UInt64)?.magnitude, 871)
|
||||
}
|
||||
|
||||
func testStrideComparable() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
func testStrideFromTooMuchSecondsCrashes() {
|
||||
assertCrashes {
|
||||
#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
|
||||
)
|
||||
#elseif arch(i386) || arch(arm)
|
||||
// 32-bit platforms
|
||||
XCTAssertGreaterThan(
|
||||
Stride.seconds(Double(Int.max) / 1_000_000_000 + 1).magnitude,
|
||||
.max
|
||||
)
|
||||
#else
|
||||
#error("This architecture isn't known. Add it to the 32-bit or 64-bit line.")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
func testStrideComparable() {
|
||||
XCTAssertLessThan(Stride.nanoseconds(1), .nanoseconds(2))
|
||||
XCTAssertGreaterThan(Stride.nanoseconds(-2), .microseconds(-10))
|
||||
XCTAssertLessThan(Stride.milliseconds(29), .seconds(29))
|
||||
XCTAssertLessThan(Stride.milliseconds(2), .seconds(2))
|
||||
}
|
||||
|
||||
func testStrideMultiplication() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
XCTAssertEqual((Stride.nanoseconds(0) * .nanoseconds(61346)).magnitude, 0)
|
||||
XCTAssertEqual((Stride.nanoseconds(61346) * .nanoseconds(0)).magnitude, 0)
|
||||
XCTAssertEqual((Stride.nanoseconds(18) * .nanoseconds(1)).magnitude,
|
||||
18000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(18) * .microseconds(1)).magnitude,
|
||||
18000000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(1) * .nanoseconds(18)).magnitude,
|
||||
18000000000)
|
||||
XCTAssertEqual((Stride.microseconds(1) * .nanoseconds(18)).magnitude,
|
||||
18000000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(15) * .nanoseconds(2)).magnitude,
|
||||
30000000000)
|
||||
XCTAssertEqual((Stride.microseconds(-3) * .nanoseconds(10)).magnitude,
|
||||
-30000000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(18) * .nanoseconds(1)).magnitude, 18)
|
||||
XCTAssertEqual((Stride.nanoseconds(18) * .microseconds(1)).magnitude, 18000)
|
||||
XCTAssertEqual((Stride.nanoseconds(1) * .nanoseconds(18)).magnitude, 18)
|
||||
XCTAssertEqual((Stride.microseconds(1) * .nanoseconds(18)).magnitude, 18000)
|
||||
XCTAssertEqual((Stride.nanoseconds(15) * .nanoseconds(2)).magnitude, 30)
|
||||
XCTAssertEqual((Stride.microseconds(-3) * .nanoseconds(10)).magnitude, -30000)
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(0)
|
||||
@@ -176,149 +308,131 @@ final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
do {
|
||||
var stride = Stride.nanoseconds(18)
|
||||
stride *= .nanoseconds(1)
|
||||
XCTAssertEqual(stride.magnitude, 18000000000)
|
||||
XCTAssertEqual(stride.magnitude, 18)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(18)
|
||||
stride *= .microseconds(1)
|
||||
XCTAssertEqual(stride.magnitude, 18000000000000)
|
||||
XCTAssertEqual(stride.magnitude, 18000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(1)
|
||||
stride *= .nanoseconds(18)
|
||||
XCTAssertEqual(stride.magnitude, 18000000000)
|
||||
XCTAssertEqual(stride.magnitude, 18)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.microseconds(1)
|
||||
stride *= .nanoseconds(18)
|
||||
XCTAssertEqual(stride.magnitude, 18000000000000)
|
||||
XCTAssertEqual(stride.magnitude, 18000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(15)
|
||||
stride *= .nanoseconds(2)
|
||||
XCTAssertEqual(stride.magnitude, 30000000000)
|
||||
XCTAssertEqual(stride.magnitude, 30)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.microseconds(-3)
|
||||
stride *= .nanoseconds(10)
|
||||
XCTAssertEqual(stride.magnitude, -30000000000000)
|
||||
XCTAssertEqual(stride.magnitude, -30000)
|
||||
}
|
||||
}
|
||||
|
||||
func testStrideAddition() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
XCTAssertEqual((Stride.nanoseconds(0) + .microseconds(2)).magnitude,
|
||||
2000000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(2) + .microseconds(0)).magnitude,
|
||||
2000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(7) + .nanoseconds(12)).magnitude,
|
||||
19000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(12) + .nanoseconds(7)).magnitude,
|
||||
19000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(7) + .nanoseconds(-12)).magnitude,
|
||||
-5000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(-12) + .nanoseconds(7)).magnitude,
|
||||
-5000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(0) + .microseconds(2)).magnitude, 2000)
|
||||
XCTAssertEqual((Stride.nanoseconds(2) + .microseconds(0)).magnitude, 2)
|
||||
XCTAssertEqual((Stride.nanoseconds(7) + .nanoseconds(12)).magnitude, 19)
|
||||
XCTAssertEqual((Stride.nanoseconds(12) + .nanoseconds(7)).magnitude, 19)
|
||||
XCTAssertEqual((Stride.nanoseconds(7) + .nanoseconds(-12)).magnitude, -5)
|
||||
XCTAssertEqual((Stride.nanoseconds(-12) + .nanoseconds(7)).magnitude, -5)
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(0)
|
||||
stride += .microseconds(2)
|
||||
XCTAssertEqual(stride.magnitude, 2000000000000)
|
||||
XCTAssertEqual(stride.magnitude, 2000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(2)
|
||||
stride += .microseconds(0)
|
||||
XCTAssertEqual(stride.magnitude, 2000000000)
|
||||
XCTAssertEqual(stride.magnitude, 2)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(7)
|
||||
stride += .nanoseconds(12)
|
||||
XCTAssertEqual(stride.magnitude, 19000000000)
|
||||
XCTAssertEqual(stride.magnitude, 19)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(12)
|
||||
stride += .nanoseconds(7)
|
||||
XCTAssertEqual(stride.magnitude, 19000000000)
|
||||
XCTAssertEqual(stride.magnitude, 19)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(7)
|
||||
stride += .nanoseconds(-12)
|
||||
XCTAssertEqual(stride.magnitude, -5000000000)
|
||||
XCTAssertEqual(stride.magnitude, -5)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(-12)
|
||||
stride += .nanoseconds(7)
|
||||
XCTAssertEqual(stride.magnitude, -5000000000)
|
||||
XCTAssertEqual(stride.magnitude, -5)
|
||||
}
|
||||
}
|
||||
|
||||
func testStrideSubtraction() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
XCTAssertEqual((Stride.nanoseconds(0) - .microseconds(2)).magnitude,
|
||||
-2000000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(2) - .microseconds(0)).magnitude,
|
||||
2000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(7) - .nanoseconds(12)).magnitude,
|
||||
-5000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(12) - .nanoseconds(7)).magnitude,
|
||||
5000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(7) - .nanoseconds(-12)).magnitude,
|
||||
19000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(-12) - .nanoseconds(7)).magnitude,
|
||||
-19000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(0) - .microseconds(2)).magnitude, -2000)
|
||||
XCTAssertEqual((Stride.nanoseconds(2) - .microseconds(0)).magnitude, 2)
|
||||
XCTAssertEqual((Stride.nanoseconds(7) - .nanoseconds(12)).magnitude, -5)
|
||||
XCTAssertEqual((Stride.nanoseconds(12) - .nanoseconds(7)).magnitude, 5)
|
||||
XCTAssertEqual((Stride.nanoseconds(7) - .nanoseconds(-12)).magnitude, 19)
|
||||
XCTAssertEqual((Stride.nanoseconds(-12) - .nanoseconds(7)).magnitude, -19)
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(0)
|
||||
stride -= .microseconds(2)
|
||||
XCTAssertEqual(stride.magnitude, -2000000000000)
|
||||
XCTAssertEqual(stride.magnitude, -2000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(2)
|
||||
stride -= .microseconds(0)
|
||||
XCTAssertEqual(stride.magnitude, 2000000000)
|
||||
XCTAssertEqual(stride.magnitude, 2)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(7)
|
||||
stride -= .nanoseconds(12)
|
||||
XCTAssertEqual(stride.magnitude, -5000000000)
|
||||
XCTAssertEqual(stride.magnitude, -5)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(12)
|
||||
stride -= .nanoseconds(7)
|
||||
XCTAssertEqual(stride.magnitude, 5000000000)
|
||||
XCTAssertEqual(stride.magnitude, 5)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(7)
|
||||
stride -= .nanoseconds(-12)
|
||||
XCTAssertEqual(stride.magnitude, 19000000000)
|
||||
XCTAssertEqual(stride.magnitude, 19)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(-12)
|
||||
stride -= .nanoseconds(7)
|
||||
XCTAssertEqual(stride.magnitude, -19000000000)
|
||||
XCTAssertEqual(stride.magnitude, -19)
|
||||
}
|
||||
}
|
||||
|
||||
func testStrideCodable() throws {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
@@ -456,6 +570,9 @@ private let backgroundScheduler = DispatchQueue.global(qos: .background).ocombin
|
||||
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
private struct KeyedWrapper<Value: Codable & Equatable>: Codable, Equatable {
|
||||
let value: Value
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
//
|
||||
// TopLevelDecoder+Extensions.swift
|
||||
//
|
||||
//
|
||||
// Created by Joseph Spadafora on 6/29/19.
|
||||
//
|
||||
|
||||
#if !OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Foundation
|
||||
import OpenCombine
|
||||
|
||||
extension JSONDecoder: TopLevelDecoder {}
|
||||
extension JSONEncoder: TopLevelEncoder {}
|
||||
|
||||
extension PropertyListDecoder: TopLevelDecoder {}
|
||||
extension PropertyListEncoder: TopLevelEncoder {}
|
||||
#endif
|
||||
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// JSONDecoderTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
import OpenCombineFoundation
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class JSONDecoderTests: XCTestCase {
|
||||
func testSuccessfullyDecode() {
|
||||
let decoder = JSONDecoder()
|
||||
let input = #"[{"success":true}]"#
|
||||
var actualOutput: [Subscribers.Completion<TestingError>]?
|
||||
var actualCompletion: Subscribers.Completion<Error>?
|
||||
let cancellable = Just(input)
|
||||
.map { Data($0.utf8) }
|
||||
.decode(type: [Subscribers.Completion<TestingError>].self, decoder: decoder)
|
||||
.sink(receiveCompletion: { actualCompletion = $0 },
|
||||
receiveValue: { actualOutput = $0 })
|
||||
switch actualCompletion {
|
||||
case .finished?:
|
||||
XCTAssertEqual(actualOutput, [.finished])
|
||||
case .failure(let error)?:
|
||||
XCTFail("Unexpected failure received: \(error)")
|
||||
case nil:
|
||||
XCTFail("Expected completion")
|
||||
}
|
||||
cancellable.cancel()
|
||||
}
|
||||
|
||||
func testDecodingFailure() {
|
||||
let decoder = JSONDecoder()
|
||||
let input = #"{"a":1,"b":2}"#
|
||||
var actualOutput: [Int]?
|
||||
var actualCompletion: Subscribers.Completion<Error>?
|
||||
let cancellable = Just(input)
|
||||
.map { Data($0.utf8) }
|
||||
.decode(type: [Int].self, decoder: decoder)
|
||||
.sink(receiveCompletion: { actualCompletion = $0 },
|
||||
receiveValue: { actualOutput = $0 })
|
||||
switch actualCompletion {
|
||||
case .finished?:
|
||||
XCTFail("Unexpected success")
|
||||
case .failure(DecodingError.typeMismatch)?:
|
||||
XCTAssertNil(actualOutput)
|
||||
case .failure(let error)?:
|
||||
XCTFail("Unexpected failure received: \(error)")
|
||||
case nil:
|
||||
XCTFail("Expected completion")
|
||||
}
|
||||
cancellable.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// JSONEncoderTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
import OpenCombineFoundation
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class JSONEncoderTests: XCTestCase {
|
||||
|
||||
func testSuccessfullyEncode() {
|
||||
let encoder = JSONEncoder()
|
||||
let input = [Subscribers.Completion<TestingError>.finished]
|
||||
var actualOutput: String?
|
||||
var actualCompletion: Subscribers.Completion<Error>?
|
||||
|
||||
let cancellable = Just(input)
|
||||
.encode(encoder: encoder)
|
||||
.map { String(decoding: $0, as: UTF8.self) }
|
||||
.sink(receiveCompletion: { actualCompletion = $0 },
|
||||
receiveValue: { actualOutput = $0 })
|
||||
|
||||
switch actualCompletion {
|
||||
case .finished?:
|
||||
XCTAssertEqual(actualOutput, #"[{"success":true}]"#)
|
||||
case .failure(let error)?:
|
||||
XCTFail("Unexpected failure received: \(error)")
|
||||
case nil:
|
||||
XCTFail("Expected completion")
|
||||
}
|
||||
cancellable.cancel()
|
||||
}
|
||||
|
||||
func testEncodingFailure() {
|
||||
let encoder = JSONEncoder()
|
||||
let input = Double.nan
|
||||
var actualOutput: String?
|
||||
var actualCompletion: Subscribers.Completion<Error>?
|
||||
let cancellable = Just(input)
|
||||
.encode(encoder: encoder)
|
||||
.map { String(decoding: $0, as: UTF8.self) }
|
||||
.sink(receiveCompletion: { actualCompletion = $0 },
|
||||
receiveValue: { actualOutput = $0 })
|
||||
|
||||
switch actualCompletion {
|
||||
case .finished?:
|
||||
XCTFail("Unexpected success")
|
||||
case .failure(EncodingError.invalidValue)?:
|
||||
XCTAssertNil(actualOutput)
|
||||
case .failure(let error)?:
|
||||
XCTFail("Unexpected failure received: \(error)")
|
||||
case nil:
|
||||
XCTFail("Expected completion")
|
||||
}
|
||||
cancellable.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,625 @@
|
||||
//
|
||||
// NotificationCenterTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
import OpenCombineFoundation
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class NotificationCenterTests: XCTestCase {
|
||||
|
||||
func testRequestingDemand() {
|
||||
|
||||
let initialDemands: [Subscribers.Demand?] = [
|
||||
nil,
|
||||
.max(1),
|
||||
.max(2),
|
||||
.max(10),
|
||||
.unlimited
|
||||
]
|
||||
|
||||
let subsequentDemands: [[Subscribers.Demand]] = [
|
||||
Array(repeating: .max(0), count: 5),
|
||||
Array(repeating: .max(1), count: 10),
|
||||
[.max(1), .max(0), .max(1), .max(0)],
|
||||
[.max(0), .max(1), .max(2)],
|
||||
[.unlimited, .max(1)]
|
||||
]
|
||||
|
||||
var numberOfInputsHistory: [Int] = []
|
||||
let expectedNumberOfInputsHistory = [
|
||||
0, 0, 0, 0, 0, 1, 11, 2, 1, 20, 2, 12, 4, 5, 20, 10, 20, 12, 13, 20, 20,
|
||||
20, 20, 20, 20
|
||||
]
|
||||
|
||||
for initialDemand in initialDemands {
|
||||
for subsequentDemand in subsequentDemands {
|
||||
|
||||
var i = 0
|
||||
|
||||
let center = TestNotificationCenter()
|
||||
let name = Notification.Name(rawValue: "testName")
|
||||
let publisher = makePublisher(center, for: name, object: nil)
|
||||
|
||||
let subscriber = TrackingSubscriberBase<Notification, Never>(
|
||||
receiveSubscription: { initialDemand.map($0.request) },
|
||||
receiveValue: { _ in
|
||||
defer { i += 1 }
|
||||
return i < subsequentDemand.endIndex ? subsequentDemand[i] : .none
|
||||
}
|
||||
)
|
||||
|
||||
XCTAssertEqual(subscriber.subscriptions.count, 0)
|
||||
XCTAssertEqual(subscriber.inputs.count, 0)
|
||||
XCTAssertEqual(subscriber.completions.count, 0)
|
||||
|
||||
publisher.subscribe(subscriber)
|
||||
|
||||
XCTAssertEqual(subscriber.subscriptions.count, 1)
|
||||
XCTAssertEqual(subscriber.inputs.count, 0)
|
||||
XCTAssertEqual(subscriber.completions.count, 0)
|
||||
|
||||
for _ in 0..<20 {
|
||||
center.post(name: name, object: TestObject.two)
|
||||
}
|
||||
|
||||
XCTAssertEqual(subscriber.subscriptions.count, 1)
|
||||
XCTAssertEqual(subscriber.completions.count, 0)
|
||||
|
||||
numberOfInputsHistory.append(subscriber.inputs.count)
|
||||
}
|
||||
}
|
||||
|
||||
XCTAssertEqual(numberOfInputsHistory, expectedNumberOfInputsHistory)
|
||||
}
|
||||
|
||||
func testBasicBehavior() throws {
|
||||
let center = TestNotificationCenter()
|
||||
let name = Notification.Name(rawValue: "testName")
|
||||
let publisher = makePublisher(center, for: name, object: TestObject.one)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriberBase<Notification, Never>(
|
||||
receiveSubscription: { downstreamSubscription = $0 }
|
||||
)
|
||||
|
||||
XCTAssertEqual(center.history, [])
|
||||
|
||||
publisher.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, TestObject.one, nil)])
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("NotificationCenter Observer")])
|
||||
|
||||
let note = Notification(name: name, object: TestObject.one, userInfo: nil)
|
||||
center.post(note)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, TestObject.one, nil),
|
||||
.postNotification(note)])
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("NotificationCenter Observer")])
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(3))
|
||||
|
||||
center.post(note)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, TestObject.one, nil),
|
||||
.postNotification(note),
|
||||
.postNotification(note)])
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("NotificationCenter Observer"),
|
||||
.value(note)])
|
||||
|
||||
let unrelatedNote1 = Notification(name: Notification.Name("unrelatedNote1"),
|
||||
object: TestObject.one,
|
||||
userInfo: nil)
|
||||
center.post(unrelatedNote1)
|
||||
|
||||
let unrelatedNote2 = Notification(name: name,
|
||||
object: TestObject.two,
|
||||
userInfo: nil)
|
||||
center.post(unrelatedNote2)
|
||||
center.post(name: name, object: nil)
|
||||
center.post(name: name, object: nil)
|
||||
center.post(name: name, object: TestObject.one)
|
||||
center.post(name: name, object: TestObject.one)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, TestObject.one, nil),
|
||||
.postNotification(note),
|
||||
.postNotification(note),
|
||||
.postNotification(unrelatedNote1),
|
||||
.postNotification(unrelatedNote2),
|
||||
.postNotificationWithName(name, nil, nil),
|
||||
.postNotification(Notification(name: name)),
|
||||
.postNotificationWithName(name, nil, nil),
|
||||
.postNotification(Notification(name: name)),
|
||||
.postNotificationWithName(name, TestObject.one, nil),
|
||||
.postNotification(Notification(name: name,
|
||||
object: TestObject.one)),
|
||||
.postNotificationWithName(name, TestObject.one, nil),
|
||||
.postNotification(Notification(name: name,
|
||||
object: TestObject.one))])
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("NotificationCenter Observer"),
|
||||
.value(note),
|
||||
.value(unrelatedNote1),
|
||||
.value(unrelatedNote2)])
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).request(.unlimited)
|
||||
|
||||
center.post(note)
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
|
||||
center.post(note)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, TestObject.one, nil),
|
||||
.postNotification(note),
|
||||
.postNotification(note),
|
||||
.postNotification(unrelatedNote1),
|
||||
.postNotification(unrelatedNote2),
|
||||
.postNotificationWithName(name, nil, nil),
|
||||
.postNotification(Notification(name: name)),
|
||||
.postNotificationWithName(name, nil, nil),
|
||||
.postNotification(Notification(name: name)),
|
||||
.postNotificationWithName(name, TestObject.one, nil),
|
||||
.postNotification(Notification(name: name,
|
||||
object: TestObject.one)),
|
||||
.postNotificationWithName(name, TestObject.one, nil),
|
||||
.postNotification(Notification(name: name,
|
||||
object: TestObject.one)),
|
||||
.postNotification(note),
|
||||
.removeObserver,
|
||||
.removeObserverForName(nil, nil),
|
||||
.postNotification(note)])
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("NotificationCenter Observer"),
|
||||
.value(note),
|
||||
.value(unrelatedNote1),
|
||||
.value(unrelatedNote2),
|
||||
.value(note)])
|
||||
}
|
||||
|
||||
func testBasicBehaviorNilObject() throws {
|
||||
let center = TestNotificationCenter()
|
||||
let name = Notification.Name(rawValue: "testName")
|
||||
let publisher = makePublisher(center, for: name, object: nil)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriberBase<Notification, Never>(
|
||||
receiveSubscription: { downstreamSubscription = $0 }
|
||||
)
|
||||
|
||||
XCTAssertEqual(center.history, [])
|
||||
|
||||
publisher.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, nil, nil)])
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("NotificationCenter Observer")])
|
||||
|
||||
let note = Notification(name: name, object: TestObject.one, userInfo: nil)
|
||||
center.post(note)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, nil, nil),
|
||||
.postNotification(note)])
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("NotificationCenter Observer")])
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(3))
|
||||
|
||||
center.post(note)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, nil, nil),
|
||||
.postNotification(note),
|
||||
.postNotification(note)])
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("NotificationCenter Observer"),
|
||||
.value(note)])
|
||||
|
||||
let unrelatedNote = Notification(name: Notification.Name("unrelatedNote"),
|
||||
object: TestObject.one,
|
||||
userInfo: nil)
|
||||
center.post(unrelatedNote)
|
||||
center.post(name: name, object: nil)
|
||||
center.post(name: name, object: nil)
|
||||
center.post(name: name, object: TestObject.one)
|
||||
center.post(name: name, object: TestObject.one)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, nil, nil),
|
||||
.postNotification(note),
|
||||
.postNotification(note),
|
||||
.postNotification(unrelatedNote),
|
||||
.postNotificationWithName(name, nil, nil),
|
||||
.postNotification(Notification(name: name)),
|
||||
.postNotificationWithName(name, nil, nil),
|
||||
.postNotification(Notification(name: name)),
|
||||
.postNotificationWithName(name, TestObject.one, nil),
|
||||
.postNotification(note),
|
||||
.postNotificationWithName(name, TestObject.one, nil),
|
||||
.postNotification(note)])
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("NotificationCenter Observer"),
|
||||
.value(note),
|
||||
.value(unrelatedNote),
|
||||
.value(Notification(name: name))])
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).request(.unlimited)
|
||||
|
||||
center.post(note)
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
|
||||
center.post(note)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, nil, nil),
|
||||
.postNotification(note),
|
||||
.postNotification(note),
|
||||
.postNotification(unrelatedNote),
|
||||
.postNotificationWithName(name, nil, nil),
|
||||
.postNotification(Notification(name: name)),
|
||||
.postNotificationWithName(name, nil, nil),
|
||||
.postNotification(Notification(name: name)),
|
||||
.postNotificationWithName(name, TestObject.one, nil),
|
||||
.postNotification(note),
|
||||
.postNotificationWithName(name, TestObject.one, nil),
|
||||
.postNotification(note),
|
||||
.postNotification(note),
|
||||
.removeObserver,
|
||||
.removeObserverForName(nil, nil),
|
||||
.postNotification(note)])
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("NotificationCenter Observer"),
|
||||
.value(note),
|
||||
.value(unrelatedNote),
|
||||
.value(Notification(name: name)),
|
||||
.value(note)])
|
||||
}
|
||||
|
||||
func testRecursivelyReceiveValue() throws {
|
||||
let center = TestNotificationCenter()
|
||||
let name = Notification.Name(rawValue: "testName")
|
||||
let publisher = makePublisher(center, for: name, object: nil)
|
||||
let tracking = TrackingSubscriberBase<Notification, Never>(
|
||||
receiveSubscription: { $0.request(.max(3)) },
|
||||
receiveValue: { _ in .unlimited }
|
||||
)
|
||||
publisher.subscribe(tracking)
|
||||
|
||||
let note = Notification(name: name)
|
||||
var recursionCounter = 7
|
||||
tracking.onValue = { _ in
|
||||
if recursionCounter == 0 { return }
|
||||
recursionCounter -= 1
|
||||
center.post(note)
|
||||
}
|
||||
|
||||
center.post(note)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("NotificationCenter Observer"),
|
||||
.value(note),
|
||||
.value(note),
|
||||
.value(note)])
|
||||
|
||||
center.post(note)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("NotificationCenter Observer"),
|
||||
.value(note),
|
||||
.value(note),
|
||||
.value(note),
|
||||
.value(note),
|
||||
.value(note),
|
||||
.value(note),
|
||||
.value(note),
|
||||
.value(note)])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let center = TestNotificationCenter()
|
||||
let name = Notification.Name(rawValue: "testName")
|
||||
let publisher = makePublisher(center, for: name, object: nil)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriberBase<Notification, Never>(
|
||||
receiveSubscription: { downstreamSubscription = $0 }
|
||||
)
|
||||
publisher.subscribe(tracking)
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("NotificationCenter Observer")])
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, nil, nil),
|
||||
.removeObserver,
|
||||
.removeObserverForName(nil, nil)])
|
||||
}
|
||||
|
||||
func testCancellingReleasesNotificationCenter() throws {
|
||||
var centerDestroyed = false
|
||||
var downstreamSubscription: Subscription?
|
||||
do {
|
||||
let center = TestNotificationCenter()
|
||||
center.onDeinit = { centerDestroyed = true }
|
||||
let name = Notification.Name(rawValue: "testName")
|
||||
let publisher = makePublisher(center, for: name, object: nil)
|
||||
let tracking = TrackingSubscriberBase<Notification, Never>(
|
||||
receiveSubscription: { downstreamSubscription = $0 }
|
||||
)
|
||||
publisher.subscribe(tracking)
|
||||
}
|
||||
XCTAssertFalse(centerDestroyed)
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
XCTAssertTrue(centerDestroyed)
|
||||
}
|
||||
|
||||
func testWeakCaptureWhenAddingObserver() {
|
||||
let center = TestNotificationCenter()
|
||||
let name = Notification.Name("testName")
|
||||
var value: Notification?
|
||||
do {
|
||||
let publisher = makePublisher(center, for: name, object: nil)
|
||||
let tracking = TrackingSubscriberBase<Notification, Never>(
|
||||
receiveSubscription: { $0.request(.max(1)) },
|
||||
receiveValue: { value = $0; return .none }
|
||||
)
|
||||
publisher.subscribe(tracking)
|
||||
tracking.clearHistory() // Release the subscription
|
||||
}
|
||||
center.post(name: name, object: nil)
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, nil, nil),
|
||||
.postNotificationWithName(name, nil, nil),
|
||||
.postNotification(Notification(name: name))])
|
||||
XCTAssertNil(value)
|
||||
}
|
||||
|
||||
func testZeroDemand() throws {
|
||||
let center = TestNotificationCenter()
|
||||
let name = Notification.Name(rawValue: "testName")
|
||||
let publisher = makePublisher(center, for: name, object: nil)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriberBase<Notification, Never>(
|
||||
receiveSubscription: { downstreamSubscription = $0 }
|
||||
)
|
||||
publisher.subscribe(tracking)
|
||||
try XCTUnwrap(downstreamSubscription).request(.none)
|
||||
|
||||
XCTAssertEqual(center.history,
|
||||
[.addObserver(name, nil, nil)])
|
||||
XCTAssertEqual(tracking.history, [.subscription("NotificationCenter Observer")])
|
||||
}
|
||||
|
||||
func testNotificationCenterSubscriptionReflection() throws {
|
||||
let center = TestNotificationCenter()
|
||||
let name = Notification.Name(rawValue: "testName")
|
||||
let object = TestObject.one
|
||||
let publisher = makePublisher(center, for: name, object: object)
|
||||
|
||||
try testSubscriptionReflection(
|
||||
description: "NotificationCenter Observer",
|
||||
customMirror: expectedChildren(
|
||||
("center", .matches(String(describing: Optional(center)))),
|
||||
("name", .contains(String(describing: name))),
|
||||
("object", .matches(String(describing: Optional(object)))),
|
||||
("demand", "max(0)")
|
||||
),
|
||||
playgroundDescription: "NotificationCenter Observer",
|
||||
sut: publisher
|
||||
)
|
||||
}
|
||||
|
||||
func testEquatable() {
|
||||
let center1 = NotificationCenter()
|
||||
let center2 = NotificationCenter()
|
||||
let name1 = Notification.Name(rawValue: "abcdefg")
|
||||
let name2 = Notification.Name(rawValue: "1234567")
|
||||
let object1 = TestObject.one
|
||||
let object2 = TestObject.two
|
||||
|
||||
XCTAssertEqual(makePublisher(center1, for: name1, object: object1),
|
||||
makePublisher(center1, for: name1, object: object1))
|
||||
XCTAssertEqual(makePublisher(center2, for: name2, object: object2),
|
||||
makePublisher(center2, for: name2, object: object2))
|
||||
XCTAssertEqual(makePublisher(center1, for: name1, object: nil),
|
||||
makePublisher(center1, for: name1, object: nil))
|
||||
XCTAssertNotEqual(makePublisher(center1, for: name1, object: object1),
|
||||
makePublisher(center1, for: name1, object: nil))
|
||||
XCTAssertNotEqual(makePublisher(center1, for: name1, object: nil),
|
||||
makePublisher(center1, for: name1, object: object2))
|
||||
XCTAssertNotEqual(makePublisher(center1, for: name1, object: object1),
|
||||
makePublisher(center1, for: name1, object: object2))
|
||||
XCTAssertNotEqual(makePublisher(center1, for: name1, object: object1),
|
||||
makePublisher(center1, for: name2, object: object1))
|
||||
XCTAssertNotEqual(makePublisher(center1, for: name1, object: object1),
|
||||
makePublisher(center2, for: name1, object: object1))
|
||||
}
|
||||
}
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private func makePublisher(
|
||||
_ center: NotificationCenter,
|
||||
for name: Notification.Name,
|
||||
object: AnyObject?
|
||||
) -> NotificationCenter.Publisher {
|
||||
return center.publisher(for: name, object: object)
|
||||
}
|
||||
#else
|
||||
private func makePublisher(
|
||||
_ center: NotificationCenter,
|
||||
for name: Notification.Name,
|
||||
object: AnyObject?
|
||||
) -> NotificationCenter.OCombine.Publisher {
|
||||
return center.ocombine.publisher(for: name, object: object)
|
||||
}
|
||||
#endif
|
||||
|
||||
/// A simple mock notification center that always sends notifications to **all**
|
||||
/// observers in non-thread safe manner.
|
||||
private final class TestNotificationCenter: NotificationCenter {
|
||||
|
||||
enum Event {
|
||||
case postNotificationWithName(Notification.Name, Any?, [AnyHashable : Any]?)
|
||||
case postNotification(Notification)
|
||||
case addObserver(Notification.Name?, Any?, OperationQueue?)
|
||||
case removeObserver
|
||||
case removeObserverForName(Notification.Name?, Any?)
|
||||
}
|
||||
|
||||
private final class Observation {
|
||||
let callback: (Notification) -> Void
|
||||
|
||||
init(callback: @escaping (Notification) -> Void) {
|
||||
self.callback = callback
|
||||
}
|
||||
}
|
||||
|
||||
private final class Token: NSObject {
|
||||
weak var observation: Observation?
|
||||
|
||||
init(observer: TestNotificationCenter.Observation) {
|
||||
self.observation = observer
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var history = [Event]()
|
||||
|
||||
private var observations: [Observation] = []
|
||||
|
||||
var onDeinit: (() -> Void)?
|
||||
|
||||
deinit {
|
||||
onDeinit?()
|
||||
}
|
||||
|
||||
override func post(name aName: Notification.Name,
|
||||
object anObject: Any?,
|
||||
userInfo aUserInfo: [AnyHashable : Any]? = nil) {
|
||||
history.append(.postNotificationWithName(aName, anObject, aUserInfo))
|
||||
let notification = Notification(name: aName,
|
||||
object: anObject,
|
||||
userInfo: aUserInfo)
|
||||
post(notification)
|
||||
}
|
||||
|
||||
override func post(_ notification: Notification) {
|
||||
history.append(.postNotification(notification))
|
||||
for observation in observations {
|
||||
observation.callback(notification)
|
||||
}
|
||||
}
|
||||
|
||||
override func addObserver(
|
||||
forName name: NSNotification.Name?,
|
||||
object obj: Any?,
|
||||
queue: OperationQueue?,
|
||||
using block: @escaping (Notification) -> Void
|
||||
) -> NSObjectProtocol {
|
||||
history.append(.addObserver(name, obj, queue))
|
||||
let observer = Observation(callback: block)
|
||||
observations.append(observer)
|
||||
return Token(observer: observer)
|
||||
}
|
||||
|
||||
override func removeObserver(_ observer: Any) {
|
||||
history.append(.removeObserver)
|
||||
removeObserver(observer, name: nil, object: nil)
|
||||
}
|
||||
|
||||
override func removeObserver(_ observer: Any,
|
||||
name aName: NSNotification.Name?,
|
||||
object anObject: Any?) {
|
||||
history.append(.removeObserverForName(aName, anObject))
|
||||
guard let observer = observer as? Token else { return }
|
||||
observations.removeAll { $0 === observer.observation }
|
||||
}
|
||||
}
|
||||
|
||||
private final class TestObject: NSObject {
|
||||
|
||||
static let one = TestObject()
|
||||
|
||||
static let two = TestObject()
|
||||
}
|
||||
|
||||
extension TestNotificationCenter.Event: Equatable {
|
||||
fileprivate static func == (lhs: TestNotificationCenter.Event,
|
||||
rhs: TestNotificationCenter.Event) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (.postNotification(lhsNote), .postNotification(rhsNote)):
|
||||
return lhsNote == rhsNote
|
||||
case let (.postNotificationWithName(lhsName,
|
||||
lhsObject as TestObject?,
|
||||
lhsUserInfo),
|
||||
.postNotificationWithName(rhsName,
|
||||
rhsObject as TestObject?,
|
||||
rhsUserInfo)):
|
||||
return lhsName == rhsName &&
|
||||
lhsObject === rhsObject &&
|
||||
(lhsUserInfo == nil) == (rhsUserInfo == nil)
|
||||
case let (.addObserver(lhsName, lhsObject as TestObject?, lhsQueue),
|
||||
.addObserver(rhsName, rhsObject as TestObject?, rhsQueue)):
|
||||
return lhsName == rhsName &&
|
||||
lhsObject === rhsObject &&
|
||||
lhsQueue == rhsQueue
|
||||
case (.removeObserver, .removeObserver):
|
||||
return true
|
||||
case let (.removeObserverForName(lhsName, lhsObject as TestObject?),
|
||||
.removeObserverForName(rhsName, rhsObject as TestObject?)):
|
||||
return lhsName == rhsName && lhsObject === rhsObject
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TestNotificationCenter.Event: CustomStringConvertible {
|
||||
var description: String {
|
||||
switch self {
|
||||
case let .postNotificationWithName(name, object, userInfo):
|
||||
return """
|
||||
.postNotificationWithName(\
|
||||
.init(rawValue: \"\(name.rawValue)\"), \
|
||||
\(object.map(String.init(describing:)) ?? "nil"), \
|
||||
\(userInfo.map(String.init(describing:)) ?? "nil"))
|
||||
"""
|
||||
case .postNotification:
|
||||
return ".postNotification(note)"
|
||||
case let .addObserver(name, object, queue):
|
||||
let nameDescription = name.map { ".init(rawValue: \($0.rawValue))" } ?? "nil"
|
||||
return """
|
||||
.addObserver(\
|
||||
\(nameDescription), \
|
||||
\(object.map(String.init(describing:)) ?? "nil"), \
|
||||
\(queue.map(String.init(describing:)) ?? "nil"))
|
||||
"""
|
||||
case .removeObserver:
|
||||
return ".removeObserver"
|
||||
case let .removeObserverForName(name, object):
|
||||
let nameDescription = name.map { ".init(rawValue: \($0.rawValue))" } ?? "nil"
|
||||
return """
|
||||
.removeObserverForName(\
|
||||
\(nameDescription), \
|
||||
\(object.map(String.init(describing:)) ?? "nil"))
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
//
|
||||
// PropertyListDecoderTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
import OpenCombineFoundation
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class PropertyListDecoderTests: XCTestCase {
|
||||
func testSuccessfullyDecode() {
|
||||
let decoder = PropertyListDecoder()
|
||||
let input = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" \
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<array>
|
||||
\t<dict>
|
||||
\t\t<key>success</key>
|
||||
\t\t<true/>
|
||||
\t</dict>
|
||||
</array>
|
||||
</plist>
|
||||
|
||||
"""
|
||||
var actualOutput: [Subscribers.Completion<TestingError>]?
|
||||
var actualCompletion: Subscribers.Completion<Error>?
|
||||
let cancellable = Just(input)
|
||||
.map { Data($0.utf8) }
|
||||
.decode(type: [Subscribers.Completion<TestingError>].self, decoder: decoder)
|
||||
.sink(receiveCompletion: { actualCompletion = $0 },
|
||||
receiveValue: { actualOutput = $0 })
|
||||
switch actualCompletion {
|
||||
case .finished?:
|
||||
XCTAssertEqual(actualOutput, [.finished])
|
||||
case .failure(let error)?:
|
||||
XCTFail("Unexpected failure received: \(error)")
|
||||
case nil:
|
||||
XCTFail("Expected completion")
|
||||
}
|
||||
cancellable.cancel()
|
||||
}
|
||||
|
||||
func testDecodingFailure() {
|
||||
let decoder = PropertyListDecoder()
|
||||
let input = "000000"
|
||||
var actualOutput: [Int]?
|
||||
var actualCompletion: Subscribers.Completion<Error>?
|
||||
let cancellable = Just(input)
|
||||
.map { Data($0.utf8) }
|
||||
.decode(type: [Int].self, decoder: decoder)
|
||||
.sink(receiveCompletion: { actualCompletion = $0 },
|
||||
receiveValue: { actualOutput = $0 })
|
||||
switch actualCompletion {
|
||||
case .finished?:
|
||||
XCTFail("Unexpected success")
|
||||
case .failure(DecodingError.typeMismatch)?:
|
||||
XCTAssertNil(actualOutput)
|
||||
case .failure(let error)?:
|
||||
XCTFail("Unexpected failure received: \(error)")
|
||||
case nil:
|
||||
XCTFail("Expected completion")
|
||||
}
|
||||
cancellable.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
//
|
||||
// PropertyListEncoderTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
import OpenCombineFoundation
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class PropertyListEncoderTests: XCTestCase {
|
||||
|
||||
func testSuccessfullyEncode() {
|
||||
let encoder = PropertyListEncoder()
|
||||
encoder.outputFormat = .xml
|
||||
let input = [Subscribers.Completion<TestingError>.finished]
|
||||
var actualOutput: String?
|
||||
var actualCompletion: Subscribers.Completion<Error>?
|
||||
|
||||
let cancellable = Just(input)
|
||||
.encode(encoder: encoder)
|
||||
.map { String(decoding: $0, as: UTF8.self) }
|
||||
.sink(receiveCompletion: { actualCompletion = $0 },
|
||||
receiveValue: { actualOutput = $0 })
|
||||
|
||||
switch actualCompletion {
|
||||
case .finished?:
|
||||
XCTAssertEqual(actualOutput, """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" \
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<array>
|
||||
\t<dict>
|
||||
\t\t<key>success</key>
|
||||
\t\t<true/>
|
||||
\t</dict>
|
||||
</array>
|
||||
</plist>
|
||||
|
||||
""")
|
||||
case .failure(let error)?:
|
||||
XCTFail("Unexpected failure received: \(error)")
|
||||
case nil:
|
||||
XCTFail("Expected completion")
|
||||
}
|
||||
cancellable.cancel()
|
||||
}
|
||||
|
||||
func testEncodingFailure() {
|
||||
let encoder = PropertyListEncoder()
|
||||
let input = Double.nan
|
||||
var actualOutput: String?
|
||||
var actualCompletion: Subscribers.Completion<Error>?
|
||||
let cancellable = Just(input)
|
||||
.encode(encoder: encoder)
|
||||
.map { String(decoding: $0, as: UTF8.self) }
|
||||
.sink(receiveCompletion: { actualCompletion = $0 },
|
||||
receiveValue: { actualOutput = $0 })
|
||||
|
||||
switch actualCompletion {
|
||||
case .finished?:
|
||||
XCTFail("Unexpected success")
|
||||
case .failure(EncodingError.invalidValue)?:
|
||||
XCTAssertNil(actualOutput)
|
||||
case .failure(let error)?:
|
||||
XCTFail("Unexpected failure received: \(error)")
|
||||
case nil:
|
||||
XCTFail("Expected completion")
|
||||
}
|
||||
cancellable.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,724 @@
|
||||
//
|
||||
// URLSessionTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 13.12.2019.
|
||||
//
|
||||
|
||||
// swiftlint:disable multiline_arguments
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
// We can't test it on non-Darwin platforms because swift-corelibs-foundation
|
||||
// doesn't allow us to override some URLSession methods that we need.
|
||||
//
|
||||
// As soon as https://github.com/apple/swift-corelibs-foundation/pull/2587 makes it
|
||||
// into a release, we can enable these tests on non-Darwin platforms.
|
||||
//
|
||||
// The publisher itself though should work alright on those platforms.
|
||||
#if canImport(Darwin)
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
import OpenCombineFoundation
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class URLSessionTests: XCTestCase {
|
||||
|
||||
private typealias TrackingSubscriber =
|
||||
TrackingSubscriberBase<(data: Data, response: URLResponse), URLError>
|
||||
|
||||
private let testURL = URL(string: "https://github.com")!
|
||||
|
||||
private let testRequest = URLRequest(url: URL(string: "https://github.com")!,
|
||||
cachePolicy: .reloadIgnoringCacheData,
|
||||
timeoutInterval: 42)
|
||||
|
||||
private let testData = Data("test data".utf8)
|
||||
|
||||
private let testResponse = URLResponse(url: URL(string: "https://example.com")!,
|
||||
mimeType: "text/markdown",
|
||||
expectedContentLength: 300,
|
||||
textEncodingName: "utf-8")
|
||||
|
||||
private let testError = URLError(.cannotParseResponse, userInfo: ["a" : 1])
|
||||
|
||||
private let unknownError = URLError(.unknown)
|
||||
|
||||
func testDataTaskPublisherFromURL() {
|
||||
let publisher = makePublisher(TestURLSession(testDataTask: .init()), testURL)
|
||||
let expectedRequest = URLRequest(url: testURL)
|
||||
XCTAssertEqual(publisher.request, expectedRequest)
|
||||
}
|
||||
|
||||
func testDataTaskPublisherFromRequest() {
|
||||
let publisher = makePublisher(TestURLSession(testDataTask: .init()), testRequest)
|
||||
XCTAssertEqual(publisher.request, testRequest)
|
||||
}
|
||||
|
||||
func testReceiveNothing() {
|
||||
testReceiveResult(nil, nil, nil,
|
||||
expected: [.subscription("DataTaskPublisher"),
|
||||
.completion(.failure(unknownError))])
|
||||
}
|
||||
|
||||
func testReceiveOnlyData() {
|
||||
testReceiveResult(testData, nil, nil,
|
||||
expected: [.subscription("DataTaskPublisher"),
|
||||
.completion(.failure(unknownError))])
|
||||
}
|
||||
|
||||
func testReceiveDataAndResponse() {
|
||||
testReceiveResult(testData, testResponse, nil,
|
||||
expected: [.subscription("DataTaskPublisher"),
|
||||
.value((testData, testResponse)),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testReceiveDataAndURLError() {
|
||||
testReceiveResult(testData, nil, testError,
|
||||
expected: [.subscription("DataTaskPublisher"),
|
||||
.completion(.failure(testError))])
|
||||
}
|
||||
|
||||
func testReceiveDataAndUnrelatedError() {
|
||||
testReceiveResult(testData, nil, TestingError.oops,
|
||||
expected: [.subscription("DataTaskPublisher"),
|
||||
.completion(.failure(unknownError))])
|
||||
}
|
||||
|
||||
func testReceiveOnlyResponse() {
|
||||
testReceiveResult(nil, testResponse, nil,
|
||||
expected: [.subscription("DataTaskPublisher"),
|
||||
.value((Data(), testResponse)),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testReceiveResponseAndURLError() {
|
||||
testReceiveResult(nil, testResponse, testError,
|
||||
expected: [.subscription("DataTaskPublisher"),
|
||||
.completion(.failure(testError))])
|
||||
}
|
||||
|
||||
func testReceiveResponseAndUnrelatedError() {
|
||||
testReceiveResult(nil, testResponse, TestingError.oops,
|
||||
expected: [.subscription("DataTaskPublisher"),
|
||||
.completion(.failure(unknownError))])
|
||||
}
|
||||
|
||||
func testReceiveOnlyURLError() {
|
||||
testReceiveResult(nil, nil, testError,
|
||||
expected: [.subscription("DataTaskPublisher"),
|
||||
.completion(.failure(testError))])
|
||||
}
|
||||
|
||||
func testReceiveOnlyUnrelatedError() {
|
||||
testReceiveResult(nil, nil, TestingError.oops,
|
||||
expected: [.subscription("DataTaskPublisher"),
|
||||
.completion(.failure(unknownError))])
|
||||
}
|
||||
|
||||
func testRequesting() throws {
|
||||
let dataTask = TestURLSessionDataTask()
|
||||
let session = TestURLSession(testDataTask: dataTask)
|
||||
let publisher = makePublisher(session, testRequest)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { downstreamSubscription = $0 }
|
||||
)
|
||||
publisher.subscribe(tracking)
|
||||
|
||||
tracking.assertHistoryEqual([.subscription("DataTaskPublisher")],
|
||||
valueComparator: ==)
|
||||
XCTAssertEqual(dataTask.history, [])
|
||||
XCTAssertEqual(session.history, [])
|
||||
|
||||
session.completeDataTasks(testData, testResponse, nil)
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(2))
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(1))
|
||||
|
||||
tracking.assertHistoryEqual([.subscription("DataTaskPublisher")],
|
||||
valueComparator: ==)
|
||||
XCTAssertEqual(dataTask.history, [.resume, .resume])
|
||||
XCTAssertEqual(session.history, [.dataTaskWithRequestAndCompletion(testRequest)])
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
|
||||
tracking.assertHistoryEqual([.subscription("DataTaskPublisher")],
|
||||
valueComparator: ==)
|
||||
XCTAssertEqual(dataTask.history, [.resume, .resume, .cancel])
|
||||
XCTAssertEqual(session.history, [.dataTaskWithRequestAndCompletion(testRequest)])
|
||||
|
||||
session.completeDataTasks(testData, testResponse, nil)
|
||||
|
||||
tracking.assertHistoryEqual([.subscription("DataTaskPublisher")],
|
||||
valueComparator: ==)
|
||||
XCTAssertEqual(dataTask.history, [.resume, .resume, .cancel])
|
||||
XCTAssertEqual(session.history, [.dataTaskWithRequestAndCompletion(testRequest)])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let dataTask = TestURLSessionDataTask()
|
||||
let session = TestURLSession(testDataTask: dataTask)
|
||||
let publisher = makePublisher(session, testRequest)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { downstreamSubscription = $0 }
|
||||
)
|
||||
publisher.subscribe(tracking)
|
||||
|
||||
tracking.assertHistoryEqual([.subscription("DataTaskPublisher")],
|
||||
valueComparator: ==)
|
||||
XCTAssertEqual(dataTask.history, [])
|
||||
XCTAssertEqual(session.history, [])
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(1))
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(1))
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
|
||||
tracking.assertHistoryEqual([.subscription("DataTaskPublisher")],
|
||||
valueComparator: ==)
|
||||
XCTAssertEqual(dataTask.history, [.resume, .cancel])
|
||||
XCTAssertEqual(session.history, [.dataTaskWithRequestAndCompletion(testRequest)])
|
||||
}
|
||||
|
||||
func testCrashesOnZeroDemand() throws {
|
||||
let dataTask = TestURLSessionDataTask()
|
||||
let session = TestURLSession(testDataTask: dataTask)
|
||||
let publisher = makePublisher(session, testURL)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { downstreamSubscription = $0 }
|
||||
)
|
||||
publisher.subscribe(tracking)
|
||||
|
||||
try assertCrashes {
|
||||
try XCTUnwrap(downstreamSubscription).request(.none)
|
||||
}
|
||||
}
|
||||
|
||||
func testURLSessionSubscriptionReflection() throws {
|
||||
let dataTask = TestURLSessionDataTask()
|
||||
let session = TestURLSession(testDataTask: dataTask)
|
||||
let publisher = makePublisher(session, testURL)
|
||||
try testSubscriptionReflection(
|
||||
description: "DataTaskPublisher",
|
||||
customMirror: expectedChildren(
|
||||
("task", "nil"),
|
||||
("downstream", .contains("TrackingSubscriberBase")),
|
||||
("parent", .matches(String(describing: Optional(publisher)))),
|
||||
("demand", "max(0)")
|
||||
),
|
||||
playgroundDescription: "DataTaskPublisher",
|
||||
sut: publisher
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Generic tests
|
||||
|
||||
private func testReceiveResult(_ data: Data?,
|
||||
_ response: URLResponse?,
|
||||
_ error: Error?,
|
||||
expected: [TrackingSubscriber.Event]) {
|
||||
let dataTask = TestURLSessionDataTask()
|
||||
let session = TestURLSession(testDataTask: dataTask)
|
||||
let publisher = makePublisher(session, testRequest)
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.max(1)) })
|
||||
publisher.subscribe(tracking)
|
||||
|
||||
tracking.assertHistoryEqual([.subscription("DataTaskPublisher")],
|
||||
valueComparator: ==)
|
||||
XCTAssertEqual(dataTask.history, [.resume])
|
||||
XCTAssertEqual(session.history, [.dataTaskWithRequestAndCompletion(testRequest)])
|
||||
|
||||
session.completeDataTasks(data, response, error)
|
||||
session.completeDataTasks(data, response, error)
|
||||
session.completeDataTasks(data, response, error)
|
||||
|
||||
tracking.assertHistoryEqual(expected, valueComparator: ==)
|
||||
XCTAssertEqual(dataTask.history, [.resume])
|
||||
XCTAssertEqual(session.history, [.dataTaskWithRequestAndCompletion(testRequest)])
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple mock URLSession that records its history and allows executing
|
||||
/// callbacks synchronously
|
||||
private class TestURLSession: URLSession {
|
||||
|
||||
enum Event: Equatable {
|
||||
case delegateQueue
|
||||
case delegate
|
||||
case configuration
|
||||
case getSessionDescription
|
||||
case setSessionDescription(String?)
|
||||
case finishTasksAndInvalidate
|
||||
case invalidateAndCancel
|
||||
case reset
|
||||
case flush
|
||||
case getTasksWithCompletionHandler
|
||||
case getAllTasks
|
||||
case dataTaskWithRequest(URLRequest)
|
||||
case dataTaskWithRequestAndCompletion(URLRequest)
|
||||
case dataTaskWithURL(URL)
|
||||
case dataTaskWithURLAndCompletion(URL)
|
||||
case uploadTaskWithRequestFromFile(URLRequest, URL)
|
||||
case uploadTaskWithRequestFromFileWithCompletion(URLRequest, URL)
|
||||
case uploadTaskWithRequestFromData(URLRequest, Data)
|
||||
case uploadTaskWithRequestFromDataWithCompletion(URLRequest, Data?)
|
||||
case uploadTaskWithStreamedRequest(URLRequest)
|
||||
case downloadTaskWithRequest(URLRequest)
|
||||
case downloadTaskWithRequestAndCompletion(URLRequest)
|
||||
case downloadTaskWithURL(URL)
|
||||
case downloadTaskWithURLAndCompletion(URL)
|
||||
case downloadTaskWithResumeData(Data)
|
||||
case downloadTaskWithResumeDataAndCompletion(Data)
|
||||
case streamTaskWithHostNameAndPort(String, Int)
|
||||
#if canImport(Darwin) && swift(>=5.1)
|
||||
case streamTaskWithService(NetService)
|
||||
case webSocketTaskWithURL(URL)
|
||||
case webSocketTaskWithURLAndProtocols(URL, [String])
|
||||
case webSocketTaskWithRequest(URLRequest)
|
||||
#endif // canImport(Darwin) && swift(>=5.1)
|
||||
}
|
||||
|
||||
private(set) var history = [Event]()
|
||||
|
||||
private(set) var dataTaskCompletionHandlers: [(Data?, URLResponse?, Error?) -> Void]
|
||||
|
||||
private let testDataTask: TestURLSessionDataTask
|
||||
|
||||
init(testDataTask: TestURLSessionDataTask) {
|
||||
self.testDataTask = testDataTask
|
||||
self.dataTaskCompletionHandlers = []
|
||||
}
|
||||
|
||||
// MARK: Testing
|
||||
|
||||
func completeDataTasks(_ data: Data?, _ response: URLResponse?, _ error: Error?) {
|
||||
for completionHandler in dataTaskCompletionHandlers {
|
||||
completionHandler(data, response, error)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Overrides
|
||||
|
||||
override class var shared: URLSession { fatalError("shared session is unavailable") }
|
||||
|
||||
override var delegateQueue: OperationQueue {
|
||||
history.append(.delegateQueue)
|
||||
return super.delegateQueue
|
||||
}
|
||||
|
||||
override var delegate: URLSessionDelegate? {
|
||||
history.append(.delegate)
|
||||
return super.delegate
|
||||
}
|
||||
|
||||
override var configuration: URLSessionConfiguration {
|
||||
history.append(.configuration)
|
||||
return super.configuration
|
||||
}
|
||||
|
||||
override var sessionDescription: String? {
|
||||
get {
|
||||
history.append(.getSessionDescription)
|
||||
return super.sessionDescription
|
||||
}
|
||||
set {
|
||||
history.append(.setSessionDescription(newValue))
|
||||
super.sessionDescription = newValue
|
||||
}
|
||||
}
|
||||
|
||||
override func finishTasksAndInvalidate() {
|
||||
history.append(.finishTasksAndInvalidate)
|
||||
super.finishTasksAndInvalidate()
|
||||
}
|
||||
|
||||
override func invalidateAndCancel() {
|
||||
history.append(.invalidateAndCancel)
|
||||
super.invalidateAndCancel()
|
||||
}
|
||||
|
||||
override func reset(completionHandler: @escaping () -> Void) {
|
||||
history.append(.reset)
|
||||
super.reset(completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
override func flush(completionHandler: @escaping () -> Void) {
|
||||
history.append(.flush)
|
||||
super.flush(completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
override func getTasksWithCompletionHandler(
|
||||
_ completionHandler: @escaping ([URLSessionDataTask],
|
||||
[URLSessionUploadTask],
|
||||
[URLSessionDownloadTask]) -> Void
|
||||
) {
|
||||
history.append(.getTasksWithCompletionHandler)
|
||||
super.getTasksWithCompletionHandler(completionHandler)
|
||||
}
|
||||
|
||||
@available(macOS 10.11, iOS 9.0, *)
|
||||
override func getAllTasks(completionHandler: @escaping ([URLSessionTask]) -> Void) {
|
||||
history.append(.getAllTasks)
|
||||
super.getAllTasks(completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
override func dataTask(with request: URLRequest) -> URLSessionDataTask {
|
||||
history.append(.dataTaskWithRequest(request))
|
||||
return testDataTask
|
||||
}
|
||||
|
||||
override func dataTask(with url: URL) -> URLSessionDataTask {
|
||||
history.append(.dataTaskWithURL(url))
|
||||
return testDataTask
|
||||
}
|
||||
|
||||
override func dataTask(
|
||||
with url: URL,
|
||||
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void
|
||||
) -> URLSessionDataTask {
|
||||
history.append(.dataTaskWithURLAndCompletion(url))
|
||||
dataTaskCompletionHandlers.append(completionHandler)
|
||||
return testDataTask
|
||||
}
|
||||
|
||||
override func dataTask(
|
||||
with request: URLRequest,
|
||||
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void
|
||||
) -> URLSessionDataTask {
|
||||
history.append(.dataTaskWithRequestAndCompletion(request))
|
||||
dataTaskCompletionHandlers.append(completionHandler)
|
||||
return testDataTask
|
||||
}
|
||||
|
||||
override func uploadTask(with request: URLRequest,
|
||||
fromFile fileURL: URL) -> URLSessionUploadTask {
|
||||
history.append(.uploadTaskWithRequestFromFile(request, fileURL))
|
||||
return super.uploadTask(with: request, fromFile: fileURL)
|
||||
}
|
||||
|
||||
override func uploadTask(with request: URLRequest,
|
||||
from bodyData: Data) -> URLSessionUploadTask {
|
||||
history.append(.uploadTaskWithRequestFromData(request, bodyData))
|
||||
return super.uploadTask(with: request, from: bodyData)
|
||||
}
|
||||
|
||||
override func uploadTask(
|
||||
withStreamedRequest request: URLRequest
|
||||
) -> URLSessionUploadTask {
|
||||
history.append(.uploadTaskWithStreamedRequest(request))
|
||||
return super.uploadTask(withStreamedRequest: request)
|
||||
}
|
||||
|
||||
override func uploadTask(
|
||||
with request: URLRequest,
|
||||
fromFile fileURL: URL,
|
||||
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void
|
||||
) -> URLSessionUploadTask {
|
||||
history.append(.uploadTaskWithRequestFromFileWithCompletion(request, fileURL))
|
||||
return super.uploadTask(with: request,
|
||||
fromFile: fileURL,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
override func uploadTask(
|
||||
with request: URLRequest,
|
||||
from bodyData: Data?,
|
||||
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void
|
||||
) -> URLSessionUploadTask {
|
||||
history.append(.uploadTaskWithRequestFromDataWithCompletion(request, bodyData))
|
||||
return super.uploadTask(with: request,
|
||||
from: bodyData,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
override func downloadTask(with request: URLRequest) -> URLSessionDownloadTask {
|
||||
history.append(.downloadTaskWithRequest(request))
|
||||
return super.downloadTask(with: request)
|
||||
}
|
||||
|
||||
override func downloadTask(with url: URL) -> URLSessionDownloadTask {
|
||||
history.append(.downloadTaskWithURL(url))
|
||||
return super.downloadTask(with: url)
|
||||
}
|
||||
|
||||
override func downloadTask(
|
||||
with request: URLRequest,
|
||||
completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void
|
||||
) -> URLSessionDownloadTask {
|
||||
history.append(.downloadTaskWithRequestAndCompletion(request))
|
||||
return super.downloadTask(with: request, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
override func downloadTask(
|
||||
with url: URL,
|
||||
completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void
|
||||
) -> URLSessionDownloadTask {
|
||||
history.append(.downloadTaskWithURLAndCompletion(url))
|
||||
return super.downloadTask(with: url, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
override func downloadTask(
|
||||
withResumeData resumeData: Data,
|
||||
completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void
|
||||
) -> URLSessionDownloadTask {
|
||||
history.append(.downloadTaskWithResumeDataAndCompletion(resumeData))
|
||||
return super.downloadTask(withResumeData: resumeData,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
override func downloadTask(
|
||||
withResumeData resumeData: Data
|
||||
) -> URLSessionDownloadTask {
|
||||
history.append(.downloadTaskWithResumeData(resumeData))
|
||||
return super.downloadTask(withResumeData: resumeData)
|
||||
}
|
||||
|
||||
@available(macOS 10.11, iOS 9.0, *)
|
||||
override func streamTask(withHostName hostname: String,
|
||||
port: Int) -> URLSessionStreamTask {
|
||||
history.append(.streamTaskWithHostNameAndPort(hostname, port))
|
||||
return super.streamTask(withHostName: hostname, port: port)
|
||||
}
|
||||
|
||||
#if canImport(Darwin) && swift(>=5.1)
|
||||
@available(macOS 10.11, iOS 9.0, *)
|
||||
override func streamTask(with service: NetService) -> URLSessionStreamTask {
|
||||
history.append(.streamTaskWithService(service))
|
||||
return super.streamTask(with: service)
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
override func webSocketTask(with url: URL) -> URLSessionWebSocketTask {
|
||||
history.append(.webSocketTaskWithURL(url))
|
||||
return super.webSocketTask(with: url)
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
override func webSocketTask(with url: URL,
|
||||
protocols: [String]) -> URLSessionWebSocketTask {
|
||||
history.append(.webSocketTaskWithURLAndProtocols(url, protocols))
|
||||
return super.webSocketTask(with: url, protocols: protocols)
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
override func webSocketTask(with request: URLRequest) -> URLSessionWebSocketTask {
|
||||
history.append(.webSocketTaskWithRequest(request))
|
||||
return super.webSocketTask(with: request)
|
||||
}
|
||||
#endif // canImport(Darwin) && swift(>=5.1)
|
||||
}
|
||||
|
||||
private final class TestURLSessionDataTask: URLSessionDataTask {
|
||||
|
||||
enum Event: Equatable {
|
||||
case taskIdentifier
|
||||
case originalRequest
|
||||
case currentRequest
|
||||
case response
|
||||
case progress
|
||||
case getEarliestBeginDate
|
||||
case setEarliestBeginDate(Date?)
|
||||
case getCountOfBytesClientExpectsToSend
|
||||
case setCountOfBytesClientExpectsToSend(Int64)
|
||||
case getCountOfBytesClientExpectsToReceive
|
||||
case setCountOfBytesClientExpectsToReceive(Int64)
|
||||
case countOfBytesReceived
|
||||
case countOfBytesSent
|
||||
case countOfBytesExpectedToSend
|
||||
case countOfBytesExpectedToReceive
|
||||
case getTaskDescription
|
||||
case setTaskDescription(String?)
|
||||
case cancel
|
||||
case state
|
||||
case error
|
||||
case suspend
|
||||
case resume
|
||||
case getPriority
|
||||
case setPriority(Float)
|
||||
}
|
||||
|
||||
private(set) var history = [Event]()
|
||||
|
||||
override init() {}
|
||||
|
||||
override var taskIdentifier: Int {
|
||||
history.append(.taskIdentifier)
|
||||
return super.taskIdentifier
|
||||
}
|
||||
|
||||
override var originalRequest: URLRequest? {
|
||||
history.append(.originalRequest)
|
||||
return super.originalRequest
|
||||
}
|
||||
|
||||
override var currentRequest: URLRequest? {
|
||||
history.append(.currentRequest)
|
||||
return super.currentRequest
|
||||
}
|
||||
|
||||
override var response: URLResponse? {
|
||||
history.append(.response)
|
||||
return super.response
|
||||
}
|
||||
|
||||
@available(macOS 10.13, iOS 11.0, *)
|
||||
override var progress: Progress {
|
||||
history.append(.progress)
|
||||
return super.progress
|
||||
}
|
||||
|
||||
@available(macOS 10.13, iOS 11.0, *)
|
||||
override var earliestBeginDate: Date? {
|
||||
get {
|
||||
history.append(.getEarliestBeginDate)
|
||||
#if canImport(Darwin)
|
||||
return super.earliestBeginDate
|
||||
#else
|
||||
return nil // Deprecated in swift-corerlibs-foundation
|
||||
#endif
|
||||
}
|
||||
set {
|
||||
history.append(.setEarliestBeginDate(newValue))
|
||||
#if canImport(Darwin)
|
||||
super.earliestBeginDate = newValue
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.13, iOS 11.0, *)
|
||||
override var countOfBytesClientExpectsToSend: Int64 {
|
||||
get {
|
||||
history.append(.getCountOfBytesClientExpectsToSend)
|
||||
return super.countOfBytesClientExpectsToSend
|
||||
}
|
||||
set {
|
||||
history.append(.setCountOfBytesClientExpectsToSend(newValue))
|
||||
super.countOfBytesClientExpectsToSend = newValue
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.13, iOS 11.0, *)
|
||||
override var countOfBytesClientExpectsToReceive: Int64 {
|
||||
get {
|
||||
history.append(.getCountOfBytesClientExpectsToReceive)
|
||||
return super.countOfBytesClientExpectsToReceive
|
||||
}
|
||||
set {
|
||||
history.append(.setCountOfBytesClientExpectsToReceive(newValue))
|
||||
super.countOfBytesClientExpectsToReceive = newValue
|
||||
}
|
||||
}
|
||||
|
||||
override var countOfBytesReceived: Int64 {
|
||||
history.append(.countOfBytesReceived)
|
||||
return super.countOfBytesReceived
|
||||
}
|
||||
|
||||
override var countOfBytesSent: Int64 {
|
||||
history.append(.countOfBytesSent)
|
||||
return super.countOfBytesSent
|
||||
}
|
||||
|
||||
override var countOfBytesExpectedToSend: Int64 {
|
||||
history.append(.countOfBytesExpectedToSend)
|
||||
return super.countOfBytesExpectedToSend
|
||||
}
|
||||
|
||||
override var countOfBytesExpectedToReceive: Int64 {
|
||||
history.append(.countOfBytesExpectedToReceive)
|
||||
return super.countOfBytesExpectedToReceive
|
||||
}
|
||||
|
||||
override var taskDescription: String? {
|
||||
get {
|
||||
history.append(.getTaskDescription)
|
||||
return super.taskDescription
|
||||
}
|
||||
set {
|
||||
history.append(.setTaskDescription(newValue))
|
||||
super.taskDescription = newValue
|
||||
}
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
history.append(.cancel)
|
||||
}
|
||||
|
||||
override var state: URLSessionTask.State {
|
||||
history.append(.state)
|
||||
return super.state
|
||||
}
|
||||
|
||||
override var error: Error? {
|
||||
history.append(.error)
|
||||
return super.error
|
||||
}
|
||||
|
||||
override func suspend() {
|
||||
history.append(.suspend)
|
||||
}
|
||||
|
||||
override func resume() {
|
||||
history.append(.resume)
|
||||
}
|
||||
|
||||
override var priority: Float {
|
||||
get {
|
||||
history.append(.getPriority)
|
||||
return super.priority
|
||||
}
|
||||
set {
|
||||
history.append(.setPriority(newValue))
|
||||
super.priority = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension URLError: EquatableError {}
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private func makePublisher(
|
||||
_ session: URLSession,
|
||||
_ url: URL
|
||||
) -> URLSession.DataTaskPublisher {
|
||||
return session.dataTaskPublisher(for: url)
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private func makePublisher(
|
||||
_ session: URLSession,
|
||||
_ request: URLRequest
|
||||
) -> URLSession.DataTaskPublisher {
|
||||
return session.dataTaskPublisher(for: request)
|
||||
}
|
||||
#else
|
||||
private func makePublisher(
|
||||
_ session: URLSession,
|
||||
_ url: URL
|
||||
) -> URLSession.OCombine.DataTaskPublisher {
|
||||
return session.ocombine.dataTaskPublisher(for: url)
|
||||
}
|
||||
|
||||
private func makePublisher(
|
||||
_ session: URLSession,
|
||||
_ request: URLRequest
|
||||
) -> URLSession.OCombine.DataTaskPublisher {
|
||||
return session.ocombine.dataTaskPublisher(for: request)
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // canImport(Darwin)
|
||||
@@ -0,0 +1,227 @@
|
||||
//
|
||||
// CommonTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 25.10.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension XCTest {
|
||||
|
||||
enum ValueBeforeSubscriptionBehavior<Value, Failure: Error> {
|
||||
case crash
|
||||
case history([TrackingSubscriberBase<Value, Failure>.Event],
|
||||
demand: Subscribers.Demand,
|
||||
comparator: (Value, Value) -> Bool)
|
||||
}
|
||||
|
||||
func testReceiveValueBeforeSubscription<Value, Operator: Publisher>(
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line,
|
||||
value: Value,
|
||||
expected: ValueBeforeSubscriptionBehavior<Operator.Output, Operator.Failure>,
|
||||
_ makeOperator: (CustomConnectablePublisherBase<Value, Never>) -> Operator
|
||||
) {
|
||||
let publisher = CustomConnectablePublisherBase<Value, Never>(subscription: nil)
|
||||
let operatorPublisher = makeOperator(publisher)
|
||||
let tracking = TrackingSubscriberBase<Operator.Output, Operator.Failure>(
|
||||
receiveValue: { _ in .max(42) }
|
||||
)
|
||||
operatorPublisher.subscribe(tracking)
|
||||
switch expected {
|
||||
case .crash:
|
||||
assertCrashes {
|
||||
_ = publisher.send(value)
|
||||
}
|
||||
case let .history(history, demand, comparator):
|
||||
XCTAssertEqual(publisher.send(value), demand, file: file, line: line)
|
||||
tracking.assertHistoryEqual(history,
|
||||
valueComparator: comparator,
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
}
|
||||
|
||||
enum CompletionBeforeSubscriptionBehavior<Value, Failure: Error> {
|
||||
case crash
|
||||
case history([TrackingSubscriberBase<Value, Failure>.Event],
|
||||
comparator: (Value, Value) -> Bool)
|
||||
}
|
||||
|
||||
func testReceiveCompletionBeforeSubscription<Value, Operator: Publisher>(
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line,
|
||||
inputType: Value.Type,
|
||||
expected: CompletionBeforeSubscriptionBehavior<Operator.Output, Operator.Failure>,
|
||||
_ makeOperator: (CustomConnectablePublisherBase<Value, Never>) -> Operator
|
||||
) {
|
||||
|
||||
let publisher = CustomConnectablePublisherBase<Value, Never>(subscription: nil)
|
||||
let operatorPublisher = makeOperator(publisher)
|
||||
let tracking = TrackingSubscriberBase<Operator.Output, Operator.Failure>()
|
||||
operatorPublisher.subscribe(tracking)
|
||||
|
||||
switch expected {
|
||||
case .crash:
|
||||
assertCrashes {
|
||||
publisher.send(completion: .finished)
|
||||
}
|
||||
case let .history(history, comparator: comparator):
|
||||
publisher.send(completion: .finished)
|
||||
tracking.assertHistoryEqual(history,
|
||||
valueComparator: comparator,
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
}
|
||||
|
||||
func testRequestBeforeSubscription<Value, Operator: Publisher>(
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line,
|
||||
inputType: Value.Type,
|
||||
shouldCrash: Bool,
|
||||
_ makeOperator: (CustomConnectablePublisherBase<Value, Never>) -> Operator
|
||||
) {
|
||||
|
||||
let publisher = CustomConnectablePublisherBase<Value, Never>(subscription: nil)
|
||||
let operatorPublisher = makeOperator(publisher)
|
||||
let tracking = TrackingSubscriberBase<Operator.Output, Operator.Failure>()
|
||||
operatorPublisher.subscribe(tracking)
|
||||
|
||||
guard let subscription = publisher.erasedSubscriber as? Subscription else {
|
||||
XCTFail("The subscriber must also be a subscription", file: file, line: line)
|
||||
return
|
||||
}
|
||||
|
||||
if shouldCrash {
|
||||
assertCrashes {
|
||||
subscription.request(.max(1))
|
||||
}
|
||||
} else {
|
||||
subscription.request(.max(1))
|
||||
}
|
||||
}
|
||||
|
||||
func testCancelBeforeSubscription<Value, Operator: Publisher>(
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line,
|
||||
inputType: Value.Type,
|
||||
shouldCrash: Bool,
|
||||
_ makeOperator: (CustomConnectablePublisherBase<Value, Never>) -> Operator
|
||||
) {
|
||||
|
||||
let publisher = CustomConnectablePublisherBase<Value, Never>(subscription: nil)
|
||||
let operatorPublisher = makeOperator(publisher)
|
||||
let tracking = TrackingSubscriberBase<Operator.Output, Operator.Failure>()
|
||||
operatorPublisher.subscribe(tracking)
|
||||
|
||||
guard let subscription = publisher.erasedSubscriber as? Subscription else {
|
||||
XCTFail("The subscriber must also be a subscription", file: file, line: line)
|
||||
return
|
||||
}
|
||||
|
||||
if shouldCrash {
|
||||
assertCrashes {
|
||||
subscription.cancel()
|
||||
}
|
||||
} else {
|
||||
subscription.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
func testReceiveSubscriptionTwice<Operator: Publisher>(
|
||||
_ makeOperator: (CustomPublisher) -> Operator
|
||||
) throws where Operator.Output: Equatable {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: makeOperator
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
|
||||
let secondSubscription = CustomSubscription()
|
||||
|
||||
try XCTUnwrap(helper.publisher.subscriber)
|
||||
.receive(subscription: secondSubscription)
|
||||
|
||||
XCTAssertEqual(secondSubscription.history, [.cancelled])
|
||||
|
||||
try XCTUnwrap(helper.publisher.subscriber)
|
||||
.receive(subscription: helper.subscription)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.cancelled])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.cancelled, .cancelled])
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension XCTestCase.ValueBeforeSubscriptionBehavior where Value: Equatable {
|
||||
static func history(
|
||||
_ history: [TrackingSubscriberBase<Value, Failure>.Event],
|
||||
demand: Subscribers.Demand
|
||||
) -> XCTestCase.ValueBeforeSubscriptionBehavior<Value, Failure> {
|
||||
return .history(history, demand: demand, comparator: ==)
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension XCTestCase.CompletionBeforeSubscriptionBehavior where Value: Equatable {
|
||||
static func history(
|
||||
_ history: [TrackingSubscriberBase<Value, Failure>.Event]
|
||||
) -> XCTestCase.CompletionBeforeSubscriptionBehavior<Value, Failure> {
|
||||
return .history(history, comparator: ==)
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable generic_type_name
|
||||
|
||||
func shouldNotBeCalled<S, T>(
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line
|
||||
) -> (S, T) -> S {
|
||||
return { s, _ in
|
||||
XCTFail("should not be called", file: file, line: line)
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
func shouldNotBeCalled<T>(
|
||||
file: StaticString = #file, line: UInt = #line
|
||||
) -> (T, T) -> Bool {
|
||||
return { _, _ in
|
||||
XCTFail("Should not be called", file: file, line: line)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func shouldNotBeCalled<T>(
|
||||
file: StaticString = #file, line: UInt = #line
|
||||
) -> (T) -> Bool {
|
||||
return { _ in
|
||||
XCTFail("Should not be called", file: file, line: line)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func unreachable<T>(_: T) -> Never {
|
||||
fatalError("unreachable")
|
||||
}
|
||||
|
||||
func unreachable() -> Never {
|
||||
fatalError("unreachable")
|
||||
}
|
||||
|
||||
// swiftlint:enable generic_type_name
|
||||
@@ -34,22 +34,39 @@ import OpenCombine
|
||||
typealias CustomPublisher = CustomPublisherBase<Int, TestingError>
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
class CustomPublisherBase<Output: Equatable, Failure: Error>: Publisher {
|
||||
class CustomPublisherBase<Output, Failure: Error>: Publisher, Cancellable {
|
||||
|
||||
private(set) var subscriber: AnySubscriber<Output, Failure>?
|
||||
private(set) var erasedSubscriber: Any?
|
||||
private let subscription: Subscription?
|
||||
|
||||
var willSubscribe: ((AnySubscriber<Output, Failure>) -> Void)?
|
||||
|
||||
var didSubscribe: ((AnySubscriber<Output, Failure>) -> Void)?
|
||||
|
||||
var onDeinit: (() -> Void)?
|
||||
|
||||
required init(subscription: Subscription?) {
|
||||
self.subscription = subscription
|
||||
}
|
||||
|
||||
deinit {
|
||||
onDeinit?()
|
||||
}
|
||||
|
||||
func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
self.subscriber = AnySubscriber(subscriber)
|
||||
let anySubscriber = AnySubscriber(subscriber)
|
||||
self.subscriber = anySubscriber
|
||||
willSubscribe?(anySubscriber)
|
||||
erasedSubscriber = subscriber
|
||||
subscription.map(subscriber.receive(subscription:))
|
||||
didSubscribe?(anySubscriber)
|
||||
}
|
||||
|
||||
func send(subscription: CustomSubscription) {
|
||||
subscriber!.receive(subscription: subscription)
|
||||
}
|
||||
|
||||
func send(_ value: Output) -> Subscribers.Demand {
|
||||
@@ -57,7 +74,12 @@ class CustomPublisherBase<Output: Equatable, Failure: Error>: Publisher {
|
||||
}
|
||||
|
||||
func send(completion: Subscribers.Completion<Failure>) {
|
||||
subscriber!.receive(completion: completion)
|
||||
subscriber?.receive(completion: completion)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
subscriber = nil
|
||||
erasedSubscriber = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,13 +36,13 @@ final class CustomSubscription: Subscription, CustomStringConvertible {
|
||||
/// The history of requests and cancellations of this subscription.
|
||||
private(set) var history: [Event] = []
|
||||
|
||||
private let _requested: ((Subscribers.Demand) -> Void)?
|
||||
private let _cancelled: (() -> Void)?
|
||||
var onRequest: ((Subscribers.Demand) -> Void)?
|
||||
var onCancel: (() -> Void)?
|
||||
|
||||
init(onRequest: ((Subscribers.Demand) -> Void)? = nil,
|
||||
onCancel: (() -> Void)? = nil) {
|
||||
_requested = onRequest
|
||||
_cancelled = onCancel
|
||||
self.onRequest = onRequest
|
||||
self.onCancel = onCancel
|
||||
}
|
||||
|
||||
var lastRequested: Subscribers.Demand? {
|
||||
@@ -60,13 +60,13 @@ final class CustomSubscription: Subscription, CustomStringConvertible {
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
history.append(.requested(demand))
|
||||
_requested?(demand)
|
||||
onRequest?(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
history.append(.cancelled)
|
||||
cancelled = true
|
||||
_cancelled?()
|
||||
onCancel?()
|
||||
}
|
||||
|
||||
var description: String { return "CustomSubscription" }
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
//
|
||||
// FairPriorityQueue.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 02.12.2019.
|
||||
//
|
||||
|
||||
/// A priproty queue based on binary min-heap.
|
||||
/// If two elements with the same priority are added, the element that was added
|
||||
/// earlier has will have "better" priority (i. e. it will be also extracted earlier).
|
||||
struct FairPriorityQueue<Priority: Comparable, Element> {
|
||||
|
||||
private var storage: [((Priority, UInt), Element)] = []
|
||||
private var next: UInt = 0
|
||||
|
||||
init() {}
|
||||
|
||||
mutating func insert(_ element: Element, priority: Priority) {
|
||||
storage.append(((priority, next), element))
|
||||
next += 1
|
||||
var newElementIndex = storage.endIndex - 1
|
||||
while let parent = self.parent(of: newElementIndex),
|
||||
storage[parent].0 > storage[newElementIndex].0 {
|
||||
storage.swapAt(newElementIndex, parent)
|
||||
newElementIndex = parent
|
||||
}
|
||||
}
|
||||
|
||||
func min() -> Element? {
|
||||
return storage.first?.1
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
mutating func extractMin() -> (Priority, Element)? {
|
||||
guard let max = storage.first else { return nil }
|
||||
storage[0] = storage[storage.endIndex - 1]
|
||||
storage.removeLast()
|
||||
minHeapify(0)
|
||||
return (max.0.0, max.1)
|
||||
}
|
||||
|
||||
var count: Int {
|
||||
return storage.count
|
||||
}
|
||||
|
||||
var isEmpty: Bool {
|
||||
return storage.isEmpty
|
||||
}
|
||||
|
||||
private func leftChild(of index: Int) -> Int? {
|
||||
assert(index >= 0)
|
||||
let childIndex = 2 * index + 1
|
||||
return childIndex < storage.endIndex ? childIndex : nil
|
||||
}
|
||||
|
||||
private func rightChild(of index: Int) -> Int? {
|
||||
assert(index >= 0)
|
||||
let childIndex = 2 * index + 2
|
||||
return childIndex < storage.endIndex ? childIndex : nil
|
||||
}
|
||||
|
||||
private func parent(of index: Int) -> Int? {
|
||||
assert(index >= 0)
|
||||
if index == 0 { return nil }
|
||||
return (index - 1) / 2
|
||||
}
|
||||
|
||||
private mutating func minHeapify(_ root: Int) {
|
||||
var root = root
|
||||
var largest = root
|
||||
while true {
|
||||
assert(largest == root)
|
||||
if let left = leftChild(of: root), storage[root].0 > storage[left].0 {
|
||||
largest = left
|
||||
}
|
||||
if let right = rightChild(of: root), storage[largest].0 > storage[right].0 {
|
||||
largest = right
|
||||
}
|
||||
if largest == root {
|
||||
break
|
||||
}
|
||||
storage.swapAt(root, largest)
|
||||
root = largest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FairPriorityQueue: Sequence {
|
||||
struct Iterator: IteratorProtocol {
|
||||
private var queue: FairPriorityQueue
|
||||
|
||||
fileprivate init(_ queue: FairPriorityQueue) {
|
||||
self.queue = queue
|
||||
}
|
||||
|
||||
mutating func next() -> (Priority, Element)? {
|
||||
return queue.extractMin()
|
||||
}
|
||||
}
|
||||
|
||||
func makeIterator() -> Iterator {
|
||||
return Iterator(self)
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import OpenCombine
|
||||
/// testing an operator. It is initialized with a publisher type and creates a
|
||||
/// `CustomSubscription`, `CustomPublisherBase` and `TrackingSubscriberBase`.
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
class OperatorTestHelper<SourceValue: Equatable,
|
||||
class OperatorTestHelper<SourceValue,
|
||||
SourceError: Error,
|
||||
SourcePublisher,
|
||||
Sut: Publisher>
|
||||
@@ -65,7 +65,14 @@ class OperatorTestHelper<SourceValue: Equatable,
|
||||
},
|
||||
receiveValue: { _ in receiveValueDemand }
|
||||
)
|
||||
tracking.onSubscribe = { self.downstreamSubscription = $0 }
|
||||
tracking.onSubscribe = { [weak self] in
|
||||
self?.downstreamSubscription = $0
|
||||
}
|
||||
sut.subscribe(tracking)
|
||||
}
|
||||
|
||||
deinit {
|
||||
downstreamSubscription?.cancel()
|
||||
tracking.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 13.06.2019.
|
||||
//
|
||||
|
||||
import GottaGoFast
|
||||
import XCTest
|
||||
|
||||
class PerformanceTestCase: GottaGoFast.PerformanceTestCase {
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
override var testInfo: String {
|
||||
"OPENCOMBINE_COMPATIBILITY_TEST"
|
||||
}
|
||||
#endif
|
||||
|
||||
@discardableResult
|
||||
override func benchmark(file: StaticString = #file,
|
||||
line: UInt = #line,
|
||||
allowFailure: Bool = false,
|
||||
executionCount: Int = 10,
|
||||
strategy: BenchmarkStrategy = .minimum,
|
||||
_ block: () throws -> Void) throws -> BenchmarkResult? {
|
||||
#if DEBUG
|
||||
print("⚠️ Benchmarks will only be run in release configuration")
|
||||
return nil
|
||||
#else
|
||||
return try super.benchmark(file: file,
|
||||
line: line,
|
||||
allowFailure: allowFailure,
|
||||
executionCount: executionCount,
|
||||
strategy: strategy,
|
||||
block)
|
||||
#endif
|
||||
}
|
||||
|
||||
@inline(never)
|
||||
func blackHole<Value>(_: Value) {}
|
||||
}
|
||||
|
||||
extension XCTestCase {
|
||||
|
||||
var isDebug: Bool {
|
||||
#if DEBUG
|
||||
return true
|
||||
#else
|
||||
return false
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ func testLifecycle<UpstreamOutput, Operator: Publisher>(
|
||||
line: UInt = #line,
|
||||
sendValue valueToBeSent: UpstreamOutput,
|
||||
cancellingSubscriptionReleasesSubscriber: Bool,
|
||||
finishingIsPassedThrough: Bool = true,
|
||||
_ makeOperator: (PassthroughSubject<UpstreamOutput, TestingError>) -> Operator
|
||||
) throws {
|
||||
var deinitCounter = 0
|
||||
@@ -101,7 +102,11 @@ func testLifecycle<UpstreamOutput, Operator: Publisher>(
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
try XCTUnwrap(subscription, file: file, line: line).cancel()
|
||||
try XCTUnwrap(subscription,
|
||||
"Lifecycle test #3: subscription should be saved",
|
||||
file: file,
|
||||
line: line)
|
||||
.cancel()
|
||||
|
||||
if cancellingSubscriptionReleasesSubscriber {
|
||||
XCTAssertEqual(deinitCounter,
|
||||
@@ -134,8 +139,15 @@ func testLifecycle<UpstreamOutput, Operator: Publisher>(
|
||||
passthrough.send(completion: .finished)
|
||||
}
|
||||
|
||||
XCTAssertTrue(subscriberDestroyed,
|
||||
"Lifecycle test #4: deinit should be called",
|
||||
file: file,
|
||||
line: line)
|
||||
if finishingIsPassedThrough {
|
||||
XCTAssertTrue(subscriberDestroyed,
|
||||
"Lifecycle test #4: deinit should be called",
|
||||
file: file,
|
||||
line: line)
|
||||
} else {
|
||||
XCTAssertFalse(subscriberDestroyed,
|
||||
"Lifecycle test #4: deinit should not be called",
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,10 @@ import OpenCombine
|
||||
|
||||
let childrenIsEmpty: (Mirror) -> Bool = { $0.children.isEmpty }
|
||||
|
||||
enum ExpectedMirrorChildValue: Equatable, ExpressibleByStringLiteral {
|
||||
enum ExpectedMirrorChildValue: ExpressibleByStringLiteral {
|
||||
case anything
|
||||
case matches(String)
|
||||
case contains(String)
|
||||
case matches(@autoclosure () -> String)
|
||||
case contains(@autoclosure () -> String)
|
||||
|
||||
typealias StringLiteralType = String
|
||||
|
||||
@@ -27,6 +27,7 @@ enum ExpectedMirrorChildValue: Equatable, ExpressibleByStringLiteral {
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func expectedChildren(_ expectedChildren: (String?, ExpectedMirrorChildValue)...,
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line) -> (Mirror) -> Bool {
|
||||
@@ -36,16 +37,23 @@ func expectedChildren(_ expectedChildren: (String?, ExpectedMirrorChildValue)...
|
||||
.children
|
||||
.map { ($0, String(describing: $1)) }
|
||||
|
||||
XCTAssertEqual(actualChildren.count,
|
||||
expectedChildren.count,
|
||||
"The children collections are of different sizes",
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
for (actualChild, expectedChild) in zip(actualChildren, expectedChildren) {
|
||||
XCTAssertEqual(actualChild.0, expectedChild.0, file: file, line: line)
|
||||
switch (actualChild.1, expectedChild.1) {
|
||||
case (_, .anything):
|
||||
continue
|
||||
case let (lhs, .matches(rhs)):
|
||||
XCTAssertEqual(lhs, rhs, file: file, line: line)
|
||||
XCTAssertEqual(lhs, rhs(), file: file, line: line)
|
||||
case let (lhs, .contains(rhs)):
|
||||
XCTAssert(lhs.contains(rhs),
|
||||
"\"\(lhs)\" doesn't contain substring \"\(rhs)\"",
|
||||
let evaluatedRHS = rhs()
|
||||
XCTAssert(lhs.contains(evaluatedRHS),
|
||||
"\"\(lhs)\" doesn't contain substring \"\(evaluatedRHS)\"",
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
@@ -72,9 +80,9 @@ internal func testReflection<Output, Failure: Error, Operator: Publisher>(
|
||||
line: UInt = #line,
|
||||
parentInput: Output.Type,
|
||||
parentFailure: Failure.Type,
|
||||
description expectedDescription: String,
|
||||
description expectedDescription: String?,
|
||||
customMirror customMirrorPredicate: ((Mirror) -> Bool)?,
|
||||
playgroundDescription: String,
|
||||
playgroundDescription: String?,
|
||||
_ makeOperator: (CustomConnectablePublisherBase<Output, Failure>) -> Operator
|
||||
) throws {
|
||||
let publisher = CustomConnectablePublisherBase<Output, Failure>(subscription: nil)
|
||||
@@ -90,16 +98,18 @@ internal func testReflection<Output, Failure: Error, Operator: Publisher>(
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
let customMirror =
|
||||
try XCTUnwrap((erasedSubscriber as? CustomReflectable)?.customMirror,
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
if let customMirrorPredicate = customMirrorPredicate {
|
||||
let customMirror =
|
||||
try XCTUnwrap((erasedSubscriber as? CustomReflectable)?.customMirror,
|
||||
file: file,
|
||||
line: line)
|
||||
XCTAssert(customMirrorPredicate(customMirror),
|
||||
"customMirror doesn't satisfy the predicate",
|
||||
file: file,
|
||||
line: line)
|
||||
} else {
|
||||
XCTAssertFalse(erasedSubscriber is CustomReflectable,
|
||||
"subscriber shouldn't conform to CustomReflectable")
|
||||
}
|
||||
|
||||
XCTAssertFalse(erasedSubscriber is CustomDebugStringConvertible,
|
||||
@@ -124,7 +134,7 @@ internal func testSubscriptionReflection<Sut: Publisher>(
|
||||
customMirror customMirrorPredicate: ((Mirror) -> Bool)?,
|
||||
playgroundDescription: String,
|
||||
sut: Sut
|
||||
) throws where Sut.Output: Equatable {
|
||||
) throws {
|
||||
let tracking = TrackingSubscriberBase<Sut.Output, Sut.Failure>()
|
||||
sut.subscribe(tracking)
|
||||
|
||||
|
||||
@@ -40,6 +40,20 @@ extension TestingError: ExpressibleByStringLiteral {
|
||||
}
|
||||
}
|
||||
|
||||
protocol EquatableError: Error {
|
||||
func isEqual(_ other: EquatableError) -> Bool
|
||||
}
|
||||
|
||||
extension EquatableError where Self: Equatable {
|
||||
func isEqual(_ other: EquatableError) -> Bool {
|
||||
return self == (other as? Self)
|
||||
}
|
||||
}
|
||||
|
||||
extension TestingError: EquatableError {}
|
||||
|
||||
extension NSError: EquatableError {}
|
||||
|
||||
func assertThrowsError<Result>(_ expression: @autoclosure () throws -> Result,
|
||||
_ expected: TestingError,
|
||||
_ message: @autoclosure () -> String = "",
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
//
|
||||
// TrackingEncoder.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 08.11.2019.
|
||||
//
|
||||
|
||||
final class TrackingEncoder {
|
||||
|
||||
enum Event: Equatable {
|
||||
// Encoder
|
||||
case getCodingPath
|
||||
case getUserInfo
|
||||
case containerKeyedBy
|
||||
case unkeyedContainer
|
||||
case singleValueContainer
|
||||
|
||||
// KeyedEncodingContainerProtocol
|
||||
case keyedContainerCodingPath
|
||||
case keyedContainerEncodeNil(String)
|
||||
case keyedContainerEncodeBool(Bool, String)
|
||||
case keyedContainerEncodeString(String, String)
|
||||
case keyedContainerEncodeDouble(Double, String)
|
||||
case keyedContainerEncodeFloat(Float, String)
|
||||
case keyedContainerEncodeInt(Int, String)
|
||||
case keyedContainerEncodeInt8(Int8, String)
|
||||
case keyedContainerEncodeInt16(Int16, String)
|
||||
case keyedContainerEncodeInt32(Int32, String)
|
||||
case keyedContainerEncodeInt64(Int64, String)
|
||||
case keyedContainerEncodeUInt(UInt, String)
|
||||
case keyedContainerEncodeUInt8(UInt8, String)
|
||||
case keyedContainerEncodeUInt16(UInt16, String)
|
||||
case keyedContainerEncodeUInt32(UInt32, String)
|
||||
case keyedContainerEncodeUInt64(UInt64, String)
|
||||
case keyedContainerEncodeEncodable(String)
|
||||
case keyedContainerNestedKeyedContainer(String)
|
||||
case keyedContainerNestedUnkeyedContainer(String)
|
||||
case keyedContainerSuperEncoder
|
||||
case keyedContainerSuperEncoderForKey(String)
|
||||
|
||||
// UnkeyedEncodingContainer
|
||||
case unkeyedContainerCodingPath
|
||||
case unkeyedContainerCount
|
||||
case unkeyedContainerEncodeNil
|
||||
case unkeyedContainerEncodeBool(Bool)
|
||||
case unkeyedContainerEncodeString(String)
|
||||
case unkeyedContainerEncodeDouble(Double)
|
||||
case unkeyedContainerEncodeFloat(Float)
|
||||
case unkeyedContainerEncodeInt(Int)
|
||||
case unkeyedContainerEncodeInt8(Int8)
|
||||
case unkeyedContainerEncodeInt16(Int16)
|
||||
case unkeyedContainerEncodeInt32(Int32)
|
||||
case unkeyedContainerEncodeInt64(Int64)
|
||||
case unkeyedContainerEncodeUInt(UInt)
|
||||
case unkeyedContainerEncodeUInt8(UInt8)
|
||||
case unkeyedContainerEncodeUInt16(UInt16)
|
||||
case unkeyedContainerEncodeUInt32(UInt32)
|
||||
case unkeyedContainerEncodeUInt64(UInt64)
|
||||
case unkeyedContainerEncodeEncodable
|
||||
case unkeyedContainerNestedKeyedContainer
|
||||
case unkeyedContainerNestedUnkeyedContainer
|
||||
case unkeyedContainerSuperEncoder
|
||||
|
||||
// SingleValueEncodingContainer
|
||||
case singleValueContainerCodingPath
|
||||
case singleValueContainerEncodeNil
|
||||
case singleValueContainerEncodeBool(Bool)
|
||||
case singleValueContainerEncodeString(String)
|
||||
case singleValueContainerEncodeDouble(Double)
|
||||
case singleValueContainerEncodeFloat(Float)
|
||||
case singleValueContainerEncodeInt(Int)
|
||||
case singleValueContainerEncodeInt8(Int8)
|
||||
case singleValueContainerEncodeInt16(Int16)
|
||||
case singleValueContainerEncodeInt32(Int32)
|
||||
case singleValueContainerEncodeInt64(Int64)
|
||||
case singleValueContainerEncodeUInt(UInt)
|
||||
case singleValueContainerEncodeUInt8(UInt8)
|
||||
case singleValueContainerEncodeUInt16(UInt16)
|
||||
case singleValueContainerEncodeUInt32(UInt32)
|
||||
case singleValueContainerEncodeUInt64(UInt64)
|
||||
case singleValueContainerEncodeEncodable
|
||||
}
|
||||
|
||||
fileprivate(set) var history: [Event] = []
|
||||
|
||||
fileprivate var _codingPath: [CodingKey] = []
|
||||
|
||||
fileprivate var _userInfo: [CodingUserInfoKey : Any] = [:]
|
||||
|
||||
fileprivate var _unkeyedContainerCount = 0
|
||||
}
|
||||
|
||||
extension TrackingEncoder: Encoder {
|
||||
|
||||
var codingPath: [CodingKey] {
|
||||
history.append(.getCodingPath)
|
||||
return _codingPath
|
||||
}
|
||||
|
||||
var userInfo: [CodingUserInfoKey : Any] {
|
||||
history.append(.getUserInfo)
|
||||
return _userInfo
|
||||
}
|
||||
|
||||
func container<Key: CodingKey>(
|
||||
keyedBy type: Key.Type
|
||||
) -> KeyedEncodingContainer<Key> {
|
||||
history.append(.containerKeyedBy)
|
||||
return .init(TrackingKeyedEncoder(encoder: self))
|
||||
}
|
||||
|
||||
func unkeyedContainer() -> UnkeyedEncodingContainer {
|
||||
history.append(.unkeyedContainer)
|
||||
return TrackingUnkeyedEncoder(encoder: self)
|
||||
}
|
||||
|
||||
func singleValueContainer() -> SingleValueEncodingContainer {
|
||||
history.append(.singleValueContainer)
|
||||
return TrackingSingleValueEncoder(encoder: self)
|
||||
}
|
||||
}
|
||||
|
||||
private struct TrackingKeyedEncoder<Key: CodingKey>: KeyedEncodingContainerProtocol {
|
||||
let encoder: TrackingEncoder
|
||||
|
||||
var codingPath: [CodingKey] {
|
||||
encoder.history.append(.keyedContainerCodingPath)
|
||||
return encoder._codingPath
|
||||
}
|
||||
|
||||
mutating func encodeNil(forKey key: Key) throws {
|
||||
encoder.history.append(.keyedContainerEncodeNil(key.stringValue))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Bool, forKey key: Key) throws {
|
||||
encoder.history.append(.keyedContainerEncodeBool(value, key.stringValue))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: String, forKey key: Key) throws {
|
||||
encoder.history.append(.keyedContainerEncodeString(value, key.stringValue))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Double, forKey key: Key) throws {
|
||||
encoder.history.append(.keyedContainerEncodeDouble(value, key.stringValue))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Float, forKey key: Key) throws {
|
||||
encoder.history.append(.keyedContainerEncodeFloat(value, key.stringValue))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Int, forKey key: Key) throws {
|
||||
encoder.history.append(.keyedContainerEncodeInt(value, key.stringValue))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Int8, forKey key: Key) throws {
|
||||
encoder.history.append(.keyedContainerEncodeInt8(value, key.stringValue))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Int16, forKey key: Key) throws {
|
||||
encoder.history.append(.keyedContainerEncodeInt16(value, key.stringValue))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Int32, forKey key: Key) throws {
|
||||
encoder.history.append(.keyedContainerEncodeInt32(value, key.stringValue))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Int64, forKey key: Key) throws {
|
||||
encoder.history.append(.keyedContainerEncodeInt64(value, key.stringValue))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: UInt, forKey key: Key) throws {
|
||||
encoder.history.append(.keyedContainerEncodeUInt(value, key.stringValue))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: UInt8, forKey key: Key) throws {
|
||||
encoder.history.append(.keyedContainerEncodeUInt8(value, key.stringValue))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: UInt16, forKey key: Key) throws {
|
||||
encoder.history.append(.keyedContainerEncodeUInt16(value, key.stringValue))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: UInt32, forKey key: Key) throws {
|
||||
encoder.history.append(.keyedContainerEncodeUInt32(value, key.stringValue))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: UInt64, forKey key: Key) throws {
|
||||
encoder.history.append(.keyedContainerEncodeUInt64(value, key.stringValue))
|
||||
}
|
||||
|
||||
mutating func encode<Value: Encodable>(_ value: Value, forKey key: Key) throws {
|
||||
encoder.history.append(.keyedContainerEncodeEncodable(key.stringValue))
|
||||
try value.encode(to: encoder)
|
||||
}
|
||||
|
||||
mutating func nestedContainer<NestedKey: CodingKey>(
|
||||
keyedBy keyType: NestedKey.Type,
|
||||
forKey key: Key
|
||||
) -> KeyedEncodingContainer<NestedKey> {
|
||||
encoder.history.append(.keyedContainerNestedKeyedContainer(key.stringValue))
|
||||
return .init(TrackingKeyedEncoder<NestedKey>(encoder: encoder))
|
||||
}
|
||||
|
||||
mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
|
||||
encoder.history.append(.keyedContainerNestedUnkeyedContainer(key.stringValue))
|
||||
return TrackingUnkeyedEncoder(encoder: encoder)
|
||||
}
|
||||
|
||||
mutating func superEncoder() -> Encoder {
|
||||
encoder.history.append(.keyedContainerSuperEncoder)
|
||||
return encoder
|
||||
}
|
||||
|
||||
mutating func superEncoder(forKey key: Key) -> Encoder {
|
||||
encoder.history.append(.keyedContainerSuperEncoderForKey(key.stringValue))
|
||||
return encoder
|
||||
}
|
||||
}
|
||||
|
||||
private struct TrackingUnkeyedEncoder: UnkeyedEncodingContainer {
|
||||
|
||||
let encoder: TrackingEncoder
|
||||
|
||||
var codingPath: [CodingKey] {
|
||||
encoder.history.append(.unkeyedContainerCodingPath)
|
||||
return encoder._codingPath
|
||||
}
|
||||
|
||||
var count: Int {
|
||||
encoder.history.append(.unkeyedContainerCount)
|
||||
return encoder._unkeyedContainerCount
|
||||
}
|
||||
|
||||
mutating func encodeNil() throws {
|
||||
encoder.history.append(.unkeyedContainerEncodeNil)
|
||||
encoder._unkeyedContainerCount += 1
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Bool) throws {
|
||||
encoder.history.append(.unkeyedContainerEncodeBool(value))
|
||||
encoder._unkeyedContainerCount += 1
|
||||
}
|
||||
|
||||
mutating func encode(_ value: String) throws {
|
||||
encoder.history.append(.unkeyedContainerEncodeString(value))
|
||||
encoder._unkeyedContainerCount += 1
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Double) throws {
|
||||
encoder.history.append(.unkeyedContainerEncodeDouble(value))
|
||||
encoder._unkeyedContainerCount += 1
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Float) throws {
|
||||
encoder.history.append(.unkeyedContainerEncodeFloat(value))
|
||||
encoder._unkeyedContainerCount += 1
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Int) throws {
|
||||
encoder.history.append(.unkeyedContainerEncodeInt(value))
|
||||
encoder._unkeyedContainerCount += 1
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Int8) throws {
|
||||
encoder.history.append(.unkeyedContainerEncodeInt8(value))
|
||||
encoder._unkeyedContainerCount += 1
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Int16) throws {
|
||||
encoder.history.append(.unkeyedContainerEncodeInt16(value))
|
||||
encoder._unkeyedContainerCount += 1
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Int32) throws {
|
||||
encoder.history.append(.unkeyedContainerEncodeInt32(value))
|
||||
encoder._unkeyedContainerCount += 1
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Int64) throws {
|
||||
encoder.history.append(.unkeyedContainerEncodeInt64(value))
|
||||
encoder._unkeyedContainerCount += 1
|
||||
}
|
||||
|
||||
mutating func encode(_ value: UInt) throws {
|
||||
encoder.history.append(.unkeyedContainerEncodeUInt(value))
|
||||
encoder._unkeyedContainerCount += 1
|
||||
}
|
||||
|
||||
mutating func encode(_ value: UInt8) throws {
|
||||
encoder.history.append(.unkeyedContainerEncodeUInt8(value))
|
||||
encoder._unkeyedContainerCount += 1
|
||||
}
|
||||
|
||||
mutating func encode(_ value: UInt16) throws {
|
||||
encoder.history.append(.unkeyedContainerEncodeUInt16(value))
|
||||
encoder._unkeyedContainerCount += 1
|
||||
}
|
||||
|
||||
mutating func encode(_ value: UInt32) throws {
|
||||
encoder.history.append(.unkeyedContainerEncodeUInt32(value))
|
||||
encoder._unkeyedContainerCount += 1
|
||||
}
|
||||
|
||||
mutating func encode(_ value: UInt64) throws {
|
||||
encoder.history.append(.unkeyedContainerEncodeUInt64(value))
|
||||
encoder._unkeyedContainerCount += 1
|
||||
}
|
||||
|
||||
mutating func encode<Value: Encodable>(_ value: Value) throws {
|
||||
encoder.history.append(.unkeyedContainerEncodeEncodable)
|
||||
encoder._unkeyedContainerCount += 1
|
||||
try value.encode(to: encoder)
|
||||
}
|
||||
|
||||
func nestedContainer<NestedKey: CodingKey>(
|
||||
keyedBy keyType: NestedKey.Type
|
||||
) -> KeyedEncodingContainer<NestedKey> {
|
||||
encoder.history.append(.unkeyedContainerNestedKeyedContainer)
|
||||
return .init(TrackingKeyedEncoder(encoder: encoder))
|
||||
}
|
||||
|
||||
func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
|
||||
encoder.history.append(.unkeyedContainerNestedUnkeyedContainer)
|
||||
return self
|
||||
}
|
||||
|
||||
func superEncoder() -> Encoder {
|
||||
encoder.history.append(.unkeyedContainerSuperEncoder)
|
||||
return encoder
|
||||
}
|
||||
}
|
||||
|
||||
private struct TrackingSingleValueEncoder: SingleValueEncodingContainer {
|
||||
|
||||
let encoder: TrackingEncoder
|
||||
|
||||
var codingPath: [CodingKey] {
|
||||
encoder.history.append(.singleValueContainerCodingPath)
|
||||
return encoder._codingPath
|
||||
}
|
||||
|
||||
mutating func encodeNil() throws {
|
||||
encoder.history.append(.singleValueContainerEncodeNil)
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Bool) throws {
|
||||
encoder.history.append(.singleValueContainerEncodeBool(value))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: String) throws {
|
||||
encoder.history.append(.singleValueContainerEncodeString(value))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Double) throws {
|
||||
encoder.history.append(.singleValueContainerEncodeDouble(value))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Float) throws {
|
||||
encoder.history.append(.singleValueContainerEncodeFloat(value))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Int) throws {
|
||||
encoder.history.append(.singleValueContainerEncodeInt(value))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Int8) throws {
|
||||
encoder.history.append(.singleValueContainerEncodeInt8(value))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Int16) throws {
|
||||
encoder.history.append(.singleValueContainerEncodeInt16(value))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Int32) throws {
|
||||
encoder.history.append(.singleValueContainerEncodeInt32(value))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Int64) throws {
|
||||
encoder.history.append(.singleValueContainerEncodeInt64(value))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: UInt) throws {
|
||||
encoder.history.append(.singleValueContainerEncodeUInt(value))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: UInt8) throws {
|
||||
encoder.history.append(.singleValueContainerEncodeUInt8(value))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: UInt16) throws {
|
||||
encoder.history.append(.singleValueContainerEncodeUInt16(value))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: UInt32) throws {
|
||||
encoder.history.append(.singleValueContainerEncodeUInt32(value))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: UInt64) throws {
|
||||
encoder.history.append(.singleValueContainerEncodeUInt64(value))
|
||||
}
|
||||
|
||||
mutating func encode<Value: Encodable>(_ value: Value) throws {
|
||||
encoder.history.append(.singleValueContainerEncodeEncodable)
|
||||
try value.encode(to: encoder)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
@@ -41,6 +42,7 @@ typealias TrackingSubscriber = TrackingSubscriberBase<Int, TestingError>
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class TrackingSubscriberBase<Value, Failure: Error>
|
||||
: Subscriber,
|
||||
Cancellable,
|
||||
CustomStringConvertible
|
||||
{
|
||||
|
||||
@@ -172,6 +174,17 @@ final class TrackingSubscriberBase<Value, Failure: Error>
|
||||
line: line)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
for subscription in subscriptions {
|
||||
subscription.cancel()
|
||||
}
|
||||
clearHistory()
|
||||
}
|
||||
|
||||
func clearHistory() {
|
||||
history = []
|
||||
}
|
||||
|
||||
deinit {
|
||||
onDeinit?()
|
||||
_onDeinit?()
|
||||
@@ -187,6 +200,18 @@ extension TrackingSubscriberBase where Value: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension TrackingSubscriberBase where Value == Void {
|
||||
func assertHistoryEqual(_ expected: [Event],
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line) {
|
||||
assertHistoryEqual(expected,
|
||||
valueComparator: { _, _ in true },
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension TrackingSubscriberBase.Event {
|
||||
func isEqual(to other: TrackingSubscriberBase<Value, Failure>.Event,
|
||||
@@ -200,8 +225,8 @@ extension TrackingSubscriberBase.Event {
|
||||
switch (lhs, rhs) {
|
||||
case (.finished, .finished):
|
||||
return true
|
||||
case let (.failure(lhs), .failure(rhs)):
|
||||
return (lhs as? TestingError) == (rhs as? TestingError)
|
||||
case let (.failure(lhs as EquatableError), .failure(rhs as EquatableError)):
|
||||
return lhs.isEqual(rhs)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@@ -214,12 +239,23 @@ extension TrackingSubscriberBase.Event {
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension TrackingSubscriberBase.Event: Equatable where Value: Equatable {
|
||||
|
||||
static func == (lhs: TrackingSubscriberBase<Value, Failure>.Event,
|
||||
rhs: TrackingSubscriberBase<Value, Failure>.Event) -> Bool {
|
||||
static func == (lhs: TrackingSubscriberBase.Event,
|
||||
rhs: TrackingSubscriberBase.Event) -> Bool {
|
||||
return lhs.isEqual(to: rhs, valueComparator: ==)
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension TrackingSubscriberBase.Event where Value == Void {
|
||||
|
||||
static var signal: TrackingSubscriberBase.Event { return .value(()) }
|
||||
|
||||
static func == (lhs: TrackingSubscriberBase.Event,
|
||||
rhs: TrackingSubscriberBase.Event) -> Bool {
|
||||
return lhs.isEqual(to: rhs, valueComparator: { _, _ in true })
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
typealias TrackingSubject<Output: Equatable> = TrackingSubjectBase<Output, TestingError>
|
||||
|
||||
@@ -246,8 +282,9 @@ final class TrackingSubjectBase<Output: Equatable, Failure: Error>
|
||||
switch (lhs, rhs) {
|
||||
case (.finished, .finished):
|
||||
return true
|
||||
case let (.failure(lhs), .failure(rhs)):
|
||||
return (lhs as? TestingError) == (rhs as? TestingError)
|
||||
case let (.failure(lhs as EquatableError),
|
||||
.failure(rhs as EquatableError)):
|
||||
return lhs.isEqual(rhs)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@@ -274,10 +311,10 @@ final class TrackingSubjectBase<Output: Equatable, Failure: Error>
|
||||
|
||||
private let _passthrough = PassthroughSubject<Output, Failure>()
|
||||
private(set) var history: [Event] = []
|
||||
private let _receiveSubscriber: ((CustomCombineIdentifierConvertible) -> Void)?
|
||||
private let _receiveSubscriber: ((AnySubscriber<Output, Failure>) -> Void)?
|
||||
private let _onDeinit: (() -> Void)?
|
||||
|
||||
init(receiveSubscriber: ((CustomCombineIdentifierConvertible) -> Void)? = nil,
|
||||
init(receiveSubscriber: ((AnySubscriber<Output, Failure>) -> Void)? = nil,
|
||||
onDeinit: (() -> Void)? = nil) {
|
||||
_receiveSubscriber = receiveSubscriber
|
||||
_onDeinit = onDeinit
|
||||
@@ -305,7 +342,7 @@ final class TrackingSubjectBase<Output: Equatable, Failure: Error>
|
||||
func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
_receiveSubscriber?(subscriber)
|
||||
_receiveSubscriber?(AnySubscriber(subscriber))
|
||||
history.append(.subscriber)
|
||||
_passthrough.subscribe(subscriber)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
//
|
||||
// VirtualTimeScheduler.swift
|
||||
// OpenCombineTests
|
||||
//
|
||||
// Created by Евгений Богомолов on 14/09/2019.
|
||||
//
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class VirtualTimeScheduler: Scheduler {
|
||||
|
||||
struct SchedulerTimeType: Strideable,
|
||||
Comparable,
|
||||
Hashable,
|
||||
SchedulerTimeIntervalConvertible
|
||||
{
|
||||
|
||||
struct Stride: ExpressibleByFloatLiteral,
|
||||
Comparable,
|
||||
SignedNumeric,
|
||||
SchedulerTimeIntervalConvertible
|
||||
{
|
||||
var magnitude: Int64
|
||||
|
||||
fileprivate init(magnitude: Int64) {
|
||||
self.magnitude = magnitude
|
||||
}
|
||||
|
||||
init(integerLiteral value: Int) {
|
||||
self = .seconds(value)
|
||||
}
|
||||
|
||||
init(floatLiteral value: Double) {
|
||||
self = .seconds(value)
|
||||
}
|
||||
|
||||
init?<Source: BinaryInteger>(exactly source: Source) {
|
||||
guard let magnitude = Int64(exactly: source) else {
|
||||
return nil
|
||||
}
|
||||
self.init(magnitude: magnitude)
|
||||
}
|
||||
|
||||
static func < (lhs: Stride, rhs: Stride) -> Bool {
|
||||
return lhs.magnitude < rhs.magnitude
|
||||
}
|
||||
|
||||
static func * (lhs: Stride, rhs: Stride) -> Stride {
|
||||
return Stride(magnitude: lhs.magnitude * rhs.magnitude)
|
||||
}
|
||||
|
||||
static func + (lhs: Stride, rhs: Stride) -> Stride {
|
||||
return Stride(magnitude: lhs.magnitude + rhs.magnitude)
|
||||
}
|
||||
|
||||
static func - (lhs: Stride, rhs: Stride) -> Stride {
|
||||
return Stride(magnitude: lhs.magnitude - rhs.magnitude)
|
||||
}
|
||||
|
||||
static func -= (lhs: inout Stride, rhs: Stride) {
|
||||
lhs.magnitude -= rhs.magnitude
|
||||
}
|
||||
|
||||
static func *= (lhs: inout Stride, rhs: Stride) {
|
||||
lhs.magnitude *= rhs.magnitude
|
||||
}
|
||||
|
||||
static func += (lhs: inout Stride, rhs: Stride) {
|
||||
lhs.magnitude += rhs.magnitude
|
||||
}
|
||||
|
||||
static func seconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: Int64(value) * 1_000_000_000)
|
||||
}
|
||||
|
||||
static func seconds(_ value: Double) -> Stride {
|
||||
return Stride(magnitude: Int64(value * 1_000_000_000))
|
||||
}
|
||||
|
||||
static func milliseconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: Int64(value) * 1_000_000)
|
||||
}
|
||||
|
||||
static func microseconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: Int64(value) * 1_000)
|
||||
}
|
||||
|
||||
static func nanoseconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: Int64(value))
|
||||
}
|
||||
}
|
||||
|
||||
/// Time in virtual nanoseconds
|
||||
let time: UInt64
|
||||
|
||||
private init(nanoseconds time: UInt64) {
|
||||
self.time = time
|
||||
}
|
||||
|
||||
static func == (lhs: SchedulerTimeType, rhs: SchedulerTimeType) -> Bool {
|
||||
return lhs.time == rhs.time
|
||||
}
|
||||
|
||||
static func < (lhs: SchedulerTimeType, rhs: SchedulerTimeType) -> Bool {
|
||||
return lhs.time < rhs.time
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(time)
|
||||
}
|
||||
|
||||
func distance(to other: SchedulerTimeType) -> Stride {
|
||||
if self > other {
|
||||
return Stride(magnitude: -Int64(time - other.time))
|
||||
} else {
|
||||
return Stride(magnitude: Int64(other.time - time))
|
||||
}
|
||||
}
|
||||
|
||||
func advanced(by stride: Stride) -> SchedulerTimeType {
|
||||
return stride.magnitude < 0
|
||||
? SchedulerTimeType(nanoseconds: time - UInt64(-stride.magnitude))
|
||||
: SchedulerTimeType(nanoseconds: time + UInt64(stride.magnitude))
|
||||
}
|
||||
|
||||
static func + (lhs: SchedulerTimeType, rhs: Stride) -> SchedulerTimeType {
|
||||
return lhs.advanced(by: rhs)
|
||||
}
|
||||
|
||||
static let beginningOfTime = SchedulerTimeType(nanoseconds: 0)
|
||||
|
||||
static func seconds(_ value: Int) -> SchedulerTimeType {
|
||||
precondition(value >= 0, "value must not be negative")
|
||||
return .init(nanoseconds: UInt64(value) * 1_000_000_000)
|
||||
}
|
||||
|
||||
static func seconds(_ value: Double) -> SchedulerTimeType {
|
||||
precondition(value >= 0, "value must not be negative")
|
||||
return .init(nanoseconds: UInt64(value * 1_000_000_000))
|
||||
}
|
||||
|
||||
static func milliseconds(_ value: Int) -> SchedulerTimeType {
|
||||
precondition(value >= 0, "value must not be negative")
|
||||
return .init(nanoseconds: UInt64(value) * 1_000_000)
|
||||
}
|
||||
|
||||
static func microseconds(_ value: Int) -> SchedulerTimeType {
|
||||
precondition(value >= 0, "value must not be negative")
|
||||
return .init(nanoseconds: UInt64(value) * 1_000)
|
||||
}
|
||||
|
||||
static func nanoseconds(_ value: Int) -> SchedulerTimeType {
|
||||
precondition(value >= 0, "value must not be negative")
|
||||
return .init(nanoseconds: UInt64(value))
|
||||
}
|
||||
}
|
||||
|
||||
enum SchedulerOptions: Equatable, CustomStringConvertible {
|
||||
case nontrivialOptions
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .nontrivialOptions:
|
||||
return ".nontrivialOptions"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class CancellableToken: Cancellable {
|
||||
|
||||
private(set) var isCancelled = false
|
||||
|
||||
func cancel() {
|
||||
isCancelled = true
|
||||
}
|
||||
}
|
||||
|
||||
enum Event: Equatable, CustomStringConvertible {
|
||||
case now
|
||||
case minimumTolerance
|
||||
case schedule(options: SchedulerOptions?)
|
||||
case scheduleAfterDate(SchedulerTimeType,
|
||||
tolerance: SchedulerTimeType.Stride,
|
||||
options: SchedulerOptions?)
|
||||
case scheduleAfterDateWithInterval(SchedulerTimeType,
|
||||
interval: SchedulerTimeType.Stride,
|
||||
tolerance: SchedulerTimeType.Stride,
|
||||
options: SchedulerOptions?)
|
||||
|
||||
var description: String {
|
||||
|
||||
func describeOptions(_ options: SchedulerOptions?) -> String {
|
||||
return options.map(String.init(describing:)) ?? "nil"
|
||||
}
|
||||
|
||||
func describeDate(_ date: SchedulerTimeType) -> String {
|
||||
return ".nanoseconds(\(date.time)"
|
||||
}
|
||||
|
||||
func describeStride(_ stride: SchedulerTimeType.Stride) -> String {
|
||||
return ".nanoseconds(\(stride.magnitude))"
|
||||
}
|
||||
|
||||
switch self {
|
||||
case .now:
|
||||
return ".now"
|
||||
case .minimumTolerance:
|
||||
return ".minimumTolerance"
|
||||
case let .schedule(options):
|
||||
return ".schedule(options: \(describeOptions(options)))"
|
||||
case let .scheduleAfterDate(date, tolerance, options):
|
||||
return """
|
||||
.scheduleAfterDate(\(describeDate(date)), \
|
||||
tolerance: \(describeStride(tolerance)), \
|
||||
options: \(describeOptions(options)))
|
||||
"""
|
||||
case let .scheduleAfterDateWithInterval(date, interval, tolerance, options):
|
||||
return """
|
||||
.scheduleAfterDateWithInterval(\(describeDate(date)), \
|
||||
interval: \(describeStride(interval)), \
|
||||
tolerance: \(describeStride(tolerance)), \
|
||||
options: \(describeOptions(options)))
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var history = [Event]()
|
||||
|
||||
/// All private methods should reference this property instead of `now`
|
||||
/// to prevent polluting the scheduler history. Accessing `now` creates an entry
|
||||
/// in the `history` array.
|
||||
private var _now = SchedulerTimeType.beginningOfTime
|
||||
|
||||
private var workQueue = FairPriorityQueue<SchedulerTimeType, () -> Void>()
|
||||
|
||||
var scheduledDates: [SchedulerTimeType] {
|
||||
return workQueue.map { $0.0 }
|
||||
}
|
||||
|
||||
var now: SchedulerTimeType {
|
||||
history.append(.now)
|
||||
return _now
|
||||
}
|
||||
|
||||
var minimumTolerance: SchedulerTimeType.Stride {
|
||||
history.append(.minimumTolerance)
|
||||
return 0
|
||||
}
|
||||
|
||||
func schedule(options: SchedulerOptions?, _ action: @escaping () -> Void) {
|
||||
history.append(.schedule(options: options))
|
||||
workQueue.insert(action, priority: _now)
|
||||
}
|
||||
|
||||
func schedule(after date: SchedulerTimeType,
|
||||
tolerance: SchedulerTimeType.Stride,
|
||||
options: SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) {
|
||||
history.append(.scheduleAfterDate(date, tolerance: tolerance, options: options))
|
||||
workQueue.insert(action, priority: date)
|
||||
}
|
||||
|
||||
func schedule(after date: SchedulerTimeType,
|
||||
interval: SchedulerTimeType.Stride,
|
||||
tolerance: SchedulerTimeType.Stride,
|
||||
options: SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) -> Cancellable {
|
||||
history.append(.scheduleAfterDateWithInterval(date,
|
||||
interval: interval,
|
||||
tolerance: tolerance,
|
||||
options: options))
|
||||
let cancellableToken = CancellableToken()
|
||||
repeatedlyExecute(after: date,
|
||||
interval: interval,
|
||||
cancellableToken: cancellableToken,
|
||||
action: action)
|
||||
return cancellableToken
|
||||
}
|
||||
|
||||
private func repeatedlyExecute(after date: SchedulerTimeType,
|
||||
interval: SchedulerTimeType.Stride,
|
||||
cancellableToken: CancellableToken,
|
||||
action: @escaping () -> Void) {
|
||||
let enqueuedAction: () -> Void = { [unowned self] in
|
||||
if cancellableToken.isCancelled { return }
|
||||
action()
|
||||
self.repeatedlyExecute(after: date + interval,
|
||||
interval: interval,
|
||||
cancellableToken: cancellableToken,
|
||||
action: action)
|
||||
}
|
||||
workQueue.insert(enqueuedAction, priority: date)
|
||||
}
|
||||
|
||||
/// Sets `now` to the provided value. Useful for testing that an entity that
|
||||
/// uses the scheduler doesn't rely on clock monotonicity.
|
||||
///
|
||||
/// - Note: The actions that were already executed will not be executed again.
|
||||
/// This function does **not** provide time machine-like functionality.
|
||||
func rewind(to time: SchedulerTimeType) {
|
||||
_now = time
|
||||
}
|
||||
|
||||
func executeScheduledActions() {
|
||||
while let (time, action) = workQueue.extractMin() {
|
||||
_now = max(time, _now)
|
||||
action()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,19 @@ import XCTest
|
||||
|
||||
// FIXME: XCTUnwrap is unavailable in Swift Package Manager yet.
|
||||
|
||||
private struct UnwrappingFailure: Error {}
|
||||
private struct UnwrappingFailure: Error, LocalizedError {
|
||||
|
||||
let message: String
|
||||
|
||||
var errorDescription: String? {
|
||||
var failureDescription = "XCTUnwrap failed"
|
||||
if !message.isEmpty {
|
||||
failureDescription += ": "
|
||||
failureDescription += message
|
||||
}
|
||||
return failureDescription
|
||||
}
|
||||
}
|
||||
|
||||
/// Asserts that an expression is not `nil`, and returns its unwrapped value.
|
||||
///
|
||||
@@ -32,10 +44,11 @@ public func XCTUnwrap<Result>(_ expression: @autoclosure () throws -> Result?,
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line) throws -> Result {
|
||||
let result = try expression()
|
||||
XCTAssertNotNil(result, message(), file: file, line: line)
|
||||
if let result = result {
|
||||
return result
|
||||
} else {
|
||||
throw UnwrappingFailure()
|
||||
let error = UnwrappingFailure(message: message())
|
||||
XCTFail(error.errorDescription ?? "", file: file, line: line)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user