89 Commits

Author SHA1 Message Date
Sergej Jaskiewicz 899a04bb3f Bump version to 0.7.0 2019-12-11 02:43:17 +03:00
Sergej Jaskiewicz 5f9a700689 Implement Publishers.Concatenate (#90) 2019-12-10 13:37:44 +03:00
Sergej Jaskiewicz a300fd09d3 [CocoaPods] Make COpenCombineHelpers part of the OpenCombine pod
CocoaPods doesn't support multiple Swift modules in the same pod.
Build COpenCombineHelpers sources together with OpenCombine sources
as a single module.

Previously COpenCombineHelpers was a separate pod. This was suboptimal,
as it made making changes in both targets very hard: you'd have to
push COpenCombineHelpers to trunk in order to pass validation.
2019-12-09 15:18:53 +03:00
Sergej Jaskiewicz 5973f86c6e Implement Publishers.HandleEvents 2019-12-09 15:18:53 +03:00
Sergej Jaskiewicz 1b5afdba26 Implement Publishers.Breakpoint 2019-12-09 15:18:53 +03:00
Sergej Jaskiewicz 51d5d1e71d Implement MeasureInterval (#117) 2019-12-03 14:26:00 +03:00
Sergej Jaskiewicz a8bc5cc046 Implement SubscribeOn (#116) 2019-12-03 12:11:31 +03:00
Sergej Jaskiewicz 86d6170dc9 Implement ReceiveOn (#115) 2019-12-02 20:30:58 +03:00
Sergej Jaskiewicz 171131d768 Implement Delay (#114) 2019-12-02 18:18:46 +03:00
Sergej Jaskiewicz d6b4fb4115 Bump COpenCombineHelpers.podspec version 2019-11-26 19:21:18 +03:00
Sergej Jaskiewicz 014b82b99d Bump version (#113) 2019-11-26 19:02:02 +03:00
Sergej Jaskiewicz 7c5a76cf2b Fix @Published (#112) 2019-11-26 17:46:01 +03:00
Max Desiatov 668c292245 Add more tests for the Future publisher (#111) 2019-11-26 14:23:51 +03:00
Max Desiatov 981ab4fa09 Add Future publisher with tests (#107) 2019-11-26 02:15:43 +03:00
Sergej Jaskiewicz 8cf71e0122 If Multicast receives subscription twice, cancel the second one (#110) 2019-11-25 16:52:55 +03:00
Max Desiatov 130125cb66 Use demand.assertNonZero() in PassthroughSubject (#108) 2019-11-24 21:44:14 +03:00
Daniel Peter 7b3ceae666 CocoaPods support (#103) 2019-11-24 21:17:13 +03:00
Max Desiatov 56202b1663 Fix typo in FilterProducer.swift
> subsription
2019-11-22 20:12:34 +03:00
Sergej Jaskiewicz d0a02de7c5 Warn about unguarded availability in C++ code. Fix the warnings. 2019-11-21 20:18:28 +03:00
Sergej Jaskiewicz c8058edc5f Test that Sink calls the callbacks even without upstream subscription 2019-11-19 18:38:01 +03:00
Sergej Jaskiewicz 8b685f78a3 Test that ReplaceError saves demand until subscription arrives 2019-11-19 18:38:01 +03:00
Sergej Jaskiewicz 0816abe33c Remove superfluous 'disable' swiftlint command 2019-11-19 18:38:01 +03:00
Sergej Jaskiewicz b6c7560f4c Use pthread_mutex_t instead of std::mutex on non-Darwin platforms
std::mutex doesn't guarantee error checks.
With pthread, we can enable them manually.

Error checks are essential, since the client code may bay be crafted
in such a way that causes a non-recursive lock to be recursively
acuired, which is undefined behavior with std::mutex.
2019-11-19 18:38:01 +03:00
Sergej Jaskiewicz 7a9e8b22d2 Bump iOS version for compatibility tests in CircleCI config 2019-11-19 18:38:01 +03:00
Sergej Jaskiewicz 26f9acd75a Reimplement FlatMap to make all the tests pass 2019-11-19 18:38:01 +03:00
Sergej Jaskiewicz 4c42b434ca Add more tests for FlatMap 2019-11-19 18:38:01 +03:00
Sergej Jaskiewicz 8afe945325 Test that CombineIdentifier uses UInt64 under the hood 2019-11-19 18:38:01 +03:00
Sergej Jaskiewicz eb879213ef Automatically confirm apt install 2019-11-19 18:38:01 +03:00
Sergej Jaskiewicz e402fb3980 Remove redundant Self 2019-11-19 18:38:01 +03:00
Sergej Jaskiewicz 4f96378c02 Remove OperatorSubscription class 2019-11-19 18:38:01 +03:00
Sergej Jaskiewicz 243f3d200e Implement PrefixWhile, TryPrefixWhile using FilterProducer 2019-11-19 18:38:01 +03:00
Sergej Jaskiewicz f1fb5552b5 Implement RemoveDuplicates, TryRemoveDuplicates using FilterProducer 2019-11-19 18:38:01 +03:00
Sergej Jaskiewicz bfd875ccba Simplify CompactMap and TryCompactMap using FilterProducer 2019-11-19 18:38:01 +03:00
Sergej Jaskiewicz 27da28f378 Simplify Filter and TryFilter using FilterProducer 2019-11-19 18:38:01 +03:00
Sergej Jaskiewicz cba3a69e74 Implement FilterProducer 2019-11-19 18:38:01 +03:00
Sergej Jaskiewicz d634a76c39 A more robust test for Subscribers.Assign 2019-11-19 18:38:01 +03:00
Sergej Jaskiewicz b5e31a43ef Fix ReplaceError subscription behavior 2019-11-19 18:38:01 +03:00
Sergej Jaskiewicz 1bb3583a36 More tests for contract violations in operators
- test the behavior when a value arrives earlier than the subscription
- test the behavior when a completion arrives earlier than
  the subscription
- test the behavior when requesting before a subscription arrives
- test the behavior when cancelling before a subscription arrives
2019-11-19 18:38:01 +03:00
Sergej Jaskiewicz 449b8eef48 Test that Record doesn't finilize its Recording 2019-11-19 18:38:01 +03:00
Sergej Jaskiewicz d4bdd83a00 Implement the Record publisher 2019-11-12 17:31:29 +03:00
Sergej Jaskiewicz f1cc94adff Implement Publishers.Output 2019-11-11 20:26:31 +03:00
Sergej Jaskiewicz 5e1e10a780 Add CircleCI badge 2019-11-10 15:00:50 +03:00
Sergej Jaskiewicz 5b7358111c Remove redundant 2019-11-10 15:00:50 +03:00
Sergej Jaskiewicz 5f90c4c85f Add Gemfile.lock 2019-11-10 15:00:50 +03:00
Sergej Jaskiewicz 0dec30fcad Switch from Travis CI to CircleCI.
CircleCI has significantly faster builds. They also update their build
machines sooner than Travis CI.
2019-11-09 15:26:50 +03:00
Sergej Jaskiewicz a5ec9723e2 Make the tests pass on 32-bit platforms 2019-11-09 15:26:50 +03:00
Sergej Jaskiewicz 25ac4dfa5f Fix SwiftLint crash
https://github.com/realm/SwiftLint/issues/2793
2019-11-09 15:26:50 +03:00
Sergej Jaskiewicz f5645f605b Remove old benchmarking infrastructure 2019-11-09 15:26:50 +03:00
Sergej Jaskiewicz ffef3ac76c Update CI configuration to run compatibility tests on Xcode 11.2 2019-10-31 14:01:44 +03:00
Sergej Jaskiewicz 36a9f1999c They fix bugs in Combine, we update our awesome test suite!
The bug fixes happened in macOS 10.15.1
2019-10-31 14:01:44 +03:00
Sergej Jaskiewicz f069f9b9fa Audit DropWhile/TryDropWhile for thread safety (#87)
Add more tests, make them pass
2019-10-25 15:45:40 +03:00
Sergej Jaskiewicz 55bdbba0f9 Publishers.Print: cancel new subscription if Inner is already subscribed (#92) 2019-10-24 15:11:55 +03:00
Sergej Jaskiewicz 1290545c49 Audit IgnoreOutput for thread safety, add more tests, make them pass (#88) 2019-10-23 23:44:59 +03:00
Sergej Jaskiewicz aed074af43 Add tests for Subscriptions.empty 2019-10-23 20:30:46 +03:00
Sergej Jaskiewicz 11ec7c89e6 Gybify Encode/Decode, add more tests, make them pass 2019-10-23 20:30:46 +03:00
Sergej Jaskiewicz 46007658a1 Add missing Equatable comformances
- Collect
- Contains
- Count
- Drop
- Last
2019-10-17 14:40:56 +03:00
Sergej Jaskiewicz c4c7f2172d Add more tests for Scan, TryScan 2019-10-17 14:40:56 +03:00
Eric Patey 5b0a21a0b9 Implement Publishers.Scan 2019-10-17 14:40:56 +03:00
Sergej Jaskiewicz f4e191b2ff Add more tests for Publishers.Drop (#82) 2019-10-17 09:41:53 +03:00
Sven c275e51cdc Implement Publishers.Drop (#70) 2019-10-16 13:26:10 +03:00
Sergej Jaskiewicz a84105133c Increase timeout for DispatchQueueSchedulerTests.testScheduleActionOnceNow 2019-10-16 02:10:47 +03:00
Sergej Jaskiewicz ef3ebd965a Extract locking API into COpenCombineHelpers module 2019-10-16 02:10:47 +03:00
Sergej Jaskiewicz a08b99c886 Fix a compiler crash on Linux
This crash could only be reproduced in release configuration
2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz 3398499540 Remove Unreachable.swift 2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz 1bf193ddaa Bump Swift version on Linux CI 2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz 3a88dfd76b Implemented Comparison. Use ReduceProducer. 2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz bd0b69d7cb Implement Collect. Use ReduceProducer. 2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz dba76c3c41 Implement Contains, ContainsWhere, TryContainsWhere. Use ReduceProducer. 2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz 5863492753 Implement AllSatisfy, TryAllSatisfy. Use ReduceProducer. 2019-10-15 20:51:44 +03:00
Joe Spadafora 2f2e16ee1f Implement Last, LastWhere, TryLastWhere. Use ReduceProducer. 2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz bca131c2a4 Simplify Publishers.Count. Use ReduceProducer. 2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz e999fafdce Simplify First, FirstWhere, TryFirstWhere. Use ReduceProducer. 2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz b38830e0f1 Implement Reduce, TryReduce. Use ReduceProducer 2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz 525405f64d Implement ReduceProducer
ReduceProducer is a helper class that makes implementing reduce-like
operators trivial.
2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz 693d1145f8 Reenable compatibility tests (#79) 2019-10-13 20:22:03 +03:00
Sergej Jaskiewicz 2f9ddc2229 Bump Swift version on Travis for Linux (#78) 2019-10-10 10:52:18 +03:00
Sergej Jaskiewicz bcd1b727f8 Fix SwiftLint 2019-10-09 23:29:50 +03:00
Sergej Jaskiewicz 5d1034fcc0 Fix Linux build failure due to PTHREAD_MUTEX_ERRORCHECK there being Int, not Int32 2019-10-09 23:03:35 +03:00
Sergej Jaskiewicz 2378f3d97e Better error handling for pthread calls 2019-10-09 20:08:24 +03:00
Sergej Jaskiewicz 4a965830e7 Update docs to match Xcode 11.1 (#77) 2019-10-09 17:51:52 +03:00
Sergej Jaskiewicz 9eabadb7c9 Mention GYB in README.md 2019-10-09 00:12:07 +03:00
Sergej Jaskiewicz dcfaec2c9d Add tests for Publishers.MapKeyPath 2019-10-09 00:12:07 +03:00
Sergej Jaskiewicz 219ee38119 Update RemainingCombineInterface.swift 2019-10-09 00:12:07 +03:00
Sergej Jaskiewicz 3a5389d398 GYB cleanup 2019-10-09 00:12:07 +03:00
Sergej Jaskiewicz 69ead1c8fb Fix indentation 2019-10-09 00:12:07 +03:00
Sergej Jaskiewicz 8e6404592e Add .gitattributes 2019-10-09 00:12:07 +03:00
Sergej Jaskiewicz 14b7ced2fe Use gyb tool to implement MapKeyPath 2019-10-09 00:12:07 +03:00
Sergej Jaskiewicz d7b9e87f6d Execute tests in parallel 2019-10-08 14:26:09 +03:00
Sergej Jaskiewicz 4fd04b8a00 Test Publishers.Print for printing to stdout 2019-10-08 14:26:09 +03:00
157 changed files with 21079 additions and 5448 deletions
+268
View File
@@ -0,0 +1,268 @@
version: 2
jobs:
"Execute tests on macOS 10.15.0 (Xcode 11.2.0, Swift 5.1.2)":
macos:
xcode: "11.2.0"
environment:
SWIFT_VERSION: "5.1.2"
steps:
- checkout
- run:
name: Building and running tests in debug mode with coverage
command: |
make test-debug \
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_TEST_FLAGS="--build-path .build-test-debug-sanitize-thread"
- run:
name: Building and running tests in release mode
command: |
make test-release \
SWIFT_TEST_FLAGS="--build-path .build-test-release"
- run:
name: Generating Xcode project
command: make generate-xcodeproj
- 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.2.2 (Xcode 11.2.0, Swift 5.1.2)":
macos:
xcode: "11.2.0"
environment:
SWIFT_VERSION: "5.1.2"
steps:
- checkout
- run:
name: Generating Xcode project
command: make generate-compatibility-xcodeproj
- run:
name: Building for testing on iOS 13.2.2 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild build-for-testing \
-scheme OpenCombine-Package \
-destination "platform=iOS Simulator,name=iPhone 11,OS=13.2.2" \
-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.2.2 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild test-without-building \
-scheme OpenCombine-Package \
-destination "platform=iOS Simulator,name=iPhone 11,OS=13.2.2" \
-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
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_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_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_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.2.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.2.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.2.0, Swift 5.1.2)"
"OpenCombine: execute compatibility tests":
jobs:
- "Execute compatibility tests on iOS 13.2.2 (Xcode 11.2.0, Swift 5.1.2)"
"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"
+3
View File
@@ -0,0 +1,3 @@
*.swift.gyb linguist-language=Swift
**/GENERATED-* linguist-generated=true
+113
View File
@@ -43,3 +43,116 @@ DerivedData/
# End of https://www.gitignore.io/api/Xcode
.idea
# Created by https://www.gitignore.io/api/Python
# Edit at https://www.gitignore.io/?templates=Python
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# End of https://www.gitignore.io/api/Python
.bundle/
node_modules/
+2 -1
View File
@@ -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
-94
View File
@@ -1,94 +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 | Tests"
os: linux
dist: xenial
sudo: required
env: SWIFT_VERSION="swift-5.1-DEVELOPMENT-SNAPSHOT-2019-09-05-a" 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.0 | Swift 5.1 | Compatibility Tests"
# os: osx
# osx_image: xcode11
# env: SWIFT_VERSION="5.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.0 \
-destination "platform=iOS Simulator,name=iPhone Xs,OS=13.0" \
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
+64
View File
@@ -1,7 +1,71 @@
import Danger
import Foundation
extension StringProtocol {
func dropSuffix<S: StringProtocol>(_ suffix: S) -> SubSequence {
if hasSuffix(suffix) {
return self[..<index(endIndex, offsetBy: -suffix.count)]
} else {
return self[...]
}
}
func directoryAndFileName() -> (SubSequence, SubSequence) {
let lastPathSeparator = lastIndex(of: "/")
if let lastPathSeparator = lastPathSeparator {
return (self[..<lastPathSeparator], self[index(after: lastPathSeparator)...])
} else {
return (".", self[...])
}
}
}
let danger = Danger()
let allCreatedAndModified = danger.git.createdFiles + danger.git.modifiedFiles
do {
// Fail if the committer modified a GYB template but forgot to run `make gyb`.
let modifiedTemplates = allCreatedAndModified.filter { $0.hasSuffix(".gyb") }
for modifiedTemplate in modifiedTemplates {
let (directory, filename) = modifiedTemplate.directoryAndFileName()
let generated = "\(directory)/GENERATED-\(filename.dropSuffix(".gyb"))"
if !allCreatedAndModified.contains(generated) {
fail("""
A template \(modifiedTemplate) was modified, but the file \(generated) \
was not regenerated.
Run `make gyb` from the root of the project and commit the changes.
""")
}
}
}
do {
// Fail if the committer modified a generated file.
// A template should be modified instead.
for modifiedGeneratedFile in danger.git.modifiedFiles
where modifiedGeneratedFile.contains("GENERATED-")
{
let template = modifiedGeneratedFile
.replacingOccurrences(of: "GENERATED-", with: "") + ".gyb"
if !danger.git.modifiedFiles.contains(template) {
fail("""
A generated file \(modifiedGeneratedFile) was modified, but \
the template it was generated from was not modified.
Please modify the template \(template) instead, \
run `make gyb` from the root of the project and commit the changes.
""")
}
}
}
SwiftLint.lint(inline: true,
configFile: ".swiftlint.yml",
strict: true,
+3
View File
@@ -0,0 +1,3 @@
source 'https://rubygems.org'
gem 'xcode-install'
+162
View File
@@ -0,0 +1,162 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.1)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
atomos (0.1.3)
babosa (1.0.3)
claide (1.0.3)
colored (1.2)
colored2 (3.1.2)
commander-fastlane (4.4.6)
highline (~> 1.7.2)
declarative (0.0.10)
declarative-option (0.1.0)
digest-crc (0.4.1)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.5)
emoji_regex (1.0.1)
excon (0.68.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
+15 -9
View File
@@ -1,32 +1,37 @@
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)
clean:
rm -rf .build
@@ -38,4 +43,5 @@ clean:
test-compatibility-debug \
generate-compatibility-xcodeproj \
generate-xcodeproj \
gyb \
clean
+27
View File
@@ -0,0 +1,27 @@
Pod::Spec.new do |spec|
spec.name = "OpenCombine"
spec.version = "0.7.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
+25
View File
@@ -0,0 +1,25 @@
Pod::Spec.new do |spec|
spec.name = "OpenCombineDispatch"
spec.version = "0.7.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.6'
end
-25
View File
@@ -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
}
+1 -5
View File
@@ -8,17 +8,13 @@ let package = Package(
.library(name: "OpenCombine", targets: ["OpenCombine"]),
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
],
dependencies: [
.package(url: "https://github.com/broadwaylamb/GottaGoFast.git", from: "0.1.0")
],
targets: [
.target(name: "COpenCombineHelpers"),
.target(name: "OpenCombine", dependencies: ["COpenCombineHelpers"]),
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
.testTarget(name: "OpenCombineTests",
dependencies: ["OpenCombine",
"OpenCombineDispatch",
"GottaGoFast"],
"OpenCombineDispatch"],
swiftSettings: [.unsafeFlags(["-enable-testing"])])
],
cxxLanguageStandard: .cxx1z
+60 -2
View File
@@ -1,8 +1,9 @@
# OpenCombine
[![Build Status](https://travis-ci.org/broadwaylamb/OpenCombine.svg?branch=master)](https://travis-ci.org/broadwaylamb/OpenCombine)
[![CircleCI](https://circleci.com/gh/broadwaylamb/OpenCombine/tree/master.svg?style=svg)](https://circleci.com/gh/broadwaylamb/OpenCombine/tree/master)
[![codecov](https://codecov.io/gh/broadwaylamb/OpenCombine/branch/master/graph/badge.svg)](https://codecov.io/gh/broadwaylamb/OpenCombine)
![Language](https://img.shields.io/badge/Swift-5.0-orange.svg)
![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20iOS%20%7C%20watchOS%20%7C%20tvOS-lightgrey.svg)
![Cocoapods](https://img.shields.io/cocoapods/v/OpenCombine?color=blue)
[<img src="https://img.shields.io/badge/slack-OpenCombine-yellow.svg?logo=slack">](https://join.slack.com/t/opencombine/shared_invite/enQtNzE2MjE5NzkxODI0LTYxMjkzNDUxZWViZWI1Njc2YjBhODgxNjRjOTdkZTcxOGU2ZjJjZjYxMGI3NWZkN2RkNGFmZTUzNmU3MGE2ZWM)
Open-source implementation of Apple's [Combine](https://developer.apple.com/documentation/combine) framework for processing values over time.
@@ -11,6 +12,42 @@ The main goal of this project is to provide a compatible, reliable and efficient
The project is in early development.
### Installation
`OpenCombine` contains two public targets: `OpenCombine` and `OpenCombineDispatch` (the third 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`.
##### 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.7.0")
],
targets: [
.target(name: "MyAwesomePackage", dependencies: ["OpenCombine", "OpenCombineDispatch"])
]
```
###### 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.7'
pod 'OpenCombineDispatch', '~> 0.7'
```
### Contributing
In order to work on this project you will need Xcode 10.2 and Swift 5.0 or later.
@@ -22,9 +59,30 @@ You can refer to [this gist](https://gist.github.com/broadwaylamb/c2c8550d76b3ff
You can run compatibility tests against Apple's Combine. In order to do that you will need either macOS 10.14 with iOS 13 simulator installed (since the only way we can get Apple's Combine on macOS 10.14 is using the simulator), or macOS 10.15 (Apple's Combine is bundled with the OS). Execute the following command from the root of the package:
```
$ swift test -Xswiftc -DOPENCOMBINE_COMPATIBILITY_TEST
$ make test-compatibility
```
Or enable the `-DOPENCOMBINE_COMPATIBILITY_TEST` compiler flag in Xcode's build settings. Note that on iOS only the latter will work.
> NOTE: Before starting to work on some feature, please consult the [GitHub project](https://github.com/broadwaylamb/OpenCombine/projects/2) to make sure that nobody's already making progress on the same feature! If not, then please create a draft PR to indicate that you're beginning your work.
#### GYB
Some publishers in OpenCombine (like `Publishers.MapKeyPath`, `Publishers.Merge`) exist in several
different flavors in order to support several arities. For example, there are also `Publishers.MapKeyPath2`
and `Publishers.MapKeyPath3`, which are very similar but different enough that Swift's type system
can't help us here (because there's no support for variadic generics). Maintaining multiple instances of
those generic types is tedious and error-prone (they can get out of sync), so we use the GYB tool for
generating those instances from a template.
GYB is a Python script that evaluates Python code written inside a template file, so it's very flexible —
templates can be arbitrarily complex. There is a good article about GYB on
[NSHipster](https://nshipster.com/swift-gyb/).
GYB is part of the [Swift Open Source Project](https://github.com/apple/swift/blob/master/utils/gyb.py)
and can be distributed under the same license as Swift itself.
GYB template files have the `.gyb` extension. Run `make gyb` to generate Swift code from those
templates. The generated files are prefixed with `GENERATED-` and are checked into source control. Those
files should never be edited directly. Instead, the `.gyb` template should be edited, and after that the files
should be regenerated using `make gyb`.
File diff suppressed because it is too large Load Diff
@@ -8,8 +8,231 @@
#include "COpenCombineHelpers.h"
#include <atomic>
#include <cstdlib>
#include <system_error>
#include <pthread.h>
extern "C" uint64_t opencombine_next_combine_identifier() {
static std::atomic<uint64_t> next_combine_identifier;
#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 {
// 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 {
std::atomic<uint64_t> next_combine_identifier;
class PlatformIndependentMutex {
public:
virtual void lock() = 0;
virtual void unlock() = 0;
virtual void assertOwner() {}
virtual ~PlatformIndependentMutex() noexcept(false) {}
};
class PThreadMutex : PlatformIndependentMutex {
private:
pthread_mutex_t mutex_;
public:
PThreadMutex() {
Attributes attrs;
attrs.setErrorCheck();
initialize(attrs);
}
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__
class OS_UNFAIR_LOCK_AVAILABILITY OSUnfairLock final : PlatformIndependentMutex {
os_unfair_lock mutex_ = OS_UNFAIR_LOCK_INIT;
public:
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_);
}
void unlock() override {
os_unfair_lock_unlock(&mutex_);
}
void assertOwner() override {
os_unfair_lock_assert_owner(&mutex_);
}
};
#endif // __APPLE__
} // end anonymous namespace
extern "C" {
uint64_t opencombine_next_combine_identifier(void) {
return next_combine_identifier.fetch_add(1);
}
OpenCombineUnfairLock opencombine_unfair_lock_alloc(void) {
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
#ifdef __APPLE__
if (__builtin_available(macOS 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *)) {
return {new OSUnfairLock};
} else {
return {new PThreadMutex};
}
#else
return {new PThreadMutex};
#endif
OPENCOMBINE_HANDLE_EXCEPTION_END
}
OpenCombineUnfairRecursiveLock opencombine_unfair_recursive_lock_alloc(void) {
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
// TODO: Use os_unfair_recursive_lock on Darwin as soon as it becomes public API.
return {new PThreadRecursiveMutex};
OPENCOMBINE_HANDLE_EXCEPTION_END
}
void opencombine_unfair_lock_lock(OpenCombineUnfairLock lock) {
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
static_cast<PlatformIndependentMutex*>(lock.opaque)->lock();
OPENCOMBINE_HANDLE_EXCEPTION_END
}
void opencombine_unfair_lock_unlock(OpenCombineUnfairLock mutex) {
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
static_cast<PlatformIndependentMutex*>(mutex.opaque)->unlock();
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();
OPENCOMBINE_HANDLE_EXCEPTION_END
}
void opencombine_unfair_recursive_lock_unlock(OpenCombineUnfairRecursiveLock mutex) {
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
static_cast<PlatformIndependentMutex*>(mutex.opaque)->unlock();
OPENCOMBINE_HANDLE_EXCEPTION_END
}
void opencombine_unfair_lock_dealloc(OpenCombineUnfairLock lock) {
return delete static_cast<PlatformIndependentMutex*>(lock.opaque);
}
void opencombine_unfair_recursive_lock_dealloc(OpenCombineUnfairRecursiveLock lock) {
return delete static_cast<PlatformIndependentMutex*>(lock.opaque);
}
} // extern "C"
@@ -9,12 +9,80 @@
#define COPENCOMBINEHELPERS_H
#include <stdint.h>
#include <signal.h>
#if __has_attribute(swift_name)
# define OPENCOMBINE_SWIFT_NAME(_name) __attribute__((swift_name(#_name)))
#else
# 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
uint64_t opencombine_next_combine_identifier();
#pragma mark - CombineIdentifier
uint64_t opencombine_next_combine_identifier(void)
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;
/// 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());
void opencombine_unfair_lock_lock(OpenCombineUnfairLock)
OPENCOMBINE_SWIFT_NAME(__UnfairLock.lock(self:));
void opencombine_unfair_lock_unlock(OpenCombineUnfairLock)
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:));
#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;
OpenCombineUnfairRecursiveLock opencombine_unfair_recursive_lock_alloc(void)
OPENCOMBINE_SWIFT_NAME(__UnfairRecursiveLock.allocate());
void opencombine_unfair_recursive_lock_lock(OpenCombineUnfairRecursiveLock)
OPENCOMBINE_SWIFT_NAME(__UnfairRecursiveLock.lock(self:));
void opencombine_unfair_recursive_lock_unlock(OpenCombineUnfairRecursiveLock)
OPENCOMBINE_SWIFT_NAME(__UnfairRecursiveLock.unlock(self:));
void opencombine_unfair_recursive_lock_dealloc(OpenCombineUnfairRecursiveLock lock)
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"
+1
View File
@@ -10,6 +10,7 @@
/// Subscriber implementations can use this type to provide a cancellation token that
/// makes it possible for a caller to cancel a publisher, but not to use the
/// `Subscription` object to request items.
/// An AnyCancellable instance automatically calls `cancel()` when deinitialized.
public final class AnyCancellable: Cancellable, Hashable {
private var _cancel: (() -> Void)?
+5
View File
@@ -6,6 +6,11 @@
//
extension Publisher {
/// Wraps this publisher with a type eraser.
///
/// Use `eraseToAnyPublisher()` to expose an instance of `AnyPublisher` to
/// the downstream subscriber, rather than this publishers actual type.
public func eraseToAnyPublisher() -> AnyPublisher<Output, Failure> {
return .init(self)
}
+7 -5
View File
@@ -5,21 +5,23 @@
// Created by Sergej Jaskiewicz on 10.06.2019.
//
import func COpenCombineHelpers.opencombine_next_combine_identifier
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
public struct CombineIdentifier: Hashable, CustomStringConvertible {
private let id: UInt64
private let value: UInt64
public init() {
self.id = opencombine_next_combine_identifier()
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,11 +5,15 @@
// Created by Sergej Jaskiewicz on 11.06.2019.
//
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
/// A subject that wraps a single value and publishes a new element whenever the value
/// changes.
public final class CurrentValueSubject<Output, Failure: Error>: Subject {
private let _lock = unfairRecursiveLock()
private let _lock = UnfairRecursiveLock.allocate()
// TODO: Combine uses bag data structure
private var _subscriptions: [Conduit] = []
@@ -43,6 +47,7 @@ public final class CurrentValueSubject<Output, Failure: Error>: Subject {
for subscription in _subscriptions {
subscription._downstream = nil
}
_lock.deallocate()
}
public func send(subscription: Subscription) {
+122
View File
@@ -0,0 +1,122 @@
//
// Future.swift
//
//
// Created by Max Desiatov on 24/11/2019.
//
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
/// 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,180 @@
//
// FilterProducer.swift
//
//
// Created by Sergej Jaskiewicz on 23.10.2019.
//
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
/// 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 -128
View File
@@ -5,21 +5,14 @@
// Created by Sergej Jaskiewicz on 11.06.2019.
//
#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Glibc
#else
#error("How to do locking on this platform?")
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
@usableFromInline
internal protocol UnfairLock: AnyObject {
func lock()
func unlock()
}
internal typealias UnfairLock = __UnfairLock
internal typealias UnfairRecursiveLock = __UnfairRecursiveLock
extension UnfairLock {
extension UnfairRecursiveLock {
@inlinable
internal func `do`<Result>(_ body: () throws -> Result) rethrows -> Result {
@@ -28,119 +21,3 @@ extension UnfairLock {
return try body()
}
}
internal protocol UnfairRecursiveLock: UnfairLock {}
internal func unfairLock() -> UnfairLock {
#if canImport(Darwin)
if #available(macOS 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *) {
return OSUnfairLock()
} else {
return PThreadMutexLock()
}
#else
return PThreadMutexLock()
#endif
}
internal func unfairRecursiveLock() -> UnfairRecursiveLock {
// TODO: Use os_unfair_recursive_lock on Darwin as soon as it becomes public API.
return PThreadMutexRecursiveLock()
}
private class PThreadMutexLock
: UnfairLock,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
{
private let mutex = UnsafeMutablePointer<pthread_mutex_t>.allocate(capacity: 1)
init() {
var status: CInt
var attributes = pthread_mutexattr_t()
status = pthread_mutexattr_init(&attributes)
precondition(status == 0,
"pthread_mutexattr_init returned non-zero status: \(status)")
// Enable error detection
status = pthread_mutexattr_settype(&attributes, CInt(PTHREAD_MUTEX_ERRORCHECK))
precondition(status == 0,
"pthread_mutexattr_settype returned non-zero status: \(status)")
setAdditionalAttributes(&attributes)
status = pthread_mutex_init(mutex, &attributes)
precondition(status == 0,
"pthread_mutex_init returned non-zero status: \(status)")
}
fileprivate func setAdditionalAttributes(
_ attributes: UnsafeMutablePointer<pthread_mutexattr_t>
) {
// Do nothing for non-recursive locks
}
final func lock() {
let status = pthread_mutex_lock(mutex)
precondition(status == 0,
"pthread_mutex_lock returned non-zero status: \(status)")
}
final func unlock() {
let status = pthread_mutex_unlock(mutex)
precondition(status == 0,
"pthread_mutex_lock returned non-zero status: \(status)")
}
final var description: String { return String(describing: mutex.pointee) }
final var customMirror: Mirror { return Mirror(reflecting: mutex.pointee) }
final var playgroundDescription: Any { return description }
deinit {
let status = pthread_mutex_destroy(mutex)
precondition(status == 0,
"pthread_mutex_destroy returned non-zero status: \(status)")
mutex.deallocate()
}
}
private final class PThreadMutexRecursiveLock: PThreadMutexLock, UnfairRecursiveLock {
override func setAdditionalAttributes(
_ attributes: UnsafeMutablePointer<pthread_mutexattr_t>
) {
let status = pthread_mutexattr_settype(attributes, CInt(PTHREAD_MUTEX_RECURSIVE))
precondition(status == 0,
"pthread_mutexattr_settype returned non-zero status: \(status)")
}
}
#if canImport(Darwin)
@available(macOS 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *)
private final class OSUnfairLock
: UnfairLock,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
{
private var mutex = os_unfair_lock()
func lock() {
os_unfair_lock_lock(&mutex)
}
func unlock() {
os_unfair_lock_unlock(&mutex)
}
var description: String { return String(describing: mutex) }
var customMirror: Mirror { return Mirror(reflecting: mutex) }
var playgroundDescription: Any { return description }
}
#endif // canImport(Darwin)
@@ -0,0 +1,26 @@
//
// PartialCompletion.swift
//
//
// Created by Sergej Jaskiewicz on 22.09.2019.
//
/// A value of this type is returned by the overridden `receive(newValue:)` method
/// of the `ReduceProducer` and `FilterProducer` classes.
internal enum PartialCompletion<Value, Failure: Error> {
/// Indicate that we should continue accepting the upstream's output.
case `continue`(Value)
/// Indicate that no values should be received from the upstream anymore.
case finished
/// Indicate that there was a failure and we should send it downstream.
case failure(Failure)
}
extension PartialCompletion where Value == Void {
/// Indicate that we should continue accepting the upstream's output.
internal static var `continue`: PartialCompletion { return .continue(()) }
}
@@ -0,0 +1,254 @@
//
// ReduceProducer.swift
//
//
// Created by Sergej Jaskiewicz on 22.09.2019.
//
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
/// A helper class that acts like both subscriber and subscription.
///
/// Reduce-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 subsription).
///
/// Reduce-like operators include `Publishers.Reduce`, `Publishers.TryReduce`,
/// `Publishers.Count`, `Publishers.FirstWhere`, `Publishers.AllSatisfy` and more.
///
/// Subclasses must override the `receive(newValue:)` and `description`.
internal class ReduceProducer<Downstream: Subscriber,
Input,
Output,
UpstreamFailure: Error,
Reducer>
: CustomStringConvertible,
CustomReflectable
where Downstream.Input == Output
{
// NOTE: This class has been audited for thread safety
// MARK: - State
internal final var result: Output?
private let initial: Output?
internal final let reduce: Reducer
private var status = SubscriptionStatus.awaitingSubscription
private let downstream: Downstream
private let lock = UnfairLock.allocate()
private var downstreamRequested = false
private var cancelled = false
private var completed = false
private var upstreamCompleted = false
private var empty = true
internal init(downstream: Downstream, initial: Output?, reduce: Reducer) {
self.downstream = downstream
self.initial = initial
self.result = initial
self.reduce = reduce
}
deinit {
lock.deallocate()
}
// MARK: - Abstract methods
internal func receive(
newValue: Input
) -> PartialCompletion<Void, Downstream.Failure> {
abstractMethod()
}
internal var description: String {
abstractMethod()
}
// MARK: - CustomReflectable
internal var customMirror: Mirror {
lock.lock()
defer { lock.unlock() }
let children: [Mirror.Child] = [
("downstream", downstream),
("result", result as Any),
("initial", initial as Any),
("status", status)
]
return Mirror(self, children: children)
}
// MARK: - Private
/// - Precondition: `lock` is held.
private func receiveFinished() {
guard !cancelled, !completed, !upstreamCompleted else {
lock.unlock()
// This should never happen, because `receive(completion:)`
// (from which this function is called) early exists if
// `status` is `.terminal`.
assertionFailure("The subscription should have been terminated by now")
return
}
upstreamCompleted = true
self.completed = downstreamRequested || empty
let completed = self.completed
let result = self.result
lock.unlock()
if completed {
sendResultAndFinish(result)
}
}
/// - Precondition: `lock` is held.
private func receiveFailure(_ failure: UpstreamFailure) {
guard !cancelled, !completed, !upstreamCompleted else {
lock.unlock()
// This should never happen, because `receive(completion:)`
// (from which this function is called) early exists if
// `status` is `.terminal`.
assertionFailure("The subscription should have been terminated by now")
return
}
upstreamCompleted = true
completed = true
lock.unlock()
downstream.receive(completion: .failure(failure as! Downstream.Failure))
}
private func sendResultAndFinish(_ result: Output?) {
assert(completed && upstreamCompleted)
if let result = result {
_ = downstream.receive(result)
}
downstream.receive(completion: .finished)
}
// MARK: -
}
extension ReduceProducer: Subscriber {
internal 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)
}
internal func receive(_ input: Input) -> Subscribers.Demand {
lock.lock()
guard case let .subscribed(subscription) = status else {
lock.unlock()
return .none
}
empty = false
lock.unlock()
// Combine doesn't hold the lock when calling `receive(newValue:)`.
//
// This can lead to data races if the contract is violated
// (like when we receive input from multiple threads simultaneously).
switch self.receive(newValue: input) {
case .continue:
break
case .finished:
lock.lock()
upstreamCompleted = true
let downstreamRequested = self.downstreamRequested
if downstreamRequested {
completed = true
}
status = .terminal
let result = self.result
lock.unlock()
subscription.cancel()
guard downstreamRequested else { break }
sendResultAndFinish(result)
case let .failure(error):
lock.lock()
upstreamCompleted = true
completed = true
status = .terminal
lock.unlock()
subscription.cancel()
downstream.receive(completion: .failure(error))
}
return .none
}
internal func receive(completion: Subscribers.Completion<UpstreamFailure>) {
lock.lock()
guard case .subscribed = status else {
lock.unlock()
return
}
status = .terminal
switch completion {
case .finished:
receiveFinished()
case let .failure(error):
receiveFailure(error)
}
}
}
extension ReduceProducer: Subscription {
internal func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
lock.lock()
guard !downstreamRequested, !cancelled, !completed else {
lock.unlock()
return
}
downstreamRequested = true
guard upstreamCompleted else {
lock.unlock()
return
}
completed = true
let result = self.result
lock.unlock()
sendResultAndFinish(result)
}
internal func cancel() {
lock.lock()
guard case let .subscribed(subscription) = status else {
lock.unlock()
return
}
cancelled = true
status = .terminal
lock.unlock()
subscription.cancel()
}
}
extension ReduceProducer: CustomPlaygroundDisplayConvertible {
internal var playgroundDescription: Any { return description }
}
@@ -5,6 +5,10 @@
// Created by Sergej Jaskiewicz on 16/09/2019.
//
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
// NOTE: This class has been audited for thread safety.
internal final class SubjectSubscriber<Downstream: Subject>
: Subscriber,
@@ -13,7 +17,7 @@ internal final class SubjectSubscriber<Downstream: Subject>
CustomPlaygroundDisplayConvertible,
Subscription
{
private let lock = unfairLock()
private let lock = UnfairLock.allocate()
private var downstreamSubject: Downstream?
private var upstreamSubscription: Subscription?
@@ -23,6 +27,10 @@ internal final class SubjectSubscriber<Downstream: Subject>
self.downstreamSubject = parent
}
deinit {
lock.deallocate()
}
internal func receive(subscription: Subscription) {
lock.lock()
guard upstreamSubscription == nil, let subject = downstreamSubject else {
@@ -1,63 +0,0 @@
//
// Unreachable.swift
// Unreachable
//
// The MIT License (MIT)
//
// Copyright (c) 2017 Nikolai Vazquez
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// All the credits go to https://github.com/nvzqz/Unreachable
/// An unreachable code path.
///
/// This can be used for whenever the compiler can't determine that a
/// path is unreachable, such as dynamically terminating an iterator.
@inline(__always)
private func unsafeUnreachable() -> Never {
return unsafeBitCast((), to: Never.self)
}
/// Asserts that the code path is unreachable.
///
/// Calls `assertionFailure(_:file:line:)` in unoptimized builds and `unreachable()`
/// otherwise.
///
/// - parameter message: The message to print. The default is
/// "Encountered unreachable path".
/// - parameter file: The file name to print with the message. The default is the file
/// where this function is called.
/// - parameter line: The line number to print with the message. The default is the line
/// where this function is called.
@inline(__always)
internal func unreachable(
_ message: @autoclosure () -> String = "Encountered unreachable path",
file: StaticString = #file,
line: UInt = #line
) -> Never {
var isDebug = false
assert({ isDebug = true; return true }())
if isDebug {
fatalError(message(), file: file, line: line)
} else {
unsafeUnreachable()
}
}
+1 -2
View File
@@ -19,9 +19,8 @@ internal func APIViolationUnexpectedCompletion(file: StaticString = #file,
fatalError("API Violation: received an unexpected completion", file: file, line: line)
}
@inline(__always)
internal func abstractMethod(file: StaticString = #file, line: UInt = #line) -> Never {
unreachable("Abstract method call", file: file, line: line)
fatalError("Abstract method call", file: file, line: line)
}
extension Subscribers.Demand {
+3 -2
View File
@@ -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,85 @@
//
// 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 subject: PassthroughSubject<Void, Never>
public init() {
subject = .init()
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == Void, Downstream.Failure == Never
{
subject.subscribe(subscriber)
}
public func send() {
subject.send()
}
}
+7 -2
View File
@@ -5,13 +5,17 @@
// Created by Sergej Jaskiewicz on 11.06.2019.
//
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
/// A subject that passes along values and completion.
///
/// Use a `PassthroughSubject` in unit tests when you want a publisher than can publish
/// specific values on-demand during tests.
public final class PassthroughSubject<Output, Failure: Error>: Subject {
private let _lock = unfairRecursiveLock()
private let _lock = UnfairRecursiveLock.allocate()
private var _completion: Subscribers.Completion<Failure>?
@@ -28,6 +32,7 @@ public final class PassthroughSubject<Output, Failure: Error>: Subject {
for subscription in _subscriptions {
subscription._downstream = nil
}
_lock.deallocate()
}
public func send(subscription: Subscription) {
@@ -117,7 +122,7 @@ extension PassthroughSubject {
}
fileprivate func request(_ demand: Subscribers.Demand) {
precondition(demand > 0, "demand must not be zero")
demand.assertNonZero()
_parent?._lock.do {
_demand += demand
}
+39 -33
View File
@@ -12,36 +12,31 @@
/// and a publisher which sends any new values after the property value
/// has been sent. New subscribers will receive the current value
/// of the property first.
@propertyWrapper public struct Published<Value> {
/// Note that the `@Published` property is class-constrained.
/// Use it with properties of classes, not with non-class types like structures.
@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
}
/// 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
{
@@ -57,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 {
@@ -70,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 }
}
}
@@ -0,0 +1,316 @@
//
//
// Auto-generated from GYB template. DO NOT EDIT!
//
//
//
//
// Publishers.MapKeyPath.swift.gyb
//
//
// Created by Sergej Jaskiewicz on 03/10/2019.
//
extension Publisher {
/// Returns a publisher that publishes the values of a keyt path as a tuple.
///
/// - Parameters:
/// - keyPath: The key path of a property on `Output`
/// - Returns: A publisher that publishes the value of the key path.
public func map<Result>(
_ keyPath: KeyPath<Output, Result>
) -> Publishers.MapKeyPath<Self, Result> {
return .init(
upstream: self,
keyPath: keyPath
)
}
/// Returns a publisher that publishes the values of two key paths as a tuple.
///
/// - Parameters:
/// - keyPath0: The key path of a property on `Output`
/// - keyPath1: The key path of another property on `Output`
/// - Returns: A publisher that publishes the values of two key paths as a tuple.
public func map<Result0, Result1>(
_ keyPath0: KeyPath<Output, Result0>,
_ keyPath1: KeyPath<Output, Result1>
) -> Publishers.MapKeyPath2<Self, Result0, Result1> {
return .init(
upstream: self,
keyPath0: keyPath0,
keyPath1: keyPath1
)
}
/// Returns a publisher that publishes the values of three key paths as a tuple.
///
/// - Parameters:
/// - keyPath0: The key path of a property on `Output`
/// - keyPath1: The key path of another property on `Output`
/// - keyPath2: The key path of a third property on `Output`
/// - Returns: A publisher that publishes the values of three key paths as a tuple.
public func map<Result0, Result1, Result2>(
_ keyPath0: KeyPath<Output, Result0>,
_ keyPath1: KeyPath<Output, Result1>,
_ keyPath2: KeyPath<Output, Result2>
) -> Publishers.MapKeyPath3<Self, Result0, Result1, Result2> {
return .init(
upstream: self,
keyPath0: keyPath0,
keyPath1: keyPath1,
keyPath2: keyPath2
)
}
}
extension Publishers {
/// A publisher that publishes the value of a key path.
public struct MapKeyPath<Upstream: Publisher, Output>: Publisher {
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// The key path of a property to publish.
public let keyPath: KeyPath<Upstream.Output, Output>
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Output == Downstream.Input, Failure == Downstream.Failure
{
upstream.subscribe(Inner(downstream: subscriber, parent: self))
}
}
/// A publisher that publishes the values of two key paths as a tuple.
public struct MapKeyPath2<Upstream: Publisher, Output0, Output1>: Publisher {
public typealias Output = (Output0, Output1)
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// The key path of a property to publish.
public let keyPath0: KeyPath<Upstream.Output, Output0>
/// The key path of a second property to publish.
public let keyPath1: KeyPath<Upstream.Output, Output1>
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Output == Downstream.Input, Failure == Downstream.Failure
{
upstream.subscribe(Inner(downstream: subscriber, parent: self))
}
}
/// A publisher that publishes the values of three key paths as a tuple.
public struct MapKeyPath3<Upstream: Publisher, Output0, Output1, Output2>: Publisher {
public typealias Output = (Output0, Output1, Output2)
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// The key path of a property to publish.
public let keyPath0: KeyPath<Upstream.Output, Output0>
/// The key path of a second property to publish.
public let keyPath1: KeyPath<Upstream.Output, Output1>
/// The key path of a third property to publish.
public let keyPath2: KeyPath<Upstream.Output, Output2>
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Output == Downstream.Input, Failure == Downstream.Failure
{
upstream.subscribe(Inner(downstream: subscriber, parent: self))
}
}
}
extension Publishers.MapKeyPath {
private struct Inner<Downstream: Subscriber>
: Subscriber,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Output, Downstream.Failure == Upstream.Failure
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private let downstream: Downstream
private let keyPath: KeyPath<Input, Output>
let combineIdentifier = CombineIdentifier()
fileprivate init(
downstream: Downstream,
parent: Publishers.MapKeyPath<Upstream, Output>
) {
self.downstream = downstream
self.keyPath = parent.keyPath
}
func receive(subscription: Subscription) {
downstream.receive(subscription: subscription)
}
func receive(_ input: Input) -> Subscribers.Demand {
let output = (
input[keyPath: keyPath]
)
return downstream.receive(output)
}
func receive(completion: Subscribers.Completion<Failure>) {
downstream.receive(completion: completion)
}
var description: String { return "ValueForKey" }
var customMirror: Mirror {
let children: [Mirror.Child] = [
("keyPath", keyPath),
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
}
}
extension Publishers.MapKeyPath2 {
private struct Inner<Downstream: Subscriber>
: Subscriber,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Output, Downstream.Failure == Upstream.Failure
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private let downstream: Downstream
private let keyPath0: KeyPath<Input, Output0>
private let keyPath1: KeyPath<Input, Output1>
let combineIdentifier = CombineIdentifier()
fileprivate init(
downstream: Downstream,
parent: Publishers.MapKeyPath2<Upstream, Output0, Output1>
) {
self.downstream = downstream
self.keyPath0 = parent.keyPath0
self.keyPath1 = parent.keyPath1
}
func receive(subscription: Subscription) {
downstream.receive(subscription: subscription)
}
func receive(_ input: Input) -> Subscribers.Demand {
let output = (
input[keyPath: keyPath0],
input[keyPath: keyPath1]
)
return downstream.receive(output)
}
func receive(completion: Subscribers.Completion<Failure>) {
downstream.receive(completion: completion)
}
var description: String { return "ValueForKeys" }
var customMirror: Mirror {
let children: [Mirror.Child] = [
("keyPath0", keyPath0),
("keyPath1", keyPath1),
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
}
}
extension Publishers.MapKeyPath3 {
private struct Inner<Downstream: Subscriber>
: Subscriber,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Output, Downstream.Failure == Upstream.Failure
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private let downstream: Downstream
private let keyPath0: KeyPath<Input, Output0>
private let keyPath1: KeyPath<Input, Output1>
private let keyPath2: KeyPath<Input, Output2>
let combineIdentifier = CombineIdentifier()
fileprivate init(
downstream: Downstream,
parent: Publishers.MapKeyPath3<Upstream, Output0, Output1, Output2>
) {
self.downstream = downstream
self.keyPath0 = parent.keyPath0
self.keyPath1 = parent.keyPath1
self.keyPath2 = parent.keyPath2
}
func receive(subscription: Subscription) {
downstream.receive(subscription: subscription)
}
func receive(_ input: Input) -> Subscribers.Demand {
let output = (
input[keyPath: keyPath0],
input[keyPath: keyPath1],
input[keyPath: keyPath2]
)
return downstream.receive(output)
}
func receive(completion: Subscribers.Completion<Failure>) {
downstream.receive(completion: completion)
}
var description: String { return "ValueForKeys" }
var customMirror: Mirror {
let children: [Mirror.Child] = [
("keyPath0", keyPath0),
("keyPath1", keyPath1),
("keyPath2", keyPath2),
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
}
}
+20
View File
@@ -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,178 @@
//
// Publishers.AllSatisfy.swift
//
//
// Created by Sergej Jaskiewicz on 09.10.2019.
//
extension Publisher {
/// Publishes a single Boolean value that indicates whether all received elements pass
/// a given predicate.
///
/// When this publisher receives an element, it runs the predicate against
/// the element. If the predicate returns `false`, the publisher produces a `false`
/// value and finishes. If the upstream publisher finishes normally, this publisher
/// produces a `true` value and finishes.
/// As a `reduce`-style operator, this publisher produces at most one value.
/// Backpressure note: Upon receiving any request greater than zero, this publisher
/// requests unlimited elements from the upstream publisher.
///
/// - Parameter predicate: A closure that evaluates each received element.
/// Return `true` to continue, or `false` to cancel the upstream and complete.
/// - Returns: A publisher that publishes a Boolean value that indicates whether
/// all received elements pass a given predicate.
public func allSatisfy(
_ predicate: @escaping (Output) -> Bool
) -> Publishers.AllSatisfy<Self> {
return .init(upstream: self, predicate: predicate)
}
/// Publishes a single Boolean value that indicates whether all received elements pass
/// a given error-throwing predicate.
///
/// When this publisher receives an element, it runs the predicate against
/// the element. If the predicate returns `false`, the publisher produces a `false`
/// value and finishes. If the upstream publisher finishes normally, this publisher
/// produces a `true` value and finishes. If the predicate throws an error,
/// the publisher fails, passing the error to its downstream.
/// As a `reduce`-style operator, this publisher produces at most one value.
/// Backpressure note: Upon receiving any request greater than zero, this publisher
/// requests unlimited elements from the upstream publisher.
///
/// - Parameter predicate: A closure that evaluates each received element.
/// Return `true` to continue, or `false` to cancel the upstream and complete.
/// The closure may throw, in which case the publisher cancels the upstream
/// publisher and fails with the thrown error.
/// - Returns: A publisher that publishes a Boolean value that indicates whether
/// all received elements pass a given predicate.
public func tryAllSatisfy(
_ predicate: @escaping (Output) throws -> Bool
) -> Publishers.TryAllSatisfy<Self> {
return .init(upstream: self, predicate: predicate)
}
}
extension Publishers {
/// A publisher that publishes a single Boolean value that indicates whether
/// all received elements pass a given predicate.
public struct AllSatisfy<Upstream: Publisher>: Publisher {
public typealias Output = Bool
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// A closure that evaluates each received element.
///
/// Return `true` to continue, or `false` to cancel the upstream and finish.
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, Downstream.Input == Bool
{
upstream.subscribe(Inner(downstream: subscriber, predicate: predicate))
}
}
/// A publisher that publishes a single Boolean value that indicates whether
/// all received elements pass a given error-throwing predicate.
public struct TryAllSatisfy<Upstream: Publisher>: Publisher {
public typealias Output = Bool
public typealias Failure = Error
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// A closure that evaluates each received element.
///
/// Return `true` to continue, or `false` to cancel the upstream and complete.
/// The closure may throw, in which case the publisher cancels the upstream
/// publisher and fails with the thrown error.
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 Downstream.Failure == Error, Downstream.Input == Bool
{
upstream.subscribe(Inner(downstream: subscriber, predicate: predicate))
}
}
}
extension Publishers.AllSatisfy {
private final class Inner<Downstream: Subscriber>
: ReduceProducer<Downstream,
Upstream.Output,
Bool,
Upstream.Failure,
(Upstream.Output) -> Bool>
where Downstream.Input == Output, Upstream.Failure == Downstream.Failure
{
fileprivate init(downstream: Downstream,
predicate: @escaping (Upstream.Output) -> Bool) {
super.init(downstream: downstream, initial: true, reduce: predicate)
}
override func receive(
newValue: Upstream.Output
) -> PartialCompletion<Void, Downstream.Failure> {
if !reduce(newValue) {
result = false
return .finished
}
return .continue
}
override var description: String { return "AllSatisfy" }
}
}
extension Publishers.TryAllSatisfy {
private final class Inner<Downstream: Subscriber>
: ReduceProducer<Downstream,
Upstream.Output,
Bool,
Upstream.Failure,
(Upstream.Output) throws -> Bool>
where Downstream.Input == Output, Downstream.Failure == Error
{
fileprivate init(downstream: Downstream,
predicate: @escaping (Upstream.Output) throws -> Bool) {
super.init(downstream: downstream, initial: true, reduce: predicate)
}
override func receive(
newValue: Upstream.Output
) -> PartialCompletion<Void, Downstream.Failure> {
do {
if try !reduce(newValue) {
result = false
return .finished
}
} catch {
return .failure(error)
}
return .continue
}
override var description: String { return "TryAllSatisfy" }
}
}
@@ -5,6 +5,10 @@
// Created by Sergej Jaskiewicz on 18/09/2019.
//
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
extension ConnectablePublisher {
/// Automates the process of connecting or disconnecting from this connectable
@@ -45,7 +49,7 @@ extension Publishers {
/// The publisher from which this publisher receives elements.
public final let upstream: Upstream
private let lock = unfairLock()
private let lock = UnfairLock.allocate()
private var state = State.disconnected
@@ -53,6 +57,10 @@ extension Publishers {
self.upstream = upstream
}
deinit {
lock.deallocate()
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == Output, Downstream.Failure == Failure
{
@@ -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,85 @@
//
// Publishers.Collect.swift
//
//
// Created by Sergej Jaskiewicz on 09.10.2019.
//
extension Publisher {
/// Collects all received elements, and emits a single array of the collection when
/// the upstream publisher finishes.
///
/// If the upstream publisher fails with an error, this publisher forwards the error
/// to the downstream receiver instead of sending its output.
/// This publisher requests an unlimited number of elements from the upstream
/// publisher. It only sends the collected array to its downstream after a request
/// whose demand is greater than 0 items.
/// Note: This publisher uses an unbounded amount of memory to store the received
/// values.
///
/// - Returns: A publisher that collects all received items and returns them as
/// an array upon completion.
public func collect() -> Publishers.Collect<Self> {
return .init(upstream: self)
}
}
extension Publishers {
/// A publisher that buffers items.
public struct Collect<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
public init(upstream: Upstream) {
self.upstream = upstream
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Upstream.Failure == Downstream.Failure,
Downstream.Input == [Upstream.Output]
{
upstream.subscribe(Inner(downstream: subscriber))
}
}
}
extension Publishers.Collect: Equatable where Upstream: Equatable {}
extension Publishers.Collect {
private final class Inner<Downstream: Subscriber>
: ReduceProducer<Downstream,
Upstream.Output,
[Upstream.Output],
Upstream.Failure,
Void>
where Downstream.Input == [Upstream.Output],
Downstream.Failure == Upstream.Failure
{
fileprivate init(downstream: Downstream) {
super.init(downstream: downstream, initial: [], reduce: ())
}
override func receive(
newValue: Upstream.Output
) -> PartialCompletion<Void, Downstream.Failure> {
result!.append(newValue)
return .continue
}
override var description: String {
return "Collect"
}
override var customMirror: Mirror {
let children: CollectionOfOne<Mirror.Child> = .init(("count", result!.count))
return Mirror(self, children: children)
}
}
}
@@ -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" }
}
}
@@ -0,0 +1,246 @@
//
// Publishers.Comparison.swift
// OpenCombine
//
// Created by Ilija Puaca on 22/7/19.
//
extension Publisher where Output: Comparable {
/// Publishes the minimum value received from the upstream publisher, after it
/// finishes.
///
/// After this publisher receives a request for more than 0 items, it requests
/// unlimited items from its upstream publisher.
///
/// - Returns: A publisher that publishes the minimum value received from the upstream
/// publisher, after the upstream publisher finishes.
public func min() -> Publishers.Comparison<Self> {
return max(by: >)
}
/// Publishes the maximum value received from the upstream publisher, after it
/// finishes.
///
/// After this publisher receives a request for more than 0 items, it requests
/// unlimited items from its upstream publisher.
///
/// - Returns: A publisher that publishes the maximum value received from the upstream
/// publisher, after the upstream publisher finishes.
public func max() -> Publishers.Comparison<Self> {
return max(by: <)
}
}
extension Publisher {
/// Publishes the minimum value received from the upstream publisher, after it
/// finishes.
///
/// After this publisher receives a request for more than 0 items, it requests
/// unlimited items from its upstream publisher.
///
/// - Parameter areInIncreasingOrder: A closure that receives two elements and returns
/// `true` if they are in increasing order.
/// - Returns: A publisher that publishes the minimum value received from the upstream
/// publisher, after the upstream publisher finishes.
public func min(
by areInIncreasingOrder: @escaping (Output, Output) -> Bool
) -> Publishers.Comparison<Self> {
return max(by: { areInIncreasingOrder($1, $0) })
}
/// Publishes the minimum value received from the upstream publisher, using the
/// provided error-throwing closure to order the items.
///
/// After this publisher receives a request for more than 0 items, it requests
/// unlimited items from its upstream publisher.
///
/// - Parameter areInIncreasingOrder: A throwing closure that receives two elements
/// and returns `true` if they are in increasing order. If this closure throws, the
/// publisher terminates with a `Failure`.
/// - Returns: A publisher that publishes the minimum value received from the upstream
/// publisher, after the upstream publisher finishes.
public func tryMin(
by areInIncreasingOrder: @escaping (Output, Output) throws -> Bool
) -> Publishers.TryComparison<Self> {
return tryMax(by: { try areInIncreasingOrder($1, $0) })
}
/// Publishes the maximum value received from the upstream publisher, using the
/// provided ordering closure.
///
/// After this publisher receives a request for more than 0 items, it requests
/// unlimited items from its upstream publisher.
///
/// - Parameter areInIncreasingOrder: A closure that receives two elements and returns
/// `true` if they are in increasing order.
/// - Returns: A publisher that publishes the maximum value received from the upstream
/// publisher, after the upstream publisher finishes.
public func max(
by areInIncreasingOrder: @escaping (Output, Output) -> Bool
) -> Publishers.Comparison<Self> {
return .init(upstream: self, areInIncreasingOrder: areInIncreasingOrder)
}
/// Publishes the maximum value received from the upstream publisher, using the
/// provided error-throwing closure to order the items.
///
/// After this publisher receives a request for more than 0 items, it requests
/// unlimited items from its upstream publisher.
/// - Parameter areInIncreasingOrder: A throwing closure that receives two elements
/// and returns `true` if they are in increasing order. If this closure throws, the
/// publisher terminates with a `Failure`.
/// - Returns: A publisher that publishes the maximum value received from the upstream
/// publisher, after the upstream publisher finishes.
public func tryMax(
by areInIncreasingOrder: @escaping (Output, Output) throws -> Bool
) -> Publishers.TryComparison<Self> {
return .init(upstream: self, areInIncreasingOrder: areInIncreasingOrder)
}
}
extension Publishers {
/// A publisher that republishes items from another publisher only if each new item is
/// in increasing order from the previously-published item.
public struct Comparison<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
/// A closure that receives two elements and returns `true` if they are in
/// increasing order.
public let areInIncreasingOrder: (Upstream.Output, Upstream.Output) -> Bool
public init(
upstream: Upstream,
areInIncreasingOrder: @escaping (Upstream.Output, Upstream.Output) -> Bool
) {
self.upstream = upstream
self.areInIncreasingOrder = areInIncreasingOrder
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Upstream.Failure == Downstream.Failure,
Upstream.Output == Downstream.Input
{
let inner = Inner(downstream: subscriber,
areInIncreasingOrder: areInIncreasingOrder)
upstream.subscribe(inner)
}
}
/// A publisher that republishes items from another publisher only if each new item is
/// in increasing order from the previously-published item, and fails if the ordering
/// logic throws an error.
public struct TryComparison<Upstream: Publisher>: Publisher {
public typealias Output = Upstream.Output
public typealias Failure = Error
/// The publisher that this publisher receives elements from.
public let upstream: Upstream
/// A closure that receives two elements and returns `true` if they are in
/// increasing order.
public let areInIncreasingOrder: (Upstream.Output, Upstream.Output) throws -> Bool
public init(
upstream: Upstream,
areInIncreasingOrder:
@escaping (Upstream.Output, Upstream.Output) throws -> Bool
) {
self.upstream = upstream
self.areInIncreasingOrder = areInIncreasingOrder
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Upstream.Output == Downstream.Input, Downstream.Failure == Error
{
let inner = Inner(downstream: subscriber,
areInIncreasingOrder: areInIncreasingOrder)
upstream.subscribe(inner)
}
}
}
extension Publishers.Comparison {
private final class Inner<Downstream: Subscriber>
: ReduceProducer<Downstream,
Upstream.Output,
Upstream.Output,
Upstream.Failure,
(Upstream.Output, Upstream.Output) -> Bool>
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
{
fileprivate init(
downstream: Downstream,
areInIncreasingOrder: @escaping (Upstream.Output, Upstream.Output) -> Bool
) {
super.init(downstream: downstream, initial: nil, reduce: areInIncreasingOrder)
}
override func receive(
newValue: Upstream.Output
) -> PartialCompletion<Void, Downstream.Failure> {
if let result = self.result {
if reduce(result, newValue) {
self.result = newValue
}
} else {
self.result = newValue
}
return .continue
}
override var description: String {
return "Comparison"
}
}
}
extension Publishers.TryComparison {
private final class Inner<Downstream: Subscriber>
: ReduceProducer<Downstream,
Upstream.Output,
Upstream.Output,
Upstream.Failure,
(Upstream.Output, Upstream.Output) throws -> Bool>
where Downstream.Input == Upstream.Output, Downstream.Failure == Error
{
fileprivate init(
downstream: Downstream,
areInIncreasingOrder:
@escaping (Upstream.Output, Upstream.Output) throws -> Bool
) {
super.init(downstream: downstream, initial: nil, reduce: areInIncreasingOrder)
}
override func receive(
newValue: Upstream.Output
) -> PartialCompletion<Void, Downstream.Failure> {
do {
if let result = self.result {
if try reduce(result, newValue) {
self.result = newValue
}
} else {
self.result = newValue
}
return .continue
} catch {
return .failure(error)
}
}
override var description: String {
return "TryComparison"
}
}
}
@@ -0,0 +1,250 @@
//
// 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 publishers elements.
/// - Returns: A publisher that prefixes the specified elements prior to this
/// publishers 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 publishers
/// elements.
/// - Returns: A publisher that prefixes the sequence of elements prior to this
/// publishers 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 publishers output with the elements emitted by the given publisher.
///
/// The resulting publisher doesnt emit any elements until the prefixing publisher
/// finishes.
///
/// - Parameter publisher: The prefixing publisher.
/// - Returns: A publisher that prefixes the prefixing publishers elements prior to
/// this publishers 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 publishers output with the elements emitted by the given publisher.
///
/// This operator produces no elements until this publisher finishes. It then produces
/// this publishers elements, followed by the given publishers elements.
/// If this publisher fails with an error, the prefixing publisher does not publish
/// the provided publishers elements.
///
/// - Parameter publisher: The appending publisher.
/// - Returns: A publisher that appends the appending publishers elements after this
/// publishers 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 publishers 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()
// ??? This lock is non-recursive in Combine, but it should be!
// (FB7404824 if Apple folks are watching)
private let downstreamLock = UnfairLock.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 }
}
}
@@ -0,0 +1,235 @@
//
// Publishers.Contains.swift
//
//
// Created by Sergej Jaskiewicz on 09.10.2019.
//
extension Publisher where Output: Equatable {
/// Publishes a Boolean value upon receiving an element equal to the argument.
///
/// The contains publisher consumes all received elements until the upstream publisher
/// produces a matching element. At that point, it emits `true` and finishes normally.
/// If the upstream finishes normally without producing a matching element,
/// this publisher emits `false`, then finishes.
///
/// - Parameter output: An element to match against.
/// - Returns: A publisher that emits the Boolean value `true` when the upstream
/// publisher emits a matching value.
public func contains(_ output: Output) -> Publishers.Contains<Self> {
return .init(upstream: self, output: output)
}
}
extension Publisher {
/// Publishes a Boolean value upon receiving an element that satisfies the predicate
/// closure.
///
/// This operator consumes elements produced from the upstream publisher until
/// the upstream publisher produces a matching element.
///
/// - Parameter predicate: A closure that takes an element as its parameter and
/// returns a Boolean value indicating whether the element satisfies the closures
/// comparison logic.
/// - Returns: A publisher that emits the Boolean value `true` when the upstream
/// publisher emits a matching value.
public func contains(
where predicate: @escaping (Output) -> Bool
) -> Publishers.ContainsWhere<Self> {
return .init(upstream: self, predicate: predicate)
}
/// Publishes a Boolean value upon receiving an element that satisfies
/// the throwing predicate closure.
///
/// This operator consumes elements produced from the upstream publisher until
/// the upstream publisher produces a matching element. If the closure throws,
/// the stream fails with an error.
///
/// - Parameter predicate: A closure that takes an element as its parameter and
/// returns a Boolean value indicating whether the element satisfies the closures
/// comparison logic.
/// - Returns: A publisher that emits the Boolean value `true` when the upstream
/// publisher emits a matching value.
public func tryContains(
where predicate: @escaping (Output) throws -> Bool
) -> Publishers.TryContainsWhere<Self> {
return .init(upstream: self, predicate: predicate)
}
}
extension Publishers {
/// A publisher that emits a Boolean value when a specified element is received from
/// its upstream publisher.
public struct Contains<Upstream: Publisher>: Publisher
where Upstream.Output: Equatable
{
public typealias Output = Bool
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// The element to scan for in the upstream publisher.
public let output: Upstream.Output
public init(upstream: Upstream, output: Upstream.Output) {
self.upstream = upstream
self.output = output
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Upstream.Failure == Downstream.Failure, Downstream.Input == Bool
{
upstream.subscribe(Inner(downstream: subscriber, output: output))
}
}
/// A publisher that emits a Boolean value upon receiving an element that satisfies
/// the predicate closure.
public struct ContainsWhere<Upstream: Publisher>: Publisher {
public typealias Output = Bool
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// The closure that determines whether the publisher should consider an element
/// as a match.
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, Downstream.Input == Bool
{
upstream.subscribe(Inner(downstream: subscriber, predicate: predicate))
}
}
/// A publisher that emits a Boolean value upon receiving an element that satisfies
/// the throwing predicate closure.
public struct TryContainsWhere<Upstream: Publisher>: Publisher {
public typealias Output = Bool
public typealias Failure = Error
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// The error-throwing closure that determines whether this publisher should
/// emit a `true` element.
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 Downstream.Failure == Error, Downstream.Input == Bool
{
upstream.subscribe(Inner(downstream: subscriber, predicate: predicate))
}
}
}
extension Publishers.Contains {
private final class Inner<Downstream: Subscriber>
: ReduceProducer<Downstream, Upstream.Output, Bool, Upstream.Failure, Void>
where Upstream.Failure == Downstream.Failure, Downstream.Input == Bool
{
private let output: Upstream.Output
fileprivate init(downstream: Downstream, output: Upstream.Output) {
self.output = output
super.init(downstream: downstream, initial: false, reduce: ())
}
override func receive(
newValue: Upstream.Output
) -> PartialCompletion<Void, Downstream.Failure> {
if newValue == output {
result = true
return .finished
}
return .continue
}
override var description: String { return "Contains" }
}
}
extension Publishers.Contains : Equatable where Upstream: Equatable {}
extension Publishers.ContainsWhere {
private final class Inner<Downstream: Subscriber>
: ReduceProducer<Downstream,
Upstream.Output, Bool,
Upstream.Failure,
(Upstream.Output) -> Bool>
where Upstream.Failure == Downstream.Failure, Downstream.Input == Bool
{
fileprivate init(downstream: Downstream,
predicate: @escaping (Upstream.Output) -> Bool) {
super.init(downstream: downstream, initial: false, reduce: predicate)
}
override func receive(
newValue: Upstream.Output
) -> PartialCompletion<Void, Downstream.Failure> {
if reduce(newValue) {
result = true
return .finished
}
return .continue
}
override var description: String { return "ContainsWhere" }
}
}
extension Publishers.TryContainsWhere {
private final class Inner<Downstream: Subscriber>
: ReduceProducer<Downstream,
Upstream.Output, Bool,
Upstream.Failure,
(Upstream.Output) throws -> Bool>
where Downstream.Failure == Error, Downstream.Input == Bool
{
fileprivate init(downstream: Downstream,
predicate: @escaping (Upstream.Output) throws -> Bool) {
super.init(downstream: downstream, initial: false, reduce: predicate)
}
override func receive(
newValue: Upstream.Output
) -> PartialCompletion<Void, Downstream.Failure> {
do {
if try reduce(newValue) {
result = true
return .finished
}
} catch {
return .failure(error)
}
return .continue
}
override var description: String { return "TryContainsWhere" }
}
}
@@ -5,6 +5,17 @@
// Created by Joseph Spadafora on 6/25/19.
//
extension Publisher {
/// Publishes the number of elements received from the upstream publisher.
///
/// - Returns: A publisher that consumes all elements until the upstream publisher
/// finishes, then emits a single value with the total number of elements received.
public func count() -> Publishers.Count<Self> {
return Publishers.Count(upstream: self)
}
}
extension Publishers {
/// A publisher that publishes the number of elements received
@@ -37,58 +48,30 @@ extension Publishers {
where Upstream.Failure == Downstream.Failure,
Downstream.Input == Output
{
let count = _Count<Upstream, Downstream>(downstream: subscriber)
upstream.subscribe(count)
upstream.subscribe(Inner(downstream: subscriber))
}
}
}
extension Publisher {
extension Publishers.Count: Equatable where Upstream: Equatable {}
/// Publishes the number of elements received from the upstream publisher.
///
/// - Returns: A publisher that consumes all elements until the upstream publisher
/// finishes, then emits a single value with the total number of elements received.
public func count() -> Publishers.Count<Self> {
return Publishers.Count(upstream: self)
}
}
private final class _Count<Upstream: Publisher, Downstream: Subscriber>
: OperatorSubscription<Downstream>,
Subscriber,
CustomStringConvertible,
Subscription
where Downstream.Input == Int,
Upstream.Failure == Downstream.Failure
{
typealias Input = Upstream.Output
typealias Output = Int
typealias Failure = Downstream.Failure
private var _count = 0
var description: String { return "Count" }
func receive(subscription: Subscription) {
upstreamSubscription = subscription
downstream.receive(subscription: self)
upstreamSubscription?.request(.unlimited)
}
func receive(_ input: Input) -> Subscribers.Demand {
_count += 1
return .none
}
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
if case .finished = completion {
_ = downstream.receive(_count)
extension Publishers.Count {
private final class Inner<Downstream: Subscriber>
: ReduceProducer<Downstream, Upstream.Output, Int, Failure, Void>
where Downstream.Input == Int,
Upstream.Failure == Downstream.Failure
{
fileprivate init(downstream: Downstream) {
super.init(downstream: downstream, initial: 0, reduce: ())
}
downstream.receive(completion: completion)
}
func request(_ demand: Subscribers.Demand) {
override func receive(
newValue: Upstream.Output
) -> PartialCompletion<Void, Downstream.Failure> {
result! += 1
return .continue
}
override var description: String { return "Count" }
}
}
@@ -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,229 @@
//
// Publishers.Delay.swift
// OpenCombine
//
// Created by Евгений Богомолов on 07/09/2019.
//
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
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 = UnfairLock.allocate()
fileprivate init(_ publisher: Delay, downstream: Downstream) {
state = .ready(publisher, downstream)
}
deinit {
lock.deallocate()
downstreamLock.deallocate()
}
private func schedule(_ delay: Delay,
immediate: Bool,
work: @escaping () -> Void) {
if immediate {
delay.scheduler.schedule(options: delay.options, work)
return
}
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()
schedule(delay, immediate: true) { [weak self] in
self?.scheduledReceive(subscription: subscription)
}
}
private func scheduledReceive(subscription: Subscription) {
lock.lock()
guard case let .subscribed(_, downstream, _) = state else {
lock.unlock()
return
}
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, immediate: false) { [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<Failure>) {
lock.lock()
guard case let .subscribed(delay, downstream, _) = state else {
lock.unlock()
return
}
state = .terminal
lock.unlock()
schedule(delay, immediate: false) { [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()
}
}
}
@@ -0,0 +1,147 @@
//
// Publishers.Drop.swift
//
//
// Created by Sven Weidauer on 03.10.2019.
//
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
extension Publisher {
/// Omits the specified number of elements before republishing subsequent elements.
///
/// - Parameter count: The number of elements to omit.
/// - Returns: A publisher that does not republish the first `count` elements.
public func dropFirst(_ count: Int = 1) -> Publishers.Drop<Self> {
return .init(upstream: self, count: count)
}
}
extension Publishers {
/// A publisher that omits a specified number of elements before republishing
/// later elements.
public struct Drop<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 number of elements to drop.
public let count: Int
public init(upstream: Upstream, count: Int) {
self.upstream = upstream
self.count = count
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Upstream.Failure == Downstream.Failure,
Upstream.Output == Downstream.Input
{
let inner = Inner(downstream: subscriber, count: count)
upstream.subscribe(inner)
subscriber.receive(subscription: inner)
}
}
}
extension Publishers.Drop: Equatable where Upstream: Equatable {}
extension Publishers.Drop {
private final class Inner<Downstream: Subscriber>
: Subscription,
Subscriber,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Upstream.Output == Downstream.Input,
Upstream.Failure == Downstream.Failure
{
// NOTE: This class has been audited for thread safety.
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private let downstream: Downstream
private let lock = UnfairLock.allocate()
private var subscription: Subscription?
private var pendingDemand = Subscribers.Demand.none
private var count: Int
fileprivate init(downstream: Downstream, count: Int) {
self.downstream = downstream
self.count = count
}
deinit {
lock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard self.subscription == nil else {
lock.unlock()
subscription.cancel()
return
}
self.subscription = subscription
precondition(count >= 0, "count must not be negative")
let demandToRequestFromUpstream = pendingDemand + count
lock.unlock()
if demandToRequestFromUpstream > 0 {
subscription.request(demandToRequestFromUpstream)
}
}
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
// Combine doesn't lock here!
if count > 0 {
count -= 1
return .none
}
return downstream.receive(input)
}
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
// Combine doesn't lock here!
subscription = nil
downstream.receive(completion: completion)
}
func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
lock.lock()
guard let subscription = self.subscription else {
self.pendingDemand += demand
lock.unlock()
return
}
lock.unlock()
subscription.request(demand)
}
func cancel() {
// Combine doesn't lock here!
subscription?.cancel()
subscription = nil
}
var description: String { return "Drop" }
var customMirror: Mirror {
return Mirror(self, children: EmptyCollection())
}
var playgroundDescription: Any { return description }
}
}
@@ -5,6 +5,44 @@
// Created by Sergej Jaskiewicz on 16.06.2019.
//
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
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 publishers
/// 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 publishers
/// 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 +67,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 +93,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 publishers
/// 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 publishers
/// 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" }
}
}
@@ -65,11 +65,9 @@ extension Publishers {
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Failure == Downstream.Failure,
Output == Downstream.Input
where Failure == Downstream.Failure, Output == Downstream.Input
{
let inner = Inner(downstream: subscriber, predicate: { _ in .success(true) })
upstream.receive(subscriber: inner)
upstream.subscribe(Inner(downstream: subscriber))
}
}
@@ -93,11 +91,9 @@ extension Publishers {
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Failure == Downstream.Failure,
Output == Downstream.Input
where Failure == Downstream.Failure, Output == Downstream.Input
{
let inner = Inner(downstream: subscriber, predicate: catching(predicate))
upstream.receive(subscriber: inner)
upstream.subscribe(Inner(downstream: subscriber, predicate: predicate))
}
}
@@ -121,175 +117,94 @@ extension Publishers {
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Failure == Downstream.Failure,
Output == Downstream.Input
where Failure == Downstream.Failure, Output == Downstream.Input
{
let inner = Inner(downstream: subscriber, predicate: catching(predicate))
upstream.receive(subscriber: inner)
upstream.subscribe(Inner(downstream: subscriber, predicate: predicate))
}
}
}
extension Publishers.First: Equatable where Upstream: Equatable {}
private class _FirstWhere<Upstream: Publisher, Downstream: Subscriber>
: OperatorSubscription<Downstream>,
Subscription
where Downstream.Input == Upstream.Output
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
typealias Predicate = (Input) -> Result<Bool, Downstream.Failure>
//
// .pending(input)
//
// receive(input) request(demand)
//
//
//
//
// .waitingForDemand .finished
//
//
//
//
// request(demand) receive(input)
//
// .downstreamHasRequested
//
enum State {
case waitingForDemand
case pending(Input)
case downstreamHasRequested
case finished
}
var predicate: Predicate?
private var _state: State = .waitingForDemand
var isCompleted: Bool {
return predicate == nil
}
init(downstream: Downstream, predicate: @escaping Predicate) {
self.predicate = predicate
super.init(downstream: downstream)
}
func receive(subscription: Subscription) {
upstreamSubscription = subscription
subscription.request(.unlimited)
downstream.receive(subscription: self)
}
func receive(_ input: Input) -> Subscribers.Demand {
switch _state {
case .pending, .finished:
break
case .downstreamHasRequested:
_ifSatisfiesPredicate(input) {
_state = .finished
_sendDownstream(input)
}
case .waitingForDemand:
_ifSatisfiesPredicate(input) {
_state = .pending(input)
}
}
return .none
}
private func _ifSatisfiesPredicate(_ input: Input, _ onSuccess: () -> Void) {
guard let predicate = self.predicate else { return }
switch predicate(input) {
case .success(true):
onSuccess()
case .success(false):
return
case .failure(let error):
cancel()
downstream.receive(completion: .failure(error))
return
}
}
private func _sendDownstream(_ input: Input) {
_ = downstream.receive(input)
cancel()
downstream.receive(completion: .finished)
}
func request(_ demand: Subscribers.Demand) {
precondition(demand > 0, "demand must not be zero")
switch _state {
case .waitingForDemand:
_state = .downstreamHasRequested
case .pending(let input):
_state = .finished
_sendDownstream(input)
case .finished, .downstreamHasRequested:
break
}
}
override func cancel() {
predicate = nil
upstreamSubscription?.cancel()
upstreamSubscription = nil
}
}
extension Publishers.First {
private final class Inner<Downstream: Subscriber>
: _FirstWhere<Upstream, Downstream>,
Subscriber,
CustomStringConvertible
: ReduceProducer<Downstream,
Upstream.Output,
Upstream.Output,
Upstream.Failure,
Void>
where Upstream.Output == Downstream.Input,
Upstream.Failure == Downstream.Failure
{
var description: String { return "First" }
func receive(completion: Subscribers.Completion<Failure>) {
guard !isCompleted else { return }
predicate = nil
downstream.receive(completion: completion)
fileprivate init(downstream: Downstream) {
super.init(downstream: downstream, initial: nil, reduce: ())
}
override func receive(
newValue: Upstream.Output
) -> PartialCompletion<Void, Downstream.Failure> {
result = newValue
return .finished
}
override var description: String { return "First" }
}
}
extension Publishers.FirstWhere {
private final class Inner<Downstream: Subscriber>
: _FirstWhere<Upstream, Downstream>,
Subscriber,
CustomStringConvertible
: ReduceProducer<Downstream, Output, Output, Failure, (Output) -> Bool>
where Upstream.Output == Downstream.Input,
Upstream.Failure == Downstream.Failure
{
var description: String { return "TryFirst" }
func receive(completion: Subscribers.Completion<Failure>) {
guard !isCompleted else { return }
predicate = nil
downstream.receive(completion: completion)
fileprivate init(downstream: Downstream, predicate: @escaping (Output) -> Bool) {
super.init(downstream: downstream, initial: nil, reduce: predicate)
}
override func receive(
newValue: Output
) -> PartialCompletion<Void, Downstream.Failure> {
if reduce(newValue) {
result = newValue
return .finished
} else {
return .continue
}
}
override var description: String { return "TryFirst" }
}
}
extension Publishers.TryFirstWhere {
private final class Inner<Downstream: Subscriber>
: _FirstWhere<Upstream, Downstream>,
Subscriber,
CustomStringConvertible
where Upstream.Output == Downstream.Input,
Downstream.Failure == Error
: ReduceProducer<Downstream,
Output,
Output,
Upstream.Failure,
(Output) throws -> Bool>
where Upstream.Output == Downstream.Input, Downstream.Failure == Error
{
var description: String { return "TryFirstWhere" }
func receive(completion: Subscribers.Completion<Failure>) {
guard !isCompleted else { return }
predicate = nil
downstream.receive(completion: completion.eraseError())
fileprivate init(downstream: Downstream,
predicate: @escaping (Output) throws -> Bool) {
super.init(downstream: downstream, initial: nil, reduce: predicate)
}
override func receive(
newValue: Output
) -> PartialCompletion<Void, Error> {
do {
if try reduce(newValue) {
result = newValue
return .finished
} else {
return .continue
}
} catch {
return .failure(error)
}
}
override var description: String { return "TryFirstWhere" }
}
}
@@ -4,8 +4,11 @@
// Created by Eric Patey on 16.08.2019.
//
extension Publisher {
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
extension Publisher {
/// Transforms all elements from an upstream publisher into a new or existing
/// publisher.
///
@@ -23,23 +26,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
@@ -54,359 +52,379 @@ 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
private let lock = unfairLock()
/// 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()
// Must be recursive lock. Probably a bug in Combine.
/// All the calls to the downstream subscriber should be made with this lock
/// acquired.
private let downstreamLock = UnfairLock.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
}
final func cancel() {
deinit {
outerLock.deallocate()
lock.deallocate()
downstreamLock.deallocate()
}
let (upstreamToCancel, childrenToCancel) = lock
.do { () -> (Subscription?, Set<ChildSubscriber>) in
let upstreamToCancel = upstreamSubscription
upstreamSubscription = nil
return (upstreamToCancel, lockedDeactivateAndReturnChildrenToCancel())
// MARK: - Subscriber
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,197 @@
//
// Publishers.HandleEvents.swift
//
//
// Created by Sergej Jaskiewicz on 03.12.2019.
//
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
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 }
}
}
@@ -4,6 +4,10 @@
// Created by Eric Patey on 16.08.2019.
//
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
extension Publisher {
/// Ingores all upstream elements, but passes along a completion
@@ -12,7 +16,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 +25,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 +36,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 +91,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 +105,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 }
}
}
@@ -0,0 +1,203 @@
//
// Publishers.Last.swift
//
//
// Created by Joseph Spadafora on 7/9/19.
//
extension Publisher {
/// Only publishes the last element of a stream, after the stream finishes.
/// - Returns: A publisher that only publishes the last element of a stream.
public func last() -> Publishers.Last<Self> {
return .init(upstream: self)
}
/// Only publishes the last element of a stream that satisfies a predicate closure,
/// after the stream finishes.
///
/// - Parameter predicate: A closure that takes an element as its parameter and
/// returns a Boolean value indicating whether to publish the element.
/// - Returns: A publisher that only publishes the last element satisfying
/// the given predicate.
public func last(
where predicate: @escaping (Output) -> Bool
) -> Publishers.LastWhere<Self> {
return .init(upstream: self, predicate: predicate)
}
/// Only publishes the last element of a stream that satisfies an error-throwing
/// predicate closure, after the stream finishes.
///
/// If the predicate 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 to publish the element.
/// - Returns: A publisher that only publishes the last element satisfying
/// the given predicate.
public func tryLast(
where predicate: @escaping (Output) throws -> Bool
) -> Publishers.TryLastWhere<Self> {
return .init(upstream: self, predicate: predicate)
}
}
extension Publishers {
/// A publisher that only publishes the last element of a stream,
/// after the stream finishes.
public struct Last<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
public init(upstream: Upstream) {
self.upstream = upstream
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Failure == Downstream.Failure, Output == Downstream.Input
{
upstream.subscribe(Inner(downstream: subscriber))
}
}
/// A publisher that only publishes the last element of a stream that satisfies
/// a predicate closure, once the stream finishes.
public struct LastWhere<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 to publish an element.
public let predicate: (Upstream.Output) -> Bool
public init(upstream: Upstream, predicate: @escaping (Output) -> Bool) {
self.upstream = upstream
self.predicate = predicate
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Failure == Downstream.Failure, Output == Downstream.Input
{
upstream.subscribe(Inner(downstream: subscriber, predicate: predicate))
}
}
/// A publisher that only publishes the last element of a stream that satisfies
/// an error-throwing predicate closure, once the stream finishes.
public struct TryLastWhere<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 to publish an element.
public let predicate: (Upstream.Output) throws -> Bool
public init(upstream: Upstream, predicate: @escaping (Output) throws -> Bool) {
self.upstream = upstream
self.predicate = predicate
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Failure == Error, Output == Downstream.Input
{
upstream.subscribe(Inner(downstream: subscriber, predicate: predicate))
}
}
}
extension Publishers.Last: Equatable where Upstream: Equatable {}
extension Publishers.Last {
private final class Inner<Downstream: Subscriber>
: ReduceProducer<Downstream,
Upstream.Output,
Upstream.Output,
Upstream.Failure,
Void>
where Upstream.Output == Downstream.Input, Upstream.Failure == Downstream.Failure
{
fileprivate init(downstream: Downstream) {
super.init(downstream: downstream, initial: nil, reduce: ())
}
override func receive(
newValue: Upstream.Output
) -> PartialCompletion<Void, Downstream.Failure> {
result = newValue
return .continue
}
override var description: String { return "Last" }
}
}
extension Publishers.LastWhere {
private final class Inner<Downstream: Subscriber>
: ReduceProducer<Downstream,
Upstream.Output,
Upstream.Output,
Upstream.Failure,
(Upstream.Output) -> Bool>
where Upstream.Output == Downstream.Input, Upstream.Failure == Downstream.Failure
{
fileprivate init(downstream: Downstream,
predicate: @escaping (Upstream.Output) -> Bool) {
super.init(downstream: downstream, initial: nil, reduce: predicate)
}
override func receive(
newValue: Upstream.Output
) -> PartialCompletion<Void, Downstream.Failure> {
if reduce(newValue) {
result = newValue
}
return .continue
}
override var description: String { return "LastWhere" }
}
}
extension Publishers.TryLastWhere {
private final class Inner<Downstream: Subscriber>
: ReduceProducer<Downstream,
Upstream.Output,
Upstream.Output,
Upstream.Failure,
(Upstream.Output) throws -> Bool>
where Upstream.Output == Downstream.Input, Downstream.Failure == Error
{
fileprivate init(downstream: Downstream,
predicate: @escaping (Upstream.Output) throws -> Bool) {
super.init(downstream: downstream, initial: nil, reduce: predicate)
}
override func receive(
newValue: Upstream.Output
) -> PartialCompletion<Void, Downstream.Failure> {
do {
if try reduce(newValue) {
result = newValue
}
return .continue
} catch {
return .failure(error)
}
}
override var description: String { return "TryLastWhere" }
}
}
@@ -5,6 +5,10 @@
// Created by Anton Nazarov on 25.06.2019.
//
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
extension Publisher {
/// Transforms all elements from the upstream publisher with a provided closure.
@@ -30,7 +34,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)
}
@@ -187,7 +191,7 @@ extension Publishers.TryMap {
private var status = SubscriptionStatus.awaitingSubscription
private let lock = unfairLock()
private let lock = UnfairLock.allocate()
let combineIdentifier = CombineIdentifier()
@@ -197,6 +201,10 @@ extension Publishers.TryMap {
self.map = map
}
deinit {
lock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard case .awaitingSubscription = status else {
@@ -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,175 @@
${template_header}
//
// Publishers.MapKeyPath.swift.gyb
//
//
// Created by Sergej Jaskiewicz on 03/10/2019.
//
%{
from gyb_opencombine_support import (
suffix_variadic,
list_with_suffix_variadic
)
instantiations = [(1, '', ''),
(2, 'two', 'second '),
(3, 'three', 'third ')]
def key_path_var(index, arity):
return suffix_variadic('keyPath', index, arity)
def make_publisher_name(arity):
return suffix_variadic('MapKeyPath', arity, arity)
def make_output_types(arity):
return list_with_suffix_variadic('Output', arity)
}%
extension Publisher {
% for arity, cardinal, _ in instantiations:
% result_types = list_with_suffix_variadic('Result', arity)
% cs_result_types = ', '.join(result_types)
%
% method_args = \
% ['_ {}: KeyPath<Output, {}>'.format(key_path_var(i, arity), result_types[i]) \
% for i in range(arity)]
% method_args_joined = ',\n '.join(method_args)
%
% init_args = ['{}: {}'.format(key_path_var(i, arity), key_path_var(i, arity)) \
% for i in range(arity)]
% init_args_joined = ',\n '.join(init_args)
%
% publisher_name = make_publisher_name(arity)
%
% doc_cardinal = 'a keyt path' if arity == 1 else cardinal + ' key paths'
/// Returns a publisher that publishes the values of ${doc_cardinal} as a tuple.
///
/// - Parameters:
% for i in range(arity):
% ordinal = 'another ' if i == 1 else 'a ' + instantiations[i][2]
/// - ${key_path_var(i, arity)}: The key path of ${ordinal}property on `Output`
% end
%
% doc_comment_suffix = 'value of the key path' \
% if arity == 1 else 'values of {} key paths as a tuple'.format(cardinal)
/// - Returns: A publisher that publishes the ${doc_comment_suffix}.
public func map<${cs_result_types}>(
${method_args_joined}
) -> Publishers.${publisher_name}<Self, ${cs_result_types}> {
return .init(
upstream: self,
${init_args_joined}
)
}
% end
}
extension Publishers {
% for arity, cardinal, ordinal in instantiations:
%
% doc_comment_suffix = 'value of a key path' \
% if arity == 1 else 'values of {} key paths as a tuple'.format(cardinal)
%
% output_types = make_output_types(arity)
% cs_output_types = ', '.join(output_types)
%
% publisher_name = make_publisher_name(arity)
/// A publisher that publishes the ${doc_comment_suffix}.
public struct ${publisher_name}<Upstream: Publisher, ${cs_output_types}>: Publisher {
% if arity != 1:
public typealias Output = (${cs_output_types})
% end
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
% for i in range(arity):
% ordinal = instantiations[i][2]
/// The key path of a ${ordinal}property to publish.
public let ${key_path_var(i, arity)}: KeyPath<Upstream.Output, ${output_types[i]}>
% end
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Output == Downstream.Input, Failure == Downstream.Failure
{
upstream.subscribe(Inner(downstream: subscriber, parent: self))
}
}
% end
}
% for arity, _, _ in instantiations:
% output_types = make_output_types(arity)
% cs_output_types = ', '.join(output_types)
%
% publisher_name = make_publisher_name(arity)
extension Publishers.${publisher_name} {
private struct Inner<Downstream: Subscriber>
: Subscriber,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Output, Downstream.Failure == Upstream.Failure
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private let downstream: Downstream
% for i in range(arity):
private let ${key_path_var(i, arity)}: KeyPath<Input, ${output_types[i]}>
% end
let combineIdentifier = CombineIdentifier()
fileprivate init(
downstream: Downstream,
parent: Publishers.${publisher_name}<Upstream, ${cs_output_types}>
) {
self.downstream = downstream
% for i in range(arity):
self.${key_path_var(i, arity)} = parent.${key_path_var(i, arity)}
% end
}
func receive(subscription: Subscription) {
downstream.receive(subscription: subscription)
}
func receive(_ input: Input) -> Subscribers.Demand {
% output_components = \
% ['input[keyPath: {}]'.format(key_path_var(i, arity)) for i in range(arity)]
% output_components_joined = ',\n '.join(output_components)
let output = (
${output_components_joined}
)
return downstream.receive(output)
}
func receive(completion: Subscribers.Completion<Failure>) {
downstream.receive(completion: completion)
}
% inner_description = 'ValueForKey' + ('' if arity == 1 else 's')
var description: String { return "${inner_description}" }
var customMirror: Mirror {
let children: [Mirror.Child] = [
% for i in range(arity):
("${key_path_var(i, arity)}", ${key_path_var(i, arity)}),
% end
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
}
}
% end
@@ -0,0 +1,170 @@
//
// Publishers.MeasureInterval.swift
//
//
// Created by Sergej Jaskiewicz on 03.12.2019.
//
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
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,21 @@
// Created by Sergej Jaskiewicz on 14.06.2019.
//
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
extension Publisher {
/// Applies a closure to create a subject that delivers elements to subscribers.
///
/// Use a multicast publisher when you have multiple downstream subscribers, but you
/// want upstream publishers to only process one `receive(_:)` call per event.
/// In contrast with `multicast(subject:)`, this method produces a publisher that
/// creates a separate Subject for each subscriber.
///
/// - Parameter createSubject: A closure to create a new Subject each time
/// a subscriber attaches to the multicast publisher.
public func multicast<SubjectType: Subject>(
_ createSubject: @escaping () -> SubjectType
) -> Publishers.Multicast<Self, SubjectType>
@@ -15,6 +28,14 @@ extension Publisher {
return Publishers.Multicast(upstream: self, createSubject: createSubject)
}
/// Provides a subject to deliver elements to multiple subscribers.
///
/// Use a multicast publisher when you have multiple downstream subscribers, but you
/// want upstream publishers to only process one `receive(_:)` call per event.
/// In contrast with `multicast(_:)`, this method produces a publisher shares
/// the provided Subject among all the downstream subscribers.
///
/// - Parameter subject: A subject to deliver elements to downstream subscribers.
public func multicast<SubjectType: Subject>(
subject: SubjectType
) -> Publishers.Multicast<Self, SubjectType>
@@ -26,6 +47,7 @@ extension Publisher {
extension Publishers {
/// A publisher that uses a subject to deliver elements to multiple subscribers.
public final class Multicast<Upstream: Publisher, SubjectType: Subject>
: ConnectablePublisher
where Upstream.Failure == SubjectType.Failure,
@@ -37,11 +59,14 @@ extension Publishers {
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// A closure to create a new Subject each time a subscriber attaches
/// to the multicast publisher.
public let createSubject: () -> SubjectType
private let lock = unfairLock()
private let lock = UnfairLock.allocate()
private var subject: SubjectType?
@@ -58,11 +83,22 @@ extension Publishers {
return subject
}
/// Creates a multicast publisher that applies a closure to create a subject
/// that delivers elements to subscribers.
///
/// - Parameter upstream: The publisher from which this publisher receives
/// elements.
/// - Parameter createSubject: A closure to create a new Subject each time
/// a subscriber attaches to the multicast publisher.
public init(upstream: Upstream, createSubject: @escaping () -> SubjectType) {
self.upstream = upstream
self.createSubject = createSubject
}
deinit {
lock.deallocate()
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where SubjectType.Failure == Downstream.Failure,
SubjectType.Output == Downstream.Input
@@ -100,7 +136,7 @@ extension Publishers.Multicast {
case terminal
}
private let lock = unfairLock()
private let lock = UnfairLock.allocate()
private var state: State
@@ -109,6 +145,10 @@ extension Publishers.Multicast {
state = .ready(upstream: parent.upstream, downstream: downstream)
}
deinit {
lock.deallocate()
}
fileprivate var description: String { return "Multicast" }
fileprivate var customMirror: Mirror {
@@ -121,6 +161,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,209 @@
//
// Publishers.Output.swift
//
//
// Created by Sergej Jaskiewicz on 24.10.2019.
//
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
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 doesnt 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 doesnt 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,6 +5,23 @@
// Created by Sergej Jaskiewicz on 16.06.2019.
//
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
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 {
/// A publisher that prints log messages for all publishing events, optionally
@@ -52,24 +69,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
@@ -87,8 +92,8 @@ extension Publishers.Print {
private var downstream: Downstream
private let prefix: String
private var stream: PrintTarget?
private var subscription: Subscription?
private let lock = unfairLock()
private var status = SubscriptionStatus.awaitingSubscription
private let lock = UnfairLock.allocate()
init(downstream: Downstream, prefix: String, stream: TextOutputStream?) {
self.downstream = downstream
@@ -96,11 +101,20 @@ extension Publishers.Print {
self.stream = stream.map(PrintTarget.init)
}
deinit {
lock.deallocate()
}
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)
}
@@ -124,6 +138,9 @@ extension Publishers.Print {
case .failure(let error):
log("\(prefix)receive error: (\(error))")
}
lock.lock()
status = .terminal
lock.unlock()
downstream.receive(completion: completion)
}
@@ -133,24 +150,40 @@ 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)
} else {
Swift.print("", text)
Swift.print(text)
}
}
}
@@ -0,0 +1,217 @@
//
// Publishers.ReceiveOn.swift
//
//
// Created by Sergej Jaskiewicz on 02.12.2019.
//
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
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 = UnfairLock.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()
receiveOn.scheduler.schedule(options: receiveOn.options) { [weak self] in
self?.scheduledReceive(subscription: subscription)
}
}
private func scheduledReceive(subscription: Subscription) {
lock.lock()
guard case let .subscribed(_, downstream, _) = state else {
lock.unlock()
return
}
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,168 @@
//
// Publishers.Reduce.swift
//
//
// Created by Sergej Jaskiewicz on 09.10.2019.
//
extension Publisher {
/// Applies a closure that accumulates each element of a stream and publishes
/// a final result upon completion.
///
/// - Parameters:
/// - initialResult: The value the closure receives the first time it is called.
/// - nextPartialResult: A closure that takes the previously-accumulated value and
/// the next element from the upstream publisher to produce a new value.
/// - Returns: A publisher that applies the closure to all received elements and
/// produces an accumulated value when the upstream publisher finishes.
public func reduce<Accumulator>(
_ initialResult: Accumulator,
_ nextPartialResult: @escaping (Accumulator, Output) -> Accumulator
) -> Publishers.Reduce<Self, Accumulator> {
return .init(upstream: self,
initial: initialResult,
nextPartialResult: nextPartialResult)
}
/// Applies an error-throwing closure that accumulates each element of a stream and
/// publishes a final result upon completion.
///
/// If the closure throws an error, the publisher fails, passing the error
/// to its subscriber.
///
/// - Parameters:
/// - initialResult: The value the closure receives the first time it is called.
/// - nextPartialResult: An error-throwing closure that takes
/// the previously-accumulated value and the next element from the upstream
/// publisher to produce a new value.
/// - Returns: A publisher that applies the closure to all received elements and
/// produces an accumulated value when the upstream publisher finishes.
public func tryReduce<Accumulator>(
_ initialResult: Accumulator,
_ nextPartialResult: @escaping (Accumulator, Output) throws -> Accumulator
) -> Publishers.TryReduce<Self, Accumulator> {
return .init(upstream: self,
initial: initialResult,
nextPartialResult: nextPartialResult)
}
}
extension Publishers {
/// A publisher that applies a closure to all received elements and produces
/// an accumulated value when the upstream publisher finishes.
public struct Reduce<Upstream: Publisher, Output>: Publisher {
public typealias Failure = Upstream.Failure
public let upstream: Upstream
/// The initial value provided on the first invocation of the closure.
public let initial: Output
/// A closure that takes the previously-accumulated value and the next element
/// from the upstream publisher to produce a new value.
public let nextPartialResult: (Output, Upstream.Output) -> Output
public init(upstream: Upstream,
initial: Output,
nextPartialResult: @escaping (Output, Upstream.Output) -> Output) {
self.upstream = upstream
self.initial = initial
self.nextPartialResult = nextPartialResult
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Output == Downstream.Input, Upstream.Failure == Downstream.Failure
{
let inner = Inner(downstream: subscriber,
initial: initial,
reduce: nextPartialResult)
upstream.subscribe(inner)
}
}
/// A publisher that applies an error-throwing closure to all received elements and
/// produces an accumulated value when the upstream publisher finishes.
public struct TryReduce<Upstream: Publisher, Output>: Publisher {
public typealias Failure = Error
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// The initial value provided on the first invocation of the closure.
public let initial: Output
/// An error-throwing closure that takes the previously-accumulated value and
/// the next element from the upstream to produce a new value.
///
/// If this closure throws an error, the publisher fails and passes the error
/// to its subscriber.
public let nextPartialResult: (Output, Upstream.Output) throws -> Output
public init(
upstream: Upstream,
initial: Output,
nextPartialResult: @escaping (Output, Upstream.Output) throws -> Output
) {
self.upstream = upstream
self.initial = initial
self.nextPartialResult = nextPartialResult
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Output == Downstream.Input, Downstream.Failure == Error
{
let inner = Inner(downstream: subscriber,
initial: initial,
reduce: nextPartialResult)
upstream.subscribe(inner)
}
}
}
extension Publishers.Reduce {
private final class Inner<Downstream: Subscriber>
: ReduceProducer<Downstream,
Upstream.Output,
Output,
Upstream.Failure,
(Output, Upstream.Output) -> Output>
where Downstream.Input == Output, Upstream.Failure == Downstream.Failure
{
override func receive(
newValue: Upstream.Output
) -> PartialCompletion<Void, Downstream.Failure> {
result = reduce(result!, newValue)
return .continue
}
override var description: String { return "Reduce" }
}
}
extension Publishers.TryReduce {
private final class Inner<Downstream: Subscriber>
: ReduceProducer<Downstream,
Upstream.Output,
Output,
Upstream.Failure,
(Output, Upstream.Output) throws -> Output>
where Downstream.Input == Output, Downstream.Failure == Error
{
override func receive(
newValue: Upstream.Output
) -> PartialCompletion<Void, Downstream.Failure> {
do {
result = try reduce(result!, newValue)
return .continue
} catch {
return .failure(error)
}
}
override var description: String { return "TryReduce" }
}
}
@@ -0,0 +1,196 @@
//
// Publishers.RemoveDuplicates.swift
//
//
// Created by Sergej Jaskiewicz on 24.10.2019.
//
extension Publisher where Output: Equatable {
/// Publishes only elements that dont 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 dont 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 dont 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 dont 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 dont 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 dont 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 dont 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)
}
}
}
@@ -5,6 +5,10 @@
// Created by Bogdan Vlad on 8/29/19.
//
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
extension Publisher {
/// Replaces any errors in the stream with the provided element.
///
@@ -52,7 +56,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)
}
}
}
@@ -81,13 +87,17 @@ extension Publishers.ReplaceError {
private var status = SubscriptionStatus.awaitingSubscription
private var terminated = false
private var pendingDemand = Subscribers.Demand.none
private var lock = unfairLock()
private var lock = UnfairLock.allocate()
fileprivate init(downstream: Downstream, output: Upstream.Output) {
self.downstream = downstream
self.output = output
}
deinit {
lock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard case .awaitingSubscription = status else {
@@ -96,8 +106,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 {
@@ -0,0 +1,300 @@
//
// Publishers.Scan.swift
//
// Created by Eric Patey on 26.08.2019.
//
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
extension Publisher {
/// Transforms elements from the upstream publisher by providing the current element
/// to a closure along with the last value returned by the closure.
///
/// let pub = (0...5)
/// .publisher
/// .scan(0, { return $0 + $1 })
/// .sink(receiveValue: { print ("\($0)", terminator: " ") })
/// // Prints "0 1 3 6 10 15 ".
///
///
/// - Parameters:
/// - initialResult: The previous result returned by the `nextPartialResult`
/// closure.
/// - nextPartialResult: A closure that takes as its arguments the previous value
/// returned by the closure and the next element emitted from the upstream
/// publisher.
/// - Returns: A publisher that transforms elements by applying a closure that
/// receives its previous return value and the next element from the upstream
/// publisher.
public func scan<Result>(
_ initialResult: Result,
_ nextPartialResult: @escaping (Result, Output) -> Result
) -> Publishers.Scan<Self, Result> {
return .init(upstream: self,
initialResult: initialResult,
nextPartialResult: nextPartialResult)
}
/// Transforms elements from the upstream publisher by providing the current element
/// to an error-throwing closure along with the last value returned by the closure.
///
/// If the closure throws an error, the publisher fails with the error.
/// - Parameters:
/// - initialResult: The previous result returned by the `nextPartialResult`
/// closure.
/// - nextPartialResult: An error-throwing closure that takes as its arguments the
/// previous value returned by the closure and the next element emitted from the
/// upstream publisher.
/// - Returns: A publisher that transforms elements by applying a closure that
/// receives its previous return value and the next element from the upstream
/// publisher.
public func tryScan<Result>(
_ initialResult: Result,
_ nextPartialResult: @escaping (Result, Output) throws -> Result
) -> Publishers.TryScan<Self, Result> {
return .init(upstream: self,
initialResult: initialResult,
nextPartialResult: nextPartialResult)
}
}
extension Publishers {
public struct Scan<Upstream: Publisher, Output>: Publisher {
public typealias Failure = Upstream.Failure
public let upstream: Upstream
public let initialResult: Output
public let nextPartialResult: (Output, Upstream.Output) -> Output
public init(upstream: Upstream,
initialResult: Output,
nextPartialResult: @escaping (Output, Upstream.Output) -> Output) {
self.upstream = upstream
self.initialResult = initialResult
self.nextPartialResult = nextPartialResult
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Output == Downstream.Input, Upstream.Failure == Downstream.Failure
{
upstream.subscribe(Inner(downstream: subscriber,
initialResult: initialResult,
nextPartialResult: nextPartialResult))
}
}
public struct TryScan<Upstream: Publisher, Output>: Publisher {
public typealias Failure = Error
public let upstream: Upstream
public let initialResult: Output
public let nextPartialResult: (Output, Upstream.Output) throws -> Output
public init(
upstream: Upstream,
initialResult: Output,
nextPartialResult: @escaping (Output, Upstream.Output) throws -> Output
) {
self.upstream = upstream
self.initialResult = initialResult
self.nextPartialResult = nextPartialResult
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Output == Downstream.Input, Downstream.Failure == Error
{
upstream.subscribe(Inner(downstream: subscriber,
initialResult: initialResult,
nextPartialResult: nextPartialResult))
}
}
}
extension Publishers.Scan {
private final class Inner<Downstream: Subscriber>
: Subscriber,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Upstream.Failure == Downstream.Failure
{
// 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 nextPartialResult: (Downstream.Input, Input) -> Downstream.Input
private var result: Downstream.Input
fileprivate init(
downstream: Downstream,
initialResult: Downstream.Input,
nextPartialResult: @escaping (Downstream.Input, Input) -> Downstream.Input
)
{
self.downstream = downstream
self.result = initialResult
self.nextPartialResult = nextPartialResult
}
func receive(subscription: Subscription) {
downstream.receive(subscription: subscription)
}
func receive(_ input: Input) -> Subscribers.Demand {
result = nextPartialResult(result, input)
return downstream.receive(result)
}
func receive(completion: Subscribers.Completion<Failure>) {
downstream.receive(completion: completion)
}
var description: String { return "Scan" }
var customMirror: Mirror {
let children: [Mirror.Child] = [
("downstream", downstream),
("result", result)
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
}
}
extension Publishers.TryScan {
private final class Inner<Downstream: Subscriber>
: Subscriber,
Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Failure == Error
{
// NOTE: this class has been audited for thread safety.
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private let downstream: Downstream
private let nextPartialResult:
(Downstream.Input, Input) throws -> Downstream.Input
private var result: Downstream.Input
private var status = SubscriptionStatus.awaitingSubscription
private let lock = UnfairLock.allocate()
private var finished = false
fileprivate init(
downstream: Downstream,
initialResult: Downstream.Input,
nextPartialResult:
@escaping (Downstream.Input, Input) throws -> Downstream.Input
) {
self.downstream = downstream
self.nextPartialResult = nextPartialResult
self.result = initialResult
}
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 {
do {
result = try nextPartialResult(result, input)
return downstream.receive(result)
} catch {
lock.lock()
guard case let .subscribed(subscription) = status else {
lock.unlock()
return .none
}
status = .terminal
lock.unlock()
subscription.cancel()
downstream.receive(completion: .failure(error))
return .none
}
}
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
// Combine doesn't use locking in this method!
guard case .subscribed = status else {
return
}
downstream.receive(completion: completion.eraseError())
}
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 "TryScan" }
var customMirror: Mirror {
lock.lock()
defer { lock.unlock() }
let children: [Mirror.Child] = [
("downstream", downstream),
("status", status),
("result", result)
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
}
}
@@ -5,6 +5,10 @@
// Created by Sergej Jaskiewicz on 19.06.2019.
//
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
extension Publishers {
/// A publisher that publishes a given sequence of elements.
@@ -62,7 +66,7 @@ extension Publishers.Sequence {
private var next: Element?
private var pendingDemand = Subscribers.Demand.none
private var recursion = false
private var lock = unfairLock()
private var lock = UnfairLock.allocate()
fileprivate init(downstream: Downstream, sequence: Elements) {
self.sequence = sequence
@@ -71,6 +75,10 @@ extension Publishers.Sequence {
next = iterator.next()
}
deinit {
lock.deallocate()
}
var description: String {
return sequence.map(String.init(describing:)) ?? "Sequence"
}
@@ -101,7 +109,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,192 @@
//
// Publishers.SubscribeOn.swift
//
//
// Created by Sergej Jaskiewicz on 02.12.2019.
//
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
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 }
}
}
+230
View File
@@ -0,0 +1,230 @@
//
// Record.swift
//
//
// Created by Sergej Jaskiewicz on 12.11.2019.
//
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
/// 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()
}
}
}
-16
View File
@@ -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) } }
}
-15
View File
@@ -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.
///
@@ -30,12 +30,18 @@ extension Subscribers {
}
/// Requests as many values as the `Publisher` can produce.
public static let unlimited = Demand(rawValue: .max)
@inline(__always)
@inlinable
public static var unlimited: Demand {
return Demand(rawValue: .max)
}
/// A demand for no items.
///
/// This is equivalent to `Demand.max(0)`.
public static let none = Demand.max(0)
@inline(__always)
@inlinable
public static var none: Demand { return .max(0) }
/// Limits the maximum number of values.
/// The `Publisher` may send fewer than the requested number.
+17 -10
View File
@@ -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 }
}
}
@@ -51,7 +51,7 @@ extension DispatchQueue {
/// - Returns: The time interval between this time and the provided time.
public func distance(to other: SchedulerTimeType) -> Stride {
return .nanoseconds(
Int(other.dispatchTime.rawValue - dispatchTime.rawValue)
dispatchTime.rawValue.distance(to: other.dispatchTime.rawValue)
)
}
@@ -163,36 +163,29 @@ 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))
}
@@ -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)
}
}
@@ -25,13 +25,7 @@ final class DispatchQueueSchedulerTests: XCTestCase {
let time2 = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10431))
XCTAssertEqual(time1.distance(to: time2), .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(time2.distance(to: time1), .nanoseconds(-431))
}
func testSchedulerTimeTypeAdvanced() {
@@ -58,10 +52,7 @@ final class DispatchQueueSchedulerTests: XCTestCase {
XCTAssertEqual(time1, time2)
XCTAssertEqual(time2, time1)
XCTAssertNotEqual(time1, time3)
assertCrashes {
XCTAssertNotEqual(time3, time1)
}
XCTAssertNotEqual(time3, time1)
}
func testSchedulerTimeTypeHashable() {
@@ -95,14 +86,14 @@ final class DispatchQueueSchedulerTests: XCTestCase {
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) {
case (.nanoseconds(2_000_000_000),
.nanoseconds(2_000_000),
.nanoseconds(2_000),
.nanoseconds(2)):
break // pass
case let intervals:
XCTFail("Unexpected DispatchTimeInterval: \(intervals)")
@@ -112,24 +103,24 @@ final class DispatchQueueSchedulerTests: XCTestCase {
func testStrideFromDispatchTimeInterval() {
typealias Stride = Scheduler.SchedulerTimeType.Stride
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(.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(.never).magnitude, .max)
}
func testStrideFromNumericValue() {
typealias Stride = Scheduler.SchedulerTimeType.Stride
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)
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((12.756 as Stride).magnitude, 12756000000)
XCTAssertEqual((34 as Stride).magnitude, 34000000000)
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)
@@ -140,7 +131,7 @@ final class DispatchQueueSchedulerTests: XCTestCase {
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() {
@@ -148,18 +139,12 @@ final class DispatchQueueSchedulerTests: XCTestCase {
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,143 +161,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)
}
}
@@ -388,7 +361,7 @@ final class DispatchQueueSchedulerTests: XCTestCase {
XCTAssertFalse(didExecuteMainAction, "action should be executed asynchronously")
// Wait for the background scheduler to execute the work.
XCTAssertEqual(group.wait(timeout: .now() + 0.1), .success)
XCTAssertEqual(group.wait(timeout: .now() + 5.0), .success)
XCTAssertFalse(didExecuteMainAction, "action should be executed asynchronously")
XCTAssertTrue(didExecuteBackgroundAction.value)
@@ -0,0 +1,40 @@
//
// CleaningUpSubscriber.swift
//
//
// Created by Sergej Jaskiewicz on 17.10.2019.
//
#if OPENCOMBINE_COMPATIBILITY_TEST
import Combine
#else
import OpenCombine
#endif
@available(macOS 10.15, iOS 13.0, *)
final class CleaningUpSubscriber<Input, Failure: Error>: Subscriber {
private(set) var subscription: Subscription?
private let onDeinit: () -> Void
init(onDeinit: @escaping () -> Void) {
self.onDeinit = onDeinit
}
deinit {
onDeinit()
}
func receive(subscription: Subscription) {
self.subscription = subscription
}
func receive(_ input: Input) -> Subscribers.Demand {
return .none
}
func receive(completion: Subscribers.Completion<Failure>) {
subscription = nil
}
}
@@ -0,0 +1,223 @@
//
// 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")
}
// swiftlint:enable generic_type_name
@@ -34,12 +34,16 @@ 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)?
required init(subscription: Subscription?) {
self.subscription = subscription
}
@@ -47,9 +51,16 @@ class CustomPublisherBase<Output: Equatable, Failure: Error>: Publisher {
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 +68,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,11 +17,11 @@ 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>
where Sut.Output: Equatable,
SourcePublisher: CustomPublisherBase<SourceValue, TestingError>
where SourcePublisher: CustomPublisherBase<SourceValue, SourceError>
{
typealias Value = Sut.Output
typealias Failure = Sut.Failure
@@ -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
}
}
@@ -0,0 +1,153 @@
//
// TestLifecycle.swift
//
//
// Created by Sergej Jaskiewicz on 08.10.2019.
//
import XCTest
#if OPENCOMBINE_COMPATIBILITY_TEST
import Combine
#else
import OpenCombine
#endif
@available(macOS 10.15, iOS 13.0, *)
func testLifecycle<UpstreamOutput, Operator: Publisher>(
file: StaticString = #file,
line: UInt = #line,
sendValue valueToBeSent: UpstreamOutput,
cancellingSubscriptionReleasesSubscriber: Bool,
finishingIsPassedThrough: Bool = true,
_ makeOperator: (PassthroughSubject<UpstreamOutput, TestingError>) -> Operator
) throws {
var deinitCounter = 0
let onDeinit = { deinitCounter += 1 }
// Lifecycle test #1
do {
let passthrough = PassthroughSubject<UpstreamOutput, TestingError>()
let operatorPublisher = makeOperator(passthrough)
let emptySubscriber =
TrackingSubscriberBase<Operator.Output, Operator.Failure>(onDeinit: onDeinit)
XCTAssertTrue(emptySubscriber.history.isEmpty,
"Lifecycle test #1: thesubscriber's history should be empty",
file: file,
line: line)
operatorPublisher.subscribe(emptySubscriber)
passthrough.send(valueToBeSent)
passthrough.send(completion: .failure("failure"))
}
if cancellingSubscriptionReleasesSubscriber {
XCTAssertEqual(deinitCounter,
1,
"""
Lifecycle test #1: deinit should be called, because \
the subscription has completed
""",
file: file,
line: line)
} else {
XCTAssertEqual(deinitCounter,
0,
"""
Lifecycle test #1: deinit should not be called
""",
file: file,
line: line)
}
// Lifecycle test #2
do {
let passthrough = PassthroughSubject<UpstreamOutput, TestingError>()
let operatorPublisher = makeOperator(passthrough)
let emptySubscriber =
TrackingSubscriberBase<Operator.Output, Operator.Failure>(onDeinit: onDeinit)
operatorPublisher.subscribe(emptySubscriber)
}
XCTAssertEqual(deinitCounter,
cancellingSubscriptionReleasesSubscriber ? 1 : 0,
"""
Lifecycle test #2: deinit should not be called, \
because the subscription is never cancelled
""",
file: file,
line: line)
// Lifecycle test #3
var subscription: Subscription?
do {
let passthrough = PassthroughSubject<UpstreamOutput, TestingError>()
let operatorPublisher = makeOperator(passthrough)
let emptySubscriber = TrackingSubscriberBase<Operator.Output, Operator.Failure>(
receiveSubscription: { subscription = $0; $0.request(.unlimited) },
onDeinit: onDeinit
)
operatorPublisher.subscribe(emptySubscriber)
passthrough.send(valueToBeSent)
}
XCTAssertEqual(deinitCounter,
cancellingSubscriptionReleasesSubscriber ? 1 : 0,
"""
Lifecycle test #3: deinit should not be called, \
because the subscription is not cancelled yet
""",
file: file,
line: line)
try XCTUnwrap(subscription,
"Lifecycle test #3: subscription should be saved",
file: file,
line: line)
.cancel()
if cancellingSubscriptionReleasesSubscriber {
XCTAssertEqual(deinitCounter,
2,
"""
Lifecycle test #3: deinit should be called, because
the subscription has been cancelled
""",
file: file,
line: line)
} else {
XCTAssertEqual(deinitCounter,
0,
"Lifecycle test #3: deinit should not be called",
file: file,
line: line)
}
// Lifecycle test #4
var subscriberDestroyed = false
do {
let passthrough = PassthroughSubject<UpstreamOutput, TestingError>()
let operatorPublisher = makeOperator(passthrough)
let emptySubscriber = CleaningUpSubscriber<Operator.Output, Operator.Failure> {
subscriberDestroyed = true
}
operatorPublisher.subscribe(emptySubscriber)
passthrough.send(completion: .finished)
}
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)
}
}
@@ -13,9 +13,7 @@ import Combine
import OpenCombine
#endif
func childrenIsEmpty(_ mirror: Mirror) -> Bool {
return mirror.children.isEmpty
}
let childrenIsEmpty: (Mirror) -> Bool = { $0.children.isEmpty }
enum ExpectedMirrorChildValue: Equatable, ExpressibleByStringLiteral {
case anything
@@ -29,6 +27,7 @@ enum ExpectedMirrorChildValue: Equatable, ExpressibleByStringLiteral {
}
}
@discardableResult
func expectedChildren(_ expectedChildren: (String?, ExpectedMirrorChildValue)...,
file: StaticString = #file,
line: UInt = #line) -> (Mirror) -> Bool {
@@ -38,6 +37,12 @@ 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) {
@@ -56,17 +61,29 @@ func expectedChildren(_ expectedChildren: (String?, ExpectedMirrorChildValue)...
}
}
func reduceLikeOperatorMirror(file: StaticString = #file,
line: UInt = #line) -> (Mirror) -> Bool {
return expectedChildren(
("downstream", .contains("TrackingSubscriberBase")),
("result", .anything),
("initial", .anything),
("status", .contains("awaitingSubscription")),
file: file,
line: line
)
}
@available(macOS 10.15, iOS 13.0, *)
internal func testReflection<Output, Failure: Error, Operator: Publisher>(
file: StaticString = #file,
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 where Operator.Output: Equatable {
) throws {
let publisher = CustomConnectablePublisherBase<Output, Failure>(subscription: nil)
let operatorPublisher = makeOperator(publisher)
let tracking = TrackingSubscriberBase<Operator.Output, Operator.Failure>()
@@ -80,17 +97,25 @@ 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,
"subscriber shouldn't conform to CustomDebugStringConvertible",
file: file,
line: line)
XCTAssertEqual(
((erasedSubscriber as? CustomPlaygroundDisplayConvertible)?
.playgroundDescription as? String),
@@ -135,6 +160,11 @@ internal func testSubscriptionReflection<Sut: Publisher>(
line: line)
}
XCTAssertFalse(subscription is CustomDebugStringConvertible,
"subscriber shouldn't conform to CustomDebugStringConvertible",
file: file,
line: line)
XCTAssertEqual(
((subscription as? CustomPlaygroundDisplayConvertible)?
.playgroundDescription as? 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,8 @@
// Created by Sergej Jaskiewicz on 11.06.2019.
//
import XCTest
#if OPENCOMBINE_COMPATIBILITY_TEST
import Combine
#else
@@ -37,36 +39,17 @@ typealias TrackingSubscriber = TrackingSubscriberBase<Int, TestingError>
/// is considered equal to any other subscription no matter what the subscription object
/// actually is.
@available(macOS 10.15, iOS 13.0, *)
final class TrackingSubscriberBase<Value: Equatable, Failure: Error>
final class TrackingSubscriberBase<Value, Failure: Error>
: Subscriber,
Cancellable,
CustomStringConvertible
{
enum Event: Equatable, CustomStringConvertible {
enum Event: CustomStringConvertible {
case subscription(StringSubscription)
case value(Value)
case completion(Subscribers.Completion<Failure>)
static func == (lhs: Event, rhs: Event) -> Bool {
switch (lhs, rhs) {
case let (.subscription(lhs), .subscription(rhs)):
return lhs == rhs
case let (.value(lhs), .value(rhs)):
return lhs == rhs
case let (.completion(lhs), .completion(rhs)):
switch (lhs, rhs) {
case (.finished, .finished):
return true
case let (.failure(lhs), .failure(rhs)):
return (lhs as? TestingError) == (rhs as? TestingError)
default:
return false
}
default:
return false
}
}
var description: String {
switch self {
case .subscription(let subscription):
@@ -175,12 +158,99 @@ final class TrackingSubscriberBase<Value: Equatable, Failure: Error>
return "\(type(of: self)): \(history)"
}
func assertHistoryEqual(_ expected: [Event],
valueComparator: (Value, Value) -> Bool,
file: StaticString = #file,
line: UInt = #line) {
let equals = history.count == expected.count &&
zip(history, expected)
.allSatisfy { $0.isEqual(to: $1, valueComparator: valueComparator) }
XCTAssert(equals,
"\(history) is not equal to \(expected)",
file: file,
line: line)
}
func cancel() {
for subscription in subscriptions {
subscription.cancel()
}
history = []
}
deinit {
onDeinit?()
_onDeinit?()
}
}
@available(macOS 10.15, iOS 13.0, *)
extension TrackingSubscriberBase where Value: Equatable {
func assertHistoryEqual(_ expected: [Event],
file: StaticString = #file,
line: UInt = #line) {
assertHistoryEqual(expected, valueComparator: ==, file: file, line: line)
}
}
@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,
valueComparator: (Value, Value) -> Bool) -> Bool {
switch (self, other) {
case let (.subscription(lhs), .subscription(rhs)):
return lhs == rhs
case let (.value(lhs), .value(rhs)):
return valueComparator(lhs, rhs)
case let (.completion(lhs), .completion(rhs)):
switch (lhs, rhs) {
case (.finished, .finished):
return true
case let (.failure(lhs), .failure(rhs)):
return (lhs as? TestingError) == (rhs as? TestingError)
default:
return false
}
default:
return false
}
}
}
@available(macOS 10.15, iOS 13.0, *)
extension TrackingSubscriberBase.Event: Equatable where Value: Equatable {
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>
@@ -235,10 +305,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
@@ -266,7 +336,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()
}
}
}
+16 -3
View File
@@ -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
}
}
@@ -0,0 +1,76 @@
//
// ObservableObjectPublisherTests.swift
//
//
// Created by Sergej Jaskiewicz on 26.11.2019.
//
import XCTest
#if OPENCOMBINE_COMPATIBILITY_TEST
import Combine
#else
import OpenCombine
#endif
@available(macOS 10.15, iOS 13.0, *)
final class ObservableObjectPublisherTests: XCTestCase {
func testBasicBehavior() {
let publisher = ObservableObjectPublisher()
var downstreamSubscription1: Subscription?
let tracking1 = TrackingSubscriberBase<Void, Never>(
receiveSubscription: { downstreamSubscription1 = $0 }
)
publisher.subscribe(tracking1)
tracking1.assertHistoryEqual([.subscription("PassthroughSubject")])
downstreamSubscription1?.request(.max(1))
tracking1.assertHistoryEqual([.subscription("PassthroughSubject")])
publisher.send()
tracking1.assertHistoryEqual([.subscription("PassthroughSubject"),
.signal])
publisher.send()
publisher.send()
downstreamSubscription1?.request(.max(3))
tracking1.assertHistoryEqual([.subscription("PassthroughSubject"),
.signal])
publisher.send()
publisher.send()
publisher.send()
publisher.send()
tracking1.assertHistoryEqual([.subscription("PassthroughSubject"),
.signal,
.signal,
.signal,
.signal])
downstreamSubscription1?.request(.unlimited)
let tracking2 = TrackingSubscriberBase<Void, Never>(
receiveSubscription: { $0.request(.unlimited) }
)
publisher.subscribe(tracking2)
tracking2.assertHistoryEqual([.subscription("PassthroughSubject")])
publisher.send()
tracking1.assertHistoryEqual([.subscription("PassthroughSubject"),
.signal,
.signal,
.signal,
.signal,
.signal])
tracking2.assertHistoryEqual([.subscription("PassthroughSubject"),
.signal])
downstreamSubscription1?.cancel()
publisher.send()
tracking1.assertHistoryEqual([.subscription("PassthroughSubject"),
.signal,
.signal,
.signal,
.signal,
.signal])
tracking2.assertHistoryEqual([.subscription("PassthroughSubject"),
.signal,
.signal])
}
}
+111
View File
@@ -0,0 +1,111 @@
//
// PublishedTests.swift
//
//
// Created by Sergej Jaskiewicz on 08/09/2019.
//
import XCTest
#if swift(>=5.1)
#if OPENCOMBINE_COMPATIBILITY_TEST
import Combine
@available(macOS 10.15, iOS 13.0, *)
private typealias Published = Combine.Published
@available(macOS 10.15, iOS 13.0, *)
private typealias ObservableObject = Combine.ObservableObject
#else
import OpenCombine
private typealias Published = OpenCombine.Published
private typealias ObservableObject = OpenCombine.ObservableObject
#endif
@available(macOS 10.15, iOS 13.0, *)
final class PublishedTests: XCTestCase {
func testBasicBehavior() {
let testObject = TestObject()
var downstreamSubscription1: Subscription?
let tracking1 = TrackingSubscriberBase<Int, Never>(
receiveSubscription: { downstreamSubscription1 = $0 }
)
testObject.$state.subscribe(tracking1)
XCTAssertEqual(tracking1.history, [.subscription("CurrentValueSubject")])
downstreamSubscription1?.request(.max(2))
XCTAssertEqual(tracking1.history, [.subscription("CurrentValueSubject"),
.value(0)])
testObject.state += 1
testObject.state += 2
testObject.state += 3
XCTAssertEqual(tracking1.history, [.subscription("CurrentValueSubject"),
.value(0),
.value(1)])
downstreamSubscription1?.request(.max(10))
XCTAssertEqual(tracking1.history, [.subscription("CurrentValueSubject"),
.value(0),
.value(1),
.value(6)])
let tracking2 = TrackingSubscriberBase<Int, Never>(
receiveSubscription: { $0.request(.unlimited) }
)
testObject.$state.subscribe(tracking2)
XCTAssertEqual(tracking2.history, [.subscription("CurrentValueSubject"),
.value(6)])
testObject.state = 42
XCTAssertEqual(tracking1.history, [.subscription("CurrentValueSubject"),
.value(0),
.value(1),
.value(6),
.value(42)])
XCTAssertEqual(tracking2.history, [.subscription("CurrentValueSubject"),
.value(6),
.value(42)])
downstreamSubscription1?.cancel()
testObject.state = -1
XCTAssertEqual(tracking1.history, [.subscription("CurrentValueSubject"),
.value(0),
.value(1),
.value(6),
.value(42)])
XCTAssertEqual(tracking2.history, [.subscription("CurrentValueSubject"),
.value(6),
.value(42),
.value(-1)])
}
func testObservableObjectWithCustomObjectWillChange() {
let testObject = TestObject()
var downstreamSubscription: Subscription?
let tracking1 = TrackingSubscriberBase<Void, Never>(
receiveSubscription: { downstreamSubscription = $0 }
)
testObject.objectWillChange.subscribe(tracking1)
tracking1.assertHistoryEqual([.subscription("PassthroughSubject")])
downstreamSubscription?.request(.max(2))
tracking1.assertHistoryEqual([.subscription("PassthroughSubject")])
testObject.state = 100
tracking1.assertHistoryEqual([.subscription("PassthroughSubject")])
}
}
@available(macOS 10.15, iOS 13.0, *)
private final class TestObject: ObservableObject {
let objectWillChange = ObservableObjectPublisher()
@Published var state: Int
init() {
_state = Published(initialValue: 0)
}
}
#endif
@@ -0,0 +1,357 @@
//
// AllSatisfyTests.swift
//
//
// Created by Sergej Jaskiewicz on 15.10.2019.
//
import XCTest
#if OPENCOMBINE_COMPATIBILITY_TEST
import Combine
#else
import OpenCombine
#endif
@available(macOS 10.15, iOS 13.0, *)
final class AllSatisfyTests: XCTestCase {
// MARK: - AllSatisfy
func testAllSatisfyAllElementsSatisfyPredicate() {
AllSatisfyTests.testAllElementsSatisfyPredicate(
expectedSubscription: "AllSatisfy",
expectedResult: true,
{ upstream, predicate in upstream.allSatisfy(predicate) }
)
}
func testAllSatisfyContainsElementNotSatisfyingPredicate() {
AllSatisfyTests.testContainsElementNotSatisfyingPredicate(
expectedSubscription: "AllSatisfy",
expectedResult: false,
{ upstream, predicate in upstream.allSatisfy(predicate) }
)
}
func testAllSatisfyUpstreamFinishesWithError() {
ReduceTests.testUpstreamFinishesWithError(
expectedSubscription: "AllSatisfy",
{ $0.allSatisfy(shouldNotBeCalled()) }
)
}
func testAllSatisfyUpstreamFinishesImmediately() {
ReduceTests.testUpstreamFinishesImmediately(
expectedSubscription: "AllSatisfy",
expectedResult: true,
{ $0.allSatisfy(shouldNotBeCalled()) }
)
}
func testAllSatisfyCancelAlreadyCancelled() throws {
try ReduceTests.testCancelAlreadyCancelled {
$0.allSatisfy(shouldNotBeCalled())
}
}
func testAllSatisfyRequestsUnlimitedThenSendsSubscription() {
ReduceTests.testRequestsUnlimitedThenSendsSubscription {
$0.allSatisfy(shouldNotBeCalled())
}
}
func testAllSatisfyReceiveSubscriptionTwice() throws {
try ReduceTests.testReceiveSubscriptionTwice(
expectedSubscription: "AllSatisfy",
expectedResult: .earlyCompletion(false),
{ $0.allSatisfy { $0 > 0 } }
)
try ReduceTests.testReceiveSubscriptionTwice(
expectedSubscription: "AllSatisfy",
expectedResult: .normalCompletion(true),
{ $0.allSatisfy { $0 == 0 } }
)
}
func testAllSatisfyReceiveValueBeforeSubscription() {
testReceiveValueBeforeSubscription(value: 0,
expected: .history([], demand: .none),
{ $0.allSatisfy(shouldNotBeCalled()) })
}
func testAllSatisfyReceiveCompletionBeforeSubscription() {
testReceiveCompletionBeforeSubscription(
inputType: Int.self,
expected: .history([]),
{ $0.allSatisfy(shouldNotBeCalled()) }
)
}
func testAllSatisfyRequestBeforeSubscription() {
testRequestBeforeSubscription(inputType: Int.self,
shouldCrash: false,
{ $0.allSatisfy(shouldNotBeCalled()) })
}
func testAllSatisfyCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
{ $0.allSatisfy(shouldNotBeCalled()) })
}
func testAllSatisfyLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
{ $0.allSatisfy { _ in true } })
}
func testAllSatisfyReflection() throws {
try testReflection(parentInput: Int.self,
parentFailure: Error.self,
description: "AllSatisfy",
customMirror: reduceLikeOperatorMirror(),
playgroundDescription: "AllSatisfy",
{ $0.allSatisfy(shouldNotBeCalled()) })
}
// MARK: - TryAllSatisfy
func testTryAllSatisfyAllElementsSatisfyPredicate() {
AllSatisfyTests.testAllElementsSatisfyPredicate(
expectedSubscription: "TryAllSatisfy",
expectedResult: true,
{ upstream, predicate in upstream.tryAllSatisfy(predicate) }
)
}
func testTryAllSatisfyContainsElementNotSatisfyingPredicate() {
AllSatisfyTests.testContainsElementNotSatisfyingPredicate(
expectedSubscription: "TryAllSatisfy",
expectedResult: false,
{ upstream, predicate in upstream.tryAllSatisfy(predicate) }
)
}
func testFailureBecauseOfThrow() throws {
func predicate(_ input: Int) throws -> Bool {
if input == 3 {
throw TestingError.oops
}
return input < 3
}
try ReduceTests.testFailureBecauseOfThrow(expectedSubscription: "TryAllSatisfy",
expectedFailure: TestingError.oops,
{ $0.tryAllSatisfy(predicate) })
}
func testTryAllSatisfyUpstreamFinishesWithError() {
ReduceTests.testUpstreamFinishesWithError(
expectedSubscription: "TryAllSatisfy",
{ $0.tryAllSatisfy(shouldNotBeCalled()) }
)
}
func testTryAllSatisfyUpstreamFinishesImmediately() {
ReduceTests.testUpstreamFinishesImmediately(
expectedSubscription: "TryAllSatisfy",
expectedResult: true,
{ $0.tryAllSatisfy(shouldNotBeCalled()) }
)
}
func testTryAllSatisfyCancelAlreadyCancelled() throws {
try ReduceTests.testCancelAlreadyCancelled {
$0.tryAllSatisfy(shouldNotBeCalled())
}
}
func testTryAllSatisfyRequestsUnlimitedThenSendsSubscription() {
ReduceTests.testRequestsUnlimitedThenSendsSubscription {
$0.tryAllSatisfy(shouldNotBeCalled())
}
}
func testTryAllSatisfyReceiveSubscriptionTwice() throws {
try ReduceTests.testReceiveSubscriptionTwice(
expectedSubscription: "TryAllSatisfy",
expectedResult: .earlyCompletion(false),
{ $0.tryAllSatisfy { $0 > 0 } }
)
try ReduceTests.testReceiveSubscriptionTwice(
expectedSubscription: "TryAllSatisfy",
expectedResult: .normalCompletion(true),
{ $0.tryAllSatisfy { $0 == 0 } }
)
try ReduceTests.testReceiveSubscriptionTwice(
expectedSubscription: "TryAllSatisfy",
expectedResult: .failure(TestingError.oops),
{ $0.tryAllSatisfy { _ in throw TestingError.oops } }
)
}
func testTryAllSatisfyReceiveValueBeforeSubscription() {
testReceiveValueBeforeSubscription(value: 0,
expected: .history([], demand: .none),
{ $0.tryAllSatisfy(shouldNotBeCalled()) })
}
func testTryAllSatisfyReceiveCompletionBeforeSubscription() {
testReceiveCompletionBeforeSubscription(
inputType: Int.self,
expected: .history([]),
{ $0.tryAllSatisfy(shouldNotBeCalled()) }
)
}
func testTryAllSatisfyRequestBeforeSubscription() {
testRequestBeforeSubscription(inputType: Int.self,
shouldCrash: false,
{ $0.tryAllSatisfy(shouldNotBeCalled()) })
}
func testTryAllSatisfyCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
shouldCrash: false,
{ $0.tryAllSatisfy(shouldNotBeCalled()) })
}
func testTryAllSatisfyLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
{ $0.tryAllSatisfy { _ in true } })
}
func testTryAllSatisfyReflection() throws {
try testReflection(parentInput: Int.self,
parentFailure: Error.self,
description: "TryAllSatisfy",
customMirror: reduceLikeOperatorMirror(),
playgroundDescription: "TryAllSatisfy",
{ $0.tryAllSatisfy(shouldNotBeCalled()) })
}
// MARK: - Generic tests
/// Publishes -2, 0, 2, 4, 7
static func testAllElementsSatisfyPredicate<Operator: Publisher>(
expectedSubscription: StringSubscription,
expectedResult: Bool,
countPredicateCalls: Bool = true,
_ makeOperator: (CustomPublisher, @escaping (Int) -> Bool) -> Operator
) where Operator.Output == Bool {
var predicateCounter = 0
func predicate(_ value: Int) -> Bool {
predicateCounter += 1
return value.isMultiple(of: 2)
}
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: .max(1),
receiveValueDemand: .none,
createSut: { makeOperator($0, predicate) }
)
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
XCTAssertEqual(helper.publisher.send(-2), .none)
XCTAssertEqual(helper.publisher.send(0), .none)
XCTAssertEqual(helper.publisher.send(2), .none)
XCTAssertEqual(helper.publisher.send(4), .none)
if countPredicateCalls {
XCTAssertEqual(predicateCounter, 4)
}
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
helper.publisher.send(completion: .finished)
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
.value(expectedResult),
.completion(.finished)])
XCTAssertEqual(helper.publisher.send(7), .none)
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
.value(expectedResult),
.completion(.finished)])
if countPredicateCalls {
XCTAssertEqual(predicateCounter, 4)
}
}
/// Publishes -2, 0, 2, 4, 7, 8, 3
static func testContainsElementNotSatisfyingPredicate<Operator: Publisher>(
expectedSubscription: StringSubscription,
expectedResult: Bool,
countPredicateCalls: Bool = true,
_ makeOperator: (CustomPublisher, @escaping (Int) -> Bool) -> Operator
) where Operator.Output == Bool {
var predicateCounter = 0
func predicate(_ value: Int) -> Bool {
predicateCounter += 1
return value.isMultiple(of: 2)
}
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: .max(1),
receiveValueDemand: .none,
createSut: { makeOperator($0, predicate) }
)
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
XCTAssertEqual(helper.publisher.send(-2), .none)
XCTAssertEqual(helper.publisher.send(0), .none)
XCTAssertEqual(helper.publisher.send(2), .none)
XCTAssertEqual(helper.publisher.send(4), .none)
if countPredicateCalls {
XCTAssertEqual(predicateCounter, 4)
}
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
XCTAssertEqual(helper.publisher.send(7), .none)
if countPredicateCalls {
XCTAssertEqual(predicateCounter, 5)
}
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
.value(expectedResult),
.completion(.finished)])
XCTAssertEqual(helper.publisher.send(8), .none)
XCTAssertEqual(helper.publisher.send(3), .none)
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
.value(expectedResult),
.completion(.finished)])
if countPredicateCalls {
XCTAssertEqual(predicateCounter, 5)
}
}
}
@@ -119,6 +119,21 @@ final class AutoconnectTests: XCTestCase {
.disconnected])
}
func testAutoconnectReceiveValueBeforeSubscription() {
testReceiveValueBeforeSubscription(value: 0,
expected: .history([.value(0)],
demand: .max(42)),
{ $0.autoconnect() })
}
func testAutoconnectReceiveCompletionBeforeSubscription() {
testReceiveCompletionBeforeSubscription(
inputType: Int.self,
expected: .history([.completion(.finished)]),
{ $0.autoconnect() }
)
}
func testAutoconnectReflection() throws {
let customMirrorPredicate = expectedChildren(

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