107 Commits

Author SHA1 Message Date
Maximilian Wendel 915a7efaf5 Prepare for 0.10.1 (#185) 2020-10-04 15:01:09 +03:00
Sergej Jaskiewicz 024e576b0f Add link to generated interfaces for Combine 2020-10-01 13:21:33 +03:00
Max Desiatov f4a611e95f Run compatibility tests on iOS 13.6/Xcode 11.6 (#181) 2020-08-31 12:12:38 +01:00
Maximilian Wendel c09e47f792 Fix OperationQueue scheduler on non-Darwin platforms before Swift 5.1 (#177) 2020-07-29 16:26:50 +03:00
Maximilian Wendel dd6be33016 Don't use PropertyListEncoder on non-Darwin platforms before Swift 5.1 (#176)
PropertyListEncoder and PropertyListDecoder are both unavailable prior to Swift 5.1, causing a build error for Swift 5.0.
2020-07-29 16:24:28 +03:00
dependabot[bot] 5af4fb6ba4 Bump json from 2.2.0 to 2.3.1 (#175) 2020-07-28 07:22:03 +00:00
Adam Leonard 0ca4c7658f Fix a build error on linux: kCFStringEncodingUTF8 is not defined. (#173)
Instead, use `CFStringBuiltInEncodings.UTF8.rawValue`.

Also fix a type error I was getting in a unit test.

Co-authored-by: adaml <adam@seesaw.me>
2020-07-21 16:05:48 +03:00
Alexey Salangin 8cf59d6d2a Fix some typos (#172) 2020-07-14 08:48:35 +03:00
Sergej Jaskiewicz f3d068d6f2 Bump the version to 0.10.0 (#171) 2020-06-28 20:39:03 +03:00
Sergej Jaskiewicz 1cfb4a2eae Implement Publishers.Debounce (#133) 2020-06-28 19:50:45 +03:00
Sergej Jaskiewicz 2b64b7981d Implement Publishers.Timeout (#164) 2020-06-28 14:31:15 +03:00
Sergej Jaskiewicz ad95dfdc8c Update CircleCI badge 2020-06-26 17:25:12 +03:00
Sergej Jaskiewicz 988644159e Update badges after migrating to an organization 2020-06-26 16:32:22 +03:00
Sergej Jaskiewicz a9fa1ed4f4 Update the repository URL after migrating to an organization 2020-06-26 16:19:42 +03:00
Sergej Jaskiewicz 3f125b30e1 Implement OperationQueue scheduler (#165) 2020-06-26 15:40:15 +03:00
Sergej Jaskiewicz c9e7293a2a Fix behavior of CurrentValueSubject when setting new value after completion 2020-06-26 11:38:57 +03:00
Sergej Jaskiewicz f5d2c39c58 Add a test for CurrentValueSUbject 2020-06-26 11:17:32 +03:00
Sergej Jaskiewicz 70bf8e8bb3 Run compatibility tests on iOS 13.5/Xcode 11.5 2020-06-26 00:11:34 +03:00
Sergej Jaskiewicz f04053e1eb A more efficient and correct implementation of Future 2020-06-26 00:11:34 +03:00
Sergej Jaskiewicz af510706d7 A more efficient and correct implementation of CurrentValueSubject 2020-06-26 00:11:34 +03:00
Sergej Jaskiewicz 29fbf7de31 A more efficient and correct implementation of PassthroughSubject 2020-06-26 00:11:34 +03:00
Sergej Jaskiewicz 102eef88a0 Implement ConduitList 2020-06-26 00:11:34 +03:00
Sergej Jaskiewicz b34d4652d3 Make TimerPublisher tests more stable (#167) 2020-06-24 16:09:15 +03:00
Max Desiatov fcc2a4350a Add TimerPublisher and Timer.publish (#156)
Co-authored-by: Sergej Jaskiewicz <jaskiewiczs@icloud.com>
2020-06-23 20:55:20 +03:00
Sergej Jaskiewicz 59183ce0a5 Document the release process 2020-06-12 23:54:21 +03:00
Sergej Jaskiewicz b1f676d273 Bump the version to 0.9.0 2020-06-12 23:54:21 +03:00
Sergej Jaskiewicz b2784a1011 Implement Publishers.Catch and Publishers.TryCatch (#140) 2020-06-11 22:17:16 +03:00
Max Desiatov d67e77c84d Test with Swift 5.2 on Ubuntu 18.04 (#159)
I don't think it makes much sense to test on an older version of Swift on Ubuntu. Since we tested only a single version, I've updated that to the latest available, but let me know if you'd like to test with multiple Swift versions on Linux.

As a sidenote, I hope we could also switch to GitHub Actions in the future. Circle CI seems to be annoyingly slow.
2020-06-06 19:43:20 +01:00
Vuk Radosavljevic d680f09932 Change collection to set in documentation (#151) 2020-04-10 10:16:26 +01:00
Sergej Jaskiewicz 30b5dd4c2f Update for Xcode 11.4 release (#150) 2020-03-28 21:23:57 +03:00
Sergej Jaskiewicz 621f970998 Update to match the behavior in Xcode 11.4 beta 2 SDKs. (#148) 2020-02-26 13:56:18 +03:00
Sergej Jaskiewicz d6b70ad309 Implement the RunLoop scheduler (#131) 2020-02-05 02:11:10 +03:00
Sergej Jaskiewicz 918e9131ad Implement Publishers.SwitchToLatest (#142) 2020-02-04 13:26:17 +03:00
Rob Mayoff 7f7f397062 Add opencombine_lldb.py for better Demand formatting in lldb/Xcode (#146) 2020-01-29 14:57:44 +03:00
Rob Mayoff 3b1437e46c Work around SR-11680 (#145)
The Swift bug report: https://bugs.swift.org/browse/SR-11680

Swift nightly toolchains are available here: https://swift.org/download/

The Swift nightly toolchains cannot build OpenCombine. Here's why:

The COpenCombineHelpers target defines a non-static function
(`opencombine_stop_in_debugger`) in a header file. This function is
emitted in the target's IR, but not in the target's TBD.

Swift nightly toolchains have assertions enabled, so they use the
-validate-tbd-against-ir=missing build setting. This build setting
makes the compiler fail if the TBD doesn't match the IR.

This commit un-inlines `opencombine_stop_in_debugger`, so it
is not emitted in the IR. This stops the TBD validator from
complaining.
2020-01-24 02:17:40 +03:00
Sergej Jaskiewicz 79899f7742 Bump podspec version for OpenCombineFoundation 2020-01-17 17:25:10 +03:00
Sergej Jaskiewicz 1496bab272 Prepare for 0.8.0 2020-01-17 17:15:39 +03:00
Sergej Jaskiewicz 1ebbdb8ea9 Implement Publishers.Buffer (#143) 2020-01-17 11:01:43 +03:00
Sergej Jaskiewicz f861335dc3 Implement Publishers.AssertNoFailure (#138) 2019-12-25 19:15:57 +03:00
Sergej Jaskiewicz 769c3c818f Implement Publishers.CollectByCount (#137) 2019-12-25 03:01:34 +03:00
Sergej Jaskiewicz 910d21da4c Implement Publishers.DropUntilOutput (#136) 2019-12-24 22:14:10 +03:00
Sergej Jaskiewicz 6e20956d6d Guess unknown DisaptchTimeInterval more precisely (#135) 2019-12-24 19:18:29 +03:00
dependabot[bot] e453879d75 Bump excon from 0.68.0 to 0.71.0 (#132)
Bumps [excon](https://github.com/excon/excon) from 0.68.0 to 0.71.0.
- [Release notes](https://github.com/excon/excon/releases)
- [Changelog](https://github.com/excon/excon/blob/master/changelog.txt)
- [Commits](https://github.com/excon/excon/compare/v0.68.0...v0.71.0)

Signed-off-by: dependabot[bot] <support@github.com>
2019-12-17 02:25:13 +03:00
Sergej Jaskiewicz 98f6b6b337 Fix more overflows in DispatchQueue.SchedulerTimeType (#130) 2019-12-15 15:57:05 +03:00
Sergej Jaskiewicz 74b739d74e Work around the 'default will never be executed' warning on Linux (#129)
Also, enable the -warnings-as-errors flag on CI.
2019-12-15 02:37:33 +03:00
Sergej Jaskiewicz bcba9a19d4 Update for Xcode 11.3 (#123)
- Send subscription synchronously in ReceiveOn and Delay operators
- Some locks made recursive, as they should be
- ObservableObjectPublisher doesn't use PassthroughSubject under the hood anymore
2019-12-14 23:11:47 +03:00
Max Desiatov 486e166462 Expose OpenCombineFoundation target as a product (#128)
This should fix `import OpenCombineFoundation` issue reported in #124.
2019-12-14 00:02:03 +00:00
Sergej Jaskiewicz c6536cf8d3 Implement URLSession.DataTaskPublisher (#127) 2019-12-13 16:44:03 +03:00
Sergej Jaskiewicz cf41c25cf7 Implement NotificationCenter.Publisher 2019-12-13 10:34:47 +03:00
Sergej Jaskiewicz b4557fb311 Create OpenCombineFoundation target
Implement TopLevelEncoder/TopLevelDecoder conformances for:

- JSONEncoder
- JSONDecoder
- PropertyListEncoder
- PropertyListDecoder
2019-12-13 10:34:47 +03:00
Sergej Jaskiewicz f8e6e66ab4 Fix integer overflows in DispatchQueue.SchedulerTimeType.Stride (#126) 2019-12-12 23:45:01 +03:00
Joe Spadafora 95b42abce3 Implement Publishers.ReplaceEmpty (#122) 2019-12-11 19:34:24 +03:00
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
195 changed files with 27420 additions and 4421 deletions
+274
View File
@@ -0,0 +1,274 @@
version: 2
jobs:
"Execute tests on macOS 10.15.0 (Xcode 11.3.0, Swift 5.1.3)":
macos:
xcode: "11.3.0"
environment:
SWIFT_VERSION: "5.1.3"
steps:
- checkout
- run:
name: Building and running tests in debug mode with coverage
command: |
make test-debug \
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
SWIFT_TEST_FLAGS="--enable-code-coverage --build-path .build-test-debug"
xcrun llvm-cov show \
-instr-profile=.build-test-debug/debug/codecov/default.profdata \
.build-test-debug/debug/OpenCombinePackageTests.xctest/Contents/MacOS/OpenCombinePackageTests \
> coverage.txt
- run:
name: Building and running tests in debug mode with TSan
command: |
make test-debug-sanitize-thread \
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
SWIFT_TEST_FLAGS="--build-path .build-test-debug-sanitize-thread"
- run:
name: Building and running tests in release mode
command: |
make test-release \
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
SWIFT_TEST_FLAGS="--build-path .build-test-release"
- run:
name: Generating Xcode project
command: make generate-xcodeproj SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors"
- run:
name: Building for testing on macOS 10.15.0 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild build-for-testing \
-scheme OpenCombine-Package \
-sdk macosx10.15 \
-derivedDataPath DerivedData \
| tee xcodebuild_build-for-testing.log \
| xcpretty
- store_artifacts:
path: xcodebuild_build-for-testing.log
- run:
name: Testing on macOS 10.15.0 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild test-without-building \
-scheme OpenCombine-Package \
-sdk macosx10.15 \
-derivedDataPath DerivedData \
| tee xcodebuild_test-without-building.log \
| xcpretty --report junit -o build/reports/results.xml
- store_artifacts:
path: xcodebuild_test-without-building.log
- store_test_results:
path: build/reports
- run:
name: Uploading code coverage
command: |
bash <(curl -s https://codecov.io/bash) -D DerivedData
"Execute compatibility tests on iOS 13.6 (Xcode 11.6.0, Swift 5.2.4)":
macos:
xcode: "11.6.0"
environment:
SWIFT_VERSION: "5.2.4"
steps:
- checkout
- run:
name: Generating Xcode project
command: make generate-compatibility-xcodeproj
- run:
name: Building for testing on iOS 13.6 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild build-for-testing \
-scheme OpenCombine-Package \
-destination "platform=iOS Simulator,name=iPhone 11,OS=13.6" \
-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.6 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild test-without-building \
-scheme OpenCombine-Package \
-destination "platform=iOS Simulator,name=iPhone 11,OS=13.6" \
-derivedDataPath DerivedData \
| tee xcodebuild_test-without-building.log \
| xcpretty --report junit -o build/reports/results.xml
- store_artifacts:
path: xcodebuild_test-without-building.log
- store_test_results:
path: build/reports
"Execute tests on iOS 9.3 (Xcode 10.2.1, Swift 5.0.1)":
macos:
xcode: "10.2.1"
environment:
BUNDLE_PATH: .bundle # path to install gems and use for caching
SWIFT_VERSION: "5.0.1"
steps:
- checkout
- run:
name: Installing gem dependencies
command: bundle install && bundle clean
- restore_cache:
keys:
- v1-simulator-cache-{{ arch }}
- run:
# CircleCI doesn't have an iOS 9 simulator, so we need to install it manually.
name: Installing iOS 9 simulator
command: |
bundle exec xcversion simulators --install="iOS 9.3"
bundle exec xcversion simulators
xcrun simctl list
- save_cache:
key: v1-simulator-cache-{{ arch }}
paths:
- ~/Library/Caches/XcodeInstall
- run:
name: Generating Xcode project
command: |
make generate-xcodeproj SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors"
xcodebuild -scheme OpenCombine-Package -showdestinations
- run:
name: Building for testing on iOS 9.3 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild build-for-testing \
-scheme OpenCombine-Package \
-destination "platform=iOS Simulator,name=iPhone 4s,OS=9.3" \
-derivedDataPath DerivedData \
| tee xcodebuild_build-for-testing.log \
| xcpretty
- store_artifacts:
path: xcodebuild_build-for-testing.log
- run:
name: Testing on iOS 9.3 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild test-without-building \
-scheme OpenCombine-Package \
-destination "platform=iOS Simulator,name=iPhone 4s,OS=9.3" \
-derivedDataPath DerivedData \
| tee xcodebuild_test-without-building.log \
| xcpretty --report junit -o build/reports/results.xml
- store_artifacts:
path: xcodebuild_test-without-building.log
- store_test_results:
path: build/reports
- run:
name: Uploading code coverage
command: |
bash <(curl -s https://codecov.io/bash) -D DerivedData
"Execute tests on Ubuntu 18.04 (Swift 5.2)":
docker:
- image: swift:5.2-bionic
environment:
SWIFT_VERSION: "5.2"
steps:
- checkout
- run:
name: Installing dependencies
command: |
apt update -y
apt upgrade -y
apt install -y curl
- run:
name: Building and running tests in debug mode with coverage
command: | # We need to run the test command twice because of https://bugs.swift.org/browse/SR-10783
make test-debug \
SWIFT_TEST_FLAGS="--enable-test-discovery \
--enable-index-store \
--enable-code-coverage \
--build-path .build-test-debug" \
> /dev/null 2>&1 \
|| true
make test-debug \
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
SWIFT_TEST_FLAGS="--enable-test-discovery \
--enable-index-store \
--enable-code-coverage \
--build-path .build-test-debug"
llvm-cov show \
-instr-profile=.build-test-debug/debug/codecov/default.profdata \
.build-test-debug/debug/OpenCombinePackageTests.xctest \
> coverage.txt
- run:
name: Building and running tests in debug mode with TSan
command: | # We need to run the test command twice because of https://bugs.swift.org/browse/SR-10783
make test-debug-sanitize-thread \
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
SWIFT_TEST_FLAGS="--enable-test-discovery \
--enable-index-store \
--build-path .build-test-debug-sanitize-thread" \
> /dev/null 2>&1 \
|| true
make test-debug-sanitize-thread \
SWIFT_TEST_FLAGS="--enable-test-discovery \
--enable-index-store \
--build-path .build-test-debug-sanitize-thread" \
- run:
name: Building and running tests in release mode
command: |
make test-release \
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
SWIFT_TEST_FLAGS="--enable-test-discovery \
--enable-index-store \
--build-path .build-test-release"
- run:
name: Uploading code coverage
command: |
bash <(curl -s https://codecov.io/bash)
"Run SwiftLint and Danger":
macos:
xcode: "11.3.0"
environment:
HOMEBREW_NO_AUTO_UPDATE: "1"
steps:
- checkout
- run:
name: Install SwiftLint
command: |
brew install swiftlint
- run:
name: Install danger-swift
command: |
brew install danger/tap/danger-swift
- run:
name: Run danger-swift
command: danger-swift ci
"Run Pod spec lint":
macos:
xcode: "11.3.0"
environment:
HOMEBREW_NO_AUTO_UPDATE: "1"
steps:
- checkout
- run:
name: Pod lib lint
command: |
pod lib lint --allow-warnings --verbose
workflows:
version: 2
"OpenCombine: execute tests on macOS":
jobs:
- "Execute tests on macOS 10.15.0 (Xcode 11.3.0, Swift 5.1.3)"
"OpenCombine: execute compatibility tests":
jobs:
- "Execute compatibility tests on iOS 13.6 (Xcode 11.6.0, Swift 5.2.4)"
"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.2)"
"OpenCombine: run SwiftLint and Danger":
jobs:
- "Run SwiftLint and Danger"
"OpenCombine: validate podspec files":
jobs:
- "Run Pod spec lint"
+3
View File
@@ -153,3 +153,6 @@ dmypy.json
.pyre/
# End of https://www.gitignore.io/api/Python
.bundle/
node_modules/
+7 -1
View File
@@ -16,11 +16,14 @@ disabled_rules:
- identifier_name
- nesting
- notification_center_detachment
- no_fallthrough_only
- no_space_in_method_call
- redundant_string_enum_value
- todo
- trailing_comma
- type_body_length
- opening_brace
- untyped_error_in_catch
opt_in_rules:
- array_init
@@ -59,11 +62,14 @@ 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
implicit_return:
included:
- closure
line_length:
warning: 90
error: 120
-90
View File
@@ -1,90 +0,0 @@
language: generic
addons:
homebrew:
taps:
- danger/tap
packages:
- swiftlint
- danger-swift
update: true
cache:
directories:
- .build
- ~/.danger-swift
- ~/.swiftenv
- ~/Library/Caches/Homebrew
matrix:
include:
- name: "Ubuntu 16.04 | Swift 5.1.1 | Tests"
os: linux
dist: xenial
sudo: required
env: SWIFT_VERSION="5.1.1" OPENCOMBINE_TEST="YES"
- name: "macOS 10.14 | Swift 5.0 | Tests"
os: osx
osx_image: xcode10.2
env: SWIFT_VERSION="5.0" CODE_COVERAGE="YES" OPENCOMBINE_TEST="YES"
- name: "iOS 13.1 | Swift 5.1.1 | Compatibility Tests"
os: osx
osx_image: xcode11.1
env: SWIFT_VERSION="5.1.1" OPENCOMBINE_COMPATIBILITY_TEST="YES"
- name: "macOS 10.14 | Swift 5.0 | Code Quality"
os: osx
osx_image: xcode10.2
env: SWIFT_VERSION="5.0" RUN_DANGER="YES" SWIFT_LINT="YES"
before_cache:
- brew cleanup
before_install:
- if [[ $TRAVIS_OS_NAME == "linux" ]]; then
cat /proc/cpuinfo;
fi
install:
- if [[ $TRAVIS_OS_NAME == "linux" ]]; then
eval "$(curl -sL https://swiftenv.fuller.li/install.sh)";
fi
- if [[ $CODE_COVERAGE == "YES" ]]; then
gem install xcpretty;
fi
script:
- if [[ $OPENCOMBINE_TEST == "YES" ]]; then
if [[ $TRAVIS_OS_NAME == "linux" ]]; then
make SWIFT_TEST_FLAGS="--enable-test-discovery --enable-index-store" test-debug;
else
make test-debug;
fi
fi
- if [[ $OPENCOMBINE_TEST == "YES" ]]; then
if [[ $TRAVIS_OS_NAME == "linux" ]]; then
make SWIFT_TEST_FLAGS="--enable-test-discovery --enable-index-store" test-debug-sanitize-thread;
else
make test-debug-sanitize-thread;
fi
fi
- if [[ $OPENCOMBINE_TEST == "YES" ]]; then
if [[ $TRAVIS_OS_NAME == "linux" ]]; then
make SWIFT_TEST_FLAGS="--enable-test-discovery --enable-index-store" test-release;
else
make test-release;
fi
fi
- if [[ $OPENCOMBINE_COMPATIBILITY_TEST == "YES" ]]; then
make generate-compatibility-xcodeproj;
set -o pipefail && xcodebuild -scheme OpenCombine-Package -sdk iphonesimulator13.1 -destination "platform=iOS Simulator,name=iPhone 11,OS=13.1" build test | xcpretty;
fi
- if [[ $SWIFT_LINT == "YES" ]]; then
swiftlint lint --strict --reporter "emoji";
fi
- if [[ $RUN_DANGER == "YES" ]]; then
danger-swift ci;
fi
after_success:
- if [[ $CODE_COVERAGE == "YES" ]]; then
make generate-xcodeproj;
xcodebuild -scheme OpenCombine-Package build test | xcpretty;
bash <(curl -s https://codecov.io/bash);
fi
+3
View File
@@ -0,0 +1,3 @@
source 'https://rubygems.org'
gem 'xcode-install'
+162
View File
@@ -0,0 +1,162 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.1)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
atomos (0.1.3)
babosa (1.0.3)
claide (1.0.3)
colored (1.2)
colored2 (3.1.2)
commander-fastlane (4.4.6)
highline (~> 1.7.2)
declarative (0.0.10)
declarative-option (0.1.0)
digest-crc (0.4.1)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.5)
emoji_regex (1.0.1)
excon (0.71.0)
faraday (0.17.0)
multipart-post (>= 1.2, < 3)
faraday-cookie_jar (0.0.6)
faraday (>= 0.7.4)
http-cookie (~> 1.0.0)
faraday_middleware (0.13.1)
faraday (>= 0.7.4, < 1.0)
fastimage (2.1.7)
fastlane (2.134.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
babosa (>= 1.0.2, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
commander-fastlane (>= 4.4.6, < 5.0.0)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 2.0)
excon (>= 0.45.0, < 1.0.0)
faraday (~> 0.17)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 0.13.1)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-api-client (>= 0.21.2, < 0.24.0)
google-cloud-storage (>= 1.15.0, < 2.0.0)
highline (>= 1.7.2, < 2.0.0)
json (< 3.0.0)
jwt (~> 2.1.0)
mini_magick (>= 4.9.4, < 5.0.0)
multi_xml (~> 0.5)
multipart-post (~> 2.0.0)
plist (>= 3.1.0, < 4.0.0)
public_suffix (~> 2.0.0)
rubyzip (>= 1.3.0, < 2.0.0)
security (= 0.1.3)
simctl (~> 1.6.3)
slack-notifier (>= 2.0.0, < 3.0.0)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (>= 1.4.5, < 2.0.0)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.8.1, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
gh_inspector (1.1.3)
google-api-client (0.23.9)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.5, < 0.7.0)
httpclient (>= 2.8.1, < 3.0)
mime-types (~> 3.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
signet (~> 0.9)
google-cloud-core (1.3.2)
google-cloud-env (~> 1.0)
google-cloud-env (1.2.1)
faraday (~> 0.11)
google-cloud-storage (1.16.0)
digest-crc (~> 0.4)
google-api-client (~> 0.23)
google-cloud-core (~> 1.2)
googleauth (>= 0.6.2, < 0.10.0)
googleauth (0.6.7)
faraday (~> 0.12)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (~> 0.7)
highline (1.7.10)
http-cookie (1.0.3)
domain_name (~> 0.5)
httpclient (2.8.3)
json (2.3.1)
jwt (2.1.0)
memoist (0.16.1)
mime-types (3.3)
mime-types-data (~> 3.2015)
mime-types-data (3.2019.1009)
mini_magick (4.9.5)
multi_json (1.14.1)
multi_xml (0.6.0)
multipart-post (2.0.0)
nanaimo (0.2.6)
naturally (2.2.0)
os (1.0.1)
plist (3.5.0)
public_suffix (2.0.5)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rouge (2.0.7)
rubyzip (1.3.0)
security (0.1.3)
signet (0.11.0)
addressable (~> 2.3)
faraday (~> 0.9)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.6)
CFPropertyList
naturally
slack-notifier (2.3.2)
terminal-notifier (2.0.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
tty-cursor (0.7.0)
tty-screen (0.7.0)
tty-spinner (0.9.1)
tty-cursor (~> 0.7)
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.6)
unicode-display_width (1.6.0)
word_wrap (1.0.0)
xcode-install (2.6.2)
claide (>= 0.9.1, < 1.1.0)
fastlane (>= 2.1.0, < 3.0.0)
xcodeproj (1.13.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.2.6)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.0)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
ruby
DEPENDENCIES
xcode-install
BUNDLED WITH
2.0.1
+11 -9
View File
@@ -1,32 +1,34 @@
SWIFT_EXE=swift
SWIFT_TEST_FLAGS=
SWIFT_BUILD_FLAGS=-Xcc -Wunguarded-availability
debug:
swift build -c debug
$(SWIFT_EXE) build -c debug $(SWIFT_BUILD_FLAGS)
release:
swift build -c release
$(SWIFT_EXE) build -c release $(SWIFT_BUILD_FLAGS)
test-debug:
swift test -c debug $(SWIFT_TEST_FLAGS)
$(SWIFT_EXE) test -c debug $(SWIFT_BUILD_FLAGS) $(SWIFT_TEST_FLAGS)
test-debug-sanitize-thread:
swift test -c debug --sanitize thread $(SWIFT_TEST_FLAGS)
$(SWIFT_EXE) test -c debug --sanitize thread $(SWIFT_BUILD_FLAGS) $(SWIFT_TEST_FLAGS)
test-release:
swift test -c release $(SWIFT_TEST_FLAGS)
$(SWIFT_EXE) test -c release $(SWIFT_BUILD_FLAGS) $(SWIFT_TEST_FLAGS)
swift-version:
swift -version
$(SWIFT_EXE) -version
test-compatibility:
swift test -Xswiftc -DOPENCOMBINE_COMPATIBILITY_TEST
$(SWIFT_EXE) test -Xswiftc -DOPENCOMBINE_COMPATIBILITY_TEST
generate-compatibility-xcodeproj:
swift package generate-xcodeproj --xcconfig-overrides Combine-Compatibility.xcconfig; \
$(SWIFT_EXE) package generate-xcodeproj --xcconfig-overrides Combine-Compatibility.xcconfig; \
open OpenCombine.xcodeproj
generate-xcodeproj:
swift package generate-xcodeproj --enable-code-coverage
$(SWIFT_EXE) package $(SWIFT_BUILD_FLAGS) generate-xcodeproj --enable-code-coverage
gyb:
$(shell ./utils/recursively_gyb.sh)
+27
View File
@@ -0,0 +1,27 @@
Pod::Spec.new do |spec|
spec.name = "OpenCombine"
spec.version = "0.10.1"
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.10.1"
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.9'
end
+25
View File
@@ -0,0 +1,25 @@
Pod::Spec.new do |spec|
spec.name = "OpenCombineFoundation"
spec.version = "0.10.1"
spec.summary = "OpenCombine + OpenCombineFoundation interoperability"
spec.description = <<-DESC
Adds publishers to Foundation types like NotificationCenter, URLSession etc.
DESC
spec.homepage = "https://github.com/broadwaylamb/OpenCombine/"
spec.license = "MIT"
spec.authors = { "Sergej Jaskiewicz" => "jaskiewiczs@icloud.com" }
spec.source = { :git => "https://github.com/broadwaylamb/OpenCombine.git", :tag => "#{spec.version}" }
spec.swift_version = "5.0"
spec.osx.deployment_target = "10.10"
spec.ios.deployment_target = "8.0"
spec.watchos.deployment_target = "2.0"
spec.tvos.deployment_target = "9.0"
spec.source_files = "Sources/OpenCombineFoundation/**/*.swift"
spec.dependency "OpenCombine", '>= 0.9'
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
}
+4 -4
View File
@@ -7,18 +7,18 @@ let package = Package(
products: [
.library(name: "OpenCombine", targets: ["OpenCombine"]),
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
],
dependencies: [
.package(url: "https://github.com/broadwaylamb/GottaGoFast.git", from: "0.1.0")
.library(name: "OpenCombineFoundation", targets: ["OpenCombineFoundation"]),
],
targets: [
.target(name: "COpenCombineHelpers"),
.target(name: "OpenCombine", dependencies: ["COpenCombineHelpers"]),
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
.target(name: "OpenCombineFoundation", dependencies: ["OpenCombine",
"COpenCombineHelpers"]),
.testTarget(name: "OpenCombineTests",
dependencies: ["OpenCombine",
"OpenCombineDispatch",
"GottaGoFast"],
"OpenCombineFoundation"],
swiftSettings: [.unsafeFlags(["-enable-testing"])])
],
cxxLanguageStandard: .cxx1z
+80 -5
View File
@@ -1,8 +1,9 @@
# OpenCombine
[![Build Status](https://travis-ci.org/broadwaylamb/OpenCombine.svg?branch=master)](https://travis-ci.org/broadwaylamb/OpenCombine)
[![codecov](https://codecov.io/gh/broadwaylamb/OpenCombine/branch/master/graph/badge.svg)](https://codecov.io/gh/broadwaylamb/OpenCombine)
[![OpenCombine](https://circleci.com/gh/OpenCombine/OpenCombine.svg?style=svg)](https://circleci.com/gh/OpenCombine/OpenCombine)
[![codecov](https://codecov.io/gh/OpenCombine/OpenCombine/branch/master/graph/badge.svg)](https://codecov.io/gh/OpenCombine/OpenCombine)
![Language](https://img.shields.io/badge/Swift-5.0-orange.svg)
![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,13 +12,52 @@ The main goal of this project is to provide a compatible, reliable and efficient
The project is in early development.
### Installation
`OpenCombine` contains three public targets: `OpenCombine`, `OpenCombineFoundation` and `OpenCombineDispatch` (the fourth one, `COpenCombineHelpers`, is considered private. Don't import it in your projects).
OpenCombine itself does not have any dependencies. Not even Foundation or Dispatch. If you want to use OpenCombine with Dispatch (for example for using `DispatchQueue` as `Scheduler` for operators like `debounce`, `receive(on:)` etc.), you will need to import both `OpenCombine` and `OpenCombineDispatch`. The same applies to Foundation: if you want to use, for instance, `NotificationCenter` or `URLSession` publishers, you'll need to also import `OpenCombineFoundation`
##### Swift Package Manager
###### Swift Package
To add `OpenCombine` to your [SPM](https://swift.org/package-manager/) package, add the `OpenCombine` package to the list of package and target dependencies in your `Package.swift` file.
```swift
dependencies: [
.package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.10.1")
],
targets: [
.target(name: "MyAwesomePackage", dependencies: ["OpenCombine",
"OpenCombineDispatch",
"OpenCombineFoundation"])
]
```
###### Xcode
`OpenCombine` can also be added as a SPM dependency directly in your Xcode project *(requires Xcode 11 upwards)*.
To do so, open Xcode, use **File****Swift Packages****Add Package Dependency…**, enter the [repository URL](https://github.com/OpenCombine/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.10.1'
pod 'OpenCombineDispatch', '~> 0.10.1'
pod 'OpenCombineFoundation', '~> 0.10.1'
```
### Contributing
In order to work on this project you will need Xcode 10.2 and Swift 5.0 or later.
Please refer to the [issue #1](https://github.com/broadwaylamb/OpenCombine/issues/1) for the list of operators that remain unimplemented, as well as the [RemainingCombineInterface.swift](https://github.com/broadwaylamb/OpenCombine/blob/master/RemainingCombineInterface.swift) file. The latter contains the generated interface of Apple's Combine from the latest Xcode 11 version. When the functionality is implemented in OpenCombine, it should be removed from the RemainingCombineInterface.swift file.
Please refer to the [issue #1](https://github.com/OpenCombine/OpenCombine/issues/1) for the list of operators that remain unimplemented, as well as the [RemainingCombineInterface.swift](https://github.com/OpenCombine/OpenCombine/blob/master/RemainingCombineInterface.swift) file. The latter contains the generated interface of Apple's Combine from the latest Xcode 11 version. When the functionality is implemented in OpenCombine, it should be removed from the RemainingCombineInterface.swift file.
You can refer to [this gist](https://gist.github.com/broadwaylamb/c2c8550d76b3ff851c4c1dbf0a872e26) to observe Apple's Combine API changes between different Xcode (beta) versions, or to [this gist](https://gist.github.com/broadwaylamb/82dc2ce4ffbe06527c2c352b8f10910f) to see the relevant contents of the .swiftinterface file for Combine.
You can refer to [this repo](https://github.com/OpenCombine/combine-interfaces) to observe Apple's Combine API and documentation changes between different Xcode (beta) versions.
You can run compatibility tests against Apple's Combine. In order to do that you will need either macOS 10.14 with iOS 13 simulator installed (since the only way we can get Apple's Combine on macOS 10.14 is using the simulator), or macOS 10.15 (Apple's Combine is bundled with the OS). Execute the following command from the root of the package:
@@ -27,7 +67,29 @@ $ 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.
> NOTE: Before starting to work on some feature, please consult the [GitHub project](https://github.com/OpenCombine/OpenCombine/projects/2) to make sure that nobody's already making progress on the same feature! If not, then please create a draft PR to indicate that you're beginning your work.
#### Releasing a new version
1. Create a new branch from master and call it `release/<major>.<minor>.<patch>`.
1. Replace the usages of the old version in `README.md` with the new version (make sure to check the [Swift Package Manager](#swift-package-manager) and [CocoaPods](#cocoapods) sections).
1. Bump the version in `OpenCombine.podspec`, `OpenCombineDispatch.podspec` and `OpenCombineFoundation.podspec`. In the latter two you will also need to set the `spec.dependency "OpenCombine"` property to the **previous** version. Why? Because otherwise the `pod lib lint` command that we run on our regular CI will fail when validating the `OpenCombineDispatch` and `OpenCombineFoundation` podspecs, since the dependencies are not yet in the trunk. If we set the dependencies to the previous version (which is already in the trunk), everything will be fine. This is purely to make the CI work. The clients will not experience any issues, since the version is specified as `>=`.
1. Create a pull request to master for the release branch and make sure the CI passes.
1. Merge the pull request.
1. In the GitHub web interface on the [releases](https://github.com/OpenCombine/OpenCombine/releases) page, click the **Draft a new release** button.
1. The **Tag version** and **Release title** fields should be filled with the version number.
1. The description of the release should be consistent with the previous releases. It is a good practice to divide the description into several sections: additions, bugfixes, known issues etc. Also, be sure to mention the nicknames of the contributors of the new release.
1. Publish the release.
1. Switch to the master branch and pull the changes.
1. Push the release to CocoaPods trunk. For that, execute the following commands:
```
pod trunk push OpenCombine.podspec --verbose --allow-warnings
pod trunk push OpenCombineDispatch.podspec --verbose --allow-warnings
pod trunk push OpenCombineFoundation.podspec --verbose --allow-warnings
```
Note that you need to be one of the owners of the pod for that.
#### GYB
@@ -49,3 +111,16 @@ GYB template files have the `.gyb` extension. Run `make gyb` to generate Swift c
templates. The generated files are prefixed with `GENERATED-` and are checked into source control. Those
files should never be edited directly. Instead, the `.gyb` template should be edited, and after that the files
should be regenerated using `make gyb`.
#### Debugger Support
The file `opencombine_lldb.py` defines some `lldb` type summaries for easier debugging. These type summaries improve the way `lldb` and Xcode display some OpenCombine values.
To use `opencombine_lldb.py`, figure out its full path. Let's say the full path is `~/projects/OpenCombine/opencombine_lldb.py`. Then the following statement to your `~/.lldbinit` file:
command script import ~/projects/OpenCombine/opencombine_lldb.py
Currently, `opencombine_lldb.py` defines type summaries for these types:
- `Subscribers.Demand`
- That's all for now.
File diff suppressed because it is too large Load Diff
+39
View File
@@ -0,0 +1,39 @@
// From /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/lib/swift/Foundation.swiftmodule/x86_64.swiftinterface
// swift-interface-format-version: 1.0
// swift-compiler-version: Apple Swift version 5.1.1 (swiftlang-1100.8.275.1 clang-1100.0.32.1)
// swift-module-flags: -target x86_64-apple-macosx10.15 -enable-objc-interop -autolink-force-load -enable-library-evolution -module-link-name swiftFoundation -swift-version 5 -O -enforce-exclusivity=unchecked -module-name Foundation
public typealias Published = Combine.Published
public typealias ObservableObject = Combine.ObservableObject
public protocol _KeyValueCodingAndObservingPublishing {
}
extension NSObject : Foundation._KeyValueCodingAndObservingPublishing {
}
extension _KeyValueCodingAndObservingPublishing where Self : ObjectiveC.NSObject {
public func publisher<Value>(for keyPath: Swift.KeyPath<Self, Value>, options: Foundation.NSKeyValueObservingOptions = [.initial, .new]) -> ObjectiveC.NSObject.KeyValueObservingPublisher<Self, Value>
}
extension NSObject.KeyValueObservingPublisher {
public func didChange() -> Combine.Publishers.Map<ObjectiveC.NSObject.KeyValueObservingPublisher<Subject, Value>, Swift.Void>
}
extension NSObject {
public struct KeyValueObservingPublisher<Subject, Value> : Swift.Equatable where Subject : ObjectiveC.NSObject {
public let object: Subject
public let keyPath: Swift.KeyPath<Subject, Value>
public let options: Foundation.NSKeyValueObservingOptions
public init(object: Subject, keyPath: Swift.KeyPath<Subject, Value>, options: Foundation.NSKeyValueObservingOptions)
public static func == (lhs: ObjectiveC.NSObject.KeyValueObservingPublisher<Subject, Value>, rhs: ObjectiveC.NSObject.KeyValueObservingPublisher<Subject, Value>) -> Swift.Bool
}
}
extension NSObject.KeyValueObservingPublisher : Combine.Publisher {
public typealias Output = Value
public typealias Failure = Swift.Never
public func receive<S>(subscriber: S) where Value == S.Input, S : Combine.Subscriber, S.Failure == ObjectiveC.NSObject.KeyValueObservingPublisher<Subject, Value>.Failure
}
@@ -8,55 +8,150 @@
#include "COpenCombineHelpers.h"
#include <atomic>
#include <mutex>
#include <cstdlib>
#include <system_error>
#include <pthread.h>
#include <signal.h>
#ifdef __APPLE__
#include <os/lock.h>
#endif // __APPLE__
// Throwing exceptions through language boundaries is undefined behavior,
// so we must catch all of them in our extern "C" functions.
#define OPENCOMBINE_HANDLE_EXCEPTION_BEGIN try {
#define OPENCOMBINE_HANDLE_EXCEPTION_END } catch (...) { abort(); }
// std::terminate will print the type and the error message of the in-flight exception.
#define OPENCOMBINE_HANDLE_EXCEPTION_END } catch (...) { std::terminate(); }
// See 'double expansion trick'
#define OPENCOMBINE_STRINGIFY(value) #value
#define OPENCOMBINE_STRINGIFY_(value) OPENCOMBINE_STRINGIFY(value)
#define OPENCOMBINE_STRING_LINE_NUMBER OPENCOMBINE_STRINGIFY_(__LINE__)
// Throw an exception if the argument is non-zero with filename and line where the error
// occured.
#define OPENCOMBINE_HANDLE_PTHREAD_CALL(errc) \
if ((errc) != 0) { \
const char* what = __FILE__ ":" OPENCOMBINE_STRING_LINE_NUMBER ": " #errc; \
throw std::system_error((errc), std::system_category(), what); \
}
namespace {
static std::atomic<uint64_t> next_combine_identifier;
std::atomic<uint64_t> next_combine_identifier;
class PlatformIndependentMutex {
public:
virtual void lock() = 0;
virtual void unlock() = 0;
virtual void assertOwner() {}
virtual ~PlatformIndependentMutex() {}
virtual ~PlatformIndependentMutex() noexcept(false) {}
};
template <typename Mutex>
class GenericMutex final : PlatformIndependentMutex {
Mutex mutex_;
class PThreadMutex : PlatformIndependentMutex {
private:
pthread_mutex_t mutex_;
public:
void lock() override {
mutex_.lock();
PThreadMutex() {
Attributes attrs;
attrs.setErrorCheck();
initialize(attrs);
}
void unlock() override {
mutex_.unlock();
PThreadMutex(const PThreadMutex&) = delete;
PThreadMutex& operator=(const PThreadMutex&) = delete;
PThreadMutex(PThreadMutex&&) = delete;
PThreadMutex& operator=(PThreadMutex&&) = delete;
void lock() override final {
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutex_lock(&mutex_));
}
void unlock() override final {
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutex_unlock(&mutex_));
}
~PThreadMutex() {
// Yep, this destructor may throw. This is deliberate, since pthread_mutex_destroy
// may fail.
//
// The altrenative is to just silently ignore the error, which is even worse.
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutex_destroy(&mutex_));
}
protected:
class Attributes {
pthread_mutexattr_t attrs_;
public:
Attributes() {
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutexattr_init(&attrs_));
}
Attributes(const Attributes&) = delete;
Attributes& operator=(const Attributes&) = delete;
Attributes(Attributes&&) = delete;
Attributes& operator=(Attributes&&) = delete;
const pthread_mutexattr_t* raw() const noexcept {
return &attrs_;
}
void setRecursive() {
setType(PTHREAD_MUTEX_RECURSIVE);
}
void setErrorCheck() {
setType(PTHREAD_MUTEX_ERRORCHECK);
}
~Attributes() noexcept(false) {
// Yep, this destructor may throw. This is deliberate,
// since pthread_mutexattr_destroy may fail.
//
// The altrenative is to just silently ignore the error, which is even worse.
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutexattr_destroy(&attrs_));
}
private:
void setType(int type) {
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutexattr_settype(&attrs_, type));
}
};
void initialize(const Attributes& attributes) {
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutex_init(&mutex_, attributes.raw()));
}
};
class PThreadRecursiveMutex final : PThreadMutex {
public:
PThreadRecursiveMutex() {
Attributes attrs;
attrs.setRecursive();
initialize(attrs);
}
PThreadRecursiveMutex(const PThreadRecursiveMutex&) = delete;
PThreadRecursiveMutex& operator=(const PThreadRecursiveMutex&) = delete;
PThreadRecursiveMutex(PThreadRecursiveMutex&&) = delete;
PThreadRecursiveMutex& operator=(PThreadRecursiveMutex&&) = delete;
};
#ifdef __APPLE__
bool isOSUnfairLockAvailable() {
// We're linking weakly, so if we're back-deploying, this will be null.
return os_unfair_lock_lock != nullptr;
}
template <>
class GenericMutex<os_unfair_lock> final : PlatformIndependentMutex {
class OS_UNFAIR_LOCK_AVAILABILITY OSUnfairLock final : PlatformIndependentMutex {
os_unfair_lock mutex_ = OS_UNFAIR_LOCK_INIT;
public:
GenericMutex() = default;
GenericMutex(const GenericMutex&) = delete;
GenericMutex& operator=(const GenericMutex&) = delete;
OSUnfairLock() = default;
OSUnfairLock(const OSUnfairLock&) = delete;
OSUnfairLock& operator=(const OSUnfairLock&) = delete;
OSUnfairLock(OSUnfairLock&&) = delete;
OSUnfairLock& operator=(OSUnfairLock&&) = delete;
void lock() override {
os_unfair_lock_lock(&mutex_);
@@ -65,6 +160,10 @@ public:
void unlock() override {
os_unfair_lock_unlock(&mutex_);
}
void assertOwner() override {
os_unfair_lock_assert_owner(&mutex_);
}
};
#endif // __APPLE__
@@ -80,13 +179,13 @@ OpenCombineUnfairLock opencombine_unfair_lock_alloc(void) {
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
#ifdef __APPLE__
if (isOSUnfairLockAvailable()) {
return {new GenericMutex<os_unfair_lock>};
if (__builtin_available(macOS 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *)) {
return {new OSUnfairLock};
} else {
return {new GenericMutex<std::mutex>};
return {new PThreadMutex};
}
#else
return {new GenericMutex<std::mutex>};
return {new PThreadMutex};
#endif
OPENCOMBINE_HANDLE_EXCEPTION_END
@@ -95,7 +194,7 @@ OpenCombineUnfairLock opencombine_unfair_lock_alloc(void) {
OpenCombineUnfairRecursiveLock opencombine_unfair_recursive_lock_alloc(void) {
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
// TODO: Use os_unfair_recursive_lock on Darwin as soon as it becomes public API.
return {new GenericMutex<std::recursive_mutex>};
return {new PThreadRecursiveMutex};
OPENCOMBINE_HANDLE_EXCEPTION_END
}
@@ -111,6 +210,12 @@ void opencombine_unfair_lock_unlock(OpenCombineUnfairLock mutex) {
OPENCOMBINE_HANDLE_EXCEPTION_END
}
void opencombine_unfair_lock_assert_owner(OpenCombineUnfairLock mutex) {
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
static_cast<PlatformIndependentMutex*>(mutex.opaque)->assertOwner();
OPENCOMBINE_HANDLE_EXCEPTION_END
}
void opencombine_unfair_recursive_lock_lock(OpenCombineUnfairRecursiveLock lock) {
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
static_cast<PlatformIndependentMutex*>(lock.opaque)->lock();
@@ -131,4 +236,8 @@ void opencombine_unfair_recursive_lock_dealloc(OpenCombineUnfairRecursiveLock lo
return delete static_cast<PlatformIndependentMutex*>(lock.opaque);
}
void opencombine_stop_in_debugger(void) {
raise(SIGTRAP);
}
} // extern "C"
@@ -23,47 +23,54 @@ extern "C" {
#pragma mark - CombineIdentifier
uint64_t opencombine_next_combine_identifier(void)
OPENCOMBINE_SWIFT_NAME(nextCombineIdentifier());
OPENCOMBINE_SWIFT_NAME(__nextCombineIdentifier());
#pragma mark - OpenCombineUnfairLock
/// A wrapper around an opaque pointer for type safety in Swift.
typedef struct OpenCombineUnfairLock {
void* _Nonnull opaque;
} OPENCOMBINE_SWIFT_NAME(UnfairLock) OpenCombineUnfairLock;
} OPENCOMBINE_SWIFT_NAME(__UnfairLock) OpenCombineUnfairLock;
/// Allocates a lock object. The allocated object must be destroyed by calling
/// the destroy() method.
OpenCombineUnfairLock opencombine_unfair_lock_alloc(void)
OPENCOMBINE_SWIFT_NAME(UnfairLock.allocate());
OPENCOMBINE_SWIFT_NAME(__UnfairLock.allocate());
void opencombine_unfair_lock_lock(OpenCombineUnfairLock)
OPENCOMBINE_SWIFT_NAME(UnfairLock.lock(self:));
OPENCOMBINE_SWIFT_NAME(__UnfairLock.lock(self:));
void opencombine_unfair_lock_unlock(OpenCombineUnfairLock)
OPENCOMBINE_SWIFT_NAME(UnfairLock.unlock(self:));
OPENCOMBINE_SWIFT_NAME(__UnfairLock.unlock(self:));
void opencombine_unfair_lock_assert_owner(OpenCombineUnfairLock mutex)
OPENCOMBINE_SWIFT_NAME(__UnfairLock.assertOwner(self:));
void opencombine_unfair_lock_dealloc(OpenCombineUnfairLock lock)
OPENCOMBINE_SWIFT_NAME(UnfairLock.deallocate(self:));
OPENCOMBINE_SWIFT_NAME(__UnfairLock.deallocate(self:));
#pragma mark - OpenCombineUnfairRecursiveLock
/// A wrapper around an opaque pointer for type safety in Swift.
typedef struct OpenCombineUnfairRecursiveLock {
void* _Nonnull opaque;
} OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock) OpenCombineUnfairRecursiveLock;
} OPENCOMBINE_SWIFT_NAME(__UnfairRecursiveLock) OpenCombineUnfairRecursiveLock;
OpenCombineUnfairRecursiveLock opencombine_unfair_recursive_lock_alloc(void)
OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock.allocate());
OPENCOMBINE_SWIFT_NAME(__UnfairRecursiveLock.allocate());
void opencombine_unfair_recursive_lock_lock(OpenCombineUnfairRecursiveLock)
OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock.lock(self:));
OPENCOMBINE_SWIFT_NAME(__UnfairRecursiveLock.lock(self:));
void opencombine_unfair_recursive_lock_unlock(OpenCombineUnfairRecursiveLock)
OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock.unlock(self:));
OPENCOMBINE_SWIFT_NAME(__UnfairRecursiveLock.unlock(self:));
void opencombine_unfair_recursive_lock_dealloc(OpenCombineUnfairRecursiveLock lock)
OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock.deallocate(self:));
OPENCOMBINE_SWIFT_NAME(__UnfairRecursiveLock.deallocate(self:));
#pragma mark - Breakpoint
void opencombine_stop_in_debugger(void) OPENCOMBINE_SWIFT_NAME(__stopInDebugger());
#ifdef __cplusplus
} // extern "C"
+1 -1
View File
@@ -57,7 +57,7 @@ extension AnyCancellable {
/// Stores this AnyCancellable in the specified set.
/// Parameters:
/// - collection: The set to store this AnyCancellable.
/// - set: The set to store this AnyCancellable.
public func store(in set: inout Set<AnyCancellable>) {
set.insert(self)
}
+1
View File
@@ -11,6 +11,7 @@ extension Publisher {
///
/// Use `eraseToAnyPublisher()` to expose an instance of `AnyPublisher` to
/// the downstream subscriber, rather than this publishers actual type.
@inlinable
public func eraseToAnyPublisher() -> AnyPublisher<Output, Failure> {
return .init(self)
}
+2 -2
View File
@@ -62,8 +62,8 @@ public struct AnySubscriber<Input, Failure: Error>: Subscriber,
if let playgroundDescription = subscriber as? CustomPlaygroundDisplayConvertible {
playgroundDescriptionThunk = { playgroundDescription.playgroundDescription }
} else if let desccription = subscriber as? CustomStringConvertible {
playgroundDescriptionThunk = { desccription.description }
} else if let description = subscriber as? CustomStringConvertible {
playgroundDescriptionThunk = { description.description }
} else {
let fixedDescription = String(describing: type(of: subscriber))
playgroundDescriptionThunk = { fixedDescription }
+1 -1
View File
@@ -28,7 +28,7 @@ extension Cancellable {
/// Stores this Cancellable in the specified set.
/// Parameters:
/// - collection: The set to store this Cancellable.
/// - set: The set to store this Cancellable.
public func store(in set: inout Set<AnyCancellable>) {
AnyCancellable(self).store(in: &set)
}
+7 -5
View File
@@ -5,21 +5,23 @@
// Created by Sergej Jaskiewicz on 10.06.2019.
//
import func COpenCombineHelpers.nextCombineIdentifier
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
public struct CombineIdentifier: Hashable, CustomStringConvertible {
private let id: UInt64
private let value: UInt64
public init() {
self.id = nextCombineIdentifier()
value = __nextCombineIdentifier()
}
public init(_ obj: AnyObject) {
id = UInt64(UInt(bitPattern: ObjectIdentifier(obj)))
value = UInt64(UInt(bitPattern: ObjectIdentifier(obj)))
}
public var description: String {
return "0x\(String(id, radix: 16))"
return "0x\(String(value, radix: 16))"
}
}
+179 -87
View File
@@ -5,32 +5,33 @@
// Created by Sergej Jaskiewicz on 11.06.2019.
//
import COpenCombineHelpers
/// A subject that wraps a single value and publishes a new element whenever the value
/// changes.
public final class CurrentValueSubject<Output, Failure: Error>: Subject {
private let _lock = UnfairRecursiveLock.allocate()
private let lock = UnfairLock.allocate()
// TODO: Combine uses bag data structure
private var _subscriptions: [Conduit] = []
private var active = true
private var _value: Output
private var completion: Subscribers.Completion<Failure>?
private var _completion: Subscribers.Completion<Failure>?
private var downstreams = ConduitList<Output, Failure>.empty
internal var upstreamSubscriptions: [Subscription] = []
private var currentValue: Output
internal var hasAnyDownstreamDemand = false
private var upstreamSubscriptions: [Subscription] = []
/// The value wrapped by this subject, published as a new element whenever it changes.
public var value: Output {
get {
return _value
lock.lock()
defer { lock.unlock() }
return currentValue
}
set {
send(newValue)
lock.lock()
currentValue = newValue
sendValueAndConsumeLock(newValue)
}
}
@@ -38,122 +39,213 @@ public final class CurrentValueSubject<Output, Failure: Error>: Subject {
///
/// - Parameter value: The initial value to publish.
public init(_ value: Output) {
self._value = value
self.currentValue = value
}
deinit {
for subscription in _subscriptions {
subscription._downstream = nil
for subscription in upstreamSubscriptions {
subscription.cancel()
}
_lock.deallocate()
lock.deallocate()
}
public func send(subscription: Subscription) {
_lock.do {
upstreamSubscriptions.append(subscription)
subscription.request(.unlimited)
}
lock.lock()
upstreamSubscriptions.append(subscription)
lock.unlock()
subscription.request(.unlimited)
}
public func receive<Subscriber: OpenCombine.Subscriber>(subscriber: Subscriber)
where Output == Subscriber.Input, Failure == Subscriber.Failure
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Output == Downstream.Input, Failure == Downstream.Failure
{
_lock.do {
if let completion = _completion {
subscriber.receive(subscription: Subscriptions.empty)
subscriber.receive(completion: completion)
return
} else {
let subscription = Conduit(parent: self,
downstream: AnySubscriber(subscriber))
_subscriptions.append(subscription)
subscriber.receive(subscription: subscription)
}
lock.lock()
if active {
let conduit = Conduit(parent: self, downstream: subscriber)
downstreams.insert(conduit)
lock.unlock()
subscriber.receive(subscription: conduit)
} else {
let completion = self.completion!
lock.unlock()
subscriber.receive(subscription: Subscriptions.empty)
subscriber.receive(completion: completion)
}
}
public func send(_ input: Output) {
_lock.do {
_value = input
for subscription in _subscriptions where !subscription.isCompleted {
if subscription._demand > 0 {
subscription._offer(input)
subscription._demand -= 1
} else {
subscription._delivered = false
}
}
lock.lock()
sendValueAndConsumeLock(input)
}
private func sendValueAndConsumeLock(_ newValue: Output) {
#if DEBUG
lock.assertOwner()
#endif
guard active else {
lock.unlock()
return
}
currentValue = newValue
let downstreams = self.downstreams
lock.unlock()
downstreams.forEach { conduit in
conduit.offer(newValue)
}
}
public func send(completion: Subscribers.Completion<Failure>) {
_completion = completion
_lock.do {
for subscriber in _subscriptions {
subscriber._receive(completion: completion)
}
lock.lock()
guard active else {
lock.unlock()
return
}
active = false
self.completion = completion
let downstreams = self.downstreams
self.downstreams.removeAll()
lock.unlock()
downstreams.forEach { conduit in
conduit.finish(completion: completion)
}
}
private func disassociate(_ conduit: ConduitBase<Output, Failure>) {
lock.lock()
guard active else {
lock.unlock()
return
}
downstreams.remove(conduit)
lock.unlock()
}
}
extension CurrentValueSubject {
fileprivate class Conduit: Subscription {
private final class Conduit<Downstream: Subscriber>
: ConduitBase<Output, Failure>,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Output, Downstream.Failure == Failure
{
fileprivate var _parent: CurrentValueSubject?
fileprivate var parent: CurrentValueSubject?
fileprivate var _downstream: AnySubscriber<Output, Failure>?
fileprivate var downstream: Downstream?
fileprivate var _demand: Subscribers.Demand = .none
fileprivate var demand = Subscribers.Demand.none
/// Whethere we satisfied the demand
fileprivate var _delivered = false
private var lock = UnfairLock.allocate()
var isCompleted: Bool {
return _parent == nil
}
private var downstreamLock = UnfairRecursiveLock.allocate()
fileprivate func _offer(_ value: Output) {
let newDemand = _downstream?.receive(value) ?? .none
_demand += newDemand
_delivered = true
}
private var deliveredCurrentValue = false
fileprivate init(parent: CurrentValueSubject,
downstream: AnySubscriber<Output, Failure>) {
_parent = parent
_downstream = downstream
downstream: Downstream) {
self.parent = parent
self.downstream = downstream
}
fileprivate func _receive(completion: Subscribers.Completion<Failure>) {
if !isCompleted {
_parent = nil
_downstream?.receive(completion: completion)
deinit {
lock.deallocate()
downstreamLock.deallocate()
}
override func offer(_ output: Output) {
lock.lock()
guard demand > 0, let downstream = self.downstream else {
deliveredCurrentValue = false
lock.unlock()
return
}
demand -= 1
deliveredCurrentValue = true
lock.unlock()
downstreamLock.lock()
let newDemand = downstream.receive(output)
downstreamLock.unlock()
guard newDemand > 0 else { return }
lock.lock()
demand += newDemand
lock.unlock()
}
func request(_ demand: Subscribers.Demand) {
precondition(demand > 0)
_parent?._lock.do {
if !_delivered, let value = _parent?.value {
_offer(value)
_demand += demand
_demand -= 1
} else {
_demand = demand
}
_parent?.hasAnyDownstreamDemand = true
override func finish(completion: Subscribers.Completion<Failure>) {
lock.lock()
guard let downstream = self.downstream else {
lock.unlock()
return
}
self.downstream = nil
let parent = self.parent
self.parent = nil
lock.unlock()
parent?.disassociate(self)
downstreamLock.lock()
downstream.receive(completion: completion)
downstreamLock.unlock()
}
func cancel() {
_parent = nil
override func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
lock.lock()
guard let downstream = self.downstream else {
lock.unlock()
return
}
if deliveredCurrentValue {
self.demand += demand
lock.unlock()
return
}
// Hasn't yet delivered the current value
self.demand += demand
deliveredCurrentValue = true
if let currentValue = self.parent?.value {
self.demand -= 1
lock.unlock()
downstreamLock.lock()
let newDemand = downstream.receive(currentValue)
downstreamLock.unlock()
guard newDemand > 0 else { return }
lock.lock()
self.demand += newDemand
}
lock.unlock()
}
override func cancel() {
lock.lock()
if self.downstream == nil {
lock.unlock()
return
}
self.downstream = nil
let parent = self.parent
self.parent = nil
lock.unlock()
parent?.disassociate(self)
}
var description: String { return "CurrentValueSubject" }
var customMirror: Mirror {
lock.lock()
defer { lock.unlock() }
let children: [Mirror.Child] = [
("parent", parent as Any),
("downstream", downstream as Any),
("demand", demand),
("subject", parent as Any)
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
}
}
extension CurrentValueSubject.Conduit: CustomStringConvertible {
fileprivate var description: String { return "CurrentValueSubject" }
}
+202
View File
@@ -0,0 +1,202 @@
//
// Future.swift
//
//
// Created by Max Desiatov on 24/11/2019.
//
/// A publisher that eventually produces one value and then finishes or fails.
public final class Future<Output, Failure: Error>: Publisher {
public typealias Promise = (Result<Output, Failure>) -> Void
private let lock = UnfairLock.allocate()
private var downstreams = ConduitList<Output, Failure>.empty
private var result: Result<Output, Failure>?
public init(
_ attemptToFulfill: @escaping (@escaping Promise) -> Void
) {
attemptToFulfill(self.promise)
}
deinit {
lock.deallocate()
}
private func promise(_ result: Result<Output, Failure>) {
lock.lock()
guard self.result == nil else {
lock.unlock()
return
}
self.result = result
let downstreams = self.downstreams
self.downstreams.removeAll()
lock.unlock()
switch result {
case .success(let output):
downstreams.forEach { $0.offer(output) }
case .failure(let error):
downstreams.forEach { $0.finish(completion: .failure(error)) }
}
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Output == Downstream.Input, Failure == Downstream.Failure
{
let conduit = Conduit(parent: self, downstream: subscriber)
lock.lock()
if let result = self.result {
downstreams.insert(conduit)
lock.unlock()
subscriber.receive(subscription: conduit)
conduit.fulfill(result)
} else {
downstreams.insert(conduit)
lock.unlock()
subscriber.receive(subscription: conduit)
}
}
private func disassociate(_ conduit: ConduitBase<Output, Failure>) {
lock.lock()
downstreams.remove(conduit)
lock.unlock()
}
}
extension Future {
private final class Conduit<Downstream: Subscriber>
: ConduitBase<Output, Failure>,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Output, Downstream.Failure == Failure
{
fileprivate var parent: Future?
fileprivate var downstream: Downstream?
fileprivate var hasAnyDemand = false
private var lock = UnfairLock.allocate()
private var downstreamLock = UnfairRecursiveLock.allocate()
fileprivate init(parent: Future, downstream: Downstream) {
self.parent = parent
self.downstream = downstream
}
deinit {
lock.deallocate()
downstreamLock.deallocate()
}
fileprivate func fulfill(_ result: Result<Output, Failure>) {
lock.lock()
guard let downstream = self.downstream else {
lock.unlock()
return
}
let parent = self.parent
if case .success = result, !hasAnyDemand {
lock.unlock()
return
}
self.downstream = nil
self.parent = nil
lock.unlock()
downstreamLock.lock()
switch result {
case .success(let output):
_ = downstream.receive(output)
downstream.receive(completion: .finished)
case .failure(let error):
downstream.receive(completion: .failure(error))
}
downstreamLock.unlock()
parent?.disassociate(self)
}
override func offer(_ output: Output) {
fulfill(.success(output))
}
override func finish(completion: Subscribers.Completion<Failure>) {
switch completion {
case .finished:
assertionFailure("unreachable")
case .failure(let error):
fulfill(.failure(error))
}
}
override func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
lock.lock()
guard let downstream = self.downstream, let parent = self.parent else {
lock.unlock()
return
}
hasAnyDemand = true
parent.lock.lock()
guard let result = parent.result else {
parent.lock.unlock()
lock.unlock()
return
}
parent.lock.unlock()
self.downstream = nil
self.parent = nil
lock.unlock()
downstreamLock.lock()
switch result {
case .success(let output):
_ = downstream.receive(output)
downstream.receive(completion: .finished)
case .failure(let error):
// This branch is not reachable under normal circumstances,
// but may be reachable in case of a race condition.
downstream.receive(completion: .failure(error))
}
downstreamLock.unlock()
parent.disassociate(self)
}
override func cancel() {
lock.lock()
if self.downstream == nil {
lock.unlock()
return
}
self.downstream = nil
let parent = self.parent
self.parent = nil
lock.unlock()
parent?.disassociate(self)
}
var description: String { return "Future" }
var customMirror: Mirror {
lock.lock()
defer { lock.unlock() }
let children: [Mirror.Child] = [
("parent", parent as Any),
("downstream", downstream as Any),
("hasAnyDemand", hasAnyDemand),
("subject", parent as Any)
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
}
}
@@ -0,0 +1,40 @@
//
// ConduitBase.swift
//
//
// Created by Sergej Jaskiewicz on 25.06.2020.
//
internal class ConduitBase<Output, Failure: Error>: Subscription {
internal init() {}
internal func offer(_ output: Output) {
abstractMethod()
}
internal func finish(completion: Subscribers.Completion<Failure>) {
abstractMethod()
}
internal func request(_ demand: Subscribers.Demand) {
abstractMethod()
}
internal func cancel() {
abstractMethod()
}
}
extension ConduitBase: Equatable {
internal static func == (lhs: ConduitBase<Output, Failure>,
rhs: ConduitBase<Output, Failure>) -> Bool {
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
}
}
extension ConduitBase: Hashable {
internal func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(self))
}
}
@@ -0,0 +1,57 @@
//
// ConduitList.swift
//
//
// Created by Sergej Jaskiewicz on 25.06.2020.
//
internal enum ConduitList<Output, Failure: Error> {
case empty
case single(ConduitBase<Output, Failure>)
case many(Set<ConduitBase<Output, Failure>>)
}
extension ConduitList {
internal mutating func insert(_ conduit: ConduitBase<Output, Failure>) {
switch self {
case .empty:
self = .single(conduit)
case .single(conduit):
break // This element already exists.
case .single(let existingConduit):
self = .many([existingConduit, conduit])
case .many(var set):
set.insert(conduit)
self = .many(set)
}
}
internal func forEach(
_ body: (ConduitBase<Output, Failure>) throws -> Void
) rethrows {
switch self {
case .empty:
break
case .single(let conduit):
try body(conduit)
case .many(let set):
try set.forEach(body)
}
}
internal mutating func remove(_ conduit: ConduitBase<Output, Failure>) {
switch self {
case .single(conduit):
self = .empty
case .empty, .single:
break
case .many(var set):
set.remove(conduit)
self = .many(set)
}
}
internal mutating func removeAll() {
self = .empty
}
}
@@ -0,0 +1,176 @@
//
// FilterProducer.swift
//
//
// Created by Sergej Jaskiewicz on 23.10.2019.
//
/// A helper class that acts like both subscriber and subscription.
///
/// Filter-like operators send an instance of their `Inner` class that is subclass
/// of this class to the upstream publisher (as subscriber) and
/// to the downstream subscriber (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 }
}
+4 -19
View File
@@ -5,24 +5,9 @@
// Created by Sergej Jaskiewicz on 11.06.2019.
//
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
extension UnfairLock {
@inlinable
internal func `do`<Result>(_ body: () throws -> Result) rethrows -> Result {
lock()
defer { unlock() }
return try body()
}
}
extension UnfairRecursiveLock {
@inlinable
internal func `do`<Result>(_ body: () throws -> Result) rethrows -> Result {
lock()
defer { unlock() }
return try body()
}
}
internal typealias UnfairLock = __UnfairLock
internal typealias UnfairRecursiveLock = __UnfairRecursiveLock
@@ -5,13 +5,11 @@
// Created by Sergej Jaskiewicz on 22.09.2019.
//
import COpenCombineHelpers
/// A helper class that acts like both subscriber and subscription.
///
/// Reduce-like operators send an instance of their `Inner` class that is subclass
/// of this class to the upstream publisher (as subscriber) and
/// to the downstream subcriber (as subsription).
/// to the downstream subscriber (as subscription).
///
/// Reduce-like operators include `Publishers.Reduce`, `Publishers.TryReduce`,
/// `Publishers.Count`, `Publishers.FirstWhere`, `Publishers.AllSatisfy` and more.
@@ -5,8 +5,6 @@
// Created by Sergej Jaskiewicz on 16/09/2019.
//
import COpenCombineHelpers
// NOTE: This class has been audited for thread safety.
internal final class SubjectSubscriber<Downstream: Subject>
: Subscriber,
@@ -42,23 +40,21 @@ internal final class SubjectSubscriber<Downstream: Subject>
internal func receive(_ input: Downstream.Output) -> Subscribers.Demand {
lock.lock()
guard let downstreamSubject = downstreamSubject else {
guard let subject = downstreamSubject, upstreamSubscription != nil else {
lock.unlock()
return .none
}
guard upstreamSubscription != nil else { APIViolationValueBeforeSubscription() }
lock.unlock()
downstreamSubject.send(input)
subject.send(input)
return .none
}
internal func receive(completion: Subscribers.Completion<Downstream.Failure>) {
lock.lock()
guard let subject = downstreamSubject else {
guard let subject = downstreamSubject, upstreamSubscription != nil else {
lock.unlock()
return
}
guard upstreamSubscription != nil else { APIViolationUnexpectedCompletion() }
lock.unlock()
subject.send(completion: completion)
downstreamSubject = nil
@@ -89,11 +85,7 @@ internal final class SubjectSubscriber<Downstream: Subject>
internal func cancel() {
lock.lock()
if isCancelled {
lock.unlock()
return
}
guard let subscription = upstreamSubscription else {
guard !isCancelled, let subscription = upstreamSubscription else {
lock.unlock()
return
}
@@ -10,3 +10,14 @@ internal enum SubscriptionStatus {
case subscribed(Subscription)
case terminal
}
extension SubscriptionStatus {
internal var isAwaitingSubscription: Bool {
switch self {
case .awaitingSubscription:
return true
default:
return false
}
}
}
+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.
+192
View File
@@ -0,0 +1,192 @@
//
// ObservableObject.swift
//
//
// Created by Sergej Jaskiewicz on 08/09/2019.
//
/// A type of object with a publisher that emits before the object has changed.
///
/// By default an `ObservableObject` will synthesize an `objectWillChange`
/// publisher that emits before any of its `@Published` properties changes:
///
/// class Contact : ObservableObject {
/// @Published var name: String
/// @Published var age: Int
///
/// init(name: String, age: Int) {
/// self.name = name
/// self.age = age
/// }
///
/// func haveBirthday() -> Int {
/// age += 1
/// }
/// }
///
/// let john = Contact(name: "John Appleseed", age: 24)
/// john.objectWillChange.sink { _ in print("will change") }
/// print(john.haveBirthday)
/// // Prints "will change"
/// // Prints "25"
///
public protocol ObservableObject: AnyObject {
/// The type of publisher that emits before the object has changed.
associatedtype ObjectWillChangePublisher: Publisher = ObservableObjectPublisher
where ObjectWillChangePublisher.Failure == Never
/// A publisher that emits before the object has changed.
var objectWillChange: ObjectWillChangePublisher { get }
}
extension ObservableObject where ObjectWillChangePublisher == ObservableObjectPublisher {
// swiftlint:disable let_var_whitespace
#if swift(>=5.1)
/// A publisher that emits before the object has changed.
@available(*, unavailable, message: """
The default implementation of objectWillChange is not available yet. \
It's being worked on in \
https://github.com/broadwaylamb/OpenCombine/pull/97
""")
public var objectWillChange: ObservableObjectPublisher {
fatalError("unimplemented")
}
#else
public var objectWillChange: ObservableObjectPublisher {
return ObservableObjectPublisher()
}
#endif
// swiftlint:enable let_var_whitespace
}
/// The default publisher of an `ObservableObject`.
public final class ObservableObjectPublisher: Publisher {
public typealias Output = Void
public typealias Failure = Never
private let lock = UnfairLock.allocate()
private var connections = Set<Conduit>()
// TODO: Combine needs this for some reason
private var identifier: ObjectIdentifier?
public init() {}
deinit {
lock.deallocate()
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == Void, Downstream.Failure == Never
{
let inner = Inner(downstream: subscriber, parent: self)
lock.lock()
connections.insert(inner)
lock.unlock()
subscriber.receive(subscription: inner)
}
public func send() {
lock.lock()
let connections = self.connections
lock.unlock()
for connection in connections {
connection.send()
}
}
private func remove(_ conduit: Conduit) {
lock.lock()
connections.remove(conduit)
lock.unlock()
}
}
extension ObservableObjectPublisher {
private class Conduit: Hashable {
fileprivate func send() {
abstractMethod()
}
fileprivate static func == (lhs: Conduit, rhs: Conduit) -> Bool {
return lhs === rhs
}
fileprivate func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(self))
}
}
private final class Inner<Downstream: Subscriber>
: Conduit,
Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Void, Downstream.Failure == Never
{
private enum State {
case initialized
case active
case terminal
}
private weak var parent: ObservableObjectPublisher?
private let downstream: Downstream
private let downstreamLock = UnfairRecursiveLock.allocate()
private let lock = UnfairLock.allocate()
private var state = State.initialized
init(downstream: Downstream, parent: ObservableObjectPublisher) {
self.parent = parent
self.downstream = downstream
}
deinit {
downstreamLock.deallocate()
lock.deallocate()
}
override func send() {
lock.lock()
let state = self.state
lock.unlock()
if state == .active {
downstreamLock.lock()
_ = downstream.receive()
downstreamLock.unlock()
}
}
func request(_ demand: Subscribers.Demand) {
lock.lock()
if state == .initialized {
state = .active
}
lock.unlock()
}
func cancel() {
lock.lock()
state = .terminal
lock.unlock()
parent?.remove(self)
}
var description: String { return "ObservableObjectPublisher" }
var customMirror: Mirror {
let children = CollectionOfOne<Mirror.Child>(("downstream", downstream))
return Mirror(self, children: children)
}
var playgroundDescription: Any {
return description
}
}
}
+156 -72
View File
@@ -5,20 +5,19 @@
// Created by Sergej Jaskiewicz on 11.06.2019.
//
import COpenCombineHelpers
/// A subject that passes along values and completion.
///
/// Use a `PassthroughSubject` in unit tests when you want a publisher than can publish
/// specific values on-demand during tests.
public final class PassthroughSubject<Output, Failure: Error>: Subject {
public final class PassthroughSubject<Output, Failure: Error>: Subject {
private let _lock = UnfairRecursiveLock.allocate()
private let lock = UnfairLock.allocate()
private var _completion: Subscribers.Completion<Failure>?
private var active = true
// TODO: Combine uses bag data structure
private var _subscriptions: [Conduit] = []
private var completion: Subscribers.Completion<Failure>?
private var downstreams = ConduitList<Output, Failure>.empty
internal var upstreamSubscriptions: [Subscription] = []
@@ -27,112 +26,197 @@ public final class PassthroughSubject<Output, Failure: Error>: Subject {
public init() {}
deinit {
for subscription in _subscriptions {
subscription._downstream = nil
for subscription in upstreamSubscriptions {
subscription.cancel()
}
_lock.deallocate()
lock.deallocate()
}
public func send(subscription: Subscription) {
_lock.do {
upstreamSubscriptions.append(subscription)
if hasAnyDownstreamDemand {
subscription.request(.unlimited)
}
lock.lock()
upstreamSubscriptions.append(subscription)
let hasAnyDownstreamDemand = self.hasAnyDownstreamDemand
lock.unlock()
if hasAnyDownstreamDemand {
subscription.request(.unlimited)
}
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Output == Downstream.Input, Failure == Downstream.Failure
{
_lock.do {
if let completion = _completion {
subscriber.receive(subscription: Subscriptions.empty)
subscriber.receive(completion: completion)
return
} else {
let subscription = Conduit(parent: self,
downstream: AnySubscriber(subscriber))
_subscriptions.append(subscription)
subscriber.receive(subscription: subscription)
}
lock.lock()
if active {
let conduit = Conduit(parent: self, downstream: subscriber)
downstreams.insert(conduit)
lock.unlock()
subscriber.receive(subscription: conduit)
} else {
let completion = self.completion!
lock.unlock()
subscriber.receive(subscription: Subscriptions.empty)
subscriber.receive(completion: completion)
}
}
public func send(_ input: Output) {
_lock.do {
for subscription in _subscriptions
where !subscription._isCompleted && subscription._demand > 0
{
let newDemand = subscription._downstream?.receive(input) ?? .none
subscription._demand += newDemand
subscription._demand -= 1
}
lock.lock()
guard active else {
lock.unlock()
return
}
let downstreams = self.downstreams
lock.unlock()
downstreams.forEach { conduit in
conduit.offer(input)
}
}
public func send(completion: Subscribers.Completion<Failure>) {
_lock.do {
_completion = completion
for subscriber in _subscriptions {
subscriber._receive(completion: completion)
}
lock.lock()
guard active else {
lock.unlock()
return
}
active = false
self.completion = completion
let downstreams = self.downstreams
self.downstreams.removeAll()
lock.unlock()
downstreams.forEach { conduit in
conduit.finish(completion: completion)
}
}
private func _acknowledgeDownstreamDemand() {
_lock.do {
guard !hasAnyDownstreamDemand else { return }
hasAnyDownstreamDemand = true
for subscription in upstreamSubscriptions {
subscription.request(.unlimited)
}
private func acknowledgeDownstreamDemand() {
lock.lock()
if hasAnyDownstreamDemand {
lock.unlock()
return
}
hasAnyDownstreamDemand = true
let upstreamSubscriptions = self.upstreamSubscriptions
lock.unlock()
for subscription in upstreamSubscriptions {
subscription.request(.unlimited)
}
}
private func disassociate(_ conduit: ConduitBase<Output, Failure>) {
lock.lock()
guard active else {
lock.unlock()
return
}
downstreams.remove(conduit)
lock.unlock()
}
}
extension PassthroughSubject {
fileprivate final class Conduit: Subscription {
private final class Conduit<Downstream: Subscriber>
: ConduitBase<Output, Failure>,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Output, Downstream.Failure == Failure
{
fileprivate var _parent: PassthroughSubject?
fileprivate var parent: PassthroughSubject?
fileprivate var _downstream: AnySubscriber<Output, Failure>?
fileprivate var downstream: Downstream?
fileprivate var _demand: Subscribers.Demand = .none
fileprivate var demand = Subscribers.Demand.none
fileprivate var _isCompleted: Bool {
return _parent == nil
}
private var lock = UnfairLock.allocate()
private var downstreamLock = UnfairRecursiveLock.allocate()
fileprivate init(parent: PassthroughSubject,
downstream: AnySubscriber<Output, Failure>) {
_parent = parent
_downstream = downstream
downstream: Downstream) {
self.parent = parent
self.downstream = downstream
}
fileprivate func _receive(completion: Subscribers.Completion<Failure>) {
if !_isCompleted {
_parent = nil
_downstream?.receive(completion: completion)
deinit {
lock.deallocate()
downstreamLock.deallocate()
}
override func offer(_ output: Output) {
lock.lock()
guard demand > 0, let downstream = self.downstream else {
lock.unlock()
return
}
demand -= 1
lock.unlock()
downstreamLock.lock()
let newDemand = downstream.receive(output)
downstreamLock.unlock()
guard newDemand > 0 else { return }
lock.lock()
demand += newDemand
lock.unlock()
}
fileprivate func request(_ demand: Subscribers.Demand) {
precondition(demand > 0, "demand must not be zero")
_parent?._lock.do {
_demand += demand
override func finish(completion: Subscribers.Completion<Failure>) {
lock.lock()
guard let downstream = self.downstream else {
lock.unlock()
return
}
_parent?._acknowledgeDownstreamDemand()
self.downstream = nil
let parent = self.parent
self.parent = nil
lock.unlock()
parent?.disassociate(self)
downstreamLock.lock()
downstream.receive(completion: completion)
downstreamLock.unlock()
}
fileprivate func cancel() {
_parent = nil
override func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
lock.lock()
if self.downstream == nil {
lock.unlock()
return
}
self.demand += demand
let parent = self.parent
lock.unlock()
parent?.acknowledgeDownstreamDemand()
}
override func cancel() {
lock.lock()
if self.downstream == nil {
lock.unlock()
return
}
self.downstream = nil
let parent = self.parent
self.parent = nil
lock.unlock()
parent?.disassociate(self)
}
var description: String { return "PassthroughSubject" }
var customMirror: Mirror {
lock.lock()
defer { lock.unlock() }
let children: [Mirror.Child] = [
("parent", parent as Any),
("downstream", downstream as Any),
("demand", demand),
("subject", parent as Any)
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
}
}
extension PassthroughSubject.Conduit: CustomStringConvertible {
fileprivate var description: String { return "PassthroughSubject" }
}
+35 -33
View File
@@ -14,15 +14,17 @@
/// of the property first.
/// Note that the `@Published` property is class-constrained.
/// Use it with properties of classes, not with non-class types like structures.
@propertyWrapper public struct Published<Value> {
@available(swift, introduced: 5.1)
@propertyWrapper
public struct Published<Value> {
/// Initialize the storage of the Published
/// property as well as the corresponding `Publisher`.
@inlinable // trivially forwarding
public init(initialValue: Value) {
value = initialValue
self.init(wrappedValue: initialValue)
}
@available(*, unavailable)
/// Initialize the storage of the `Published` property as well as the corresponding
/// `Publisher`.
public init(wrappedValue: Value) {
value = wrappedValue
}
@@ -30,21 +32,10 @@
/// A publisher for properties marked with the `@Published` attribute.
public struct Publisher: OpenCombine.Publisher {
/// The kind of values published by this publisher.
public typealias Output = Value
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = Never
/// This function is called to attach the specified
/// `Subscriber` to this `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == Value, Downstream.Failure == Never
{
@@ -60,8 +51,12 @@
private var value: Value
/// The property that can be accessed with the
/// `$` syntax and allows access to the `Publisher`
private var publisher: Publisher?
internal var objectWillChange: ObservableObjectPublisher?
/// The property that can be accessed with the `$` syntax and allows access to
/// the `Publisher`
public var projectedValue: Publisher {
mutating get {
if let publisher = publisher {
@@ -73,28 +68,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,620 @@
//
//
// Auto-generated from GYB template. DO NOT EDIT!
//
//
//
//
// Publishers.Catch.swift
//
//
// Created by Sergej Jaskiewicz on 25.12.2019.
//
extension Publisher {
/// Handles errors from an upstream publisher by replacing it with another publisher.
///
/// The following example replaces any error from the upstream publisher and replaces
/// the upstream with a `Just` publisher. This continues the stream by publishing
/// a single value and completing normally.
/// ```
/// enum SimpleError: Error { case error }
/// let errorPublisher = (0..<10).publisher.tryMap { v -> Int in
/// if v < 5 {
/// return v
/// } else {
/// throw SimpleError.error
/// }
/// }
///
/// let noErrorPublisher = errorPublisher.catch { _ in
/// return Just(100)
/// }
/// ```
/// Backpressure note: This publisher passes through `request` and `cancel` to
/// the upstream. After receiving an error, the publisher sends sends any unfulfilled
/// demand to the new `Publisher`.
///
/// - Parameter handler: A closure that accepts the upstream failure as input and
/// returns a publisher to replace the upstream publisher.
/// - Returns: A publisher that handles errors from an upstream publisher by replacing
/// the failed publisher with another publisher.
public func `catch`<NewPublisher: Publisher>(
_ handler: @escaping (Failure) -> NewPublisher
) -> Publishers.Catch<Self, NewPublisher>
where NewPublisher.Output == Output
{
return .init(upstream: self, handler: handler)
}
/// Handles errors from an upstream publisher by either replacing it with another
/// publisher or `throw`ing a new error.
///
/// - Parameter handler: A `throw`ing closure that accepts the upstream failure as
/// input and returns a publisher to replace the upstream publisher or if an error
/// is thrown will send the error downstream.
/// - Returns: A publisher that handles errors from an upstream publisher by replacing
/// the failed publisher with another publisher.
public func tryCatch<NewPublisher: Publisher>(
_ handler: @escaping (Failure) throws -> NewPublisher
) -> Publishers.TryCatch<Self, NewPublisher>
where NewPublisher.Output == Output
{
return .init(upstream: self, handler: handler)
}
}
extension Publishers {
/// A publisher that handles errors from an upstream publisher by replacing the failed
/// publisher with another publisher.
public struct Catch<Upstream: Publisher, NewPublisher: Publisher>: Publisher
where Upstream.Output == NewPublisher.Output
{
public typealias Output = Upstream.Output
public typealias Failure = NewPublisher.Failure
/// The publisher that this publisher receives elements from.
public let upstream: Upstream
/// A closure that accepts the upstream failure as input and returns a publisher
/// to replace the upstream publisher.
public let handler: (Upstream.Failure) -> NewPublisher
/// Creates a publisher that handles errors from an upstream publisher by
/// replacing the failed publisher with another publisher.
///
/// - Parameters:
/// - upstream: The publisher that this publisher receives elements from.
/// - handler: A closure that accepts the upstream failure as input and returns
/// a publisher to replace the upstream publisher.
public init(upstream: Upstream,
handler: @escaping (Upstream.Failure) -> NewPublisher) {
self.upstream = upstream
self.handler = handler
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == Output, Downstream.Failure == Failure
{
let inner = Inner(downstream: subscriber, handler: handler)
let uncaughtS = Inner.UncaughtS(inner: inner)
upstream.subscribe(uncaughtS)
}
}
/// A publisher that handles errors from an upstream publisher by replacing the failed
/// publisher with another publisher or optionally producing a new error.
public struct TryCatch<Upstream: Publisher, NewPublisher: Publisher>: Publisher
where Upstream.Output == NewPublisher.Output
{
public typealias Output = Upstream.Output
public typealias Failure = Error
public let upstream: Upstream
public let handler: (Upstream.Failure) throws -> NewPublisher
public init(upstream: Upstream,
handler: @escaping (Upstream.Failure) throws -> NewPublisher) {
self.upstream = upstream
self.handler = handler
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == Output, Downstream.Failure == Failure
{
let inner = Inner(downstream: subscriber, handler: handler)
let uncaughtS = Inner.UncaughtS(inner: inner)
upstream.subscribe(uncaughtS)
}
}
}
extension Publishers.Catch {
private final class Inner<Downstream: Subscriber>
: Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Upstream.Output,
Downstream.Failure == NewPublisher.Failure
{
struct UncaughtS: Subscriber,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
let inner: Inner
var combineIdentifier: CombineIdentifier { return inner.combineIdentifier }
func receive(subscription: Subscription) {
inner.receivePre(subscription: subscription)
}
func receive(_ input: Input) -> Subscribers.Demand {
return inner.receivePre(input)
}
func receive(completion: Subscribers.Completion<Failure>) {
return inner.receivePre(completion: completion)
}
var description: String { return inner.description }
var customMirror: Mirror { return inner.customMirror }
var playgroundDescription: Any { return description }
}
struct CaughtS: Subscriber,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
{
typealias Input = NewPublisher.Output
typealias Failure = NewPublisher.Failure
let inner: Inner
var combineIdentifier: CombineIdentifier { return inner.combineIdentifier }
func receive(subscription: Subscription) {
inner.receivePost(subscription: subscription)
}
func receive(_ input: Input) -> Subscribers.Demand {
return inner.receivePost(input)
}
func receive(completion: Subscribers.Completion<Failure>) {
inner.receivePost(completion: completion)
}
var description: String { return inner.description }
var customMirror: Mirror { return inner.customMirror }
var playgroundDescription: Any { return description }
}
private enum State {
case pendingPre
case pre(Subscription)
case pendingPost
case post(Subscription)
case cancelled
}
private let lock = UnfairLock.allocate()
private var demand = Subscribers.Demand.none
private var state = State.pendingPre
private let downstream: Downstream
private let handler: (Upstream.Failure) -> NewPublisher
init(downstream: Downstream,
handler: @escaping (Upstream.Failure) -> NewPublisher) {
self.downstream = downstream
self.handler = handler
}
deinit {
lock.deallocate()
}
func receivePre(subscription: Subscription) {
lock.lock()
guard case .pendingPre = state else {
lock.unlock()
subscription.cancel()
return
}
state = .pre(subscription)
lock.unlock()
downstream.receive(subscription: self)
}
func receivePre(_ input: Upstream.Output) -> Subscribers.Demand {
lock.lock()
demand -= 1
lock.unlock()
let newDemand = downstream.receive(input)
lock.lock()
demand += newDemand
lock.unlock()
return newDemand
}
func receivePre(completion: Subscribers.Completion<Upstream.Failure>) {
switch completion {
case .finished:
lock.lock()
switch state {
case .pre:
state = .cancelled
lock.unlock()
downstream.receive(completion: .finished)
case .pendingPre, .pendingPost, .post, .cancelled:
lock.unlock()
}
case .failure(let error):
lock.lock()
switch state {
case .pre:
state = .pendingPost
lock.unlock()
handler(error).subscribe(CaughtS(inner: self))
case .cancelled:
lock.unlock()
case .pendingPre, .post, .pendingPost:
completionBeforeSubscription()
}
}
}
func receivePost(subscription: Subscription) {
lock.lock()
guard case .pendingPost = state else {
lock.unlock()
subscription.cancel()
return
}
state = .post(subscription)
let demand = self.demand
lock.unlock()
if demand > 0 {
subscription.request(demand)
}
}
func receivePost(_ input: NewPublisher.Output) -> Subscribers.Demand {
return downstream.receive(input)
}
func receivePost(completion: Subscribers.Completion<NewPublisher.Failure>) {
lock.lock()
guard case .post = state else {
lock.unlock()
return
}
state = .cancelled
lock.unlock()
downstream.receive(completion: completion)
}
func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
lock.lock()
switch state {
case .pendingPre:
// The client is only able to call the `request` method after we've sent
// `self` downstream. We only do it in the `receivePre(subscription:)`
// method, after setting `state` to `pre`.
// After that `state` never becomes `pendingPre`.
requestBeforeSubscription()
case let .pre(subscription):
self.demand += demand
lock.unlock()
subscription.request(demand)
case .pendingPost:
self.demand += demand
lock.unlock()
case let .post(subscription):
lock.unlock()
subscription.request(demand)
case .cancelled:
lock.unlock()
}
}
func cancel() {
lock.lock()
switch state {
case let .pre(subscription), let .post(subscription):
state = .cancelled
lock.unlock()
subscription.cancel()
case .pendingPre, .pendingPost, .cancelled:
lock.unlock()
}
}
var description: String { return "Catch" }
var customMirror: Mirror {
let children: [Mirror.Child] = [
("downstream", downstream),
("demand", demand)
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
}
}
extension Publishers.TryCatch {
private final class Inner<Downstream: Subscriber>
: Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Upstream.Output,
Downstream.Failure == Error
{
struct UncaughtS: Subscriber,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
let inner: Inner
var combineIdentifier: CombineIdentifier { return inner.combineIdentifier }
func receive(subscription: Subscription) {
inner.receivePre(subscription: subscription)
}
func receive(_ input: Input) -> Subscribers.Demand {
return inner.receivePre(input)
}
func receive(completion: Subscribers.Completion<Failure>) {
return inner.receivePre(completion: completion)
}
var description: String { return inner.description }
var customMirror: Mirror { return inner.customMirror }
var playgroundDescription: Any { return description }
}
struct CaughtS: Subscriber,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
{
typealias Input = NewPublisher.Output
typealias Failure = NewPublisher.Failure
let inner: Inner
var combineIdentifier: CombineIdentifier { return inner.combineIdentifier }
func receive(subscription: Subscription) {
inner.receivePost(subscription: subscription)
}
func receive(_ input: Input) -> Subscribers.Demand {
return inner.receivePost(input)
}
func receive(completion: Subscribers.Completion<Failure>) {
inner.receivePost(completion: completion)
}
var description: String { return inner.description }
var customMirror: Mirror { return inner.customMirror }
var playgroundDescription: Any { return description }
}
private enum State {
case pendingPre
case pre(Subscription)
case pendingPost
case post(Subscription)
case cancelled
}
private let lock = UnfairLock.allocate()
private var demand = Subscribers.Demand.none
private var state = State.pendingPre
private let downstream: Downstream
private let handler: (Upstream.Failure) throws -> NewPublisher
init(downstream: Downstream,
handler: @escaping (Upstream.Failure) throws -> NewPublisher) {
self.downstream = downstream
self.handler = handler
}
deinit {
lock.deallocate()
}
func receivePre(subscription: Subscription) {
lock.lock()
guard case .pendingPre = state else {
lock.unlock()
subscription.cancel()
return
}
state = .pre(subscription)
lock.unlock()
downstream.receive(subscription: self)
}
func receivePre(_ input: Upstream.Output) -> Subscribers.Demand {
lock.lock()
demand -= 1
lock.unlock()
let newDemand = downstream.receive(input)
lock.lock()
demand += newDemand
lock.unlock()
return newDemand
}
func receivePre(completion: Subscribers.Completion<Upstream.Failure>) {
switch completion {
case .finished:
lock.lock()
switch state {
case .pre:
state = .cancelled
lock.unlock()
downstream.receive(completion: .finished)
case .pendingPre, .pendingPost, .post, .cancelled:
lock.unlock()
}
case .failure(let error):
lock.lock()
switch state {
case .pre:
state = .pendingPost
lock.unlock()
do {
try handler(error).subscribe(CaughtS(inner: self))
} catch let anotherError {
lock.lock()
state = .cancelled
lock.unlock()
downstream.receive(completion: .failure(anotherError))
}
case .cancelled:
lock.unlock()
case .pendingPre, .post, .pendingPost:
completionBeforeSubscription()
}
}
}
func receivePost(subscription: Subscription) {
lock.lock()
guard case .pendingPost = state else {
lock.unlock()
subscription.cancel()
return
}
state = .post(subscription)
let demand = self.demand
lock.unlock()
if demand > 0 {
subscription.request(demand)
}
}
func receivePost(_ input: NewPublisher.Output) -> Subscribers.Demand {
return downstream.receive(input)
}
func receivePost(completion: Subscribers.Completion<NewPublisher.Failure>) {
lock.lock()
guard case .post = state else {
lock.unlock()
return
}
state = .cancelled
lock.unlock()
downstream.receive(completion: completion.eraseError())
}
func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
lock.lock()
switch state {
case .pendingPre:
// The client is only able to call the `request` method after we've sent
// `self` downstream. We only do it in the `receivePre(subscription:)`
// method, after setting `state` to `pre`.
// After that `state` never becomes `pendingPre`.
requestBeforeSubscription()
case let .pre(subscription):
self.demand += demand
lock.unlock()
subscription.request(demand)
case .pendingPost:
self.demand += demand
lock.unlock()
case let .post(subscription):
lock.unlock()
subscription.request(demand)
case .cancelled:
lock.unlock()
}
}
func cancel() {
lock.lock()
switch state {
case let .pre(subscription), let .post(subscription):
state = .cancelled
lock.unlock()
subscription.cancel()
case .pendingPre, .pendingPost, .cancelled:
lock.unlock()
}
}
var description: String { return "TryCatch" }
var customMirror: Mirror {
let children: [Mirror.Child] = [
("downstream", downstream),
("demand", demand)
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
}
}
private func completionBeforeSubscription(file: StaticString = #file,
line: UInt = #line) -> Never {
fatalError("Unexpected state: received completion but do not have subscription",
file: file,
line: line)
}
private func requestBeforeSubscription(file: StaticString = #file,
line: UInt = #line) -> Never {
fatalError("Unexpected state: request before subscription sent",
file: file,
line: line)
}
@@ -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 }
}
}
+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
}
}
@@ -235,11 +235,13 @@ extension Optional.OCombine.Publisher {
in range: RangeExpression
) -> Optional<Output>.OCombine.Publisher where RangeExpression.Bound == Int {
let range = range.relative(to: 0 ..< Int.max)
precondition(range.lowerBound >= 0, "lowerBould must not be negative")
precondition(range.lowerBound >= 0, "lowerBound must not be negative")
// I don't know why, but Combine has this precondition
precondition(range.upperBound < .max - 1)
return .init(output.flatMap { range.contains(0) ? $0 : nil })
return .init(
output.flatMap { (range.lowerBound == 0 && range.upperBound != 0) ? $0 : nil }
)
}
public func prefix(_ maxLength: Int) -> Optional<Output>.OCombine.Publisher {
@@ -0,0 +1,132 @@
//
// Publishers.AssertNoFailure.swift
//
//
// Created by Sergej Jaskiewicz on 25.12.2019.
//
extension Publisher {
/// Raises a fatal error when its upstream publisher fails, and otherwise republishes
/// all received input.
///
/// Use this function for internal sanity checks that are active during testing but
/// do not impact performance of shipping code.
///
/// - Parameters:
/// - prefix: A string used at the beginning of the fatal error message.
/// - file: A filename used in the error message. This defaults to `#file`.
/// - line: A line number used in the error message. This defaults to `#line`.
/// - Returns: A publisher that raises a fatal error when its upstream publisher
/// fails.
public func assertNoFailure(_ prefix: String = "",
file: StaticString = #file,
line: UInt = #line) -> Publishers.AssertNoFailure<Self> {
return .init(upstream: self, prefix: prefix, file: file, line: line)
}
}
extension Publishers {
/// A publisher that raises a fatal error upon receiving any failure, and otherwise
/// republishes all received input.
///
/// Use this function for internal sanity checks that are active during testing but
/// do not impact performance of shipping code.
public struct AssertNoFailure<Upstream: Publisher>: Publisher {
public typealias Output = Upstream.Output
public typealias Failure = Never
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// The string used at the beginning of the fatal error message.
public let prefix: String
/// The filename used in the error message.
public let file: StaticString
/// The line number used in the error message.
public let line: UInt
public init(upstream: Upstream, prefix: String, file: StaticString, line: UInt) {
self.upstream = upstream
self.prefix = prefix
self.file = file
self.line = line
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == Output, Downstream.Failure == Never
{
upstream.subscribe(Inner(downstream: subscriber,
prefix: prefix,
file: file,
line: line))
}
}
}
extension Publishers.AssertNoFailure {
private struct Inner<Downstream: Subscriber>
: Subscriber,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Upstream.Output, Downstream.Failure == Never
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private let downstream: Downstream
private let prefix: String
private let file: StaticString
private let line: UInt
let combineIdentifier = CombineIdentifier()
init(downstream: Downstream, prefix: String, file: StaticString, line: UInt) {
self.downstream = downstream
self.prefix = prefix
self.file = file
self.line = line
}
func receive(subscription: Subscription) {
downstream.receive(subscription: subscription)
}
func receive(_ input: Input) -> Subscribers.Demand {
return downstream.receive(input)
}
func receive(completion: Subscribers.Completion<Failure>) {
switch completion {
case .finished:
downstream.receive(completion: .finished)
case .failure(let error):
let prefix = self.prefix.isEmpty ? "" : self.prefix + ": "
fatalError("\(prefix)\(error)", file: file, line: line)
}
}
var description: String { return "AssertNoFailure" }
var customMirror: Mirror {
let children: [Mirror.Child] = [
("file", file),
("line", line),
("prefix", prefix)
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
}
}
@@ -5,8 +5,6 @@
// Created by Sergej Jaskiewicz on 18/09/2019.
//
import COpenCombineHelpers
extension ConnectablePublisher {
/// Automates the process of connecting or disconnecting from this connectable
@@ -0,0 +1,183 @@
//
// Publishers.Breakpoint.swift
//
//
// Created by Sergej Jaskiewicz on 03.12.2019.
//
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
extension Publisher {
/// Raises a debugger signal when a provided closure needs to stop the process in
/// the debugger.
///
/// When any of the provided closures returns `true`, this publisher raises
/// the `SIGTRAP` signal to stop the process in the debugger.
/// Otherwise, this publisher passes through values and completions as-is.
///
/// - Parameters:
/// - receiveSubscription: A closure that executes when when the publisher receives
/// a subscription. Return `true` from this closure to raise `SIGTRAP`, or `false`
/// to continue.
/// - receiveOutput: A closure that executes when when the publisher receives
/// a value. Return `true` from this closure to raise `SIGTRAP`, or `false`
/// to continue.
/// - receiveCompletion: A closure that executes when when the publisher receives
/// a completion. Return `true` from this closure to raise `SIGTRAP`, or `false`
/// to continue.
/// - Returns: A publisher that raises a debugger signal when one of the provided
/// closures returns `true`.
public func breakpoint(
receiveSubscription: ((Subscription) -> Bool)? = nil,
receiveOutput: ((Output) -> Bool)? = nil,
receiveCompletion: ((Subscribers.Completion<Failure>) -> Bool)? = nil
) -> Publishers.Breakpoint<Self> {
return .init(upstream: self,
receiveSubscription: receiveSubscription,
receiveOutput: receiveOutput,
receiveCompletion: receiveCompletion)
}
/// Raises a debugger signal upon receiving a failure.
///
/// When the upstream publisher fails with an error, this publisher raises
/// the `SIGTRAP` signal, which stops the process in the debugger.
/// Otherwise, this publisher passes through values and completions as-is.
///
/// - Returns: A publisher that raises a debugger signal upon receiving a failure.
public func breakpointOnError() -> Publishers.Breakpoint<Self> {
return breakpoint { completion in
switch completion {
case .finished:
return false
case .failure:
return true
}
}
}
}
extension Publishers {
/// A publisher that raises a debugger signal when a provided closure needs to stop
/// the process in the debugger.
///
/// When any of the provided closures returns `true`, this publisher raises
/// the `SIGTRAP` signal to stop the process in the debugger.
/// Otherwise, this publisher passes through values and completions as-is.
public struct Breakpoint<Upstream: Publisher>: Publisher {
public typealias Output = Upstream.Output
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// A closure that executes when the publisher receives a subscription, and can
/// raise a debugger signal by returning a `true` Boolean value.
public let receiveSubscription: ((Subscription) -> Bool)?
/// A closure that executes when the publisher receives output from the upstream
/// publisher, and can raise a debugger signal by returning a `true` Boolean
/// value.
public let receiveOutput: ((Upstream.Output) -> Bool)?
/// A closure that executes when the publisher receives completion, and can raise
/// a debugger signal by returning a `true` Boolean value.
public let receiveCompletion:
((Subscribers.Completion<Upstream.Failure>) -> Bool)?
/// Creates a breakpoint publisher with the provided upstream publisher and
/// breakpoint-raising closures.
///
/// - Parameters:
/// - upstream: The publisher from which this publisher receives elements.
/// - receiveSubscription: A closure that executes when the publisher receives
/// a subscription, and can raise a debugger signal by returning a `true`
/// Boolean value.
/// - receiveOutput: A closure that executes when the publisher receives output
/// from the upstream publisher, and can raise a debugger signal by returning
/// a `true` Boolean value.
/// - receiveCompletion: A closure that executes when the publisher receives
/// completion, and can raise a debugger signal by returning a `true` Boolean
/// value.
public init(
upstream: Upstream,
receiveSubscription: ((Subscription) -> Bool)? = nil,
receiveOutput: ((Upstream.Output) -> Bool)? = nil,
receiveCompletion: ((Subscribers.Completion<Failure>) -> Bool)? = nil
) {
self.upstream = upstream
self.receiveSubscription = receiveSubscription
self.receiveOutput = receiveOutput
self.receiveCompletion = receiveCompletion
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Upstream.Failure == Downstream.Failure,
Upstream.Output == Downstream.Input
{
upstream.subscribe(Inner(self, downstream: subscriber))
}
}
}
extension Publishers.Breakpoint {
private struct Inner<Downstream: Subscriber>
: Subscriber,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private let downstream: Downstream
private let breakpoint: Publishers.Breakpoint<Upstream>
let combineIdentifier = CombineIdentifier()
init(_ breakpoint: Publishers.Breakpoint<Upstream>,
downstream: Downstream) {
self.downstream = downstream
self.breakpoint = breakpoint
}
func receive(subscription: Subscription) {
if breakpoint.receiveSubscription?(subscription) == true {
__stopInDebugger()
}
downstream.receive(subscription: subscription)
}
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
if breakpoint.receiveOutput?(input) == true {
__stopInDebugger()
}
return downstream.receive(input)
}
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
if breakpoint.receiveCompletion?(completion) == true {
__stopInDebugger()
}
downstream.receive(completion: completion)
}
var description: String { return "Breakpoint" }
var customMirror: Mirror {
let children = CollectionOfOne<Mirror.Child>(
("upstream", breakpoint.upstream)
)
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
}
}
@@ -0,0 +1,336 @@
//
// Publishers.Buffer.swift
//
//
// Created by Sergej Jaskiewicz on 08.01.2020.
//
extension Publisher {
/// Buffers elements received from an upstream publisher.
/// - Parameter size: The maximum number of elements to store.
/// - Parameter prefetch: The strategy for initially populating the buffer.
/// - Parameter whenFull: The action to take when the buffer becomes full.
public func buffer(
size: Int,
prefetch: Publishers.PrefetchStrategy,
whenFull: Publishers.BufferingStrategy<Failure>
) -> Publishers.Buffer<Self> {
return .init(upstream: self,
size: size,
prefetch: prefetch,
whenFull: whenFull)
}
}
extension Publishers {
/// A strategy for filling a buffer.
///
/// * keepFull: A strategy to fill the buffer at subscription time, and keep it full
/// thereafter.
/// * byRequest: A strategy that avoids prefetching and instead performs requests
/// on demand.
public enum PrefetchStrategy {
/// A strategy to fill the buffer at subscription time, and keep it full
/// thereafter.
case keepFull
/// A strategy that avoids prefetching and instead performs requests
/// on demand.
case byRequest
}
/// A strategy for handling exhaustion of a buffers capacity.
///
/// * dropNewest: When full, discard the newly-received element without buffering it.
/// * dropOldest: When full, remove the least recently-received element from the
/// buffer.
/// * customError: When full, execute the closure to provide a custom error.
public enum BufferingStrategy<Failure: Error> {
/// When full, discard the newly-received element without buffering it.
case dropNewest
/// When full, remove the least recently-received element from the buffer.
case dropOldest
/// When full, execute the closure to provide a custom error.
case customError(() -> Failure)
}
/// A publisher that buffers elements received from an upstream publisher.
public struct Buffer<Upstream: Publisher>: Publisher {
public typealias Output = Upstream.Output
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// The maximum number of elements to store.
public let size: Int
/// The strategy for initially populating the buffer.
public let prefetch: Publishers.PrefetchStrategy
/// The action to take when the buffer becomes full.
public let whenFull: Publishers.BufferingStrategy<Failure>
/// Creates a publisher that buffers elements received from an upstream publisher.
/// - Parameter upstream: The publisher from which this publisher receives
/// elements.
/// - Parameter size: The maximum number of elements to store.
/// - Parameter prefetch: The strategy for initially populating the buffer.
/// - Parameter whenFull: The action to take when the buffer becomes full.
public init(upstream: Upstream,
size: Int,
prefetch: Publishers.PrefetchStrategy,
whenFull: Publishers.BufferingStrategy<Failure>) {
self.upstream = upstream
self.size = size
self.prefetch = prefetch
self.whenFull = whenFull
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == Output, Downstream.Failure == Failure
{
upstream.subscribe(Inner(downstream: subscriber, buffer: self))
}
}
}
extension Publishers.PrefetchStrategy: Equatable {}
extension Publishers.PrefetchStrategy: Hashable {}
extension Publishers.Buffer {
private final class Inner<Downstream: Subscriber>
: Subscriber,
Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private enum State {
case ready(Publishers.Buffer<Upstream>, Downstream)
case subscribed(Publishers.Buffer<Upstream>, Downstream, Subscription)
case terminal
}
private let lock = UnfairLock.allocate()
private var recursion = false
private var state: State
private var downstreamDemand = Subscribers.Demand.none
// TODO: Use a deque here?
// Need to measure performance with large buffers and `dropOldest` strategy.
private var values = [Input]()
private var upstreamFailed = false
private var terminal: Subscribers.Completion<Failure>?
init(downstream: Downstream, buffer: Publishers.Buffer<Upstream>) {
state = .ready(buffer, downstream)
}
deinit {
lock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard case let .ready(buffer, downstream) = state else {
lock.unlock()
subscription.cancel()
return
}
state = .subscribed(buffer, downstream, subscription)
lock.unlock()
let upstreamDemand: Subscribers.Demand
switch buffer.prefetch {
case .keepFull:
upstreamDemand = .max(buffer.size)
case .byRequest:
upstreamDemand = .unlimited
}
subscription.request(upstreamDemand)
downstream.receive(subscription: self)
}
func receive(_ input: Input) -> Subscribers.Demand {
lock.lock()
guard case let .subscribed(buffer, _, subscription) = state else {
lock.unlock()
return .none
}
switch terminal {
case nil, .finished?:
if values.count >= buffer.size {
switch buffer.whenFull {
case .dropNewest:
lock.unlock()
return drain()
case .dropOldest:
values.removeFirst()
case let .customError(makeError):
terminal = .failure(makeError())
lock.unlock()
subscription.cancel()
return .none
}
}
values.append(input)
lock.unlock()
return drain()
case .failure?:
lock.unlock()
return .none
}
}
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
lock.lock()
guard case .subscribed = state, terminal == nil else {
lock.unlock()
return
}
terminal = completion
lock.unlock()
_ = drain()
}
func request(_ demand: Subscribers.Demand) {
lock.lock()
guard case let .subscribed(_, _, subscription) = state else {
lock.unlock()
return
}
downstreamDemand += demand
let recursion = self.recursion
lock.unlock()
if recursion {
return
}
// Request the number of items just enough to fill the buffer.
subscription.request(drain() + demand)
}
func cancel() {
lock.lock()
guard case let .subscribed(_, _, subscription) = state else {
lock.unlock()
return
}
state = .terminal
values = []
lock.unlock()
subscription.cancel()
}
private func drain() -> Subscribers.Demand {
var upstreamDemand = Subscribers.Demand.none
lock.lock()
while true {
guard case let .subscribed(buffer, downstream, _) = state else {
lock.unlock()
return upstreamDemand
}
if downstreamDemand > 0 {
if values.isEmpty {
if let completion = terminal {
state = .terminal
lock.unlock()
downstream.receive(completion: completion)
} else {
lock.unlock()
}
return upstreamDemand
}
} else {
if let completion = terminal, case .failure = completion {
state = .terminal
lock.unlock()
downstream.receive(completion: completion)
} else {
lock.unlock()
}
return upstreamDemand
}
let poppedValues = lockedPop(downstreamDemand)
assert(poppedValues.count > 0,
"""
We check that the buffer is not empty and downstreamDemand is \
nonzero, how can this be triggered?
""")
// This should not crash because `lockedPop(_:)` returns at most
// `downstreamDemand` items.
downstreamDemand -= poppedValues.count
recursion = true
lock.unlock()
var newDownstreamDemand = Subscribers.Demand.none
var additionalUpstreamDemand = 0
for value in poppedValues {
newDownstreamDemand += downstream.receive(value)
additionalUpstreamDemand += 1
}
if buffer.prefetch == .keepFull {
upstreamDemand += additionalUpstreamDemand
}
lock.lock()
recursion = false
downstreamDemand += newDownstreamDemand
}
}
private func lockedPop(_ demand: Subscribers.Demand) -> [Input] {
assert(demand > 0)
guard let max = demand.max else {
let poppedValues = self.values
self.values = []
return poppedValues
}
let poppedValues = Array(values.prefix(max))
values.removeFirst(poppedValues.count)
return poppedValues
}
var description: String { return "Buffer" }
var customMirror: Mirror {
let children: [Mirror.Child] = [
("values", values),
("state", state),
("downstreamDemand", downstreamDemand),
("terminal", terminal as Any)
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
}
}
@@ -0,0 +1,401 @@
${template_header}
//
// Publishers.Catch.swift
//
//
// Created by Sergej Jaskiewicz on 25.12.2019.
//
%{
instantiations = ['Catch', 'TryCatch']
}%
extension Publisher {
/// Handles errors from an upstream publisher by replacing it with another publisher.
///
/// The following example replaces any error from the upstream publisher and replaces
/// the upstream with a `Just` publisher. This continues the stream by publishing
/// a single value and completing normally.
/// ```
/// enum SimpleError: Error { case error }
/// let errorPublisher = (0..<10).publisher.tryMap { v -> Int in
/// if v < 5 {
/// return v
/// } else {
/// throw SimpleError.error
/// }
/// }
///
/// let noErrorPublisher = errorPublisher.catch { _ in
/// return Just(100)
/// }
/// ```
/// Backpressure note: This publisher passes through `request` and `cancel` to
/// the upstream. After receiving an error, the publisher sends sends any unfulfilled
/// demand to the new `Publisher`.
///
/// - Parameter handler: A closure that accepts the upstream failure as input and
/// returns a publisher to replace the upstream publisher.
/// - Returns: A publisher that handles errors from an upstream publisher by replacing
/// the failed publisher with another publisher.
public func `catch`<NewPublisher: Publisher>(
_ handler: @escaping (Failure) -> NewPublisher
) -> Publishers.Catch<Self, NewPublisher>
where NewPublisher.Output == Output
{
return .init(upstream: self, handler: handler)
}
/// Handles errors from an upstream publisher by either replacing it with another
/// publisher or `throw`ing a new error.
///
/// - Parameter handler: A `throw`ing closure that accepts the upstream failure as
/// input and returns a publisher to replace the upstream publisher or if an error
/// is thrown will send the error downstream.
/// - Returns: A publisher that handles errors from an upstream publisher by replacing
/// the failed publisher with another publisher.
public func tryCatch<NewPublisher: Publisher>(
_ handler: @escaping (Failure) throws -> NewPublisher
) -> Publishers.TryCatch<Self, NewPublisher>
where NewPublisher.Output == Output
{
return .init(upstream: self, handler: handler)
}
}
extension Publishers {
/// A publisher that handles errors from an upstream publisher by replacing the failed
/// publisher with another publisher.
public struct Catch<Upstream: Publisher, NewPublisher: Publisher>: Publisher
where Upstream.Output == NewPublisher.Output
{
public typealias Output = Upstream.Output
public typealias Failure = NewPublisher.Failure
/// The publisher that this publisher receives elements from.
public let upstream: Upstream
/// A closure that accepts the upstream failure as input and returns a publisher
/// to replace the upstream publisher.
public let handler: (Upstream.Failure) -> NewPublisher
/// Creates a publisher that handles errors from an upstream publisher by
/// replacing the failed publisher with another publisher.
///
/// - Parameters:
/// - upstream: The publisher that this publisher receives elements from.
/// - handler: A closure that accepts the upstream failure as input and returns
/// a publisher to replace the upstream publisher.
public init(upstream: Upstream,
handler: @escaping (Upstream.Failure) -> NewPublisher) {
self.upstream = upstream
self.handler = handler
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == Output, Downstream.Failure == Failure
{
let inner = Inner(downstream: subscriber, handler: handler)
let uncaughtS = Inner.UncaughtS(inner: inner)
upstream.subscribe(uncaughtS)
}
}
/// A publisher that handles errors from an upstream publisher by replacing the failed
/// publisher with another publisher or optionally producing a new error.
public struct TryCatch<Upstream: Publisher, NewPublisher: Publisher>: Publisher
where Upstream.Output == NewPublisher.Output
{
public typealias Output = Upstream.Output
public typealias Failure = Error
public let upstream: Upstream
public let handler: (Upstream.Failure) throws -> NewPublisher
public init(upstream: Upstream,
handler: @escaping (Upstream.Failure) throws -> NewPublisher) {
self.upstream = upstream
self.handler = handler
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == Output, Downstream.Failure == Failure
{
let inner = Inner(downstream: subscriber, handler: handler)
let uncaughtS = Inner.UncaughtS(inner: inner)
upstream.subscribe(uncaughtS)
}
}
}
% for instantiation in instantiations:
% throws_modifier = ' throws' if instantiation == 'TryCatch' else ''
extension Publishers.${instantiation} {
private final class Inner<Downstream: Subscriber>
: Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Upstream.Output,
% if instantiation == 'Catch':
Downstream.Failure == NewPublisher.Failure
% else:
Downstream.Failure == Error
% end
{
struct UncaughtS: Subscriber,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
let inner: Inner
var combineIdentifier: CombineIdentifier { return inner.combineIdentifier }
func receive(subscription: Subscription) {
inner.receivePre(subscription: subscription)
}
func receive(_ input: Input) -> Subscribers.Demand {
return inner.receivePre(input)
}
func receive(completion: Subscribers.Completion<Failure>) {
return inner.receivePre(completion: completion)
}
var description: String { return inner.description }
var customMirror: Mirror { return inner.customMirror }
var playgroundDescription: Any { return description }
}
struct CaughtS: Subscriber,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
{
typealias Input = NewPublisher.Output
typealias Failure = NewPublisher.Failure
let inner: Inner
var combineIdentifier: CombineIdentifier { return inner.combineIdentifier }
func receive(subscription: Subscription) {
inner.receivePost(subscription: subscription)
}
func receive(_ input: Input) -> Subscribers.Demand {
return inner.receivePost(input)
}
func receive(completion: Subscribers.Completion<Failure>) {
inner.receivePost(completion: completion)
}
var description: String { return inner.description }
var customMirror: Mirror { return inner.customMirror }
var playgroundDescription: Any { return description }
}
private enum State {
case pendingPre
case pre(Subscription)
case pendingPost
case post(Subscription)
case cancelled
}
private let lock = UnfairLock.allocate()
private var demand = Subscribers.Demand.none
private var state = State.pendingPre
private let downstream: Downstream
private let handler: (Upstream.Failure)${throws_modifier} -> NewPublisher
init(downstream: Downstream,
handler: @escaping (Upstream.Failure)${throws_modifier} -> NewPublisher) {
self.downstream = downstream
self.handler = handler
}
deinit {
lock.deallocate()
}
func receivePre(subscription: Subscription) {
lock.lock()
guard case .pendingPre = state else {
lock.unlock()
subscription.cancel()
return
}
state = .pre(subscription)
lock.unlock()
downstream.receive(subscription: self)
}
func receivePre(_ input: Upstream.Output) -> Subscribers.Demand {
lock.lock()
demand -= 1
lock.unlock()
let newDemand = downstream.receive(input)
lock.lock()
demand += newDemand
lock.unlock()
return newDemand
}
func receivePre(completion: Subscribers.Completion<Upstream.Failure>) {
switch completion {
case .finished:
lock.lock()
switch state {
case .pre:
state = .cancelled
lock.unlock()
downstream.receive(completion: .finished)
case .pendingPre, .pendingPost, .post, .cancelled:
lock.unlock()
}
case .failure(let error):
lock.lock()
switch state {
case .pre:
state = .pendingPost
lock.unlock()
% if instantiation == 'Catch':
handler(error).subscribe(CaughtS(inner: self))
% else:
do {
try handler(error).subscribe(CaughtS(inner: self))
} catch let anotherError {
lock.lock()
state = .cancelled
lock.unlock()
downstream.receive(completion: .failure(anotherError))
}
% end
case .cancelled:
lock.unlock()
case .pendingPre, .post, .pendingPost:
completionBeforeSubscription()
}
}
}
func receivePost(subscription: Subscription) {
lock.lock()
guard case .pendingPost = state else {
lock.unlock()
subscription.cancel()
return
}
state = .post(subscription)
let demand = self.demand
lock.unlock()
if demand > 0 {
subscription.request(demand)
}
}
func receivePost(_ input: NewPublisher.Output) -> Subscribers.Demand {
return downstream.receive(input)
}
func receivePost(completion: Subscribers.Completion<NewPublisher.Failure>) {
lock.lock()
guard case .post = state else {
lock.unlock()
return
}
state = .cancelled
lock.unlock()
% if instantiation == 'Catch':
downstream.receive(completion: completion)
% else:
downstream.receive(completion: completion.eraseError())
% end
}
func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
lock.lock()
switch state {
case .pendingPre:
// The client is only able to call the `request` method after we've sent
// `self` downstream. We only do it in the `receivePre(subscription:)`
// method, after setting `state` to `pre`.
// After that `state` never becomes `pendingPre`.
requestBeforeSubscription()
case let .pre(subscription):
self.demand += demand
lock.unlock()
subscription.request(demand)
case .pendingPost:
self.demand += demand
lock.unlock()
case let .post(subscription):
lock.unlock()
subscription.request(demand)
case .cancelled:
lock.unlock()
}
}
func cancel() {
lock.lock()
switch state {
case let .pre(subscription), let .post(subscription):
state = .cancelled
lock.unlock()
subscription.cancel()
case .pendingPre, .pendingPost, .cancelled:
lock.unlock()
}
}
var description: String { return "${instantiation}" }
var customMirror: Mirror {
let children: [Mirror.Child] = [
("downstream", downstream),
("demand", demand)
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
}
}
% end
private func completionBeforeSubscription(file: StaticString = #file,
line: UInt = #line) -> Never {
fatalError("Unexpected state: received completion but do not have subscription",
file: file,
line: line)
}
private func requestBeforeSubscription(file: StaticString = #file,
line: UInt = #line) -> Never {
fatalError("Unexpected state: request before subscription sent",
file: file,
line: line)
}
@@ -0,0 +1,185 @@
//
// Publishers.CollectByCount.swift
//
//
// Created by Sergej Jaskiewicz on 24.12.2019.
//
extension Publisher {
/// Collects up to the specified number of elements, and then emits a single array of
/// the collection.
///
/// If the upstream publisher finishes before filling the buffer, this publisher sends
/// an array of all the items it has received. This may be fewer than `count`
/// elements.
/// If the upstream publisher fails with an error, this publisher forwards the error
/// to the downstream receiver instead of sending its output.
/// Note: When this publisher receives a request for `.max(n)` elements, it requests
/// `.max(count * n)` from the upstream publisher.
///
/// - Parameter count: The maximum number of received elements to buffer before
/// publishing.
/// - Returns: A publisher that collects up to the specified number of elements, and
/// then publishes them as an array.
public func collect(_ count: Int) -> Publishers.CollectByCount<Self> {
return .init(upstream: self, count: count)
}
}
extension Publishers {
/// A publisher that buffers a maximum number of items.
public struct CollectByCount<Upstream: Publisher>: Publisher {
public typealias Output = [Upstream.Output]
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// The maximum number of received elements to buffer before publishing.
public let count: Int
public init(upstream: Upstream, count: Int) {
self.upstream = upstream
self.count = count
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Failure == Failure, Downstream.Input == Output
{
upstream.subscribe(Inner(downstream: subscriber, count: count))
}
}
}
extension Publishers.CollectByCount: Equatable where Upstream: Equatable {}
extension Publishers.CollectByCount {
private final class Inner<Downstream: Subscriber>
: Subscriber,
Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == [Upstream.Output],
Downstream.Failure == Upstream.Failure
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private let downstream: Downstream
private let count: Int
private var buffer: [Input] = []
private var subscription: Subscription?
private var finished = false
private let lock = UnfairLock.allocate()
init(downstream: Downstream, count: Int) {
self.downstream = downstream
self.count = count
}
deinit {
lock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
if finished || self.subscription != nil {
lock.unlock()
subscription.cancel()
return
}
self.subscription = subscription
lock.unlock()
downstream.receive(subscription: self)
}
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
lock.lock()
if subscription == nil {
lock.unlock()
return .none
}
buffer.append(input)
guard buffer.count == count else {
lock.unlock()
return .none
}
let output = self.buffer
self.buffer = []
lock.unlock()
return downstream.receive(output) * count
}
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
lock.lock()
subscription = nil
finished = true
switch completion {
case .finished:
if buffer.isEmpty {
lock.unlock()
} else {
let buffer = self.buffer
self.buffer = []
lock.unlock()
_ = downstream.receive(buffer)
}
case .failure:
buffer = []
lock.unlock()
}
downstream.receive(completion: completion)
}
func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
lock.lock()
if let subscription = self.subscription {
lock.unlock()
subscription.request(demand * count)
} else {
lock.unlock()
}
}
func cancel() {
lock.lock()
if let subscription = self.subscription {
buffer = []
finished = true
self.subscription = nil
lock.unlock()
subscription.cancel()
} else {
lock.unlock()
}
}
var description: String { return "CollectByCount" }
var customMirror: Mirror {
lock.lock()
defer { lock.unlock() }
let children: [Mirror.Child] = [
("downstream", downstream),
("upstreamSubscription", subscription as Any),
("buffer", buffer),
("count", count)
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
}
}
@@ -5,65 +5,6 @@
// Created by Sergej Jaskiewicz on 11.07.2019.
//
extension Publishers {
/// A publisher that republishes all non-`nil` results of calling a closure
/// with each received element.
public struct CompactMap<Upstream: Publisher, Output>: Publisher {
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// A closure that receives values from the upstream publisher
/// and returns optional values.
public let transform: (Upstream.Output) -> Output?
public init(upstream: Upstream,
transform: @escaping (Upstream.Output) -> Output?) {
self.upstream = upstream
self.transform = transform
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == Output, Downstream.Failure == Failure
{
let inner = Inner(downstream: subscriber, transform: catching(transform))
upstream.subscribe(inner)
}
}
/// A publisher that republishes all non-`nil` results of calling an error-throwing
/// closure with each received element.
public struct TryCompactMap<Upstream: Publisher, Output>: Publisher {
public typealias Failure = Error
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// An error-throwing closure that receives values from the upstream publisher
/// and returns optional values.
///
/// If this closure throws an error, the publisher fails.
public let transform: (Upstream.Output) throws -> Output?
public init(upstream: Upstream,
transform: @escaping (Upstream.Output) throws -> Output?) {
self.upstream = upstream
self.transform = transform
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == Output, Downstream.Failure == Failure
{
let inner = Inner(downstream: subscriber, transform: catching(transform))
upstream.subscribe(inner)
}
}
}
extension Publisher {
/// Calls a closure with each received element and publishes any returned
@@ -123,89 +64,105 @@ extension Publishers.TryCompactMap {
}
}
private class _CompactMap<Upstream: Publisher, Downstream: Subscriber>
: OperatorSubscription<Downstream>,
Subscription
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
typealias Transform = (Input) -> Result<Downstream.Input?, Downstream.Failure>
extension Publishers {
fileprivate var _transform: Transform?
/// A publisher that republishes all non-`nil` results of calling a closure
/// with each received element.
public struct CompactMap<Upstream: Publisher, Output>: Publisher {
var _isCompleted: Bool {
return _transform == nil
}
public typealias Failure = Upstream.Failure
init(downstream: Downstream, transform: @escaping Transform) {
_transform = transform
super.init(downstream: downstream)
}
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
func receive(subscription: Subscription) {
upstreamSubscription = subscription
downstream.receive(subscription: self)
}
/// A closure that receives values from the upstream publisher
/// and returns optional values.
public let transform: (Upstream.Output) -> Output?
func receive(_ input: Input) -> Subscribers.Demand {
guard let transform = _transform else { return .none }
public init(upstream: Upstream,
transform: @escaping (Upstream.Output) -> Output?) {
self.upstream = upstream
self.transform = transform
}
switch transform(input) {
case .success(let output?):
return downstream.receive(output)
case .success(nil):
return .max(1)
case .failure(let error):
downstream.receive(completion: .failure(error))
_transform = nil
return .none
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == Output, Downstream.Failure == Failure
{
upstream.subscribe(Inner(downstream: subscriber, filter: transform))
}
}
func request(_ demand: Subscribers.Demand) {
guard !_isCompleted else { return }
upstreamSubscription?.request(demand)
}
/// A publisher that republishes all non-`nil` results of calling an error-throwing
/// closure with each received element.
public struct TryCompactMap<Upstream: Publisher, Output>: Publisher {
override func cancel() {
_transform = nil
upstreamSubscription?.cancel()
upstreamSubscription = nil
public typealias Failure = Error
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// An error-throwing closure that receives values from the upstream publisher
/// and returns optional values.
///
/// If this closure throws an error, the publisher fails.
public let transform: (Upstream.Output) throws -> Output?
public init(upstream: Upstream,
transform: @escaping (Upstream.Output) throws -> Output?) {
self.upstream = upstream
self.transform = transform
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == Output, Downstream.Failure == Failure
{
upstream.subscribe(Inner(downstream: subscriber, filter: transform))
}
}
}
extension Publishers.CompactMap {
private final class Inner<Downstream: Subscriber>
: _CompactMap<Upstream, Downstream>,
Subscriber,
CustomStringConvertible
where Downstream.Failure == Upstream.Failure
: FilterProducer<Downstream,
Upstream.Output,
Output,
Upstream.Failure,
(Upstream.Output) -> Output?>
where Downstream.Failure == Upstream.Failure, Downstream.Input == Output
{
var description: String { return "CompactMap" }
// NOTE: This class has been audited for thread safety
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
if !_isCompleted {
_transform = nil
downstream.receive(completion: completion)
}
override func receive(
newValue: Upstream.Output
) -> PartialCompletion<Output?, Downstream.Failure> {
return .continue(filter(newValue))
}
override var description: String { return "CompactMap" }
}
}
extension Publishers.TryCompactMap {
private final class Inner<Downstream: Subscriber>
: _CompactMap<Upstream, Downstream>,
Subscriber,
CustomStringConvertible
where Downstream.Failure == Error
: FilterProducer<Downstream,
Upstream.Output,
Output,
Upstream.Failure,
(Upstream.Output) throws -> Output?>
where Downstream.Failure == Error, Downstream.Input == Output
{
var description: String { return "TryCompactMap" }
// NOTE: This class has been audited for thread safety
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
if !_isCompleted {
_transform = nil
downstream.receive(completion: completion.eraseError())
override func receive(
newValue: Upstream.Output
) -> PartialCompletion<Output?, Error> {
do {
return try .continue(filter(newValue))
} catch {
return .failure(error)
}
}
override var description: String { return "TryCompactMap" }
}
}
@@ -94,7 +94,7 @@ extension Publisher {
/// - Returns: A publisher that publishes the maximum value received from the upstream
/// publisher, after the upstream publisher finishes.
public func tryMax(
by areInIncreasingOrder: @escaping (Self.Output, Self.Output) throws -> Bool
by areInIncreasingOrder: @escaping (Output, Output) throws -> Bool
) -> Publishers.TryComparison<Self> {
return .init(upstream: self, areInIncreasingOrder: areInIncreasingOrder)
}
@@ -0,0 +1,248 @@
//
// Publishers.Concatenate.swift
//
//
// Created by Sergej Jaskiewicz on 24.10.2019.
//
extension Publisher {
/// Prefixes a `Publisher`'s output with the specified sequence.
///
/// - Parameter elements: The elements to publish before this 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 another
/// 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)
subscriber.receive(subscription: inner)
prefix.subscribe(inner)
}
}
}
extension Publishers.Concatenate: Equatable where Prefix: Equatable, Suffix: Equatable {}
extension Publishers.Concatenate {
private final class Inner<Downstream: Subscriber>
: Subscriber,
Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Suffix.Output, Downstream.Failure == Suffix.Failure
{
typealias Input = Suffix.Output
typealias Failure = Suffix.Failure
private let downstream: Downstream
private let suffix: Suffix
private var prefixFinished = false
private var demand = Subscribers.Demand.none
private var upstream: Subscription?
private var expectedSubscriptions = 2
private let lock = UnfairLock.allocate()
private let downstreamLock = UnfairRecursiveLock.allocate()
fileprivate init(downstream: Downstream, suffix: Suffix) {
self.downstream = downstream
self.suffix = suffix
}
deinit {
lock.deallocate()
downstreamLock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard upstream == nil, expectedSubscriptions > 0 else {
lock.unlock()
subscription.cancel()
return
}
upstream = subscription
expectedSubscriptions -= 1
let demand = self.demand
lock.unlock()
if demand > 0 {
subscription.request(demand)
}
}
func receive(_ input: Input) -> Subscribers.Demand {
lock.lock()
demand -= 1
lock.unlock()
downstreamLock.lock()
let newDemand = downstream.receive(input)
downstreamLock.unlock()
lock.lock()
demand += newDemand
lock.unlock()
return newDemand
}
func receive(completion: Subscribers.Completion<Failure>) {
// Reading prefixFinished should be locked. Combine doesn't lock here.
if prefixFinished {
downstreamLock.lock()
downstream.receive(completion: completion)
downstreamLock.unlock()
return
}
guard case .finished = completion else {
downstreamLock.lock()
downstream.receive(completion: completion)
downstreamLock.unlock()
return
}
prefixFinished = true // Should be locked as well?
lock.lock()
upstream = nil
lock.unlock()
suffix.subscribe(self)
}
func request(_ demand: Subscribers.Demand) {
lock.lock()
self.demand += demand
guard let subscription = upstream else {
lock.unlock()
return
}
lock.unlock()
subscription.request(demand)
}
func cancel() {
lock.lock()
guard let subscription = upstream else {
lock.unlock()
return
}
upstream = nil
lock.unlock()
subscription.cancel()
}
var description: String { return "Concatenate" }
var customMirror: Mirror {
let children: [Mirror.Child] = [
("downstream", downstream),
("upstreamSubscription", upstream as Any),
("suffix", suffix),
("demand", demand)
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
}
}
@@ -0,0 +1,264 @@
//
// Publishers.Debounce.swift
//
//
// Created by Sergej Jaskiewicz on 17.12.2019.
//
extension Publisher {
/// Publishes elements only after a specified time interval elapses between events.
///
/// Use this operator when you want to wait for a pause in the delivery of events from
/// the upstream publisher. For example, call `debounce` on the publisher from a text
/// field to only receive elements when the user pauses or stops typing. When they
/// start typing again, the `debounce` holds event delivery until the next pause.
///
/// - Parameters:
/// - dueTime: The time the publisher should wait before publishing an element.
/// - scheduler: The scheduler on which this publisher delivers elements
/// - options: Scheduler options that customize this publishers delivery
/// of elements.
/// - Returns: A publisher that publishes events only after a specified time elapses.
public func debounce<Context: Scheduler>(
for dueTime: Context.SchedulerTimeType.Stride,
scheduler: Context,
options: Context.SchedulerOptions? = nil
) -> Publishers.Debounce<Self, Context> {
return .init(upstream: self,
dueTime: dueTime,
scheduler: scheduler,
options: options)
}
}
extension Publishers {
/// A publisher that publishes elements only after a specified time interval elapses
/// between events.
public struct Debounce<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 amount of time the publisher should wait before publishing an element.
public let dueTime: Context.SchedulerTimeType.Stride
/// The scheduler on which this publisher delivers elements.
public let scheduler: Context
/// Scheduler options that customize this publishers delivery of elements.
public let options: Context.SchedulerOptions?
public init(upstream: Upstream,
dueTime: Context.SchedulerTimeType.Stride,
scheduler: Context,
options: Context.SchedulerOptions?) {
self.upstream = upstream
self.dueTime = dueTime
self.scheduler = scheduler
self.options = options
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Failure == Failure, Downstream.Input == Output
{
let inner = Inner(downstream: subscriber,
dueTime: dueTime,
scheduler: scheduler,
options: options)
upstream.subscribe(inner)
}
}
}
extension Publishers.Debounce {
private final class Inner<Downstream: Subscriber>
: Subscriber,
Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Upstream.Output == Downstream.Input,
Upstream.Failure == Downstream.Failure
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private typealias Generation = UInt64
private let lock = UnfairLock.allocate()
private let downstreamLock = UnfairRecursiveLock.allocate()
private let downstream: Downstream
private let dueTime: Context.SchedulerTimeType.Stride
private let scheduler: Context
private let options: Context.SchedulerOptions?
private var state = SubscriptionStatus.awaitingSubscription
private var currentCanceller: Cancellable?
private var currentValue: Output?
private var currentGeneration: Generation = 0
private var downstreamDemand = Subscribers.Demand.none
init(downstream: Downstream,
dueTime: Context.SchedulerTimeType.Stride,
scheduler: Context,
options: Context.SchedulerOptions?) {
self.downstream = downstream
self.dueTime = dueTime
self.scheduler = scheduler
self.options = options
}
deinit {
lock.deallocate()
downstreamLock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard case .awaitingSubscription = state else {
lock.unlock()
subscription.cancel()
return
}
state = .subscribed(subscription)
lock.unlock()
downstreamLock.lock()
downstream.receive(subscription: self)
downstreamLock.unlock()
subscription.request(.unlimited)
}
func receive(_ input: Input) -> Subscribers.Demand {
lock.lock()
precondition(!state.isAwaitingSubscription)
guard case .subscribed = state else {
lock.unlock()
return .none
}
currentGeneration += 1
let generation = currentGeneration
currentValue = input
let due = scheduler.now.advanced(by: dueTime)
lock.unlock()
let newCanceller = scheduler.schedule(after: due,
interval: dueTime,
tolerance: scheduler.minimumTolerance,
options: options) { [weak self] in
self?.due(generation: generation)
}
lock.lock()
let canceller = currentCanceller
currentCanceller = newCanceller
lock.unlock()
canceller?.cancel()
return .none
}
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
lock.lock()
precondition(!state.isAwaitingSubscription)
guard case .subscribed = state else {
lock.unlock()
return
}
state = .terminal
let canceller = currentCanceller
lock.unlock()
canceller?.cancel()
scheduler.schedule {
self.downstreamLock.lock()
self.downstream.receive(completion: completion)
self.downstreamLock.unlock()
}
}
func request(_ demand: Subscribers.Demand) {
lock.lock()
precondition(!state.isAwaitingSubscription)
guard case .subscribed = state else {
lock.unlock()
return
}
downstreamDemand += demand
lock.unlock()
}
func cancel() {
lock.lock()
guard case .subscribed(let subscription) = state else {
lock.unlock()
return
}
state = .terminal
lock.unlock()
subscription.cancel()
}
var description: String { return "Debounce" }
var customMirror: Mirror {
let children: [Mirror.Child] = [
("downstream", downstream),
("downstreamDemand", downstreamDemand),
("currentValue", currentValue as Any)
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
private func due(generation: Generation) {
lock.lock()
guard case .subscribed = state else {
lock.unlock()
return
}
// If this condition holds, it means that no values were received
// in this time frame => we should propagate the current value downstream.
guard generation == currentGeneration, let value = currentValue else {
let canceller = currentCanceller
lock.unlock()
canceller?.cancel()
return
}
let hasAnyDemand = downstreamDemand > 0
if hasAnyDemand {
downstreamDemand -= 1
}
let canceller = currentCanceller!
lock.unlock()
canceller.cancel()
guard hasAnyDemand else { return }
downstreamLock.lock()
let newDemand = downstream.receive(value)
downstreamLock.unlock()
if newDemand == .none { return }
lock.lock()
downstreamDemand += newDemand
lock.unlock()
}
}
}
@@ -1,109 +0,0 @@
//
// Publishers.Decode.swift
//
//
// Created by Joseph Spadafora on 6/21/19.
//
extension Publishers {
public struct Decode<Upstream, Output, Coder>: Publisher
where Upstream: Publisher,
Output: Decodable,
Coder: TopLevelDecoder,
Upstream.Output == Coder.Input
{
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = Error
public let upstream: Upstream
private let _decoder: Coder
public init(upstream: Upstream, decoder: Coder) {
self.upstream = upstream
self._decoder = decoder
}
/// This function is called to attach the specified `Subscriber`
/// to this `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Failure == Downstream.Failure, Output == Downstream.Input
{
let decodeSubscriber = _Decode<Upstream, Downstream, Coder>(
downstream: subscriber,
decoder: _decoder
)
upstream.subscribe(decodeSubscriber)
}
}
}
private final class _Decode<Upstream: Publisher,
Downstream: Subscriber,
Coder: TopLevelDecoder>
: OperatorSubscription<Downstream>,
Subscriber,
CustomStringConvertible,
Subscription
where Downstream.Input: Decodable,
Coder.Input == Upstream.Output,
Downstream.Failure == Error {
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
typealias Output = Downstream.Input
private let _decoder: Coder
var description: String { return "Decode" }
init(downstream: Downstream, decoder: Coder) {
self._decoder = decoder
super.init(downstream: downstream)
}
func receive(subscription: Subscription) {
upstreamSubscription = subscription
downstream.receive(subscription: self)
}
func receive(_ input: Input) -> Subscribers.Demand {
do {
let value = try _decoder.decode(Downstream.Input.self, from: input)
return downstream.receive(value)
} catch {
downstream.receive(completion: .failure(error))
cancel()
return .none
}
}
func receive(completion: Subscribers.Completion<Failure>) {
downstream.receive(completion: completion.eraseError())
}
func request(_ demand: Subscribers.Demand) {
upstreamSubscription?.request(demand)
}
}
extension Publisher {
public func decode<Item: Decodable,
Coder: TopLevelDecoder>(
type: Item.Type,
decoder: Coder
) -> Publishers.Decode<Self, Item, Coder>
where Self.Output == Coder.Input
{
return Publishers.Decode(upstream: self, decoder: decoder)
}
}
@@ -0,0 +1,207 @@
//
// Publishers.Delay.swift
// OpenCombine
//
// Created by Евгений Богомолов on 07/09/2019.
//
extension Publisher {
/// Delays delivery of all output to the downstream receiver by a specified amount
/// of time on a particular scheduler.
///
/// The delay affects the delivery of elements and completion, but not of the original
/// subscription.
///
/// - Parameters:
/// - interval: The amount of time to delay.
/// - tolerance: The allowed tolerance in firing delayed events.
/// - scheduler: The scheduler to deliver the delayed events.
/// - Returns: A publisher that delays delivery of elements and completion to
/// the downstream receiver.
public func delay<Context: Scheduler>(
for interval: Context.SchedulerTimeType.Stride,
tolerance: Context.SchedulerTimeType.Stride? = nil,
scheduler: Context,
options: Context.SchedulerOptions? = nil
) -> Publishers.Delay<Self, Context> {
return .init(upstream: self,
interval: interval,
tolerance: tolerance ?? scheduler.minimumTolerance,
scheduler: scheduler,
options: options)
}
}
extension Publishers {
/// A publisher that delays delivery of elements and completion
/// to the downstream receiver.
public struct Delay<Upstream: Publisher, Context: Scheduler>: Publisher {
public typealias Output = Upstream.Output
public typealias Failure = Upstream.Failure
/// The publisher that this publisher receives elements from.
public let upstream: Upstream
/// The amount of time to delay.
public let interval: Context.SchedulerTimeType.Stride
/// The allowed tolerance in firing delayed events.
public let tolerance: Context.SchedulerTimeType.Stride
/// The scheduler to deliver the delayed events.
public let scheduler: Context
public let options: Context.SchedulerOptions?
public init(upstream: Upstream,
interval: Context.SchedulerTimeType.Stride,
tolerance: Context.SchedulerTimeType.Stride,
scheduler: Context,
options: Context.SchedulerOptions? = nil)
{
self.upstream = upstream
self.interval = interval
self.tolerance = tolerance
self.scheduler = scheduler
self.options = options
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Upstream.Failure == Downstream.Failure,
Upstream.Output == Downstream.Input
{
upstream.subscribe(Inner(self, downstream: subscriber))
}
}
}
extension Publishers.Delay {
private final class Inner<Downstream: Subscriber>
: Subscriber,
Subscription
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
{
// NOTE: This class has been audited for thread safety
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
fileprivate typealias Delay = Publishers.Delay<Upstream, Context>
private enum State {
case ready(Delay, Downstream)
case subscribed(Delay, Downstream, Subscription)
case terminal
}
private let lock = UnfairLock.allocate()
private var state: State
private let downstreamLock = UnfairRecursiveLock.allocate()
fileprivate init(_ publisher: Delay, downstream: Downstream) {
state = .ready(publisher, downstream)
}
deinit {
lock.deallocate()
downstreamLock.deallocate()
}
private func schedule(_ delay: Delay, work: @escaping () -> Void) {
delay
.scheduler
.schedule(after: delay.scheduler.now.advanced(by: delay.interval),
tolerance: delay.tolerance,
options: delay.options,
work)
}
func receive(subscription: Subscription) {
lock.lock()
guard case let .ready(delay, downstream) = state else {
lock.unlock()
subscription.cancel()
return
}
state = .subscribed(delay, downstream, subscription)
lock.unlock()
downstreamLock.lock()
downstream.receive(subscription: self)
downstreamLock.unlock()
}
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
lock.lock()
guard case let .subscribed(delay, downstream, _) = state else {
lock.unlock()
return .none
}
lock.unlock()
schedule(delay) {
self.scheduledReceive(input, downstream: downstream)
}
return .none
}
private func scheduledReceive(_ input: Upstream.Output, downstream: Downstream) {
downstreamLock.lock()
let newDemand = downstream.receive(input)
downstreamLock.unlock()
guard newDemand > 0 else {
return
}
lock.lock()
guard case let .subscribed(_, _, subscription) = state else {
lock.unlock()
return
}
lock.unlock()
subscription.request(newDemand)
}
func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
guard case let .subscribed(delay, downstream, _) = state else {
lock.unlock()
return
}
state = .terminal
lock.unlock()
schedule(delay) {
self.scheduledReceive(completion: completion, downstream: downstream)
}
}
private func scheduledReceive(completion: Subscribers.Completion<Failure>,
downstream: Downstream) {
downstreamLock.lock()
downstream.receive(completion: completion)
downstreamLock.unlock()
}
func request(_ demand: Subscribers.Demand) {
lock.lock()
guard case let .subscribed(_, _, subscription) = state else {
lock.unlock()
return
}
lock.unlock()
subscription.request(demand)
}
func cancel() {
lock.lock()
guard case let .subscribed(_, _, subscription) = state else {
lock.unlock()
return
}
state = .terminal
lock.unlock()
subscription.cancel()
}
}
}
@@ -5,8 +5,6 @@
// Created by Sven Weidauer on 03.10.2019.
//
import COpenCombineHelpers
extension Publisher {
/// Omits the specified number of elements before republishing subsequent elements.
///
@@ -0,0 +1,267 @@
//
// Publishers.DropUntilOutput.swift
//
//
// Created by Sergej Jaskiewicz on 24.12.2019.
//
extension Publisher {
/// Ignores elements from the upstream publisher until it receives an element from
/// a second publisher.
///
/// This publisher requests a single value from the upstream publisher, and it ignores
/// (drops) all elements from that publisher until the upstream publisher produces
/// a value. After the `other` publisher produces an element, this publisher cancels
/// its subscription to the `other` publisher, and allows events from the `upstream`
/// publisher to pass through.
/// After this publisher receives a subscription from the upstream publisher, it
/// passes through backpressure requests from downstream to the upstream publisher.
/// If the upstream publisher acts on those requests before the other publisher
/// produces an item, this publisher drops the elements it receives from the upstream
/// publisher.
///
/// - Parameter publisher: A publisher to monitor for its first emitted element.
/// - Returns: A publisher that drops elements from the upstream publisher until the
/// `other` publisher produces a value.
public func drop<Other: Publisher>(
untilOutputFrom publisher: Other
) -> Publishers.DropUntilOutput<Self, Other> where Failure == Other.Failure {
return .init(upstream: self, other: publisher)
}
}
extension Publishers {
/// A publisher that ignores elements from the upstream publisher until it receives
/// an element from second publisher.
public struct DropUntilOutput<Upstream: Publisher, Other: Publisher>: Publisher
where Upstream.Failure == Other.Failure
{
public typealias Output = Upstream.Output
public typealias Failure = Upstream.Failure
/// The publisher that this publisher receives elements from.
public let upstream: Upstream
/// A publisher to monitor for its first emitted element.
public let other: Other
/// Creates a publisher that ignores elements from the upstream publisher until
/// it receives an element from another publisher.
///
/// - Parameters:
/// - upstream: A publisher to drop elements from while waiting for another
/// publisher to emit elements.
/// - other: A publisher to monitor for its first emitted element.
public init(upstream: Upstream, other: Other) {
self.upstream = upstream
self.other = other
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Upstream.Output == Downstream.Input,
Other.Failure == Downstream.Failure
{
let inner = Inner(downstream: subscriber)
subscriber.receive(subscription: inner)
other.subscribe(Inner.OtherSubscriber(inner: inner))
upstream.subscribe(inner)
}
}
}
extension Publishers.DropUntilOutput: Equatable
where Upstream: Equatable, Other: Equatable {}
extension Publishers.DropUntilOutput {
fileprivate final class Inner<Downstream: Subscriber>
: Subscriber,
Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private let downstream: Downstream
private var triggered = false
private let lock = UnfairLock.allocate()
private let downstreamLock = UnfairRecursiveLock.allocate()
private var upstreamSubscription: Subscription?
private var pendingDemand = Subscribers.Demand.none
private var otherSubscription: Subscription?
private var otherFinished = false
private var cancelled = false
init(downstream: Downstream) {
self.downstream = downstream
}
deinit {
lock.deallocate()
downstreamLock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard upstreamSubscription == nil && !cancelled else {
lock.unlock()
subscription.cancel()
return
}
upstreamSubscription = subscription
if pendingDemand > 0 {
lock.unlock()
subscription.request(pendingDemand)
} else {
lock.unlock()
}
}
func receive(_ input: Input) -> Subscribers.Demand {
lock.lock()
if !triggered || cancelled {
pendingDemand -= 1
lock.unlock()
return .none
}
lock.unlock()
downstreamLock.lock()
let newDemand = downstream.receive(input)
downstreamLock.unlock()
return newDemand
}
func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
if cancelled {
lock.unlock()
return
}
cancelled = true
lock.unlock()
downstreamLock.lock()
downstream.receive(completion: completion)
downstreamLock.unlock()
}
private func receiveOther(subscription: Subscription) {
// Combine doesn't lock here
guard otherSubscription == nil else {
subscription.cancel()
return
}
otherSubscription = subscription
subscription.request(.max(1))
}
private func receiveOther(_ input: Other.Output) -> Subscribers.Demand {
lock.lock()
triggered = true
otherSubscription = nil
lock.unlock()
return .none
}
private func receiveOther(completion: Subscribers.Completion<Other.Failure>) {
lock.lock()
if triggered {
otherSubscription = nil
lock.unlock()
return
}
otherFinished = true
if let upstreamSubscription = self.upstreamSubscription {
self.upstreamSubscription = nil
lock.unlock()
upstreamSubscription.cancel()
} else {
lock.unlock()
}
downstreamLock.lock()
downstream.receive(completion: completion)
downstreamLock.unlock()
}
func request(_ demand: Subscribers.Demand) {
lock.lock()
pendingDemand += demand
if let subscription = upstreamSubscription {
lock.unlock()
subscription.request(demand)
} else {
lock.unlock()
}
}
func cancel() {
lock.lock()
let upstreamSubscription = self.upstreamSubscription
let otherSubscription = self.otherSubscription
self.upstreamSubscription = nil
self.otherSubscription = nil
cancelled = true
lock.unlock()
upstreamSubscription?.cancel()
otherSubscription?.cancel()
}
var description: String { return "DropUntilOutput" }
var customMirror: Mirror {
return Mirror(self, children: EmptyCollection())
}
var playgroundDescription: Any { return description }
}
}
extension Publishers.DropUntilOutput.Inner {
fileprivate struct OtherSubscriber
: Subscriber,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
{
let inner: Publishers.DropUntilOutput<Upstream, Other>.Inner<Downstream>
var combineIdentifier: CombineIdentifier {
return inner.combineIdentifier
}
func receive(subscription: Subscription) {
inner.receiveOther(subscription: subscription)
}
func receive(_ input: Other.Output) -> Subscribers.Demand {
return inner.receiveOther(input)
}
func receive(completion: Subscribers.Completion<Other.Failure>) {
inner.receiveOther(completion: completion)
}
var description: String { return "DropUntilOutput" }
var customMirror: Mirror {
return Mirror(self, children: EmptyCollection())
}
var playgroundDescription: Any { return description }
}
}
@@ -5,6 +5,40 @@
// Created by Sergej Jaskiewicz on 16.06.2019.
//
extension Publisher {
/// Omits elements from the upstream publisher until a given closure returns false,
/// before republishing all remaining elements.
///
/// - Parameter predicate: A closure that takes an element as a parameter and returns
/// a Boolean value indicating whether to drop the element from the 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 +63,7 @@ extension Publishers {
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Failure == Downstream.Failure, Output == Downstream.Input
{
let inner = Inner(downstream: subscriber, predicate: catching(predicate))
upstream.subscribe(inner)
upstream.subscribe(Inner(downstream: subscriber, predicate: predicate))
}
}
@@ -56,148 +89,249 @@ extension Publishers {
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Output == Downstream.Input, Downstream.Failure == Error
{
let inner = Inner(downstream: subscriber, predicate: catching(predicate))
upstream.subscribe(inner)
upstream.subscribe(Inner(downstream: subscriber, predicate: predicate))
}
}
}
private class _DropWhile<Upstream: Publisher, Downstream: Subscriber>
: OperatorSubscription<Downstream>,
Subscription
where Upstream.Output == Downstream.Input
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
typealias Predicate = (Input) -> Result<Bool, Downstream.Failure>
/// The predicate is reset to `nil` as soon as it returns `false`.
var predicate: Predicate?
var isCompleted = false
init(downstream: Downstream, predicate: @escaping Predicate) {
self.predicate = predicate
super.init(downstream: downstream)
}
func receive(subscription: Subscription) {
upstreamSubscription = subscription
downstream.receive(subscription: self)
}
func receive(_ input: Input) -> Subscribers.Demand {
guard upstreamSubscription != nil else {
return .none
}
guard let predicate = self.predicate else {
return downstream.receive(input)
}
// NOTE: until the predicate returns false, we will ask the upstream publisher
// for elements one by one.
//
// However, IF the downstream requests anything, we accumulate this demand in the
// `demand` property so that later we can provide the downstream with the correct
// amount of values.
//
// As soon as the predicate returns false, we switch to the mode where
// we just forward all the requests from the downstream to the upstream.
switch predicate(input) {
case .success(true):
return .max(1)
case .success(false):
// Okay, we hit the first element not satisfying the predicate,
// from now on we just republish the values to the downstream.
self.predicate = nil
return downstream.receive(input)
case .failure(let error):
downstream.receive(completion: .failure(error))
isCompleted = true
cancel()
return .none
}
}
func request(_ demand: Subscribers.Demand) {
upstreamSubscription?.request(demand)
}
override func cancel() {
upstreamSubscription?.cancel()
upstreamSubscription = nil
isCompleted = true
// Don't zero out downstream, that's what Combine does (probably a bug)
}
}
extension Publishers.DropWhile {
private final class Inner<Downstream: Subscriber>
: _DropWhile<Upstream, Downstream>,
: Subscriber,
Subscription,
CustomStringConvertible,
Subscriber
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Upstream.Output == Downstream.Input, Downstream.Failure == Upstream.Failure
{
var description: String { return "DropWhile" }
// NOTE: This class has been audited for thread safety.
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private var status = SubscriptionStatus.awaitingSubscription
private let downstream: Downstream
private var predicate: ((Input) -> Bool)?
private var dropping = true
private let lock = UnfairLock.allocate()
fileprivate init(downstream: Downstream, predicate: @escaping (Input) -> Bool) {
self.downstream = downstream
self.predicate = predicate
}
deinit {
lock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard case .awaitingSubscription = status else {
lock.unlock()
subscription.cancel()
return
}
status = .subscribed(subscription)
lock.unlock()
downstream.receive(subscription: self)
}
func receive(_ input: Input) -> Subscribers.Demand {
lock.lock()
guard case .subscribed = status, let shouldDrop = predicate else {
lock.unlock()
return .none
}
let dropping = self.dropping
lock.unlock()
if dropping {
if shouldDrop(input) {
return .max(1)
} else {
lock.lock()
self.dropping = false
lock.unlock()
}
}
return downstream.receive(input)
}
func receive(completion: Subscribers.Completion<Failure>) {
guard !isCompleted else { return }
lock.lock()
guard case .subscribed = status else {
lock.unlock()
return
}
status = .terminal
predicate = nil
lock.unlock()
downstream.receive(completion: completion)
isCompleted = true
}
func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
lock.lock()
guard case let .subscribed(subscription) = status else {
lock.unlock()
return
}
lock.unlock()
subscription.request(demand)
}
func cancel() {
lock.lock()
guard case let .subscribed(subscription) = status else {
lock.unlock()
return
}
status = .terminal
predicate = nil
lock.unlock()
subscription.cancel()
}
var description: String { return "DropWhile" }
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
var playgroundDescription: Any { return description }
}
}
extension Publishers.TryDropWhile {
private final class Inner<Downstream: Subscriber>
: _DropWhile<Upstream, Downstream>,
: Subscriber,
Subscription,
CustomStringConvertible,
Subscriber
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Upstream.Output == Downstream.Input, Downstream.Failure == Error
{
var description: String { return "TryDropWhile" }
// NOTE: This class has been audited for thread safety.
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private var status = SubscriptionStatus.awaitingSubscription
private let downstream: Downstream
private var predicate: ((Input) throws -> Bool)?
private var dropping = true
private var finished = false
private let lock = UnfairLock.allocate()
fileprivate init(downstream: Downstream,
predicate: @escaping (Input) throws -> Bool) {
self.downstream = downstream
self.predicate = predicate
}
deinit {
lock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard case .awaitingSubscription = status else {
lock.unlock()
subscription.cancel()
return
}
status = .subscribed(subscription)
lock.unlock()
downstream.receive(subscription: self)
}
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
lock.lock()
guard case let .subscribed(subscription) = status,
let shouldDrop = predicate else {
lock.unlock()
return .none
}
let dropping = self.dropping
lock.unlock()
if dropping {
do {
if try shouldDrop(input) {
return .max(1)
} else {
lock.lock()
self.dropping = false
lock.unlock()
}
} catch {
lock.lock()
status = .terminal
predicate = nil
finished = true
lock.unlock()
subscription.cancel()
downstream.receive(completion: .failure(error))
return .none
}
}
return downstream.receive(input)
}
func receive(completion: Subscribers.Completion<Failure>) {
guard !isCompleted else { return }
downstream.receive(completion: completion.eraseError())
isCompleted = true
lock.lock()
guard case .subscribed = status else {
lock.unlock()
return
}
status = .terminal
let wasFinished = finished
finished = true
lock.unlock()
if !wasFinished {
downstream.receive(completion: completion.eraseError())
}
}
}
}
extension Publisher {
/// Omits elements from the upstream publisher until a given closure returns false,
/// before republishing all remaining elements.
///
/// - Parameter predicate: A closure that takes an element as a parameter and returns
/// a Boolean value indicating whether to drop the element from the 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" }
}
}
@@ -24,7 +24,7 @@ extension Publisher {
/// - Parameter predicate: A closure that takes an element as a parameter and
/// returns a Boolean value that indicates whether to publish the element.
/// - Returns: A publisher that only publishes the first element of a stream
/// that satifies the predicate.
/// that satisfies the predicate.
public func first(
where predicate: @escaping (Output) -> Bool
) -> Publishers.FirstWhere<Self> {
@@ -40,7 +40,7 @@ extension Publisher {
/// - Parameter predicate: A closure that takes an element as a parameter and
/// returns a Boolean value that indicates whether to publish the element.
/// - Returns: A publisher that only publishes the first element of a stream
/// that satifies the predicate.
/// that satisfies the predicate.
public func tryFirst(
where predicate: @escaping (Output) throws -> Bool
) -> Publishers.TryFirstWhere<Self> {
@@ -4,10 +4,7 @@
// Created by Eric Patey on 16.08.2019.
//
import COpenCombineHelpers
extension Publisher {
/// Transforms all elements from an upstream publisher into a new or existing
/// publisher.
///
@@ -25,23 +22,18 @@ extension Publisher {
_ transform: @escaping (Output) -> Child
) -> Publishers.FlatMap<Child, Self>
where Result == Child.Output, Failure == Child.Failure {
return Publishers.FlatMap(upstream: self,
maxPublishers: maxPublishers,
transform: transform)
return .init(upstream: self,
maxPublishers: maxPublishers,
transform: transform)
}
}
extension Publishers {
public struct FlatMap<Child: Publisher, Upstream: Publisher>: Publisher
where Child.Failure == Upstream.Failure
{
/// The kind of values published by this publisher.
public typealias Output = Child.Output
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = Upstream.Failure
public let upstream: Upstream
@@ -56,363 +48,377 @@ extension Publishers {
self.maxPublishers = maxPublishers
self.transform = transform
}
/// This function is called to attach the specified `Subscriber` to this
/// `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Child.Output == Downstream.Input, Upstream.Failure == Downstream.Failure
{
let inner = Inner(downstream: subscriber,
maxPublishers: maxPublishers,
transform: transform)
map: transform)
subscriber.receive(subscription: inner)
upstream.subscribe(inner)
}
}
}
extension Publishers.FlatMap {
fileprivate final class Inner<Downstream: Subscriber>
: CustomStringConvertible,
Cancellable
private final class Inner<Downstream: Subscriber>
: Subscriber,
Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Child.Output, Downstream.Failure == Upstream.Failure
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private typealias PendingValue = (
value: Downstream.Input,
// If the value was buffered at the time it became available, and the child's
// demand was left at `.none` we keep track of the child in `pausedChild` so
// that we can demand some more of it after sending this value.
pausedChild: ChildSubscriber?
)
private typealias SubscriptionIndex = Int
/// All requests to this subscription should be made with the `outerLock`
/// acquired.
private var outerSubscription: Subscription?
/// The lock for requesting from `outerSubscription`.
private let outerLock = UnfairRecursiveLock.allocate()
/// The lock for modifying the state. All mutable state here should be
/// read and modified with this lock acquired.
/// The only exception is the `downstreamRecursive` field, which is guarded
/// by the `downstreamLock`.
private let lock = UnfairLock.allocate()
/// All the calls to the downstream subscriber should be made with this lock
/// acquired.
private let downstreamLock = UnfairRecursiveLock.allocate()
private let downstream: Downstream
private var downstreamDemand = Subscribers.Demand.none
/// This variable is set to `true` whenever we call `downstream.receive(_:)`,
/// and then set back to `false`.
private var downstreamRecursive = false
private var innerRecursive = false
private var subscriptions = [SubscriptionIndex : Subscription]()
private var nextInnerIndex: SubscriptionIndex = 0
private var pendingSubscriptions = 0
private var buffer = [(SubscriptionIndex, Child.Output)]()
private let maxPublishers: Subscribers.Demand
private let transform: (Input) -> Child
// Locking rules for this class.
// - All mutable state must only be accessed while `lock` is held.
// - In order to avoid any deadlock potential, it is absolutely forbidden to have
// any sort of call out from this class while the lock is held. This is why
// the draining of the work queue uses a relatively complex pattern.
private var downstream: Downstream?
private var childSubscribers = Set<ChildSubscriber>()
private var downstreamDemand = Subscribers.Demand.unlimited
private var valuesToSend = [PendingValue]()
private var queueIsBeingProcessed = false
private var sendFinishedAfterDrainingQueue = false
private var upstreamSubscription: Subscription?
var description: String { return "FlatMap" }
private let map: (Input) -> Child
private var cancelledOrCompleted = false
private var outerFinished = false
init(downstream: Downstream,
maxPublishers: Subscribers.Demand,
transform: @escaping (Upstream.Output) -> Child) {
map: @escaping (Upstream.Output) -> Child) {
self.downstream = downstream
self.maxPublishers = maxPublishers
self.transform = transform
self.map = map
}
deinit {
outerLock.deallocate()
lock.deallocate()
downstreamLock.deallocate()
}
final func cancel() {
// MARK: - Subscriber
let (upstreamToCancel, childrenToCancel) = lock
.do { () -> (Subscription?, Set<ChildSubscriber>) in
let upstreamToCancel = upstreamSubscription
upstreamSubscription = nil
return (upstreamToCancel, lockedDeactivateAndReturnChildrenToCancel())
fileprivate func receive(subscription: Subscription) {
lock.lock()
guard outerSubscription == nil, !cancelledOrCompleted else {
lock.unlock()
subscription.cancel()
return
}
outerSubscription = subscription
lock.unlock()
subscription.request(maxPublishers)
}
fileprivate func receive(_ input: Upstream.Output) -> Subscribers.Demand {
lock.lock()
let cancelledOrCompleted = self.cancelledOrCompleted
lock.unlock()
if cancelledOrCompleted {
return .none
}
let child = map(input)
lock.lock()
let innerIndex = nextInnerIndex
nextInnerIndex += 1
pendingSubscriptions += 1
lock.unlock()
child.subscribe(Side(index: innerIndex, inner: self))
return .none
}
fileprivate func receive(completion: Subscribers.Completion<Child.Failure>) {
outerSubscription = nil
lock.lock()
outerFinished = true
switch completion {
case .finished:
releaseLockThenSendCompletionDownstreamIfNeeded(outerFinished: true)
return
case .failure:
let wasAlreadyCancelledOrCompleted = cancelledOrCompleted
cancelledOrCompleted = true
for (_, subscription) in subscriptions {
subscription.cancel()
}
upstreamToCancel?.cancel()
cancelChildren(childrenToCancel)
subscriptions = [:]
lock.unlock()
if wasAlreadyCancelledOrCompleted {
return
}
downstreamLock.lock()
downstream.receive(completion: completion)
downstreamLock.unlock()
}
}
}
}
// Private implementation
extension Publishers.FlatMap.Inner {
// MARK: - Subscription
private func deactivate() {
cancelChildren(lock.do(lockedDeactivateAndReturnChildrenToCancel))
}
// Must be called with lock held.
private func lockedDeactivateAndReturnChildrenToCancel() -> Set<ChildSubscriber> {
downstream = nil
downstreamDemand = .none
let result = childSubscribers
childSubscribers.removeAll()
upstreamSubscription = nil
return result
}
private func cancelChildren(_ childrenToCancel: Set<ChildSubscriber>) {
childrenToCancel.forEach { $0.cancel() }
}
/// In a thread-safe way, this function performs the passed in work with the lock held
/// and then checks to see if either upstream or any of the child subscriptions remain
/// active. If there are no remaining active subscriptions, it enqueues the sending
/// of `.finished` downstream using the processing queue.
/// - Parameter lockedWork: block to be formed with the lock held.
private final func maybeSendFinishedAfterExecutingWork(lockedWork: () -> Void) {
let shouldProcessQueue: Bool = lock.do {
lockedWork()
if childSubscribers.isEmpty && upstreamSubscription == nil {
sendFinishedAfterDrainingQueue = true
if !queueIsBeingProcessed {
queueIsBeingProcessed = true
return true
fileprivate func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
if downstreamRecursive {
// downstreamRecursive being true means that downstreamLock
// is already acquired.
downstreamDemand += demand
return
}
lock.lock()
if cancelledOrCompleted {
lock.unlock()
return
}
if demand == .unlimited {
downstreamDemand = .unlimited
let buffer = self.buffer
self.buffer = []
let subscriptions = self.subscriptions
lock.unlock()
downstreamLock.lock()
downstreamRecursive = true
for (_, childOutput) in buffer {
_ = downstream.receive(childOutput)
}
downstreamRecursive = false
downstreamLock.unlock()
for (_, subscription) in subscriptions {
subscription.request(.unlimited)
}
lock.lock()
} else {
downstreamDemand += demand
while !buffer.isEmpty && downstreamDemand > 0 {
// FIXME: This has quadratic complexity.
// This is what Combine does.
// Can we improve perfomance by using e. g. Deque instead of Array?
// Or array's cache locality makes this solution more efficient?
// Must benchmark before optimizing!
//
// https://www.cocoawithlove.com/blog/2016/09/22/deque.html
let (index, value) = buffer.removeFirst()
downstreamDemand -= 1
let subscription = subscriptions[index]
lock.unlock()
downstreamLock.lock()
downstreamRecursive = true
let additionalDemand = downstream.receive(value)
downstreamRecursive = false
downstreamLock.unlock()
if additionalDemand != .none {
lock.lock()
downstreamDemand += additionalDemand
lock.unlock()
}
if let subscription = subscription {
innerRecursive = true
subscription.request(.max(1))
innerRecursive = false
}
lock.lock()
}
}
releaseLockThenSendCompletionDownstreamIfNeeded(outerFinished: outerFinished)
}
fileprivate func cancel() {
lock.lock()
cancelledOrCompleted = true
let subscriptions = self.subscriptions
self.subscriptions = [:]
lock.unlock()
for (_, subscription) in subscriptions {
subscription.cancel()
}
// Combine doesn't acquire the lock here. Weird.
outerSubscription?.cancel()
outerSubscription = nil
}
// MARK: - Reflection
fileprivate var description: String { return "FlatMap" }
fileprivate var customMirror: Mirror {
return Mirror(self, children: EmptyCollection())
}
fileprivate var playgroundDescription: Any { return description }
// MARK: - Private
private func receiveInner(subscription: Subscription,
_ index: SubscriptionIndex) {
lock.lock()
pendingSubscriptions -= 1
subscriptions[index] = subscription
let demand = downstreamDemand == .unlimited
? Subscribers.Demand.unlimited
: .max(1)
lock.unlock()
subscription.request(demand)
}
private func receiveInner(_ input: Child.Output,
_ index: SubscriptionIndex) -> Subscribers.Demand {
lock.lock()
if downstreamDemand == .unlimited {
lock.unlock()
downstreamLock.lock()
downstreamRecursive = true
_ = downstream.receive(input)
downstreamRecursive = false
downstreamLock.unlock()
return .none
}
if downstreamDemand == .none || innerRecursive {
buffer.append((index, input))
lock.unlock()
return .none
}
downstreamDemand -= 1
lock.unlock()
downstreamLock.lock()
downstreamRecursive = true
let newDemand = downstream.receive(input)
downstreamRecursive = false
downstreamLock.unlock()
if newDemand > 0 {
lock.lock()
downstreamDemand += newDemand
lock.unlock()
}
return .max(1)
}
private func receiveInner(completion: Subscribers.Completion<Child.Failure>,
_ index: SubscriptionIndex) {
switch completion {
case .finished:
lock.lock()
subscriptions.removeValue(forKey: index)
let downstreamCompleted = releaseLockThenSendCompletionDownstreamIfNeeded(
outerFinished: outerFinished
)
if !downstreamCompleted {
requestOneMorePublisher()
}
case .failure:
lock.lock()
if cancelledOrCompleted {
lock.unlock()
return
}
cancelledOrCompleted = true
let subscriptions = self.subscriptions
self.subscriptions = [:]
lock.unlock()
for (i, subscription) in subscriptions where i != index {
subscription.cancel()
}
downstreamLock.lock()
downstream.receive(completion: completion)
downstreamLock.unlock()
}
}
private func requestOneMorePublisher() {
if maxPublishers != .unlimited {
outerLock.lock()
outerSubscription?.request(.max(1))
outerLock.unlock()
}
}
/// - Precondition: `lock` is acquired
/// - Postcondition: `lock` is released
///
/// - Returns: `true` if a completion was sent downstream
@discardableResult
private func releaseLockThenSendCompletionDownstreamIfNeeded(
outerFinished: Bool
) -> Bool {
#if DEBUG
lock.assertOwner() // Sanity check
#endif
if !cancelledOrCompleted && outerFinished && buffer.isEmpty &&
subscriptions.count + pendingSubscriptions == 0 {
cancelledOrCompleted = true
lock.unlock()
downstreamLock.lock()
downstream.receive(completion: .finished)
downstreamLock.unlock()
return true
}
lock.unlock()
return false
}
if shouldProcessQueue {
processQueue()
}
}
// MARK: - Side
private func receivedCompletion(_ completion: Subscribers.Completion<Failure>,
fromChild child: ChildSubscriber) {
switch completion {
case .finished:
removeActiveSubscription(forChild: child)
case .failure:
downstream?.receive(completion: completion)
deactivate()
}
}
private struct Side: Subscriber,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible {
private let index: SubscriptionIndex
private let inner: Inner
fileprivate let combineIdentifier = CombineIdentifier()
private func removeActiveSubscription(forChild child: ChildSubscriber) {
maybeSendFinishedAfterExecutingWork { childSubscribers.remove(child) }
}
private func receivedValue(_ value: Child.Output,
fromChild child: ChildSubscriber) -> Subscribers.Demand {
// When receiving a value from a child, we need to determine what additional
// demand to return to the child. Apple's logic for this determination is as
// follows:
// - If we are in `.unlimited` mode, we always request `.none` additional
// else
// - If there is a surplus relative to the demand, we request `.none`
// else
// - There is not yet a surplus, so request `.max(1)` more from the child
let (surplusAvailable, processTheQueue): (Bool, Bool) = lock.do {
// If we already have enough values to satisfy the demand, we "buffer" this
// child value establishing a surplus.
if downstreamDemand <= valuesToSend.count {
valuesToSend.append((value, child))
return (surplusAvailable: true, processTheQueue: false)
} else {
valuesToSend.append((value, nil))
if queueIsBeingProcessed {
return (surplusAvailable: false, processTheQueue: false)
}
queueIsBeingProcessed = true
return (surplusAvailable: false, processTheQueue: true)
}
}
let demandResult = surplusAvailable || demandForChild() == .unlimited
? Subscribers.Demand.none
: .max(1)
if processTheQueue {
processQueue()
}
return demandResult
}
private func demandForChild() -> Subscribers.Demand {
return downstreamDemand == .unlimited ? .unlimited : .max(1)
}
private enum QueueWorkStatus {
case noWork
case sendFinish
case sendValues(values: ArraySlice<PendingValue>)
}
private func processQueue() {
assert(queueIsBeingProcessed)
// We loop processing the queue in case somebody put stuff on the queue while we
// were sending values with the lock unlocked.
while true {
let work: QueueWorkStatus = lock.do {
if downstreamDemand == .none || valuesToSend.isEmpty {
if sendFinishedAfterDrainingQueue && valuesToSend.isEmpty {
return .sendFinish
} else {
queueIsBeingProcessed = false
return .noWork
}
}
let countToSend = min(valuesToSend.count, downstreamDemand.max ?? .max)
let result = valuesToSend[0..<countToSend]
// TODO: Consider an alternative storage to avoid O(n) removeFirst
valuesToSend.removeFirst(countToSend)
downstreamDemand -= countToSend
return .sendValues(values: result)
fileprivate init(index: SubscriptionIndex, inner: Inner) {
self.index = index
self.inner = inner
}
guard let downstream = downstream else { return }
switch work {
case .noWork:
return
case .sendFinish:
downstream.receive(completion: .finished)
deactivate()
return
case .sendValues(let values):
var newDemand = Subscribers.Demand.none
values.forEach {
newDemand += downstream.receive($0.value)
// pausedChild is present only if the value was buffered and the
// child's demand was left at `.none`. In that case, once we send the
// buffered value, we need to tell the child to get another value.
$0.pausedChild?.request(.max(1))
}
if newDemand != .none {
lock.do { downstreamDemand += newDemand }
}
fileprivate func receive(subscription: Subscription) {
inner.receiveInner(subscription: subscription, index)
}
fileprivate func receive(_ input: Child.Output) -> Subscribers.Demand {
return inner.receiveInner(input, index)
}
fileprivate func receive(completion: Subscribers.Completion<Child.Failure>) {
inner.receiveInner(completion: completion, index)
}
fileprivate var description: String { return "FlatMap" }
fileprivate var customMirror: Mirror {
let children = CollectionOfOne<Mirror.Child>(
("parentSubscription", inner.combineIdentifier)
)
return Mirror(self, children: children)
}
fileprivate var playgroundDescription: Any { return description }
}
}
}
// This `Subscriber` implementation is for `FlatMap`'s upstream subscription
extension Publishers.FlatMap.Inner: Subscriber {
fileprivate func receive(subscription: Subscription) {
upstreamSubscription = subscription
downstream?.receive(subscription: self)
subscription.request(maxPublishers)
}
/// Receive a new value from the upstream subscription. A new child subscription
/// will be made on the `Child` that the input value is transformed into.
/// - Parameter input: a value to be transformed by `transform`
fileprivate func receive(_ input: Input) -> Subscribers.Demand {
let newChildSubscriber = ChildSubscriber(parent: self)
lock.do { _ = childSubscribers.insert(newChildSubscriber) }
self.transform(input).subscribe(newChildSubscriber)
return .none
}
fileprivate func receive(completion: Subscribers.Completion<Failure>) {
switch completion {
case .finished:
maybeSendFinishedAfterExecutingWork { upstreamSubscription = nil }
case .failure:
downstream?.receive(completion: completion)
deactivate()
}
}
}
// Inner is the `Subscription` for `Downstream`
extension Publishers.FlatMap.Inner: Subscription {
fileprivate func request(_ demand: Subscribers.Demand) {
let (drainTheQueue, becameUnlimited) = lock.do { () -> (Bool, Bool) in
let becameUnlimited = demand == .unlimited && downstreamDemand != .unlimited
downstreamDemand = demand
defer { queueIsBeingProcessed = true }
return (!queueIsBeingProcessed, becameUnlimited)
}
if becameUnlimited {
// TODO: This code isn't yet thread safe. The correct change is to do this
// through the queue just like sending values and finished. Finished is
// done through the queue as a bit of a hack. The right design is to have
// an enum of actions on the queue. That enum will include (send value,
// send finished, set child demand).
let newChildDemand = demandForChild()
childSubscribers.forEach { $0.request(newChildDemand) }
}
if drainTheQueue {
processQueue()
}
}
}
extension Publishers.FlatMap.Inner {
/// ChildSubscriber is needed to help implement the backpressure/demand strategy.
/// Specifically, a custom subscriber is needed to manage the demand of the child
/// subscription:
/// - Send .max(1) request when the subscription is received
/// - Send .max(1) request when downstream subscriber demands more and a previously
/// buffered value from the child was sent. (When the value was buffered, the
/// child's demand reached .none - effectively pausing the child.)
fileprivate final class ChildSubscriber: Hashable {
internal typealias Input = Downstream.Input
internal typealias Failure = Downstream.Failure
private var _upstreamSubscription: Subscription?
private unowned let _parent: Publishers.FlatMap<Child, Upstream>.Inner<Downstream>
init(parent: Publishers.FlatMap<Child, Upstream>.Inner<Downstream>) {
_parent = parent
}
fileprivate func request(_ demand: Subscribers.Demand) {
_upstreamSubscription?.request(demand)
}
public static func == (lhs: ChildSubscriber, rhs: ChildSubscriber) -> Bool {
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
}
public func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(self))
}
}
}
extension Publishers.FlatMap.Inner.ChildSubscriber: Cancellable {
fileprivate func cancel() {
_upstreamSubscription?.cancel()
_upstreamSubscription = nil
}
}
extension Publishers.FlatMap.Inner.ChildSubscriber: Subscriber {
fileprivate func receive(subscription: Subscription) {
if _upstreamSubscription == nil {
_upstreamSubscription = subscription
subscription.request(_parent.demandForChild())
} else {
assertionFailure()
subscription.cancel()
}
}
fileprivate func receive(_ input: Input) -> Subscribers.Demand {
return _parent.receivedValue(input, fromChild: self)
}
fileprivate func receive(completion: Subscribers.Completion<Failure>) {
_parent.receivedCompletion(completion, fromChild: self)
}
}
@@ -0,0 +1,193 @@
//
// Publishers.HandleEvents.swift
//
//
// Created by Sergej Jaskiewicz on 03.12.2019.
//
extension Publisher {
/// Performs the specified closures when publisher events occur.
///
/// - Parameters:
/// - receiveSubscription: A closure that executes when the publisher receives
/// the subscription from the upstream publisher. Defaults to `nil`.
/// - receiveOutput: A closure that executes when the publisher receives a value
/// from the upstream publisher. Defaults to `nil`.
/// - receiveCompletion: A closure that executes when the publisher receives
/// the completion from the upstream publisher. Defaults to `nil`.
/// - receiveCancel: A closure that executes when the downstream receiver cancels
/// publishing. Defaults to `nil`.
/// - receiveRequest: A closure that executes when the publisher receives a request
/// for more elements. Defaults to `nil`.
/// - Returns: A publisher that performs the specified closures when publisher events
/// occur.
public func handleEvents(
receiveSubscription: ((Subscription) -> Void)? = nil,
receiveOutput: ((Output) -> Void)? = nil,
receiveCompletion: ((Subscribers.Completion<Failure>) -> Void)? = nil,
receiveCancel: (() -> Void)? = nil,
receiveRequest: ((Subscribers.Demand) -> Void)? = nil
) -> Publishers.HandleEvents<Self> {
return .init(upstream: self,
receiveSubscription: receiveSubscription,
receiveOutput: receiveOutput,
receiveCompletion: receiveCompletion,
receiveCancel: receiveCancel,
receiveRequest: receiveRequest)
}
}
extension Publishers {
/// A publisher that performs the specified closures when publisher events occur.
public struct HandleEvents<Upstream: Publisher>: Publisher {
public typealias Output = Upstream.Output
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// A closure that executes when the publisher receives the subscription from
/// the upstream publisher.
public var receiveSubscription: ((Subscription) -> Void)?
/// A closure that executes when the publisher receives a value from the upstream
/// publisher.
public var receiveOutput: ((Upstream.Output) -> Void)?
/// A closure that executes when the publisher receives the completion from
/// the upstream publisher.
public var receiveCompletion:
((Subscribers.Completion<Upstream.Failure>) -> Void)?
/// A closure that executes when the downstream receiver cancels publishing.
public var receiveCancel: (() -> Void)?
/// A closure that executes when the publisher receives a request for more
/// elements.
public var receiveRequest: ((Subscribers.Demand) -> Void)?
public init(
upstream: Upstream,
receiveSubscription: ((Subscription) -> Void)? = nil,
receiveOutput: ((Output) -> Void)? = nil,
receiveCompletion: ((Subscribers.Completion<Failure>) -> Void)? = nil,
receiveCancel: (() -> Void)? = nil,
receiveRequest: ((Subscribers.Demand) -> Void)?
) {
self.upstream = upstream
self.receiveSubscription = receiveSubscription
self.receiveOutput = receiveOutput
self.receiveCompletion = receiveCompletion
self.receiveCancel = receiveCancel
self.receiveRequest = receiveRequest
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Upstream.Failure == Downstream.Failure,
Upstream.Output == Downstream.Input
{
let inner = Inner(self, downstream: subscriber)
subscriber.receive(subscription: inner)
upstream.subscribe(inner)
}
}
}
extension Publishers.HandleEvents {
private final class Inner<Downstream: Subscriber>
: Subscriber,
Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private var status = SubscriptionStatus.awaitingSubscription
private var pendingDemand = Subscribers.Demand.none
private let lock = UnfairLock.allocate()
private var events: Publishers.HandleEvents<Upstream>?
private let downstream: Downstream
init(_ events: Publishers.HandleEvents<Upstream>, downstream: Downstream) {
self.events = events
self.downstream = downstream
}
deinit {
lock.deallocate()
}
func receive(subscription: Subscription) {
events?.receiveSubscription?(subscription)
lock.lock()
guard case .awaitingSubscription = status else {
lock.unlock()
subscription.cancel()
return
}
status = .subscribed(subscription)
let pendingDemand = self.pendingDemand
self.pendingDemand = .none
lock.unlock()
if pendingDemand > 0 {
subscription.request(pendingDemand)
}
}
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
events?.receiveOutput?(input)
let newDemand = downstream.receive(input)
if newDemand > 0 {
events?.receiveRequest?(newDemand)
}
return newDemand
}
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
events?.receiveCompletion?(completion)
lock.lock()
events = nil
status = .terminal
lock.unlock()
downstream.receive(completion: completion)
}
func request(_ demand: Subscribers.Demand) {
events?.receiveRequest?(demand)
lock.lock()
if case let .subscribed(subscription) = status {
lock.unlock()
subscription.request(demand)
return
}
pendingDemand += demand
lock.unlock()
}
func cancel() {
events?.receiveCancel?()
lock.lock()
guard case let .subscribed(subscription) = status else {
lock.unlock()
return
}
events = nil
status = .terminal
lock.unlock()
subscription.cancel()
}
var description: String { return "HandleEvents" }
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
var playgroundDescription: Any { return description }
}
}
@@ -6,13 +6,13 @@
extension Publisher {
/// Ingores all upstream elements, but passes along a completion
/// Ignores all upstream elements, but passes along a completion
/// state (finished or failed).
///
/// The output type of this publisher is `Never`.
/// - Returns: A publisher that ignores all upstream elements.
public func ignoreOutput() -> Publishers.IgnoreOutput<Self> {
return Publishers.IgnoreOutput(upstream: self)
return .init(upstream: self)
}
}
@@ -21,12 +21,8 @@ extension Publishers {
/// state (finish or failed).
public struct IgnoreOutput<Upstream: Publisher>: Publisher {
/// The kind of values published by this publisher.
public typealias Output = Never
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
@@ -36,39 +32,52 @@ extension Publishers {
self.upstream = upstream
}
/// This function is called to attach the specified `Subscriber`
/// to this `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Failure == Upstream.Failure, Downstream.Input == Never {
let inner = Inner<Downstream>(downstream: subscriber)
upstream.subscribe(inner)
where Downstream.Failure == Upstream.Failure, Downstream.Input == Never
{
upstream.subscribe(Inner<Downstream>(downstream: subscriber))
}
}
}
extension Publishers.IgnoreOutput {
private final class Inner<Downstream: Subscriber>
: OperatorSubscription<Downstream>,
Subscriber,
: Subscriber,
Subscription,
CustomStringConvertible,
Subscription
where Downstream.Input == Never,
Downstream.Failure == Upstream.Failure
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Never, Downstream.Failure == Upstream.Failure
{
// NOTE: This class has been audited for thread safety
typealias Input = Upstream.Output
typealias Output = Never
typealias Failure = Upstream.Failure
var description: String { return "IgnoreOutput" }
private let downstream: Downstream
private var status = SubscriptionStatus.awaitingSubscription
private let lock = UnfairLock.allocate()
fileprivate init(downstream: Downstream) {
self.downstream = downstream
}
deinit {
lock.deallocate()
}
func receive(subscription: Subscription) {
upstreamSubscription = subscription
lock.lock()
guard case .awaitingSubscription = status else {
lock.unlock()
subscription.cancel()
return
}
status = .subscribed(subscription)
lock.unlock()
downstream.receive(subscription: self)
subscription.request(.unlimited)
}
@@ -78,6 +87,13 @@ extension Publishers.IgnoreOutput {
}
func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
guard case .subscribed = status else {
lock.unlock()
return
}
status = .terminal
lock.unlock()
downstream.receive(completion: completion)
}
@@ -85,6 +101,31 @@ extension Publishers.IgnoreOutput {
// ignore and requests from downstream since we'll never send
// any values
}
func cancel() {
lock.lock()
guard case let .subscribed(subscription) = status else {
lock.unlock()
return
}
status = .terminal
lock.unlock()
subscription.cancel()
}
var description: String { return "IgnoreOutput" }
var customMirror: Mirror {
lock.lock()
defer { lock.unlock() }
let children: [Mirror.Child] = [
("downstream", downstream),
("status", status)
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
}
}
@@ -5,8 +5,6 @@
// Created by Anton Nazarov on 25.06.2019.
//
import COpenCombineHelpers
extension Publisher {
/// Transforms all elements from the upstream publisher with a provided closure.
@@ -32,7 +30,7 @@ extension Publisher {
/// - Returns: A publisher that uses the provided closure to map elements from
/// the upstream publisher to new elements that it then publishes.
public func tryMap<Result>(
_ transform: @escaping (Self.Output) throws -> Result
_ transform: @escaping (Output) throws -> Result
) -> Publishers.TryMap<Self, Result> {
return Publishers.TryMap(upstream: self, transform: transform)
}
@@ -53,7 +53,7 @@ extension Publisher {
/// - Returns: A publisher that replaces any upstream failure with a
/// new error produced by the `transform` closure.
public func mapError<NewFailure: Error>(
_ transform: @escaping (Self.Failure) -> NewFailure
_ transform: @escaping (Failure) -> NewFailure
) -> Publishers.MapError<Self, NewFailure>
{
return Publishers.MapError(upstream: self, transform)
@@ -0,0 +1,166 @@
//
// Publishers.MeasureInterval.swift
//
//
// Created by Sergej Jaskiewicz on 03.12.2019.
//
extension Publisher {
/// Measures and emits the time interval between events received from an upstream
/// publisher.
///
/// The output type of the returned scheduler is the time interval of the provided
/// scheduler.
///
/// - Parameters:
/// - scheduler: The scheduler on which to deliver elements.
/// - options: Options that customize the delivery of elements.
/// - Returns: A publisher that emits elements representing the time interval between
/// the elements it receives.
public func measureInterval<Context: Scheduler>(
using scheduler: Context,
options: Context.SchedulerOptions? = nil
) -> Publishers.MeasureInterval<Self, Context> {
return .init(upstream: self, scheduler: scheduler)
}
}
extension Publishers {
/// A publisher that measures and emits the time interval between events received from
/// an upstream publisher.
public struct MeasureInterval<Upstream: Publisher, Context: Scheduler>: Publisher {
public typealias Output = Context.SchedulerTimeType.Stride
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// The scheduler on which to deliver elements.
public let scheduler: Context
public init(upstream: Upstream, scheduler: Context) {
self.upstream = upstream
self.scheduler = scheduler
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Upstream.Failure == Downstream.Failure,
Downstream.Input == Context.SchedulerTimeType.Stride
{
upstream.subscribe(Inner(self, downstream: subscriber))
}
}
}
extension Publishers.MeasureInterval {
private final class Inner<Downstream: Subscriber>
: Subscriber,
Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Context.SchedulerTimeType.Stride,
Downstream.Failure == Upstream.Failure
{
// NOTE: This class has been audited for thread safety
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
typealias MeasureInterval = Publishers.MeasureInterval<Upstream, Context>
private enum State {
case ready(MeasureInterval, Downstream)
case subscribed(MeasureInterval, Downstream, Subscription)
case terminal
}
private let lock = UnfairLock.allocate()
private var state: State
private var last: Context.SchedulerTimeType?
init(_ measureInterval: MeasureInterval, downstream: Downstream) {
state = .ready(measureInterval, downstream)
}
deinit {
lock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard case let .ready(measureInterval, downstream) = state else {
lock.unlock()
subscription.cancel()
return
}
state = .subscribed(measureInterval, downstream, subscription)
last = measureInterval.scheduler.now
lock.unlock()
downstream.receive(subscription: self)
}
func receive(_: Input) -> Subscribers.Demand {
lock.lock()
guard case let .subscribed(measureInterval, downstream, subscription) = state,
let previousTime = last else {
lock.unlock()
return .none
}
let now = measureInterval.scheduler.now
last = now
lock.unlock()
let newDemand = downstream.receive(previousTime.distance(to: now))
if newDemand > 0 {
subscription.request(newDemand)
}
return .none
}
func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
guard case let .subscribed(_, downstream, _) = state else {
lock.unlock()
return
}
state = .terminal
last = nil
lock.unlock()
downstream.receive(completion: completion)
}
func request(_ demand: Subscribers.Demand) {
lock.lock()
guard case let .subscribed(_, _, subscription) = state else {
lock.unlock()
return
}
lock.unlock()
subscription.request(demand)
}
func cancel() {
lock.lock()
guard case let .subscribed(_, _, subscription) = state else {
lock.unlock()
return
}
state = .terminal
last = nil
lock.unlock()
subscription.cancel()
}
var description: String { return "MeasureInterval" }
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
var playgroundDescription: Any { return description }
}
}
@@ -5,8 +5,6 @@
// Created by Sergej Jaskiewicz on 14.06.2019.
//
import COpenCombineHelpers
extension Publisher {
/// Applies a closure to create a subject that delivers elements to subscribers.
@@ -159,6 +157,7 @@ extension Publishers.Multicast {
lock.lock()
guard case let .ready(upstream, downstream) = state else {
lock.unlock()
subscription.cancel()
return
}
state = .subscribed(upstream: upstream,
@@ -0,0 +1,205 @@
//
// Publishers.Output.swift
//
//
// Created by Sergej Jaskiewicz on 24.10.2019.
//
extension Publisher {
/// Republishes elements up to the specified maximum count.
///
/// - Parameter maxLength: The maximum number of elements to republish.
/// - Returns: A publisher that publishes up to the specified number of elements
/// before completing.
public func prefix(_ maxLength: Int) -> Publishers.Output<Self> {
return output(in: ..<maxLength)
}
}
extension Publisher {
/// Publishes a specific element, indicated by its index in the sequence of published
/// elements.
///
/// If the publisher completes normally or with an error before publishing
/// the specified element, then the publisher 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,7 +5,18 @@
// Created by Sergej Jaskiewicz on 16.06.2019.
//
import COpenCombineHelpers
extension Publisher {
/// Prints log messages for all publishing events.
///
/// - Parameter prefix: A string with which to prefix all log messages. Defaults to
/// an empty string.
/// - Returns: A publisher that prints log messages for all publishing events.
public func print(_ prefix: String = "",
to stream: TextOutputStream? = nil) -> Publishers.Print<Self> {
return .init(upstream: self, prefix: prefix, to: stream)
}
}
extension Publishers {
@@ -54,24 +65,12 @@ extension Publishers {
}
}
extension Publisher {
/// Prints log messages for all publishing events.
///
/// - Parameter prefix: A string with which to prefix all log messages. Defaults to
/// an empty string.
/// - Returns: A publisher that prints log messages for all publishing events.
public func print(_ prefix: String = "",
to stream: TextOutputStream? = nil) -> Publishers.Print<Self> {
return Publishers.Print(upstream: self, prefix: prefix, to: stream)
}
}
extension Publishers.Print {
private final class Inner<Downstream: Subscriber>: Subscriber,
Subscription,
CustomStringConvertible,
CustomReflectable
CustomReflectable,
CustomPlaygroundDisplayConvertible
{
typealias Input = Downstream.Input
typealias Failure = Downstream.Failure
@@ -89,7 +88,7 @@ extension Publishers.Print {
private var downstream: Downstream
private let prefix: String
private var stream: PrintTarget?
private var subscription: Subscription?
private var status = SubscriptionStatus.awaitingSubscription
private let lock = UnfairLock.allocate()
init(downstream: Downstream, prefix: String, stream: TextOutputStream?) {
@@ -104,9 +103,14 @@ extension Publishers.Print {
func receive(subscription: Subscription) {
log("\(prefix)receive subscription: (\(subscription))")
lock.do {
self.subscription = subscription
lock.lock()
guard case .awaitingSubscription = status else {
lock.unlock()
subscription.cancel()
return
}
status = .subscribed(subscription)
lock.unlock()
downstream.receive(subscription: self)
}
@@ -130,6 +134,9 @@ extension Publishers.Print {
case .failure(let error):
log("\(prefix)receive error: (\(error))")
}
lock.lock()
status = .terminal
lock.unlock()
downstream.receive(completion: completion)
}
@@ -139,19 +146,35 @@ extension Publishers.Print {
} else {
log("\(prefix)request unlimited")
}
subscription?.request(demand)
lock.lock()
guard case let .subscribed(subscription) = status else {
lock.unlock()
return
}
lock.unlock()
subscription.request(demand)
}
func cancel() {
log("\(prefix)receive cancel")
subscription?.cancel()
subscription = nil
lock.lock()
guard case let .subscribed(subscription) = status else {
lock.unlock()
return
}
status = .terminal
lock.unlock()
subscription.cancel()
}
var description: String { return "Print" }
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
var playgroundDescription: Any { return description }
// MARK: - Private
private func log(_ text: String) {
if var stream = stream {
Swift.print(text, to: &stream)
@@ -0,0 +1,201 @@
//
// Publishers.ReceiveOn.swift
//
//
// Created by Sergej Jaskiewicz on 02.12.2019.
//
extension Publisher {
/// Specifies the scheduler on which to receive elements from the publisher.
///
/// You use the `receive(on:options:)` operator to receive results on a specific
/// scheduler, such as performing UI work on the main run loop.
/// In contrast with `subscribe(on:options:)`, which affects upstream messages,
/// `receive(on:options:)` changes the execution context of downstream messages.
/// In the following example, requests to `jsonPublisher` are performed on
/// `backgroundQueue`, but elements received from it are performed on `RunLoop.main`.
///
/// // Some publisher.
/// let jsonPublisher = MyJSONLoaderPublisher()
///
/// // Some subscriber that updates the UI.
/// let labelUpdater = MyLabelUpdateSubscriber()
///
/// jsonPublisher
/// .subscribe(on: backgroundQueue)
/// .receiveOn(on: RunLoop.main)
/// .subscribe(labelUpdater)
///
/// - Parameters:
/// - scheduler: The scheduler the publisher is to use for element delivery.
/// - options: Scheduler options that customize the element delivery.
/// - Returns: A publisher that delivers elements using the specified scheduler.
public func receive<Context: Scheduler>(
on scheduler: Context,
options: Context.SchedulerOptions? = nil
) -> Publishers.ReceiveOn<Self, Context> {
return .init(upstream: self, scheduler: scheduler, options: options)
}
}
extension Publishers {
/// A publisher that delivers elements to its downstream subscriber on a specific
/// scheduler.
public struct ReceiveOn<Upstream: Publisher, Context: Scheduler>: Publisher {
public typealias Output = Upstream.Output
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// The scheduler the publisher is to use for element delivery.
public let scheduler: Context
/// Scheduler options that customize the delivery of elements.
public let options: Context.SchedulerOptions?
public init(upstream: Upstream,
scheduler: Context,
options: Context.SchedulerOptions?) {
self.upstream = upstream
self.scheduler = scheduler
self.options = options
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Upstream.Failure == Downstream.Failure,
Upstream.Output == Downstream.Input
{
upstream.subscribe(Inner(self, downstream: subscriber))
}
}
}
extension Publishers.ReceiveOn {
private final class Inner<Downstream: Subscriber>
: Subscriber,
Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
typealias ReceiveOn = Publishers.ReceiveOn<Upstream, Context>
private enum State {
case ready(ReceiveOn, Downstream)
case subscribed(ReceiveOn, Downstream, Subscription)
case terminal
}
private let lock = UnfairLock.allocate()
private var state: State
private let downstreamLock = UnfairRecursiveLock.allocate()
init(_ receiveOn: ReceiveOn, downstream: Downstream) {
state = .ready(receiveOn, downstream)
}
deinit {
lock.deallocate()
downstreamLock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard case let .ready(receiveOn, downstream) = state else {
lock.unlock()
subscription.cancel()
return
}
state = .subscribed(receiveOn, downstream, subscription)
lock.unlock()
downstreamLock.lock()
downstream.receive(subscription: self)
downstreamLock.unlock()
}
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
lock.lock()
guard case let .subscribed(receiveOn, downstream, _) = state else {
lock.unlock()
return .none
}
lock.unlock()
receiveOn.scheduler.schedule(options: receiveOn.options) {
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) {
self.scheduledReceive(completion: completion, downstream: downstream)
}
}
private func scheduledReceive(completion: Subscribers.Completion<Failure>,
downstream: Downstream) {
downstreamLock.lock()
downstream.receive(completion: completion)
downstreamLock.unlock()
}
func request(_ demand: Subscribers.Demand) {
lock.lock()
guard case let .subscribed(_, _, subscription) = state else {
lock.unlock()
return
}
lock.unlock()
subscription.request(demand)
}
func cancel() {
lock.lock()
guard case let .subscribed(_, _, subscription) = state else {
lock.unlock()
return
}
state = .terminal
lock.unlock()
subscription.cancel()
}
var description: String { return "ReceiveOn" }
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
var playgroundDescription: Any { return description }
}
}
@@ -0,0 +1,196 @@
//
// Publishers.RemoveDuplicates.swift
//
//
// Created by Sergej Jaskiewicz on 24.10.2019.
//
extension Publisher where Output: Equatable {
/// Publishes only elements that 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 previous
/// 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)
}
}
}
@@ -0,0 +1,180 @@
//
// Publishers.ReplaceEmpty.swift
// OpenCombine
//
// Created by Joe Spadafora on 12/10/19.
//
extension Publisher {
/// Replaces an empty stream with the provided element.
///
/// If the upstream publisher finishes without producing any elements,
/// this publisher emits the provided element, then finishes normally.
/// - Parameter output: An element to emit when the upstream publisher
/// finishes without emitting any elements.
/// - Returns: A publisher that replaces an empty stream with
/// the provided output element.
public func replaceEmpty(with output: Output) -> Publishers.ReplaceEmpty<Self> {
return .init(upstream: self, output: output)
}
}
extension Publishers {
/// A publisher that replaces an empty stream with a provided element.
public struct ReplaceEmpty<Upstream: Publisher>: Publisher {
public typealias Output = Upstream.Output
public typealias Failure = Upstream.Failure
/// The element to deliver when the upstream publisher finishes
/// without delivering any elements.
public let output: Upstream.Output
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
public init(upstream: Upstream, output: Output) {
self.upstream = upstream
self.output = output
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Upstream.Failure == Downstream.Failure,
Upstream.Output == Downstream.Input
{
let inner = Inner(downstream: subscriber, output: output)
upstream.subscribe(inner)
}
}
}
extension Publishers.ReplaceEmpty: Equatable
where Upstream: Equatable, Upstream.Output: Equatable {}
extension Publishers.ReplaceEmpty {
private final class Inner<Downstream: Subscriber>
: Subscriber,
Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Upstream.Failure == Downstream.Failure,
Upstream.Output == Downstream.Input
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private let output: Output
private let downstream: Downstream
private var receivedUpstream = false
private var lock = UnfairLock.allocate()
private var downstreamRequested = false
private var finishedWithoutUpstream = false
private var status = SubscriptionStatus.awaitingSubscription
fileprivate init(downstream: Downstream, output: Output) {
self.downstream = downstream
self.output = output
}
deinit {
lock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard case .awaitingSubscription = status else {
lock.unlock()
subscription.cancel()
return
}
status = .subscribed(subscription)
lock.unlock()
downstream.receive(subscription: self)
subscription.request(.unlimited)
}
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
lock.lock()
guard case .subscribed = status else {
lock.unlock()
return .none
}
receivedUpstream = true
lock.unlock()
return downstream.receive(input)
}
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
lock.lock()
guard case .subscribed = status else {
lock.unlock()
return
}
status = .terminal
if receivedUpstream {
lock.unlock()
downstream.receive(completion: completion)
return
}
switch completion {
case .finished:
if downstreamRequested {
lock.unlock()
_ = downstream.receive(output)
downstream.receive(completion: completion)
return
}
finishedWithoutUpstream = true
lock.unlock()
case .failure:
lock.unlock()
downstream.receive(completion: completion)
}
}
func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
lock.lock()
downstreamRequested = true
if finishedWithoutUpstream {
lock.unlock()
_ = downstream.receive(output)
downstream.receive(completion: .finished)
return
}
guard case let .subscribed(subscription) = status else {
lock.unlock()
return
}
lock.unlock()
subscription.request(demand)
}
func cancel() {
lock.lock()
guard case let .subscribed(subscription) = status else {
lock.unlock()
return
}
status = .terminal
lock.unlock()
subscription.cancel()
}
var description: String { return "ReplaceEmpty" }
var customMirror: Mirror {
return Mirror(self, children: EmptyCollection())
}
var playgroundDescription: Any { return description }
}
}
@@ -5,8 +5,6 @@
// Created by Bogdan Vlad on 8/29/19.
//
import COpenCombineHelpers
extension Publisher {
/// Replaces any errors in the stream with the provided element.
///
@@ -54,7 +52,9 @@ extension Publishers {
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Upstream.Output == Downstream.Input, Downstream.Failure == Failure
{
upstream.subscribe(Inner(downstream: subscriber, output: output))
let inner = Inner(downstream: subscriber, output: output)
upstream.subscribe(inner)
subscriber.receive(subscription: inner)
}
}
}
@@ -102,8 +102,11 @@ extension Publishers.ReplaceError {
return
}
status = .subscribed(subscription)
let pendingDemand = self.pendingDemand
lock.unlock()
downstream.receive(subscription: self)
if pendingDemand > 0 {
subscription.request(pendingDemand)
}
}
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
@@ -7,7 +7,7 @@
extension Publisher {
/// Replaces nil elements in the stream with the proviced element.
/// Replaces nil elements in the stream with the provided element.
///
/// - Parameter output: The element to use when replacing `nil`.
/// - Returns: A publisher that replaces `nil` elements from
@@ -4,8 +4,6 @@
// Created by Eric Patey on 26.08.2019.
//
import COpenCombineHelpers
extension Publisher {
/// Transforms elements from the upstream publisher by providing the current element
@@ -5,8 +5,6 @@
// Created by Sergej Jaskiewicz on 19.06.2019.
//
import COpenCombineHelpers
extension Publishers {
/// A publisher that publishes a given sequence of elements.
@@ -107,7 +105,7 @@ extension Publishers.Sequence {
// Combine calls next() while the lock is held.
// It is possible to engineer a custom Sequence that would cause
// a dedlock here, but it would be something insane.
// a deadlock here, but it would be something insane.
let next = iterator.next()
recursion = true
lock.unlock()
@@ -9,7 +9,7 @@ extension Publisher {
/// Returns a publisher as a class instance.
///
/// The downstream subscriber receieves elements and completion states unchanged from
/// The downstream subscriber receives elements and completion states unchanged from
/// the upstream publisher. Use this operator when you want to use
/// reference semantics, such as storing a publisher instance in a property.
///
@@ -0,0 +1,188 @@
//
// Publishers.SubscribeOn.swift
//
//
// Created by Sergej Jaskiewicz on 02.12.2019.
//
extension Publisher {
/// Specifies the scheduler on which to perform subscribe, cancel, and request
/// operations.
///
/// In contrast with `receive(on:options:)`, which affects downstream messages,
/// `subscribe(on:)` changes the execution context of upstream messages.
/// In the following example, requests to `jsonPublisher` are performed on
/// `backgroundQueue`, but elements received from it are performed on `RunLoop.main`.
///
/// let ioPerformingPublisher == // Some publisher.
/// let uiUpdatingSubscriber == // Some subscriber that updates the UI.
///
/// ioPerformingPublisher
/// .subscribe(on: backgroundQueue)
/// .receiveOn(on: RunLoop.main)
/// .subscribe(uiUpdatingSubscriber)
///
/// - Parameters:
/// - scheduler: The scheduler on which to receive upstream messages.
/// - options: Options that customize the delivery of elements.
/// - Returns: A publisher which performs upstream operations on the specified
/// scheduler.
public func subscribe<Context: Scheduler>(
on scheduler: Context,
options: Context.SchedulerOptions? = nil
) -> Publishers.SubscribeOn<Self, Context> {
return .init(upstream: self, scheduler: scheduler, options: options)
}
}
extension Publishers {
/// A publisher that receives elements from an upstream publisher on a specific
/// scheduler.
public struct SubscribeOn<Upstream: Publisher, Context: Scheduler>: Publisher {
public typealias Output = Upstream.Output
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// The scheduler the publisher should use to receive elements.
public let scheduler: Context
/// Scheduler options that customize the delivery of elements.
public let options: Context.SchedulerOptions?
public init(upstream: Upstream,
scheduler: Context,
options: Context.SchedulerOptions?) {
self.upstream = upstream
self.scheduler = scheduler
self.options = options
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Upstream.Failure == Downstream.Failure,
Upstream.Output == Downstream.Input
{
scheduler.schedule(options: options) {
self.upstream.subscribe(Inner(self, downstream: subscriber))
}
}
}
}
extension Publishers.SubscribeOn {
private final class Inner<Downstream: Subscriber>
: Subscriber,
Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
typealias SubscribeOn = Publishers.SubscribeOn<Upstream, Context>
private enum State {
case ready(SubscribeOn, Downstream)
case subscribed(SubscribeOn, Downstream, Subscription)
case terminal
}
private let lock = UnfairLock.allocate()
private var state: State
private let upstreamLock = UnfairLock.allocate()
init(_ subscribeOn: SubscribeOn, downstream: Downstream) {
state = .ready(subscribeOn, downstream)
}
deinit {
lock.deallocate()
upstreamLock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard case let .ready(subscribeOn, downstream) = state else {
lock.unlock()
subscription.cancel()
return
}
state = .subscribed(subscribeOn, downstream, subscription)
lock.unlock()
downstream.receive(subscription: self)
}
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
lock.lock()
guard case let .subscribed(_, downstream, _) = state else {
lock.unlock()
return .none
}
lock.unlock()
return downstream.receive(input)
}
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
lock.lock()
guard case let .subscribed(_, downstream, _) = state else {
lock.unlock()
return
}
state = .terminal
lock.unlock()
downstream.receive(completion: completion)
}
func request(_ demand: Subscribers.Demand) {
lock.lock()
guard case let .subscribed(subscribeOn, _, subscription) = state else {
lock.unlock()
return
}
lock.unlock()
subscribeOn.scheduler.schedule(options: subscribeOn.options) { [weak self] in
self?.scheduledRequest(demand, subscription: subscription)
}
}
private func scheduledRequest(_ demand: Subscribers.Demand,
subscription: Subscription) {
upstreamLock.lock()
subscription.request(demand)
upstreamLock.unlock()
}
func cancel() {
lock.lock()
guard case let .subscribed(subscribeOn, _, subscription) = state else {
lock.unlock()
return
}
state = .terminal
lock.unlock()
subscribeOn.scheduler.schedule(options: subscribeOn.options) { [weak self] in
self?.scheduledCancel(subscription)
}
}
private func scheduledCancel(_ subscription: Subscription) {
upstreamLock.lock()
subscription.cancel()
upstreamLock.unlock()
}
var description: String { return "SubscribeOn" }
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
var playgroundDescription: Any { return description }
}
}
@@ -0,0 +1,336 @@
//
// Publishers.SwitchToLatest.swift
//
//
// Created by Sergej Jaskiewicz on 07.01.2020.
//
extension Publisher where Output: Publisher, Output.Failure == Failure {
/// Flattens the stream of events from multiple upstream publishers to appear as if
/// they were coming from a single stream of events.
///
/// This operator switches the inner publisher as new ones arrive but keeps the outer
/// one constant for downstream subscribers.
/// For example, given the type `Publisher<Publisher<Data, NSError>, Never>`,
/// calling `switchToLatest()` will result in the type `Publisher<Data, NSError>`.
/// The downstream subscriber sees a continuous stream of values even though they may
/// be coming from different upstream publishers.
public func switchToLatest() -> Publishers.SwitchToLatest<Output, Self> {
return .init(upstream: self)
}
}
extension Publishers {
/// A publisher that flattens nested publishers.
///
/// Given a publisher that publishes Publishers, the `SwitchToLatest` publisher
/// produces a sequence of events from only the most recent one.
///
/// For example, given the type `Publisher<Publisher<Data, NSError>, Never>`,
/// calling `switchToLatest()` will result in the type `Publisher<Data, NSError>`.
/// The downstream subscriber sees a continuous stream of values even though they may
/// be coming from different upstream publishers.
public struct SwitchToLatest<NestedPublisher: Publisher, Upstream: Publisher>
: Publisher
where Upstream.Output == NestedPublisher,
Upstream.Failure == NestedPublisher.Failure
{
public typealias Output = NestedPublisher.Output
public typealias Failure = NestedPublisher.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// Creates a publisher that flattens nested publishers.
///
/// - Parameter upstream: The publisher from which this publisher receives
/// elements.
public init(upstream: Upstream) {
self.upstream = upstream
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == Output, Downstream.Failure == Failure
{
let outer = Outer(downstream: subscriber)
subscriber.receive(subscription: outer)
upstream.subscribe(outer)
}
}
}
extension Publishers.SwitchToLatest {
fileprivate final class Outer<Downstream: Subscriber>
: Subscriber,
Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == NestedPublisher.Output,
Downstream.Failure == Upstream.Failure
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private let downstream: Downstream
private var outerSubscription: Subscription?
private var currentInnerSubscription: Subscription?
private var currentInnerIndex: UInt64 = 0
private var nextInnerIndex: UInt64 = 1
private let lock = UnfairLock.allocate()
private let downstreamLock = UnfairRecursiveLock.allocate()
private var cancelled = false
private var finished = false
private var sentCompletion = false
private var awaitingInnerSubscription = false
private var downstreamDemand = Subscribers.Demand.none
init(downstream: Downstream) {
self.downstream = downstream
}
deinit {
lock.deallocate()
downstreamLock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard outerSubscription == nil && !cancelled else {
lock.unlock()
subscription.cancel()
return
}
outerSubscription = subscription
lock.unlock()
subscription.request(.unlimited)
}
func receive(_ input: Input) -> Subscribers.Demand {
lock.lock()
if cancelled || finished {
lock.unlock()
return .none
}
if let currentInnerSubscription = self.currentInnerSubscription {
self.currentInnerSubscription = nil
lock.unlock()
currentInnerSubscription.cancel()
lock.lock()
}
let index = nextInnerIndex
currentInnerIndex = index
nextInnerIndex += 1
awaitingInnerSubscription = true
lock.unlock()
input.subscribe(Side(inner: self, index: index))
return .none
}
func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
outerSubscription = nil
finished = true
if cancelled {
lock.unlock()
return
}
switch completion {
case .finished:
if awaitingInnerSubscription {
lock.unlock()
return
}
if currentInnerSubscription == nil {
sentCompletion = true
lock.unlock()
downstreamLock.lock()
downstream.receive(completion: completion)
downstreamLock.unlock()
} else {
lock.unlock()
}
case .failure:
let currentInnerSubscription = self.currentInnerSubscription
self.currentInnerSubscription = nil
sentCompletion = true
lock.unlock()
currentInnerSubscription?.cancel()
downstreamLock.lock()
downstream.receive(completion: completion)
downstreamLock.unlock()
}
}
func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
lock.lock()
downstreamDemand += demand
if let currentInnerSubscription = self.currentInnerSubscription {
lock.unlock()
currentInnerSubscription.request(demand)
} else {
lock.unlock()
}
}
func cancel() {
lock.lock()
cancelled = true
let currentInnerSubscription = self.currentInnerSubscription
self.currentInnerSubscription = nil
let outerSubscription = self.outerSubscription
self.outerSubscription = nil
lock.unlock()
currentInnerSubscription?.cancel()
outerSubscription?.cancel()
}
var description: String { return "SwitchToLatest" }
var customMirror: Mirror {
return Mirror(self, children: EmptyCollection())
}
var playgroundDescription: Any { return description }
private func receiveInner(subscription: Subscription, _ index: UInt64) {
lock.lock()
guard currentInnerIndex == index &&
!cancelled &&
currentInnerSubscription == nil else {
lock.unlock()
subscription.cancel()
return
}
currentInnerSubscription = subscription
awaitingInnerSubscription = false
let downstreamDemand = self.downstreamDemand
lock.unlock()
if downstreamDemand > 0 {
subscription.request(downstreamDemand)
}
}
private func receiveInner(_ input: NestedPublisher.Output,
_ index: UInt64) -> Subscribers.Demand {
lock.lock()
guard currentInnerIndex == index && !cancelled else {
lock.unlock()
return .none
}
// This will crash if we don't have any demand yet.
// Combine crashes here too.
downstreamDemand -= 1
lock.unlock()
downstreamLock.lock()
let newDemand = downstream.receive(input)
downstreamLock.unlock()
if newDemand > 0 {
lock.lock()
downstreamDemand += newDemand
lock.unlock()
}
return newDemand
}
private func receiveInner(completion: Subscribers.Completion<Failure>,
_ index: UInt64) {
lock.lock()
guard currentInnerIndex == index && !cancelled else {
lock.unlock()
return
}
precondition(!awaitingInnerSubscription, "Unexpected completion")
currentInnerSubscription = nil
switch completion {
case .finished:
if sentCompletion || !finished {
lock.unlock()
return
}
sentCompletion = true
lock.unlock()
downstreamLock.lock()
downstream.receive(completion: completion)
downstreamLock.unlock()
case .failure:
if sentCompletion {
lock.unlock()
return
}
cancelled = true
let outerSubscription = self.outerSubscription
self.outerSubscription = nil
sentCompletion = true
lock.unlock()
outerSubscription?.cancel()
downstreamLock.lock()
downstream.receive(completion: completion)
downstreamLock.unlock()
}
}
}
}
extension Publishers.SwitchToLatest.Outer {
private struct Side
: Subscriber,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
{
typealias Input = NestedPublisher.Output
typealias Failure = NestedPublisher.Failure
typealias Outer =
Publishers.SwitchToLatest<NestedPublisher, Upstream>.Outer<Downstream>
private let index: UInt64
private let outer: Outer
let combineIdentifier = CombineIdentifier()
init(inner: Outer, index: UInt64) {
self.index = index
self.outer = inner
}
func receive(subscription: Subscription) {
outer.receiveInner(subscription: subscription, index)
}
func receive(_ input: Input) -> Subscribers.Demand {
return outer.receiveInner(input, index)
}
func receive(completion: Subscribers.Completion<Failure>) {
outer.receiveInner(completion: completion, index)
}
var description: String { return "SwitchToLatest" }
var customMirror: Mirror {
let children = CollectionOfOne<Mirror.Child>(
("parentSubscription", outer.combineIdentifier)
)
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
}
}
@@ -0,0 +1,229 @@
//
// Publishers.Timeout.swift
//
//
// Created by Sergej Jaskiewicz on 14.06.2020.
//
extension Publisher {
/// Terminates publishing if the upstream publisher exceeds the specified time
/// interval without producing an element.
///
/// - Parameters:
/// - interval: The maximum time interval the publisher can go without emitting
/// an element, expressed in the time system of the scheduler.
/// - scheduler: The scheduler to deliver events on.
/// - options: Scheduler options that customize the delivery of elements.
/// - customError: A closure that executes if the publisher times out.
/// The publisher sends the failure returned by this closure to the subscriber as
/// the reason for termination.
/// - Returns: A publisher that terminates if the specified interval elapses with no
/// events received from the upstream publisher.
public func timeout<Context: Scheduler>(
_ interval: Context.SchedulerTimeType.Stride,
scheduler: Context,
options: Context.SchedulerOptions? = nil,
customError: (() -> Self.Failure)? = nil
) -> Publishers.Timeout<Self, Context> {
return .init(upstream: self,
interval: interval,
scheduler: scheduler,
options: options,
customError: customError)
}
}
extension Publishers {
public struct Timeout<Upstream: Publisher, Context: Scheduler>: Publisher {
public typealias Output = Upstream.Output
public typealias Failure = Upstream.Failure
public let upstream: Upstream
public let interval: Context.SchedulerTimeType.Stride
public let scheduler: Context
public let options: Context.SchedulerOptions?
public let customError: (() -> Upstream.Failure)?
public init(upstream: Upstream,
interval: Context.SchedulerTimeType.Stride,
scheduler: Context,
options: Context.SchedulerOptions?,
customError: (() -> Publishers.Timeout<Upstream, Context>.Failure)?) {
self.upstream = upstream
self.interval = interval
self.scheduler = scheduler
self.options = options
self.customError = customError
}
public func receive<Downsteam: Subscriber>(subscriber: Downsteam)
where Downsteam.Failure == Failure, Downsteam.Input == Output
{
let inner = Inner(downstream: subscriber,
interval: interval,
scheduler: scheduler,
options: options,
customError: customError)
upstream.subscribe(inner)
}
}
}
extension Publishers.Timeout {
private final class Inner<Downstream: Subscriber>
: Subscriber,
Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private let lock = UnfairLock.allocate()
private let downstreamLock = UnfairRecursiveLock.allocate()
private let downstream: Downstream
private let interval: Context.SchedulerTimeType.Stride
private let scheduler: Context
private let options: Context.SchedulerOptions?
private let customError: (() -> Upstream.Failure)?
private var state = SubscriptionStatus.awaitingSubscription
private var didTimeout = false
private var timer: AnyCancellable?
init(downstream: Downstream,
interval: Context.SchedulerTimeType.Stride,
scheduler: Context,
options: Context.SchedulerOptions?,
customError: (() -> Upstream.Failure)?) {
self.downstream = downstream
self.interval = interval
self.scheduler = scheduler
self.options = options
self.customError = customError
}
deinit {
lock.deallocate()
downstreamLock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard case .awaitingSubscription = state else {
lock.unlock()
subscription.cancel()
return
}
state = .subscribed(subscription)
timer = timeoutClock()
lock.unlock()
downstreamLock.lock()
downstream.receive(subscription: self)
downstreamLock.unlock()
subscription.request(.unlimited)
}
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
lock.lock()
guard !didTimeout, case .subscribed = state else {
lock.unlock()
return .none
}
timer?.cancel()
didTimeout = false
timer = timeoutClock()
lock.unlock()
scheduler.schedule(options: options) {
self.downstreamLock.lock()
_ = self.downstream.receive(input)
self.downstreamLock.unlock()
}
return .unlimited
}
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
lock.lock()
timer?.cancel()
state = .terminal
lock.unlock()
scheduler.schedule(options: options) {
self.downstreamLock.lock()
self.downstream.receive(completion: completion)
self.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 "Timeout" }
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
var playgroundDescription: Any { return description }
private func timedOut() {
lock.lock()
guard !didTimeout, case let .subscribed(subscription) = state else {
lock.unlock()
return
}
didTimeout = true
state = .terminal
lock.unlock()
subscription.cancel()
downstreamLock.lock()
downstream
.receive(completion: customError.map { .failure($0()) } ?? .finished)
downstreamLock.unlock()
}
private func timeoutClock() -> AnyCancellable {
let cancellable = scheduler
.schedule(after: scheduler.now.advanced(by: interval),
interval: interval,
tolerance: scheduler.minimumTolerance,
options: options,
{ [weak self] in self?.timedOut() })
return AnyCancellable { cancellable.cancel() }
}
}
}
+226
View File
@@ -0,0 +1,226 @@
//
// Record.swift
//
//
// Created by Sergej Jaskiewicz on 12.11.2019.
//
/// A publisher that allows for recording a series of inputs and a completion for later
/// playback to each subscriber.
public struct Record<Output, Failure: Error>: Publisher {
/// The recorded output and completion.
public let recording: Recording
/// Interactively record a series of outputs and a completion.
public init(record: (inout Recording) -> Void) {
var recording = Recording()
record(&recording)
self.init(recording: recording)
}
/// Initialize with a recording.
public init(recording: Recording) {
self.recording = recording
}
/// Set up a complete recording with the specified output and completion.
public init(output: [Output], completion: Subscribers.Completion<Failure>) {
self.init(recording: Recording(output: output, completion: completion))
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Output == Downstream.Input, Failure == Downstream.Failure
{
if recording.output.isEmpty {
subscriber.receive(subscription: Subscriptions.empty)
subscriber.receive(completion: recording.completion)
} else {
let inner = Inner(downstream: subscriber,
sequence: recording.output,
completion: recording.completion)
subscriber.receive(subscription: inner)
}
}
/// A recorded set of `Output` and a `Subscribers.Completion`.
public struct Recording {
public typealias Input = Output
private enum State {
case input
case complete
}
private var state: State
/// The output which will be sent to a `Subscriber`.
public private(set) var output: [Output]
/// The completion which will be sent to a `Subscriber`.
public private(set) var completion: Subscribers.Completion<Failure>
/// Set up a recording in a state ready to receive output.
public init() {
state = .input
output = []
completion = .finished
}
/// Set up a complete recording with the specified output and completion.
public init(output: [Output],
completion: Subscribers.Completion<Failure> = .finished) {
self.state = .complete
self.output = output
self.completion = completion
}
/// Add an output to the recording.
///
/// A `fatalError` will be raised if output is added after adding completion.
public mutating func receive(_ input: Input) {
precondition(state == .input,
"Receiving values after completion is not allowed")
output.append(input)
}
/// Add a completion to the recording.
///
/// A `fatalError` will be raised if more than one completion is added.
public mutating func receive(completion: Subscribers.Completion<Failure>) {
precondition(state == .input,
"Receiving completion more than once is not allowed")
self.completion = completion
self.state = .complete
}
}
}
extension Record: Codable where Output: Codable, Failure: Codable {}
extension Record.Recording: Codable where Output: Codable, Failure: Codable {
private enum CodingKeys: String, CodingKey {
case output = "output"
case completion = "completion"
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let output = try container.decode([Output].self, forKey: .output)
let completion = try container.decode(Subscribers.Completion<Failure>.self,
forKey: .completion)
self.init(output: output, completion: completion)
}
public func encode(into encoder: Encoder) throws {
try encode(to: encoder)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(output, forKey: .output)
try container.encode(completion, forKey: .completion)
}
}
extension Record {
// This class is almost the same as Publishers.Sequence.Inner
// despite some small details.
private final class Inner<Downstream: Subscriber>
: Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Output, Downstream.Failure == Failure
{
// NOTE: This class has been audited for thread-safety
private var sequence: [Output]?
private let completion: Subscribers.Completion<Failure>
private var downstream: Downstream?
private var iterator: IndexingIterator<[Output]>
private var next: Output?
private var pendingDemand = Subscribers.Demand.none
private var recursion = false
private var lock = UnfairLock.allocate()
fileprivate init(downstream: Downstream,
sequence: [Output],
completion: Subscribers.Completion<Failure>) {
self.sequence = sequence
self.completion = completion
self.downstream = downstream
self.iterator = sequence.makeIterator()
next = iterator.next()
}
deinit {
lock.deallocate()
}
var description: String {
lock.lock()
defer { lock.unlock() }
return sequence.map { $0.description } ?? "Cancelled Events"
}
var customMirror: Mirror {
lock.lock()
defer { lock.unlock() }
let children: [Mirror.Child] = [
("sequence", sequence ?? [Output]()),
("completion", completion)
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
func request(_ demand: Subscribers.Demand) {
lock.lock()
guard downstream != nil else {
lock.unlock()
return
}
pendingDemand += demand
if recursion {
lock.unlock()
return
}
while let downstream = self.downstream, pendingDemand > 0 {
if let current = self.next {
pendingDemand -= 1
let next = iterator.next()
recursion = true
lock.unlock()
let additionalDemand = downstream.receive(current)
lock.lock()
recursion = false
pendingDemand += additionalDemand
self.next = next
}
if next == nil {
self.downstream = nil
self.sequence = nil
lock.unlock()
downstream.receive(completion: completion)
return
}
}
lock.unlock()
}
func cancel() {
lock.lock()
downstream = nil
sequence = nil
lock.unlock()
}
}
}
-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) } }
}
+1 -1
View File
@@ -23,7 +23,7 @@ public protocol SchedulerTimeIntervalConvertible {
///
/// A scheduler used to execute code as soon as possible, or after a future date.
/// Individual scheduler implementations use whatever time-keeping system makes sense
/// for them. Schdedulers express this as their `SchedulerTimeType`. Since this type
/// for them. Schedulers express this as their `SchedulerTimeType`. Since this type
/// conforms to `SchedulerTimeIntervalConvertible`, you can always express these times
/// with the convenience functions like `.milliseconds(500)`. Schedulers can accept
/// options to control how they execute the actions passed to them. These options may
+1 -16
View File
@@ -27,7 +27,7 @@ public protocol Subscriber: CustomCombineIdentifierConvertible {
/// Tells the subscriber that the publisher has produced an element.
///
/// - Parameter input: The published element.
/// - Returns: A `Demand` instance indicating how many more elements the subcriber
/// - Returns: A `Demand` instance indicating how many more elements the subscriber
/// expects to receive.
func receive(_ input: Input) -> Subscribers.Demand
@@ -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.
///
@@ -61,6 +61,7 @@ extension Subscribers {
}
}
/// Returns the result of adding two demands.
/// When adding any value to `.unlimited`, the result is `.unlimited`.
@inline(__always)
@inlinable
@@ -77,6 +78,7 @@ extension Subscribers {
}
}
/// Adds two demands, and assigns the result to the first demand.
/// When adding any value to `.unlimited`, the result is `.unlimited`.
@inline(__always)
@inlinable
@@ -85,6 +87,7 @@ extension Subscribers {
lhs = lhs + rhs
}
/// Returns the result of adding an integer to a demand.
/// When adding any value to` .unlimited`, the result is `.unlimited`.
@inline(__always)
@inlinable
@@ -96,6 +99,7 @@ extension Subscribers {
return isOverflow ? .unlimited : .max(sum)
}
/// Adds an integer to a demand, and assigns the result to the demand.
/// When adding any value to `.unlimited`, the result is `.unlimited`.
@inline(__always)
@inlinable
@@ -103,6 +107,9 @@ extension Subscribers {
lhs = lhs + rhs
}
/// Returns the result of multiplying a demand by an integer.
/// When multiplying any value by `.unlimited`, the result is `.unlimited`. If
/// the multiplication operation overflows, the result is `.unlimited`.
public static func * (lhs: Demand, rhs: Int) -> Demand {
if lhs == .unlimited {
return .unlimited
@@ -112,12 +119,16 @@ extension Subscribers {
return isOverflow ? .unlimited : .max(product)
}
/// Multiplies a demand by an integer, and assigns the result to the demand.
/// When multiplying any value by `.unlimited`, the result is `.unlimited`. If
/// the multiplication operation overflows, the result is `.unlimited`.
@inline(__always)
@inlinable
public static func *= (lhs: inout Demand, rhs: Int) {
lhs = lhs * rhs
}
/// Returns the result of subtracting one demand from another.
/// When subtracting any value (including `.unlimited`) from `.unlimited`,
/// the result is still `.unlimited`. Subtracting `.unlimited` from any value
/// (except `.unlimited`) results in `.max(0)`. A negative demand is not possible;
@@ -137,6 +148,7 @@ extension Subscribers {
}
}
/// Subtracts one demand from another, and assigns the result to the first demand.
/// When subtracting any value (including `.unlimited`) from `.unlimited`,
/// the result is still `.unlimited`. Subtracting unlimited from any value
/// (except `.unlimited`) results in `.max(0)`. A negative demand is not possible;
@@ -148,6 +160,7 @@ extension Subscribers {
lhs = lhs - rhs
}
/// Returns the result of subtracting an integer from a demand.
/// When subtracting any value from `.unlimited`, the result is still
/// `.unlimited`.
/// A negative demand is not possible; any operation that would result in
@@ -164,6 +177,7 @@ extension Subscribers {
return isOverflow ? .none : .max(difference)
}
/// Subtracts an integer from a demand, and assigns the result to the demand.
/// When subtracting any value from `.unlimited,` the result is still
/// `.unlimited`.
/// A negative demand is not possible; any operation that would result in
@@ -175,6 +189,10 @@ extension Subscribers {
lhs = lhs - rhs
}
/// Returns a Boolean that indicates whether the demand requests more than
/// the given number of elements.
/// If `lhs` is `.unlimited`, then the result is always `true`.
/// Otherwise, the operator compares the demands `max` value to `rhs`.
@inline(__always)
@inlinable
public static func > (lhs: Demand, rhs: Int) -> Bool {
@@ -185,6 +203,10 @@ extension Subscribers {
}
}
/// Returns a Boolean that indicates whether the first demand requests more or
/// the same number of elements as the second.
/// If `lhs` is `.unlimited`, then the result is always `true`.
/// Otherwise, the operator compares the demands `max` value to `rhs`.
@inline(__always)
@inlinable
public static func >= (lhs: Demand, rhs: Int) -> Bool {
@@ -195,6 +217,10 @@ extension Subscribers {
}
}
/// Returns a Boolean that indicates a given number of elements is greater than
/// the maximum specified by the demand.
/// If `rhs` is `.unlimited`, then the result is always `false`.
/// Otherwise, the operator compares the demands `max` value to `lhs`.
@inline(__always)
@inlinable
public static func > (lhs: Int, rhs: Demand) -> Bool {
@@ -205,6 +231,10 @@ extension Subscribers {
}
}
/// Returns a Boolean that indicates a given number of elements is greater than
/// or equal to the maximum specified by the demand.
/// If `rhs` is `.unlimited`, then the result is always `false`.
/// Otherwise, the operator compares the demands `max` value to `lhs`.
@inline(__always)
@inlinable
public static func >= (lhs: Int, rhs: Demand) -> Bool {
@@ -215,6 +245,10 @@ extension Subscribers {
}
}
/// Returns a Boolean that indicates whether the demand requests fewer than
/// the given number of elements.
/// If `lhs` is `.unlimited`, then the result is always `false`.
/// Otherwise, the operator compares the demands `max` value to `rhs`.
@inline(__always)
@inlinable
public static func < (lhs: Demand, rhs: Int) -> Bool {
@@ -225,6 +259,10 @@ extension Subscribers {
}
}
/// Returns a Boolean that indicates a given number of elements is less than
/// the maximum specified by the demand.
/// If `rhs` is `.unlimited`, then the result is always `true`.
/// Otherwise, the operator compares the demands `max` value to `lhs`.
@inline(__always)
@inlinable
public static func < (lhs: Int, rhs: Demand) -> Bool {
@@ -235,6 +273,10 @@ extension Subscribers {
}
}
/// Returns a Boolean that indicates whether the demand requests fewer or
/// the same number of elements as the given integer.
/// If `lhs` is `.unlimited`, then the result is always `false`.
/// Otherwise, the operator compares the demands `max` value to `rhs`.
@inline(__always)
@inlinable
public static func <= (lhs: Demand, rhs: Int) -> Bool {
@@ -245,6 +287,10 @@ extension Subscribers {
}
}
/// Returns a Boolean value that indicates a given number of elements is less
/// than or equal the maximum specified by the demand.
/// If `rhs` is `.unlimited`, then the result is always `true`.
/// Otherwise, the operator compares the demands `max` value to `lhs`.
@inline(__always)
@inlinable
public static func <= (lhs: Int, rhs: Demand) -> Bool {
@@ -255,9 +301,12 @@ extension Subscribers {
}
}
/// Returns a Boolean value that indicates whether the first demand requests fewer
/// elements than the second.
/// If both sides are `.unlimited`, the result is always `false`.
/// If `lhs` is `.unlimited`, then the result is always `false`.
/// If `rhs` is `.unlimited` then the result is `false` iff `lhs` is `.unlimited`
/// Otherwise, the two `.max` values are compared.
/// If `rhs` is `.unlimited`, then the result is always `true`.
/// Otherwise, this operator compares the demands `max` values.
@inline(__always)
@inlinable
public static func < (lhs: Demand, rhs: Demand) -> Bool {
@@ -271,6 +320,12 @@ extension Subscribers {
}
}
/// Returns a Boolean value that indicates whether the first demand requests fewer
/// or the same number of elements as the second.
/// If both sides are `.unlimited`, the result is always `true`.
/// If `lhs` is `.unlimited`, then the result is always `false`.
/// If `rhs` is `.unlimited` then the result is always `true`.
/// Otherwise, this operator compares the demands `max` values.
@inline(__always)
@inlinable
public static func <= (lhs: Demand, rhs: Demand) -> Bool {
@@ -286,6 +341,12 @@ extension Subscribers {
}
}
/// Returns a Boolean that indicates whether the first demand requests more or
/// the same number of elements as the second.
/// If both sides are `.unlimited`, the result is always `false`.
/// If `lhs` is `.unlimited`, then the result is always `true`.
/// If rhs is `.unlimited` then the result is always `false`.
/// Otherwise, this operator compares the demands `max` values.
@inline(__always)
@inlinable
public static func >= (lhs: Demand, rhs: Demand) -> Bool {
@@ -301,6 +362,12 @@ extension Subscribers {
}
}
/// Returns a Boolean that indicates whether the first demand requests more
/// elements than the second.
/// If both sides are `.unlimited`, the result is always `false`.
/// If `lhs` is `.unlimited`, then the result is always `true`.
/// If `rhs` is `.unlimited` then the result is always `false`.
/// Otherwise, this operator compares the demands `max` values.
@inline(__always)
@inlinable
public static func > (lhs: Demand, rhs: Demand) -> Bool {
@@ -316,8 +383,9 @@ extension Subscribers {
}
}
/// Returns `true` if `lhs` and `rhs` are equal. `.unlimited` is not equal to any
/// integer.
/// Returns a Boolean value indicating whether a demand requests the given number
/// of elements.
/// An `.unlimited` demand doesnt match any integer.
@inline(__always)
@inlinable
public static func == (lhs: Demand, rhs: Int) -> Bool {
@@ -328,8 +396,10 @@ extension Subscribers {
}
}
/// Returns `true` if `lhs` and `rhs` are not equal. `.unlimited` is not equal to
/// any integer.
/// Returns a Boolean value indicating whether a demand is not equal to
/// an integer.
/// The `.unlimited` value isnt equal to any integer.
@inlinable
public static func != (lhs: Demand, rhs: Int) -> Bool {
if lhs == .unlimited {
return true
@@ -338,8 +408,10 @@ extension Subscribers {
}
}
/// Returns `true` if `lhs` and `rhs` are equal. `.unlimited` is not equal to any
/// integer.
/// Returns a Boolean value indicating whether a given number of elements matches
/// the request of a given demand.
/// An `.unlimited` demand doesnt match any integer.
@inlinable
public static func == (lhs: Int, rhs: Demand) -> Bool {
if rhs == .unlimited {
return false
@@ -348,8 +420,10 @@ extension Subscribers {
}
}
/// Returns `true` if `lhs` and `rhs` are not equal. `.unlimited` is not equal to
/// any integer.
/// Returns a Boolean value indicating whether an integer is not equal to
/// a demand.
/// The `.unlimited` value isnt equal to any integer.
@inlinable
public static func != (lhs: Int, rhs: Demand) -> Bool {
if rhs == .unlimited {
return true
@@ -358,8 +432,13 @@ extension Subscribers {
}
}
/// Returns the number of requested values, or `nil` if `.unlimited`.
public var max: Int? {
@inlinable
public static func == (lhs: Demand, rhs: Demand) -> Bool {
return lhs.rawValue == rhs.rawValue
}
/// The number of requested values, or nil if `.unlimited`.
@inlinable public var max: Int? {
if self == .unlimited {
return nil
} else {
+1 -1
View File
@@ -7,7 +7,7 @@
/// A protocol representing the connection of a subscriber to a publisher.
///
/// Subcriptions are class constrained because a `Subscription` has identity -
/// Subscriptions are class constrained because a `Subscription` has identity -
/// defined by the moment in time a particular subscriber attached to a publisher.
/// Canceling a `Subscription` must be thread-safe.
///
+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 }
}
}
@@ -50,8 +50,12 @@ extension DispatchQueue {
/// - Parameter other: Another dispatch queue time.
/// - Returns: The time interval between this time and the provided time.
public func distance(to other: SchedulerTimeType) -> Stride {
let start = dispatchTime.rawValue
let end = other.dispatchTime.rawValue
return .nanoseconds(
Int(other.dispatchTime.rawValue - dispatchTime.rawValue)
end >= start
? Int(Int64(bitPattern: end) - Int64(bitPattern: start))
: -Int(Int64(bitPattern: start) - Int64(bitPattern: end))
)
}
@@ -62,7 +66,9 @@ extension DispatchQueue {
/// - Returns: A dispatch queue time advanced by the given
/// interval from this instances time.
public func advanced(by stride: Stride) -> SchedulerTimeType {
return .init(dispatchTime + stride.timeInterval)
return stride.magnitude == .max
? .init(.distantFuture)
: .init(dispatchTime + stride.timeInterval)
}
public func hash(into hasher: inout Hasher) {
@@ -125,13 +131,52 @@ extension DispatchQueue {
self = .microseconds(microseconds)
case .nanoseconds(let nanoseconds):
self = .nanoseconds(nanoseconds)
// This dance is to avoid the warning 'default will never be executed'
// on non-Darwin platforms.
// There really shouldn't be a warning.
// See https://forums.swift.org/t/unknown-default-produces-a-warning-on-linux-with-non-frozen-enum/31687
//
// Thanks to Jeremy David Giesbrecht for suggesting this workaround.
#if canImport(Darwin)
case .never:
fallthrough
@unknown default:
self = .nanoseconds(.max)
@unknown default:
self.init(__guessFromUnknown: timeInterval)
#else
default:
if case .never = timeInterval {
self = .nanoseconds(.max)
} else {
self.init(__guessFromUnknown: timeInterval)
}
#endif
}
}
public // testable
init(__guessFromUnknown timeInterval: DispatchTimeInterval) {
// Let's take some reference time,
// add `timeInterval` to it, take the `rawValue` from the result
// and subtract the `rawValue` of the reference time.
//
// We won't be able to provide the exact implementation though,
// because something will definitely overflow.
//
// However, we can try to support as wide a range of values
// as possible.
//
// By trial and error I got that the `rawValue` of `UInt64.max / 13`
// gives us probably the widest range of supported values:
// from `Int.min / 6.5` to `Int.max / 2.889` nanoseconds.
// That's with Int being 64 bits. Since here only UInt64 can overflow,
// when Int is 32 bits, we don't have this issue.
// It should be more than enough.
let referenceTime = DispatchTime(uptimeNanoseconds: .max / 13)
self = SchedulerTimeType(referenceTime)
.distance(to: SchedulerTimeType(referenceTime + timeInterval))
}
/// Creates a dispatch queue time interval from a floating-point
/// seconds value.
///
@@ -163,50 +208,43 @@ extension DispatchQueue {
}
public static func * (lhs: Stride, rhs: Stride) -> Stride {
// A bug in Combine, should be nanoseconds (FB7189676)
return .seconds(lhs.magnitude * rhs.magnitude)
return Stride(magnitude: lhs.magnitude * rhs.magnitude)
}
public static func + (lhs: Stride, rhs: Stride) -> Stride {
// A bug in Combine, should be nanoseconds (FB7189676)
return .seconds(lhs.magnitude + rhs.magnitude)
return Stride(magnitude: lhs.magnitude + rhs.magnitude)
}
public static func - (lhs: Stride, rhs: Stride) -> Stride {
// A bug in Combine, should be nanoseconds (FB7189676)
return .seconds(lhs.magnitude - rhs.magnitude)
return Stride(magnitude: lhs.magnitude - rhs.magnitude)
}
// swiftlint:disable shorthand_operator
public static func -= (lhs: inout Stride, rhs: Stride) {
lhs = lhs - rhs
lhs.magnitude -= rhs.magnitude
}
public static func *= (lhs: inout Stride, rhs: Stride) {
lhs = lhs * rhs
lhs.magnitude *= rhs.magnitude
}
public static func += (lhs: inout Stride, rhs: Stride) {
lhs = lhs + rhs
lhs.magnitude += rhs.magnitude
}
// swiftlint:enable shorthand_operator
public static func seconds(_ value: Double) -> Stride {
return Stride(magnitude: Int(value * 1_000_000_000))
}
public static func seconds(_ value: Int) -> Stride {
return Stride(magnitude: value * 1_000_000_000)
return Stride(magnitude: clampedIntProduct(value, 1_000_000_000))
}
public static func milliseconds(_ value: Int) -> Stride {
return Stride(magnitude: value * 1_000_000)
return Stride(magnitude: clampedIntProduct(value, 1_000_000))
}
public static func microseconds(_ value: Int) -> Stride {
return Stride(magnitude: value * 1_000)
return Stride(magnitude: clampedIntProduct(value, 1_000))
}
public static func nanoseconds(_ value: Int) -> Stride {
@@ -306,8 +344,10 @@ extension DispatchQueue {
#if !canImport(Combine)
extension DispatchQueue: OpenCombine.Scheduler {
/// Options that affect the operation of the dispatch queue scheduler.
public typealias SchedulerOptions = OCombine.SchedulerOptions
/// The scheduler time type used by the dispatch queue.
public typealias SchedulerTimeType = OCombine.SchedulerTimeType
public var minimumTolerance: OCombine.SchedulerTimeType.Stride {
@@ -343,3 +383,18 @@ extension DispatchQueue: OpenCombine.Scheduler {
}
}
#endif
// This function is taken from swift-corelibs-libdispatch:
// https://github.com/apple/swift-corelibs-libdispatch/blob/c992dacf3ca114806e6ac9ffc9113b19255be9fe/src/swift/Time.swift#L134-L144
//
// Returns m1 * m2, clamped to the range [Int.min, Int.max].
// Because of the way this function is used, we can always assume
// that m2 > 0.
private func clampedIntProduct(_ lhs: Int, _ rhs: Int) -> Int {
assert(rhs > 0, "multiplier must be positive")
let (result, overflow) = lhs.multipliedReportingOverflow(by: rhs)
if overflow {
return lhs > 0 ? .max : .min
}
return result
}
@@ -0,0 +1,15 @@
//
// Locking.swift
//
//
// Created by Sergej Jaskiewicz on 10.12.2019.
//
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
import OpenCombine
internal typealias UnfairLock = __UnfairLock
internal typealias UnfairRecursiveLock = __UnfairRecursiveLock
@@ -0,0 +1,17 @@
//
// Violations.swift
//
//
// Created by Sergej Jaskiewicz on 13.12.2019.
//
import OpenCombine
extension Subscribers.Demand {
internal func assertNonZero(file: StaticString = #file,
line: UInt = #line) {
if self == .none {
fatalError("API Violation: demand must not be zero", file: file, line: line)
}
}
}
@@ -0,0 +1,17 @@
//
// JSONEncoder.swift
//
//
// Created by Sergej Jaskiewicz on 10.10.2019.
//
import Foundation
import OpenCombine
extension JSONEncoder: TopLevelEncoder {
public typealias Output = Data
}
extension JSONDecoder: TopLevelDecoder {
public typealias Input = Data
}
@@ -0,0 +1,230 @@
//
// NotificationCenter.swift
//
//
// Created by Sergej Jaskiewicz on 10.10.2019.
//
import Foundation
import OpenCombine
extension NotificationCenter {
/// A namespace for disambiguation when both OpenCombine and Foundation are imported.
///
/// Foundation extends `NotificationCenter` with new methods and nested types.
/// If you import both OpenCombine and Foundation, you will not be able
/// to write `NotificationCenter.Publisher`,
/// because Swift is unable to understand which `Publisher`
/// you're referring to the one declared in Foundation or in OpenCombine.
///
/// So you have to write `NotificationCenter.OCombine.Publisher`.
///
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
///
/// You can omit this whenever Combine is not available (e. g. on Linux).
public struct OCombine {
public let center: NotificationCenter
public init(_ center: NotificationCenter) {
self.center = center
}
/// A publisher that emits elements when broadcasting notifications.
public struct Publisher: OpenCombine.Publisher {
public typealias Output = Notification
public typealias Failure = Never
/// The notification center this publisher uses as a source.
public let center: NotificationCenter
/// The name of notifications published by this publisher.
public let name: Notification.Name
/// The object posting the named notification.
public let object: AnyObject?
/// Creates a publisher that emits events when broadcasting notifications.
///
/// - Parameters:
/// - center: The notification center to publish notifications for.
/// - name: The name of the notification to publish.
/// - object: The object posting the named notification. If `nil`,
/// the publisher emits elements for any object producing a notification
/// with the given name.
public init(center: NotificationCenter,
name: Notification.Name,
object: AnyObject? = nil) {
self.center = center
self.name = name
self.object = object
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Failure == Never, Downstream.Input == Notification
{
let subscription = Notification.Subscription(center: center,
name: name,
object: object,
downstream: subscriber)
subscriber.receive(subscription: subscription)
}
}
/// Returns a publisher that emits events when broadcasting notifications.
///
/// - Parameters:
/// - name: The name of the notification to publish.
/// - object: The object posting the named notification. If `nil`, the publisher
/// emits elements for any object producing a notification with the given
/// name.
/// - Returns: A publisher that emits events when broadcasting notifications.
public func publisher(for name: Notification.Name,
object: AnyObject? = nil) -> Publisher {
return .init(center: center, name: name, object: object)
}
}
#if !canImport(Combine)
/// A publisher that emits elements when broadcasting notifications.
public typealias Publisher = OCombine.Publisher
#endif
}
extension NotificationCenter {
/// A namespace for disambiguation when both OpenCombine and Foundation are imported.
///
/// Foundation extends `NotificationCenter` with new methods and nested types.
/// If you import both OpenCombine and Foundation, you will not be able
/// to write `NotificationCenter.default.publisher(for: name)`,
/// because Swift is unable to understand which `publisher` method
/// you're referring to the one declared in Foundation or in OpenCombine.
///
/// So you have to write `NotificationCenter.default.ocombine.publisher(for: name)`.
///
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
///
/// You can omit this whenever Combine is not available (e. g. on Linux).
public var ocombine: OCombine { return .init(self) }
#if !canImport(Combine)
/// Returns a publisher that emits events when broadcasting notifications.
///
/// - Parameters:
/// - name: The name of the notification to publish.
/// - object: The object posting the named notification. If `nil`, the publisher
/// emits elements for any object producing a notification with the given name.
/// - Returns: A publisher that emits events when broadcasting notifications.
public func publisher(for name: Notification.Name,
object: AnyObject? = nil) -> OCombine.Publisher {
return ocombine.publisher(for: name, object: object)
}
#endif
}
extension NotificationCenter.OCombine.Publisher: Equatable {
public static func == (lhs: NotificationCenter.OCombine.Publisher,
rhs: NotificationCenter.OCombine.Publisher) -> Bool {
return lhs.center == rhs.center &&
lhs.name == rhs.name &&
lhs.object === rhs.object
}
}
extension Notification {
fileprivate final class Subscription<Downstream: Subscriber>
: OpenCombine.Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Notification, Downstream.Failure == Never
{
private let lock = UnfairLock.allocate()
fileprivate let downstreamLock = UnfairRecursiveLock.allocate()
fileprivate var demand = Subscribers.Demand.none
private var center: NotificationCenter?
private let name: Name
private var object: AnyObject?
private var observation: AnyObject?
fileprivate init(center: NotificationCenter,
name: Notification.Name,
object: AnyObject?,
downstream: Downstream) {
self.center = center
self.name = name
self.object = object
self.observation = center
.addObserver(forName: name, object: object, queue: nil) { [weak self] in
self?.didReceiveNotification($0, downstream: downstream)
}
}
deinit {
lock.deallocate()
downstreamLock.deallocate()
}
private func didReceiveNotification(_ notification: Notification,
downstream: Downstream) {
lock.lock()
guard demand > 0 else {
lock.unlock()
return
}
demand -= 1
lock.unlock()
downstreamLock.lock()
let newDemand = downstream.receive(notification)
downstreamLock.unlock()
lock.lock()
demand += newDemand
lock.unlock()
}
func request(_ demand: Subscribers.Demand) {
lock.lock()
self.demand += demand
lock.unlock()
}
func cancel() {
lock.lock()
guard let center = self.center, let observation = self.observation else {
lock.unlock()
return
}
self.center = nil
self.object = nil
self.observation = nil
lock.unlock()
center.removeObserver(observation)
}
fileprivate var description: String { return "NotificationCenter Observer" }
fileprivate var customMirror: Mirror {
lock.lock()
defer { lock.unlock() }
let children: [Mirror.Child] = [
("center", center as Any),
("name", name),
("object", object as Any),
("demand", demand)
]
return Mirror(self, children: children)
}
fileprivate var playgroundDescription: Any { return description }
}
}
@@ -0,0 +1,315 @@
//
// OperationQueue+Scheduler.swift
//
//
// Created by Sergej Jaskiewicz on 14.06.2020.
//
import Foundation
import OpenCombine
extension OperationQueue {
/// A namespace for disambiguation when both OpenCombine and Combine are imported.
///
/// Foundation overlay for Combine extends `OperationQueue` with new methods and
/// nested types.
/// If you import both OpenCombine and Foundation, you will not be able
/// to write `OperationQueue.SchedulerTimeType`,
/// because Swift is unable to understand which `SchedulerTimeType`
/// you're referring to.
///
/// So you have to write `OperationQueue.OCombine.SchedulerTimeType`.
///
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
///
/// You can omit this whenever Combine is not available (e. g. on Linux).
public struct OCombine: Scheduler {
public let queue: OperationQueue
public init(_ queue: OperationQueue) {
self.queue = queue
}
/// The scheduler time type used by the operation queue.
public struct SchedulerTimeType: Strideable, Codable, Hashable {
/// The date represented by this type.
public var date: Date
/// Initializes a operation queue scheduler time with the given date.
///
/// - Parameter date: The date to represent.
public init(_ date: Date) {
self.date = date
}
/// Returns the distance to another operation queue scheduler time.
///
/// - Parameter other: Another operation queue time.
/// - Returns: The time interval between this time and the provided time.
public func distance(to other: SchedulerTimeType) -> Stride {
let absoluteSelf = date.timeIntervalSinceReferenceDate
let absoluteOther = other.date.timeIntervalSinceReferenceDate
return Stride(absoluteSelf.distance(to: absoluteOther))
}
/// Returns an operation queue scheduler time calculated by advancing this
/// instances time by the given interval.
///
/// - Parameter n: A time interval to advance.
/// - Returns: An operation queue time advanced by the given interval from
/// this instances time.
public func advanced(by value: Stride) -> SchedulerTimeType {
return SchedulerTimeType(date + value.magnitude)
}
/// The interval by which operation queue times advance.
public struct Stride: SchedulerTimeIntervalConvertible,
Comparable,
SignedNumeric,
ExpressibleByFloatLiteral,
Codable {
public typealias FloatLiteralType = TimeInterval
public typealias IntegerLiteralType = TimeInterval
public typealias Magnitude = TimeInterval
/// The value of this time interval in seconds.
public var magnitude: TimeInterval
/// The value of this time interval in seconds.
public var timeInterval: TimeInterval {
return magnitude
}
public init(integerLiteral value: TimeInterval) {
magnitude = value
}
public init(floatLiteral value: TimeInterval) {
magnitude = value
}
public init(_ timeInterval: TimeInterval) {
magnitude = timeInterval
}
public init?<Source: BinaryInteger>(exactly source: Source) {
guard let value = TimeInterval(exactly: source) else { return nil }
magnitude = value
}
public static func < (lhs: Stride, rhs: Stride) -> Bool {
return lhs.magnitude < rhs.magnitude
}
public static func * (lhs: Stride, rhs: Stride) -> Stride {
return Stride(lhs.magnitude * rhs.magnitude)
}
public static func + (lhs: Stride, rhs: Stride) -> Stride {
return Stride(lhs.magnitude + rhs.magnitude)
}
public static func - (lhs: Stride, rhs: Stride) -> Stride {
return Stride(lhs.magnitude - rhs.magnitude)
}
public static func *= (lhs: inout Stride, rhs: Stride) {
lhs.magnitude *= rhs.magnitude
}
public static func += (lhs: inout Stride, rhs: Stride) {
lhs.magnitude += rhs.magnitude
}
public static func -= (lhs: inout Stride, rhs: Stride) {
lhs.magnitude -= rhs.magnitude
}
public static func seconds(_ value: Int) -> Stride {
return Stride(TimeInterval(value))
}
public static func seconds(_ value: Double) -> Stride {
return Stride(TimeInterval(value))
}
public static func milliseconds(_ value: Int) -> Stride {
return Stride(TimeInterval(value) / 1_000)
}
public static func microseconds(_ value: Int) -> Stride {
return Stride(TimeInterval(value) / 1_000_000)
}
public static func nanoseconds(_ value: Int) -> Stride {
return Stride(TimeInterval(value) / 1_000_000_000)
}
}
}
/// Options that affect the operation of the operation queue scheduler.
public struct SchedulerOptions {
}
private final class DelayReadyOperation: Operation, Cancellable {
private static let readySchedulingQueue =
DispatchQueue(label: "DelayReadyOperation")
private var action: (() -> Void)?
private var readyFromAfter = false
private let lock = UnfairLock.allocate()
init(_ action: @escaping() -> Void, after: SchedulerTimeType) {
self.action = action
super.init()
let deadline = DispatchTime.now() + after.date.timeIntervalSinceNow
DelayReadyOperation.readySchedulingQueue
.asyncAfter(deadline: deadline) { [weak self] in
self?.becomeReady()
}
}
deinit {
lock.deallocate()
}
override func main() {
action!()
action = nil
}
private func becomeReady() {
// Smart key paths don't work with NSOperation in swift-corelibs-foundation prior to
// Swift 5.1.
#if canImport(Darwin) || swift(<5.1)
// The smart key paths don't work with NSOperation on OS versions prior to
// iOS 11. The string key paths work fine everywhere.
// https://forums.swift.org/t/keypath-translation-for-kvo-notification-seems-to-not-work-properly-on-ios-10/15898
willChangeValue(forKey: "isReady")
#else
willChangeValue(for: \.isReady)
#endif
lock.lock()
readyFromAfter = true
lock.unlock()
// Smart key paths don't work with NSOperation in swift-corelibs-foundation prior to
// Swift 5.1.
#if canImport(Darwin) || swift(<5.1)
// The smart key paths don't work with NSOperation on OS versions prior to
// iOS 11. The string key paths work fine everywhere.
// https://forums.swift.org/t/keypath-translation-for-kvo-notification-seems-to-not-work-properly-on-ios-10/15898
didChangeValue(forKey: "isReady")
#else
didChangeValue(for: \.isReady)
#endif
}
override var isReady: Bool {
guard super.isReady else { return false }
lock.lock()
defer { lock.unlock() }
return readyFromAfter
}
}
public func schedule(options: SchedulerOptions?,
_ action: @escaping () -> Void) {
let op = BlockOperation(block: action)
queue.addOperation(op)
}
public func schedule(after date: SchedulerTimeType,
tolerance: SchedulerTimeType.Stride,
options: SchedulerOptions?,
_ action: @escaping () -> Void) {
let op = DelayReadyOperation(action, after: date)
queue.addOperation(op)
}
public func schedule(after date: SchedulerTimeType,
interval: SchedulerTimeType.Stride,
tolerance: SchedulerTimeType.Stride,
options: SchedulerOptions?,
_ action: @escaping () -> Void) -> Cancellable {
let op = DelayReadyOperation(action, after: date.advanced(by: interval))
queue.addOperation(op)
return AnyCancellable(op)
}
public var now: SchedulerTimeType {
return .init(Date())
}
public var minimumTolerance: SchedulerTimeType.Stride {
return .init(0.0)
}
}
/// A namespace for disambiguation when both OpenCombine and Foundation are imported.
///
/// Foundation overlay for Combine extends `OperationQueue` with new methods and
/// nested types.
/// If you import both OpenCombine and Foundation, you will not be able
/// to write `OperationQueue.main.schedule { doThings() }`,
/// because Swift is unable to understand which `schedule` method
/// you're referring to.
///
/// So you have to write `OperationQueue.main.ocombine.schedule { doThings() }`.
///
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
///
/// You can omit this whenever Combine is not available (e. g. on Linux).
public var ocombine: OCombine {
return OCombine(self)
}
}
#if !canImport(Combine)
extension OperationQueue: OpenCombine.Scheduler {
/// Options that affect the operation of the run loop scheduler.
public typealias SchedulerOptions = OCombine.SchedulerOptions
/// The scheduler time type used by the run loop.
public typealias SchedulerTimeType = OCombine.SchedulerTimeType
public func schedule(options: SchedulerOptions?, _ action: @escaping () -> Void) {
ocombine.schedule(options: options, action)
}
public func schedule(after date: SchedulerTimeType,
tolerance: SchedulerTimeType.Stride,
options: SchedulerOptions?,
_ action: @escaping () -> Void) {
ocombine.schedule(after: date, tolerance: tolerance, options: options, action)
}
public func schedule(after date: SchedulerTimeType,
interval: SchedulerTimeType.Stride,
tolerance: SchedulerTimeType.Stride,
options: SchedulerOptions?,
_ action: @escaping () -> Void) -> Cancellable {
return ocombine.schedule(after: date,
interval: interval,
tolerance: tolerance,
options: options,
action)
}
public var now: SchedulerTimeType {
return ocombine.now
}
public var minimumTolerance: SchedulerTimeType.Stride {
return ocombine.minimumTolerance
}
}
#endif
@@ -0,0 +1,21 @@
//
// PropertyListEncoder.swift
//
//
// Created by Sergej Jaskiewicz on 10.12.2019.
//
import Foundation
import OpenCombine
// PropertyListEncoder and PropertyListDecoder are unavailable in
// swift-corelibs-foundation prior to Swift 5.1.
#if canImport(Darwin) || swift(>=5.1)
extension PropertyListEncoder: TopLevelEncoder {
public typealias Output = Data
}
extension PropertyListDecoder: TopLevelDecoder {
public typealias Input = Data
}
#endif
@@ -0,0 +1,290 @@
//
// RunLoop+Scheduler.swift
//
//
// Created by Sergej Jaskiewicz on 13.12.2019.
//
import CoreFoundation
import Foundation
import OpenCombine
extension RunLoop {
/// A namespace for disambiguation when both OpenCombine and Combine are imported.
///
/// Foundation overlay for Combine extends `RunLoop` with new methods and nested
/// types.
/// If you import both OpenCombine and Foundation, you will not be able
/// to write `RunLoop.SchedulerTimeType`,
/// because Swift is unable to understand which `SchedulerTimeType`
/// you're referring to.
///
/// So you have to write `RunLoop.OCombine.SchedulerTimeType`.
///
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
///
/// You can omit this whenever Combine is not available (e. g. on Linux).
public struct OCombine: Scheduler {
public let runLoop: RunLoop
public init(_ runLoop: RunLoop) {
self.runLoop = runLoop
}
/// The scheduler time type used by the run loop.
public struct SchedulerTimeType: Strideable, Codable, Hashable {
/// The date represented by this type.
public var date: Date
/// Initializes a run loop scheduler time with the given date.
///
/// - Parameter date: The date to represent.
public init(_ date: Date) {
self.date = date
}
/// Returns the distance to another run loop scheduler time.
///
/// - Parameter other: Another run loop time.
/// - Returns: The time interval between this time and the provided time.
public func distance(to other: SchedulerTimeType) -> Stride {
let absoluteSelf = date.timeIntervalSinceReferenceDate
let absoluteOther = other.date.timeIntervalSinceReferenceDate
return Stride(absoluteSelf.distance(to: absoluteOther))
}
/// Returns a run loop scheduler time calculated by advancing this instances
/// time by the given interval.
///
/// - Parameter value: A time interval to advance.
/// - Returns: A run loop time advanced by the given interval from this
/// instances time.
public func advanced(by value: Stride) -> SchedulerTimeType {
return SchedulerTimeType(date + value.magnitude)
}
/// The interval by which run loop times advance.
public struct Stride: SchedulerTimeIntervalConvertible,
Comparable,
SignedNumeric,
ExpressibleByFloatLiteral,
Codable {
public typealias FloatLiteralType = TimeInterval
public typealias IntegerLiteralType = TimeInterval
/// A type that can represent the absolute value of any possible value
/// of the conforming type.
public typealias Magnitude = TimeInterval
/// The value of this time interval in seconds.
public var magnitude: TimeInterval
/// The value of this time interval in seconds.
public var timeInterval: TimeInterval { return magnitude }
public init(integerLiteral value: TimeInterval) {
self.magnitude = value
}
public init(floatLiteral value: TimeInterval) {
self.magnitude = value
}
public init(_ timeInterval: TimeInterval) {
self.magnitude = timeInterval
}
public init?<Source: BinaryInteger>(exactly source: Source) {
guard let value = TimeInterval(exactly: source) else { return nil }
magnitude = value
}
public static func < (lhs: Stride, rhs: Stride) -> Bool {
return lhs.magnitude < rhs.magnitude
}
public static func * (lhs: Stride, rhs: Stride) -> Stride {
return Stride(lhs.magnitude * rhs.magnitude)
}
public static func + (lhs: Stride, rhs: Stride) -> Stride {
return Stride(lhs.magnitude + rhs.magnitude)
}
public static func - (lhs: Stride, rhs: Stride) -> Stride {
return Stride(lhs.magnitude - rhs.magnitude)
}
public static func *= (lhs: inout Stride, rhs: Stride) {
lhs.magnitude *= rhs.magnitude
}
public static func += (lhs: inout Stride, rhs: Stride) {
lhs.magnitude += rhs.magnitude
}
public static func -= (lhs: inout Stride, rhs: Stride) {
lhs.magnitude -= rhs.magnitude
}
public static func seconds(_ value: Int) -> Stride {
return Stride(TimeInterval(value))
}
public static func seconds(_ value: Double) -> Stride {
return Stride(TimeInterval(value))
}
public static func milliseconds(_ value: Int) -> Stride {
return Stride(TimeInterval(value) / 1_000)
}
public static func microseconds(_ value: Int) -> Stride {
return Stride(TimeInterval(value) / 1_000_000)
}
public static func nanoseconds(_ value: Int) -> Stride {
return Stride(TimeInterval(value) / 1_000_000_000)
}
}
}
/// Options that affect the operation of the run loop scheduler.
public struct SchedulerOptions {
}
public func schedule(options: SchedulerOptions?, _ action: @escaping () -> Void) {
let cfRunLoop = runLoop.getCFRunLoop()
CFRunLoopPerformBlock(cfRunLoop, defaultRunLoopModeString, action)
CFRunLoopWakeUp(cfRunLoop)
}
public func schedule(after date: SchedulerTimeType,
tolerance: SchedulerTimeType.Stride,
options: SchedulerOptions?,
_ action: @escaping () -> Void) {
let timer = CFRunLoopTimerCreateWithHandler(
nil,
date.date.timeIntervalSinceReferenceDate,
0,
0,
0,
{ _ in action() }
)
// A bug in Combine. The schedule(after:tolerance:options:_:) methods
// always executes the action on the current runloop.
// (FB7493579 if Apple folks are watching)
let theWrongRunLoop = CFRunLoopGetCurrent()
CFRunLoopAddTimer(theWrongRunLoop, timer, defaultRunLoopMode)
CFRunLoopWakeUp(theWrongRunLoop)
}
public func schedule(after date: SchedulerTimeType,
interval: SchedulerTimeType.Stride,
tolerance: SchedulerTimeType.Stride,
options: SchedulerOptions?,
_ action: @escaping () -> Void) -> Cancellable {
let timer = CFRunLoopTimerCreateWithHandler(
nil,
date.date.timeIntervalSinceReferenceDate,
interval.magnitude,
0,
0,
{ _ in action() }
)
let cfRunLoop = runLoop.getCFRunLoop()
CFRunLoopAddTimer(cfRunLoop, timer, defaultRunLoopMode)
CFRunLoopWakeUp(cfRunLoop)
return AnyCancellable { CFRunLoopTimerInvalidate(timer) }
}
public var now: SchedulerTimeType {
return .init(Date())
}
public var minimumTolerance: SchedulerTimeType.Stride {
return .init(0)
}
}
/// A namespace for disambiguation when both OpenCombine and Foundation are imported.
///
/// Foundation overlay for Combine extends `RunLoop` with new methods and nested
/// types.
/// If you import both OpenCombine and Foundation, you will not be able
/// to write `RunLoop.main.schedule { doThings() }`,
/// because Swift is unable to understand which `schedule` method
/// you're referring to.
///
/// So you have to write `RunLoop.main.ocombine.schedule { doThings() }`.
///
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
///
/// You can omit this whenever Combine is not available (e. g. on Linux).
public var ocombine: OCombine {
return OCombine(self)
}
}
#if !canImport(Combine)
extension RunLoop: OpenCombine.Scheduler {
/// Options that affect the operation of the run loop scheduler.
public typealias SchedulerOptions = OCombine.SchedulerOptions
/// The scheduler time type used by the run loop.
public typealias SchedulerTimeType = OCombine.SchedulerTimeType
public func schedule(options: SchedulerOptions?, _ action: @escaping () -> Void) {
ocombine.schedule(options: options, action)
}
public func schedule(after date: SchedulerTimeType,
tolerance: SchedulerTimeType.Stride,
options: SchedulerOptions?,
_ action: @escaping () -> Void) {
ocombine.schedule(after: date, tolerance: tolerance, options: options, action)
}
public func schedule(after date: SchedulerTimeType,
interval: SchedulerTimeType.Stride,
tolerance: SchedulerTimeType.Stride,
options: SchedulerOptions?,
_ action: @escaping () -> Void) -> Cancellable {
return ocombine.schedule(after: date,
interval: interval,
tolerance: tolerance,
options: options,
action)
}
public var now: SchedulerTimeType {
return ocombine.now
}
public var minimumTolerance: SchedulerTimeType.Stride {
return ocombine.minimumTolerance
}
}
#endif
private var defaultRunLoopMode: CFRunLoopMode {
#if canImport(Darwin)
return CFRunLoopMode.defaultMode
#else
return kCFRunLoopDefaultMode
#endif
}
private var defaultRunLoopModeString: CFString {
#if canImport(Darwin)
return CFRunLoopMode.defaultMode.rawValue
#else
return kCFRunLoopDefaultMode
#endif
}
@@ -0,0 +1,354 @@
//
// Timer+Publisher.swift
//
//
// Created by Sergej Jaskiewicz on 23.06.2020.
//
import CoreFoundation
import Foundation
import OpenCombine
extension Timer {
/// Returns a publisher that repeatedly emits the current date on the given interval.
///
/// - Parameters:
/// - interval: The time interval on which to publish events. For example,
/// a value of `0.5` publishes an event approximately every half-second.
/// - tolerance: The allowed timing variance when emitting events.
/// Defaults to `nil`, which allows any variance.
/// - runLoop: The run loop on which the timer runs.
/// - mode: The run loop mode in which to run the timer.
/// - options: Scheduler options passed to the timer. Defaults to `nil`.
/// - Returns: A publisher that repeatedly emits the current date on the given
/// interval.
public static func publish(
every interval: TimeInterval,
tolerance _: TimeInterval? = nil,
on runLoop: RunLoop,
in mode: RunLoop.Mode,
options: RunLoop.OCombine.SchedulerOptions? = nil
) -> OCombine.TimerPublisher {
// A bug in Combine: tolerance is ignored.
return .init(interval: interval, runLoop: runLoop, mode: mode, options: options)
}
/// A namespace for disambiguation when both OpenCombine and Combine are imported.
///
/// Foundation overlay for Combine extends `Timer` with new methods and nested
/// types.
/// If you import both OpenCombine and Foundation, you will not be able
/// to write `Timer.TimerPublisher`,
/// because Swift is unable to understand which `TimerPublisher`
/// you're referring to.
///
/// So you have to write `Timer.OCombine.TimerPublisher`.
///
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
///
/// You can omit this whenever Combine is not available (e. g. on Linux).
public enum OCombine {
/// A publisher that repeatedly emits the current date on a given interval.
public final class TimerPublisher: ConnectablePublisher {
public typealias Output = Date
public typealias Failure = Never
public let interval: TimeInterval
public let tolerance: TimeInterval?
public let runLoop: RunLoop
public let mode: RunLoop.Mode
public let options: RunLoop.OCombine.SchedulerOptions?
private lazy var routingSubscription: RoutingSubscription = {
RoutingSubscription(parent: self)
}()
/// Creates a publisher that repeatedly emits the current date
/// on the given interval.
///
/// - Parameters:
/// - interval: The interval on which to publish events.
/// - tolerance: The allowed timing variance when emitting events.
/// Defaults to `nil`, which allows any variance.
/// - runLoop: The run loop on which the timer runs.
/// - mode: The run loop mode in which to run the timer.
/// - options: Scheduler options passed to the timer. Defaults to `nil`.
public init(
interval: TimeInterval,
tolerance: TimeInterval? = nil,
runLoop: RunLoop,
mode: RunLoop.Mode,
options: RunLoop.OCombine.SchedulerOptions? = nil
) {
self.interval = interval
self.tolerance = tolerance
self.runLoop = runLoop
self.mode = mode
self.options = options
}
/// Adapter subscription to allow `Timer` to multiplex to multiple subscribers
/// the values produced by a single `TimerPublisher.Inner`
private final class RoutingSubscription
: Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
{
typealias Input = Date
typealias Failure = Never
private typealias ErasedSubscriber = AnySubscriber<Output, Failure>
private let lock = UnfairLock.allocate()
// Inner is IUP due to init requirements
// swiftlint:disable:next implicitly_unwrapped_optional
private var inner: Inner!
private var subscribers: [ErasedSubscriber] = []
private var isConnected = false
init(parent: TimerPublisher) {
inner = Inner(parent: parent, downstream: self)
}
deinit {
lock.deallocate()
}
func addSubscriber<Downstream: Subscriber>(_ downstream: Downstream)
where Downstream.Failure == Failure, Downstream.Input == Output
{
lock.lock()
subscribers.append(AnySubscriber(downstream))
lock.unlock()
downstream.receive(subscription: self)
}
func receive(_ value: Input) -> Subscribers.Demand {
var resultingDemand = Subscribers.Demand.none
lock.lock()
let subscribers = self.subscribers
let isConnected = self.isConnected
lock.unlock()
guard isConnected else {
// This branch is only reachable in case of a race condition.
return .none
}
for subscriber in subscribers {
resultingDemand += subscriber.receive(value)
}
return resultingDemand
}
func request(_ demand: Subscribers.Demand) {
lock.lock()
let inner = self.inner!
lock.unlock()
inner.request(demand)
}
func cancel() {
lock.lock()
let inner = self.inner!
isConnected = false
subscribers = []
lock.unlock()
inner.cancel()
}
var description: String { return "Timer" }
var customMirror: Mirror { return inner.customMirror }
var playgroundDescription: Any { return description }
var combineIdentifier: CombineIdentifier {
return inner.combineIdentifier
}
func startPublishing() {
lock.lock()
let isConnected = self.isConnected
self.isConnected = true
let inner = self.inner!
lock.unlock()
if isConnected { return }
inner.startPublishing()
}
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Failure == Downstream.Failure, Output == Downstream.Input
{
routingSubscription.addSubscriber(subscriber)
}
public func connect() -> Cancellable {
routingSubscription.startPublishing()
return routingSubscription
}
private typealias Parent = TimerPublisher
private final class Inner
: NSObject,
Subscription,
CustomReflectable,
CustomPlaygroundDisplayConvertible
{
private lazy var timer: CFRunLoopTimer? = {
let timer = CFRunLoopTimerCreateWithHandler(
nil,
Date().timeIntervalSinceReferenceDate,
parent?.interval ?? 0,
0,
0,
{ [weak self] _ in self?.timerFired() }
)!
CFRunLoopTimerSetTolerance(timer, parent?.tolerance ?? 0)
return timer
}()
private let lock = UnfairLock.allocate()
private var downstream: RoutingSubscription?
private var parent: Parent?
private var started = false
private var demand = Subscribers.Demand.none
init(parent: Parent, downstream: RoutingSubscription) {
self.parent = parent
self.downstream = downstream
}
deinit {
lock.deallocate()
}
func startPublishing() {
lock.lock()
guard let timer = self.timer,
let parent = self.parent,
!started else {
lock.unlock()
return
}
started = true
lock.unlock()
CFRunLoopAddTimer(parent.runLoop.getCFRunLoop(),
timer,
parent.mode.asCFRunLoopMode())
}
func cancel() {
lock.lock()
guard let timer = self.timer else {
lock.unlock()
return
}
downstream = nil
parent = nil
started = false
demand = .none
self.timer = nil
lock.unlock()
CFRunLoopTimerInvalidate(timer)
}
func request(_ demand: Subscribers.Demand) {
lock.lock()
defer { lock.unlock() }
guard parent != nil else {
return
}
self.demand += demand
}
override var description: String { return "Timer" }
var customMirror: Mirror {
lock.lock()
defer { lock.unlock() }
let children: [Mirror.Child] = [
("downstream", downstream as Any),
("interval", parent?.interval as Any),
("tolerance", parent?.tolerance as Any),
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
private func timerFired() {
lock.lock()
guard let downstream = self.downstream,
parent != nil,
demand > 0
else {
lock.unlock()
return
}
demand -= 1
lock.unlock()
let newDemand = downstream.receive(Date())
guard newDemand > 0 else {
return
}
lock.lock()
demand += newDemand
lock.unlock()
}
}
}
}
}
#if !canImport(Combine)
extension Timer {
/// A publisher that repeatedly emits the current date on a given interval.
public typealias TimerPublisher = OCombine.TimerPublisher
}
#endif
extension RunLoop.Mode {
fileprivate func asCFRunLoopMode() -> CFRunLoopMode {
#if canImport(Darwin)
return CFRunLoopMode(rawValue as CFString)
#else
return rawValue.withCString {
#if swift(>=5.3)
let encoding = CFStringBuiltInEncodings.UTF8.rawValue
#else
let encoding = CFStringEncoding(kCFStringEncodingUTF8)
#endif // swift(>=5.3)
return CFStringCreateWithCString(
nil,
$0,
encoding
)
}
#endif
}
}
@@ -0,0 +1,233 @@
//
// URLSession.swift
//
//
// Created by Sergej Jaskiewicz on 13.12.2019.
//
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
import OpenCombine
extension URLSession {
/// A namespace for disambiguation when both OpenCombine and Foundation are imported.
///
/// Foundation extends `URLSession` with new methods and nested types.
/// If you import both OpenCombine and Foundation, you will not be able
/// to write `URLSession.DataTaskPublisher`,
/// because Swift is unable to understand which `DataTaskPublisher`
/// you're referring to the one declared in Foundation or in OpenCombine.
///
/// So you have to write `URLSession.OCombine.DataTaskPublisher`.
///
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
///
/// You can omit this whenever Combine is not available (e. g. on Linux).
public struct OCombine {
public let session: URLSession
public init(_ session: URLSession) {
self.session = session
}
public struct DataTaskPublisher: Publisher {
public typealias Output = (data: Data, response: URLResponse)
public typealias Failure = URLError
public let request: URLRequest
public let session: URLSession
public init(request: URLRequest, session: URLSession) {
self.request = request
self.session = session
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Failure == Failure, Downstream.Input == Output
{
let subscription = Inner(parent: self, downstream: subscriber)
subscriber.receive(subscription: subscription)
}
}
/// Returns a publisher that wraps a URL session data task for a given URL.
///
/// The publisher publishes data when the task completes, or terminates if
/// the task fails with an error.
///
/// - Parameter url: The URL for which to create a data task.
/// - Returns: A publisher that wraps a data task for the URL.
public func dataTaskPublisher(for url: URL) -> DataTaskPublisher {
return dataTaskPublisher(for: URLRequest(url: url))
}
/// Returns a publisher that wraps a URL session data task for a given
/// URL request.
///
/// The publisher publishes data when the task completes, or terminates if
/// the task fails with an error.
///
/// - Parameter request: The URL request for which to create a data task.
/// - Returns: A publisher that wraps a data task for the URL request.
public func dataTaskPublisher(for request: URLRequest) -> DataTaskPublisher {
return .init(request: request, session: session)
}
}
#if !canImport(Combine)
public typealias DataTaskPublisher = OCombine.DataTaskPublisher
#endif
}
extension URLSession {
/// A namespace for disambiguation when both OpenCombine and Foundation are imported.
///
/// Foundation extends `URLSession` with new methods and nested types.
/// If you import both OpenCombine and Foundation, you will not be able
/// to write `URLSession.shared.dataTaskPublisher(for: url)`,
/// because Swift is unable to understand which `dataTaskPublisher` method
/// you're referring to the one declared in Foundation or in OpenCombine.
///
/// So you have to write `URLSession.shared.ocombine.dataTaskPublisher(for: url)`.
///
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
///
/// You can omit this whenever Combine is not available (e. g. on Linux).
public var ocombine: OCombine { return .init(self) }
#if !canImport(Combine)
/// Returns a publisher that wraps a URL session data task for a given URL.
///
/// The publisher publishes data when the task completes, or terminates if the task
/// fails with an error.
///
/// - Parameter url: The URL for which to create a data task.
/// - Returns: A publisher that wraps a data task for the URL.
public func dataTaskPublisher(for url: URL) -> DataTaskPublisher {
return ocombine.dataTaskPublisher(for: url)
}
/// Returns a publisher that wraps a URL session data task for a given URL request.
///
/// The publisher publishes data when the task completes, or terminates if the task
/// fails with an error.
///
/// - Parameter request: The URL request for which to create a data task.
/// - Returns: A publisher that wraps a data task for the URL request.
public func dataTaskPublisher(for request: URLRequest) -> DataTaskPublisher {
return ocombine.dataTaskPublisher(for: request)
}
#endif
}
extension URLSession.OCombine.DataTaskPublisher {
private class Inner<Downstream: Subscriber>
: Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == (data: Data, response: URLResponse),
Downstream.Failure == URLError
{
private let lock = UnfairLock.allocate()
private var parent: URLSession.OCombine.DataTaskPublisher?
private var downstream: Downstream?
private var demand = Subscribers.Demand.none
private var task: URLSessionDataTask?
fileprivate init(parent: URLSession.OCombine.DataTaskPublisher,
downstream: Downstream) {
self.parent = parent
self.downstream = downstream
}
deinit {
lock.deallocate()
}
func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
lock.lock()
guard let parent = self.parent else {
lock.unlock()
return
}
if self.task == nil {
task = parent.session.dataTask(with: parent.request,
completionHandler: handleResponse)
}
self.demand += demand
let task = self.task
lock.unlock()
task?.resume()
}
private func handleResponse(data: Data?, response: URLResponse?, error: Error?) {
lock.lock()
guard demand > 0, parent != nil, let downstream = self.downstream else {
lock.unlock()
return
}
lockedTerminate()
lock.unlock()
switch (data, response, error) {
case let (data, response?, nil):
_ = downstream.receive((data ?? Data(), response))
downstream.receive(completion: .finished)
case let (_, _, error as URLError):
downstream.receive(completion: .failure(error))
default:
downstream.receive(completion: .failure(URLError(.unknown)))
}
}
func cancel() {
lock.lock()
guard parent != nil else {
lock.unlock()
return
}
let task = self.task
lockedTerminate()
lock.unlock()
task?.cancel()
}
private func lockedTerminate() {
parent = nil
downstream = nil
demand = .none
task = nil
}
var description: String { return "DataTaskPublisher" }
var customMirror: Mirror {
lock.lock()
defer { lock.unlock() }
let children: [Mirror.Child] = [
("task", task as Any),
("downstream", downstream as Any),
("parent", parent as Any),
("demand", demand)
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { return description }
}
}
@@ -5,7 +5,6 @@
// Created by Sergej Jaskiewicz on 13.06.2019.
//
import GottaGoFast
import XCTest
#if OPENCOMBINE_COMPATIBILITY_TEST
@@ -15,7 +14,7 @@ import OpenCombine
#endif
@available(macOS 10.15, iOS 13.0, *)
final class CombineIdentifierTests: PerformanceTestCase {
final class CombineIdentifierTests: XCTestCase {
func testDefaultInitialized() {
let id1 = CombineIdentifier()
@@ -43,11 +42,9 @@ final class CombineIdentifierTests: PerformanceTestCase {
"0x\(String(UInt(bitPattern: ObjectIdentifier(c1)), radix: 16))")
}
func testDefaultInitializedPerformance() throws {
try benchmark(allowFailure: isDebug, executionCount: 500) {
for _ in 0..<2000 {
blackHole(CombineIdentifier())
}
}
func testUsesUInt64UnderTheHood() {
let mirror = Mirror(reflecting: CombineIdentifier())
XCTAssertEqual(mirror.children.count, 1)
XCTAssertNotNil(mirror.descendant("value") as? UInt64)
}
}
@@ -102,6 +102,36 @@ final class CurrentValueSubjectTests: XCTestCase {
XCTAssertEqual(numberOfInputsHistory, expectedNumberOfInputsHistory)
}
func testRequestSeveralTimes() throws {
let cvs = Sut(-1)
var downstreamSubscription: Subscription?
let tracking = TrackingSubscriber(
receiveSubscription: { downstreamSubscription = $0 }
)
cvs.subscribe(tracking)
XCTAssertEqual(tracking.history, [.subscription("CurrentValueSubject")])
try XCTUnwrap(downstreamSubscription).request(.max(2))
try XCTUnwrap(downstreamSubscription).request(.max(3))
try XCTUnwrap(downstreamSubscription).request(.max(1))
XCTAssertEqual(tracking.history, [.subscription("CurrentValueSubject"),
.value(-1)])
for i in 0 ..< 10 {
cvs.send(i)
}
XCTAssertEqual(tracking.history, [.subscription("CurrentValueSubject"),
.value(-1),
.value(0),
.value(1),
.value(2),
.value(3),
.value(4)])
}
func testCrashOnZeroInitialDemand() {
assertCrashes {
let subscriber = TrackingSubscriber(
@@ -139,6 +169,20 @@ final class CurrentValueSubjectTests: XCTestCase {
.completion(.failure(.oops))])
}
func testChangeValueAfterCompletion() {
let cvs = Sut(0)
cvs.send(completion: .finished)
cvs.value = 42
XCTAssertEqual(cvs.value, 42)
}
func testSendValueAfterCompletion() {
let cvs = Sut(0)
cvs.send(completion: .finished)
cvs.send(42)
XCTAssertEqual(cvs.value, 0)
}
func testMultipleSubscriptions() {
let cvs = Sut(112)
@@ -224,6 +268,42 @@ final class CurrentValueSubjectTests: XCTestCase {
.subscription("CurrentValueSubject"),
.value(112),
.subscription("CurrentValueSubject")])
for (i, subscription) in subscriber.tracking.subscriptions.enumerated()
where i.isMultiple(of: 2)
{
subscription.cancel()
}
cvs.value = 200
XCTAssertEqual(subscriber.tracking.history,
[.value(112),
.subscription("CurrentValueSubject"),
.value(112),
.subscription("CurrentValueSubject"),
.value(112),
.subscription("CurrentValueSubject"),
.value(112),
.subscription("CurrentValueSubject"),
.value(112),
.subscription("CurrentValueSubject"),
.value(112),
.subscription("CurrentValueSubject"),
.value(112),
.subscription("CurrentValueSubject"),
.value(112),
.subscription("CurrentValueSubject"),
.value(112),
.subscription("CurrentValueSubject"),
.value(112),
.subscription("CurrentValueSubject"),
.value(112),
.subscription("CurrentValueSubject"),
.value(200),
.value(200),
.value(200),
.value(200),
.value(200)])
}
// Reactive Streams Spec: Rule #6
@@ -375,6 +455,100 @@ final class CurrentValueSubjectTests: XCTestCase {
XCTAssertEqual(subscription2.history, [.requested(.unlimited)])
}
func testCompletion() throws {
let passthrough = Sut(42)
var downstreamSubscription: Subscription?
let tracking = TrackingSubscriber(
receiveSubscription: { downstreamSubscription = $0 }
)
passthrough.subscribe(tracking)
try XCTUnwrap(downstreamSubscription).request(.max(12))
passthrough.send(1)
expectedChildren(
("parent", .contains("CurrentValueSubject")),
("downstream", .contains("TrackingSubscriberBase")),
("demand", "max(10)"),
("subject", .contains("CurrentValueSubject"))
)(Mirror(reflecting: try XCTUnwrap(downstreamSubscription)))
passthrough.send(completion: .finished)
if !hasCustomMirrorUseAfterFreeBug {
expectedChildren(
("parent", "nil"),
("downstream", "nil"),
("demand", "max(10)"),
("subject", "nil")
)(Mirror(reflecting: try XCTUnwrap(downstreamSubscription)))
}
XCTAssertEqual(tracking.history, [.subscription("CurrentValueSubject"),
.value(42),
.value(1),
.completion(.finished)])
passthrough.send(completion: .failure(.oops))
try XCTUnwrap(downstreamSubscription).cancel()
try XCTUnwrap(downstreamSubscription).request(.max(3))
if !hasCustomMirrorUseAfterFreeBug {
expectedChildren(
("parent", "nil"),
("downstream", "nil"),
("demand", "max(10)"),
("subject", "nil")
)(Mirror(reflecting: try XCTUnwrap(downstreamSubscription)))
}
XCTAssertEqual(tracking.history, [.subscription("CurrentValueSubject"),
.value(42),
.value(1),
.completion(.finished)])
}
func testCancellation() throws {
let cvs = Sut(42)
var downstreamSubscription: Subscription?
let tracking = TrackingSubscriber(
receiveSubscription: { downstreamSubscription = $0 }
)
cvs.subscribe(tracking)
try XCTUnwrap(downstreamSubscription).request(.max(12))
cvs.send(1)
expectedChildren(
("parent", .contains("CurrentValueSubject")),
("downstream", .contains("TrackingSubscriberBase")),
("demand", "max(10)"),
("subject", .contains("CurrentValueSubject"))
)(Mirror(reflecting: try XCTUnwrap(downstreamSubscription)))
try XCTUnwrap(downstreamSubscription).cancel()
try XCTUnwrap(downstreamSubscription).cancel()
try XCTUnwrap(downstreamSubscription).request(.max(3))
try XCTUnwrap(downstreamSubscription).request(.max(4))
if !hasCustomMirrorUseAfterFreeBug {
expectedChildren(
("parent", "nil"),
("downstream", "nil"),
("demand", "max(10)"),
("subject", "nil")
)(Mirror(reflecting: try XCTUnwrap(downstreamSubscription)))
}
XCTAssertEqual(tracking.history, [.subscription("CurrentValueSubject"),
.value(42),
.value(1)])
}
func testLifecycle() throws {
var deinitCounter = 0
@@ -425,68 +599,87 @@ final class CurrentValueSubjectTests: XCTestCase {
XCTAssertEqual(deinitCounter, 2)
}
func testSynchronization() {
let subscriptions = Atomic<[Subscription]>([])
let inputs = Atomic<[Int]>([])
let completions = Atomic<[Subscribers.Completion<TestingError>]>([])
let cvs = Sut(112)
let subscriber = AnySubscriber<Int, TestingError>(
receiveSubscription: { subscription in
subscriptions.do { $0.append(subscription) }
subscription.request(.unlimited)
},
receiveValue: { value in
inputs.do { $0.append(value) }
return .none
},
receiveCompletion: { completion in
completions.do { $0.append(completion) }
func testCancelsUpstreamSubscriptionsOnDeinit() {
let subscription = CustomSubscription()
do {
let cvs = Sut(42)
for _ in 0 ..< 5 {
cvs.send(subscription: subscription)
}
)
XCTAssertEqual(subscription.history, [.requested(.unlimited),
.requested(.unlimited),
.requested(.unlimited),
.requested(.unlimited),
.requested(.unlimited)])
}
race(
{
cvs.subscribe(subscriber)
},
{
cvs.subscribe(subscriber)
XCTAssertEqual(subscription.history, [.requested(.unlimited),
.requested(.unlimited),
.requested(.unlimited),
.requested(.unlimited),
.requested(.unlimited),
.cancelled,
.cancelled,
.cancelled,
.cancelled,
.cancelled])
}
func testReleasesEverythingOnTermination() {
enum TerminationReason: CaseIterable {
case cancelled
case finished
case failed
}
for reason in TerminationReason.allCases {
weak var weakSubscriber: TrackingSubscriber?
weak var weakSubject: Sut?
weak var weakSubscription: AnyObject?
do {
let subject = Sut(42)
do {
let subscriber = TrackingSubscriber(
receiveSubscription: {
weakSubscription = $0 as AnyObject
}
)
weakSubscriber = subscriber
weakSubject = subject
subject.subscribe(subscriber)
}
switch reason {
case .cancelled:
(weakSubscription as? Subscription)?.cancel()
case .finished:
subject.send(completion: .finished)
case .failed:
subject.send(completion: .failure(.oops))
}
XCTAssertNil(weakSubscriber, "Subscriber leaked - \(reason)")
XCTAssertNil(weakSubscription, "Subscription leaked - \(reason)")
}
XCTAssertNil(weakSubject, "Subject leaked - \(reason)")
}
}
func testConduitReflection() throws {
try testSubscriptionReflection(
description: "CurrentValueSubject",
customMirror: expectedChildren(
("parent", .contains("CurrentValueSubject")),
("downstream", .contains("TrackingSubscriberBase")),
("demand", "max(0)"),
("subject", .contains("CurrentValueSubject"))
),
playgroundDescription: "CurrentValueSubject",
sut: CurrentValueSubject<Int, Error>(42)
)
XCTAssertEqual(subscriptions.value.count, 200)
race(
{
cvs.value = 42
},
{
cvs.value = 42
}
)
XCTAssertEqual(inputs.value.count, 40200)
XCTAssertEqual(cvs.value, 42)
race(
{
subscriptions.value[0].request(.max(4))
},
{
subscriptions.value[0].request(.max(10))
}
)
race(
{
cvs.send(completion: .finished)
},
{
cvs.send(completion: .failure(""))
}
)
XCTAssertEqual(completions.value.count, 200)
}
}

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