Compare commits
20 Commits
0.11.0
...
combine-latest
| Author | SHA1 | Date | |
|---|---|---|---|
| 26e86a9905 | |||
| bab8e08d2f | |||
| 4060ee9f57 | |||
| 5996772433 | |||
| cd45c77fac | |||
| e618d179fe | |||
| 4fa5f48c19 | |||
| 28993ae57d | |||
| 3d61bf87e7 | |||
| 911a4e1aa3 | |||
| beb38dec0e | |||
| 1fbf688897 | |||
| 5436868053 | |||
| 4977ca158f | |||
| 96214ac5f9 | |||
| 21fda909f5 | |||
| 8438d09b82 | |||
| 30a60b52cc | |||
| a93ed143fb | |||
| e054a884ef |
+44
-15
@@ -27,24 +27,24 @@ macOS_tests_steps: &macOS_tests_steps
|
||||
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
|
||||
name: Building for testing on macOS with xcodebuild
|
||||
command: |
|
||||
set -o pipefail \
|
||||
&& xcodebuild build-for-testing \
|
||||
-scheme OpenCombine-Package \
|
||||
-sdk macosx10.15 \
|
||||
-sdk macosx \
|
||||
-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
|
||||
name: Testing on macOS with xcodebuild
|
||||
command: |
|
||||
set -o pipefail \
|
||||
&& xcodebuild test-without-building \
|
||||
-scheme OpenCombine-Package \
|
||||
-sdk macosx10.15 \
|
||||
-sdk macosx \
|
||||
-derivedDataPath DerivedData \
|
||||
| tee xcodebuild_test-without-building.log \
|
||||
| xcpretty --report junit -o build/reports/results.xml
|
||||
@@ -113,36 +113,43 @@ jobs:
|
||||
environment:
|
||||
SWIFT_VERSION: "5.3.0"
|
||||
<<: *macOS_tests_steps
|
||||
|
||||
"Execute compatibility tests on iOS 14.1 (Xcode 12.1.0, Swift 5.3.0)":
|
||||
|
||||
"Execute tests on macOS 11.4.0 (Xcode 12.5.0, Swift 5.4.0)":
|
||||
macos:
|
||||
xcode: "12.1.0"
|
||||
xcode: "12.5.0"
|
||||
environment:
|
||||
SWIFT_VERSION: "5.3.0"
|
||||
SWIFT_VERSION: "5.4.0"
|
||||
<<: *macOS_tests_steps
|
||||
|
||||
"Execute compatibility tests on iOS 14.5 (Xcode 12.5.0, Swift 5.4.0)":
|
||||
macos:
|
||||
xcode: "12.5.0"
|
||||
environment:
|
||||
SWIFT_VERSION: "5.4.0"
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Generating Xcode project
|
||||
command: make generate-compatibility-xcodeproj
|
||||
- run:
|
||||
name: Building for testing on iOS 14.1 with xcodebuild
|
||||
name: Building for testing on iOS 14.5 with xcodebuild
|
||||
command: |
|
||||
set -o pipefail \
|
||||
&& xcodebuild build-for-testing \
|
||||
-scheme OpenCombine-Package \
|
||||
-destination "platform=iOS Simulator,name=iPhone 11,OS=14.1" \
|
||||
-destination "platform=iOS Simulator,name=iPhone 12,OS=14.5" \
|
||||
-derivedDataPath DerivedData \
|
||||
| tee xcodebuild_build-for-testing.log \
|
||||
| xcpretty
|
||||
- store_artifacts:
|
||||
path: xcodebuild_build-for-testing.log
|
||||
- run:
|
||||
name: Testing against Combine on iOS 14.1 with xcodebuild
|
||||
name: Testing against Combine on iOS 14.5 with xcodebuild
|
||||
command: |
|
||||
set -o pipefail \
|
||||
&& xcodebuild test-without-building \
|
||||
-scheme OpenCombine-Package \
|
||||
-destination "platform=iOS Simulator,name=iPhone 11,OS=14.1" \
|
||||
-destination "platform=iOS Simulator,name=iPhone 12,OS=14.5" \
|
||||
-derivedDataPath DerivedData \
|
||||
| tee xcodebuild_test-without-building.log \
|
||||
| xcpretty --report junit -o build/reports/results.xml
|
||||
@@ -151,6 +158,18 @@ jobs:
|
||||
- store_test_results:
|
||||
path: build/reports
|
||||
|
||||
"Execute compatibility tests on macOS 11.4.0 (Xcode 12.5.0, Swift 5.4.0)":
|
||||
macos:
|
||||
xcode: "12.5.0"
|
||||
environment:
|
||||
SWIFT_VERSION: "5.4.0"
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Testing against Combine on macOS 11.4.0
|
||||
command: |
|
||||
make test-compatibility
|
||||
|
||||
"Execute tests on iOS 9.3 (Xcode 10.2.1, Swift 5.0.1)":
|
||||
macos:
|
||||
xcode: "10.2.1"
|
||||
@@ -239,10 +258,17 @@ jobs:
|
||||
environment:
|
||||
SWIFT_VERSION: "5.3"
|
||||
<<: *ubuntu_tests_steps
|
||||
|
||||
"Execute tests on Ubuntu 18.04 (Swift 5.4)":
|
||||
docker:
|
||||
- image: swift:5.4-bionic
|
||||
environment:
|
||||
SWIFT_VERSION: "5.4"
|
||||
<<: *ubuntu_tests_steps
|
||||
|
||||
"Run SwiftLint and Danger":
|
||||
macos:
|
||||
xcode: "11.3.0"
|
||||
xcode: "11.4.0"
|
||||
environment:
|
||||
HOMEBREW_NO_AUTO_UPDATE: "1"
|
||||
steps:
|
||||
@@ -261,7 +287,7 @@ jobs:
|
||||
|
||||
"Run Pod spec lint":
|
||||
macos:
|
||||
xcode: "11.3.0"
|
||||
xcode: "11.4.0"
|
||||
environment:
|
||||
HOMEBREW_NO_AUTO_UPDATE: "1"
|
||||
steps:
|
||||
@@ -277,9 +303,11 @@ workflows:
|
||||
jobs:
|
||||
- "Execute tests on macOS 10.15.0 (Xcode 11.3.0, Swift 5.1.3)"
|
||||
- "Execute tests on macOS 10.15.0 (Xcode 12.1.0, Swift 5.3.0)"
|
||||
- "Execute tests on macOS 11.4.0 (Xcode 12.5.0, Swift 5.4.0)"
|
||||
"OpenCombine: execute compatibility tests":
|
||||
jobs:
|
||||
- "Execute compatibility tests on iOS 14.1 (Xcode 12.1.0, Swift 5.3.0)"
|
||||
# - "Execute compatibility tests on iOS 14.5 (Xcode 12.5.0, Swift 5.4.0)"
|
||||
- "Execute compatibility tests on macOS 11.4.0 (Xcode 12.5.0, Swift 5.4.0)"
|
||||
"OpenCombine: execute tests on iOS":
|
||||
jobs:
|
||||
- "Execute tests on iOS 9.3 (Xcode 10.2.1, Swift 5.0.1)"
|
||||
@@ -289,6 +317,7 @@ workflows:
|
||||
- "Execute tests on Ubuntu 18.04 (Swift 5.1)"
|
||||
- "Execute tests on Ubuntu 18.04 (Swift 5.2)"
|
||||
- "Execute tests on Ubuntu 18.04 (Swift 5.3)"
|
||||
- "Execute tests on Ubuntu 18.04 (Swift 5.4)"
|
||||
"OpenCombine: run SwiftLint and Danger":
|
||||
jobs:
|
||||
- "Run SwiftLint and Danger"
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
name: SwiftWasm
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
carton_wasmer_test:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: swiftwasm/swiftwasm-action@v5.3
|
||||
|
||||
@@ -2,6 +2,8 @@ included:
|
||||
- Sources
|
||||
- Tests
|
||||
|
||||
child_config: Tests/.swiftlint.yml
|
||||
|
||||
disabled_rules:
|
||||
- block_based_kvo
|
||||
- class_delegate_protocol
|
||||
|
||||
+299
@@ -0,0 +1,299 @@
|
||||
# 0.12.0 (29 Jan 2021)
|
||||
|
||||
This release adds a new `OpenCombineShim` product that will conditionally re-export either
|
||||
Combine on Apple platforms, or OpenCombine on other platforms. Additionally, `ObservableObject`
|
||||
protocol is now available and working on all platforms.
|
||||
|
||||
A bug with `Timer(timeInterval:repeats:block:)` firing immediately not accounting for the passed
|
||||
`timeInterval` is fixed.
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Fix `Timer(timeInterval:repeats:block:)` not accounting `timeInterval` ([#196](https://github.com/OpenCombine/OpenCombine/pull/196)) via [@grigorye](https://github.com/grigorye)
|
||||
- Add `OpenCombineShim` product for easier importing ([#197](https://github.com/OpenCombine/OpenCombine/pull/197)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Implementation for `ObservableObject` with `Mirror` ([#201](https://github.com/OpenCombine/OpenCombine/pull/201)) via [@kateinoigakukun](https://github.com/kateinoigakukun)
|
||||
|
||||
# 0.11.0 (29 Oct 2020)
|
||||
|
||||
This release is compatible with Xcode 12.1.
|
||||
|
||||
### Additions
|
||||
- `Publisher.assigned(to:)` method that accepts a `Published.Publisher`.
|
||||
- New `Publisher.switchToLatest()` overloads.
|
||||
- New `Publisher.flatMap(maxPublishers:_:)` overloads.
|
||||
- `Optional.publisher` property.
|
||||
- New `_Introspection` protocol that allows to track and explore the subscription graph and data flow.
|
||||
|
||||
### Bugfixes
|
||||
- The project should now compile without warnings.
|
||||
- The following entities have been updated to match the behavior of the newest Combine version:
|
||||
- `Subscribers.Assign`
|
||||
- `Publishers.Breakpoint`
|
||||
- `Publishers.Buffer`
|
||||
- `CombineIdentifier`
|
||||
- `Publishers.CompactMap`
|
||||
- `Publishers.Concatenate`
|
||||
- `Publishers.Debounce`
|
||||
- `Publishers.Delay`
|
||||
- `DispatchQueue.SchedulerTimeType.Stride`
|
||||
- `Publishers.Drop`
|
||||
- `Publishers.Encode`
|
||||
- `Publishers.Decode`
|
||||
- `Publishers.Filter`
|
||||
- `Publishers.HandleEvents`
|
||||
- `Publishers.IgnoreOutput`
|
||||
- `Publishers.MeasureInterval`
|
||||
- `OperationQueue` scheduler
|
||||
- `Published`
|
||||
- `Publishers.ReceiveOn`
|
||||
- `Publishers.ReplaceError`
|
||||
- `RunLoop scheduler`
|
||||
- `Publishers.Sequence`
|
||||
- `Subscribers.Sink`
|
||||
- `Publishers.SubscribeOn`
|
||||
- `Publishers.Timeout`
|
||||
- `Timer` publisher
|
||||
|
||||
### Known issues
|
||||
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1 and later.
|
||||
|
||||
# 0.10.2 (23 Oct 2020)
|
||||
|
||||
### Bugfixes
|
||||
- Fixed a crash caused by recursive acquisition of a non-recursive lock in SubbjectSubscriber (#186, thanks @stuaustin for the bug report)
|
||||
|
||||
### Known issues
|
||||
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1 and later.
|
||||
|
||||
# 0.10.1 (4 Oct 2020)
|
||||
|
||||
### Bugfixes
|
||||
- Fixed build errors on Linux with Swift 5.0 and Swift 5.3 toolchains (thanks, @adamleonard and @devmaximilian)
|
||||
|
||||
### Known issues
|
||||
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1 and later.
|
||||
|
||||
# 0.10.0 (28 Jun 2020)
|
||||
|
||||
This release is compatible with Xcode 11.5.
|
||||
|
||||
### Additions
|
||||
- `Timer.publish(every:tolerance:on:in:options:)` (#156, thank you @MaxDesiatov)
|
||||
- `OperationQueue` scheduler (#165)
|
||||
- `Publishers.Timeout` (#164)
|
||||
- `Publishers.Debounce` (#133)
|
||||
|
||||
### Bugfixes
|
||||
- `PassthroughSubject`, `CurrentValueSubject` and `Future` have been rewritten from scratch. They are now faster, more correct and no longer leak subscriptions (#170).
|
||||
|
||||
### Known issues
|
||||
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1 and later.
|
||||
|
||||
# 0.9.0 (12 Jun 2020)
|
||||
|
||||
This release is compatible with Xcode 11.5.
|
||||
|
||||
### Additions
|
||||
- The `Subscribers.Demand` struct can be nicely formatted in LLDB (#146, thank you @mayoff).
|
||||
- `Publishers.SwitchToLatest` (#142).
|
||||
- The `RunLoop` scheduler in `OpenCombineFoundation` (#131).
|
||||
- `Publishers.Catch` and `Publishers.TryCatch` (#140).
|
||||
|
||||
### Bugfixes
|
||||
- Worked around a [bug in the Swift compiler](https://bugs.swift.org/browse/SR-11680) when building the `COpenCombineHelpers` target (#145, thank you @mayoff).
|
||||
- Improved documentation.
|
||||
|
||||
### Known issues
|
||||
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1 and later.
|
||||
|
||||
# 0.8.0 (17 Jan 2020)
|
||||
|
||||
This release is compatible with Xcode 11.3.1.
|
||||
|
||||
### Additions
|
||||
- `Publishers.ReplaceEmpty` (#122, thank you @spadafiva)
|
||||
- `NotificationCenter.Publisher` (#84)
|
||||
- `URLSession.DataTaskPublisher` (#127)
|
||||
- `Publishers.DropUntilOutput` (#136)
|
||||
- `Publishers.CollectByCount` (#137)
|
||||
- `Publishers.AssertNoFailure` (#138)
|
||||
- `Publishers.Buffer` (#143)
|
||||
|
||||
### Bugfixes
|
||||
- Fixed integer overflows in `DispatchQueue.SchedulerTimeType.Stride` (#126, #130)
|
||||
- Fixed the 'default will never be executed' warning on non-Darwin platforms (like Linux) (#129)
|
||||
|
||||
### Known issues
|
||||
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1.
|
||||
|
||||
# 0.7.0 (10 Dec 2019)
|
||||
|
||||
This release is compatible with Xcode 11.2.1.
|
||||
|
||||
### Additions
|
||||
- `Publishers.Delay` (#114)
|
||||
- `Publishers.ReceiveOn` (#115)
|
||||
- `Publishers.SubscribeOn` (#116)
|
||||
- `Publishers.MeasureInterval` (#117)
|
||||
- `Publishers.Breakpoint` (#118)
|
||||
- `Publishers.HandleEvents` (#118)
|
||||
- `Publishers.Concatenate` (#90)
|
||||
|
||||
### Known issues
|
||||
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1.
|
||||
|
||||
# 0.6.0 (26 Nov 2019)
|
||||
|
||||
This release is compatible with Xcode 11.2.1.
|
||||
|
||||
### Thread safety
|
||||
- `Publishers.IgnoreOutput` has been audited for thread safety (#88)
|
||||
- `Publishers.DropWhile` and `Publishers.TryDropWhile` have been audited for thread safety (#87)
|
||||
|
||||
### Additions
|
||||
- `Publishers.Output` (#91)
|
||||
- `Record` (#100)
|
||||
- `Publishers.RemoveDuplicates`, `Publishers.TryRemoveDuplicates` (#89)
|
||||
- `Publishers.PrefixWhile`, `Publishers.TryPrefixWhile` (#89)
|
||||
- `Future` (#107, thanks @MaxDesiatov!)
|
||||
|
||||
### Bugfixes
|
||||
- The behavior of the `Publishers.Encode` and `Publishers.Decode` subscriptions is fixed (#112)
|
||||
- The behavior of the `Publishers.IgnoreOutput` subscription is fixed (#88)
|
||||
- The behavior of the `Publishers.Print` subscription is fixed (#92)
|
||||
- The behavior of the `Publishers.ReplaceError` subscription is fixed (#89)
|
||||
- The behavior of the `Publishers.Filter` and `Publishers.TryFilter` subscriptions is fixed (#89)
|
||||
- The behavior of the `Publishers.CompactMap` and `Publishers.TryCompactMap` subscriptions is fixed (#89)
|
||||
- The behavior of the `Publishers.Multicast` subscription is fixed (#110)
|
||||
- `Publishers.FlatMap` is reimplemented from scratch. Its behavior is fixed in many ways, it now fully matches that of Combine (#89)
|
||||
- `@Published` property wrapper is fixed! (#112)
|
||||
- The behavior of `DispatchQueue.SchedulerTimeType` is fixed to match that of the latest SDKs (#96)
|
||||
- OpenCombine is now usable on 32 bit platforms. Why? Because we can.
|
||||
|
||||
|
||||
### Known issues
|
||||
- The default implementation of the `objectWillChange` requirement of the `ObservableObject` protocol is not available in Swift 5.1.
|
||||
|
||||
# 0.5.0 (17 Oct 2019)
|
||||
|
||||
This release is compatible with Xcode 11.1.
|
||||
|
||||
### Additions
|
||||
- `Publishers.MapKeyPath` (#71)
|
||||
- `Publishers.Reduce` (#76)
|
||||
- `Publishers.TryReduce` (#76)
|
||||
- `Publishers.Last` (#76)
|
||||
- `Publishers.LastWhere` (#76)
|
||||
- `Publishers.TryLastWhere` (#76)
|
||||
- `Publishers.AllSatisfy` (#76)
|
||||
- `Publishers.TryAllSatisfy` (#76)
|
||||
- `Publishers.Contains` (#76)
|
||||
- `Publishers.ContainsWhere` (#76)
|
||||
- `Publishers.TryContainsWhere` (#76)
|
||||
- `Publishers.Collect` (#76)
|
||||
- `Publishers.Comparison` (#76)
|
||||
- `Publishers.Drop` (#70, thank you @5sw!)
|
||||
- `Publishers.Scan` (#83, thank you @epatey!)
|
||||
- `Publishers.TryScan` (#83, thank you @epatey!)
|
||||
|
||||
### Bugfixes
|
||||
- `Publishers.Print` doesn't print a redundant whitespace anymore.
|
||||
|
||||
### Known issues
|
||||
- `@Published` property wrapper doesn't work yet
|
||||
|
||||
# 0.4.0 (8 Oct 2019)
|
||||
|
||||
This release is compatible with Xcode 11.1.
|
||||
|
||||
### Thread safety
|
||||
- `SubjectSubscriber` (which is used when you subscribe a subject to a publisher) has been audited for thread-safety
|
||||
- `Publishers.Multicast` has been audited for thread safety (#63)
|
||||
- `Publishers.TryMap` has been audited for thread safety
|
||||
- `Just` has been audited for thread safety
|
||||
- `Optional.Publisher` has been audited for thread safety
|
||||
- `Publishers.Sequence` has been audited for thread safety
|
||||
- `Publishers.ReplaceError` has been audited for thread safety
|
||||
- `Subscribers.Assign` has been audited for thread safety
|
||||
- `Subscribers.Sink` has been audited for thread safety
|
||||
|
||||
### Bugfixes
|
||||
- The semantics of `Publishers.Print`, `Publishers.TryMap` have been fixed
|
||||
- Fix `iterator.next()` being called twice in `Publishers.Sequence` (#62)
|
||||
- The default initializer of `CombineIdentifier` (the one that takes no arguments) is now much faster (#66, #69)
|
||||
- When `Publishers.Sequence` subscription is cancelled while it emits values, the cancellation is respected (#73, thanks @5sw!)
|
||||
|
||||
### Additions
|
||||
- `DispatchQueueScheduler` (#46)
|
||||
- `Equatable` conformances for `First`, `ReplaceError`
|
||||
- Added `eraseToAnyPublisher()` method (#59, thanks @evyasafhouzz for reporting!)
|
||||
- `Publishers.MakeConnectable` (#61)
|
||||
- `Publishers.Autoconnect` (#60)
|
||||
- `Publishers.Share` (#60)
|
||||
|
||||
### Known issues
|
||||
- `@Published` property wrapper doesn't work yet
|
||||
|
||||
# 0.3.0 (13 Sep 2019)
|
||||
|
||||
Among other things this release is compatible with Xcode 11.0 GM seed.
|
||||
|
||||
### Bugfixes
|
||||
- Store newly send value in internal variable inside CurrentValueObject (#39, thanks @FranzBusch!)
|
||||
|
||||
### Additions
|
||||
- `Filter`/`TryFilter` (#22, thanks @spadafiva!)
|
||||
- `First`/`FirstWhere`/`TryFirstWhere` (#22, thanks again @spadafiva!)
|
||||
- `CompactMap`/`TryCompacrMap` (#32)
|
||||
- `IgnoreOutput` (#44, thanks @epatey!)
|
||||
- `ReplaceError` (#50, thanks @vladiulianbogdan!)
|
||||
- `FlatMap` (#45, thanks again @epatey!)
|
||||
|
||||
### Known issues
|
||||
- `@Published` property wrapper doesn't work yet
|
||||
|
||||
# 0.2.0 (31 Jul 2019)
|
||||
|
||||
Updated for the newest Xcode 11.0 beta 5
|
||||
|
||||
# 0.1.0 (4 Jul 2019)
|
||||
|
||||
The first pre-pre-pre-alpha release is here!
|
||||
|
||||
Lots of stuff still unimplemented.
|
||||
|
||||
For now we have:
|
||||
|
||||
- `Just`
|
||||
- `Publishers.Decode`
|
||||
- `Publishers.DropWhile`
|
||||
- `Publishers.Empty`
|
||||
- `Publishers.Encode`
|
||||
- `Publishers.Fail`
|
||||
- `Publishers.Map`
|
||||
- `Publishers.Multicast`
|
||||
- `Publishers.Once`
|
||||
- `Publishers.Optional`
|
||||
- `Publishers.Print`
|
||||
- `Publishers.Sequence`
|
||||
- `Subscribers.Assign`
|
||||
- `Subscribers.Completion`
|
||||
- `Subscribers.Demand`
|
||||
- `Subscribers.Sink`
|
||||
- `AnyCancellable`
|
||||
- `AnyPublisher`
|
||||
- `AnySubject`
|
||||
- `AnySubscriber`
|
||||
- `Cancellable`
|
||||
- `CombineIdentifier`
|
||||
- `ConnectablePublisher`
|
||||
- `CurrentValueSubject`
|
||||
- `CustomCombineIdentifierConvertible`
|
||||
- `ImmediateScheduler`
|
||||
- `PassthroughSubject`
|
||||
- `Publisher`
|
||||
- `Result`
|
||||
- `Scheduler`
|
||||
- `Subject`
|
||||
- `Subscriber`
|
||||
- `Subscription`
|
||||
+3
-3
@@ -66,10 +66,10 @@ do {
|
||||
}
|
||||
}
|
||||
|
||||
SwiftLint.lint(inline: true,
|
||||
SwiftLint.lint(.all(directory: nil),
|
||||
inline: true,
|
||||
configFile: ".swiftlint.yml",
|
||||
strict: true,
|
||||
lintAllFiles: true)
|
||||
strict: true)
|
||||
|
||||
if danger.warnings.isEmpty, danger.fails.isEmpty {
|
||||
markdown("LGTM")
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = "OpenCombine"
|
||||
spec.version = "0.11.0"
|
||||
spec.version = "0.12.0"
|
||||
spec.summary = "Open source implementation of Apple's Combine framework for processing values over time."
|
||||
|
||||
spec.description = <<-DESC
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = "OpenCombineDispatch"
|
||||
spec.version = "0.11.0"
|
||||
spec.version = "0.12.0"
|
||||
spec.summary = "OpenCombine + Dispatch interoperability"
|
||||
|
||||
spec.description = <<-DESC
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = "OpenCombineFoundation"
|
||||
spec.version = "0.11.0"
|
||||
spec.version = "0.12.0"
|
||||
spec.summary = "OpenCombine + OpenCombineFoundation interoperability"
|
||||
|
||||
spec.description = <<-DESC
|
||||
|
||||
+73
-9
@@ -1,25 +1,89 @@
|
||||
// swift-tools-version:5.0
|
||||
// swift-tools-version:5.3
|
||||
|
||||
import PackageDescription
|
||||
|
||||
// This list should be updated whenever SwiftPM adds support for a new platform.
|
||||
// See: https://bugs.swift.org/browse/SR-13814
|
||||
let supportedPlatforms: [Platform] = [
|
||||
.macOS,
|
||||
.iOS,
|
||||
.watchOS,
|
||||
.tvOS,
|
||||
.linux,
|
||||
.android,
|
||||
// Disable Windows because of https://bugs.swift.org/browse/SR-13817
|
||||
// .windows,
|
||||
.wasi,
|
||||
]
|
||||
|
||||
let package = Package(
|
||||
name: "OpenCombine",
|
||||
products: [
|
||||
.library(name: "OpenCombine", targets: ["OpenCombine"]),
|
||||
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
|
||||
.library(name: "OpenCombineFoundation", targets: ["OpenCombineFoundation"]),
|
||||
.library(name: "OpenCombineShim", targets: ["OpenCombineShim"]),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "COpenCombineHelpers"),
|
||||
.target(name: "OpenCombine", dependencies: ["COpenCombineHelpers"]),
|
||||
.target(
|
||||
name: "OpenCombine",
|
||||
dependencies: [
|
||||
.target(name: "COpenCombineHelpers",
|
||||
condition: .when(platforms: supportedPlatforms.except([.wasi])))
|
||||
],
|
||||
exclude: [
|
||||
"Publishers/Publishers.Encode.swift.gyb",
|
||||
"Publishers/Publishers.MapKeyPath.swift.gyb",
|
||||
"Publishers/Publishers.Catch.swift.gyb"
|
||||
],
|
||||
swiftSettings: [.define("WASI", .when(platforms: [.wasi]))]
|
||||
),
|
||||
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
|
||||
.target(name: "OpenCombineFoundation", dependencies: ["OpenCombine",
|
||||
"COpenCombineHelpers"]),
|
||||
.testTarget(name: "OpenCombineTests",
|
||||
dependencies: ["OpenCombine",
|
||||
"OpenCombineDispatch",
|
||||
"OpenCombineFoundation"],
|
||||
swiftSettings: [.unsafeFlags(["-enable-testing"])])
|
||||
.target(
|
||||
name: "OpenCombineFoundation",
|
||||
dependencies: [
|
||||
"OpenCombine",
|
||||
.target(name: "COpenCombineHelpers",
|
||||
condition: .when(platforms: supportedPlatforms.except([.wasi])))
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "OpenCombineShim",
|
||||
dependencies: [
|
||||
"OpenCombine",
|
||||
.target(name: "OpenCombineDispatch",
|
||||
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
|
||||
.target(name: "OpenCombineFoundation",
|
||||
condition: .when(platforms: supportedPlatforms.except([.wasi])))
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "OpenCombineTests",
|
||||
dependencies: [
|
||||
"OpenCombine",
|
||||
.target(name: "OpenCombineDispatch",
|
||||
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
|
||||
.target(name: "OpenCombineFoundation",
|
||||
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
|
||||
],
|
||||
swiftSettings: [
|
||||
.unsafeFlags(["-enable-testing"]),
|
||||
.define("WASI", .when(platforms: [.wasi]))
|
||||
]
|
||||
)
|
||||
],
|
||||
cxxLanguageStandard: .cxx1z
|
||||
)
|
||||
|
||||
// MARK: Helpers
|
||||
|
||||
extension Array where Element == Platform {
|
||||
func except(_ exceptions: [Platform]) -> [Platform] {
|
||||
// See: https://bugs.swift.org/browse/SR-13813
|
||||
let exceptionsDescriptions = exceptions.map(String.init(describing:))
|
||||
return filter { platform in
|
||||
!exceptionsDescriptions.contains(String(describing: platform))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// swift-tools-version:5.0
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "OpenCombine",
|
||||
products: [
|
||||
.library(name: "OpenCombine", targets: ["OpenCombine"]),
|
||||
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
|
||||
.library(name: "OpenCombineFoundation", targets: ["OpenCombineFoundation"]),
|
||||
.library(name: "OpenCombineShim", targets: ["OpenCombineShim"]),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "COpenCombineHelpers"),
|
||||
.target(name: "OpenCombine", dependencies: ["COpenCombineHelpers"]),
|
||||
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
|
||||
.target(name: "OpenCombineFoundation", dependencies: ["OpenCombine",
|
||||
"COpenCombineHelpers"]),
|
||||
.target(
|
||||
name: "OpenCombineShim",
|
||||
dependencies: [
|
||||
"OpenCombine",
|
||||
"OpenCombineDispatch",
|
||||
"OpenCombineFoundation",
|
||||
]
|
||||
),
|
||||
.testTarget(name: "OpenCombineTests",
|
||||
dependencies: ["OpenCombine",
|
||||
"OpenCombineDispatch",
|
||||
"OpenCombineFoundation"],
|
||||
swiftSettings: [.unsafeFlags(["-enable-testing"])])
|
||||
],
|
||||
cxxLanguageStandard: .cxx1z
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
// swift-tools-version:5.1
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "OpenCombine",
|
||||
products: [
|
||||
.library(name: "OpenCombine", targets: ["OpenCombine"]),
|
||||
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
|
||||
.library(name: "OpenCombineFoundation", targets: ["OpenCombineFoundation"]),
|
||||
.library(name: "OpenCombineShim", targets: ["OpenCombineShim"]),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "COpenCombineHelpers"),
|
||||
.target(name: "OpenCombine", dependencies: ["COpenCombineHelpers"]),
|
||||
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
|
||||
.target(name: "OpenCombineFoundation", dependencies: ["OpenCombine",
|
||||
"COpenCombineHelpers"]),
|
||||
.target(
|
||||
name: "OpenCombineShim",
|
||||
dependencies: [
|
||||
"OpenCombine",
|
||||
"OpenCombineDispatch",
|
||||
"OpenCombineFoundation",
|
||||
]
|
||||
),
|
||||
.testTarget(name: "OpenCombineTests",
|
||||
dependencies: ["OpenCombine",
|
||||
"OpenCombineDispatch",
|
||||
"OpenCombineFoundation"],
|
||||
swiftSettings: [.unsafeFlags(["-enable-testing"])])
|
||||
],
|
||||
cxxLanguageStandard: .cxx1z
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
// swift-tools-version:5.2
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "OpenCombine",
|
||||
products: [
|
||||
.library(name: "OpenCombine", targets: ["OpenCombine"]),
|
||||
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
|
||||
.library(name: "OpenCombineFoundation", targets: ["OpenCombineFoundation"]),
|
||||
.library(name: "OpenCombineShim", targets: ["OpenCombineShim"]),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "COpenCombineHelpers"),
|
||||
.target(name: "OpenCombine", dependencies: ["COpenCombineHelpers"]),
|
||||
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
|
||||
.target(name: "OpenCombineFoundation", dependencies: ["OpenCombine",
|
||||
"COpenCombineHelpers"]),
|
||||
.target(
|
||||
name: "OpenCombineShim",
|
||||
dependencies: [
|
||||
"OpenCombine",
|
||||
"OpenCombineDispatch",
|
||||
"OpenCombineFoundation",
|
||||
]
|
||||
),
|
||||
.testTarget(name: "OpenCombineTests",
|
||||
dependencies: ["OpenCombine",
|
||||
"OpenCombineDispatch",
|
||||
"OpenCombineFoundation"],
|
||||
swiftSettings: [.unsafeFlags(["-enable-testing"])])
|
||||
],
|
||||
cxxLanguageStandard: .cxx1z
|
||||
)
|
||||
@@ -2,36 +2,46 @@
|
||||
[](https://circleci.com/gh/OpenCombine/OpenCombine)
|
||||
[](https://codecov.io/gh/OpenCombine/OpenCombine)
|
||||

|
||||

|
||||

|
||||

|
||||
[<img src="https://img.shields.io/badge/slack-OpenCombine-yellow.svg?logo=slack">](https://join.slack.com/t/opencombine/shared_invite/enQtNzE2MjE5NzkxODI0LTYxMjkzNDUxZWViZWI1Njc2YjBhODgxNjRjOTdkZTcxOGU2ZjJjZjYxMGI3NWZkN2RkNGFmZTUzNmU3MGE2ZWM)
|
||||
|
||||
Open-source implementation of Apple's [Combine](https://developer.apple.com/documentation/combine) framework for processing values over time.
|
||||
|
||||
The main goal of this project is to provide a compatible, reliable and efficient implementation which can be used on Apple's operating systems before macOS 10.15 and iOS 13, as well as Linux and Windows.
|
||||
The main goal of this project is to provide a compatible, reliable and efficient implementation which can be used on Apple's operating systems before macOS 10.15 and iOS 13, as well as Linux and WebAssembly.
|
||||
|
||||
### Installation
|
||||
`OpenCombine` contains three public targets: `OpenCombine`, `OpenCombineFoundation` and `OpenCombineDispatch` (the fourth one, `COpenCombineHelpers`, is considered private. Don't import it in your projects).
|
||||
|
||||
OpenCombine itself does not have any dependencies. Not even Foundation or Dispatch. If you want to use OpenCombine with Dispatch (for example for using `DispatchQueue` as `Scheduler` for operators like `debounce`, `receive(on:)` etc.), you will need to import both `OpenCombine` and `OpenCombineDispatch`. The same applies to Foundation: if you want to use, for instance, `NotificationCenter` or `URLSession` publishers, you'll need to also import `OpenCombineFoundation`
|
||||
OpenCombine itself does not have any dependencies. Not even Foundation or Dispatch. If you want to use OpenCombine with Dispatch (for example for using `DispatchQueue` as `Scheduler` for operators like `debounce`, `receive(on:)` etc.), you will need to import both `OpenCombine` and `OpenCombineDispatch`. The same applies to Foundation: if you want to use, for instance, `NotificationCenter` or `URLSession` publishers, you'll need to also import `OpenCombineFoundation`.
|
||||
|
||||
If you develop code for multiple platforms, you may find it more convenient to import the
|
||||
`OpenCombineShim` module instead. It conditionally re-exports Combine on Apple platforms (if
|
||||
available), and all OpenCombine modules on other platforms. You can import `OpenCombineShim` only
|
||||
when using SwiftPM. It is not currently available for CocoaPods.
|
||||
|
||||
##### Swift Package Manager
|
||||
###### Swift Package
|
||||
To add `OpenCombine` to your [SPM](https://swift.org/package-manager/) package, add the `OpenCombine` package to the list of package and target dependencies in your `Package.swift` file.
|
||||
To add `OpenCombine` to your [SwiftPM](https://swift.org/package-manager/) package, add the `OpenCombine` package to the list of package and target dependencies in your `Package.swift` file. `OpenCombineDispatch` and `OpenCombineFoundation` products are currently not supported on WebAssembly. If your project targets WebAssembly exclusively, you should omit them from the list of your dependencies. If it targets multiple platforms including WebAssembly, depend on them only on non-WebAssembly platforms with [conditional target dependencies](https://github.com/apple/swift-evolution/blob/main/proposals/0273-swiftpm-conditional-target-dependencies.md).
|
||||
|
||||
```swift
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.11.0")
|
||||
.package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.12.0")
|
||||
],
|
||||
targets: [
|
||||
.target(name: "MyAwesomePackage", dependencies: ["OpenCombine",
|
||||
"OpenCombineDispatch",
|
||||
"OpenCombineFoundation"])
|
||||
.target(
|
||||
name: "MyAwesomePackage",
|
||||
dependencies: [
|
||||
"OpenCombine",
|
||||
.product(name: "OpenCombineFoundation", package: "OpenCombine"),
|
||||
.product(name: "OpenCombineDispatch", package: "OpenCombine")
|
||||
]
|
||||
),
|
||||
]
|
||||
```
|
||||
|
||||
###### Xcode
|
||||
`OpenCombine` can also be added as a SPM dependency directly in your Xcode project *(requires Xcode 11 upwards)*.
|
||||
`OpenCombine` can also be added as a SwiftPM dependency directly in your Xcode project *(requires Xcode 11 upwards)*.
|
||||
|
||||
To do so, open Xcode, use **File** → **Swift Packages** → **Add Package Dependency…**, enter the [repository URL](https://github.com/OpenCombine/OpenCombine.git), choose the latest available version, and activate the checkboxes:
|
||||
|
||||
@@ -44,9 +54,9 @@ To do so, open Xcode, use **File** → **Swift Packages** → **Add Package Depe
|
||||
To add `OpenCombine` to a project using [CocoaPods](https://cocoapods.org/), add `OpenCombine` and `OpenCombineDispatch` to the list of target dependencies in your `Podfile`.
|
||||
|
||||
```ruby
|
||||
pod 'OpenCombine', '~> 0.11.0'
|
||||
pod 'OpenCombineDispatch', '~> 0.11.0'
|
||||
pod 'OpenCombineFoundation', '~> 0.11.0'
|
||||
pod 'OpenCombine', '~> 0.12.0'
|
||||
pod 'OpenCombineDispatch', '~> 0.12.0'
|
||||
pod 'OpenCombineFoundation', '~> 0.12.0'
|
||||
```
|
||||
|
||||
### Contributing
|
||||
|
||||
@@ -2,165 +2,6 @@
|
||||
// Please remove the corresponding piece from this file if you implement something,
|
||||
// and complement this file as features are added in Apple's Combine
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that receives and combines the latest elements from two publishers.
|
||||
public struct CombineLatest<A, B> : Publisher where A : Publisher, B : Publisher, A.Failure == B.Failure {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = (A.Output, B.Output)
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = A.Failure
|
||||
|
||||
public let a: A
|
||||
|
||||
public let b: B
|
||||
|
||||
public init(_ a: A, _ b: B)
|
||||
|
||||
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
|
||||
///
|
||||
/// - SeeAlso: `subscribe(_:)`
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<S>(subscriber: S) where S : Subscriber, B.Failure == S.Failure, S.Input == (A.Output, B.Output)
|
||||
}
|
||||
|
||||
/// A publisher that receives and combines the latest elements from three publishers.
|
||||
public struct CombineLatest3<A, B, C> : Publisher where A : Publisher, B : Publisher, C : Publisher, A.Failure == B.Failure, B.Failure == C.Failure {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = (A.Output, B.Output, C.Output)
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = A.Failure
|
||||
|
||||
public let a: A
|
||||
|
||||
public let b: B
|
||||
|
||||
public let c: C
|
||||
|
||||
public init(_ a: A, _ b: B, _ c: C)
|
||||
|
||||
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
|
||||
///
|
||||
/// - SeeAlso: `subscribe(_:)`
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<S>(subscriber: S) where S : Subscriber, C.Failure == S.Failure, S.Input == (A.Output, B.Output, C.Output)
|
||||
}
|
||||
|
||||
/// A publisher that receives and combines the latest elements from four publishers.
|
||||
public struct CombineLatest4<A, B, C, D> : Publisher where A : Publisher, B : Publisher, C : Publisher, D : Publisher, A.Failure == B.Failure, B.Failure == C.Failure, C.Failure == D.Failure {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = (A.Output, B.Output, C.Output, D.Output)
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = A.Failure
|
||||
|
||||
public let a: A
|
||||
|
||||
public let b: B
|
||||
|
||||
public let c: C
|
||||
|
||||
public let d: D
|
||||
|
||||
public init(_ a: A, _ b: B, _ c: C, _ d: D)
|
||||
|
||||
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
|
||||
///
|
||||
/// - SeeAlso: `subscribe(_:)`
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<S>(subscriber: S) where S : Subscriber, D.Failure == S.Failure, S.Input == (A.Output, B.Output, C.Output, D.Output)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Subscribes to an additional publisher and publishes a tuple upon receiving output from either publisher.
|
||||
///
|
||||
/// The combined publisher passes through any requests to *all* upstream publishers. However, it still obeys the demand-fulfilling rule of only sending the request amount downstream. If the demand isn’t `.unlimited`, it drops values from upstream publishers. It implements this by using a buffer size of 1 for each upstream, and holds the most recent value in each buffer.
|
||||
/// All upstream publishers need to finish for this publisher to finsh. If an upstream publisher never publishes a value, this publisher never finishes.
|
||||
/// If any of the combined publishers terminates with a failure, this publisher also fails.
|
||||
/// - Parameters:
|
||||
/// - other: Another publisher to combine with this one.
|
||||
/// - Returns: A publisher that receives and combines elements from this and another publisher.
|
||||
public func combineLatest<P>(_ other: P) -> Publishers.CombineLatest<Self, P> where P : Publisher, Self.Failure == P.Failure
|
||||
|
||||
/// Subscribes to an additional publisher and invokes a closure upon receiving output from either publisher.
|
||||
///
|
||||
/// The combined publisher passes through any requests to *all* upstream publishers. However, it still obeys the demand-fulfilling rule of only sending the request amount downstream. If the demand isn’t `.unlimited`, it drops values from upstream publishers. It implements this by using a buffer size of 1 for each upstream, and holds the most recent value in each buffer.
|
||||
/// All upstream publishers need to finish for this publisher to finsh. If an upstream publisher never publishes a value, this publisher never finishes.
|
||||
/// If any of the combined publishers terminates with a failure, this publisher also fails.
|
||||
/// - Parameters:
|
||||
/// - other: Another publisher to combine with this one.
|
||||
/// - transform: A closure that receives the most recent value from each publisher and returns a new value to publish.
|
||||
/// - Returns: A publisher that receives and combines elements from this and another publisher.
|
||||
public func combineLatest<P, T>(_ other: P, _ transform: @escaping (Self.Output, P.Output) -> T) -> Publishers.Map<Publishers.CombineLatest<Self, P>, T> where P : Publisher, Self.Failure == P.Failure
|
||||
|
||||
/// Subscribes to two additional publishers and publishes a tuple upon receiving output from any of the publishers.
|
||||
///
|
||||
/// The combined publisher passes through any requests to *all* upstream publishers. However, it still obeys the demand-fulfilling rule of only sending the request amount downstream. If the demand isn’t `.unlimited`, it drops values from upstream publishers. It implements this by using a buffer size of 1 for each upstream, and holds the most recent value in each buffer.
|
||||
/// All upstream publishers need to finish for this publisher to finish. If an upstream publisher never publishes a value, this publisher never finishes.
|
||||
/// If any of the combined publishers terminates with a failure, this publisher also fails.
|
||||
/// - Parameters:
|
||||
/// - publisher1: A second publisher to combine with this one.
|
||||
/// - publisher2: A third publisher to combine with this one.
|
||||
/// - Returns: A publisher that receives and combines elements from this publisher and two other publishers.
|
||||
public func combineLatest<P, Q>(_ publisher1: P, _ publisher2: Q) -> Publishers.CombineLatest3<Self, P, Q> where P : Publisher, Q : Publisher, Self.Failure == P.Failure, P.Failure == Q.Failure
|
||||
|
||||
/// Subscribes to two additional publishers and invokes a closure upon receiving output from any of the publishers.
|
||||
///
|
||||
/// The combined publisher passes through any requests to *all* upstream publishers. However, it still obeys the demand-fulfilling rule of only sending the request amount downstream. If the demand isn’t `.unlimited`, it drops values from upstream publishers. It implements this by using a buffer size of 1 for each upstream, and holds the most recent value in each buffer.
|
||||
/// All upstream publishers need to finish for this publisher to finish. If an upstream publisher never publishes a value, this publisher never finishes.
|
||||
/// If any of the combined publishers terminates with a failure, this publisher also fails.
|
||||
/// - Parameters:
|
||||
/// - publisher1: A second publisher to combine with this one.
|
||||
/// - publisher2: A third publisher to combine with this one.
|
||||
/// - transform: A closure that receives the most recent value from each publisher and returns a new value to publish.
|
||||
/// - Returns: A publisher that receives and combines elements from this publisher and two other publishers.
|
||||
public func combineLatest<P, Q, T>(_ publisher1: P, _ publisher2: Q, _ transform: @escaping (Self.Output, P.Output, Q.Output) -> T) -> Publishers.Map<Publishers.CombineLatest3<Self, P, Q>, T> where P : Publisher, Q : Publisher, Self.Failure == P.Failure, P.Failure == Q.Failure
|
||||
|
||||
/// Subscribes to three additional publishers and publishes a tuple upon receiving output from any of the publishers.
|
||||
///
|
||||
/// The combined publisher passes through any requests to *all* upstream publishers. However, it still obeys the demand-fulfilling rule of only sending the request amount downstream. If the demand isn’t `.unlimited`, it drops values from upstream publishers. It implements this by using a buffer size of 1 for each upstream, and holds the most recent value in each buffer.
|
||||
/// All upstream publishers need to finish for this publisher to finish. If an upstream publisher never publishes a value, this publisher never finishes.
|
||||
/// If any of the combined publishers terminates with a failure, this publisher also fails.
|
||||
/// - Parameters:
|
||||
/// - publisher1: A second publisher to combine with this one.
|
||||
/// - publisher2: A third publisher to combine with this one.
|
||||
/// - publisher3: A fourth publisher to combine with this one.
|
||||
/// - Returns: A publisher that receives and combines elements from this publisher and three other publishers.
|
||||
public func combineLatest<P, Q, R>(_ publisher1: P, _ publisher2: Q, _ publisher3: R) -> Publishers.CombineLatest4<Self, P, Q, R> where P : Publisher, Q : Publisher, R : Publisher, Self.Failure == P.Failure, P.Failure == Q.Failure, Q.Failure == R.Failure
|
||||
|
||||
/// Subscribes to three additional publishers and invokes a closure upon receiving output from any of the publishers.
|
||||
///
|
||||
/// The combined publisher passes through any requests to *all* upstream publishers. However, it still obeys the demand-fulfilling rule of only sending the request amount downstream. If the demand isn’t `.unlimited`, it drops values from upstream publishers. It implements this by using a buffer size of 1 for each upstream, and holds the most recent value in each buffer.
|
||||
/// All upstream publishers need to finish for this publisher to finish. If an upstream publisher never publishes a value, this publisher never finishes.
|
||||
/// If any of the combined publishers terminates with a failure, this publisher also fails.
|
||||
/// - Parameters:
|
||||
/// - publisher1: A second publisher to combine with this one.
|
||||
/// - publisher2: A third publisher to combine with this one.
|
||||
/// - publisher3: A fourth publisher to combine with this one.
|
||||
/// - transform: A closure that receives the most recent value from each publisher and returns a new value to publish.
|
||||
/// - Returns: A publisher that receives and combines elements from this publisher and three other publishers.
|
||||
public func combineLatest<P, Q, R, T>(_ publisher1: P, _ publisher2: Q, _ publisher3: R, _ transform: @escaping (Self.Output, P.Output, Q.Output, R.Output) -> T) -> Publishers.Map<Publishers.CombineLatest4<Self, P, Q, R>, T> where P : Publisher, Q : Publisher, R : Publisher, Self.Failure == P.Failure, P.Failure == Q.Failure, Q.Failure == R.Failure
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A strategy for collecting received elements.
|
||||
@@ -244,47 +85,6 @@ extension Publisher {
|
||||
public func collect<S>(_ strategy: Publishers.TimeGroupingStrategy<S>, options: S.SchedulerOptions? = nil) -> Publishers.CollectByTime<Self, S> where S : Scheduler
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
public struct PrefixUntilOutput<Upstream, Other> : Publisher where Upstream : Publisher, Other : Publisher {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// Another publisher, whose first output causes this publisher to finish.
|
||||
public let other: Other
|
||||
|
||||
public init(upstream: Upstream, other: Other)
|
||||
|
||||
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
|
||||
///
|
||||
/// - SeeAlso: `subscribe(_:)`
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<S>(subscriber: S) where S : Subscriber, Upstream.Failure == S.Failure, Upstream.Output == S.Input
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Republishes elements until another publisher emits an element.
|
||||
///
|
||||
/// After the second publisher publishes an element, the publisher returned by this method finishes.
|
||||
///
|
||||
/// - Parameter publisher: A second publisher.
|
||||
/// - Returns: A publisher that republishes elements until the second publisher publishes an element.
|
||||
public func prefix<P>(untilOutputFrom publisher: P) -> Publishers.PrefixUntilOutput<Self, P> where P : Publisher
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher created by applying the merge function to two upstream publishers.
|
||||
@@ -675,147 +475,6 @@ extension Publisher {
|
||||
public func merge(with other: Self) -> Publishers.MergeMany<Self>
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that attempts to recreate its subscription to a failed upstream publisher.
|
||||
public struct Retry<Upstream> : Publisher where Upstream : Publisher {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The maximum number of retry attempts to perform.
|
||||
///
|
||||
/// If `nil`, this publisher attempts to reconnect with the upstream publisher an unlimited number of times.
|
||||
public let retries: Int?
|
||||
|
||||
/// Creates a publisher that attempts to recreate its subscription to a failed upstream publisher.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - upstream: The publisher from which this publisher receives its elements.
|
||||
/// - retries: The maximum number of retry attempts to perform. If `nil`, this publisher attempts to reconnect with the upstream publisher an unlimited number of times.
|
||||
public init(upstream: Upstream, retries: Int?)
|
||||
|
||||
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
|
||||
///
|
||||
/// - SeeAlso: `subscribe(_:)`
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<S>(subscriber: S) where S : Subscriber, Upstream.Failure == S.Failure, Upstream.Output == S.Input
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Attempts to recreate a failed subscription with the upstream publisher using a specified number of attempts to establish the connection.
|
||||
///
|
||||
/// After exceeding the specified number of retries, the publisher passes the failure to the downstream receiver.
|
||||
/// - Parameter retries: The number of times to attempt to recreate the subscription.
|
||||
/// - Returns: A publisher that attempts to recreate its subscription to a failed upstream publisher.
|
||||
public func retry(_ retries: Int) -> Publishers.Retry<Self>
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Attempts to recreate a failed subscription with the upstream publisher using a specified number of attempts to establish the connection.
|
||||
///
|
||||
/// After exceeding the specified number of retries, the publisher passes the failure to the downstream receiver.
|
||||
/// - Parameter retries: The number of times to attempt to recreate the subscription.
|
||||
/// - Returns: A publisher that attempts to recreate its subscription to a failed upstream publisher.
|
||||
public func retry(_ retries: Int) -> Publishers.Retry<Self>
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that publishes either the most-recent or first element published by the upstream publisher in a specified time interval.
|
||||
public struct Throttle<Upstream, Context> : Publisher where Upstream : Publisher, Context : Scheduler {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The interval in which to find and emit the most recent element.
|
||||
public let interval: Context.SchedulerTimeType.Stride
|
||||
|
||||
/// The scheduler on which to publish elements.
|
||||
public let scheduler: Context
|
||||
|
||||
/// A Boolean value indicating whether to publish the most recent element.
|
||||
///
|
||||
/// If `false`, the publisher emits the first element received during the interval.
|
||||
public let latest: Bool
|
||||
|
||||
public init(upstream: Upstream, interval: Context.SchedulerTimeType.Stride, scheduler: Context, latest: Bool)
|
||||
|
||||
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
|
||||
///
|
||||
/// - SeeAlso: `subscribe(_:)`
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<S>(subscriber: S) where S : Subscriber, Upstream.Failure == S.Failure, Upstream.Output == S.Input
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Publishes either the most-recent or first element published by the upstream
|
||||
/// publisher in the specified time interval.
|
||||
///
|
||||
/// Use `throttle(for:scheduler:latest:`` to selectively republish elements from
|
||||
/// an upstream publisher during an interval you specify. Other elements received from
|
||||
/// the upstream in the throttling interval aren’t republished.
|
||||
///
|
||||
/// In the example below, a `Timer.TimerPublisher` produces elements on 3-second
|
||||
/// intervals; the `throttle(for:scheduler:latest:)` operator delivers the first
|
||||
/// event, then republishes only the latest event in the following ten second
|
||||
/// intervals:
|
||||
///
|
||||
/// cancellable = Timer.publish(every: 3.0, on: .main, in: .default)
|
||||
/// .autoconnect()
|
||||
/// .print("\(Date().description)")
|
||||
/// .throttle(for: 10.0, scheduler: RunLoop.main, latest: true)
|
||||
/// .sink(
|
||||
/// receiveCompletion: { print ("Completion: \($0).") },
|
||||
/// receiveValue: { print("Received Timestamp \($0).") }
|
||||
/// )
|
||||
///
|
||||
/// // Prints:
|
||||
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:26:57 +0000)
|
||||
/// // Received Timestamp 2020-03-19 18:26:57 +0000.
|
||||
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:27:00 +0000)
|
||||
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:27:03 +0000)
|
||||
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:27:06 +0000)
|
||||
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:27:09 +0000)
|
||||
/// // Received Timestamp 2020-03-19 18:27:09 +0000.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - interval: The interval at which to find and emit either the most recent or
|
||||
/// the first element, expressed in the time system of the scheduler.
|
||||
/// - scheduler: The scheduler on which to publish elements.
|
||||
/// - latest: A Boolean value that indicates whether to publish the most recent
|
||||
/// element. If `false`, the publisher emits the first element received during
|
||||
/// the interval.
|
||||
/// - Returns: A publisher that emits either the most-recent or first element received
|
||||
/// during the specified interval.
|
||||
public func throttle<S>(for interval: S.SchedulerTimeType.Stride, scheduler: S, latest: Bool) -> Publishers.Throttle<Self, S> where S : Scheduler
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher created by applying the zip function to two upstream publishers.
|
||||
@@ -979,43 +638,6 @@ extension Publisher {
|
||||
public func zip<P, Q, R, T>(_ publisher1: P, _ publisher2: Q, _ publisher3: R, _ transform: @escaping (Self.Output, P.Output, Q.Output, R.Output) -> T) -> Publishers.Map<Publishers.Zip4<Self, P, Q, R>, T> where P : Publisher, Q : Publisher, R : Publisher, Self.Failure == P.Failure, P.Failure == Q.Failure, Q.Failure == R.Failure
|
||||
}
|
||||
|
||||
extension Publishers.CombineLatest : Equatable where A : Equatable, B : Equatable {
|
||||
|
||||
/// Returns a Boolean value that indicates whether two publishers are equivalent.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - lhs: A combineLatest publisher to compare for equality.
|
||||
/// - rhs: Another combineLatest publisher to compare for equality.
|
||||
/// - Returns: `true` if the corresponding upstream publishers of each combineLatest publisher are equal, `false` otherwise.
|
||||
public static func == (lhs: Publishers.CombineLatest<A, B>, rhs: Publishers.CombineLatest<A, B>) -> Bool
|
||||
}
|
||||
|
||||
extension Publishers.CombineLatest3 : Equatable where A : Equatable, B : Equatable, C : Equatable {
|
||||
|
||||
/// Returns a Boolean value indicating whether two values are equal.
|
||||
///
|
||||
/// Equality is the inverse of inequality. For any values `a` and `b`,
|
||||
/// `a == b` implies that `a != b` is `false`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - lhs: A value to compare.
|
||||
/// - rhs: Another value to compare.
|
||||
public static func == (lhs: Publishers.CombineLatest3<A, B, C>, rhs: Publishers.CombineLatest3<A, B, C>) -> Bool
|
||||
}
|
||||
|
||||
extension Publishers.CombineLatest4 : Equatable where A : Equatable, B : Equatable, C : Equatable, D : Equatable {
|
||||
|
||||
/// Returns a Boolean value indicating whether two values are equal.
|
||||
///
|
||||
/// Equality is the inverse of inequality. For any values `a` and `b`,
|
||||
/// `a == b` implies that `a != b` is `false`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - lhs: A value to compare.
|
||||
/// - rhs: Another value to compare.
|
||||
public static func == (lhs: Publishers.CombineLatest4<A, B, C, D>, rhs: Publishers.CombineLatest4<A, B, C, D>) -> Bool
|
||||
}
|
||||
|
||||
extension Publishers.Merge : Equatable where A : Equatable, B : Equatable {
|
||||
|
||||
/// Returns a Boolean value that indicates whether two publishers are equivalent.
|
||||
@@ -1106,19 +728,6 @@ extension Publishers.MergeMany : Equatable where Upstream : Equatable {
|
||||
public static func == (lhs: Publishers.MergeMany<Upstream>, rhs: Publishers.MergeMany<Upstream>) -> Bool
|
||||
}
|
||||
|
||||
extension Publishers.Retry : Equatable where Upstream : Equatable {
|
||||
|
||||
/// Returns a Boolean value indicating whether two values are equal.
|
||||
///
|
||||
/// Equality is the inverse of inequality. For any values `a` and `b`,
|
||||
/// `a == b` implies that `a != b` is `false`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - lhs: A value to compare.
|
||||
/// - rhs: Another value to compare.
|
||||
public static func == (lhs: Publishers.Retry<Upstream>, rhs: Publishers.Retry<Upstream>) -> Bool
|
||||
}
|
||||
|
||||
extension Publishers.Zip : Equatable where A : Equatable, B : Equatable {
|
||||
|
||||
/// Returns a Boolean value that indicates whether two publishers are equivalent.
|
||||
|
||||
@@ -9,7 +9,7 @@ extension Publisher {
|
||||
|
||||
/// Wraps this publisher with a type eraser.
|
||||
///
|
||||
/// Use `eraseToAnyPublisher()` to expose an instance of `AnyPublishe`` to
|
||||
/// Use `eraseToAnyPublisher()` to expose an instance of `AnyPublisher`` to
|
||||
/// the downstream subscriber, rather than this publisher’s actual type.
|
||||
/// This form of _type erasure_ preserves abstraction across API boundaries, such as
|
||||
/// different modules.
|
||||
|
||||
@@ -9,6 +9,15 @@
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
#if WASI
|
||||
private var __identifier: UInt64 = 0
|
||||
|
||||
internal func __nextCombineIdentifier() -> UInt64 {
|
||||
defer { __identifier += 1 }
|
||||
return __identifier
|
||||
}
|
||||
#endif // WASI
|
||||
|
||||
/// A unique identifier for identifying publisher streams.
|
||||
///
|
||||
/// To conform to `CustomCombineIdentifierConvertible` in a
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
//
|
||||
// AbstractCombineLatest.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
internal class AbstractCombineLatest<Output, Failure, Downstream: Subscriber>
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure
|
||||
{
|
||||
private let downstream: Downstream
|
||||
|
||||
// TODO: The size of these arrays always stays the same.
|
||||
// Maybe we can leverage ManagedBuffer/ManagedBufferPointer here
|
||||
// to avoid additional allocations.
|
||||
private var buffers: [Any?] // 0x78
|
||||
private var subscriptions: [Subscription?] // 0x80
|
||||
|
||||
private var demand = Subscribers.Demand.none // 0x88
|
||||
|
||||
private var recursion = false // 0x90
|
||||
|
||||
private var finished = false // 0x98
|
||||
|
||||
private var errored = false // 0xA0
|
||||
|
||||
private var cancelled = false // 0xA8
|
||||
|
||||
private let upstreamCount: Int // 0xB0
|
||||
|
||||
private var finishCount = 0 // 0xB8
|
||||
|
||||
private let lock = UnfairLock.allocate() // 0xC0
|
||||
|
||||
private let downstreamLock = UnfairRecursiveLock.allocate() // 0xC8
|
||||
|
||||
internal init(downstream: Downstream, upstreamCount: Int) {
|
||||
self.downstream = downstream
|
||||
self.buffers = Array(repeating: nil, count: upstreamCount)
|
||||
self.subscriptions = Array(repeating: nil, count: upstreamCount)
|
||||
self.upstreamCount = upstreamCount
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
downstreamLock.deallocate()
|
||||
}
|
||||
|
||||
// TODO: There should be more type-safe (and faster) way.
|
||||
// E. g. what if we store `buffers` in subclasses?
|
||||
internal func convert(values: [Any?]) -> Output {
|
||||
abstractMethod()
|
||||
}
|
||||
|
||||
fileprivate final func receive(subscription: Subscription, index: Int) {
|
||||
lock.lock()
|
||||
guard !cancelled && subscriptions[index] == nil else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
subscriptions[index] = subscription
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
fileprivate final func receive(_ input: Any, index: Int) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
if cancelled || finished {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
buffers[index] = input
|
||||
guard !recursion && demand > 0 && buffers.allSatisfy({ $0 != nil }) else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
demand -= 1
|
||||
recursion = true
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
let newDemand = downstream.receive(convert(values: buffers))
|
||||
downstreamLock.unlock()
|
||||
lock.lock()
|
||||
recursion = false
|
||||
demand += newDemand
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
|
||||
fileprivate final func receive(completion: Subscribers.Completion<Failure>,
|
||||
index: Int) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
lock.lock()
|
||||
if finished {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
finishCount += 1
|
||||
subscriptions[index] = nil
|
||||
if finishCount == upstreamCount {
|
||||
finished = true
|
||||
buffers = Array(repeating: nil, count: upstreamCount)
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
case .failure:
|
||||
lock.lock()
|
||||
finished = true
|
||||
errored = true
|
||||
let subscriptions = self.subscriptions
|
||||
self.subscriptions = Array(repeating: nil, count: upstreamCount)
|
||||
buffers = Array(repeating: nil, count: upstreamCount)
|
||||
lock.unlock()
|
||||
for (i, subscription) in subscriptions.enumerated() where i != index {
|
||||
subscription?.cancel()
|
||||
}
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AbstractCombineLatest: Subscription {
|
||||
internal func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero() // TODO: Test this
|
||||
lock.lock()
|
||||
guard !cancelled && !finished else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
self.demand += demand
|
||||
lock.unlock()
|
||||
for subscription in subscriptions {
|
||||
subscription?.request(demand)
|
||||
}
|
||||
}
|
||||
|
||||
internal func cancel() {
|
||||
lock.lock()
|
||||
cancelled = true
|
||||
let subscriptions = self.subscriptions
|
||||
self.subscriptions = Array(repeating: nil, count: upstreamCount)
|
||||
buffers = Array(repeating: nil, count: upstreamCount)
|
||||
lock.unlock()
|
||||
for subscription in subscriptions {
|
||||
subscription?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AbstractCombineLatest: CustomStringConvertible {
|
||||
internal var description: String { return "CombineLatest" }
|
||||
}
|
||||
|
||||
extension AbstractCombineLatest: CustomReflectable {
|
||||
internal var customMirror: Mirror {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
let children: [Mirror.Child] = [
|
||||
("downstream", downstream),
|
||||
("upstreamSubscriptions", subscriptions),
|
||||
("demand", demand),
|
||||
("buffers", buffers)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
}
|
||||
|
||||
extension AbstractCombineLatest: CustomPlaygroundDisplayConvertible {
|
||||
internal final var playgroundDescription: Any { return description }
|
||||
}
|
||||
|
||||
extension AbstractCombineLatest {
|
||||
internal struct Side<Input>: Subscriber, CustomStringConvertible {
|
||||
private let index: Int
|
||||
private let combiner: AbstractCombineLatest
|
||||
|
||||
internal let combineIdentifier = CombineIdentifier()
|
||||
|
||||
internal init(index: Int, combiner: AbstractCombineLatest) {
|
||||
self.index = index
|
||||
self.combiner = combiner
|
||||
}
|
||||
|
||||
internal func receive(subscription: Subscription) {
|
||||
combiner.receive(subscription: subscription, index: index)
|
||||
}
|
||||
|
||||
internal func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return combiner.receive(input, index: index)
|
||||
}
|
||||
|
||||
internal func receive(completion: Subscribers.Completion<Failure>) {
|
||||
combiner.receive(completion: completion, index: index)
|
||||
}
|
||||
|
||||
internal var description: String { return "CombineLatest" }
|
||||
}
|
||||
}
|
||||
@@ -9,5 +9,22 @@
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
#if WASI
|
||||
internal struct __UnfairLock { // swiftlint:disable:this type_name
|
||||
internal static func allocate() -> UnfairLock { return .init() }
|
||||
internal func lock() {}
|
||||
internal func unlock() {}
|
||||
internal func assertOwner() {}
|
||||
internal func deallocate() {}
|
||||
}
|
||||
|
||||
internal struct __UnfairRecursiveLock { // swiftlint:disable:this type_name
|
||||
internal static func allocate() -> UnfairRecursiveLock { return .init() }
|
||||
internal func lock() {}
|
||||
internal func unlock() {}
|
||||
internal func deallocate() {}
|
||||
}
|
||||
#endif // WASI
|
||||
|
||||
internal typealias UnfairLock = __UnfairLock
|
||||
internal typealias UnfairRecursiveLock = __UnfairRecursiveLock
|
||||
|
||||
@@ -48,8 +48,6 @@ internal class ReduceProducer<Downstream: Subscriber,
|
||||
|
||||
private var upstreamCompleted = false
|
||||
|
||||
private var empty = true
|
||||
|
||||
internal init(downstream: Downstream, initial: Output?, reduce: Reducer) {
|
||||
self.downstream = downstream
|
||||
self.initial = initial
|
||||
@@ -100,7 +98,9 @@ internal class ReduceProducer<Downstream: Subscriber,
|
||||
return
|
||||
}
|
||||
upstreamCompleted = true
|
||||
self.completed = downstreamRequested || empty
|
||||
if downstreamRequested {
|
||||
self.completed = true
|
||||
}
|
||||
let completed = self.completed
|
||||
let result = self.result
|
||||
lock.unlock()
|
||||
@@ -157,7 +157,6 @@ extension ReduceProducer: Subscriber {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
empty = false
|
||||
lock.unlock()
|
||||
|
||||
// Combine doesn't hold the lock when calling `receive(newValue:)`.
|
||||
|
||||
@@ -42,26 +42,58 @@ public protocol ObservableObject: AnyObject {
|
||||
var objectWillChange: ObjectWillChangePublisher { get }
|
||||
}
|
||||
|
||||
extension ObservableObject where ObjectWillChangePublisher == ObservableObjectPublisher {
|
||||
// swiftlint:disable let_var_whitespace
|
||||
#if swift(>=5.1)
|
||||
/// A publisher that emits before the object has changed.
|
||||
@available(*, unavailable, message: """
|
||||
The default implementation of objectWillChange is not available yet. \
|
||||
It's being worked on in \
|
||||
https://github.com/broadwaylamb/OpenCombine/pull/97
|
||||
""")
|
||||
public var objectWillChange: ObservableObjectPublisher {
|
||||
fatalError("unimplemented")
|
||||
}
|
||||
#else
|
||||
public var objectWillChange: ObservableObjectPublisher {
|
||||
return ObservableObjectPublisher()
|
||||
}
|
||||
#endif
|
||||
// swiftlint:enable let_var_whitespace
|
||||
private protocol _ObservableObjectProperty {
|
||||
var objectWillChange: ObservableObjectPublisher? { get nonmutating set }
|
||||
}
|
||||
|
||||
#if swift(>=5.1)
|
||||
extension Published: _ObservableObjectProperty {}
|
||||
|
||||
extension ObservableObject where ObjectWillChangePublisher == ObservableObjectPublisher {
|
||||
|
||||
/// A publisher that emits before the object has changed.
|
||||
public var objectWillChange: ObservableObjectPublisher {
|
||||
var installedPublisher: ObservableObjectPublisher?
|
||||
var reflection: Mirror? = Mirror(reflecting: self)
|
||||
while let aClass = reflection {
|
||||
for (_, property) in aClass.children {
|
||||
guard let property = property as? _ObservableObjectProperty else {
|
||||
// Visit other fields until we meet a @Published field
|
||||
continue
|
||||
}
|
||||
|
||||
// Now we know that the field is @Published.
|
||||
if let alreadyInstalledPublisher = property.objectWillChange {
|
||||
installedPublisher = alreadyInstalledPublisher
|
||||
// Don't visit other fields, as all @Published fields
|
||||
// already have a publisher installed.
|
||||
break
|
||||
}
|
||||
|
||||
// Okay, this field doesn't have a publisher installed.
|
||||
// This means that other fields don't have it either
|
||||
// (because we install it only once and fields can't be added at runtime).
|
||||
var lazilyCreatedPublisher: ObjectWillChangePublisher {
|
||||
if let publisher = installedPublisher {
|
||||
return publisher
|
||||
}
|
||||
let publisher = ObservableObjectPublisher()
|
||||
installedPublisher = publisher
|
||||
return publisher
|
||||
}
|
||||
|
||||
property.objectWillChange = lazilyCreatedPublisher
|
||||
|
||||
// Continue visiting other fields.
|
||||
}
|
||||
reflection = aClass.superclassMirror
|
||||
}
|
||||
return installedPublisher ?? ObservableObjectPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/// A publisher that publishes changes from observable objects.
|
||||
public final class ObservableObjectPublisher: Publisher {
|
||||
|
||||
|
||||
@@ -107,8 +107,16 @@ public struct Published<Value> {
|
||||
case value(Value)
|
||||
case publisher(Publisher)
|
||||
}
|
||||
@propertyWrapper
|
||||
private final class Box {
|
||||
var wrappedValue: Storage
|
||||
|
||||
private var storage: Storage
|
||||
init(wrappedValue: Storage) {
|
||||
self.wrappedValue = wrappedValue
|
||||
}
|
||||
}
|
||||
|
||||
@Box private var storage: Storage
|
||||
|
||||
internal var objectWillChange: ObservableObjectPublisher? {
|
||||
get {
|
||||
@@ -119,8 +127,8 @@ public struct Published<Value> {
|
||||
return publisher.subject.objectWillChange
|
||||
}
|
||||
}
|
||||
set {
|
||||
projectedValue.subject.objectWillChange = newValue
|
||||
nonmutating set {
|
||||
getPublisher().subject.objectWillChange = newValue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +153,7 @@ public struct Published<Value> {
|
||||
///
|
||||
/// - Parameter initialValue: The publisher's initial value.
|
||||
public init(wrappedValue: Value) {
|
||||
storage = .value(wrappedValue)
|
||||
_storage = Box(wrappedValue: .value(wrappedValue))
|
||||
}
|
||||
|
||||
/// The property for which this instance exposes a publisher.
|
||||
@@ -153,14 +161,7 @@ public struct Published<Value> {
|
||||
/// The `projectedValue` is the property accessed with the `$` operator.
|
||||
public var projectedValue: Publisher {
|
||||
mutating get {
|
||||
switch storage {
|
||||
case .value(let value):
|
||||
let publisher = Publisher(value)
|
||||
storage = .publisher(publisher)
|
||||
return publisher
|
||||
case .publisher(let publisher):
|
||||
return publisher
|
||||
}
|
||||
return getPublisher()
|
||||
}
|
||||
set { // swiftlint:disable:this unused_setter_value
|
||||
switch storage {
|
||||
@@ -172,6 +173,17 @@ public struct Published<Value> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Note: This method can mutate `storage`
|
||||
internal func getPublisher() -> Publisher {
|
||||
switch storage {
|
||||
case .value(let value):
|
||||
let publisher = Publisher(value)
|
||||
storage = .publisher(publisher)
|
||||
return publisher
|
||||
case .publisher(let publisher):
|
||||
return publisher
|
||||
}
|
||||
}
|
||||
// swiftlint:disable let_var_whitespace
|
||||
@available(*, unavailable, message: """
|
||||
@Published is only available on properties of classes
|
||||
|
||||
@@ -0,0 +1,408 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ┃
|
||||
// ┃ Auto-generated from GYB template. DO NOT EDIT! ┃
|
||||
// ┃ ┃
|
||||
// ┃ ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
//
|
||||
// Publishers.CombineLatest.swift.gyb
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
// swiftlint:disable generic_type_name
|
||||
// swiftlint:disable large_tuple
|
||||
|
||||
// MARK: - CombineLatest methods on Publisher
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Subscribes to an additional publisher and publishes a tuple upon
|
||||
/// receiving output from either publisher.
|
||||
///
|
||||
/// The combined publisher passes through any requests to *all* upstream publishers.
|
||||
/// However, it still obeys the demand-fulfilling rule of only sending the request
|
||||
/// amount downstream. If the demand isn’t `.unlimited`, it drops values from upstream
|
||||
/// publishers. It implements this by using a buffer size of 1 for each upstream, and
|
||||
/// holds the most recent value in each buffer.
|
||||
/// All upstream publishers need to finish for this publisher to finsh. If an upstream
|
||||
/// publisher never publishes a value, this publisher never finishes.
|
||||
/// If any of the combined publishers terminates with a failure, this publisher also
|
||||
/// fails.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - other: Another publisher to combine with this one.
|
||||
/// - Returns: A publisher that receives and combines elements from this and another
|
||||
/// publisher.
|
||||
public func combineLatest<P: Publisher>(
|
||||
_ other: P
|
||||
) -> Publishers.CombineLatest<Self, P>
|
||||
where Failure == P.Failure
|
||||
{
|
||||
return .init(self, other)
|
||||
}
|
||||
|
||||
/// Subscribes to an additional publisher and invokes a closure
|
||||
/// upon receiving output from either publisher.
|
||||
///
|
||||
/// The combined publisher passes through any requests to *all* upstream publishers.
|
||||
/// However, it still obeys the demand-fulfilling rule of only sending the request
|
||||
/// amount downstream. If the demand isn’t `.unlimited`, it drops values from upstream
|
||||
/// publishers. It implements this by using a buffer size of 1 for each upstream, and
|
||||
/// holds the most recent value in each buffer.
|
||||
/// All upstream publishers need to finish for this publisher to finsh. If an upstream
|
||||
/// publisher never publishes a value, this publisher never finishes.
|
||||
/// If any of the combined publishers terminates with a failure, this publisher also
|
||||
/// fails.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - other: Another publisher to combine with this one.
|
||||
/// - transform: A closure that receives the most recent value from each publisher
|
||||
/// and returns a new value to publish.
|
||||
/// - Returns: A publisher that receives and combines elements from this and another
|
||||
/// publisher.
|
||||
public func combineLatest<P: Publisher, Result>(
|
||||
_ other: P,
|
||||
_ transform: @escaping (Output, P.Output) -> Result
|
||||
) -> Publishers.Map<Publishers.CombineLatest<Self, P>, Result>
|
||||
where Failure == P.Failure
|
||||
{
|
||||
return Publishers.CombineLatest(self, other).map {
|
||||
transform($0, $1)
|
||||
}
|
||||
}
|
||||
/// Subscribes to two additional publishers and publishes a tuple upon
|
||||
/// receiving output from either publisher.
|
||||
///
|
||||
/// The combined publisher passes through any requests to *all* upstream publishers.
|
||||
/// However, it still obeys the demand-fulfilling rule of only sending the request
|
||||
/// amount downstream. If the demand isn’t `.unlimited`, it drops values from upstream
|
||||
/// publishers. It implements this by using a buffer size of 1 for each upstream, and
|
||||
/// holds the most recent value in each buffer.
|
||||
/// All upstream publishers need to finish for this publisher to finsh. If an upstream
|
||||
/// publisher never publishes a value, this publisher never finishes.
|
||||
/// If any of the combined publishers terminates with a failure, this publisher also
|
||||
/// fails.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - publisher1: A second publisher to combine with this one.
|
||||
/// - publisher2: A third publisher to combine with this one.
|
||||
/// - Returns: A publisher that receives and combines elements from this and another
|
||||
/// publisher.
|
||||
public func combineLatest<P: Publisher, Q: Publisher>(
|
||||
_ publisher1: P,
|
||||
_ publisher2: Q
|
||||
) -> Publishers.CombineLatest3<Self, P, Q>
|
||||
where Failure == P.Failure,
|
||||
P.Failure == Q.Failure
|
||||
{
|
||||
return .init(self, publisher1, publisher2)
|
||||
}
|
||||
|
||||
/// Subscribes to two additional publishers and invokes a closure
|
||||
/// upon receiving output from either publisher.
|
||||
///
|
||||
/// The combined publisher passes through any requests to *all* upstream publishers.
|
||||
/// However, it still obeys the demand-fulfilling rule of only sending the request
|
||||
/// amount downstream. If the demand isn’t `.unlimited`, it drops values from upstream
|
||||
/// publishers. It implements this by using a buffer size of 1 for each upstream, and
|
||||
/// holds the most recent value in each buffer.
|
||||
/// All upstream publishers need to finish for this publisher to finsh. If an upstream
|
||||
/// publisher never publishes a value, this publisher never finishes.
|
||||
/// If any of the combined publishers terminates with a failure, this publisher also
|
||||
/// fails.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - publisher1: A second publisher to combine with this one.
|
||||
/// - publisher2: A third publisher to combine with this one.
|
||||
/// - transform: A closure that receives the most recent value from each publisher
|
||||
/// and returns a new value to publish.
|
||||
/// - Returns: A publisher that receives and combines elements from this and another
|
||||
/// publisher.
|
||||
public func combineLatest<P: Publisher, Q: Publisher, Result>(
|
||||
_ publisher1: P,
|
||||
_ publisher2: Q,
|
||||
_ transform: @escaping (Output, P.Output, Q.Output) -> Result
|
||||
) -> Publishers.Map<Publishers.CombineLatest3<Self, P, Q>, Result>
|
||||
where Failure == P.Failure,
|
||||
P.Failure == Q.Failure
|
||||
{
|
||||
return Publishers.CombineLatest3(self, publisher1, publisher2).map {
|
||||
transform($0, $1, $2)
|
||||
}
|
||||
}
|
||||
/// Subscribes to three additional publishers and publishes a tuple upon
|
||||
/// receiving output from either publisher.
|
||||
///
|
||||
/// The combined publisher passes through any requests to *all* upstream publishers.
|
||||
/// However, it still obeys the demand-fulfilling rule of only sending the request
|
||||
/// amount downstream. If the demand isn’t `.unlimited`, it drops values from upstream
|
||||
/// publishers. It implements this by using a buffer size of 1 for each upstream, and
|
||||
/// holds the most recent value in each buffer.
|
||||
/// All upstream publishers need to finish for this publisher to finsh. If an upstream
|
||||
/// publisher never publishes a value, this publisher never finishes.
|
||||
/// If any of the combined publishers terminates with a failure, this publisher also
|
||||
/// fails.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - publisher1: A second publisher to combine with this one.
|
||||
/// - publisher2: A third publisher to combine with this one.
|
||||
/// - publisher3: A fourth publisher to combine with this one.
|
||||
/// - Returns: A publisher that receives and combines elements from this and another
|
||||
/// publisher.
|
||||
public func combineLatest<P: Publisher, Q: Publisher, R: Publisher>(
|
||||
_ publisher1: P,
|
||||
_ publisher2: Q,
|
||||
_ publisher3: R
|
||||
) -> Publishers.CombineLatest4<Self, P, Q, R>
|
||||
where Failure == P.Failure,
|
||||
P.Failure == Q.Failure,
|
||||
Q.Failure == R.Failure
|
||||
{
|
||||
return .init(self, publisher1, publisher2, publisher3)
|
||||
}
|
||||
|
||||
/// Subscribes to three additional publishers and invokes a closure
|
||||
/// upon receiving output from either publisher.
|
||||
///
|
||||
/// The combined publisher passes through any requests to *all* upstream publishers.
|
||||
/// However, it still obeys the demand-fulfilling rule of only sending the request
|
||||
/// amount downstream. If the demand isn’t `.unlimited`, it drops values from upstream
|
||||
/// publishers. It implements this by using a buffer size of 1 for each upstream, and
|
||||
/// holds the most recent value in each buffer.
|
||||
/// All upstream publishers need to finish for this publisher to finsh. If an upstream
|
||||
/// publisher never publishes a value, this publisher never finishes.
|
||||
/// If any of the combined publishers terminates with a failure, this publisher also
|
||||
/// fails.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - publisher1: A second publisher to combine with this one.
|
||||
/// - publisher2: A third publisher to combine with this one.
|
||||
/// - publisher3: A fourth publisher to combine with this one.
|
||||
/// - transform: A closure that receives the most recent value from each publisher
|
||||
/// and returns a new value to publish.
|
||||
/// - Returns: A publisher that receives and combines elements from this and another
|
||||
/// publisher.
|
||||
public func combineLatest<P: Publisher, Q: Publisher, R: Publisher, Result>(
|
||||
_ publisher1: P,
|
||||
_ publisher2: Q,
|
||||
_ publisher3: R,
|
||||
_ transform: @escaping (Output, P.Output, Q.Output, R.Output) -> Result
|
||||
) -> Publishers.Map<Publishers.CombineLatest4<Self, P, Q, R>, Result>
|
||||
where Failure == P.Failure,
|
||||
P.Failure == Q.Failure,
|
||||
Q.Failure == R.Failure
|
||||
{
|
||||
return Publishers.CombineLatest4(self, publisher1, publisher2, publisher3).map {
|
||||
transform($0, $1, $2, $3)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CombineLatest publishers
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that receives and combines the latest elements from two
|
||||
/// publishers.
|
||||
public struct CombineLatest<A: Publisher, B: Publisher>
|
||||
: Publisher
|
||||
where A.Failure == B.Failure
|
||||
{
|
||||
public typealias Output = (A.Output, B.Output)
|
||||
|
||||
public typealias Failure = A.Failure
|
||||
|
||||
public let a: A
|
||||
|
||||
public let b: B
|
||||
|
||||
public init(
|
||||
_ a: A,
|
||||
_ b: B
|
||||
) {
|
||||
self.a = a
|
||||
self.b = b
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == Failure,
|
||||
Downstream.Input == Output
|
||||
{
|
||||
typealias Inner = CombineLatest2Inner<A.Output,
|
||||
B.Output,
|
||||
Failure,
|
||||
Downstream>
|
||||
let inner = Inner(downstream: subscriber, upstreamCount: 2)
|
||||
a.subscribe(Inner.Side(index: 0, combiner: inner))
|
||||
b.subscribe(Inner.Side(index: 1, combiner: inner))
|
||||
subscriber.receive(subscription: inner)
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that receives and combines the latest elements from three
|
||||
/// publishers.
|
||||
public struct CombineLatest3<A: Publisher, B: Publisher, C: Publisher>
|
||||
: Publisher
|
||||
where A.Failure == B.Failure,
|
||||
B.Failure == C.Failure
|
||||
{
|
||||
public typealias Output = (A.Output, B.Output, C.Output)
|
||||
|
||||
public typealias Failure = A.Failure
|
||||
|
||||
public let a: A
|
||||
|
||||
public let b: B
|
||||
|
||||
public let c: C
|
||||
|
||||
public init(
|
||||
_ a: A,
|
||||
_ b: B,
|
||||
_ c: C
|
||||
) {
|
||||
self.a = a
|
||||
self.b = b
|
||||
self.c = c
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == Failure,
|
||||
Downstream.Input == Output
|
||||
{
|
||||
typealias Inner = CombineLatest3Inner<A.Output,
|
||||
B.Output,
|
||||
C.Output,
|
||||
Failure,
|
||||
Downstream>
|
||||
let inner = Inner(downstream: subscriber, upstreamCount: 3)
|
||||
a.subscribe(Inner.Side(index: 0, combiner: inner))
|
||||
b.subscribe(Inner.Side(index: 1, combiner: inner))
|
||||
c.subscribe(Inner.Side(index: 2, combiner: inner))
|
||||
subscriber.receive(subscription: inner)
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that receives and combines the latest elements from four
|
||||
/// publishers.
|
||||
public struct CombineLatest4<A: Publisher, B: Publisher, C: Publisher, D: Publisher>
|
||||
: Publisher
|
||||
where A.Failure == B.Failure,
|
||||
B.Failure == C.Failure,
|
||||
C.Failure == D.Failure
|
||||
{
|
||||
public typealias Output = (A.Output, B.Output, C.Output, D.Output)
|
||||
|
||||
public typealias Failure = A.Failure
|
||||
|
||||
public let a: A
|
||||
|
||||
public let b: B
|
||||
|
||||
public let c: C
|
||||
|
||||
public let d: D
|
||||
|
||||
public init(
|
||||
_ a: A,
|
||||
_ b: B,
|
||||
_ c: C,
|
||||
_ d: D
|
||||
) {
|
||||
self.a = a
|
||||
self.b = b
|
||||
self.c = c
|
||||
self.d = d
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == Failure,
|
||||
Downstream.Input == Output
|
||||
{
|
||||
typealias Inner = CombineLatest4Inner<A.Output,
|
||||
B.Output,
|
||||
C.Output,
|
||||
D.Output,
|
||||
Failure,
|
||||
Downstream>
|
||||
let inner = Inner(downstream: subscriber, upstreamCount: 4)
|
||||
a.subscribe(Inner.Side(index: 0, combiner: inner))
|
||||
b.subscribe(Inner.Side(index: 1, combiner: inner))
|
||||
c.subscribe(Inner.Side(index: 2, combiner: inner))
|
||||
d.subscribe(Inner.Side(index: 3, combiner: inner))
|
||||
subscriber.receive(subscription: inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Equatable conformances
|
||||
|
||||
extension Publishers.CombineLatest: Equatable
|
||||
where
|
||||
A: Equatable,
|
||||
B: Equatable {}
|
||||
|
||||
extension Publishers.CombineLatest3: Equatable
|
||||
where
|
||||
A: Equatable,
|
||||
B: Equatable,
|
||||
C: Equatable {}
|
||||
|
||||
extension Publishers.CombineLatest4: Equatable
|
||||
where
|
||||
A: Equatable,
|
||||
B: Equatable,
|
||||
C: Equatable,
|
||||
D: Equatable {}
|
||||
|
||||
// MARK: - Inners
|
||||
|
||||
private final class CombineLatest2Inner<Input0,
|
||||
Input1,
|
||||
Failure,
|
||||
Downstream: Subscriber>
|
||||
: AbstractCombineLatest<(Input0, Input1), Failure, Downstream>
|
||||
where Downstream.Input == (Input0, Input1),
|
||||
Downstream.Failure == Failure
|
||||
{
|
||||
override func convert(values: [Any?]) -> (Input0, Input1) {
|
||||
return (values[0] as! Input0,
|
||||
values[1] as! Input1)
|
||||
}
|
||||
}
|
||||
|
||||
private final class CombineLatest3Inner<Input0,
|
||||
Input1,
|
||||
Input2,
|
||||
Failure,
|
||||
Downstream: Subscriber>
|
||||
: AbstractCombineLatest<(Input0, Input1, Input2), Failure, Downstream>
|
||||
where Downstream.Input == (Input0, Input1, Input2),
|
||||
Downstream.Failure == Failure
|
||||
{
|
||||
override func convert(values: [Any?]) -> (Input0, Input1, Input2) {
|
||||
return (values[0] as! Input0,
|
||||
values[1] as! Input1,
|
||||
values[2] as! Input2)
|
||||
}
|
||||
}
|
||||
|
||||
private final class CombineLatest4Inner<Input0,
|
||||
Input1,
|
||||
Input2,
|
||||
Input3,
|
||||
Failure,
|
||||
Downstream: Subscriber>
|
||||
: AbstractCombineLatest<(Input0, Input1, Input2, Input3), Failure, Downstream>
|
||||
where Downstream.Input == (Input0, Input1, Input2, Input3),
|
||||
Downstream.Failure == Failure
|
||||
{
|
||||
override func convert(values: [Any?]) -> (Input0, Input1, Input2, Input3) {
|
||||
return (values[0] as! Input0,
|
||||
values[1] as! Input1,
|
||||
values[2] as! Input2,
|
||||
values[3] as! Input3)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Sergej Jaskiewicz on 03.12.2019.
|
||||
//
|
||||
|
||||
#if !WASI
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
@@ -225,3 +227,5 @@ extension Publishers.Breakpoint {
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
#endif // !WASI
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
${template_header}
|
||||
//
|
||||
// Publishers.CombineLatest.swift.gyb
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
%{
|
||||
from gyb_opencombine_support import (
|
||||
suffix_variadic,
|
||||
list_with_suffix_variadic,
|
||||
indent
|
||||
)
|
||||
|
||||
import string
|
||||
|
||||
instantiations = [(2, 'two', 'A second'),
|
||||
(3, 'three', 'A third'),
|
||||
(4, 'four', 'A fourth')]
|
||||
|
||||
def make_publisher_name(arity):
|
||||
return suffix_variadic('CombineLatest', arity, arity - 1)
|
||||
|
||||
def make_upstream_types(arity, start=0):
|
||||
return [str(c) for c in string.ascii_uppercase[start:(start + arity)]]
|
||||
|
||||
def make_upstream_generic_constraints(upstream_types, first_is_self=False):
|
||||
|
||||
format_string = '{0}Failure == {1}.Failure'
|
||||
|
||||
def format(i):
|
||||
return format_string.format(upstream_types[i] + '.',
|
||||
upstream_types[i + 1])
|
||||
|
||||
result = [format(i) for i in range(len(upstream_types) - 1)]
|
||||
|
||||
if first_is_self:
|
||||
result.insert(0, format_string.format('', upstream_types[0]))
|
||||
|
||||
return result
|
||||
|
||||
def declare_combine_latest_method(arity, transform):
|
||||
arg_count = arity - 1
|
||||
declaration_format = """\
|
||||
public func combineLatest<{}>(
|
||||
{}
|
||||
) -> {}
|
||||
where {}\
|
||||
"""
|
||||
upstream_types = make_upstream_types(arg_count, 15)
|
||||
method_generic_params = \
|
||||
[upstream_type + ': Publisher' for upstream_type in upstream_types]
|
||||
if transform:
|
||||
method_generic_params.append('Result')
|
||||
cs_method_generic_params = ', '.join(method_generic_params)
|
||||
method_args = ['_ other: P'] if arg_count == 1 \
|
||||
else ['_ publisher{}: {}'.format(i + 1, upstream_types[i]) \
|
||||
for i in range(arg_count)]
|
||||
if transform:
|
||||
output_types = ['Output'] + ['{}.Output'.format(upstream_type) \
|
||||
for upstream_type in upstream_types]
|
||||
cs_output_types = ', '.join(output_types)
|
||||
method_args \
|
||||
.append('_ transform: @escaping ({}) -> Result'.format(cs_output_types))
|
||||
cs_method_args = ',\n '.join(method_args)
|
||||
|
||||
publisher_generic_params = ['Self'] + upstream_types
|
||||
|
||||
cs_publisher_generic_params = ', '.join(publisher_generic_params)
|
||||
|
||||
publisher_name = 'Publishers.{}<{}>'.format(make_publisher_name(arity),
|
||||
cs_publisher_generic_params)
|
||||
|
||||
if transform:
|
||||
publisher_name = 'Publishers.Map<{}, Result>'.format(publisher_name)
|
||||
|
||||
generic_constraints = make_upstream_generic_constraints(upstream_types,
|
||||
first_is_self=True)
|
||||
|
||||
cs_generic_constraints = ',\n '.join(generic_constraints)
|
||||
|
||||
declaration = declaration_format.format(cs_method_generic_params,
|
||||
cs_method_args,
|
||||
publisher_name,
|
||||
cs_generic_constraints)
|
||||
|
||||
return indent(declaration, 4)
|
||||
}%
|
||||
|
||||
// swiftlint:disable generic_type_name
|
||||
// swiftlint:disable large_tuple
|
||||
|
||||
// MARK: - CombineLatest methods on Publisher
|
||||
|
||||
extension Publisher {
|
||||
|
||||
% for arity, _, _ in instantiations:
|
||||
%
|
||||
% argument_names = ['other'] \
|
||||
% if arity == 2 else ['publisher{}'.format(i) for i in range(1, arity)]
|
||||
% doc_cardinal = 'an additional publisher' if arity == 2 \
|
||||
% else '{} additional publishers'.format(instantiations[arity - 3][1])
|
||||
/// Subscribes to ${doc_cardinal} and publishes a tuple upon
|
||||
/// receiving output from either publisher.
|
||||
///
|
||||
/// The combined publisher passes through any requests to *all* upstream publishers.
|
||||
/// However, it still obeys the demand-fulfilling rule of only sending the request
|
||||
/// amount downstream. If the demand isn’t `.unlimited`, it drops values from upstream
|
||||
/// publishers. It implements this by using a buffer size of 1 for each upstream, and
|
||||
/// holds the most recent value in each buffer.
|
||||
/// All upstream publishers need to finish for this publisher to finsh. If an upstream
|
||||
/// publisher never publishes a value, this publisher never finishes.
|
||||
/// If any of the combined publishers terminates with a failure, this publisher also
|
||||
/// fails.
|
||||
///
|
||||
/// - Parameters:
|
||||
% for i in range(arity - 1):
|
||||
% param_doc = 'Another' if arity == 2 else instantiations[i][2]
|
||||
/// - ${argument_names[i]}: ${param_doc} publisher to combine with this one.
|
||||
% end
|
||||
/// - Returns: A publisher that receives and combines elements from this and another
|
||||
/// publisher.
|
||||
${declare_combine_latest_method(arity, transform=False)}
|
||||
{
|
||||
return .init(self, ${', '.join(argument_names)})
|
||||
}
|
||||
|
||||
/// Subscribes to ${doc_cardinal} and invokes a closure
|
||||
/// upon receiving output from either publisher.
|
||||
///
|
||||
/// The combined publisher passes through any requests to *all* upstream publishers.
|
||||
/// However, it still obeys the demand-fulfilling rule of only sending the request
|
||||
/// amount downstream. If the demand isn’t `.unlimited`, it drops values from upstream
|
||||
/// publishers. It implements this by using a buffer size of 1 for each upstream, and
|
||||
/// holds the most recent value in each buffer.
|
||||
/// All upstream publishers need to finish for this publisher to finsh. If an upstream
|
||||
/// publisher never publishes a value, this publisher never finishes.
|
||||
/// If any of the combined publishers terminates with a failure, this publisher also
|
||||
/// fails.
|
||||
///
|
||||
/// - Parameters:
|
||||
% for i in range(arity - 1):
|
||||
% param_doc = 'Another' if arity == 2 else instantiations[i][2]
|
||||
/// - ${argument_names[i]}: ${param_doc} publisher to combine with this one.
|
||||
% end
|
||||
/// - transform: A closure that receives the most recent value from each publisher
|
||||
/// and returns a new value to publish.
|
||||
/// - Returns: A publisher that receives and combines elements from this and another
|
||||
/// publisher.
|
||||
${declare_combine_latest_method(arity, transform=True)}
|
||||
{
|
||||
% publisher_name = make_publisher_name(arity)
|
||||
return Publishers.${publisher_name}(self, ${', '.join(argument_names)}).map {
|
||||
transform(${', '.join(['${}'.format(i) for i in range(arity)])})
|
||||
}
|
||||
}
|
||||
% end
|
||||
}
|
||||
|
||||
// MARK: - CombineLatest publishers
|
||||
|
||||
extension Publishers {
|
||||
% for arity, cardinal, _ in instantiations:
|
||||
% publisher_name = make_publisher_name(arity)
|
||||
% upstream_types = make_upstream_types(arity)
|
||||
%
|
||||
% upstream_generic_params = \
|
||||
% [upstream_type + ': Publisher' for upstream_type in upstream_types]
|
||||
%
|
||||
% cs_upstream_generic_params = ', '.join(upstream_generic_params)
|
||||
%
|
||||
% output_types = [upstream_type + '.Output' for upstream_type in upstream_types]
|
||||
%
|
||||
% cs_output_types = ', '.join(output_types)
|
||||
%
|
||||
% upstream_generic_constraints = \
|
||||
% make_upstream_generic_constraints(upstream_types)
|
||||
%
|
||||
% cs_upstream_generic_constraints = \
|
||||
% ',\n '.join(upstream_generic_constraints)
|
||||
%
|
||||
% init_args = ['_ {}: {}'.format(upstream_type.lower(), upstream_type) \
|
||||
% for upstream_type in upstream_types]
|
||||
% cs_init_args = ',\n '.join(init_args)
|
||||
%
|
||||
% self_fields = [upstream_type.lower() for upstream_type in upstream_types]
|
||||
|
||||
/// A publisher that receives and combines the latest elements from ${cardinal}
|
||||
/// publishers.
|
||||
public struct ${publisher_name}<${cs_upstream_generic_params}>
|
||||
: Publisher
|
||||
where ${cs_upstream_generic_constraints}
|
||||
{
|
||||
public typealias Output = (${cs_output_types})
|
||||
|
||||
public typealias Failure = ${upstream_types[0]}.Failure
|
||||
% for upstream_type in upstream_types:
|
||||
|
||||
public let ${upstream_type.lower()}: ${upstream_type}
|
||||
% end
|
||||
|
||||
public init(
|
||||
${cs_init_args}
|
||||
) {
|
||||
% for self_field in self_fields:
|
||||
self.${self_field} = ${self_field}
|
||||
% end
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == Failure,
|
||||
Downstream.Input == Output
|
||||
{
|
||||
% cs_indented_output_types = (',\n' + (50 * ' ')).join(output_types)
|
||||
typealias Inner = CombineLatest${arity}Inner<${cs_indented_output_types},
|
||||
Failure,
|
||||
Downstream>
|
||||
let inner = Inner(downstream: subscriber, upstreamCount: ${arity})
|
||||
% for i in range(arity):
|
||||
${self_fields[i]}.subscribe(Inner.Side(index: ${i}, combiner: inner))
|
||||
% end
|
||||
subscriber.receive(subscription: inner)
|
||||
}
|
||||
}
|
||||
% end
|
||||
}
|
||||
|
||||
// MARK: - Equatable conformances
|
||||
% for arity, _, _ in instantiations:
|
||||
%
|
||||
% publisher_name = make_publisher_name(arity)
|
||||
%
|
||||
% upstream_types = make_upstream_types(arity)
|
||||
%
|
||||
% constraints = [upstream_type + ': Equatable' for upstream_type in upstream_types]
|
||||
% cs_constraints = ',\n'.join(constraints)
|
||||
% cs_constraints = indent(cs_constraints, 8)
|
||||
%
|
||||
|
||||
extension Publishers.${publisher_name}: Equatable
|
||||
where
|
||||
${cs_constraints} {}
|
||||
% end
|
||||
|
||||
// MARK: - Inners
|
||||
% for arity, _, _ in instantiations:
|
||||
%
|
||||
% publisher_name = make_publisher_name(arity)
|
||||
%
|
||||
% upstream_types = make_upstream_types(arity)
|
||||
%
|
||||
% input_types = ['Input{}'.format(i) for i in range(arity)]
|
||||
%
|
||||
% converters = ['values[{}] as! {}'.format(i, input_types[i]) for i in range(arity)]
|
||||
% output_type = '({})'.format(', '.join(input_types))
|
||||
|
||||
private final class CombineLatest${arity}Inner<${(',\n' + (40 * ' ')).join(input_types)},
|
||||
Failure,
|
||||
Downstream: Subscriber>
|
||||
: AbstractCombineLatest<${output_type}, Failure, Downstream>
|
||||
where Downstream.Input == ${output_type},
|
||||
Downstream.Failure == Failure
|
||||
{
|
||||
override func convert(values: [Any?]) -> (${', '.join(input_types)}) {
|
||||
return (${',\n '.join(converters)})
|
||||
}
|
||||
}
|
||||
% end
|
||||
@@ -157,17 +157,17 @@ extension Publishers {
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Child.Output == Downstream.Input, Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
let inner = Inner(downstream: subscriber,
|
||||
let outer = Outer(downstream: subscriber,
|
||||
maxPublishers: maxPublishers,
|
||||
map: transform)
|
||||
subscriber.receive(subscription: inner)
|
||||
upstream.subscribe(inner)
|
||||
subscriber.receive(subscription: outer)
|
||||
upstream.subscribe(outer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.FlatMap {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
private final class Outer<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
@@ -243,7 +243,7 @@ extension Publishers.FlatMap {
|
||||
subscription.request(maxPublishers)
|
||||
}
|
||||
|
||||
fileprivate func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
fileprivate func receive(_ input: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
let cancelledOrCompleted = self.cancelledOrCompleted
|
||||
lock.unlock()
|
||||
@@ -260,9 +260,9 @@ extension Publishers.FlatMap {
|
||||
return .none
|
||||
}
|
||||
|
||||
fileprivate func receive(completion: Subscribers.Completion<Child.Failure>) {
|
||||
outerSubscription = nil
|
||||
fileprivate func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
outerSubscription = nil
|
||||
outerFinished = true
|
||||
switch completion {
|
||||
case .finished:
|
||||
@@ -272,6 +272,8 @@ extension Publishers.FlatMap {
|
||||
let wasAlreadyCancelledOrCompleted = cancelledOrCompleted
|
||||
cancelledOrCompleted = true
|
||||
for (_, subscription) in subscriptions {
|
||||
// Cancelling subscriptions with the lock acquired. Not good,
|
||||
// but that's what Combine does. This code path is tested.
|
||||
subscription.cancel()
|
||||
}
|
||||
subscriptions = [:]
|
||||
@@ -354,16 +356,21 @@ extension Publishers.FlatMap {
|
||||
|
||||
fileprivate func cancel() {
|
||||
lock.lock()
|
||||
if cancelledOrCompleted {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
cancelledOrCompleted = true
|
||||
let subscriptions = self.subscriptions
|
||||
self.subscriptions = [:]
|
||||
let outerSubscription = self.outerSubscription
|
||||
self.outerSubscription = nil
|
||||
lock.unlock()
|
||||
for (_, subscription) in subscriptions {
|
||||
subscription.cancel()
|
||||
}
|
||||
// Combine doesn't acquire the lock here. Weird.
|
||||
// Combine doesn't acquire outerLock here. Weird.
|
||||
outerSubscription?.cancel()
|
||||
outerSubscription = nil
|
||||
}
|
||||
|
||||
// MARK: - Reflection
|
||||
@@ -471,9 +478,9 @@ extension Publishers.FlatMap {
|
||||
private func releaseLockThenSendCompletionDownstreamIfNeeded(
|
||||
outerFinished: Bool
|
||||
) -> Bool {
|
||||
#if DEBUG
|
||||
#if DEBUG
|
||||
lock.assertOwner() // Sanity check
|
||||
#endif
|
||||
#endif
|
||||
if !cancelledOrCompleted && outerFinished && buffer.isEmpty &&
|
||||
subscriptions.count + pendingSubscriptions == 0 {
|
||||
cancelledOrCompleted = true
|
||||
@@ -495,10 +502,10 @@ extension Publishers.FlatMap {
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible {
|
||||
private let index: SubscriptionIndex
|
||||
private let inner: Inner
|
||||
private let inner: Outer
|
||||
fileprivate let combineIdentifier = CombineIdentifier()
|
||||
|
||||
fileprivate init(index: SubscriptionIndex, inner: Inner) {
|
||||
fileprivate init(index: SubscriptionIndex, inner: Outer) {
|
||||
self.index = index
|
||||
self.inner = inner
|
||||
}
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
//
|
||||
// Publishers.PrefixUntilOutput.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 08.11.2020.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Republishes elements until another publisher emits an element.
|
||||
///
|
||||
/// After the second publisher publishes an element, the publisher returned by this
|
||||
/// method finishes.
|
||||
///
|
||||
/// - Parameter publisher: A second publisher.
|
||||
/// - Returns: A publisher that republishes elements until the second publisher
|
||||
/// publishes an element.
|
||||
public func prefix<Other: Publisher>(
|
||||
untilOutputFrom publisher: Other
|
||||
) -> Publishers.PrefixUntilOutput<Self, Other> {
|
||||
return .init(upstream: self, other: publisher)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
public struct PrefixUntilOutput<Upstream: Publisher, Other: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// Another publisher, whose first output causes this publisher to finish.
|
||||
public let other: Other
|
||||
|
||||
public init(upstream: Upstream, other: Other) {
|
||||
self.upstream = upstream
|
||||
self.other = other
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == Failure, Downstream.Input == Output
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, trigger: other))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.PrefixUntilOutput {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private struct Termination: Subscriber {
|
||||
|
||||
let inner: Inner
|
||||
|
||||
var combineIdentifier: CombineIdentifier {
|
||||
return inner.combineIdentifier
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
inner.terminationReceive(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Other.Output) -> Subscribers.Demand {
|
||||
return inner.terminationReceive(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Other.Failure>) {
|
||||
inner.terminationReceive(completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
private var termination: Termination?
|
||||
private var prefixState = SubscriptionStatus.awaitingSubscription
|
||||
private var terminationState = SubscriptionStatus.awaitingSubscription
|
||||
private var triggered = false
|
||||
private let lock = UnfairLock.allocate()
|
||||
private let downstream: Downstream
|
||||
|
||||
init(downstream: Downstream, trigger: Other) {
|
||||
self.downstream = downstream
|
||||
let termination = Termination(inner: self)
|
||||
self.termination = termination
|
||||
trigger.subscribe(termination)
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = prefixState else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
prefixState = triggered ? .terminal : .subscribed(subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case .subscribed = prefixState else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
lock.unlock()
|
||||
return downstream.receive(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
let prefixState = self.prefixState
|
||||
let terminationSubscription = terminationState.subscription
|
||||
self.prefixState = .terminal
|
||||
terminationState = .terminal
|
||||
termination = nil
|
||||
lock.unlock()
|
||||
terminationSubscription?.cancel()
|
||||
if case .subscribed = prefixState {
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = prefixState else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
let prefixSubscription = prefixState.subscription
|
||||
let terminationSubscription = terminationState.subscription
|
||||
prefixState = .terminal
|
||||
terminationState = .terminal
|
||||
lock.unlock()
|
||||
prefixSubscription?.cancel()
|
||||
terminationSubscription?.cancel()
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func terminationReceive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = terminationState else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
terminationState = .subscribed(subscription)
|
||||
lock.unlock()
|
||||
subscription.request(.max(1))
|
||||
}
|
||||
|
||||
private func terminationReceive(_ input: Other.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case .subscribed = terminationState else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
let prefixSubscription = prefixState.subscription
|
||||
prefixState = .terminal
|
||||
terminationState = .terminal
|
||||
termination = nil
|
||||
triggered = true
|
||||
lock.unlock()
|
||||
prefixSubscription?.cancel()
|
||||
downstream.receive(completion: .finished)
|
||||
return .none
|
||||
}
|
||||
|
||||
private func terminationReceive(
|
||||
completion: Subscribers.Completion<Other.Failure>
|
||||
) {
|
||||
lock.lock()
|
||||
terminationState = .terminal
|
||||
termination = nil
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
//
|
||||
// Publishers.Retry.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 28.06.2020.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Attempts to recreate a failed subscription with the upstream publisher up to
|
||||
/// the number of times you specify.
|
||||
///
|
||||
/// Use `retry(_:)` to try connecting to an upstream publisher after a failed
|
||||
/// connection attempt.
|
||||
///
|
||||
/// In the example below, a `URLSession.DataTaskPublisher` attempts to connect to
|
||||
/// a remote URL. If the connection attempt succeeds, it publishes the remote
|
||||
/// service’s HTML to the downstream publisher and completes normally. Otherwise,
|
||||
/// the retry operator attempts to reestablish the connection. If after three attempts
|
||||
/// the publisher still can’t connect to the remote URL, the `catch(_:)` operator
|
||||
/// replaces the error with a new publisher that publishes a “connection timed out”
|
||||
/// HTML page. After the downstream subscriber receives the timed out message,
|
||||
/// the stream completes normally.
|
||||
///
|
||||
/// struct WebSiteData: Codable {
|
||||
/// var rawHTML: String
|
||||
/// }
|
||||
///
|
||||
/// let myURL = URL(string: "https://www.example.com")
|
||||
///
|
||||
/// cancellable = URLSession.shared.dataTaskPublisher(for: myURL!)
|
||||
/// .retry(3)
|
||||
/// .map { page -> WebSiteData in
|
||||
/// WebSiteData(rawHTML: String(decoding: page.data, as: UTF8.self))
|
||||
/// }
|
||||
/// .catch { error in
|
||||
/// Just(
|
||||
/// WebSiteData(
|
||||
/// rawHTML: "<HTML>Unable to load page - timed out.</HTML>"
|
||||
/// )
|
||||
/// )
|
||||
/// }
|
||||
/// .sink(receiveCompletion: { print ("completion: \($0)") },
|
||||
/// receiveValue: { print ("value: \($0)") })
|
||||
///
|
||||
/// // Prints: The HTML content from the remote URL upon a successful connection,
|
||||
/// // or returns "<HTML>Unable to load page - timed out.</HTML>" if
|
||||
/// // the number of retries exceeds the specified value.
|
||||
///
|
||||
/// After exceeding the specified number of retries, the publisher passes the failure
|
||||
/// to the downstream receiver.
|
||||
/// - Parameter retries: The number of times to attempt to recreate the subscription.
|
||||
/// - Returns: A publisher that attempts to recreate its subscription to a failed
|
||||
/// upstream publisher.
|
||||
public func retry(_ retries: Int) -> Publishers.Retry<Self> {
|
||||
return .init(upstream: self, retries: retries)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that attempts to recreate its subscription to a failed upstream
|
||||
/// publisher.
|
||||
public struct Retry<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The maximum number of retry attempts to perform.
|
||||
///
|
||||
/// If `nil`, this publisher attempts to reconnect with the upstream publisher
|
||||
/// an unlimited number of times.
|
||||
public let retries: Int?
|
||||
|
||||
/// Creates a publisher that attempts to recreate its subscription to a failed
|
||||
/// upstream publisher.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - upstream: The publisher from which this publisher receives its elements.
|
||||
/// - retries: The maximum number of retry attempts to perform. If `nil`, this
|
||||
/// publisher attempts to reconnect with the upstream publisher an unlimited
|
||||
/// number of times.
|
||||
public init(upstream: Upstream, retries: Int?) {
|
||||
self.upstream = upstream
|
||||
self.retries = retries
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure
|
||||
{
|
||||
upstream.subscribe(Inner(parent: self, downstream: subscriber))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Retry: Equatable where Upstream: Equatable {}
|
||||
|
||||
extension Publishers.Retry {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Failure == Failure, Downstream.Input == Output
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private enum State {
|
||||
case ready(Publishers.Retry<Upstream>, Downstream)
|
||||
case terminal
|
||||
}
|
||||
|
||||
private enum Chances {
|
||||
case finite(Int)
|
||||
case infinite
|
||||
}
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private var state: State
|
||||
|
||||
private var upstreamSubscription: Subscription?
|
||||
|
||||
private var remaining: Chances
|
||||
|
||||
private var downstreamNeedsSubscription = true
|
||||
|
||||
private var downstreamDemand = Subscribers.Demand.none
|
||||
|
||||
private var completionRecursion = false
|
||||
|
||||
private var needsSubscribe = false
|
||||
|
||||
init(parent: Publishers.Retry<Upstream>, downstream: Downstream) {
|
||||
state = .ready(parent, downstream)
|
||||
remaining = parent.retries.map(Chances.finite) ?? .infinite
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case let .ready(_, downstream) = state, upstreamSubscription == nil
|
||||
else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
upstreamSubscription = subscription
|
||||
let downstreamDemand = self.downstreamDemand
|
||||
let downstreamNeedsSubscription = self.downstreamNeedsSubscription
|
||||
self.downstreamNeedsSubscription = false
|
||||
lock.unlock()
|
||||
if downstreamNeedsSubscription {
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
if downstreamDemand != .none {
|
||||
subscription.request(downstreamDemand)
|
||||
}
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case let .ready(_, downstream) = state else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
downstreamDemand -= 1
|
||||
lock.unlock()
|
||||
|
||||
let newDemand = downstream.receive(input)
|
||||
|
||||
if newDemand == .none { return .none }
|
||||
|
||||
lock.lock()
|
||||
downstreamDemand += newDemand
|
||||
|
||||
if let upstreamSubscription = self.upstreamSubscription {
|
||||
lock.unlock()
|
||||
upstreamSubscription.request(newDemand)
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
return .none
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
guard case let .ready(parent, downstream) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
|
||||
if case .failure = completion {
|
||||
upstreamSubscription = nil
|
||||
switch remaining {
|
||||
case .finite(0):
|
||||
break
|
||||
case .finite(let attempts):
|
||||
remaining = .finite(attempts - 1)
|
||||
fallthrough
|
||||
case .infinite:
|
||||
if completionRecursion {
|
||||
needsSubscribe = true
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
repeat {
|
||||
completionRecursion = true
|
||||
needsSubscribe = false
|
||||
lock.unlock()
|
||||
parent.upstream.subscribe(self)
|
||||
lock.lock()
|
||||
completionRecursion = false
|
||||
} while needsSubscribe
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard case .ready = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
downstreamDemand += demand
|
||||
if let upstreamSubscription = self.upstreamSubscription {
|
||||
lock.unlock()
|
||||
upstreamSubscription.request(demand)
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case .ready = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
if let upstreamSubscription = self.upstreamSubscription {
|
||||
lock.unlock()
|
||||
upstreamSubscription.cancel()
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
var description: String { return "Retry" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
//
|
||||
// Publishers.Throttle.swift
|
||||
//
|
||||
//
|
||||
// Created by Stuart Austin on 14/11/2020.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
// swiftlint:disable generic_type_name line_length
|
||||
|
||||
/// Publishes either the most-recent or first element published by the upstream
|
||||
/// publisher in the specified time interval.
|
||||
///
|
||||
/// Use `throttle(for:scheduler:latest:`` to selectively republish elements from
|
||||
/// an upstream publisher during an interval you specify. Other elements received from
|
||||
/// the upstream in the throttling interval aren’t republished.
|
||||
///
|
||||
/// In the example below, a `Timer.TimerPublisher` produces elements on 3-second
|
||||
/// intervals; the `throttle(for:scheduler:latest:)` operator delivers the first
|
||||
/// event, then republishes only the latest event in the following ten second
|
||||
/// intervals:
|
||||
///
|
||||
/// cancellable = Timer.publish(every: 3.0, on: .main, in: .default)
|
||||
/// .autoconnect()
|
||||
/// .print("\(Date().description)")
|
||||
/// .throttle(for: 10.0, scheduler: RunLoop.main, latest: true)
|
||||
/// .sink(
|
||||
/// receiveCompletion: { print ("Completion: \($0).") },
|
||||
/// receiveValue: { print("Received Timestamp \($0).") }
|
||||
/// )
|
||||
///
|
||||
/// // Prints:
|
||||
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:26:57 +0000)
|
||||
/// // Received Timestamp 2020-03-19 18:26:57 +0000.
|
||||
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:27:00 +0000)
|
||||
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:27:03 +0000)
|
||||
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:27:06 +0000)
|
||||
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:27:09 +0000)
|
||||
/// // Received Timestamp 2020-03-19 18:27:09 +0000.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - interval: The interval at which to find and emit either the most recent or
|
||||
/// the first element, expressed in the time system of the scheduler.
|
||||
/// - scheduler: The scheduler on which to publish elements.
|
||||
/// - latest: A Boolean value that indicates whether to publish the most recent
|
||||
/// element. If `false`, the publisher emits the first element received during
|
||||
/// the interval.
|
||||
/// - Returns: A publisher that emits either the most-recent or first element received
|
||||
/// during the specified interval.
|
||||
public func throttle<S>(for interval: S.SchedulerTimeType.Stride,
|
||||
scheduler: S,
|
||||
latest: Bool) -> Publishers.Throttle<Self, S>
|
||||
where S: Scheduler
|
||||
{
|
||||
return .init(upstream: self,
|
||||
interval: interval,
|
||||
scheduler: scheduler,
|
||||
latest: latest)
|
||||
}
|
||||
|
||||
// swiftlint:enable generic_type_name line_length
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that publishes either the most-recent or first element published by
|
||||
/// the upstream publisher in a specified time interval.
|
||||
public struct Throttle<Upstream, Context>: Publisher
|
||||
where Upstream: Publisher, Context: Scheduler
|
||||
{
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The interval in which to find and emit the most recent element.
|
||||
public let interval: Context.SchedulerTimeType.Stride
|
||||
|
||||
/// The scheduler on which to publish elements.
|
||||
public let scheduler: Context
|
||||
|
||||
/// A Boolean value indicating whether to publish the most recent element.
|
||||
///
|
||||
/// If `false`, the publisher emits the first element received during
|
||||
/// the interval.
|
||||
public let latest: Bool
|
||||
|
||||
public init(upstream: Upstream,
|
||||
interval: Context.SchedulerTimeType.Stride,
|
||||
scheduler: Context,
|
||||
latest: Bool) {
|
||||
|
||||
self.upstream = upstream
|
||||
self.interval = interval
|
||||
self.scheduler = scheduler
|
||||
self.latest = latest
|
||||
}
|
||||
|
||||
// swiftlint:disable generic_type_name
|
||||
|
||||
/// Attaches the specified subscriber to this publisher.
|
||||
///
|
||||
/// Implementations of ``Publisher`` must implement this method.
|
||||
///
|
||||
/// The provided implementation of ``Publisher/subscribe(_:)-4u8kn``calls
|
||||
/// this method.
|
||||
///
|
||||
/// - Parameter subscriber: The subscriber to attach to this ``Publisher``,
|
||||
/// after which it can receive values.
|
||||
public func receive<S>(subscriber: S)
|
||||
where S: Subscriber, Upstream.Failure == S.Failure, Upstream.Output == S.Input
|
||||
{
|
||||
let inner = Inner(interval: interval,
|
||||
scheduler: scheduler,
|
||||
latest: latest,
|
||||
downstream: subscriber)
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
|
||||
// swiftlint:enable generic_type_name
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Throttle {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private enum State {
|
||||
case awaitingSubscription(Downstream)
|
||||
case subscribed(Subscription, Downstream)
|
||||
case pendingTerminal(Subscription, Downstream)
|
||||
case terminal
|
||||
}
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
private let interval: Context.SchedulerTimeType.Stride
|
||||
private let scheduler: Context
|
||||
private let latest: Bool
|
||||
private var state: State
|
||||
private let downstreamLock = UnfairRecursiveLock.allocate()
|
||||
|
||||
private var lastEmissionTime: Context.SchedulerTimeType?
|
||||
|
||||
private var pendingInput: Input?
|
||||
private var pendingCompletion: Subscribers.Completion<Failure>?
|
||||
|
||||
private var demand: Subscribers.Demand = .none
|
||||
|
||||
private var lastTime: Context.SchedulerTimeType
|
||||
|
||||
init(interval: Context.SchedulerTimeType.Stride,
|
||||
scheduler: Context,
|
||||
latest: Bool,
|
||||
downstream: Downstream) {
|
||||
self.state = .awaitingSubscription(downstream)
|
||||
self.interval = interval
|
||||
self.scheduler = scheduler
|
||||
self.latest = latest
|
||||
|
||||
self.lastTime = scheduler.now
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
downstreamLock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case let .awaitingSubscription(downstream) = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
self.lastTime = scheduler.now
|
||||
|
||||
state = .subscribed(subscription, downstream)
|
||||
lock.unlock()
|
||||
|
||||
subscription.request(.unlimited)
|
||||
|
||||
downstreamLock.lock()
|
||||
downstream.receive(subscription: self)
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case .subscribed = state else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
|
||||
let lastTime = scheduler.now
|
||||
self.lastTime = lastTime
|
||||
|
||||
guard demand > .none else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
|
||||
let hasScheduledOutput = (pendingInput != nil || pendingCompletion != nil)
|
||||
|
||||
if hasScheduledOutput && latest {
|
||||
pendingInput = input
|
||||
lock.unlock()
|
||||
} else if !hasScheduledOutput {
|
||||
let minimumEmissionTime =
|
||||
lastEmissionTime.map { $0.advanced(by: interval) }
|
||||
|
||||
let emissionTime =
|
||||
minimumEmissionTime.map { Swift.max(lastTime, $0) } ?? lastTime
|
||||
|
||||
demand -= 1
|
||||
|
||||
pendingInput = input
|
||||
lock.unlock()
|
||||
|
||||
let action: () -> Void = { [weak self] in
|
||||
self?.scheduledEmission()
|
||||
}
|
||||
|
||||
if emissionTime == lastTime {
|
||||
scheduler.schedule(action)
|
||||
} else {
|
||||
scheduler.schedule(after: emissionTime, action)
|
||||
}
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
return .none
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription, downstream) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
let lastTime = scheduler.now
|
||||
self.lastTime = lastTime
|
||||
state = .pendingTerminal(subscription, downstream)
|
||||
|
||||
let hasScheduledOutput = (pendingInput != nil || pendingCompletion != nil)
|
||||
|
||||
if hasScheduledOutput && pendingCompletion == nil {
|
||||
pendingCompletion = completion
|
||||
lock.unlock()
|
||||
} else if !hasScheduledOutput {
|
||||
pendingCompletion = completion
|
||||
lock.unlock()
|
||||
|
||||
scheduler.schedule { [weak self] in
|
||||
self?.scheduledEmission()
|
||||
}
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduledEmission() {
|
||||
lock.lock()
|
||||
|
||||
let downstream: Downstream
|
||||
|
||||
switch state {
|
||||
case .awaitingSubscription, .terminal:
|
||||
lock.unlock()
|
||||
return
|
||||
case let .subscribed(_, foundDownstream),
|
||||
let .pendingTerminal(_, foundDownstream):
|
||||
downstream = foundDownstream
|
||||
}
|
||||
|
||||
if self.pendingInput != nil && self.pendingCompletion == nil {
|
||||
lastEmissionTime = scheduler.now
|
||||
}
|
||||
|
||||
let pendingInput = self.pendingInput
|
||||
let pendingCompletion = self.pendingCompletion
|
||||
|
||||
self.pendingInput = nil
|
||||
self.pendingCompletion = nil
|
||||
|
||||
if pendingCompletion != nil {
|
||||
state = .terminal
|
||||
}
|
||||
|
||||
lock.unlock()
|
||||
|
||||
downstreamLock.lock()
|
||||
|
||||
let newDemand: Subscribers.Demand
|
||||
if let input = pendingInput {
|
||||
newDemand = downstream.receive(input)
|
||||
} else {
|
||||
newDemand = .none
|
||||
}
|
||||
|
||||
if let completion = pendingCompletion {
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
downstreamLock.unlock()
|
||||
|
||||
guard newDemand > 0 else { return }
|
||||
self.lock.lock()
|
||||
demand += newDemand
|
||||
self.lock.unlock()
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
guard demand > 0 else { return }
|
||||
lock.lock()
|
||||
guard case .subscribed = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
self.demand += demand
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
|
||||
let subscription: Subscription?
|
||||
switch state {
|
||||
case let .subscribed(existingSubscription, _),
|
||||
let .pendingTerminal(existingSubscription, _):
|
||||
subscription = existingSubscription
|
||||
case .awaitingSubscription, .terminal:
|
||||
subscription = nil
|
||||
}
|
||||
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
|
||||
subscription?.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "Throttle" }
|
||||
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -200,7 +200,7 @@ extension DispatchQueue {
|
||||
/// - Parameter exactly: A binary integer representing a time interval.
|
||||
public init?<Source: BinaryInteger>(exactly source: Source) {
|
||||
guard let value = Int(exactly: source) else { return nil }
|
||||
self = .nanoseconds(value)
|
||||
self = .seconds(value)
|
||||
}
|
||||
|
||||
public static func < (lhs: Stride, rhs: Stride) -> Bool {
|
||||
|
||||
@@ -60,7 +60,12 @@ internal struct Timer {
|
||||
repeats: Bool,
|
||||
block: @escaping (Timer) -> Void
|
||||
) {
|
||||
self.init(fire: Date(), interval: timeInterval, repeats: repeats, block: block)
|
||||
self.init(
|
||||
fire: Date() + timeInterval,
|
||||
interval: timeInterval,
|
||||
repeats: repeats,
|
||||
block: block
|
||||
)
|
||||
}
|
||||
|
||||
internal var tolerance: TimeInterval {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
#if canImport(Combine)
|
||||
@_exported import Combine
|
||||
#else
|
||||
@_exported import OpenCombine
|
||||
#if canImport(OpenCombineDispatch)
|
||||
@_exported import OpenCombineDispatch
|
||||
#endif
|
||||
#if canImport(OpenCombineFoundation)
|
||||
@_exported import OpenCombineFoundation
|
||||
#endif
|
||||
#endif
|
||||
@@ -5,4 +5,4 @@ disabled_rules:
|
||||
- explicit_acl
|
||||
- explicit_top_level_acl
|
||||
- explicit_enum_raw_value
|
||||
|
||||
- untyped_error_in_catch
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Sergej Jaskiewicz on 26.08.2019.
|
||||
//
|
||||
|
||||
#if !WASI
|
||||
|
||||
import Dispatch
|
||||
import XCTest
|
||||
|
||||
@@ -254,7 +256,7 @@ final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
XCTAssertEqual((2 as Stride).magnitude, 2_000_000_000)
|
||||
|
||||
XCTAssertNil(Stride(exactly: UInt64.max))
|
||||
XCTAssertEqual(Stride(exactly: 871 as UInt64)?.magnitude, 871)
|
||||
XCTAssertEqual(Stride(exactly: 2 as UInt64)?.magnitude, 2_000_000_000)
|
||||
}
|
||||
|
||||
func testStrideFromTooMuchSecondsCrashes() {
|
||||
@@ -570,7 +572,7 @@ private typealias Scheduler = DispatchQueue.OCombine
|
||||
private let mainScheduler = DispatchQueue.main.ocombine
|
||||
private let backgroundScheduler = DispatchQueue.global(qos: .background).ocombine
|
||||
|
||||
#endif
|
||||
#endif // OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
@@ -578,3 +580,5 @@ private typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
private struct KeyedWrapper<Value: Codable & Equatable>: Codable, Equatable {
|
||||
let value: Value
|
||||
}
|
||||
|
||||
#endif // !WASI
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
#if !WASI
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@@ -61,3 +63,5 @@ final class JSONDecoderTests: XCTestCase {
|
||||
cancellable.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
#endif // !WASI
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
#if !WASI
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@@ -65,3 +67,5 @@ final class JSONEncoderTests: XCTestCase {
|
||||
cancellable.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
#endif // !WASI
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
#if !WASI
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@@ -630,4 +632,6 @@ extension Notification.Name {
|
||||
self.init(rawValue: rawValue)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif // !canImport(Darwin) && swift(<5.1)
|
||||
|
||||
#endif // !WASI
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
// Created by Sergej Jaskiewicz on 14.06.2020.
|
||||
//
|
||||
|
||||
// OperationQueue has serious bugs in swift-corelibs-foundation prior to Swift 5.3.
|
||||
// (see https://github.com/apple/swift-corelibs-foundation/pull/2779)
|
||||
#if canImport(Darwin) || swift(>=5.3) && !WASI // TEST_DISCOVERY_CONDITION
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@@ -15,10 +19,6 @@ import OpenCombine
|
||||
import OpenCombineFoundation
|
||||
#endif
|
||||
|
||||
// OperationQueue has serious bugs in swift-corelibs-foundation prior to Swift 5.3.
|
||||
// (see https://github.com/apple/swift-corelibs-foundation/pull/2779)
|
||||
#if canImport(Darwin) || swift(>=5.3) // TEST_DISCOVERY_CONDITION
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class OperationQueueSchedulerTests: XCTestCase {
|
||||
|
||||
@@ -225,7 +225,7 @@ final class OperationQueueSchedulerTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testScheduleRepeatingWithRealQueue() {
|
||||
let mainQueue = OperationQueue.main
|
||||
let mainQueueScheduler = makeScheduler(OperationQueue.main)
|
||||
|
||||
let expectation10ticks = expectation(description: "10 ticks")
|
||||
expectation10ticks.expectedFulfillmentCount = 10
|
||||
@@ -234,13 +234,12 @@ final class OperationQueueSchedulerTests: XCTestCase {
|
||||
|
||||
let ticks = Atomic([TimeInterval]())
|
||||
|
||||
let desiredDelay: TimeInterval = 0.7
|
||||
let desiredInterval: TimeInterval = 0.3
|
||||
let desiredDelay: TimeInterval = 0.8
|
||||
let desiredInterval: TimeInterval = 0.5
|
||||
|
||||
let cancellable = executeOnBackgroundThread { () -> Cancellable in
|
||||
let scheduler = makeScheduler(mainQueue)
|
||||
return scheduler
|
||||
.schedule(after: scheduler.now.advanced(by: .init(desiredDelay)),
|
||||
let cancellable = executeOnBackgroundThread {
|
||||
mainQueueScheduler
|
||||
.schedule(after: mainQueueScheduler.now.advanced(by: .init(desiredDelay)),
|
||||
interval: .init(desiredInterval)) {
|
||||
XCTAssertTrue(Thread.isMainThread)
|
||||
ticks.append(Date().timeIntervalSinceReferenceDate)
|
||||
@@ -256,7 +255,7 @@ final class OperationQueueSchedulerTests: XCTestCase {
|
||||
RunLoop.main.run(until: Date() + 0.001)
|
||||
XCTAssertEqual(ticks.count, 0)
|
||||
|
||||
wait(for: [expectation10ticks], timeout: 5)
|
||||
wait(for: [expectation10ticks], timeout: 10)
|
||||
|
||||
if ticks.isEmpty {
|
||||
XCTFail("The scheduler doesn't work")
|
||||
@@ -471,4 +470,4 @@ private final class TestOperationQueue: OperationQueue {
|
||||
#endif // canImport(Darwin)
|
||||
}
|
||||
|
||||
#endif // canImport(Darwin) || swift(>=5.3)
|
||||
#endif // canImport(Darwin) || swift(>=5.3) && !WASI
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
// PropertyListEncoder and PropertyListDecoder are unavailable in
|
||||
// swift-corelibs-foundation prior to Swift 5.1.
|
||||
#if canImport(Darwin) || swift(>=5.1) && !WASI // TEST_DISCOVERY_CONDITION
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@@ -15,10 +19,6 @@ import OpenCombine
|
||||
import OpenCombineFoundation
|
||||
#endif
|
||||
|
||||
// PropertyListEncoder and PropertyListDecoder are unavailable in
|
||||
// swift-corelibs-foundation prior to Swift 5.1.
|
||||
#if canImport(Darwin) || swift(>=5.1) // TEST_DISCOVERY_CONDITION
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class PropertyListDecoderTests: XCTestCase {
|
||||
func testSuccessfullyDecode() {
|
||||
@@ -79,4 +79,4 @@ final class PropertyListDecoderTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
#endif // canImport(Darwin) || swift(>=5.1)
|
||||
#endif // canImport(Darwin) || swift(>=5.1) && !WASI
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
// Created by Sergej Jaskiewicz on 10.12.2019.
|
||||
//
|
||||
|
||||
// PropertyListEncoder and PropertyListDecoder are unavailable in
|
||||
// swift-corelibs-foundation prior to Swift 5.1.
|
||||
#if canImport(Darwin) || swift(>=5.1) && !WASI // TEST_DISCOVERY_CONDITION
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@@ -15,10 +19,6 @@ import OpenCombine
|
||||
import OpenCombineFoundation
|
||||
#endif
|
||||
|
||||
// PropertyListEncoder and PropertyListDecoder are unavailable in
|
||||
// swift-corelibs-foundation prior to Swift 5.1.
|
||||
#if canImport(Darwin) || swift(>=5.1) // TEST_DISCOVERY_CONDITION
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class PropertyListEncoderTests: XCTestCase {
|
||||
|
||||
@@ -84,4 +84,4 @@ final class PropertyListEncoderTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
#endif // canImport(Darwin) || swift(>=5.1)
|
||||
#endif // canImport(Darwin) || swift(>=5.1) && !WASI
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Sergej Jaskiewicz on 14.12.2019.
|
||||
//
|
||||
|
||||
#if !WASI
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@@ -627,3 +629,5 @@ extension RunLoopScheduler.SchedulerTimeType.Stride: TimeIntervalBackedScheduler
|
||||
extension RunLoopScheduler.SchedulerTimeType: DateBackedSchedulerTimeType {}
|
||||
|
||||
extension RunLoopScheduler: RunLoopLikeScheduler {}
|
||||
|
||||
#endif // !WASI
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Sergej Jaskiewicz on 23.06.2020.
|
||||
//
|
||||
|
||||
#if !WASI
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@@ -214,3 +216,5 @@ private typealias TimerPublisher = Timer.TimerPublisher
|
||||
#else
|
||||
private typealias TimerPublisher = Timer.OCombine.TimerPublisher
|
||||
#endif
|
||||
|
||||
#endif // !WASI
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
// swiftlint:disable multiline_arguments
|
||||
|
||||
#if !WASI
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@@ -51,13 +53,14 @@ final class URLSessionTests: XCTestCase {
|
||||
private let unknownError = URLError(.unknown)
|
||||
|
||||
func testDataTaskPublisherFromURL() {
|
||||
let publisher = makePublisher(TestURLSession(testDataTask: .init()), testURL)
|
||||
let publisher = makePublisher(TestURLSession.withTestDataTask(.create()), testURL)
|
||||
let expectedRequest = URLRequest(url: testURL)
|
||||
XCTAssertEqual(publisher.request, expectedRequest)
|
||||
}
|
||||
|
||||
func testDataTaskPublisherFromRequest() {
|
||||
let publisher = makePublisher(TestURLSession(testDataTask: .init()), testRequest)
|
||||
let publisher = makePublisher(TestURLSession.withTestDataTask(.create()),
|
||||
testRequest)
|
||||
XCTAssertEqual(publisher.request, testRequest)
|
||||
}
|
||||
|
||||
@@ -124,8 +127,8 @@ final class URLSessionTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testRequesting() throws {
|
||||
let dataTask = TestURLSessionDataTask()
|
||||
let session = TestURLSession(testDataTask: dataTask)
|
||||
let dataTask = TestURLSessionDataTask.create()
|
||||
let session = TestURLSession.withTestDataTask(dataTask)
|
||||
let publisher = makePublisher(session, testRequest)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
@@ -164,8 +167,8 @@ final class URLSessionTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let dataTask = TestURLSessionDataTask()
|
||||
let session = TestURLSession(testDataTask: dataTask)
|
||||
let dataTask = TestURLSessionDataTask.create()
|
||||
let session = TestURLSession.withTestDataTask(dataTask)
|
||||
let publisher = makePublisher(session, testRequest)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
@@ -190,8 +193,8 @@ final class URLSessionTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testCrashesOnZeroDemand() throws {
|
||||
let dataTask = TestURLSessionDataTask()
|
||||
let session = TestURLSession(testDataTask: dataTask)
|
||||
let dataTask = TestURLSessionDataTask.create()
|
||||
let session = TestURLSession.withTestDataTask(dataTask)
|
||||
let publisher = makePublisher(session, testURL)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
@@ -205,8 +208,8 @@ final class URLSessionTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testURLSessionSubscriptionReflection() throws {
|
||||
let dataTask = TestURLSessionDataTask()
|
||||
let session = TestURLSession(testDataTask: dataTask)
|
||||
let dataTask = TestURLSessionDataTask.create()
|
||||
let session = TestURLSession.withTestDataTask(dataTask)
|
||||
let publisher = makePublisher(session, testURL)
|
||||
try testSubscriptionReflection(
|
||||
description: "DataTaskPublisher",
|
||||
@@ -227,8 +230,8 @@ final class URLSessionTests: XCTestCase {
|
||||
_ response: URLResponse?,
|
||||
_ error: Error?,
|
||||
expected: [TrackingSubscriber.Event]) {
|
||||
let dataTask = TestURLSessionDataTask()
|
||||
let session = TestURLSession(testDataTask: dataTask)
|
||||
let dataTask = TestURLSessionDataTask.create()
|
||||
let session = TestURLSession.withTestDataTask(dataTask)
|
||||
let publisher = makePublisher(session, testRequest)
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.max(1)) })
|
||||
publisher.subscribe(tracking)
|
||||
@@ -290,16 +293,25 @@ private class TestURLSession: URLSession {
|
||||
|
||||
private(set) var history = [Event]()
|
||||
|
||||
private(set) var dataTaskCompletionHandlers: [(Data?, URLResponse?, Error?) -> Void]
|
||||
private(set) var dataTaskCompletionHandlers =
|
||||
[(Data?, URLResponse?, Error?) -> Void]()
|
||||
|
||||
private let testDataTask: TestURLSessionDataTask
|
||||
private var testDataTask: TestURLSessionDataTask?
|
||||
|
||||
init(testDataTask: TestURLSessionDataTask) {
|
||||
self.testDataTask = testDataTask
|
||||
self.dataTaskCompletionHandlers = []
|
||||
#if !canImport(Darwin)
|
||||
super.init(configuration: .default)
|
||||
static func withTestDataTask(
|
||||
_ testDataTask: TestURLSessionDataTask
|
||||
) -> TestURLSession {
|
||||
// This dance is to avoid the deprecation warning for the URLSession
|
||||
// default initializer. Believe me, I've tried to make it less ugly with
|
||||
// no success.
|
||||
#if canImport(Darwin)
|
||||
let sessionClass = TestURLSession.self as NSObject.Type
|
||||
let session = sessionClass.init() as! TestURLSession
|
||||
#else
|
||||
let session = TestURLSession(configuration: .default)
|
||||
#endif
|
||||
session.testDataTask = testDataTask
|
||||
return session
|
||||
}
|
||||
|
||||
// MARK: Testing
|
||||
@@ -377,12 +389,12 @@ private class TestURLSession: URLSession {
|
||||
|
||||
override func dataTask(with request: URLRequest) -> URLSessionDataTask {
|
||||
history.append(.dataTaskWithRequest(request))
|
||||
return testDataTask
|
||||
return testDataTask!
|
||||
}
|
||||
|
||||
override func dataTask(with url: URL) -> URLSessionDataTask {
|
||||
history.append(.dataTaskWithURL(url))
|
||||
return testDataTask
|
||||
return testDataTask!
|
||||
}
|
||||
|
||||
override func dataTask(
|
||||
@@ -391,7 +403,7 @@ private class TestURLSession: URLSession {
|
||||
) -> URLSessionDataTask {
|
||||
history.append(.dataTaskWithURLAndCompletion(url))
|
||||
dataTaskCompletionHandlers.append(completionHandler)
|
||||
return testDataTask
|
||||
return testDataTask!
|
||||
}
|
||||
|
||||
override func dataTask(
|
||||
@@ -400,7 +412,7 @@ private class TestURLSession: URLSession {
|
||||
) -> URLSessionDataTask {
|
||||
history.append(.dataTaskWithRequestAndCompletion(request))
|
||||
dataTaskCompletionHandlers.append(completionHandler)
|
||||
return testDataTask
|
||||
return testDataTask!
|
||||
}
|
||||
|
||||
override func uploadTask(with request: URLRequest,
|
||||
@@ -554,7 +566,18 @@ private final class TestURLSessionDataTask: URLSessionDataTask {
|
||||
|
||||
private(set) var history = [Event]()
|
||||
|
||||
override init() {}
|
||||
static func create() -> TestURLSessionDataTask {
|
||||
// This dance is to avoid the deprecation warning for the URLSessionDataTask
|
||||
// default initializer. Believe me, I've tried to make it less ugly with
|
||||
// no success.
|
||||
#if canImport(Darwin)
|
||||
let dataTaskClass = TestURLSessionDataTask.self as NSObject.Type
|
||||
let dataTask = dataTaskClass.init() as! TestURLSessionDataTask
|
||||
#else
|
||||
let dataTask = TestURLSessionDataTask()
|
||||
#endif
|
||||
return dataTask
|
||||
}
|
||||
|
||||
override var taskIdentifier: Int {
|
||||
history.append(.taskIdentifier)
|
||||
@@ -721,6 +744,8 @@ private func makePublisher(
|
||||
) -> URLSession.OCombine.DataTaskPublisher {
|
||||
return session.ocombine.dataTaskPublisher(for: request)
|
||||
}
|
||||
#endif
|
||||
#endif // OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
|
||||
|
||||
#endif // canImport(Darwin)
|
||||
|
||||
#endif // !WASI
|
||||
|
||||
@@ -29,7 +29,7 @@ extension XCTest {
|
||||
// Taken from swift-corelibs-foundation and slightly modified for OpenCombine
|
||||
@available(macOS 10.13, iOS 8.0, *)
|
||||
func assertCrashes(within block: () throws -> Void) rethrows {
|
||||
#if !Xcode && !os(iOS) && !os(watchOS) && !os(tvOS)
|
||||
#if !Xcode && !os(iOS) && !os(watchOS) && !os(tvOS) && !WASI
|
||||
let childProcessEnvVariable = "OPENCOMBINE_TEST_PERFORM_ASSERT_CRASHES_BLOCKS"
|
||||
let childProcessEnvVariableOnValue = "YES"
|
||||
|
||||
@@ -82,6 +82,6 @@ extension XCTest {
|
||||
printDiagnostics()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif // !Xcode && !os(iOS) && !os(watchOS) && !os(tvOS) && !WASI
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,9 +98,14 @@ extension XCTest {
|
||||
}
|
||||
}
|
||||
|
||||
enum CancelBeforeSubscriptionBehavior {
|
||||
case crash
|
||||
case history([CustomSubscription.Event])
|
||||
}
|
||||
|
||||
func testCancelBeforeSubscription<Value, Operator: Publisher>(
|
||||
inputType: Value.Type,
|
||||
shouldCrash: Bool,
|
||||
expected: CancelBeforeSubscriptionBehavior,
|
||||
_ makeOperator: (CustomConnectablePublisherBase<Value, Never>) -> Operator
|
||||
) {
|
||||
|
||||
@@ -109,17 +114,23 @@ extension XCTest {
|
||||
let tracking = TrackingSubscriberBase<Operator.Output, Operator.Failure>()
|
||||
operatorPublisher.subscribe(tracking)
|
||||
|
||||
guard let subscription = publisher.erasedSubscriber as? Subscription else {
|
||||
guard let downstreamSubscription = publisher.erasedSubscriber as? Subscription
|
||||
else {
|
||||
XCTFail("The subscriber must also be a subscription")
|
||||
return
|
||||
}
|
||||
|
||||
if shouldCrash {
|
||||
switch expected {
|
||||
case .crash:
|
||||
assertCrashes {
|
||||
subscription.cancel()
|
||||
downstreamSubscription.cancel()
|
||||
}
|
||||
} else {
|
||||
subscription.cancel()
|
||||
case let .history(history):
|
||||
downstreamSubscription.cancel()
|
||||
|
||||
let subscription = CustomSubscription()
|
||||
publisher.send(subscription: subscription)
|
||||
XCTAssertEqual(subscription.history, history)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,4 +214,19 @@ func unreachable() -> Never {
|
||||
fatalError("unreachable")
|
||||
}
|
||||
|
||||
func fromNever<T>(_ resultType: T.Type) -> (Never) -> T {
|
||||
// This is to avoid the 'Will never be executed' warning.
|
||||
//
|
||||
// The first variant doesn't produce warnings in Swift 5.1, but doesn't compile
|
||||
// with early Swift versions.
|
||||
//
|
||||
// The second variant compiles with all Swift versions,
|
||||
// but produces a warning in Swift 5.1.
|
||||
#if swift(>=5.1)
|
||||
return { (_: Never) -> T in }
|
||||
#else
|
||||
return { switch $0 {} }
|
||||
#endif
|
||||
}
|
||||
|
||||
// swiftlint:enable generic_type_name
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Sergej Jaskiewicz on 04.02.2020.
|
||||
//
|
||||
|
||||
#if !WASI
|
||||
|
||||
#if canImport(Darwin)
|
||||
import Darwin
|
||||
#elseif canImport(Glibc)
|
||||
@@ -81,3 +83,5 @@ func executeOnBackgroundThread<ResultType>(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // !WASI
|
||||
|
||||
@@ -5,20 +5,34 @@
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
#if !WASI
|
||||
import Dispatch
|
||||
#endif
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
func race(times: Int = 100, _ bodies: () -> Void...) {
|
||||
#if WASI
|
||||
for body in bodies {
|
||||
for _ in 0..<times {
|
||||
body()
|
||||
}
|
||||
}
|
||||
#else
|
||||
DispatchQueue.concurrentPerform(iterations: bodies.count) {
|
||||
for _ in 0..<times {
|
||||
bodies[$0]()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
final class Atomic<Value> {
|
||||
#if !WASI
|
||||
let lock = NSLock()
|
||||
#endif
|
||||
|
||||
private var _value: Value
|
||||
|
||||
init(_ initialValue: Value) {
|
||||
@@ -26,20 +40,29 @@ final class Atomic<Value> {
|
||||
}
|
||||
|
||||
var value: Value {
|
||||
#if !WASI
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
#endif
|
||||
|
||||
return _value
|
||||
}
|
||||
|
||||
func set(_ newValue: Value) {
|
||||
#if !WASI
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
#endif
|
||||
|
||||
_value = newValue
|
||||
}
|
||||
|
||||
func `do`(_ body: (inout Value) throws -> Void) rethrows {
|
||||
#if !WASI
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
#endif
|
||||
|
||||
try body(&_value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,6 +352,7 @@ enum StringSubscription: Subscription,
|
||||
ExpressibleByStringLiteral {
|
||||
|
||||
case string(String)
|
||||
case contains(String)
|
||||
case subscription(Subscription)
|
||||
|
||||
init(_ subscription: Subscription) {
|
||||
@@ -364,7 +365,7 @@ enum StringSubscription: Subscription,
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .string(let string):
|
||||
case .string(let string), .contains(let string):
|
||||
return string
|
||||
case .subscription(let subscription):
|
||||
return String(describing: subscription)
|
||||
@@ -379,7 +380,7 @@ enum StringSubscription: Subscription,
|
||||
switch self {
|
||||
case .subscription(let subscription):
|
||||
return subscription.combineIdentifier
|
||||
case .string:
|
||||
case .string, .contains:
|
||||
fatalError("String has no combineIdentifier")
|
||||
}
|
||||
}
|
||||
@@ -390,7 +391,7 @@ enum StringSubscription: Subscription,
|
||||
|
||||
var underlying: Subscription? {
|
||||
switch self {
|
||||
case .string:
|
||||
case .string, .contains:
|
||||
return nil
|
||||
case .subscription(let underlying):
|
||||
return underlying
|
||||
@@ -401,6 +402,25 @@ enum StringSubscription: Subscription,
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension StringSubscription: Equatable {
|
||||
static func == (lhs: StringSubscription, rhs: StringSubscription) -> Bool {
|
||||
return lhs.description == rhs.description
|
||||
// swiftlint:disable pattern_matching_keywords
|
||||
switch (lhs, rhs) {
|
||||
case (.contains(let pattern), .subscription(let subscription)),
|
||||
(.subscription(let subscription), .contains(let pattern)):
|
||||
return String(describing: subscription).contains(pattern)
|
||||
case (.contains(let pattern), .string(let string)),
|
||||
(.string(let string), .contains(let pattern)):
|
||||
return string.contains(pattern)
|
||||
case let (.subscription(lhs), .subscription(rhs)):
|
||||
return String(describing: lhs) == String(describing: rhs)
|
||||
case (.string(let string), .subscription(let subscription)),
|
||||
(.subscription(let subscription), .string(let string)):
|
||||
return String(describing: subscription) == string
|
||||
case let (.string(lhs), .string(rhs)):
|
||||
return lhs == rhs
|
||||
case let (.contains(lhs), .contains(rhs)):
|
||||
return lhs.contains(rhs) || rhs.contains(lhs)
|
||||
}
|
||||
|
||||
// swiftlint:enable pattern_matching_keywords
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,9 +357,9 @@ final class VirtualTimeScheduler: Scheduler {
|
||||
_now = time
|
||||
}
|
||||
|
||||
func executeScheduledActions(until deadline: SchedulerTimeType = .nanoseconds(.max)) {
|
||||
precondition(deadline >= _now)
|
||||
while let (time, action) = workQueue.min(), time <= deadline {
|
||||
func executeScheduledActions(until deadline: SchedulerTimeType? = nil) {
|
||||
precondition(deadline.map { $0 >= _now } ?? true)
|
||||
while let (time, action) = workQueue.min(), deadline.map({ time <= $0 }) ?? true {
|
||||
workQueue.extractMin()
|
||||
_now = max(time, _now)
|
||||
action()
|
||||
|
||||
@@ -0,0 +1,424 @@
|
||||
//
|
||||
// ObservableObjectTests.swift
|
||||
//
|
||||
//
|
||||
// Created by kateinoigakukun on 2020/12/22.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if swift(>=5.1)
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private typealias Published = Combine.Published
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private typealias ObservableObject = Combine.ObservableObject
|
||||
#else
|
||||
import OpenCombine
|
||||
|
||||
private typealias Published = OpenCombine.Published
|
||||
|
||||
private typealias ObservableObject = OpenCombine.ObservableObject
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class ObservableObjectTests: XCTestCase {
|
||||
var disposeBag = [AnyCancellable]()
|
||||
|
||||
override func tearDown() {
|
||||
disposeBag = []
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testBasicBehavior() {
|
||||
let testObject = TestObject()
|
||||
var downstreamSubscription1: Subscription?
|
||||
let tracking1 = TrackingSubscriberBase<Void, Never>(
|
||||
receiveSubscription: { downstreamSubscription1 = $0 }
|
||||
)
|
||||
|
||||
testObject.objectWillChange.subscribe(tracking1)
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher")])
|
||||
downstreamSubscription1?.request(.max(2))
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher")])
|
||||
testObject.state1 += 1
|
||||
testObject.state1 += 2
|
||||
testObject.state1 += 3
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal,
|
||||
.signal,
|
||||
.signal])
|
||||
testObject.state2 += 1
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal])
|
||||
downstreamSubscription1?.request(.max(10))
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal])
|
||||
|
||||
let tracking2 = TrackingSubscriberBase<Void, Never>(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
testObject.objectWillChange.subscribe(tracking2)
|
||||
tracking2.assertHistoryEqual([.subscription("ObservableObjectPublisher")])
|
||||
|
||||
testObject.state1 = 42
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal])
|
||||
tracking2.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal])
|
||||
|
||||
downstreamSubscription1?.cancel()
|
||||
testObject.state1 = -1
|
||||
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal])
|
||||
tracking2.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.value(()),
|
||||
.value(())])
|
||||
}
|
||||
|
||||
// TODO: `objectWillChange` should return the same `ObservableObjectPublisher`
|
||||
// every time for Combine compatibility
|
||||
//
|
||||
// func testNoFields() {
|
||||
// let observableObject = NoFields()
|
||||
// let publisher1 = observableObject.objectWillChange
|
||||
// let publisher2 = observableObject.objectWillChange
|
||||
// XCTAssert(publisher1 === publisher2)
|
||||
// }
|
||||
|
||||
// func testNoPublishedFields() {
|
||||
// let observableObject = NoPublishedFields()
|
||||
// let publisher1 = observableObject.objectWillChange
|
||||
// let publisher2 = observableObject.objectWillChange
|
||||
// XCTAssert(publisher1 === publisher2)
|
||||
// }
|
||||
|
||||
func testPublishedFieldIsConstant() {
|
||||
let observableObject = PublishedFieldIsConstant()
|
||||
|
||||
let publisher1 = observableObject.objectWillChange
|
||||
let publisher2 = observableObject.objectWillChange
|
||||
|
||||
XCTAssert(publisher1 === publisher2,
|
||||
"""
|
||||
Even if the Published field is a constant, a publisher \
|
||||
should be installed there.
|
||||
""")
|
||||
}
|
||||
|
||||
func testDerivedClassWithPublishedField() {
|
||||
let observableObject = ObservedDerivedWithObservedBase()
|
||||
|
||||
var counter = 0
|
||||
|
||||
observableObject.objectWillChange.sink {
|
||||
counter += 1
|
||||
}.store(in: &disposeBag)
|
||||
|
||||
XCTAssertEqual(observableObject.publishedValue0, 0)
|
||||
XCTAssertEqual(observableObject.simpleValue, "what")
|
||||
XCTAssertEqual(observableObject.subclassPublished0, 0)
|
||||
XCTAssertEqual(observableObject.subclassPublished1, 1)
|
||||
XCTAssertEqual(observableObject.subclassPublished2, 2)
|
||||
|
||||
observableObject.publishedValue0 += 5
|
||||
|
||||
XCTAssertEqual(counter, 1)
|
||||
XCTAssertEqual(observableObject.publishedValue0, 5)
|
||||
|
||||
Published<String>[_enclosingInstance: observableObject,
|
||||
wrapped: \.simpleValue,
|
||||
storage: \.publishedValue1] += "???"
|
||||
|
||||
XCTAssertEqual(counter, 2)
|
||||
XCTAssertEqual(observableObject.simpleValue, "what")
|
||||
|
||||
observableObject.subclassPublished0 += 3
|
||||
|
||||
XCTAssertEqual(counter, 3)
|
||||
XCTAssertEqual(observableObject.subclassPublished0, 3)
|
||||
|
||||
observableObject.subclassPublished1 += 3
|
||||
|
||||
XCTAssertEqual(counter, 4)
|
||||
XCTAssertEqual(observableObject.subclassPublished1, 4)
|
||||
|
||||
observableObject.subclassPublished2 += 3
|
||||
|
||||
XCTAssertEqual(counter, 5)
|
||||
XCTAssertEqual(observableObject.subclassPublished1, 4)
|
||||
}
|
||||
|
||||
func testObjCClassSubclass() {
|
||||
let observableObject = ObjCClassSubclass()
|
||||
let publisher1 = observableObject.objectWillChange
|
||||
let publisher2 = observableObject.objectWillChange
|
||||
XCTAssert(publisher1 === publisher2)
|
||||
}
|
||||
|
||||
func testResilientClassSubclass() {
|
||||
let observableObject = ResilientClassSubclass()
|
||||
let publisher1 = observableObject.objectWillChange
|
||||
let publisher2 = observableObject.objectWillChange
|
||||
|
||||
XCTAssert(publisher1 === publisher2)
|
||||
}
|
||||
|
||||
func testResilientClassSubclass2() {
|
||||
let observableObject = ResilientClassSubclass2()
|
||||
let publisher1 = observableObject.objectWillChange
|
||||
let publisher2 = observableObject.objectWillChange
|
||||
|
||||
XCTAssert(publisher1 === publisher2)
|
||||
}
|
||||
|
||||
func testGenericClass() {
|
||||
let observableObject = GenericClass(123, true)
|
||||
|
||||
var counter = 0
|
||||
|
||||
observableObject.objectWillChange.sink { counter += 1 }.store(in: &disposeBag)
|
||||
XCTAssertEqual(counter, 0)
|
||||
XCTAssertEqual(observableObject.value1, 123)
|
||||
XCTAssertEqual(observableObject.value2, true)
|
||||
|
||||
observableObject.value1 += 1
|
||||
|
||||
XCTAssertEqual(counter, 1)
|
||||
XCTAssertEqual(observableObject.value1, 124)
|
||||
|
||||
observableObject.value2.toggle()
|
||||
|
||||
XCTAssertEqual(counter, 2)
|
||||
XCTAssertEqual(observableObject.value2, false)
|
||||
}
|
||||
|
||||
func testGenericSubclassOfResilientClass() {
|
||||
let observableObject = ResilientClassGenericSubclass("hello", true)
|
||||
|
||||
var counter = 0
|
||||
|
||||
observableObject.objectWillChange.sink { counter += 1 }.store(in: &disposeBag)
|
||||
XCTAssertEqual(counter, 0)
|
||||
XCTAssertEqual(observableObject.value1, "hello")
|
||||
XCTAssertEqual(observableObject.value2, true)
|
||||
|
||||
observableObject.value1 += "!"
|
||||
|
||||
XCTAssertEqual(counter, 1)
|
||||
XCTAssertEqual(observableObject.value1, "hello!")
|
||||
|
||||
observableObject.value2.toggle()
|
||||
|
||||
XCTAssertEqual(counter, 2)
|
||||
XCTAssertEqual(observableObject.value2, false)
|
||||
}
|
||||
|
||||
func testGenericSubclassOfResilientClass2() {
|
||||
let observableObject = ResilientClassGenericSubclass2("hello", true)
|
||||
|
||||
var counter = 0
|
||||
|
||||
observableObject.objectWillChange.sink { counter += 1 }.store(in: &disposeBag)
|
||||
XCTAssertEqual(counter, 0)
|
||||
XCTAssertEqual(observableObject.value1, "hello")
|
||||
XCTAssertEqual(observableObject.value2, true)
|
||||
|
||||
observableObject.value1 += "!"
|
||||
|
||||
XCTAssertEqual(counter, 1)
|
||||
XCTAssertEqual(observableObject.value1, "hello!")
|
||||
|
||||
observableObject.value2.toggle()
|
||||
|
||||
XCTAssertEqual(counter, 2)
|
||||
XCTAssertEqual(observableObject.value2, false)
|
||||
|
||||
observableObject.value3.toggle()
|
||||
|
||||
XCTAssertEqual(counter, 3)
|
||||
XCTAssertEqual(observableObject.value3, true)
|
||||
}
|
||||
|
||||
func testObservableDerivedWithNonObservableBase() {
|
||||
let observableObject = ObservedDerivedWithNonObservedBase()
|
||||
var counter = 0
|
||||
observableObject.objectWillChange.sink { counter += 1 }.store(in: &disposeBag)
|
||||
|
||||
XCTAssertEqual(counter, 0)
|
||||
XCTAssertEqual(observableObject.nonObservedBaseValue0, 10)
|
||||
XCTAssertEqual(observableObject.nonObservedBaseValue1, .pi)
|
||||
XCTAssertEqual(observableObject.observedDerivedValue2,
|
||||
"Asuka is obviously the best girl.")
|
||||
XCTAssertEqual(observableObject.observedDerivedValue3, 255)
|
||||
|
||||
observableObject.nonObservedBaseValue0 -= 1
|
||||
XCTAssertEqual(counter, 1)
|
||||
XCTAssertEqual(observableObject.nonObservedBaseValue0, 9)
|
||||
|
||||
observableObject.nonObservedBaseValue1 *= 2
|
||||
XCTAssertEqual(counter, 2)
|
||||
XCTAssertEqual(observableObject.nonObservedBaseValue1, 2 * .pi)
|
||||
|
||||
observableObject.observedDerivedValue2 = "Nevermind."
|
||||
XCTAssertEqual(counter, 3)
|
||||
XCTAssertEqual(observableObject.observedDerivedValue2, "Nevermind.")
|
||||
|
||||
observableObject.observedDerivedValue3 &+= 1
|
||||
XCTAssertEqual(counter, 4)
|
||||
XCTAssertEqual(observableObject.observedDerivedValue3, 0)
|
||||
}
|
||||
|
||||
func testNSObjectSubclass() {
|
||||
let observableObject = NSObjectSubclass()
|
||||
var counter = 0
|
||||
observableObject.objectWillChange.sink { counter += 1 }.store(in: &disposeBag)
|
||||
|
||||
XCTAssertEqual(counter, 0)
|
||||
XCTAssertEqual(observableObject.value0, 0)
|
||||
XCTAssertEqual(observableObject.value1, 42)
|
||||
|
||||
observableObject.value0 += 1
|
||||
XCTAssertEqual(counter, 1)
|
||||
XCTAssertEqual(observableObject.value0, 1)
|
||||
|
||||
observableObject.value1 += 1
|
||||
XCTAssertEqual(counter, 2)
|
||||
XCTAssertEqual(observableObject.value1, 43)
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private final class NoFields: ObservableObject {}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private final class NoPublishedFields: ObservableObject {
|
||||
var field = NoFields()
|
||||
var int = 0
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private final class PublishedFieldIsConstant: ObservableObject {
|
||||
let publishedValue = Published(initialValue: 42)
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private class ObservedBase: ObservableObject {
|
||||
@Published var publishedValue0 = 0
|
||||
var publishedValue1 = Published(initialValue: "Hello!")
|
||||
let publishedValue2 = Published(initialValue: 42)
|
||||
var simpleValue = "what"
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private final class ObservedDerivedWithObservedBase: ObservedBase {
|
||||
@Published var subclassPublished0 = 0
|
||||
@Published var subclassPublished1 = 1
|
||||
@Published var subclassPublished2 = 2
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension NSNumber: ObservableObject {}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private final class ObjCClassSubclass: NSObject, ObservableObject {
|
||||
@Published var published = 10
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private class ResilientClassSubclass: JSONDecoder, ObservableObject {
|
||||
@Published var published0 = 10
|
||||
@Published var published1 = "hello!"
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private final class ResilientClassSubclass2: ResilientClassSubclass {
|
||||
@Published var published3 = true
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension JSONEncoder: ObservableObject {}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private final class GenericClass<Value1, Value2>: ObservableObject {
|
||||
@Published var value1: Value1
|
||||
@Published var value2: Value2
|
||||
|
||||
init(_ value1: Value1, _ value2: Value2) {
|
||||
self.value1 = value1
|
||||
self.value2 = value2
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private class NonObservedBase {
|
||||
@Published var nonObservedBaseValue0 = 10
|
||||
@Published var nonObservedBaseValue1 = Double.pi
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private class ObservedDerivedWithNonObservedBase: NonObservedBase, ObservableObject {
|
||||
@Published var observedDerivedValue2 = "Asuka is obviously the best girl."
|
||||
@Published var observedDerivedValue3: UInt8 = 255
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private class NSObjectSubclass: NSObject, ObservableObject {
|
||||
@Published var value0 = 0
|
||||
@Published var value1: UInt8 = 42
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private class ResilientClassGenericSubclass<Value1, Value2>
|
||||
: JSONDecoder,
|
||||
ObservableObject
|
||||
{
|
||||
@Published var value1: Value1
|
||||
@Published var value2: Value2
|
||||
|
||||
init(_ value1: Value1, _ value2: Value2) {
|
||||
self.value1 = value1
|
||||
self.value2 = value2
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private final class ResilientClassGenericSubclass2<Value1, Value2>
|
||||
: ResilientClassGenericSubclass<Value1, Value2>
|
||||
{
|
||||
@Published var value3 = false
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private final class TestObject: ObservableObject {
|
||||
@Published var state1: Int
|
||||
@Published var state2: Int
|
||||
var nonPublished: Int
|
||||
|
||||
init(_ initialValue: Int = 0) {
|
||||
_state1 = Published(initialValue: initialValue)
|
||||
_state2 = Published(initialValue: initialValue)
|
||||
nonPublished = initialValue
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -110,6 +110,7 @@ final class PublishedTests: XCTestCase {
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher")])
|
||||
}
|
||||
|
||||
@available(macOS 11.0, iOS 14.0, *)
|
||||
func testAssignToPublished() throws {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisherBase<Int, Never>(subscription: subscription)
|
||||
@@ -148,6 +149,7 @@ final class PublishedTests: XCTestCase {
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
@available(macOS 11.0, iOS 14.0, *)
|
||||
func testAssignToPublishedFinish() {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisherBase<Int, Never>(subscription: subscription)
|
||||
@@ -289,6 +291,7 @@ final class PublishedTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
@available(macOS 11.0, iOS 14.0, *)
|
||||
func testProjectedValueSetter() {
|
||||
let testObject1 = TestObject(1)
|
||||
let testObject2 = TestObject(2)
|
||||
|
||||
@@ -41,14 +41,21 @@ final class AllSatisfyTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
func testAllSatisfyUpstreamFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(
|
||||
func testAllSatisfyUpstreamFinishesImmediatelyWithDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
|
||||
expectedSubscription: "AllSatisfy",
|
||||
expectedResult: true,
|
||||
{ $0.allSatisfy(shouldNotBeCalled()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testAllSatisfyUpstreamFinishesImmediatelyWithoutDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
|
||||
expectedSubscription: "AllSatisfy",
|
||||
{ $0.allSatisfy(shouldNotBeCalled()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testAllSatisfyCancelAlreadyCancelled() throws {
|
||||
try ReduceTests.testCancelAlreadyCancelled {
|
||||
$0.allSatisfy(shouldNotBeCalled())
|
||||
@@ -97,13 +104,14 @@ final class AllSatisfyTests: XCTestCase {
|
||||
|
||||
func testAllSatisfyCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.allSatisfy(shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
func testAllSatisfyLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: false,
|
||||
{ $0.allSatisfy { _ in true } })
|
||||
}
|
||||
|
||||
@@ -155,14 +163,21 @@ final class AllSatisfyTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
func testTryAllSatisfyUpstreamFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(
|
||||
func testTryAllSatisfyUpstreamFinishesImmediatelyWithDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
|
||||
expectedSubscription: "TryAllSatisfy",
|
||||
expectedResult: true,
|
||||
{ $0.tryAllSatisfy(shouldNotBeCalled()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryAllSatisfyUpstreamFinishesImmediatelyWithoutDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
|
||||
expectedSubscription: "TryAllSatisfy",
|
||||
{ $0.tryAllSatisfy(shouldNotBeCalled()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryAllSatisfyCancelAlreadyCancelled() throws {
|
||||
try ReduceTests.testCancelAlreadyCancelled {
|
||||
$0.tryAllSatisfy(shouldNotBeCalled())
|
||||
@@ -217,13 +232,14 @@ final class AllSatisfyTests: XCTestCase {
|
||||
|
||||
func testTryAllSatisfyCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.tryAllSatisfy(shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
func testTryAllSatisfyLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: false,
|
||||
{ $0.tryAllSatisfy { _ in true } })
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Sergej Jaskiewicz on 03.12.2019.
|
||||
//
|
||||
|
||||
#if !WASI
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
@@ -170,3 +172,5 @@ final class BreakpointTests: XCTestCase {
|
||||
{ $0.breakpointOnError() })
|
||||
}
|
||||
}
|
||||
|
||||
#endif // !WASI
|
||||
|
||||
@@ -918,10 +918,23 @@ final class BufferTests: XCTestCase {
|
||||
prefetch: Publishers.PrefetchStrategy,
|
||||
whenFull: Publishers.BufferingStrategy<Never>
|
||||
) {
|
||||
testCancelBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
{ $0.buffer(size: 2, prefetch: prefetch, whenFull: whenFull) }
|
||||
)
|
||||
switch prefetch {
|
||||
case .byRequest:
|
||||
testCancelBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.buffer(size: 2, prefetch: prefetch, whenFull: whenFull) }
|
||||
)
|
||||
case .keepFull:
|
||||
testCancelBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([.requested(.max(2))]),
|
||||
{ $0.buffer(size: 2, prefetch: prefetch, whenFull: whenFull) }
|
||||
)
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
@unknown default:
|
||||
unreachable()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ final class CatchTests: XCTestCase {
|
||||
|
||||
func testCatchReceiveValueBeforeSubscription() {
|
||||
testReceiveValueBeforeSubscription(value: 1, expected: .crash) {
|
||||
$0.catch { _ in Just(13) }
|
||||
$0.catch(fromNever(Just<Int>.self))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ final class CatchTests: XCTestCase {
|
||||
testReceiveCompletionBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([]),
|
||||
{ $0.catch { _ in Just(13) } }
|
||||
{ $0.catch(fromNever(Just<Int>.self)) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -220,7 +220,7 @@ final class CatchTests: XCTestCase {
|
||||
|
||||
func testTryCatchReceiveValueBeforeSubscription() {
|
||||
testReceiveValueBeforeSubscription(value: 1, expected: .crash) {
|
||||
$0.tryCatch { _ in Just(13) }
|
||||
$0.tryCatch(fromNever(Just<Int>.self))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,7 +228,7 @@ final class CatchTests: XCTestCase {
|
||||
testReceiveCompletionBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([]),
|
||||
{ $0.tryCatch { _ in Just(13) } }
|
||||
{ $0.tryCatch(fromNever(Just<Int>.self)) }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -197,7 +197,7 @@ final class CollectByCountTests: XCTestCase {
|
||||
|
||||
func testCollectByCountCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([]),
|
||||
{ $0.collect(19) })
|
||||
}
|
||||
|
||||
|
||||
@@ -27,10 +27,19 @@ final class CollectTests: XCTestCase {
|
||||
{ $0.collect() })
|
||||
}
|
||||
|
||||
func testtestUpstreamFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Collect",
|
||||
expectedResult: [Int](),
|
||||
{ $0.collect() })
|
||||
func testUpstreamFinishesImmediatelyWithDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
|
||||
expectedSubscription: "Collect",
|
||||
expectedResult: [Int](),
|
||||
{ $0.collect() }
|
||||
)
|
||||
}
|
||||
|
||||
func testUpstreamFinishesImmediatelyWithoutDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
|
||||
expectedSubscription: "Collect",
|
||||
{ $0.collect() }
|
||||
)
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
@@ -70,13 +79,14 @@ final class CollectTests: XCTestCase {
|
||||
|
||||
func testCollectCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.collect() })
|
||||
}
|
||||
|
||||
func testCollectLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: false,
|
||||
{ $0.collect() })
|
||||
}
|
||||
|
||||
|
||||
@@ -336,7 +336,7 @@ final class CompactMapTests: XCTestCase {
|
||||
|
||||
func testTryCompactMapCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.cancelled]),
|
||||
{ $0.tryCompactMap(shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
|
||||
@@ -64,22 +64,52 @@ final class ComparisonTests: XCTestCase {
|
||||
{ $0.min(by: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
func testComparisonUpstreamFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Comparison",
|
||||
expectedResult: nil,
|
||||
{ $0.max() })
|
||||
func testComparisonUpstreamFinishesImmediatelyWithDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
|
||||
expectedSubscription: "Comparison",
|
||||
expectedResult: nil,
|
||||
{ $0.max() }
|
||||
)
|
||||
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Comparison",
|
||||
expectedResult: nil,
|
||||
{ $0.min() })
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
|
||||
expectedSubscription: "Comparison",
|
||||
expectedResult: nil,
|
||||
{ $0.min() }
|
||||
)
|
||||
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Comparison",
|
||||
expectedResult: nil,
|
||||
{ $0.max(by: shouldNotBeCalled()) })
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
|
||||
expectedSubscription: "Comparison",
|
||||
expectedResult: nil,
|
||||
{ $0.max(by: shouldNotBeCalled()) }
|
||||
)
|
||||
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Comparison",
|
||||
expectedResult: nil,
|
||||
{ $0.min(by: shouldNotBeCalled()) })
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
|
||||
expectedSubscription: "Comparison",
|
||||
expectedResult: nil,
|
||||
{ $0.min(by: shouldNotBeCalled()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testComparisonUpstreamFinishesImmediatelyWithoutDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
|
||||
expectedSubscription: "Comparison",
|
||||
{ $0.max() }
|
||||
)
|
||||
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
|
||||
expectedSubscription: "Comparison",
|
||||
{ $0.min() }
|
||||
)
|
||||
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
|
||||
expectedSubscription: "Comparison",
|
||||
{ $0.max(by: shouldNotBeCalled()) }
|
||||
)
|
||||
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
|
||||
expectedSubscription: "Comparison",
|
||||
{ $0.min(by: shouldNotBeCalled()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testComparisonCancelAlreadyCancelled() throws {
|
||||
@@ -185,29 +215,31 @@ final class ComparisonTests: XCTestCase {
|
||||
|
||||
func testComparisonCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.min() })
|
||||
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.max() })
|
||||
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.min(by: shouldNotBeCalled()) })
|
||||
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.max(by: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
func testComparisonLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: false,
|
||||
{ $0.min(by: >) })
|
||||
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: false,
|
||||
{ $0.max(by: >) })
|
||||
}
|
||||
|
||||
@@ -272,14 +304,30 @@ final class ComparisonTests: XCTestCase {
|
||||
{ $0.tryMin(by: >) })
|
||||
}
|
||||
|
||||
func testTryComparisonUpstreamFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "TryComparison",
|
||||
expectedResult: nil,
|
||||
{ $0.tryMax(by: >) })
|
||||
func testTryComparisonUpstreamFinishesImmediatelyWithDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
|
||||
expectedSubscription: "TryComparison",
|
||||
expectedResult: nil,
|
||||
{ $0.tryMax(by: >) }
|
||||
)
|
||||
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "TryComparison",
|
||||
expectedResult: nil,
|
||||
{ $0.tryMin(by: >) })
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
|
||||
expectedSubscription: "TryComparison",
|
||||
expectedResult: nil,
|
||||
{ $0.tryMin(by: >) }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryComparisonUpstreamFinishesImmediatelyWithoutDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
|
||||
expectedSubscription: "TryComparison",
|
||||
{ $0.tryMax(by: >) }
|
||||
)
|
||||
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
|
||||
expectedSubscription: "TryComparison",
|
||||
{ $0.tryMin(by: >) }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryComparisonCancelAlreadyCancelled() throws {
|
||||
@@ -347,21 +395,23 @@ final class ComparisonTests: XCTestCase {
|
||||
|
||||
func testTryComparisonCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.tryMin(by: shouldNotBeCalled()) })
|
||||
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.tryMax(by: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
func testTryComparisonLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: false,
|
||||
{ $0.tryMin(by: >) })
|
||||
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: false,
|
||||
{ $0.tryMax(by: >) })
|
||||
}
|
||||
|
||||
|
||||
@@ -41,11 +41,19 @@ final class ContainsTests: XCTestCase {
|
||||
{ $0.contains(0) })
|
||||
}
|
||||
|
||||
func testContainsUpstreamFinishesImmediately() {
|
||||
ReduceTests
|
||||
.testUpstreamFinishesImmediately(expectedSubscription: "Contains",
|
||||
expectedResult: false,
|
||||
{ $0.contains(0) })
|
||||
func testContainsUpstreamFinishesImmediatelyWithDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
|
||||
expectedSubscription: "Contains",
|
||||
expectedResult: false,
|
||||
{ $0.contains(0) }
|
||||
)
|
||||
}
|
||||
|
||||
func testContainsUpstreamFinishesImmediatelyWithoutDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
|
||||
expectedSubscription: "Contains",
|
||||
{ $0.contains(0) }
|
||||
)
|
||||
}
|
||||
|
||||
func testContainsCancelAlreadyCancelled() throws {
|
||||
@@ -92,13 +100,14 @@ final class ContainsTests: XCTestCase {
|
||||
|
||||
func testContainsCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.contains(0) })
|
||||
}
|
||||
|
||||
func testContainsLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: false,
|
||||
{ $0.contains(31) })
|
||||
}
|
||||
|
||||
@@ -142,12 +151,18 @@ final class ContainsTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
func testContainsWhereUpstreamFinishesImmediately() {
|
||||
ReduceTests
|
||||
.testUpstreamFinishesImmediately(
|
||||
expectedSubscription: "ContainsWhere",
|
||||
expectedResult: false,
|
||||
{ $0.contains(where: shouldNotBeCalled()) }
|
||||
func testContainsWhereUpstreamFinishesImmediatelyWithDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
|
||||
expectedSubscription: "ContainsWhere",
|
||||
expectedResult: false,
|
||||
{ $0.contains(where: shouldNotBeCalled()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testContainsWhereUpstreamFinishesImmediatelyWithoutDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
|
||||
expectedSubscription: "ContainsWhere",
|
||||
{ $0.contains(where: shouldNotBeCalled()) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -199,13 +214,14 @@ final class ContainsTests: XCTestCase {
|
||||
|
||||
func testContainsWhereCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.contains(where: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
func testContainsWhereLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: false,
|
||||
{ $0.contains { _ in true } })
|
||||
}
|
||||
|
||||
@@ -264,11 +280,19 @@ final class ContainsTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
func testTryContainsWhereUpstreamFinishesImmediately() {
|
||||
ReduceTests .testUpstreamFinishesImmediately(
|
||||
func testTryContainsWhereUpstreamFinishesImmediatelyWithDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
|
||||
expectedSubscription: "TryContainsWhere",
|
||||
expectedResult: false,
|
||||
{ $0.tryContains(where: shouldNotBeCalled()) })
|
||||
{ $0.tryContains(where: shouldNotBeCalled()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryContainsWhereUpstreamFinishesImmediatelyWithWithoutDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
|
||||
expectedSubscription: "TryContainsWhere",
|
||||
{ $0.tryContains(where: shouldNotBeCalled()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryContainsWhereCancelAlreadyCancelled() throws {
|
||||
@@ -325,13 +349,14 @@ final class ContainsTests: XCTestCase {
|
||||
|
||||
func testTryContainsWhereCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.tryContains(where: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
func testTryContainsWhereLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: false,
|
||||
{ $0.tryContains { _ in true } })
|
||||
}
|
||||
|
||||
|
||||
@@ -27,10 +27,19 @@ final class CountTests: XCTestCase {
|
||||
{ $0.count() })
|
||||
}
|
||||
|
||||
func testtestUpstreamFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Count",
|
||||
expectedResult: 0,
|
||||
{ $0.count() })
|
||||
func testUpstreamFinishesImmediatelyWithDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
|
||||
expectedSubscription: "Count",
|
||||
expectedResult: 0,
|
||||
{ $0.count() }
|
||||
)
|
||||
}
|
||||
|
||||
func testUpstreamFinishesImmediatelyWithoutDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
|
||||
expectedSubscription: "Count",
|
||||
{ $0.count() }
|
||||
)
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
@@ -70,13 +79,14 @@ final class CountTests: XCTestCase {
|
||||
|
||||
func testCountCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.count() })
|
||||
}
|
||||
|
||||
func testCountLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: false,
|
||||
{ $0.count() })
|
||||
}
|
||||
|
||||
|
||||
@@ -593,7 +593,7 @@ final class DebounceTests: XCTestCase {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
testCancelBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([]),
|
||||
{ $0.timeout(.nanoseconds(13), scheduler: scheduler) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -440,7 +440,7 @@ final class DelayTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testDelayCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self, shouldCrash: false) {
|
||||
testCancelBeforeSubscription(inputType: Int.self, expected: .history([])) {
|
||||
$0.delay(for: 0.35, scheduler: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ final class DropTests: XCTestCase {
|
||||
|
||||
func testDropCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([]),
|
||||
{ $0.dropFirst(0) })
|
||||
}
|
||||
|
||||
|
||||
@@ -319,7 +319,7 @@ final class DropUntilOutputTests: XCTestCase {
|
||||
|
||||
func testDropUntilOutputCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.cancelled]),
|
||||
{ $0.drop(untilOutputFrom: Empty<Int, Never>()) })
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ final class DropWhileTests: XCTestCase {
|
||||
|
||||
func testDropWhileCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([]),
|
||||
{ $0.drop(while: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ final class DropWhileTests: XCTestCase {
|
||||
|
||||
func testTryDropWhileCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([]),
|
||||
{ $0.tryDrop(while: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ final class EncodeTests: XCTestCase {
|
||||
|
||||
func testEncodeCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([]),
|
||||
{ $0.encode(encoder: encoder) })
|
||||
}
|
||||
|
||||
@@ -258,7 +258,7 @@ final class EncodeTests: XCTestCase {
|
||||
|
||||
func testDecodeCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([]),
|
||||
{ $0.decode(type: String.self, decoder: decoder) })
|
||||
}
|
||||
|
||||
|
||||
@@ -309,7 +309,7 @@ final class FilterTests: XCTestCase {
|
||||
|
||||
func testTryFilterCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.cancelled]),
|
||||
{ $0.tryFilter(shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
|
||||
@@ -77,11 +77,19 @@ final class FirstTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func testFirstFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "First",
|
||||
expectedResult: nil) {
|
||||
$0.first()
|
||||
}
|
||||
func testFirstFinishesImmediatelyWithDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
|
||||
expectedSubscription: "First",
|
||||
expectedResult: nil,
|
||||
{ $0.first() }
|
||||
)
|
||||
}
|
||||
|
||||
func testFirstFinishesImmediatelyWithoutDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
|
||||
expectedSubscription: "First",
|
||||
{ $0.first() }
|
||||
)
|
||||
}
|
||||
|
||||
func testFirstRequestsUnlimitedThenSendsSubscription() {
|
||||
@@ -118,13 +126,14 @@ final class FirstTests: XCTestCase {
|
||||
|
||||
func testFirstCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.first() })
|
||||
}
|
||||
|
||||
func testFirstLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: false,
|
||||
{ $0.first() })
|
||||
}
|
||||
|
||||
@@ -222,11 +231,19 @@ final class FirstTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func testFirstWhereFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "TryFirst",
|
||||
expectedResult: nil) {
|
||||
$0.first(where: { $0 > 2 })
|
||||
}
|
||||
func testFirstWhereFinishesImmediatelyWithDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
|
||||
expectedSubscription: "TryFirst",
|
||||
expectedResult: nil,
|
||||
{ $0.first(where: { $0 > 2 }) }
|
||||
)
|
||||
}
|
||||
|
||||
func testFirstWhereFinishesImmediatelyWithoutDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
|
||||
expectedSubscription: "TryFirst",
|
||||
{ $0.first(where: { $0 > 2 }) }
|
||||
)
|
||||
}
|
||||
|
||||
func testFirstWhereRequestsUnlimitedThenSendsSubscription() {
|
||||
@@ -271,13 +288,14 @@ final class FirstTests: XCTestCase {
|
||||
|
||||
func testFirstWhereCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.first(where: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
func testFirstWhereLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: false,
|
||||
{ $0.first { $0 > 1 } })
|
||||
}
|
||||
|
||||
@@ -369,12 +387,19 @@ final class FirstTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func testTryFirstWhereFinishesImmediately() {
|
||||
ReduceTests
|
||||
.testUpstreamFinishesImmediately(expectedSubscription: "TryFirstWhere",
|
||||
expectedResult: nil) {
|
||||
$0.tryFirst(where: { $0 > 2 })
|
||||
}
|
||||
func testTryFirstWhereFinishesImmediatelyWithDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
|
||||
expectedSubscription: "TryFirstWhere",
|
||||
expectedResult: nil,
|
||||
{ $0.tryFirst(where: { $0 > 2 }) }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryFirstWhereFinishesImmediatelyWithoutDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
|
||||
expectedSubscription: "TryFirstWhere",
|
||||
{ $0.tryFirst(where: { $0 > 2 }) }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryFirstWhereRequestsUnlimitedThenSendsSubscription() {
|
||||
@@ -439,13 +464,14 @@ final class FirstTests: XCTestCase {
|
||||
|
||||
func testTryFirstWhereCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.tryFirst(where: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
func testTryFirstWhereLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: false,
|
||||
{ $0.tryFirst { $0 > 1 } })
|
||||
}
|
||||
|
||||
|
||||
@@ -589,7 +589,7 @@ final class FlatMapTests: XCTestCase {
|
||||
createSut: { $0.flatMap(maxPublishers: .max(1)) { $0 } }
|
||||
)
|
||||
|
||||
child.willSubscribe = { subscriber, _ in
|
||||
child.willSubscribe = { _, _ in
|
||||
helper.publisher.send(completion: .finished)
|
||||
}
|
||||
|
||||
@@ -643,6 +643,31 @@ final class FlatMapTests: XCTestCase {
|
||||
XCTAssertEqual(childSubscription2.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
func testCrashesWhenUpstreamFailsDuringChildCancellation() {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisherBase<CustomPublisher, TestingError>.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.flatMap { $0 } }
|
||||
)
|
||||
|
||||
let childSubscription = CustomSubscription()
|
||||
let child = CustomPublisher(subscription: childSubscription)
|
||||
|
||||
var counter = 0
|
||||
childSubscription.onCancel = {
|
||||
if counter >= 5 { return }
|
||||
counter += 1
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
}
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(child), .none)
|
||||
|
||||
assertCrashes {
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
}
|
||||
}
|
||||
|
||||
func testDoesNotCompleteWithBufferedValues() {
|
||||
let upstreamPublisher = PassthroughSubject<Void, Never>()
|
||||
|
||||
@@ -1043,6 +1068,7 @@ final class FlatMapTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
@available(macOS 11.0, iOS 14.0, *)
|
||||
func testOverloadWhenUpstreamNeverFailsButChildrenCanFail() {
|
||||
let child = CustomPublisher(subscription: nil)
|
||||
let helper = OperatorTestHelper(
|
||||
@@ -1056,6 +1082,7 @@ final class FlatMapTests: XCTestCase {
|
||||
XCTAssertEqual(helper.sut.transform(0), child)
|
||||
}
|
||||
|
||||
@available(macOS 11.0, iOS 14.0, *)
|
||||
func testOverloadWhenUpstreamCanFailButChildrenNeverFail() {
|
||||
let child = CustomPublisherBase<Int, Never>(subscription: nil)
|
||||
|
||||
|
||||
@@ -218,9 +218,10 @@ final class HandleEventsTests: XCTestCase {
|
||||
func testHandleEventsCancelBeforeSubscription() {
|
||||
var history = [Event<Never>]()
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([]),
|
||||
{ $0.handleAllEvents { history.append($0) } })
|
||||
XCTAssertEqual(history, [.receiveCancel])
|
||||
XCTAssertEqual(history, [.receiveCancel,
|
||||
.receiveSubscription("CustomSubscription")])
|
||||
}
|
||||
|
||||
func testHandleEventsReflection() throws {
|
||||
|
||||
@@ -256,7 +256,10 @@ final class JustTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testMapErrorOperatorSpecialization() {
|
||||
XCTAssertEqual(try Sut(42).mapError { _ in TestingError.oops }.result.get(), 42)
|
||||
XCTAssertEqual(
|
||||
try Sut(42).mapError(fromNever(TestingError.self)).result.get(),
|
||||
42
|
||||
)
|
||||
}
|
||||
|
||||
func testReplaceErrorOperatorSpecialization() {
|
||||
|
||||
@@ -49,11 +49,19 @@ final class LastTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func testLastFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Last",
|
||||
expectedResult: nil) {
|
||||
$0.last()
|
||||
}
|
||||
func testLastFinishesImmediatelyWithDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
|
||||
expectedSubscription: "Last",
|
||||
expectedResult: nil,
|
||||
{ $0.last() }
|
||||
)
|
||||
}
|
||||
|
||||
func testLastFinishesImmediatelyWithoutDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
|
||||
expectedSubscription: "Last",
|
||||
{ $0.last() }
|
||||
)
|
||||
}
|
||||
|
||||
func testLastRequestsUnlimitedThenSendsSubscription() {
|
||||
@@ -90,13 +98,14 @@ final class LastTests: XCTestCase {
|
||||
|
||||
func testLastCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.last() })
|
||||
}
|
||||
|
||||
func testLastLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: false,
|
||||
{ $0.last() })
|
||||
}
|
||||
|
||||
@@ -195,11 +204,19 @@ final class LastTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func testLastWhereFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "LastWhere",
|
||||
expectedResult: nil) {
|
||||
$0.last(where: { $0 > 2 })
|
||||
}
|
||||
func testLastWhereFinishesImmediatelyWithDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
|
||||
expectedSubscription: "LastWhere",
|
||||
expectedResult: nil,
|
||||
{ $0.last(where: { $0 > 2 }) }
|
||||
)
|
||||
}
|
||||
|
||||
func testLastWhereFinishesImmediatelyWithoutDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
|
||||
expectedSubscription: "LastWhere",
|
||||
{ $0.last(where: { $0 > 2 }) }
|
||||
)
|
||||
}
|
||||
|
||||
func testLastWhereRequestsUnlimitedThenSendsSubscription() {
|
||||
@@ -244,13 +261,14 @@ final class LastTests: XCTestCase {
|
||||
|
||||
func testLastWhereCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.last(where: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
func testLastWhereLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: false,
|
||||
{ $0.last { $0 > 1 } })
|
||||
}
|
||||
|
||||
@@ -350,12 +368,19 @@ final class LastTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func testTryLastWhereFinishesImmediately() {
|
||||
ReduceTests
|
||||
.testUpstreamFinishesImmediately(expectedSubscription: "TryLastWhere",
|
||||
expectedResult: nil) {
|
||||
$0.tryLast(where: { $0 > 2 })
|
||||
}
|
||||
func testTryLastWhereFinishesImmediatelyWithDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
|
||||
expectedSubscription: "TryLastWhere",
|
||||
expectedResult: nil,
|
||||
{ $0.tryLast(where: { $0 > 2 }) }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryLastWhereFinishesImmediatelyWithoutDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
|
||||
expectedSubscription: "TryLastWhere",
|
||||
{ $0.tryLast(where: { $0 > 2 }) }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryLastWhereRequestsUnlimitedThenSendsSubscription() {
|
||||
@@ -420,13 +445,14 @@ final class LastTests: XCTestCase {
|
||||
|
||||
func testTryLastWhereCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.tryLast(where: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
func testTryLastWhereLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: false,
|
||||
{ $0.tryLast { $0 > 1 } })
|
||||
}
|
||||
|
||||
|
||||
@@ -279,7 +279,7 @@ final class MapTests: XCTestCase {
|
||||
|
||||
func testTryMapCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([]),
|
||||
{ $0.tryMap(shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
|
||||
@@ -142,9 +142,11 @@ final class MeasureIntervalTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testMeasureIntervalCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self, shouldCrash: false) {
|
||||
$0.measureInterval(using: ImmediateScheduler.shared)
|
||||
}
|
||||
testCancelBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([]),
|
||||
{ $0.measureInterval(using: ImmediateScheduler.shared) }
|
||||
)
|
||||
}
|
||||
|
||||
func testMeasureIntervalReflection() throws {
|
||||
|
||||
@@ -13,7 +13,7 @@ import Combine
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
@available(macOS 11.0, iOS 14.0, *)
|
||||
final class OptionalPublisherTests: XCTestCase {
|
||||
|
||||
private typealias Sut<Output> = OptionalPublisher<Output>
|
||||
@@ -338,8 +338,6 @@ final class OptionalPublisherTests: XCTestCase {
|
||||
XCTAssertEqual(Sut<Int>(12).output(in: ...0), Sut(12))
|
||||
XCTAssertEqual(Sut<Int>(12).output(in: ..<0), Sut(nil))
|
||||
XCTAssertEqual(Sut<Int>(12).output(in: ..<1), Sut(12))
|
||||
XCTAssertEqual(Sut<Int>(12).output(in: Range(uncheckedBounds: (0, -1))), Sut(12))
|
||||
XCTAssertEqual(Sut<Int>(12).output(in: Range(uncheckedBounds: (1, -1))), Sut(nil))
|
||||
|
||||
let trackingRange = TrackingRangeExpression(0 ..< 10)
|
||||
_ = Sut<Int>(12).output(in: trackingRange)
|
||||
@@ -385,10 +383,10 @@ final class OptionalPublisherTests: XCTestCase {
|
||||
}
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
@available(macOS 11.0, iOS 14.0, *)
|
||||
typealias OptionalPublisher<Output> = Optional<Output>.Publisher
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
@available(macOS 11.0, iOS 14.0, *)
|
||||
func makePublisher<Output>(_ optional: Output?) -> OptionalPublisher<Output> {
|
||||
return optional.publisher
|
||||
}
|
||||
|
||||
@@ -202,7 +202,7 @@ final class OutputTests: XCTestCase {
|
||||
|
||||
func testOutputCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([]),
|
||||
{ $0.output(in: 0 ..< 3) })
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
//
|
||||
// PrefixUntilOutputTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 08.11.2020.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class PrefixUntilOutputTests: XCTestCase {
|
||||
|
||||
func testBasicBehavior() {
|
||||
let terminatingSubscription = CustomSubscription()
|
||||
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
|
||||
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(3),
|
||||
receiveValueDemand: .max(2),
|
||||
createSut: { $0.prefix(untilOutputFrom: terminatingPublisher) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history,
|
||||
[.subscription(.contains("PrefixUntilOutput"))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(3))])
|
||||
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .max(2))
|
||||
XCTAssertEqual(helper.publisher.send(2), .max(2))
|
||||
|
||||
XCTAssertEqual(terminatingPublisher.send(1000), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history,
|
||||
[.subscription(.contains("PrefixUntilOutput")),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(3)), .cancelled])
|
||||
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
XCTAssertEqual(terminatingPublisher.send(1001), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history,
|
||||
[.subscription(.contains("PrefixUntilOutput")),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(3)), .cancelled])
|
||||
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
|
||||
|
||||
helper.publisher.send(completion: .finished)
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history,
|
||||
[.subscription(.contains("PrefixUntilOutput")),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(3)), .cancelled])
|
||||
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
|
||||
}
|
||||
|
||||
func testCombineIdentifiers() {
|
||||
let terminatingSubscription = CustomSubscription()
|
||||
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
|
||||
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(3),
|
||||
receiveValueDemand: .max(2),
|
||||
createSut: { $0.prefix(untilOutputFrom: terminatingPublisher) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(terminatingPublisher.subscriber?.combineIdentifier,
|
||||
helper.publisher.subscriber?.combineIdentifier)
|
||||
}
|
||||
|
||||
func testRequestZeroDemand() throws {
|
||||
let terminatingSubscription = CustomSubscription()
|
||||
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
|
||||
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(2),
|
||||
createSut: { $0.prefix(untilOutputFrom: terminatingPublisher) }
|
||||
)
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.none)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.none)])
|
||||
}
|
||||
|
||||
func testCancellation() throws {
|
||||
let terminatingSubscription = CustomSubscription()
|
||||
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
|
||||
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(2),
|
||||
createSut: { $0.prefix(untilOutputFrom: terminatingPublisher) }
|
||||
)
|
||||
|
||||
terminatingSubscription.onCancel = {
|
||||
XCTAssertEqual(helper.subscription.history,
|
||||
[.cancelled],
|
||||
"Upstream subscription should be cancelled first")
|
||||
}
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.cancelled])
|
||||
XCTAssertEqual(terminatingSubscription.history,
|
||||
[.requested(.max(1)), .cancelled])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(42))
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.cancelled])
|
||||
XCTAssertEqual(terminatingSubscription.history,
|
||||
[.requested(.max(1)), .cancelled])
|
||||
}
|
||||
|
||||
func testUpstreamCompletion() {
|
||||
let terminatingSubscription = CustomSubscription()
|
||||
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
|
||||
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(2),
|
||||
createSut: { $0.prefix(untilOutputFrom: terminatingPublisher) }
|
||||
)
|
||||
|
||||
helper.tracking.onFinish = {
|
||||
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1)),
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history,
|
||||
[.subscription(.contains("PrefixUntilOutput")),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1)),
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
func testCancelsUpstreamWhenTerminatorSendsValue() {
|
||||
let terminatingSubscription = CustomSubscription()
|
||||
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
|
||||
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(2),
|
||||
createSut: { $0.prefix(untilOutputFrom: terminatingPublisher) }
|
||||
)
|
||||
|
||||
helper.tracking.onFinish = {
|
||||
XCTAssertEqual(helper.subscription.history, [.cancelled])
|
||||
}
|
||||
|
||||
XCTAssertEqual(terminatingPublisher.send(42), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history,
|
||||
[.subscription(.contains("PrefixUntilOutput")),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.cancelled])
|
||||
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
|
||||
}
|
||||
|
||||
func testTerminatorFinishesWithoutProducingValues() {
|
||||
testTerminatorCompletesWithoutProducingValues(completion: .finished)
|
||||
}
|
||||
|
||||
func testTerminatorFailsWithoutProducingValues() {
|
||||
testTerminatorCompletesWithoutProducingValues(completion: .failure(.oops))
|
||||
}
|
||||
|
||||
private func testTerminatorCompletesWithoutProducingValues(
|
||||
completion: Subscribers.Completion<TestingError>
|
||||
) {
|
||||
let terminatingSubscription = CustomSubscription()
|
||||
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
|
||||
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(2),
|
||||
createSut: { $0.prefix(untilOutputFrom: terminatingPublisher) }
|
||||
)
|
||||
|
||||
terminatingPublisher.send(completion: completion)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(helper.tracking.history,
|
||||
[.subscription(.contains("PrefixUntilOutput"))])
|
||||
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
|
||||
|
||||
XCTAssertEqual(terminatingPublisher.send(42), .none)
|
||||
terminatingPublisher.send(completion: .finished)
|
||||
terminatingPublisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.publisher.send(1), .max(2))
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(helper.tracking.history,
|
||||
[.subscription(.contains("PrefixUntilOutput")), .value(1)])
|
||||
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
|
||||
}
|
||||
|
||||
func testTerminatorEmitsValueBeforeUpstreamSendsSubscription() {
|
||||
let terminatingSubscription = CustomSubscription()
|
||||
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
|
||||
|
||||
let upstream = CustomPublisher(subscription: nil)
|
||||
let tracking = TrackingSubscriber()
|
||||
upstream.prefix(untilOutputFrom: terminatingPublisher).subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(tracking.history, [])
|
||||
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
|
||||
|
||||
XCTAssertEqual(terminatingPublisher.send(-1), .none)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.completion(.finished)])
|
||||
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
|
||||
|
||||
let subscription = CustomSubscription()
|
||||
upstream.send(subscription: subscription)
|
||||
|
||||
XCTAssertEqual(subscription.history, [.cancelled])
|
||||
XCTAssertEqual(tracking.history, [.completion(.finished)])
|
||||
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
|
||||
}
|
||||
|
||||
func testPrefixUntilOutputReceiveValueBeforeSubscription() {
|
||||
let terminatingSubscription = CustomSubscription()
|
||||
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
|
||||
testReceiveValueBeforeSubscription(
|
||||
value: 31,
|
||||
expected: .history([], demand: .none),
|
||||
{ $0.prefix(untilOutputFrom: terminatingPublisher) }
|
||||
)
|
||||
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
|
||||
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
testReceiveValueBeforeSubscription(
|
||||
value: 31,
|
||||
expected: .history([.subscription(.contains("PrefixUntilOutput"))],
|
||||
demand: .none),
|
||||
{ publisher.prefix(untilOutputFrom: $0) }
|
||||
)
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
}
|
||||
|
||||
func testPrefixUntilOutputReceiveCompletionBeforeSubscription() {
|
||||
let terminatingSubscription = CustomSubscription()
|
||||
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
|
||||
testReceiveCompletionBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([]),
|
||||
{ $0.prefix(untilOutputFrom: terminatingPublisher) }
|
||||
)
|
||||
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1)), .cancelled])
|
||||
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
testReceiveCompletionBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([.subscription(.contains("PrefixUntilOutput"))]),
|
||||
{ publisher.prefix(untilOutputFrom: $0) }
|
||||
)
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
}
|
||||
|
||||
func testPrefixUntilOutputRequestBeforeSubscription() {
|
||||
let terminatingSubscription = CustomSubscription()
|
||||
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
|
||||
testRequestBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
{ $0.prefix(untilOutputFrom: terminatingPublisher) }
|
||||
)
|
||||
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
|
||||
}
|
||||
|
||||
func testPrefixUntilOutputCancelBeforeSubscription() {
|
||||
let terminatingSubscription = CustomSubscription()
|
||||
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
|
||||
testCancelBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([.cancelled]),
|
||||
{ $0.prefix(untilOutputFrom: terminatingPublisher) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1)), .cancelled])
|
||||
}
|
||||
|
||||
func testPrefixUntilOutputReceiveSubscriptionTwice() throws {
|
||||
let terminatingSubscription = CustomSubscription()
|
||||
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
|
||||
try testReceiveSubscriptionTwice {
|
||||
$0.prefix(untilOutputFrom: terminatingPublisher)
|
||||
}
|
||||
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1)), .cancelled])
|
||||
|
||||
do {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { publisher.prefix(untilOutputFrom: $0) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(1))])
|
||||
|
||||
let secondSubscription = CustomSubscription()
|
||||
|
||||
try XCTUnwrap(helper.publisher.subscriber)
|
||||
.receive(subscription: secondSubscription)
|
||||
|
||||
XCTAssertEqual(secondSubscription.history, [.cancelled])
|
||||
|
||||
try XCTUnwrap(helper.publisher.subscriber)
|
||||
.receive(subscription: helper.subscription)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(1)),
|
||||
.cancelled])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(1)),
|
||||
.cancelled,
|
||||
.cancelled])
|
||||
XCTAssertEqual(subscription.history, [.cancelled])
|
||||
}
|
||||
}
|
||||
|
||||
func testPrefixUntilOutputReflection() throws {
|
||||
// PrefixUntilOutput's Inner doesn't customize its reflection
|
||||
let terminatingPublisher = CustomPublisher(subscription: CustomSubscription())
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: nil,
|
||||
customMirror: nil,
|
||||
playgroundDescription: nil) {
|
||||
$0.prefix(untilOutputFrom: terminatingPublisher)
|
||||
}
|
||||
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: nil,
|
||||
customMirror: nil,
|
||||
playgroundDescription: nil) {
|
||||
publisher.prefix(untilOutputFrom: $0)
|
||||
}
|
||||
}
|
||||
|
||||
func testPrefixUntilOutputLifecycle() throws {
|
||||
let terminatingPublisher = CustomPublisher(subscription: CustomSubscription())
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: false,
|
||||
{ $0.prefix(untilOutputFrom: terminatingPublisher) })
|
||||
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: false,
|
||||
{ publisher.prefix(untilOutputFrom: $0) })
|
||||
}
|
||||
}
|
||||
@@ -91,7 +91,7 @@ final class PrefixWhileTests: XCTestCase {
|
||||
|
||||
func testPrefixWhileCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.cancelled]),
|
||||
{ $0.prefix(while: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ final class PrefixWhileTests: XCTestCase {
|
||||
|
||||
func testTryPrefixWhileCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.cancelled]),
|
||||
{ $0.tryPrefix(while: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ final class PrintTests: XCTestCase {
|
||||
|
||||
func testPrintCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([]),
|
||||
{ $0.print() })
|
||||
}
|
||||
|
||||
@@ -405,7 +405,6 @@ final class PrintTests: XCTestCase {
|
||||
}
|
||||
|
||||
private final class HistoryStream: TextOutputStream {
|
||||
|
||||
let output = Atomic([String]())
|
||||
|
||||
func write(_ string: String) {
|
||||
|
||||
@@ -366,7 +366,7 @@ final class ReceiveOnTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testReceiveOnCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self, shouldCrash: false) {
|
||||
testCancelBeforeSubscription(inputType: Int.self, expected: .history([])) {
|
||||
$0.receive(on: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,11 +30,19 @@ final class ReduceTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func testReduceFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Reduce",
|
||||
expectedResult: 1) {
|
||||
$0.reduce(1, *)
|
||||
}
|
||||
func testReduceFinishesImmediatelyWithDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
|
||||
expectedSubscription: "Reduce",
|
||||
expectedResult: 1,
|
||||
{ $0.reduce(1, *) }
|
||||
)
|
||||
}
|
||||
|
||||
func testReduceFinishesImmediatelyWithoutDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
|
||||
expectedSubscription: "Reduce",
|
||||
{ $0.reduce(1, *) }
|
||||
)
|
||||
}
|
||||
|
||||
func testReduceRequestsUnlimitedThenSendsSubscription() {
|
||||
@@ -71,13 +79,14 @@ final class ReduceTests: XCTestCase {
|
||||
|
||||
func testReduceCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.reduce(0, shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
func testReduceLifecycle() throws {
|
||||
try testLifecycle(sendValue: 42,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: false,
|
||||
{ $0.reduce(0, +) })
|
||||
}
|
||||
|
||||
@@ -118,11 +127,19 @@ final class ReduceTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func testTryReduceFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "TryReduce",
|
||||
expectedResult: 1) {
|
||||
$0.tryReduce(1, *)
|
||||
}
|
||||
func testTryReduceFinishesImmediatelyWithDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
|
||||
expectedSubscription: "TryReduce",
|
||||
expectedResult: 1,
|
||||
{ $0.tryReduce(1, *) }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryReduceFinishesImmediatelyWithoutDemand() {
|
||||
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
|
||||
expectedSubscription: "TryReduce",
|
||||
{ $0.tryReduce(1, *) }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryReduceRequestsUnlimitedThenSendsSubscription() {
|
||||
@@ -165,13 +182,14 @@ final class ReduceTests: XCTestCase {
|
||||
|
||||
func testTryReduceCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.tryReduce(0, shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
func testTryReduceLifecycle() throws {
|
||||
try testLifecycle(sendValue: 42,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: false,
|
||||
{ $0.tryReduce(0, +) })
|
||||
}
|
||||
|
||||
@@ -306,7 +324,7 @@ final class ReduceTests: XCTestCase {
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
static func testUpstreamFinishesImmediately<Operator: Publisher>(
|
||||
static func testUpstreamFinishesImmediatelyWithDemand<Operator: Publisher>(
|
||||
expectedSubscription: StringSubscription,
|
||||
expectedResult: Operator.Output?,
|
||||
_ makeOperator: (CustomPublisherBase<Int, Error>) -> Operator
|
||||
@@ -314,7 +332,7 @@ final class ReduceTests: XCTestCase {
|
||||
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisherBase<Int, Error>.self,
|
||||
initialDemand: nil, // Downstream should receive the result nonetheless
|
||||
initialDemand: .max(1),
|
||||
receiveValueDemand: .none,
|
||||
createSut: makeOperator
|
||||
)
|
||||
@@ -344,6 +362,38 @@ final class ReduceTests: XCTestCase {
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
static func testUpstreamFinishesImmediatelyWithoutDemand<Operator: Publisher>(
|
||||
expectedSubscription: StringSubscription,
|
||||
_ makeOperator: (CustomPublisherBase<Int, Error>) -> Operator
|
||||
) where Operator.Output: Equatable, Operator.Failure == Error {
|
||||
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisherBase<Int, Error>.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: makeOperator
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
let expectedHistory: [TrackingSubscriberBase<Operator.Output, Error>.Event] =
|
||||
[.subscription(expectedSubscription)]
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, expectedHistory)
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
helper.publisher.send(completion: .failure(TestingError.oops))
|
||||
XCTAssertEqual(helper.tracking.history, expectedHistory)
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(73), .none)
|
||||
XCTAssertEqual(helper.tracking.history, expectedHistory)
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
static func testCancelAlreadyCancelled<Operator: Publisher>(
|
||||
_ makeOperator: (CustomPublisherBase<Int, Error>) -> Operator
|
||||
) throws where Operator.Output: Equatable, Operator.Failure == Error {
|
||||
|
||||
@@ -137,7 +137,7 @@ final class RemoveDuplicatesTests: XCTestCase {
|
||||
|
||||
func testRemoveDuplicatesCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.cancelled]),
|
||||
{ $0.removeDuplicates(by: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
@@ -324,7 +324,7 @@ final class RemoveDuplicatesTests: XCTestCase {
|
||||
|
||||
func testTryRemoveDuplicatesCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([.cancelled]),
|
||||
{ $0.tryRemoveDuplicates(by: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
|
||||
@@ -326,9 +326,11 @@ final class ReplaceEmptyTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testReplaceEmptyCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self, shouldCrash: false) {
|
||||
$0.replaceEmpty(with: 1337)
|
||||
}
|
||||
testCancelBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([.requested(.unlimited)]),
|
||||
{ $0.replaceEmpty(with: 1337) }
|
||||
)
|
||||
}
|
||||
|
||||
func testReplaceEmptyLifecycle() throws {
|
||||
|
||||
@@ -246,7 +246,7 @@ final class ReplaceErrorTests: XCTestCase {
|
||||
|
||||
func testReplaceErrorCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([]),
|
||||
{ $0.replaceError(with: 0) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,411 @@
|
||||
//
|
||||
// RetryTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 11.07.2020.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class RetryTests: XCTestCase {
|
||||
|
||||
func testRetry3Times() throws {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let retry = publisher.retry(3)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
downstreamSubscription = $0
|
||||
$0.request(.max(5))
|
||||
}
|
||||
)
|
||||
var upstreamSubscribeCounter = 0
|
||||
publisher.didSubscribe = { _, _ in
|
||||
if upstreamSubscribeCounter == 0 {
|
||||
XCTAssertEqual(tracking.history, [.subscription("Retry")])
|
||||
}
|
||||
upstreamSubscribeCounter += 1
|
||||
}
|
||||
|
||||
retry.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(upstreamSubscribeCounter, 1)
|
||||
|
||||
XCTAssertEqual(publisher.send(1), .none)
|
||||
publisher.send(completion: .failure("oops1"))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Retry"),
|
||||
.value(1)])
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(5)),
|
||||
.requested(.max(4))])
|
||||
XCTAssertEqual(upstreamSubscribeCounter, 2)
|
||||
|
||||
XCTAssertEqual(publisher.send(2), .none)
|
||||
publisher.send(completion: .failure("oops2"))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Retry"),
|
||||
.value(1),
|
||||
.value(2)])
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(5)),
|
||||
.requested(.max(4)),
|
||||
.requested(.max(3))])
|
||||
XCTAssertEqual(upstreamSubscribeCounter, 3)
|
||||
|
||||
XCTAssertEqual(publisher.send(3), .none)
|
||||
publisher.send(completion: .failure("oops3"))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Retry"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(5)),
|
||||
.requested(.max(4)),
|
||||
.requested(.max(3)),
|
||||
.requested(.max(2))])
|
||||
XCTAssertEqual(upstreamSubscribeCounter, 4)
|
||||
|
||||
XCTAssertEqual(publisher.send(4), .none)
|
||||
publisher.send(completion: .failure("oops4"))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Retry"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.value(4),
|
||||
.completion(.failure("oops4"))])
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(5)),
|
||||
.requested(.max(4)),
|
||||
.requested(.max(3)),
|
||||
.requested(.max(2))])
|
||||
XCTAssertEqual(upstreamSubscribeCounter, 4)
|
||||
|
||||
XCTAssertEqual(publisher.send(5), .none)
|
||||
publisher.send(completion: .failure("oops5"))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Retry"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.value(4),
|
||||
.completion(.failure("oops4"))])
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(5)),
|
||||
.requested(.max(4)),
|
||||
.requested(.max(3)),
|
||||
.requested(.max(2))])
|
||||
XCTAssertEqual(upstreamSubscribeCounter, 4)
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(112))
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(5)),
|
||||
.requested(.max(4)),
|
||||
.requested(.max(3)),
|
||||
.requested(.max(2))])
|
||||
}
|
||||
|
||||
func testRetry0Times() {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(1),
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.retry(0) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Retry"),
|
||||
.value(1),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(1))])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Retry"),
|
||||
.value(1),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(1))])
|
||||
}
|
||||
|
||||
func testRetryForever() {
|
||||
testRetryForever(attempts: nil)
|
||||
}
|
||||
|
||||
func testRetryNegativeAmountOfTimes() {
|
||||
testRetryForever(attempts: -1)
|
||||
}
|
||||
|
||||
func testFinishSuccessfullyFirstTime() {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(2),
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.retry(3) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
helper.publisher.send(completion: .finished)
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Retry"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(2))])
|
||||
}
|
||||
|
||||
func testFinishSuccessfullyAfterRetry() {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(4),
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.retry(3) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
XCTAssertEqual(helper.publisher.send(4), .none)
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Retry"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.value(4),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(4)),
|
||||
.requested(.max(2))])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.retry(3) })
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Retry")])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(3))
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(42), .none)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.cancelled])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Retry")])
|
||||
}
|
||||
|
||||
func testPreservesDemand() {
|
||||
let publisher = CustomPublisher(subscription: nil)
|
||||
let retry = publisher.retry(5)
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.max(3)) },
|
||||
receiveValue: { .max($0) })
|
||||
retry.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(tracking.history, [])
|
||||
|
||||
let subscription = CustomSubscription()
|
||||
publisher.send(subscription: subscription)
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(3))])
|
||||
XCTAssertEqual(publisher.send(5), .none)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Retry"),
|
||||
.value(5)])
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(3)),
|
||||
.requested(.max(5))])
|
||||
}
|
||||
|
||||
func testCrashesOnUnwantedValue() {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.retry(2) }
|
||||
)
|
||||
|
||||
assertCrashes {
|
||||
_ = helper.publisher.send(-1)
|
||||
}
|
||||
}
|
||||
|
||||
func testSubscriptionRecursion() {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let retry = publisher.retry(5)
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.max(1)) })
|
||||
var upstreamSubscribeCounter = 0
|
||||
publisher.didSubscribe = { _, _ in
|
||||
if upstreamSubscribeCounter > 0 && upstreamSubscribeCounter < 10 {
|
||||
publisher.send(completion: .failure(.oops))
|
||||
}
|
||||
upstreamSubscribeCounter += 1
|
||||
}
|
||||
|
||||
retry.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Retry")])
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(1))])
|
||||
XCTAssertEqual(upstreamSubscribeCounter, 1)
|
||||
|
||||
publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Retry"),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(subscription.history,
|
||||
Array(repeating: .requested(.max(1)), count: 6))
|
||||
XCTAssertEqual(upstreamSubscribeCounter, 6)
|
||||
}
|
||||
|
||||
func testRecurseAndFinish() {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let retry = publisher.retry(5)
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.max(1)) })
|
||||
var upstreamSubscribeCounter = 0
|
||||
publisher.didSubscribe = { _, _ in
|
||||
if upstreamSubscribeCounter > 0 {
|
||||
if upstreamSubscribeCounter < 5 {
|
||||
publisher.send(completion: .failure(.oops))
|
||||
} else {
|
||||
publisher.send(completion: .finished)
|
||||
}
|
||||
}
|
||||
upstreamSubscribeCounter += 1
|
||||
}
|
||||
|
||||
retry.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Retry")])
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(1))])
|
||||
XCTAssertEqual(upstreamSubscribeCounter, 1)
|
||||
|
||||
publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Retry"),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(subscription.history,
|
||||
Array(repeating: .requested(.max(1)), count: 6))
|
||||
XCTAssertEqual(upstreamSubscribeCounter, 6)
|
||||
}
|
||||
|
||||
func testRecurseAndReceiveValue() {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let retry = publisher.retry(1)
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.max(3)) },
|
||||
receiveValue: { _ in .max(2) })
|
||||
var upstreamSubscribeCounter = 0
|
||||
publisher.willSubscribe = { _, _ in
|
||||
if upstreamSubscribeCounter > 0 {
|
||||
XCTAssertEqual(publisher.send(1), .none)
|
||||
}
|
||||
upstreamSubscribeCounter += 1
|
||||
}
|
||||
|
||||
retry.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Retry")])
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(3))])
|
||||
XCTAssertEqual(upstreamSubscribeCounter, 1)
|
||||
|
||||
publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Retry"),
|
||||
.value(1)])
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(3)),
|
||||
.requested(.max(4))])
|
||||
XCTAssertEqual(upstreamSubscribeCounter, 2)
|
||||
}
|
||||
|
||||
func testRetryReceiveValueBeforeSubscription() {
|
||||
testReceiveValueBeforeSubscription(
|
||||
value: 31,
|
||||
expected: .crash,
|
||||
{ $0.retry(3) }
|
||||
)
|
||||
}
|
||||
|
||||
func testRetryReceiveCompletionBeforeSubscription() {
|
||||
testReceiveCompletionBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([.completion(.finished)]),
|
||||
{ $0.retry(3) }
|
||||
)
|
||||
}
|
||||
|
||||
func testRetryRequestBeforeSubscription() {
|
||||
testRequestBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
{ $0.retry(3) })
|
||||
}
|
||||
|
||||
func testRetryCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([.cancelled]),
|
||||
{ $0.retry(3) }
|
||||
)
|
||||
}
|
||||
|
||||
func testRetryReceiveSubscriptionTwice() throws {
|
||||
try testReceiveSubscriptionTwice { $0.retry(3) }
|
||||
}
|
||||
|
||||
func testRetryLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: true,
|
||||
{ $0.retry(3) })
|
||||
}
|
||||
|
||||
func testRetryReflection() throws {
|
||||
try testReflection(
|
||||
parentInput: Int.self,
|
||||
parentFailure: TestingError.self,
|
||||
description: "Retry",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "Retry",
|
||||
{ $0.retry(3) }
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func testRetryForever(attempts: Int?) {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let retry = Publishers.Retry(upstream: publisher, retries: attempts)
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.max(1)) })
|
||||
var upstreamSubscribeCounter = 0
|
||||
publisher.didSubscribe = { _, _ in
|
||||
upstreamSubscribeCounter += 1
|
||||
}
|
||||
|
||||
retry.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(upstreamSubscribeCounter, 1)
|
||||
|
||||
for _ in 0 ..< 10000 {
|
||||
publisher.send(completion: .failure(.oops))
|
||||
}
|
||||
|
||||
XCTAssertEqual(publisher.send(0), .none)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Retry"),
|
||||
.value(0)])
|
||||
XCTAssertEqual(subscription.history,
|
||||
Array(repeating: .requested(.max(1)), count: 10001))
|
||||
XCTAssertEqual(upstreamSubscribeCounter, 10001)
|
||||
}
|
||||
}
|
||||
@@ -336,7 +336,7 @@ final class ScanTests: XCTestCase {
|
||||
|
||||
func testTryScanCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([]),
|
||||
{ $0.tryScan(0, shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
|
||||
@@ -311,7 +311,7 @@ final class SubscribeOnTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testSubscribeOnCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self, shouldCrash: false) {
|
||||
testCancelBeforeSubscription(inputType: Int.self, expected: .history([])) {
|
||||
$0.subscribe(on: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -736,6 +736,7 @@ final class SwitchToLatestTests: XCTestCase {
|
||||
XCTAssertEqual(nestedSubscription.history, [.requested(.max(1))])
|
||||
}
|
||||
|
||||
@available(macOS 11.0, iOS 14.0, *)
|
||||
func testOverloadWhenUpstreamNeverFailsButChildrenCanFail() {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisherBase<CustomPublisher, Never>.self,
|
||||
@@ -747,6 +748,7 @@ final class SwitchToLatestTests: XCTestCase {
|
||||
XCTAssertEqual(helper.sut.upstream.upstream, helper.publisher)
|
||||
}
|
||||
|
||||
@available(macOS 11.0, iOS 14.0, *)
|
||||
func testOverloadWhenUpstreamCanFailButChildrenNeverFail() {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisherBase<CustomPublisherBase<Int, Never>,
|
||||
|
||||
@@ -0,0 +1,844 @@
|
||||
//
|
||||
// ThrottleTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Stuart Austin on 14/11/2020.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class ThrottleTests: XCTestCase {
|
||||
|
||||
func testBasicBehavior() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let extractedExpr = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(100),
|
||||
receiveValueDemand: .max(12)) {
|
||||
$0.throttle(for: .seconds(1337), scheduler: scheduler, latest: true)
|
||||
}
|
||||
let helper = extractedExpr
|
||||
XCTAssertNotNil(helper.publisher.subscriber,
|
||||
"Subscription must be performed synchronously")
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(scheduler.history, [.now, // Subscriber created
|
||||
.now]) // Subscription received by Subscriber
|
||||
|
||||
// Send an initial value to the subject. This should be scheduled immediately
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
|
||||
XCTAssertEqual(scheduler.history, [.now,
|
||||
.now,
|
||||
// Checking the time when the Subscriber
|
||||
// receives the input "1"
|
||||
.now,
|
||||
// Scheduling the output
|
||||
// of the input immediately as we have not
|
||||
// output any values
|
||||
.schedule(options: nil)
|
||||
])
|
||||
|
||||
// Send some more values to the subject. Since we haven't run the scheduled
|
||||
// output above, these won't create any additional scheduled work
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(scheduler.scheduledDates, [.nanoseconds(0)])
|
||||
|
||||
XCTAssertEqual(scheduler.history, [.now,
|
||||
.now,
|
||||
.now,
|
||||
.schedule(options: nil),
|
||||
// Checking the time when the Subscriber
|
||||
// receives the input "2"
|
||||
.now,
|
||||
// Checking the time when the Subscriber
|
||||
// receives the input "3"
|
||||
.now])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
|
||||
// Expect only "3" to be output
|
||||
.value(3)])
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(scheduler.history, [.now,
|
||||
.now,
|
||||
.now,
|
||||
.schedule(options: nil),
|
||||
.now,
|
||||
.now,
|
||||
.now]) // Log the time of the output
|
||||
|
||||
// Send another value to the subject. This should be scheduled after the interval
|
||||
XCTAssertEqual(helper.publisher.send(4), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
|
||||
.value(3)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(scheduler.scheduledDates, [.seconds(1337)])
|
||||
|
||||
XCTAssertEqual(scheduler.history, [.now,
|
||||
.now,
|
||||
.now,
|
||||
.schedule(options: nil),
|
||||
.now,
|
||||
.now,
|
||||
.now,
|
||||
// Checking the time when the Subscriber
|
||||
// receives the input "4"
|
||||
.now,
|
||||
// When scheduling the output, it uses
|
||||
// the minimum tolerance
|
||||
.minimumTolerance,
|
||||
// Scheduling of the output
|
||||
.scheduleAfterDate(.seconds(1337),
|
||||
tolerance: .nanoseconds(7),
|
||||
options: nil)])
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
helper.publisher.send(completion: .finished)
|
||||
XCTAssertEqual(helper.publisher.send(5), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
|
||||
.value(3)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(scheduler.scheduledDates, [.seconds(1337)])
|
||||
XCTAssertEqual(scheduler.history, [.now,
|
||||
.now,
|
||||
.now,
|
||||
.schedule(options: nil),
|
||||
.now,
|
||||
.now,
|
||||
.now,
|
||||
.now,
|
||||
.minimumTolerance,
|
||||
.scheduleAfterDate(.seconds(1337),
|
||||
tolerance: .nanoseconds(7),
|
||||
options: nil),
|
||||
.now])
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
|
||||
.value(3),
|
||||
.value(4),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(scheduler.history, [.now,
|
||||
.now,
|
||||
.now,
|
||||
.schedule(options: nil),
|
||||
.now,
|
||||
.now,
|
||||
.now,
|
||||
.now,
|
||||
.minimumTolerance,
|
||||
.scheduleAfterDate(.seconds(1337),
|
||||
tolerance: .nanoseconds(7),
|
||||
options: nil),
|
||||
.now])
|
||||
XCTAssertEqual(scheduler.now, .seconds(1337))
|
||||
}
|
||||
|
||||
func testThrottleDemand() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let extractedExpr = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(2),
|
||||
receiveValueDemand: .none) {
|
||||
$0.throttle(for: .seconds(1337), scheduler: scheduler, latest: false)
|
||||
}
|
||||
let helper = extractedExpr
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(scheduler.history, [.now, // Subscriber created
|
||||
.now]) // Subscription received by Subscriber
|
||||
|
||||
// Send an initial value to the subject. This should be scheduled immediately
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
|
||||
XCTAssertEqual(scheduler.history, [.now,
|
||||
.now,
|
||||
// Checking the time when the Subscriber
|
||||
// receives the input "1"
|
||||
.now,
|
||||
// Scheduling the output of the input
|
||||
// immediately as we have not output any values
|
||||
.schedule(options: nil)])
|
||||
|
||||
// Send some more values to the subject.
|
||||
// Since we haven't run the scheduled output above, these won't create
|
||||
// any additional scheduled work
|
||||
XCTAssertEqual(helper.publisher.send(5), .none)
|
||||
XCTAssertEqual(helper.publisher.send(6), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(scheduler.scheduledDates, [.nanoseconds(0)])
|
||||
|
||||
XCTAssertEqual(scheduler.history, [.now,
|
||||
.now,
|
||||
.now,
|
||||
.schedule(options: nil),
|
||||
// Checking the time when the Subscriber
|
||||
// receives the input "5"
|
||||
.now,
|
||||
// Checking the time when the Subscriber
|
||||
// receives the input "6"
|
||||
.now])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
// Send a second value to the subject. This should be scheduled after the interval
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
|
||||
.value(1)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(scheduler.scheduledDates, [.seconds(1337)])
|
||||
|
||||
XCTAssertEqual(scheduler.history, [.now,
|
||||
.now,
|
||||
.now,
|
||||
.schedule(options: nil),
|
||||
.now,
|
||||
.now,
|
||||
.now,
|
||||
// Checking the time when the Subscriber
|
||||
// receives the input "2"
|
||||
.now,
|
||||
// When scheduling the output, it uses
|
||||
// the minimum tolerance
|
||||
.minimumTolerance,
|
||||
// Scheduling of the output
|
||||
.scheduleAfterDate(.seconds(1337),
|
||||
tolerance: .nanoseconds(7),
|
||||
options: nil)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
|
||||
.value(1),
|
||||
.value(2)])
|
||||
|
||||
// Send a third value to the subject.
|
||||
// This should not be output at all due to the demand
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
|
||||
.value(1),
|
||||
.value(2)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(scheduler.scheduledDates, [])
|
||||
|
||||
XCTAssertEqual(scheduler.history, [.now,
|
||||
.now,
|
||||
.now,
|
||||
.schedule(options: nil),
|
||||
.now,
|
||||
.now,
|
||||
.now,
|
||||
// Checking the time when the Subscriber
|
||||
// receives the input "4"
|
||||
.now,
|
||||
// When scheduling the output, it uses
|
||||
// the minimum tolerance
|
||||
.minimumTolerance,
|
||||
// Scheduling of the output
|
||||
.scheduleAfterDate(.seconds(1337),
|
||||
tolerance: .nanoseconds(7),
|
||||
options: nil),
|
||||
.now,
|
||||
.now])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
|
||||
.value(1),
|
||||
.value(2)])
|
||||
}
|
||||
|
||||
func testThrottleGap() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let extractedExpr = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none) {
|
||||
$0.throttle(for: .seconds(60), scheduler: scheduler, latest: false)
|
||||
}
|
||||
let helper = extractedExpr
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(scheduler.history, [.now,
|
||||
.now])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(0), .none)
|
||||
|
||||
XCTAssertEqual(scheduler.history, [.now,
|
||||
.now,
|
||||
.now,
|
||||
.schedule(options: nil)])
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(scheduler.scheduledDates, [.nanoseconds(0)])
|
||||
|
||||
XCTAssertEqual(scheduler.history, [.now,
|
||||
.now,
|
||||
.now,
|
||||
.schedule(options: nil)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(scheduler.history, [.now,
|
||||
.now,
|
||||
.now,
|
||||
.schedule(options: nil),
|
||||
.now])
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"), .value(0)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(scheduler.scheduledDates, [])
|
||||
|
||||
var future = scheduler.now + .seconds(45)
|
||||
|
||||
XCTAssertEqual(scheduler.history, [.now,
|
||||
.now,
|
||||
.now,
|
||||
.schedule(options: nil),
|
||||
.now,
|
||||
.now])
|
||||
|
||||
// change the current time to be 45 seconds into the future
|
||||
scheduler.rewind(to: future)
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
|
||||
XCTAssertEqual(scheduler.history, [.now,
|
||||
.now,
|
||||
.now,
|
||||
.schedule(options: nil),
|
||||
.now,
|
||||
.now,
|
||||
.now,
|
||||
.minimumTolerance,
|
||||
.scheduleAfterDate(.seconds(60),
|
||||
tolerance: .nanoseconds(7),
|
||||
options: nil)])
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"), .value(0)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
// next value should be emitted 60 seconds from the start of time
|
||||
XCTAssertEqual(scheduler.scheduledDates, [.seconds(60)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(scheduler.history, [.now,
|
||||
.now,
|
||||
.now,
|
||||
.schedule(options: nil),
|
||||
.now,
|
||||
.now,
|
||||
.now,
|
||||
.minimumTolerance,
|
||||
.scheduleAfterDate(.seconds(60),
|
||||
tolerance: .nanoseconds(7),
|
||||
options: nil),
|
||||
.now])
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
|
||||
.value(0),
|
||||
.value(1)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(scheduler.scheduledDates, [])
|
||||
|
||||
future = scheduler.now + .seconds(61)
|
||||
|
||||
XCTAssertEqual(scheduler.history, [.now,
|
||||
.now,
|
||||
.now,
|
||||
.schedule(options: nil),
|
||||
.now,
|
||||
.now,
|
||||
.now,
|
||||
.minimumTolerance,
|
||||
.scheduleAfterDate(.seconds(60),
|
||||
tolerance: .nanoseconds(7),
|
||||
options: nil),
|
||||
.now,
|
||||
.now])
|
||||
|
||||
// change the current time to be 61 seconds into the future
|
||||
scheduler.rewind(to: future)
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
|
||||
XCTAssertEqual(scheduler.history, [.now,
|
||||
.now,
|
||||
.now,
|
||||
.schedule(options: nil),
|
||||
.now,
|
||||
.now,
|
||||
.now,
|
||||
.minimumTolerance,
|
||||
.scheduleAfterDate(.seconds(60),
|
||||
tolerance: .nanoseconds(7),
|
||||
options: nil),
|
||||
.now,
|
||||
.now,
|
||||
.now,
|
||||
// next value should be scheduled immediately
|
||||
// as the interval has passed
|
||||
.schedule(options: nil)])
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
|
||||
.value(0),
|
||||
.value(1)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
// next value should be emitted 121 seconds from the start of time
|
||||
XCTAssertEqual(scheduler.scheduledDates, [.seconds(121)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(scheduler.history, [.now,
|
||||
.now,
|
||||
.now,
|
||||
.schedule(options: nil),
|
||||
.now,
|
||||
.now,
|
||||
.now,
|
||||
.minimumTolerance,
|
||||
.scheduleAfterDate(.seconds(60),
|
||||
tolerance: .nanoseconds(7),
|
||||
options: nil),
|
||||
.now,
|
||||
.now,
|
||||
.now,
|
||||
.schedule(options: nil),
|
||||
.now])
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
|
||||
.value(0),
|
||||
.value(1),
|
||||
.value(2)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(scheduler.scheduledDates, [])
|
||||
}
|
||||
|
||||
func testRequest() throws {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none) {
|
||||
$0.throttle(for: .seconds(10), scheduler: scheduler, latest: true)
|
||||
}
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(10))
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(4))
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(5))
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.none)
|
||||
XCTAssertEqual(helper.publisher.send(2000), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
|
||||
.value(2000)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none) {
|
||||
$0.throttle(for: .seconds(10), scheduler: scheduler, latest: true)
|
||||
}
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(42))
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
|
||||
XCTAssertEqual(scheduler.history, [.now, .now])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(0), .none)
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
|
||||
XCTAssertEqual(scheduler.history, [.now, .now])
|
||||
}
|
||||
|
||||
func testNoDemandReceivesNoValues() throws {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveValue: { _ in
|
||||
XCTFail("Unexpected value received")
|
||||
return .none
|
||||
}
|
||||
)
|
||||
|
||||
let throttle = publisher.throttle(for: .milliseconds(1),
|
||||
scheduler: ImmediateScheduler.shared,
|
||||
latest: true)
|
||||
throttle.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(publisher.send(1), .none)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
func testCancelWhileReceivingInput() throws {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
downstreamSubscription = $0
|
||||
$0.request(.unlimited)
|
||||
},
|
||||
receiveValue: { _ in
|
||||
XCTAssertNotNil(downstreamSubscription)
|
||||
downstreamSubscription?.cancel()
|
||||
return .max(42)
|
||||
}
|
||||
)
|
||||
|
||||
let throttle = publisher.throttle(for: .seconds(60),
|
||||
scheduler: scheduler,
|
||||
latest: true)
|
||||
throttle.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(scheduler.history, [.now, .now])
|
||||
|
||||
XCTAssertEqual(publisher.send(1), .none)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(scheduler.history, [.now, .now, .now, .schedule(options: nil)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Throttle"), .value(1)])
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited), .cancelled])
|
||||
XCTAssertEqual(scheduler.history, [.now,
|
||||
.now,
|
||||
.now,
|
||||
.schedule(options: nil),
|
||||
.now])
|
||||
}
|
||||
|
||||
func testCancelWhilstScheduledOutput() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
downstreamSubscription = $0
|
||||
$0.request(.unlimited)
|
||||
},
|
||||
receiveValue: { _ in
|
||||
XCTAssertNotNil(downstreamSubscription)
|
||||
return .max(42)
|
||||
}
|
||||
)
|
||||
|
||||
let throttle = publisher.throttle(for: .seconds(60),
|
||||
scheduler: scheduler,
|
||||
latest: true)
|
||||
throttle.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(scheduler.history, [.now, .now])
|
||||
|
||||
XCTAssertEqual(publisher.send(1), .none)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(scheduler.history, [.now, .now, .now, .schedule(options: nil)])
|
||||
|
||||
tracking.cancel()
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(tracking.history, [])
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited), .cancelled])
|
||||
XCTAssertEqual(scheduler.history, [.now,
|
||||
.now,
|
||||
.now,
|
||||
.schedule(options: nil)])
|
||||
}
|
||||
|
||||
func testReceiveCompletionImmediatelyAfterSubscription() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none) {
|
||||
$0.throttle(for: .seconds(60), scheduler: scheduler, latest: true)
|
||||
}
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(scheduler.history, [.now, .now, .now, .schedule(options: nil)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
func testReceiveCompletionImmediatelyAfterValue() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .max(418)) {
|
||||
$0.throttle(for: .seconds(60), scheduler: scheduler, latest: true)
|
||||
}
|
||||
XCTAssertEqual(helper.publisher.send(-1), .none)
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1000), .none)
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
|
||||
.value(-1)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(scheduler.history, [.now,
|
||||
.now,
|
||||
.now,
|
||||
.schedule(options: nil),
|
||||
.now,
|
||||
.now,
|
||||
.minimumTolerance,
|
||||
.scheduleAfterDate(.seconds(60),
|
||||
tolerance: .nanoseconds(7),
|
||||
options: nil),
|
||||
.now])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
|
||||
.value(-1),
|
||||
.value(1000),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
func testReceiveInputRecursively() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .max(418)) {
|
||||
$0.throttle(for: .seconds(60),
|
||||
scheduler: ImmediateScheduler.shared,
|
||||
latest: true)
|
||||
}
|
||||
|
||||
var recursionCounter = 5
|
||||
helper.tracking.onValue = { _ in
|
||||
if recursionCounter == 0 { return }
|
||||
recursionCounter -= 1
|
||||
_ = helper.publisher.send(-1)
|
||||
}
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(0), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
|
||||
.value(0),
|
||||
.value(-1),
|
||||
.value(-1),
|
||||
.value(-1),
|
||||
.value(-1),
|
||||
.value(-1)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
func testReceiveCompletionRecursively() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .max(418)) {
|
||||
$0.throttle(for: .seconds(60),
|
||||
scheduler: ImmediateScheduler.shared,
|
||||
latest: true)
|
||||
}
|
||||
helper.tracking.onFinish = {
|
||||
helper.publisher.send(completion: .finished)
|
||||
}
|
||||
helper.publisher.send(completion: .finished)
|
||||
}
|
||||
|
||||
func testWeakCaptureWhenSchedulingValue() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
var value: Int?
|
||||
var subscriberReleased = false
|
||||
do {
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
let throttle = publisher.throttle(for: .seconds(60),
|
||||
scheduler: scheduler,
|
||||
latest: true)
|
||||
let tracking =
|
||||
TrackingSubscriber(receiveSubscription: { $0.request(.unlimited) },
|
||||
receiveValue: { value = $0; return .none },
|
||||
onDeinit: { subscriberReleased = true })
|
||||
throttle.subscribe(tracking)
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
|
||||
XCTAssertEqual(publisher.send(42), .none)
|
||||
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
|
||||
XCTAssertEqual(scheduler.history, [.now, .now, .now, .schedule(options: nil)])
|
||||
tracking.cancel()
|
||||
publisher.cancel()
|
||||
}
|
||||
XCTAssertTrue(subscriberReleased)
|
||||
XCTAssertNil(value)
|
||||
}
|
||||
|
||||
func testWeakCaptureWhenSchedulingCompletion() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
var completion: Subscribers.Completion<TestingError>?
|
||||
var subscriberReleased = false
|
||||
do {
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
let throttle = publisher.throttle(for: .seconds(60),
|
||||
scheduler: scheduler,
|
||||
latest: true)
|
||||
let tracking = TrackingSubscriber(receiveCompletion: { completion = $0 },
|
||||
onDeinit: { subscriberReleased = true })
|
||||
throttle.subscribe(tracking)
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
|
||||
publisher.send(completion: .finished)
|
||||
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
|
||||
XCTAssertEqual(scheduler.history, [.now, .now, .now, .schedule(options: nil)])
|
||||
tracking.cancel()
|
||||
publisher.cancel()
|
||||
}
|
||||
XCTAssertTrue(subscriberReleased)
|
||||
XCTAssertNil(completion)
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertNil(completion)
|
||||
}
|
||||
|
||||
func testThrottleReceiveSubscriptionTwice() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none) {
|
||||
$0.throttle(for: .seconds(60),
|
||||
scheduler: ImmediateScheduler.shared,
|
||||
latest: true)
|
||||
}
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
let secondSubscription = CustomSubscription()
|
||||
|
||||
try XCTUnwrap(helper.publisher.subscriber)
|
||||
.receive(subscription: secondSubscription)
|
||||
|
||||
XCTAssertEqual(secondSubscription.history, [.cancelled])
|
||||
|
||||
try XCTUnwrap(helper.publisher.subscriber)
|
||||
.receive(subscription: helper.subscription)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.cancelled])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.cancelled,
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
func testThrottleReceiveValueBeforeSubscription() {
|
||||
testReceiveValueBeforeSubscription(value: 213,
|
||||
expected: .history([], demand: .none)) {
|
||||
$0.throttle(for: .seconds(60),
|
||||
scheduler: ImmediateScheduler.shared,
|
||||
latest: true)
|
||||
}
|
||||
}
|
||||
|
||||
func testThrottleReceiveCompletionBeforeSubscription() {
|
||||
testReceiveCompletionBeforeSubscription(inputType: Int.self,
|
||||
expected: .history([])) {
|
||||
$0.throttle(for: .seconds(60),
|
||||
scheduler: ImmediateScheduler.shared,
|
||||
latest: true)
|
||||
}
|
||||
}
|
||||
|
||||
func testThrottleRequestBeforeSubscription() {
|
||||
testRequestBeforeSubscription(inputType: Int.self, shouldCrash: false) {
|
||||
$0.throttle(for: .seconds(60),
|
||||
scheduler: ImmediateScheduler.shared,
|
||||
latest: true)
|
||||
}
|
||||
}
|
||||
|
||||
func testThrottleCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
expected: .history([.cancelled])) {
|
||||
$0.throttle(for: .seconds(60),
|
||||
scheduler: ImmediateScheduler.shared,
|
||||
latest: true)
|
||||
}
|
||||
}
|
||||
|
||||
func testThrottleReflection() throws {
|
||||
try testReflection(parentInput: String.self,
|
||||
parentFailure: Error.self,
|
||||
description: "Throttle",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "Throttle",
|
||||
{ $0.throttle(for: .seconds(60),
|
||||
scheduler: ImmediateScheduler.shared,
|
||||
latest: true) })
|
||||
}
|
||||
|
||||
func testThrottleLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: true) {
|
||||
$0.throttle(for: .seconds(60),
|
||||
scheduler: ImmediateScheduler.shared,
|
||||
latest: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -672,7 +672,7 @@ final class TimeoutTests: XCTestCase {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
testCancelBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
expected: .history([]),
|
||||
{ $0.timeout(.nanoseconds(13), scheduler: scheduler) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -197,6 +197,7 @@ final class SubscribersDemandTests: XCTestCase {
|
||||
XCTAssertEqual(Subscribers.Demand.unlimited.description, "unlimited")
|
||||
}
|
||||
|
||||
#if !WASI
|
||||
func testEncodeDecodeJSON() throws {
|
||||
try testEncodeDecode(
|
||||
encoder: JSONEncoder(),
|
||||
@@ -315,6 +316,8 @@ final class SubscribersDemandTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(decodedIllFormedTooBig.value.description, "unlimited")
|
||||
}
|
||||
|
||||
#endif // !WASI
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
|
||||
@@ -3,3 +3,7 @@ def suffix_variadic(name, index, arity):
|
||||
|
||||
def list_with_suffix_variadic(name, arity):
|
||||
return [suffix_variadic(name, i, arity) for i in range(arity)]
|
||||
|
||||
def indent(input, space_count):
|
||||
padding = space_count * ' '
|
||||
return ''.join(padding + line for line in input.splitlines(True))
|
||||
|
||||
Reference in New Issue
Block a user