Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 899a04bb3f | |||
| 5f9a700689 | |||
| a300fd09d3 | |||
| 5973f86c6e | |||
| 1b5afdba26 | |||
| 51d5d1e71d | |||
| a8bc5cc046 | |||
| 86d6170dc9 | |||
| 171131d768 | |||
| d6b4fb4115 | |||
| 014b82b99d |
@@ -1,28 +0,0 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = "COpenCombineHelpers"
|
||||
spec.version = "0.5.0"
|
||||
spec.summary = "C++ Helpers for OpenCombine"
|
||||
|
||||
spec.description = <<-DESC
|
||||
C++ helpers necessary for the implementation of OpenCombine
|
||||
DESC
|
||||
|
||||
spec.homepage = "https://github.com/broadwaylamb/OpenCombine/"
|
||||
spec.license = "MIT"
|
||||
|
||||
spec.authors = { "Sergej Jaskiewicz" => "jaskiewiczs@icloud.com" }
|
||||
spec.source = { :git => "https://github.com/broadwaylamb/OpenCombine.git", :tag => "#{spec.version}" }
|
||||
|
||||
spec.osx.deployment_target = "10.10"
|
||||
spec.ios.deployment_target = "8.0"
|
||||
spec.watchos.deployment_target = "2.0"
|
||||
spec.tvos.deployment_target = "9.0"
|
||||
|
||||
spec.header_mappings_dir = "Sources/COpenCombineHelpers/include"
|
||||
spec.source_files = "Sources/COpenCombineHelpers/**/*.{cpp,h}"
|
||||
spec.libraries = "c++"
|
||||
|
||||
spec.pod_target_xcconfig = {
|
||||
"DEFINES_MODULE" => "YES"
|
||||
}
|
||||
end
|
||||
+5
-3
@@ -1,6 +1,6 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = "OpenCombine"
|
||||
spec.version = "0.5.0"
|
||||
spec.version = "0.7.0"
|
||||
spec.summary = "Open source implementation of Apple's Combine framework for processing values over time."
|
||||
|
||||
spec.description = <<-DESC
|
||||
@@ -20,6 +20,8 @@ Pod::Spec.new do |spec|
|
||||
spec.watchos.deployment_target = "2.0"
|
||||
spec.tvos.deployment_target = "9.0"
|
||||
|
||||
spec.source_files = "Sources/OpenCombine/**/*.swift"
|
||||
spec.dependency "COpenCombineHelpers"
|
||||
spec.source_files = "Sources/COpenCombineHelpers/**/*.{h,cpp}", "Sources/OpenCombine/**/*.swift"
|
||||
spec.public_header_files = "Sources/COpenCombineHelpers/include/*.h"
|
||||
|
||||
spec.libraries = "c++"
|
||||
end
|
||||
@@ -1,10 +1,10 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = "OpenCombineDispatch"
|
||||
spec.version = "0.5.0"
|
||||
spec.summary = "OpenCombine Dispatching"
|
||||
spec.version = "0.7.0"
|
||||
spec.summary = "OpenCombine + Dispatch interoperability"
|
||||
|
||||
spec.description = <<-DESC
|
||||
Extends `DispatchQueue` with new methods and nested types.
|
||||
Extends `DispatchQueue` with conformance to the `Scheduler` protocol
|
||||
DESC
|
||||
|
||||
spec.homepage = "https://github.com/broadwaylamb/OpenCombine/"
|
||||
@@ -21,5 +21,5 @@ Pod::Spec.new do |spec|
|
||||
spec.tvos.deployment_target = "9.0"
|
||||
|
||||
spec.source_files = "Sources/OpenCombineDispatch/**/*.swift"
|
||||
spec.dependency "OpenCombine"
|
||||
spec.dependency "OpenCombine", '>= 0.6'
|
||||
end
|
||||
@@ -3,6 +3,7 @@
|
||||
[](https://codecov.io/gh/broadwaylamb/OpenCombine)
|
||||

|
||||

|
||||

|
||||
[<img src="https://img.shields.io/badge/slack-OpenCombine-yellow.svg?logo=slack">](https://join.slack.com/t/opencombine/shared_invite/enQtNzE2MjE5NzkxODI0LTYxMjkzNDUxZWViZWI1Njc2YjBhODgxNjRjOTdkZTcxOGU2ZjJjZjYxMGI3NWZkN2RkNGFmZTUzNmU3MGE2ZWM)
|
||||
|
||||
Open-source implementation of Apple's [Combine](https://developer.apple.com/documentation/combine) framework for processing values over time.
|
||||
@@ -22,7 +23,7 @@ To add `OpenCombine` to your [SPM](https://swift.org/package-manager/) package,
|
||||
|
||||
```swift
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/broadwaylamb/OpenCombine.git", from: "0.5.0")
|
||||
.package(url: "https://github.com/broadwaylamb/OpenCombine.git", from: "0.7.0")
|
||||
],
|
||||
targets: [
|
||||
.target(name: "MyAwesomePackage", dependencies: ["OpenCombine", "OpenCombineDispatch"])
|
||||
@@ -43,8 +44,8 @@ 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.5.0'
|
||||
pod 'OpenCombineDispatch', '~> 0.5.0'
|
||||
pod 'OpenCombine', '~> 0.7'
|
||||
pod 'OpenCombineDispatch', '~> 0.7'
|
||||
```
|
||||
|
||||
### Contributing
|
||||
|
||||
@@ -2,173 +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 elements from an upstream publisher on a specific scheduler.
|
||||
public struct SubscribeOn<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 scheduler the publisher should use to receive elements.
|
||||
public let scheduler: Context
|
||||
|
||||
/// Scheduler options that customize the delivery of elements.
|
||||
public let options: Context.SchedulerOptions?
|
||||
|
||||
public init(upstream: Upstream, scheduler: Context, options: Context.SchedulerOptions?)
|
||||
|
||||
/// 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 {
|
||||
|
||||
/// Specifies the scheduler on which to perform subscribe, cancel, and request operations.
|
||||
///
|
||||
/// In contrast with `receive(on:options:)`, which affects downstream messages, `subscribe(on:)` changes the execution context of upstream messages. In the following example, requests to `jsonPublisher` are performed on `backgroundQueue`, but elements received from it are performed on `RunLoop.main`.
|
||||
///
|
||||
/// let ioPerformingPublisher == // Some publisher.
|
||||
/// let uiUpdatingSubscriber == // Some subscriber that updates the UI.
|
||||
///
|
||||
/// ioPerformingPublisher
|
||||
/// .subscribe(on: backgroundQueue)
|
||||
/// .receiveOn(on: RunLoop.main)
|
||||
/// .subscribe(uiUpdatingSubscriber)
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - scheduler: The scheduler on which to receive upstream messages.
|
||||
/// - options: Options that customize the delivery of elements.
|
||||
/// - Returns: A publisher which performs upstream operations on the specified scheduler.
|
||||
public func subscribe<S>(on scheduler: S, options: S.SchedulerOptions? = nil) -> Publishers.SubscribeOn<Self, S> where S : Scheduler
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that measures and emits the time interval between events received from an upstream publisher.
|
||||
public struct MeasureInterval<Upstream, Context> : Publisher where Upstream : Publisher, Context : Scheduler {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Context.SchedulerTimeType.Stride
|
||||
|
||||
/// 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 scheduler on which to deliver elements.
|
||||
public let scheduler: Context
|
||||
|
||||
public init(upstream: Upstream, scheduler: Context)
|
||||
|
||||
/// 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, S.Input == Context.SchedulerTimeType.Stride
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Measures and emits the time interval between events received from an upstream publisher.
|
||||
///
|
||||
/// The output type of the returned scheduler is the time interval of the provided scheduler.
|
||||
/// - Parameters:
|
||||
/// - scheduler: The scheduler on which to deliver elements.
|
||||
/// - options: Options that customize the delivery of elements.
|
||||
/// - Returns: A publisher that emits elements representing the time interval between the elements it receives.
|
||||
public func measureInterval<S>(using scheduler: S, options: S.SchedulerOptions? = nil) -> Publishers.MeasureInterval<Self, S> where S : Scheduler
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that raises a debugger signal when a provided closure needs to stop the process in the debugger.
|
||||
///
|
||||
/// When any of the provided closures returns `true`, this publisher raises the `SIGTRAP` signal to stop the process in the debugger.
|
||||
/// Otherwise, this publisher passes through values and completions as-is.
|
||||
public struct Breakpoint<Upstream> : Publisher 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
|
||||
|
||||
/// A closure that executes when the publisher receives a subscription, and can raise a debugger signal by returning a true Boolean value.
|
||||
public let receiveSubscription: ((Subscription) -> Bool)?
|
||||
|
||||
/// A closure that executes when the publisher receives output from the upstream publisher, and can raise a debugger signal by returning a true Boolean value.
|
||||
public let receiveOutput: ((Upstream.Output) -> Bool)?
|
||||
|
||||
/// A closure that executes when the publisher receives completion, and can raise a debugger signal by returning a true Boolean value.
|
||||
public let receiveCompletion: ((Subscribers.Completion<Upstream.Failure>) -> Bool)?
|
||||
|
||||
/// Creates a breakpoint publisher with the provided upstream publisher and breakpoint-raising closures.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - upstream: The publisher from which this publisher receives elements.
|
||||
/// - receiveSubscription: A closure that executes when the publisher receives a subscription, and can raise a debugger signal by returning a true Boolean value.
|
||||
/// - receiveOutput: A closure that executes when the publisher receives output from the upstream publisher, and can raise a debugger signal by returning a true Boolean value.
|
||||
/// - receiveCompletion: A closure that executes when the publisher receives completion, and can raise a debugger signal by returning a true Boolean value.
|
||||
public init(upstream: Upstream, receiveSubscription: ((Subscription) -> Bool)? = nil, receiveOutput: ((Upstream.Output) -> Bool)? = nil, receiveCompletion: ((Subscribers.Completion<Publishers.Breakpoint<Upstream>.Failure>) -> Bool)? = nil)
|
||||
|
||||
/// 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 {
|
||||
|
||||
/// Raises a debugger signal when a provided closure needs to stop the process in the debugger.
|
||||
///
|
||||
/// When any of the provided closures returns `true`, this publisher raises the `SIGTRAP` signal to stop the process in the debugger.
|
||||
/// Otherwise, this publisher passes through values and completions as-is.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - receiveSubscription: A closure that executes when when the publisher receives a subscription. Return `true` from this closure to raise `SIGTRAP`, or false to continue.
|
||||
/// - receiveOutput: A closure that executes when when the publisher receives a value. Return `true` from this closure to raise `SIGTRAP`, or false to continue.
|
||||
/// - receiveCompletion: A closure that executes when when the publisher receives a completion. Return `true` from this closure to raise `SIGTRAP`, or false to continue.
|
||||
/// - Returns: A publisher that raises a debugger signal when one of the provided closures returns `true`.
|
||||
public func breakpoint(receiveSubscription: ((Subscription) -> Bool)? = nil, receiveOutput: ((Self.Output) -> Bool)? = nil, receiveCompletion: ((Subscribers.Completion<Self.Failure>) -> Bool)? = nil) -> Publishers.Breakpoint<Self>
|
||||
|
||||
/// Raises a debugger signal upon receiving a failure.
|
||||
///
|
||||
/// When the upstream publisher fails with an error, this publisher raises the `SIGTRAP` signal, which stops the process in the debugger.
|
||||
/// Otherwise, this publisher passes through values and completions as-is.
|
||||
/// - Returns: A publisher that raises a debugger signal upon receiving a failure.
|
||||
public func breakpointOnError() -> Publishers.Breakpoint<Self>
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that receives and combines the latest elements from two publishers.
|
||||
@@ -424,62 +257,6 @@ extension Publisher {
|
||||
public func collect<S>(_ strategy: Publishers.TimeGroupingStrategy<S>, options: S.SchedulerOptions? = nil) -> Publishers.CollectByTime<Self, S> where S : Scheduler
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that delivers elements to its downstream subscriber on a specific scheduler.
|
||||
public struct ReceiveOn<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 scheduler the publisher is to use for element delivery.
|
||||
public let scheduler: Context
|
||||
|
||||
/// Scheduler options that customize the delivery of elements.
|
||||
public let options: Context.SchedulerOptions?
|
||||
|
||||
public init(upstream: Upstream, scheduler: Context, options: Context.SchedulerOptions?)
|
||||
|
||||
/// 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 {
|
||||
|
||||
/// Specifies the scheduler on which to receive elements from the publisher.
|
||||
///
|
||||
/// You use the `receive(on:options:)` operator to receive results on a specific scheduler, such as performing UI work on the main run loop.
|
||||
/// In contrast with `subscribe(on:options:)`, which affects upstream messages, `receive(on:options:)` changes the execution context of downstream messages. In the following example, requests to `jsonPublisher` are performed on `backgroundQueue`, but elements received from it are performed on `RunLoop.main`.
|
||||
///
|
||||
/// let jsonPublisher = MyJSONLoaderPublisher() // Some publisher.
|
||||
/// let labelUpdater = MyLabelUpdateSubscriber() // Some subscriber that updates the UI.
|
||||
///
|
||||
/// jsonPublisher
|
||||
/// .subscribe(on: backgroundQueue)
|
||||
/// .receiveOn(on: RunLoop.main)
|
||||
/// .subscribe(labelUpdater)
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - scheduler: The scheduler the publisher is to use for element delivery.
|
||||
/// - options: Scheduler options that customize the element delivery.
|
||||
/// - Returns: A publisher that delivers elements using the specified scheduler.
|
||||
public func receive<S>(on scheduler: S, options: S.SchedulerOptions? = nil) -> Publishers.ReceiveOn<Self, S> where S : Scheduler
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
public struct PrefixUntilOutput<Upstream, Other> : Publisher where Upstream : Publisher, Other : Publisher {
|
||||
@@ -1205,127 +982,6 @@ extension Publisher {
|
||||
public func drop<P>(untilOutputFrom publisher: P) -> Publishers.DropUntilOutput<Self, P> where P : Publisher, Self.Failure == P.Failure
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that performs the specified closures when publisher events occur.
|
||||
public struct HandleEvents<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
|
||||
|
||||
/// A closure that executes when the publisher receives the subscription from the upstream publisher.
|
||||
public var receiveSubscription: ((Subscription) -> Void)?
|
||||
|
||||
/// A closure that executes when the publisher receives a value from the upstream publisher.
|
||||
public var receiveOutput: ((Upstream.Output) -> Void)?
|
||||
|
||||
/// A closure that executes when the publisher receives the completion from the upstream publisher.
|
||||
public var receiveCompletion: ((Subscribers.Completion<Upstream.Failure>) -> Void)?
|
||||
|
||||
/// A closure that executes when the downstream receiver cancels publishing.
|
||||
public var receiveCancel: (() -> Void)?
|
||||
|
||||
/// A closure that executes when the publisher receives a request for more elements.
|
||||
public var receiveRequest: ((Subscribers.Demand) -> Void)?
|
||||
|
||||
public init(upstream: Upstream, receiveSubscription: ((Subscription) -> Void)? = nil, receiveOutput: ((Publishers.HandleEvents<Upstream>.Output) -> Void)? = nil, receiveCompletion: ((Subscribers.Completion<Publishers.HandleEvents<Upstream>.Failure>) -> Void)? = nil, receiveCancel: (() -> Void)? = nil, receiveRequest: ((Subscribers.Demand) -> Void)?)
|
||||
|
||||
/// 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 {
|
||||
|
||||
/// Performs the specified closures when publisher events occur.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - receiveSubscription: A closure that executes when the publisher receives the subscription from the upstream publisher. Defaults to `nil`.
|
||||
/// - receiveOutput: A closure that executes when the publisher receives a value from the upstream publisher. Defaults to `nil`.
|
||||
/// - receiveCompletion: A closure that executes when the publisher receives the completion from the upstream publisher. Defaults to `nil`.
|
||||
/// - receiveCancel: A closure that executes when the downstream receiver cancels publishing. Defaults to `nil`.
|
||||
/// - receiveRequest: A closure that executes when the publisher receives a request for more elements. Defaults to `nil`.
|
||||
/// - Returns: A publisher that performs the specified closures when publisher events occur.
|
||||
public func handleEvents(receiveSubscription: ((Subscription) -> Void)? = nil, receiveOutput: ((Self.Output) -> Void)? = nil, receiveCompletion: ((Subscribers.Completion<Self.Failure>) -> Void)? = nil, receiveCancel: (() -> Void)? = nil, receiveRequest: ((Subscribers.Demand) -> Void)? = nil) -> Publishers.HandleEvents<Self>
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that emits all of one publisher’s elements before those from another publisher.
|
||||
public struct Concatenate<Prefix, Suffix> : Publisher where Prefix : Publisher, Suffix : Publisher, Prefix.Failure == Suffix.Failure, Prefix.Output == Suffix.Output {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Suffix.Output
|
||||
|
||||
/// The kind of errors this publisher might publish.
|
||||
///
|
||||
/// Use `Never` if this `Publisher` does not publish errors.
|
||||
public typealias Failure = Suffix.Failure
|
||||
|
||||
/// The publisher to republish, in its entirety, before republishing elements from `suffix`.
|
||||
public let prefix: Prefix
|
||||
|
||||
/// The publisher to republish only after `prefix` finishes.
|
||||
public let suffix: Suffix
|
||||
|
||||
public init(prefix: Prefix, suffix: Suffix)
|
||||
|
||||
/// 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, Suffix.Failure == S.Failure, Suffix.Output == S.Input
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Prefixes a `Publisher`'s output with the specified sequence.
|
||||
/// - Parameter elements: The elements to publish before this publisher’s elements.
|
||||
/// - Returns: A publisher that prefixes the specified elements prior to this publisher’s elements.
|
||||
public func prepend(_ elements: Self.Output...) -> Publishers.Concatenate<Publishers.Sequence<[Self.Output], Self.Failure>, Self>
|
||||
|
||||
/// Prefixes a `Publisher`'s output with the specified sequence.
|
||||
/// - Parameter elements: A sequence of elements to publish before this publisher’s elements.
|
||||
/// - Returns: A publisher that prefixes the sequence of elements prior to this publisher’s elements.
|
||||
public func prepend<S>(_ elements: S) -> Publishers.Concatenate<Publishers.Sequence<S, Self.Failure>, Self> where S : Sequence, Self.Output == S.Element
|
||||
|
||||
/// Prefixes this publisher’s output with the elements emitted by the given publisher.
|
||||
///
|
||||
/// The resulting publisher doesn’t emit any elements until the prefixing publisher finishes.
|
||||
/// - Parameter publisher: The prefixing publisher.
|
||||
/// - Returns: A publisher that prefixes the prefixing publisher’s elements prior to this publisher’s elements.
|
||||
public func prepend<P>(_ publisher: P) -> Publishers.Concatenate<P, Self> where P : Publisher, Self.Failure == P.Failure, Self.Output == P.Output
|
||||
|
||||
/// Append a `Publisher`'s output with the specified sequence.
|
||||
public func append(_ elements: Self.Output...) -> Publishers.Concatenate<Self, Publishers.Sequence<[Self.Output], Self.Failure>>
|
||||
|
||||
/// Appends a `Publisher`'s output with the specified sequence.
|
||||
public func append<S>(_ elements: S) -> Publishers.Concatenate<Self, Publishers.Sequence<S, Self.Failure>> where S : Sequence, Self.Output == S.Element
|
||||
|
||||
/// Appends this publisher’s output with the elements emitted by the given publisher.
|
||||
///
|
||||
/// This operator produces no elements until this publisher finishes. It then produces this publisher’s elements, followed by the given publisher’s elements. If this publisher fails with an error, the prefixing publisher does not publish the provided publisher’s elements.
|
||||
/// - Parameter publisher: The appending publisher.
|
||||
/// - Returns: A publisher that appends the appending publisher’s elements after this publisher’s elements.
|
||||
public func append<P>(_ publisher: P) -> Publishers.Concatenate<Self, P> where P : Publisher, Self.Failure == P.Failure, Self.Output == P.Output
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that publishes elements only after a specified time interval elapses between events.
|
||||
@@ -1795,68 +1451,6 @@ extension Publisher {
|
||||
public func tryCatch<P>(_ handler: @escaping (Self.Failure) throws -> P) -> Publishers.TryCatch<Self, P> where P : Publisher, Self.Output == P.Output
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that delays delivery of elements and completion to the downstream receiver.
|
||||
public struct Delay<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 that this publisher receives elements from.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The amount of time to delay.
|
||||
public let interval: Context.SchedulerTimeType.Stride
|
||||
|
||||
/// The allowed tolerance in firing delayed events.
|
||||
public let tolerance: Context.SchedulerTimeType.Stride
|
||||
|
||||
/// The scheduler to deliver the delayed events.
|
||||
public let scheduler: Context
|
||||
|
||||
public let options: Context.SchedulerOptions?
|
||||
|
||||
public init(upstream: Upstream, interval: Context.SchedulerTimeType.Stride, tolerance: Context.SchedulerTimeType.Stride, scheduler: Context, options: Context.SchedulerOptions? = nil)
|
||||
|
||||
/// 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 {
|
||||
|
||||
/// Delays delivery of all output to the downstream receiver by a specified amount of time on a particular scheduler.
|
||||
///
|
||||
/// The delay affects the delivery of elements and completion, but not of the original subscription.
|
||||
/// - Parameters:
|
||||
/// - interval: The amount of time to delay.
|
||||
/// - tolerance: The allowed tolerance in firing delayed events.
|
||||
/// - scheduler: The scheduler to deliver the delayed events.
|
||||
/// - Returns: A publisher that delays delivery of elements and completion to the downstream receiver.
|
||||
public func delay<S>(for interval: S.SchedulerTimeType.Stride, tolerance: S.SchedulerTimeType.Stride? = nil, scheduler: S, options: S.SchedulerOptions? = nil) -> Publishers.Delay<Self, S> where S : Scheduler
|
||||
}
|
||||
|
||||
extension Just {
|
||||
|
||||
public func prepend(_ elements: Output...) -> Publishers.Sequence<[Output], Just<Output>.Failure>
|
||||
|
||||
public func prepend<S>(_ elements: S) -> Publishers.Sequence<[Output], Just<Output>.Failure> where Output == S.Element, S : Sequence
|
||||
|
||||
public func append(_ elements: Output...) -> Publishers.Sequence<[Output], Just<Output>.Failure>
|
||||
|
||||
public func append<S>(_ elements: S) -> Publishers.Sequence<[Output], Just<Output>.Failure> where Output == S.Element, S : Sequence
|
||||
}
|
||||
|
||||
extension Publishers.CombineLatest : Equatable where A : Equatable, B : Equatable {
|
||||
|
||||
@@ -2035,17 +1629,6 @@ extension Publishers.DropUntilOutput : Equatable where Upstream : Equatable, Oth
|
||||
public static func == (lhs: Publishers.DropUntilOutput<Upstream, Other>, rhs: Publishers.DropUntilOutput<Upstream, Other>) -> Bool
|
||||
}
|
||||
|
||||
extension Publishers.Concatenate : Equatable where Prefix : Equatable, Suffix : Equatable {
|
||||
|
||||
/// Returns a Boolean value that indicates whether two publishers are equivalent.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - lhs: A concatenate publisher to compare for equality.
|
||||
/// - rhs: Another concatenate publisher to compare for equality.
|
||||
/// - Returns: `true` if the two publishers’ prefix and suffix properties are equal, `false` otherwise.
|
||||
public static func == (lhs: Publishers.Concatenate<Prefix, Suffix>, rhs: Publishers.Concatenate<Prefix, Suffix>) -> Bool
|
||||
}
|
||||
|
||||
extension Publishers.Zip : Equatable where A : Equatable, B : Equatable {
|
||||
|
||||
/// Returns a Boolean value that indicates whether two publishers are equivalent.
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#define COPENCOMBINEHELPERS_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <signal.h>
|
||||
|
||||
#if __has_attribute(swift_name)
|
||||
# define OPENCOMBINE_SWIFT_NAME(_name) __attribute__((swift_name(#_name)))
|
||||
@@ -16,6 +17,12 @@
|
||||
# define OPENCOMBINE_SWIFT_NAME(_name)
|
||||
#endif
|
||||
|
||||
#if __has_attribute(always_inline)
|
||||
# define OPENCOMBINE_ALWAYS_INLINE __attribute__((always_inline))
|
||||
#else
|
||||
# define OPENCOMBINE_ALWAYS_INLINE
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
@@ -23,50 +30,59 @@ extern "C" {
|
||||
#pragma mark - CombineIdentifier
|
||||
|
||||
uint64_t opencombine_next_combine_identifier(void)
|
||||
OPENCOMBINE_SWIFT_NAME(nextCombineIdentifier());
|
||||
OPENCOMBINE_SWIFT_NAME(__nextCombineIdentifier());
|
||||
|
||||
#pragma mark - OpenCombineUnfairLock
|
||||
|
||||
/// A wrapper around an opaque pointer for type safety in Swift.
|
||||
typedef struct OpenCombineUnfairLock {
|
||||
void* _Nonnull opaque;
|
||||
} OPENCOMBINE_SWIFT_NAME(UnfairLock) OpenCombineUnfairLock;
|
||||
} OPENCOMBINE_SWIFT_NAME(__UnfairLock) OpenCombineUnfairLock;
|
||||
|
||||
/// Allocates a lock object. The allocated object must be destroyed by calling
|
||||
/// the destroy() method.
|
||||
OpenCombineUnfairLock opencombine_unfair_lock_alloc(void)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairLock.allocate());
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairLock.allocate());
|
||||
|
||||
void opencombine_unfair_lock_lock(OpenCombineUnfairLock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairLock.lock(self:));
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairLock.lock(self:));
|
||||
|
||||
void opencombine_unfair_lock_unlock(OpenCombineUnfairLock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairLock.unlock(self:));
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairLock.unlock(self:));
|
||||
|
||||
void opencombine_unfair_lock_assert_owner(OpenCombineUnfairLock mutex)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairLock.assertOwner(self:));
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairLock.assertOwner(self:));
|
||||
|
||||
void opencombine_unfair_lock_dealloc(OpenCombineUnfairLock lock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairLock.deallocate(self:));
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairLock.deallocate(self:));
|
||||
|
||||
#pragma mark - OpenCombineUnfairRecursiveLock
|
||||
|
||||
/// A wrapper around an opaque pointer for type safety in Swift.
|
||||
typedef struct OpenCombineUnfairRecursiveLock {
|
||||
void* _Nonnull opaque;
|
||||
} OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock) OpenCombineUnfairRecursiveLock;
|
||||
} OPENCOMBINE_SWIFT_NAME(__UnfairRecursiveLock) OpenCombineUnfairRecursiveLock;
|
||||
|
||||
OpenCombineUnfairRecursiveLock opencombine_unfair_recursive_lock_alloc(void)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock.allocate());
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairRecursiveLock.allocate());
|
||||
|
||||
void opencombine_unfair_recursive_lock_lock(OpenCombineUnfairRecursiveLock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock.lock(self:));
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairRecursiveLock.lock(self:));
|
||||
|
||||
void opencombine_unfair_recursive_lock_unlock(OpenCombineUnfairRecursiveLock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock.unlock(self:));
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairRecursiveLock.unlock(self:));
|
||||
|
||||
void opencombine_unfair_recursive_lock_dealloc(OpenCombineUnfairRecursiveLock lock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock.deallocate(self:));
|
||||
OPENCOMBINE_SWIFT_NAME(__UnfairRecursiveLock.deallocate(self:));
|
||||
|
||||
#pragma mark - Breakpoint
|
||||
|
||||
OPENCOMBINE_ALWAYS_INLINE
|
||||
inline void opencombine_stop_in_debugger(void) OPENCOMBINE_SWIFT_NAME(__stopInDebugger());
|
||||
|
||||
void opencombine_stop_in_debugger(void) {
|
||||
raise(SIGTRAP);
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
|
||||
@@ -5,14 +5,16 @@
|
||||
// Created by Sergej Jaskiewicz on 10.06.2019.
|
||||
//
|
||||
|
||||
import func COpenCombineHelpers.nextCombineIdentifier
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
public struct CombineIdentifier: Hashable, CustomStringConvertible {
|
||||
|
||||
private let value: UInt64
|
||||
|
||||
public init() {
|
||||
value = nextCombineIdentifier()
|
||||
value = __nextCombineIdentifier()
|
||||
}
|
||||
|
||||
public init(_ obj: AnyObject) {
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
/// A subject that wraps a single value and publishes a new element whenever the value
|
||||
/// changes.
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
// Created by Max Desiatov on 24/11/2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
/// A publisher that eventually produces one value and then finishes or fails.
|
||||
public final class Future<Output, Failure>: Publisher where Failure: Error {
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
// Created by Sergej Jaskiewicz on 23.10.2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
/// A helper class that acts like both subscriber and subscription.
|
||||
///
|
||||
|
||||
@@ -5,7 +5,12 @@
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
internal typealias UnfairLock = __UnfairLock
|
||||
internal typealias UnfairRecursiveLock = __UnfairRecursiveLock
|
||||
|
||||
extension UnfairRecursiveLock {
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
// Created by Sergej Jaskiewicz on 22.09.2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
/// A helper class that acts like both subscriber and subscription.
|
||||
///
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
// Created by Sergej Jaskiewicz on 16/09/2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
// NOTE: This class has been audited for thread safety.
|
||||
internal final class SubjectSubscriber<Downstream: Subject>
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
|
||||
/// A scheduler for performing synchronous actions.
|
||||
///
|
||||
/// You can use this scheduler for immediate actions. If you attempt to schedule
|
||||
/// actions after a specific date, the scheduler produces a fatal error.
|
||||
/// You can only use this scheduler for immediate actions. If you attempt to schedule
|
||||
/// actions after a specific date, this scheduler ignores the date and performs
|
||||
/// them immediately.
|
||||
public struct ImmediateScheduler: Scheduler {
|
||||
|
||||
/// The time type used by the immediate scheduler.
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
/// A subject that passes along values and completion.
|
||||
///
|
||||
|
||||
@@ -249,6 +249,26 @@ extension Just {
|
||||
) -> Result<ElementOfResult, Error>.OCombine.Publisher {
|
||||
return .init(Result { try nextPartialResult(initialResult, output) })
|
||||
}
|
||||
|
||||
public func prepend(_ elements: Output...) -> Publishers.Sequence<[Output], Never> {
|
||||
return prepend(elements)
|
||||
}
|
||||
|
||||
public func prepend<Elements: Sequence>(
|
||||
_ elements: Elements
|
||||
) -> Publishers.Sequence<[Output], Never> where Output == Elements.Element {
|
||||
return .init(sequence: elements + [output])
|
||||
}
|
||||
|
||||
public func append(_ elements: Output...) -> Publishers.Sequence<[Output], Never> {
|
||||
return append(elements)
|
||||
}
|
||||
|
||||
public func append<Elements: Sequence>(
|
||||
_ elements: Elements
|
||||
) -> Publishers.Sequence<[Output], Never> where Output == Elements.Element {
|
||||
return .init(sequence: [output] + elements)
|
||||
}
|
||||
}
|
||||
|
||||
extension Just {
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
// Created by Sergej Jaskiewicz on 18/09/2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
extension ConnectablePublisher {
|
||||
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
//
|
||||
// Publishers.Breakpoint.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 03.12.2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Raises a debugger signal when a provided closure needs to stop the process in
|
||||
/// the debugger.
|
||||
///
|
||||
/// When any of the provided closures returns `true`, this publisher raises
|
||||
/// the `SIGTRAP` signal to stop the process in the debugger.
|
||||
/// Otherwise, this publisher passes through values and completions as-is.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - receiveSubscription: A closure that executes when when the publisher receives
|
||||
/// a subscription. Return `true` from this closure to raise `SIGTRAP`, or `false`
|
||||
/// to continue.
|
||||
/// - receiveOutput: A closure that executes when when the publisher receives
|
||||
/// a value. Return `true` from this closure to raise `SIGTRAP`, or `false`
|
||||
/// to continue.
|
||||
/// - receiveCompletion: A closure that executes when when the publisher receives
|
||||
/// a completion. Return `true` from this closure to raise `SIGTRAP`, or `false`
|
||||
/// to continue.
|
||||
/// - Returns: A publisher that raises a debugger signal when one of the provided
|
||||
/// closures returns `true`.
|
||||
public func breakpoint(
|
||||
receiveSubscription: ((Subscription) -> Bool)? = nil,
|
||||
receiveOutput: ((Output) -> Bool)? = nil,
|
||||
receiveCompletion: ((Subscribers.Completion<Failure>) -> Bool)? = nil
|
||||
) -> Publishers.Breakpoint<Self> {
|
||||
return .init(upstream: self,
|
||||
receiveSubscription: receiveSubscription,
|
||||
receiveOutput: receiveOutput,
|
||||
receiveCompletion: receiveCompletion)
|
||||
}
|
||||
|
||||
/// Raises a debugger signal upon receiving a failure.
|
||||
///
|
||||
/// When the upstream publisher fails with an error, this publisher raises
|
||||
/// the `SIGTRAP` signal, which stops the process in the debugger.
|
||||
/// Otherwise, this publisher passes through values and completions as-is.
|
||||
///
|
||||
/// - Returns: A publisher that raises a debugger signal upon receiving a failure.
|
||||
public func breakpointOnError() -> Publishers.Breakpoint<Self> {
|
||||
return breakpoint { completion in
|
||||
switch completion {
|
||||
case .finished:
|
||||
return false
|
||||
case .failure:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that raises a debugger signal when a provided closure needs to stop
|
||||
/// the process in the debugger.
|
||||
///
|
||||
/// When any of the provided closures returns `true`, this publisher raises
|
||||
/// the `SIGTRAP` signal to stop the process in the debugger.
|
||||
/// Otherwise, this publisher passes through values and completions as-is.
|
||||
public struct Breakpoint<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A closure that executes when the publisher receives a subscription, and can
|
||||
/// raise a debugger signal by returning a `true` Boolean value.
|
||||
public let receiveSubscription: ((Subscription) -> Bool)?
|
||||
|
||||
/// A closure that executes when the publisher receives output from the upstream
|
||||
/// publisher, and can raise a debugger signal by returning a `true` Boolean
|
||||
/// value.
|
||||
public let receiveOutput: ((Upstream.Output) -> Bool)?
|
||||
|
||||
/// A closure that executes when the publisher receives completion, and can raise
|
||||
/// a debugger signal by returning a `true` Boolean value.
|
||||
public let receiveCompletion:
|
||||
((Subscribers.Completion<Upstream.Failure>) -> Bool)?
|
||||
|
||||
/// Creates a breakpoint publisher with the provided upstream publisher and
|
||||
/// breakpoint-raising closures.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - upstream: The publisher from which this publisher receives elements.
|
||||
/// - receiveSubscription: A closure that executes when the publisher receives
|
||||
/// a subscription, and can raise a debugger signal by returning a `true`
|
||||
/// Boolean value.
|
||||
/// - receiveOutput: A closure that executes when the publisher receives output
|
||||
/// from the upstream publisher, and can raise a debugger signal by returning
|
||||
/// a `true` Boolean value.
|
||||
/// - receiveCompletion: A closure that executes when the publisher receives
|
||||
/// completion, and can raise a debugger signal by returning a `true` Boolean
|
||||
/// value.
|
||||
public init(
|
||||
upstream: Upstream,
|
||||
receiveSubscription: ((Subscription) -> Bool)? = nil,
|
||||
receiveOutput: ((Upstream.Output) -> Bool)? = nil,
|
||||
receiveCompletion: ((Subscribers.Completion<Failure>) -> Bool)? = nil
|
||||
) {
|
||||
self.upstream = upstream
|
||||
self.receiveSubscription = receiveSubscription
|
||||
self.receiveOutput = receiveOutput
|
||||
self.receiveCompletion = receiveCompletion
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
upstream.subscribe(Inner(self, downstream: subscriber))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Breakpoint {
|
||||
private struct Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
private let breakpoint: Publishers.Breakpoint<Upstream>
|
||||
|
||||
let combineIdentifier = CombineIdentifier()
|
||||
|
||||
init(_ breakpoint: Publishers.Breakpoint<Upstream>,
|
||||
downstream: Downstream) {
|
||||
self.downstream = downstream
|
||||
self.breakpoint = breakpoint
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
if breakpoint.receiveSubscription?(subscription) == true {
|
||||
__stopInDebugger()
|
||||
}
|
||||
downstream.receive(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
if breakpoint.receiveOutput?(input) == true {
|
||||
__stopInDebugger()
|
||||
}
|
||||
return downstream.receive(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
if breakpoint.receiveCompletion?(completion) == true {
|
||||
__stopInDebugger()
|
||||
}
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
var description: String { return "Breakpoint" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children = CollectionOfOne<Mirror.Child>(
|
||||
("upstream", breakpoint.upstream)
|
||||
)
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
//
|
||||
// Publishers.Concatenate.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 24.10.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Prefixes a `Publisher`'s output with the specified sequence.
|
||||
///
|
||||
/// - Parameter elements: The elements to publish before this publisher’s elements.
|
||||
/// - Returns: A publisher that prefixes the specified elements prior to this
|
||||
/// publisher’s elements.
|
||||
public func prepend(
|
||||
_ elements: Output...
|
||||
) -> Publishers.Concatenate<Publishers.Sequence<[Output], Failure>, Self> {
|
||||
return prepend(elements)
|
||||
}
|
||||
|
||||
/// Prefixes a `Publisher`'s output with the specified sequence.
|
||||
///
|
||||
/// - Parameter elements: A sequence of elements to publish before this publisher’s
|
||||
/// elements.
|
||||
/// - Returns: A publisher that prefixes the sequence of elements prior to this
|
||||
/// publisher’s elements.
|
||||
public func prepend<Elements: Sequence>(
|
||||
_ elements: Elements
|
||||
) -> Publishers.Concatenate<Publishers.Sequence<Elements, Failure>, Self>
|
||||
where Output == Elements.Element
|
||||
{
|
||||
return prepend(.init(sequence: elements))
|
||||
}
|
||||
|
||||
/// Prefixes this publisher’s output with the elements emitted by the given publisher.
|
||||
///
|
||||
/// The resulting publisher doesn’t emit any elements until the prefixing publisher
|
||||
/// finishes.
|
||||
///
|
||||
/// - Parameter publisher: The prefixing publisher.
|
||||
/// - Returns: A publisher that prefixes the prefixing publisher’s elements prior to
|
||||
/// this publisher’s elements.
|
||||
public func prepend<Prefix: Publisher>(
|
||||
_ publisher: Prefix
|
||||
) -> Publishers.Concatenate<Prefix, Self>
|
||||
where Failure == Prefix.Failure, Output == Prefix.Output
|
||||
{
|
||||
return .init(prefix: publisher, suffix: self)
|
||||
}
|
||||
|
||||
/// Append a `Publisher`'s output with the specified sequence.
|
||||
public func append(
|
||||
_ elements: Output...
|
||||
) -> Publishers.Concatenate<Self, Publishers.Sequence<[Output], Failure>> {
|
||||
return append(elements)
|
||||
}
|
||||
|
||||
/// Appends a `Publisher`'s output with the specified sequence.
|
||||
public func append<Elements: Sequence>(
|
||||
_ elements: Elements
|
||||
) -> Publishers.Concatenate<Self, Publishers.Sequence<Elements, Failure>>
|
||||
where Output == Elements.Element
|
||||
{
|
||||
return append(.init(sequence: elements))
|
||||
}
|
||||
|
||||
/// Appends this publisher’s output with the elements emitted by the given publisher.
|
||||
///
|
||||
/// This operator produces no elements until this publisher finishes. It then produces
|
||||
/// this publisher’s elements, followed by the given publisher’s elements.
|
||||
/// If this publisher fails with an error, the prefixing publisher does not publish
|
||||
/// the provided publisher’s elements.
|
||||
///
|
||||
/// - Parameter publisher: The appending publisher.
|
||||
/// - Returns: A publisher that appends the appending publisher’s elements after this
|
||||
/// publisher’s elements.
|
||||
public func append<Suffix: Publisher>(
|
||||
_ publisher: Suffix
|
||||
) -> Publishers.Concatenate<Self, Suffix>
|
||||
where Suffix.Failure == Failure, Suffix.Output == Output
|
||||
{
|
||||
return .init(prefix: self, suffix: publisher)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that emits all of one publisher’s elements before those from anothe
|
||||
/// publisher.
|
||||
public struct Concatenate<Prefix: Publisher, Suffix: Publisher>: Publisher
|
||||
where Prefix.Failure == Suffix.Failure, Prefix.Output == Suffix.Output
|
||||
{
|
||||
public typealias Output = Suffix.Output
|
||||
|
||||
public typealias Failure = Suffix.Failure
|
||||
|
||||
/// The publisher to republish, in its entirety, before republishing elements from
|
||||
/// `suffix`.
|
||||
public let prefix: Prefix
|
||||
|
||||
/// The publisher to republish only after `prefix` finishes.
|
||||
public let suffix: Suffix
|
||||
|
||||
public init(prefix: Prefix, suffix: Suffix) {
|
||||
self.prefix = prefix
|
||||
self.suffix = suffix
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Suffix.Failure == Downstream.Failure, Suffix.Output == Downstream.Input
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, suffix: suffix)
|
||||
prefix.subscribe(inner)
|
||||
subscriber.receive(subscription: inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Concatenate: Equatable where Prefix: Equatable, Suffix: Equatable {}
|
||||
|
||||
extension Publishers.Concatenate {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Suffix.Output, Downstream.Failure == Suffix.Failure
|
||||
{
|
||||
typealias Input = Suffix.Output
|
||||
|
||||
typealias Failure = Suffix.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let suffix: Suffix
|
||||
|
||||
private var prefixFinished = false
|
||||
|
||||
private var demand = Subscribers.Demand.none
|
||||
|
||||
private var upstream: Subscription?
|
||||
|
||||
private var expectedSubscriptions = 2
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
// ??? This lock is non-recursive in Combine, but it should be!
|
||||
// (FB7404824 if Apple folks are watching)
|
||||
private let downstreamLock = UnfairLock.allocate()
|
||||
|
||||
fileprivate init(downstream: Downstream, suffix: Suffix) {
|
||||
self.downstream = downstream
|
||||
self.suffix = suffix
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
downstreamLock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard upstream == nil, expectedSubscriptions > 0 else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
upstream = subscription
|
||||
expectedSubscriptions -= 1
|
||||
let demand = self.demand
|
||||
lock.unlock()
|
||||
if demand > 0 {
|
||||
subscription.request(demand)
|
||||
}
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
demand -= 1
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
let newDemand = downstream.receive(input)
|
||||
downstreamLock.unlock()
|
||||
lock.lock()
|
||||
demand += newDemand
|
||||
lock.unlock()
|
||||
return newDemand
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
// Reading prefixFinished should be locked. Combine doesn't lock here.
|
||||
if prefixFinished {
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
return
|
||||
}
|
||||
|
||||
guard case .finished = completion else {
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
return
|
||||
}
|
||||
|
||||
prefixFinished = true // Should be locked as well?
|
||||
lock.lock()
|
||||
upstream = nil
|
||||
lock.unlock()
|
||||
suffix.subscribe(self)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
self.demand += demand
|
||||
guard let subscription = upstream else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard let subscription = upstream else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
upstream = nil
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "Concatenate" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("downstream", downstream),
|
||||
("upstreamSubscription", upstream as Any),
|
||||
("suffix", suffix),
|
||||
("demand", demand)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
//
|
||||
// Publishers.Delay.swift
|
||||
// OpenCombine
|
||||
//
|
||||
// Created by Евгений Богомолов on 07/09/2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Delays delivery of all output to the downstream receiver by a specified amount
|
||||
/// of time on a particular scheduler.
|
||||
///
|
||||
/// The delay affects the delivery of elements and completion, but not of the original
|
||||
/// subscription.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - interval: The amount of time to delay.
|
||||
/// - tolerance: The allowed tolerance in firing delayed events.
|
||||
/// - scheduler: The scheduler to deliver the delayed events.
|
||||
/// - Returns: A publisher that delays delivery of elements and completion to
|
||||
/// the downstream receiver.
|
||||
public func delay<Context: Scheduler>(
|
||||
for interval: Context.SchedulerTimeType.Stride,
|
||||
tolerance: Context.SchedulerTimeType.Stride? = nil,
|
||||
scheduler: Context,
|
||||
options: Context.SchedulerOptions? = nil
|
||||
) -> Publishers.Delay<Self, Context> {
|
||||
return .init(upstream: self,
|
||||
interval: interval,
|
||||
tolerance: tolerance ?? scheduler.minimumTolerance,
|
||||
scheduler: scheduler,
|
||||
options: options)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that delays delivery of elements and completion
|
||||
/// to the downstream receiver.
|
||||
public struct Delay<Upstream: Publisher, Context: Scheduler>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher that this publisher receives elements from.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The amount of time to delay.
|
||||
public let interval: Context.SchedulerTimeType.Stride
|
||||
|
||||
/// The allowed tolerance in firing delayed events.
|
||||
public let tolerance: Context.SchedulerTimeType.Stride
|
||||
|
||||
/// The scheduler to deliver the delayed events.
|
||||
public let scheduler: Context
|
||||
|
||||
public let options: Context.SchedulerOptions?
|
||||
|
||||
public init(upstream: Upstream,
|
||||
interval: Context.SchedulerTimeType.Stride,
|
||||
tolerance: Context.SchedulerTimeType.Stride,
|
||||
scheduler: Context,
|
||||
options: Context.SchedulerOptions? = nil)
|
||||
{
|
||||
self.upstream = upstream
|
||||
self.interval = interval
|
||||
self.tolerance = tolerance
|
||||
self.scheduler = scheduler
|
||||
self.options = options
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
upstream.subscribe(Inner(self, downstream: subscriber))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Delay {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
// NOTE: This class has been audited for thread safety
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
fileprivate typealias Delay = Publishers.Delay<Upstream, Context>
|
||||
|
||||
private enum State {
|
||||
case ready(Delay, Downstream)
|
||||
case subscribed(Delay, Downstream, Subscription)
|
||||
case terminal
|
||||
}
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
private var state: State
|
||||
private let downstreamLock = UnfairLock.allocate()
|
||||
|
||||
fileprivate init(_ publisher: Delay, downstream: Downstream) {
|
||||
state = .ready(publisher, downstream)
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
downstreamLock.deallocate()
|
||||
}
|
||||
|
||||
private func schedule(_ delay: Delay,
|
||||
immediate: Bool,
|
||||
work: @escaping () -> Void) {
|
||||
if immediate {
|
||||
delay.scheduler.schedule(options: delay.options, work)
|
||||
return
|
||||
}
|
||||
delay
|
||||
.scheduler
|
||||
.schedule(after: delay.scheduler.now.advanced(by: delay.interval),
|
||||
tolerance: delay.tolerance,
|
||||
options: delay.options,
|
||||
work)
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case let .ready(delay, downstream) = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .subscribed(delay, downstream, subscription)
|
||||
lock.unlock()
|
||||
schedule(delay, immediate: true) { [weak self] in
|
||||
self?.scheduledReceive(subscription: subscription)
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduledReceive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
downstream.receive(subscription: self)
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case let .subscribed(delay, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
lock.unlock()
|
||||
schedule(delay, immediate: false) { [weak self] in
|
||||
self?.scheduledReceive(input, downstream: downstream)
|
||||
}
|
||||
return .none
|
||||
}
|
||||
|
||||
private func scheduledReceive(_ input: Upstream.Output, downstream: Downstream) {
|
||||
downstreamLock.lock()
|
||||
let newDemand = downstream.receive(input)
|
||||
downstreamLock.unlock()
|
||||
guard newDemand > 0 else {
|
||||
return
|
||||
}
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(newDemand)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(delay, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
schedule(delay, immediate: false) { [weak self] in
|
||||
self?.scheduledReceive(completion: completion, downstream: downstream)
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduledReceive(completion: Subscribers.Completion<Failure>,
|
||||
downstream: Downstream) {
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,9 @@
|
||||
// Created by Sven Weidauer on 03.10.2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
extension Publisher {
|
||||
/// Omits the specified number of elements before republishing subsequent elements.
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
// Created by Sergej Jaskiewicz on 16.06.2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
extension Publisher {
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
// Created by Eric Patey on 16.08.2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
extension Publisher {
|
||||
/// Transforms all elements from an upstream publisher into a new or existing
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
//
|
||||
// Publishers.HandleEvents.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 03.12.2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Performs the specified closures when publisher events occur.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - receiveSubscription: A closure that executes when the publisher receives
|
||||
/// the subscription from the upstream publisher. Defaults to `nil`.
|
||||
/// - receiveOutput: A closure that executes when the publisher receives a value
|
||||
/// from the upstream publisher. Defaults to `nil`.
|
||||
/// - receiveCompletion: A closure that executes when the publisher receives
|
||||
/// the completion from the upstream publisher. Defaults to `nil`.
|
||||
/// - receiveCancel: A closure that executes when the downstream receiver cancels
|
||||
/// publishing. Defaults to `nil`.
|
||||
/// - receiveRequest: A closure that executes when the publisher receives a request
|
||||
/// for more elements. Defaults to `nil`.
|
||||
/// - Returns: A publisher that performs the specified closures when publisher events
|
||||
/// occur.
|
||||
public func handleEvents(
|
||||
receiveSubscription: ((Subscription) -> Void)? = nil,
|
||||
receiveOutput: ((Output) -> Void)? = nil,
|
||||
receiveCompletion: ((Subscribers.Completion<Failure>) -> Void)? = nil,
|
||||
receiveCancel: (() -> Void)? = nil,
|
||||
receiveRequest: ((Subscribers.Demand) -> Void)? = nil
|
||||
) -> Publishers.HandleEvents<Self> {
|
||||
return .init(upstream: self,
|
||||
receiveSubscription: receiveSubscription,
|
||||
receiveOutput: receiveOutput,
|
||||
receiveCompletion: receiveCompletion,
|
||||
receiveCancel: receiveCancel,
|
||||
receiveRequest: receiveRequest)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that performs the specified closures when publisher events occur.
|
||||
public struct HandleEvents<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A closure that executes when the publisher receives the subscription from
|
||||
/// the upstream publisher.
|
||||
public var receiveSubscription: ((Subscription) -> Void)?
|
||||
|
||||
/// A closure that executes when the publisher receives a value from the upstream
|
||||
/// publisher.
|
||||
public var receiveOutput: ((Upstream.Output) -> Void)?
|
||||
|
||||
/// A closure that executes when the publisher receives the completion from
|
||||
/// the upstream publisher.
|
||||
public var receiveCompletion:
|
||||
((Subscribers.Completion<Upstream.Failure>) -> Void)?
|
||||
|
||||
/// A closure that executes when the downstream receiver cancels publishing.
|
||||
public var receiveCancel: (() -> Void)?
|
||||
|
||||
/// A closure that executes when the publisher receives a request for more
|
||||
/// elements.
|
||||
public var receiveRequest: ((Subscribers.Demand) -> Void)?
|
||||
|
||||
public init(
|
||||
upstream: Upstream,
|
||||
receiveSubscription: ((Subscription) -> Void)? = nil,
|
||||
receiveOutput: ((Output) -> Void)? = nil,
|
||||
receiveCompletion: ((Subscribers.Completion<Failure>) -> Void)? = nil,
|
||||
receiveCancel: (() -> Void)? = nil,
|
||||
receiveRequest: ((Subscribers.Demand) -> Void)?
|
||||
) {
|
||||
self.upstream = upstream
|
||||
self.receiveSubscription = receiveSubscription
|
||||
self.receiveOutput = receiveOutput
|
||||
self.receiveCompletion = receiveCompletion
|
||||
self.receiveCancel = receiveCancel
|
||||
self.receiveRequest = receiveRequest
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
let inner = Inner(self, downstream: subscriber)
|
||||
subscriber.receive(subscription: inner)
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.HandleEvents {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private var status = SubscriptionStatus.awaitingSubscription
|
||||
private var pendingDemand = Subscribers.Demand.none
|
||||
private let lock = UnfairLock.allocate()
|
||||
private var events: Publishers.HandleEvents<Upstream>?
|
||||
private let downstream: Downstream
|
||||
|
||||
init(_ events: Publishers.HandleEvents<Upstream>, downstream: Downstream) {
|
||||
self.events = events
|
||||
self.downstream = downstream
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
events?.receiveSubscription?(subscription)
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = status else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
status = .subscribed(subscription)
|
||||
let pendingDemand = self.pendingDemand
|
||||
self.pendingDemand = .none
|
||||
lock.unlock()
|
||||
if pendingDemand > 0 {
|
||||
subscription.request(pendingDemand)
|
||||
}
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
events?.receiveOutput?(input)
|
||||
let newDemand = downstream.receive(input)
|
||||
if newDemand > 0 {
|
||||
events?.receiveRequest?(newDemand)
|
||||
}
|
||||
return newDemand
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
events?.receiveCompletion?(completion)
|
||||
lock.lock()
|
||||
events = nil
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
events?.receiveRequest?(demand)
|
||||
lock.lock()
|
||||
if case let .subscribed(subscription) = status {
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
return
|
||||
}
|
||||
pendingDemand += demand
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
events?.receiveCancel?()
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
events = nil
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "HandleEvents" }
|
||||
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,9 @@
|
||||
// Created by Eric Patey on 16.08.2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
extension Publisher {
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
// Created by Anton Nazarov on 25.06.2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
extension Publisher {
|
||||
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
//
|
||||
// Publishers.MeasureInterval.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 03.12.2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Measures and emits the time interval between events received from an upstream
|
||||
/// publisher.
|
||||
///
|
||||
/// The output type of the returned scheduler is the time interval of the provided
|
||||
/// scheduler.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - scheduler: The scheduler on which to deliver elements.
|
||||
/// - options: Options that customize the delivery of elements.
|
||||
/// - Returns: A publisher that emits elements representing the time interval between
|
||||
/// the elements it receives.
|
||||
public func measureInterval<Context: Scheduler>(
|
||||
using scheduler: Context,
|
||||
options: Context.SchedulerOptions? = nil
|
||||
) -> Publishers.MeasureInterval<Self, Context> {
|
||||
return .init(upstream: self, scheduler: scheduler)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that measures and emits the time interval between events received from
|
||||
/// an upstream publisher.
|
||||
public struct MeasureInterval<Upstream: Publisher, Context: Scheduler>: Publisher {
|
||||
|
||||
public typealias Output = Context.SchedulerTimeType.Stride
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The scheduler on which to deliver elements.
|
||||
public let scheduler: Context
|
||||
|
||||
public init(upstream: Upstream, scheduler: Context) {
|
||||
self.upstream = upstream
|
||||
self.scheduler = scheduler
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Downstream.Input == Context.SchedulerTimeType.Stride
|
||||
{
|
||||
upstream.subscribe(Inner(self, downstream: subscriber))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.MeasureInterval {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Context.SchedulerTimeType.Stride,
|
||||
Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
// NOTE: This class has been audited for thread safety
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
typealias MeasureInterval = Publishers.MeasureInterval<Upstream, Context>
|
||||
|
||||
private enum State {
|
||||
case ready(MeasureInterval, Downstream)
|
||||
case subscribed(MeasureInterval, Downstream, Subscription)
|
||||
case terminal
|
||||
}
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private var state: State
|
||||
|
||||
private var last: Context.SchedulerTimeType?
|
||||
|
||||
init(_ measureInterval: MeasureInterval, downstream: Downstream) {
|
||||
state = .ready(measureInterval, downstream)
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case let .ready(measureInterval, downstream) = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .subscribed(measureInterval, downstream, subscription)
|
||||
last = measureInterval.scheduler.now
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case let .subscribed(measureInterval, downstream, subscription) = state,
|
||||
let previousTime = last else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
let now = measureInterval.scheduler.now
|
||||
last = now
|
||||
lock.unlock()
|
||||
let newDemand = downstream.receive(previousTime.distance(to: now))
|
||||
if newDemand > 0 {
|
||||
subscription.request(newDemand)
|
||||
}
|
||||
return .none
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
last = nil
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
last = nil
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "MeasureInterval" }
|
||||
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,9 @@
|
||||
// Created by Sergej Jaskiewicz on 14.06.2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
extension Publisher {
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
// Created by Sergej Jaskiewicz on 24.10.2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
extension Publisher {
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
// Created by Sergej Jaskiewicz on 16.06.2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
extension Publisher {
|
||||
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
//
|
||||
// Publishers.ReceiveOn.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 02.12.2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
extension Publisher {
|
||||
/// Specifies the scheduler on which to receive elements from the publisher.
|
||||
///
|
||||
/// You use the `receive(on:options:)` operator to receive results on a specific
|
||||
/// scheduler, such as performing UI work on the main run loop.
|
||||
/// In contrast with `subscribe(on:options:)`, which affects upstream messages,
|
||||
/// `receive(on:options:)` changes the execution context of downstream messages.
|
||||
/// In the following example, requests to `jsonPublisher` are performed on
|
||||
/// `backgroundQueue`, but elements received from it are performed on `RunLoop.main`.
|
||||
///
|
||||
/// // Some publisher.
|
||||
/// let jsonPublisher = MyJSONLoaderPublisher()
|
||||
///
|
||||
/// // Some subscriber that updates the UI.
|
||||
/// let labelUpdater = MyLabelUpdateSubscriber()
|
||||
///
|
||||
/// jsonPublisher
|
||||
/// .subscribe(on: backgroundQueue)
|
||||
/// .receiveOn(on: RunLoop.main)
|
||||
/// .subscribe(labelUpdater)
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - scheduler: The scheduler the publisher is to use for element delivery.
|
||||
/// - options: Scheduler options that customize the element delivery.
|
||||
/// - Returns: A publisher that delivers elements using the specified scheduler.
|
||||
public func receive<Context: Scheduler>(
|
||||
on scheduler: Context,
|
||||
options: Context.SchedulerOptions? = nil
|
||||
) -> Publishers.ReceiveOn<Self, Context> {
|
||||
return .init(upstream: self, scheduler: scheduler, options: options)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that delivers elements to its downstream subscriber on a specific
|
||||
/// scheduler.
|
||||
public struct ReceiveOn<Upstream: Publisher, Context: Scheduler>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The scheduler the publisher is to use for element delivery.
|
||||
public let scheduler: Context
|
||||
|
||||
/// Scheduler options that customize the delivery of elements.
|
||||
public let options: Context.SchedulerOptions?
|
||||
|
||||
public init(upstream: Upstream,
|
||||
scheduler: Context,
|
||||
options: Context.SchedulerOptions?) {
|
||||
self.upstream = upstream
|
||||
self.scheduler = scheduler
|
||||
self.options = options
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
upstream.subscribe(Inner(self, downstream: subscriber))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.ReceiveOn {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
typealias ReceiveOn = Publishers.ReceiveOn<Upstream, Context>
|
||||
|
||||
private enum State {
|
||||
case ready(ReceiveOn, Downstream)
|
||||
case subscribed(ReceiveOn, Downstream, Subscription)
|
||||
case terminal
|
||||
}
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
private var state: State
|
||||
private let downstreamLock = UnfairLock.allocate()
|
||||
|
||||
init(_ receiveOn: ReceiveOn, downstream: Downstream) {
|
||||
state = .ready(receiveOn, downstream)
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
downstreamLock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case let .ready(receiveOn, downstream) = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .subscribed(receiveOn, downstream, subscription)
|
||||
lock.unlock()
|
||||
receiveOn.scheduler.schedule(options: receiveOn.options) { [weak self] in
|
||||
self?.scheduledReceive(subscription: subscription)
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduledReceive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
downstreamLock.lock()
|
||||
downstream.receive(subscription: self)
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case let .subscribed(receiveOn, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
lock.unlock()
|
||||
receiveOn.scheduler.schedule(options: receiveOn.options) { [weak self] in
|
||||
self?.scheduledReceive(input, downstream: downstream)
|
||||
}
|
||||
return .none
|
||||
}
|
||||
|
||||
private func scheduledReceive(_ input: Upstream.Output, downstream: Downstream) {
|
||||
downstreamLock.lock()
|
||||
let newDemand = downstream.receive(input)
|
||||
downstreamLock.unlock()
|
||||
guard newDemand > 0 else {
|
||||
return
|
||||
}
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(newDemand)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(receiveOn, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
receiveOn.scheduler.schedule(options: receiveOn.options) { [weak self] in
|
||||
self?.scheduledReceive(completion: completion, downstream: downstream)
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduledReceive(completion: Subscribers.Completion<Failure>,
|
||||
downstream: Downstream) {
|
||||
downstreamLock.lock()
|
||||
downstream.receive(completion: completion)
|
||||
downstreamLock.unlock()
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "ReceiveOn" }
|
||||
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,9 @@
|
||||
// Created by Bogdan Vlad on 8/29/19.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
extension Publisher {
|
||||
/// Replaces any errors in the stream with the provided element.
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
// Created by Eric Patey on 26.08.2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
extension Publisher {
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
// Created by Sergej Jaskiewicz on 19.06.2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
extension Publishers {
|
||||
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
//
|
||||
// Publishers.SubscribeOn.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 02.12.2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Specifies the scheduler on which to perform subscribe, cancel, and request
|
||||
/// operations.
|
||||
///
|
||||
/// In contrast with `receive(on:options:)`, which affects downstream messages,
|
||||
/// `subscribe(on:)` changes the execution context of upstream messages.
|
||||
/// In the following example, requests to `jsonPublisher` are performed on
|
||||
/// `backgroundQueue`, but elements received from it are performed on `RunLoop.main`.
|
||||
///
|
||||
/// let ioPerformingPublisher == // Some publisher.
|
||||
/// let uiUpdatingSubscriber == // Some subscriber that updates the UI.
|
||||
///
|
||||
/// ioPerformingPublisher
|
||||
/// .subscribe(on: backgroundQueue)
|
||||
/// .receiveOn(on: RunLoop.main)
|
||||
/// .subscribe(uiUpdatingSubscriber)
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - scheduler: The scheduler on which to receive upstream messages.
|
||||
/// - options: Options that customize the delivery of elements.
|
||||
/// - Returns: A publisher which performs upstream operations on the specified
|
||||
/// scheduler.
|
||||
public func subscribe<Context: Scheduler>(
|
||||
on scheduler: Context,
|
||||
options: Context.SchedulerOptions? = nil
|
||||
) -> Publishers.SubscribeOn<Self, Context> {
|
||||
return .init(upstream: self, scheduler: scheduler, options: options)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that receives elements from an upstream publisher on a specific
|
||||
/// scheduler.
|
||||
public struct SubscribeOn<Upstream: Publisher, Context: Scheduler>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The scheduler the publisher should use to receive elements.
|
||||
public let scheduler: Context
|
||||
|
||||
/// Scheduler options that customize the delivery of elements.
|
||||
public let options: Context.SchedulerOptions?
|
||||
|
||||
public init(upstream: Upstream,
|
||||
scheduler: Context,
|
||||
options: Context.SchedulerOptions?) {
|
||||
self.upstream = upstream
|
||||
self.scheduler = scheduler
|
||||
self.options = options
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
scheduler.schedule(options: options) {
|
||||
self.upstream.subscribe(Inner(self, downstream: subscriber))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.SubscribeOn {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
typealias SubscribeOn = Publishers.SubscribeOn<Upstream, Context>
|
||||
|
||||
private enum State {
|
||||
case ready(SubscribeOn, Downstream)
|
||||
case subscribed(SubscribeOn, Downstream, Subscription)
|
||||
case terminal
|
||||
}
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
private var state: State
|
||||
private let upstreamLock = UnfairLock.allocate()
|
||||
|
||||
init(_ subscribeOn: SubscribeOn, downstream: Downstream) {
|
||||
state = .ready(subscribeOn, downstream)
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
upstreamLock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case let .ready(subscribeOn, downstream) = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .subscribed(subscribeOn, downstream, subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
lock.unlock()
|
||||
return downstream.receive(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, downstream, _) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscribeOn, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscribeOn.scheduler.schedule(options: subscribeOn.options) { [weak self] in
|
||||
self?.scheduledRequest(demand, subscription: subscription)
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduledRequest(_ demand: Subscribers.Demand,
|
||||
subscription: Subscription) {
|
||||
upstreamLock.lock()
|
||||
subscription.request(demand)
|
||||
upstreamLock.unlock()
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscribeOn, _, subscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
subscribeOn.scheduler.schedule(options: subscribeOn.options) { [weak self] in
|
||||
self?.scheduledCancel(subscription)
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduledCancel(_ subscription: Subscription) {
|
||||
upstreamLock.lock()
|
||||
subscription.cancel()
|
||||
upstreamLock.unlock()
|
||||
}
|
||||
|
||||
var description: String { return "SubscribeOn" }
|
||||
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,9 @@
|
||||
// Created by Sergej Jaskiewicz on 12.11.2019.
|
||||
//
|
||||
|
||||
#if canImport(COpenCombineHelpers)
|
||||
import COpenCombineHelpers
|
||||
#endif
|
||||
|
||||
/// A publisher that allows for recording a series of inputs and a completion for later
|
||||
/// playback to each subscriber.
|
||||
|
||||
@@ -164,13 +164,6 @@ extension XCTest {
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.cancelled, .cancelled])
|
||||
|
||||
let thirdSubscription = CustomSubscription()
|
||||
|
||||
try XCTUnwrap(helper.publisher.subscriber)
|
||||
.receive(subscription: thirdSubscription)
|
||||
|
||||
XCTAssertEqual(thirdSubscription.history, [.cancelled])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,13 +34,15 @@ import OpenCombine
|
||||
typealias CustomPublisher = CustomPublisherBase<Int, TestingError>
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
class CustomPublisherBase<Output, Failure: Error>: Publisher {
|
||||
class CustomPublisherBase<Output, Failure: Error>: Publisher, Cancellable {
|
||||
|
||||
private(set) var subscriber: AnySubscriber<Output, Failure>?
|
||||
private(set) var erasedSubscriber: Any?
|
||||
private let subscription: Subscription?
|
||||
|
||||
var onSubscribe: ((AnySubscriber<Output, Failure>) -> Void)?
|
||||
var willSubscribe: ((AnySubscriber<Output, Failure>) -> Void)?
|
||||
|
||||
var didSubscribe: ((AnySubscriber<Output, Failure>) -> Void)?
|
||||
|
||||
required init(subscription: Subscription?) {
|
||||
self.subscription = subscription
|
||||
@@ -51,9 +53,10 @@ class CustomPublisherBase<Output, Failure: Error>: Publisher {
|
||||
{
|
||||
let anySubscriber = AnySubscriber(subscriber)
|
||||
self.subscriber = anySubscriber
|
||||
onSubscribe?(anySubscriber)
|
||||
willSubscribe?(anySubscriber)
|
||||
erasedSubscriber = subscriber
|
||||
subscription.map(subscriber.receive(subscription:))
|
||||
didSubscribe?(anySubscriber)
|
||||
}
|
||||
|
||||
func send(subscription: CustomSubscription) {
|
||||
@@ -67,6 +70,11 @@ class CustomPublisherBase<Output, Failure: Error>: Publisher {
|
||||
func send(completion: Subscribers.Completion<Failure>) {
|
||||
subscriber?.receive(completion: completion)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
subscriber = nil
|
||||
erasedSubscriber = nil
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
//
|
||||
// FairPriorityQueue.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 02.12.2019.
|
||||
//
|
||||
|
||||
/// A priproty queue based on binary min-heap.
|
||||
/// If two elements with the same priority are added, the element that was added
|
||||
/// earlier has will have "better" priority (i. e. it will be also extracted earlier).
|
||||
struct FairPriorityQueue<Priority: Comparable, Element> {
|
||||
|
||||
private var storage: [((Priority, UInt), Element)] = []
|
||||
private var next: UInt = 0
|
||||
|
||||
init() {}
|
||||
|
||||
mutating func insert(_ element: Element, priority: Priority) {
|
||||
storage.append(((priority, next), element))
|
||||
next += 1
|
||||
var newElementIndex = storage.endIndex - 1
|
||||
while let parent = self.parent(of: newElementIndex),
|
||||
storage[parent].0 > storage[newElementIndex].0 {
|
||||
storage.swapAt(newElementIndex, parent)
|
||||
newElementIndex = parent
|
||||
}
|
||||
}
|
||||
|
||||
func min() -> Element? {
|
||||
return storage.first?.1
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
mutating func extractMin() -> (Priority, Element)? {
|
||||
guard let max = storage.first else { return nil }
|
||||
storage[0] = storage[storage.endIndex - 1]
|
||||
storage.removeLast()
|
||||
minHeapify(0)
|
||||
return (max.0.0, max.1)
|
||||
}
|
||||
|
||||
var count: Int {
|
||||
return storage.count
|
||||
}
|
||||
|
||||
var isEmpty: Bool {
|
||||
return storage.isEmpty
|
||||
}
|
||||
|
||||
private func leftChild(of index: Int) -> Int? {
|
||||
assert(index >= 0)
|
||||
let childIndex = 2 * index + 1
|
||||
return childIndex < storage.endIndex ? childIndex : nil
|
||||
}
|
||||
|
||||
private func rightChild(of index: Int) -> Int? {
|
||||
assert(index >= 0)
|
||||
let childIndex = 2 * index + 2
|
||||
return childIndex < storage.endIndex ? childIndex : nil
|
||||
}
|
||||
|
||||
private func parent(of index: Int) -> Int? {
|
||||
assert(index >= 0)
|
||||
if index == 0 { return nil }
|
||||
return (index - 1) / 2
|
||||
}
|
||||
|
||||
private mutating func minHeapify(_ root: Int) {
|
||||
var root = root
|
||||
var largest = root
|
||||
while true {
|
||||
assert(largest == root)
|
||||
if let left = leftChild(of: root), storage[root].0 > storage[left].0 {
|
||||
largest = left
|
||||
}
|
||||
if let right = rightChild(of: root), storage[largest].0 > storage[right].0 {
|
||||
largest = right
|
||||
}
|
||||
if largest == root {
|
||||
break
|
||||
}
|
||||
storage.swapAt(root, largest)
|
||||
root = largest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FairPriorityQueue: Sequence {
|
||||
struct Iterator: IteratorProtocol {
|
||||
private var queue: FairPriorityQueue
|
||||
|
||||
fileprivate init(_ queue: FairPriorityQueue) {
|
||||
self.queue = queue
|
||||
}
|
||||
|
||||
mutating func next() -> (Priority, Element)? {
|
||||
return queue.extractMin()
|
||||
}
|
||||
}
|
||||
|
||||
func makeIterator() -> Iterator {
|
||||
return Iterator(self)
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ func testLifecycle<UpstreamOutput, Operator: Publisher>(
|
||||
line: UInt = #line,
|
||||
sendValue valueToBeSent: UpstreamOutput,
|
||||
cancellingSubscriptionReleasesSubscriber: Bool,
|
||||
finishingIsPassedThrough: Bool = true,
|
||||
_ makeOperator: (PassthroughSubject<UpstreamOutput, TestingError>) -> Operator
|
||||
) throws {
|
||||
var deinitCounter = 0
|
||||
@@ -101,7 +102,11 @@ func testLifecycle<UpstreamOutput, Operator: Publisher>(
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
try XCTUnwrap(subscription, file: file, line: line).cancel()
|
||||
try XCTUnwrap(subscription,
|
||||
"Lifecycle test #3: subscription should be saved",
|
||||
file: file,
|
||||
line: line)
|
||||
.cancel()
|
||||
|
||||
if cancellingSubscriptionReleasesSubscriber {
|
||||
XCTAssertEqual(deinitCounter,
|
||||
@@ -134,8 +139,15 @@ func testLifecycle<UpstreamOutput, Operator: Publisher>(
|
||||
passthrough.send(completion: .finished)
|
||||
}
|
||||
|
||||
XCTAssertTrue(subscriberDestroyed,
|
||||
"Lifecycle test #4: deinit should be called",
|
||||
file: file,
|
||||
line: line)
|
||||
if finishingIsPassedThrough {
|
||||
XCTAssertTrue(subscriberDestroyed,
|
||||
"Lifecycle test #4: deinit should be called",
|
||||
file: file,
|
||||
line: line)
|
||||
} else {
|
||||
XCTAssertFalse(subscriberDestroyed,
|
||||
"Lifecycle test #4: deinit should not be called",
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,9 +79,9 @@ internal func testReflection<Output, Failure: Error, Operator: Publisher>(
|
||||
line: UInt = #line,
|
||||
parentInput: Output.Type,
|
||||
parentFailure: Failure.Type,
|
||||
description expectedDescription: String,
|
||||
description expectedDescription: String?,
|
||||
customMirror customMirrorPredicate: ((Mirror) -> Bool)?,
|
||||
playgroundDescription: String,
|
||||
playgroundDescription: String?,
|
||||
_ makeOperator: (CustomConnectablePublisherBase<Output, Failure>) -> Operator
|
||||
) throws {
|
||||
let publisher = CustomConnectablePublisherBase<Output, Failure>(subscription: nil)
|
||||
@@ -97,16 +97,18 @@ internal func testReflection<Output, Failure: Error, Operator: Publisher>(
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
let customMirror =
|
||||
try XCTUnwrap((erasedSubscriber as? CustomReflectable)?.customMirror,
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
if let customMirrorPredicate = customMirrorPredicate {
|
||||
let customMirror =
|
||||
try XCTUnwrap((erasedSubscriber as? CustomReflectable)?.customMirror,
|
||||
file: file,
|
||||
line: line)
|
||||
XCTAssert(customMirrorPredicate(customMirror),
|
||||
"customMirror doesn't satisfy the predicate",
|
||||
file: file,
|
||||
line: line)
|
||||
} else {
|
||||
XCTAssertFalse(erasedSubscriber is CustomReflectable,
|
||||
"subscriber shouldn't conform to CustomReflectable")
|
||||
}
|
||||
|
||||
XCTAssertFalse(erasedSubscriber is CustomDebugStringConvertible,
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
//
|
||||
// VirtualTimeScheduler.swift
|
||||
// OpenCombineTests
|
||||
//
|
||||
// Created by Евгений Богомолов on 14/09/2019.
|
||||
//
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class VirtualTimeScheduler: Scheduler {
|
||||
|
||||
struct SchedulerTimeType: Strideable,
|
||||
Comparable,
|
||||
Hashable,
|
||||
SchedulerTimeIntervalConvertible
|
||||
{
|
||||
|
||||
struct Stride: ExpressibleByFloatLiteral,
|
||||
Comparable,
|
||||
SignedNumeric,
|
||||
SchedulerTimeIntervalConvertible
|
||||
{
|
||||
var magnitude: Int64
|
||||
|
||||
fileprivate init(magnitude: Int64) {
|
||||
self.magnitude = magnitude
|
||||
}
|
||||
|
||||
init(integerLiteral value: Int) {
|
||||
self = .seconds(value)
|
||||
}
|
||||
|
||||
init(floatLiteral value: Double) {
|
||||
self = .seconds(value)
|
||||
}
|
||||
|
||||
init?<Source: BinaryInteger>(exactly source: Source) {
|
||||
guard let magnitude = Int64(exactly: source) else {
|
||||
return nil
|
||||
}
|
||||
self.init(magnitude: magnitude)
|
||||
}
|
||||
|
||||
static func < (lhs: Stride, rhs: Stride) -> Bool {
|
||||
return lhs.magnitude < rhs.magnitude
|
||||
}
|
||||
|
||||
static func * (lhs: Stride, rhs: Stride) -> Stride {
|
||||
return Stride(magnitude: lhs.magnitude * rhs.magnitude)
|
||||
}
|
||||
|
||||
static func + (lhs: Stride, rhs: Stride) -> Stride {
|
||||
return Stride(magnitude: lhs.magnitude + rhs.magnitude)
|
||||
}
|
||||
|
||||
static func - (lhs: Stride, rhs: Stride) -> Stride {
|
||||
return Stride(magnitude: lhs.magnitude - rhs.magnitude)
|
||||
}
|
||||
|
||||
static func -= (lhs: inout Stride, rhs: Stride) {
|
||||
lhs.magnitude -= rhs.magnitude
|
||||
}
|
||||
|
||||
static func *= (lhs: inout Stride, rhs: Stride) {
|
||||
lhs.magnitude *= rhs.magnitude
|
||||
}
|
||||
|
||||
static func += (lhs: inout Stride, rhs: Stride) {
|
||||
lhs.magnitude += rhs.magnitude
|
||||
}
|
||||
|
||||
static func seconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: Int64(value) * 1_000_000_000)
|
||||
}
|
||||
|
||||
static func seconds(_ value: Double) -> Stride {
|
||||
return Stride(magnitude: Int64(value * 1_000_000_000))
|
||||
}
|
||||
|
||||
static func milliseconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: Int64(value) * 1_000_000)
|
||||
}
|
||||
|
||||
static func microseconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: Int64(value) * 1_000)
|
||||
}
|
||||
|
||||
static func nanoseconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: Int64(value))
|
||||
}
|
||||
}
|
||||
|
||||
/// Time in virtual nanoseconds
|
||||
let time: UInt64
|
||||
|
||||
private init(nanoseconds time: UInt64) {
|
||||
self.time = time
|
||||
}
|
||||
|
||||
static func == (lhs: SchedulerTimeType, rhs: SchedulerTimeType) -> Bool {
|
||||
return lhs.time == rhs.time
|
||||
}
|
||||
|
||||
static func < (lhs: SchedulerTimeType, rhs: SchedulerTimeType) -> Bool {
|
||||
return lhs.time < rhs.time
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(time)
|
||||
}
|
||||
|
||||
func distance(to other: SchedulerTimeType) -> Stride {
|
||||
if self > other {
|
||||
return Stride(magnitude: -Int64(time - other.time))
|
||||
} else {
|
||||
return Stride(magnitude: Int64(other.time - time))
|
||||
}
|
||||
}
|
||||
|
||||
func advanced(by stride: Stride) -> SchedulerTimeType {
|
||||
return stride.magnitude < 0
|
||||
? SchedulerTimeType(nanoseconds: time - UInt64(-stride.magnitude))
|
||||
: SchedulerTimeType(nanoseconds: time + UInt64(stride.magnitude))
|
||||
}
|
||||
|
||||
static func + (lhs: SchedulerTimeType, rhs: Stride) -> SchedulerTimeType {
|
||||
return lhs.advanced(by: rhs)
|
||||
}
|
||||
|
||||
static let beginningOfTime = SchedulerTimeType(nanoseconds: 0)
|
||||
|
||||
static func seconds(_ value: Int) -> SchedulerTimeType {
|
||||
precondition(value >= 0, "value must not be negative")
|
||||
return .init(nanoseconds: UInt64(value) * 1_000_000_000)
|
||||
}
|
||||
|
||||
static func seconds(_ value: Double) -> SchedulerTimeType {
|
||||
precondition(value >= 0, "value must not be negative")
|
||||
return .init(nanoseconds: UInt64(value * 1_000_000_000))
|
||||
}
|
||||
|
||||
static func milliseconds(_ value: Int) -> SchedulerTimeType {
|
||||
precondition(value >= 0, "value must not be negative")
|
||||
return .init(nanoseconds: UInt64(value) * 1_000_000)
|
||||
}
|
||||
|
||||
static func microseconds(_ value: Int) -> SchedulerTimeType {
|
||||
precondition(value >= 0, "value must not be negative")
|
||||
return .init(nanoseconds: UInt64(value) * 1_000)
|
||||
}
|
||||
|
||||
static func nanoseconds(_ value: Int) -> SchedulerTimeType {
|
||||
precondition(value >= 0, "value must not be negative")
|
||||
return .init(nanoseconds: UInt64(value))
|
||||
}
|
||||
}
|
||||
|
||||
enum SchedulerOptions: Equatable, CustomStringConvertible {
|
||||
case nontrivialOptions
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .nontrivialOptions:
|
||||
return ".nontrivialOptions"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class CancellableToken: Cancellable {
|
||||
|
||||
private(set) var isCancelled = false
|
||||
|
||||
func cancel() {
|
||||
isCancelled = true
|
||||
}
|
||||
}
|
||||
|
||||
enum Event: Equatable, CustomStringConvertible {
|
||||
case now
|
||||
case minimumTolerance
|
||||
case schedule(options: SchedulerOptions?)
|
||||
case scheduleAfterDate(SchedulerTimeType,
|
||||
tolerance: SchedulerTimeType.Stride,
|
||||
options: SchedulerOptions?)
|
||||
case scheduleAfterDateWithInterval(SchedulerTimeType,
|
||||
interval: SchedulerTimeType.Stride,
|
||||
tolerance: SchedulerTimeType.Stride,
|
||||
options: SchedulerOptions?)
|
||||
|
||||
var description: String {
|
||||
|
||||
func describeOptions(_ options: SchedulerOptions?) -> String {
|
||||
return options.map(String.init(describing:)) ?? "nil"
|
||||
}
|
||||
|
||||
func describeDate(_ date: SchedulerTimeType) -> String {
|
||||
return ".nanoseconds(\(date.time)"
|
||||
}
|
||||
|
||||
func describeStride(_ stride: SchedulerTimeType.Stride) -> String {
|
||||
return ".nanoseconds(\(stride.magnitude))"
|
||||
}
|
||||
|
||||
switch self {
|
||||
case .now:
|
||||
return ".now"
|
||||
case .minimumTolerance:
|
||||
return ".minimumTolerance"
|
||||
case let .schedule(options):
|
||||
return ".schedule(options: \(describeOptions(options)))"
|
||||
case let .scheduleAfterDate(date, tolerance, options):
|
||||
return """
|
||||
.scheduleAfterDate(\(describeDate(date)), \
|
||||
tolerance: \(describeStride(tolerance)), \
|
||||
options: \(describeOptions(options)))
|
||||
"""
|
||||
case let .scheduleAfterDateWithInterval(date, interval, tolerance, options):
|
||||
return """
|
||||
.scheduleAfterDateWithInterval(\(describeDate(date)), \
|
||||
interval: \(describeStride(interval)), \
|
||||
tolerance: \(describeStride(tolerance)), \
|
||||
options: \(describeOptions(options)))
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var history = [Event]()
|
||||
|
||||
/// All private methods should reference this property instead of `now`
|
||||
/// to prevent polluting the scheduler history. Accessing `now` creates an entry
|
||||
/// in the `history` array.
|
||||
private var _now = SchedulerTimeType.beginningOfTime
|
||||
|
||||
private var workQueue = FairPriorityQueue<SchedulerTimeType, () -> Void>()
|
||||
|
||||
var scheduledDates: [SchedulerTimeType] {
|
||||
return workQueue.map { $0.0 }
|
||||
}
|
||||
|
||||
var now: SchedulerTimeType {
|
||||
history.append(.now)
|
||||
return _now
|
||||
}
|
||||
|
||||
var minimumTolerance: SchedulerTimeType.Stride {
|
||||
history.append(.minimumTolerance)
|
||||
return 0
|
||||
}
|
||||
|
||||
func schedule(options: SchedulerOptions?, _ action: @escaping () -> Void) {
|
||||
history.append(.schedule(options: options))
|
||||
workQueue.insert(action, priority: _now)
|
||||
}
|
||||
|
||||
func schedule(after date: SchedulerTimeType,
|
||||
tolerance: SchedulerTimeType.Stride,
|
||||
options: SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) {
|
||||
history.append(.scheduleAfterDate(date, tolerance: tolerance, options: options))
|
||||
workQueue.insert(action, priority: date)
|
||||
}
|
||||
|
||||
func schedule(after date: SchedulerTimeType,
|
||||
interval: SchedulerTimeType.Stride,
|
||||
tolerance: SchedulerTimeType.Stride,
|
||||
options: SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) -> Cancellable {
|
||||
history.append(.scheduleAfterDateWithInterval(date,
|
||||
interval: interval,
|
||||
tolerance: tolerance,
|
||||
options: options))
|
||||
let cancellableToken = CancellableToken()
|
||||
repeatedlyExecute(after: date,
|
||||
interval: interval,
|
||||
cancellableToken: cancellableToken,
|
||||
action: action)
|
||||
return cancellableToken
|
||||
}
|
||||
|
||||
private func repeatedlyExecute(after date: SchedulerTimeType,
|
||||
interval: SchedulerTimeType.Stride,
|
||||
cancellableToken: CancellableToken,
|
||||
action: @escaping () -> Void) {
|
||||
let enqueuedAction: () -> Void = { [unowned self] in
|
||||
if cancellableToken.isCancelled { return }
|
||||
action()
|
||||
self.repeatedlyExecute(after: date + interval,
|
||||
interval: interval,
|
||||
cancellableToken: cancellableToken,
|
||||
action: action)
|
||||
}
|
||||
workQueue.insert(enqueuedAction, priority: date)
|
||||
}
|
||||
|
||||
/// Sets `now` to the provided value. Useful for testing that an entity that
|
||||
/// uses the scheduler doesn't rely on clock monotonicity.
|
||||
///
|
||||
/// - Note: The actions that were already executed will not be executed again.
|
||||
/// This function does **not** provide time machine-like functionality.
|
||||
func rewind(to time: SchedulerTimeType) {
|
||||
_now = time
|
||||
}
|
||||
|
||||
func executeScheduledActions() {
|
||||
while let (time, action) = workQueue.extractMin() {
|
||||
_now = max(time, _now)
|
||||
action()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,19 @@ import XCTest
|
||||
|
||||
// FIXME: XCTUnwrap is unavailable in Swift Package Manager yet.
|
||||
|
||||
private struct UnwrappingFailure: Error {}
|
||||
private struct UnwrappingFailure: Error, LocalizedError {
|
||||
|
||||
let message: String
|
||||
|
||||
var errorDescription: String? {
|
||||
var failureDescription = "XCTUnwrap failed"
|
||||
if !message.isEmpty {
|
||||
failureDescription += ": "
|
||||
failureDescription += message
|
||||
}
|
||||
return failureDescription
|
||||
}
|
||||
}
|
||||
|
||||
/// Asserts that an expression is not `nil`, and returns its unwrapped value.
|
||||
///
|
||||
@@ -32,10 +44,11 @@ public func XCTUnwrap<Result>(_ expression: @autoclosure () throws -> Result?,
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line) throws -> Result {
|
||||
let result = try expression()
|
||||
XCTAssertNotNil(result, message(), file: file, line: line)
|
||||
if let result = result {
|
||||
return result
|
||||
} else {
|
||||
throw UnwrappingFailure()
|
||||
let error = UnwrappingFailure(message: message())
|
||||
XCTFail(error.errorDescription ?? "", file: file, line: line)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
//
|
||||
// BreakpointTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 03.12.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class BreakpointTests: XCTestCase {
|
||||
|
||||
func testReceiveSubscription() {
|
||||
var shouldStop = false
|
||||
var counter = 0
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(1)) {
|
||||
$0.breakpoint(receiveSubscription: { _ in counter += 1; return shouldStop })
|
||||
}
|
||||
|
||||
XCTAssertNotNil(helper.sut.receiveSubscription)
|
||||
XCTAssertNil(helper.sut.receiveOutput)
|
||||
XCTAssertNil(helper.sut.receiveCompletion)
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(12), .max(1))
|
||||
helper.publisher.send(completion: .finished)
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.publisher.send(21), .max(1))
|
||||
helper.publisher.send(subscription: CustomSubscription())
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CustomSubscription"),
|
||||
.value(12),
|
||||
.completion(.finished),
|
||||
.completion(.failure(.oops)),
|
||||
.value(21),
|
||||
.subscription("CustomSubscription")])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
shouldStop = true
|
||||
XCTAssertEqual(counter, 2)
|
||||
assertCrashes {
|
||||
helper.publisher.send(subscription: CustomSubscription())
|
||||
}
|
||||
}
|
||||
|
||||
func testReceiveValue() {
|
||||
var counter = 0
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(1)) {
|
||||
$0.breakpoint(receiveOutput: { counter += 1; return $0 < 0 })
|
||||
}
|
||||
|
||||
XCTAssertNil(helper.sut.receiveSubscription)
|
||||
XCTAssertNotNil(helper.sut.receiveOutput)
|
||||
XCTAssertNil(helper.sut.receiveCompletion)
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(12), .max(1))
|
||||
helper.publisher.send(completion: .finished)
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.publisher.send(21), .max(1))
|
||||
helper.publisher.send(subscription: CustomSubscription())
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CustomSubscription"),
|
||||
.value(12),
|
||||
.completion(.finished),
|
||||
.completion(.failure(.oops)),
|
||||
.value(21),
|
||||
.subscription("CustomSubscription")])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(counter, 2)
|
||||
assertCrashes {
|
||||
_ = helper.publisher.send(-1)
|
||||
}
|
||||
}
|
||||
|
||||
func testReceiveCompletion() {
|
||||
var counter = 0
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(1)) {
|
||||
$0.breakpoint(receiveCompletion: { counter += 1; return $0 == .finished })
|
||||
}
|
||||
|
||||
XCTAssertNil(helper.sut.receiveSubscription)
|
||||
XCTAssertNil(helper.sut.receiveOutput)
|
||||
XCTAssertNotNil(helper.sut.receiveCompletion)
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(12), .max(1))
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.publisher.send(21), .max(1))
|
||||
helper.publisher.send(subscription: CustomSubscription())
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CustomSubscription"),
|
||||
.value(12),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops)),
|
||||
.value(21),
|
||||
.subscription("CustomSubscription")])
|
||||
XCTAssertEqual(counter, 2)
|
||||
assertCrashes {
|
||||
helper.publisher.send(completion: .finished)
|
||||
}
|
||||
}
|
||||
|
||||
func testBreakpointOnError() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(1)) {
|
||||
$0.breakpointOnError()
|
||||
}
|
||||
|
||||
XCTAssertNil(helper.sut.receiveSubscription)
|
||||
XCTAssertNil(helper.sut.receiveOutput)
|
||||
XCTAssertNotNil(helper.sut.receiveCompletion)
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(12), .max(1))
|
||||
helper.publisher.send(completion: .finished)
|
||||
helper.publisher.send(completion: .finished)
|
||||
XCTAssertEqual(helper.publisher.send(21), .max(1))
|
||||
helper.publisher.send(subscription: CustomSubscription())
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CustomSubscription"),
|
||||
.value(12),
|
||||
.completion(.finished),
|
||||
.completion(.finished),
|
||||
.value(21),
|
||||
.subscription("CustomSubscription")])
|
||||
|
||||
XCTAssertEqual(helper.sut.receiveCompletion?(.finished), false)
|
||||
XCTAssertEqual(helper.sut.receiveCompletion?(.failure(.oops)), true)
|
||||
|
||||
assertCrashes {
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
}
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none) {
|
||||
$0.breakpointOnError()
|
||||
}
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(14))
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(100))
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.max(14)),
|
||||
.cancelled,
|
||||
.requested(.max(100)),
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
func testBreakpointReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "Breakpoint",
|
||||
customMirror: expectedChildren(
|
||||
("upstream", .contains("CustomConnectablePublisherBase"))
|
||||
),
|
||||
playgroundDescription: "Breakpoint",
|
||||
{ $0.breakpointOnError() })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
//
|
||||
// ConcatenateTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 09.12.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class ConcatenateTests: XCTestCase {
|
||||
|
||||
func testAppendBasicBehavior() {
|
||||
let subscription1 = CustomSubscription()
|
||||
let subscription2 = CustomSubscription()
|
||||
let publisher1 = CustomPublisher(subscription: subscription1)
|
||||
let publisher2 = CustomPublisher(subscription: subscription2)
|
||||
|
||||
let append = publisher1.append(publisher2)
|
||||
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.max(10)) },
|
||||
receiveValue: { .max($0) },
|
||||
receiveCompletion: { _ in }
|
||||
)
|
||||
|
||||
append.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate")])
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10))])
|
||||
XCTAssertEqual(subscription2.history, [])
|
||||
|
||||
XCTAssertEqual(publisher1.send(1), .max(1))
|
||||
XCTAssertEqual(publisher2.send(-1), .none)
|
||||
XCTAssertEqual(publisher1.send(2), .max(2))
|
||||
XCTAssertEqual(publisher2.send(-2), .none)
|
||||
XCTAssertEqual(publisher1.send(3), .max(3))
|
||||
XCTAssertEqual(publisher2.send(-3), .none)
|
||||
publisher2.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10))])
|
||||
XCTAssertEqual(subscription2.history, [])
|
||||
|
||||
publisher1.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10))])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(13))])
|
||||
|
||||
XCTAssertEqual(publisher1.send(4000), .max(4000))
|
||||
XCTAssertEqual(publisher2.send(5), .max(5))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.value(4000),
|
||||
.value(5)])
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10))])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(13))])
|
||||
|
||||
publisher2.send(completion: .finished)
|
||||
publisher2.send(completion: .finished)
|
||||
publisher1.send(completion: .finished)
|
||||
publisher1.send(completion: .failure(.oops))
|
||||
publisher2.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(publisher1.send(6000), .max(6000))
|
||||
XCTAssertEqual(publisher2.send(7), .max(7))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.value(4000),
|
||||
.value(5),
|
||||
.completion(.finished),
|
||||
.completion(.finished),
|
||||
.completion(.finished),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops)),
|
||||
.value(6000),
|
||||
.value(7)])
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10))])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(13))])
|
||||
|
||||
let subscription3 = CustomSubscription()
|
||||
publisher2.send(subscription: subscription3)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.value(4000),
|
||||
.value(5),
|
||||
.completion(.finished),
|
||||
.completion(.finished),
|
||||
.completion(.finished),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops)),
|
||||
.value(6000),
|
||||
.value(7)])
|
||||
XCTAssertEqual(subscription3.history, [.cancelled])
|
||||
}
|
||||
|
||||
func testConcatenateTwoSequences() {
|
||||
let sequence1: Publishers.Sequence =
|
||||
sequence(first: 1, next: { $0 > 20 ? nil : $0 * 2 })
|
||||
.publisher
|
||||
|
||||
let sequence2: Publishers.Sequence = (33 ..< 40).publisher
|
||||
|
||||
let expected = [1, 2, 4, 8, 16, 32, 33, 34, 35, 36, 37, 38, 39]
|
||||
|
||||
var historyAppend = [Int]()
|
||||
var appendCompleted = false
|
||||
let append: Publishers.Concatenate = sequence1.append(sequence2)
|
||||
let cancellableAppend = append
|
||||
.sink(receiveCompletion: { _ in appendCompleted = true },
|
||||
receiveValue: { historyAppend.append($0) })
|
||||
XCTAssertEqual(historyAppend, expected)
|
||||
XCTAssertTrue(appendCompleted)
|
||||
cancellableAppend.cancel()
|
||||
|
||||
var historyPrepend = [Int]()
|
||||
var prependCompleted = false
|
||||
let prepend: Publishers.Concatenate = sequence2.prepend(sequence1)
|
||||
let cancellablePrepend = prepend
|
||||
.sink(receiveCompletion: { _ in prependCompleted = true },
|
||||
receiveValue: { historyPrepend.append($0) })
|
||||
XCTAssertEqual(historyPrepend, [1, 2, 4, 8, 16, 32, 33, 34, 35, 36, 37, 38, 39])
|
||||
XCTAssertTrue(prependCompleted)
|
||||
cancellablePrepend.cancel()
|
||||
}
|
||||
|
||||
func testPrefixFailureFailsDownstream() {
|
||||
let subscription1 = CustomSubscription()
|
||||
let subscription2 = CustomSubscription()
|
||||
let publisher1 = CustomPublisher(subscription: subscription1)
|
||||
let publisher2 = CustomPublisher(subscription: subscription2)
|
||||
|
||||
let append = publisher1.append(publisher2)
|
||||
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.max(10)) },
|
||||
receiveValue: { .max($0) },
|
||||
receiveCompletion: { _ in }
|
||||
)
|
||||
|
||||
append.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate")])
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10))])
|
||||
XCTAssertEqual(subscription2.history, [])
|
||||
|
||||
publisher1.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10))])
|
||||
XCTAssertEqual(subscription2.history, [])
|
||||
|
||||
XCTAssertEqual(publisher1.send(2), .max(2))
|
||||
publisher1.send(completion: .finished)
|
||||
XCTAssertEqual(publisher2.send(3), .max(3))
|
||||
publisher2.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.completion(.failure(.oops)),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10))])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(11))])
|
||||
}
|
||||
|
||||
func testSubscribesToUpstreamThenSendsSubscriptionDownstream() {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let append = publisher.append(1, 2, 3)
|
||||
let tracking = TrackingSubscriber()
|
||||
|
||||
var didSubscribe = false
|
||||
|
||||
publisher.didSubscribe = { _ in
|
||||
XCTAssertEqual(tracking.history, [])
|
||||
didSubscribe = true
|
||||
}
|
||||
|
||||
XCTAssertEqual(tracking.history, [])
|
||||
|
||||
append.subscribe(tracking)
|
||||
|
||||
XCTAssertTrue(didSubscribe)
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate")])
|
||||
}
|
||||
|
||||
func testBackpressure() throws {
|
||||
let subscription1 = CustomSubscription()
|
||||
let subscription2 = CustomSubscription()
|
||||
let publisher1 = CustomPublisher(subscription: subscription1)
|
||||
let publisher2 = CustomPublisher(subscription: subscription2)
|
||||
|
||||
let append = publisher1.append(publisher2)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { downstreamSubscription = $0; $0.request(.max(10)) },
|
||||
receiveValue: { .max($0) },
|
||||
receiveCompletion: { _ in }
|
||||
)
|
||||
|
||||
publisher2.willSubscribe = { _ in
|
||||
downstreamSubscription?.request(.max(7))
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10)),
|
||||
.requested(.none)])
|
||||
XCTAssertEqual(subscription2.history, [])
|
||||
}
|
||||
|
||||
append.subscribe(tracking)
|
||||
try XCTUnwrap(downstreamSubscription).request(.none)
|
||||
XCTAssertEqual(publisher1.send(3), .max(3))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.value(3)])
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10)),
|
||||
.requested(.none)])
|
||||
XCTAssertEqual(subscription2.history, [])
|
||||
|
||||
publisher1.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10)),
|
||||
.requested(.none)])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(19))])
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).request(.unlimited)
|
||||
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10)),
|
||||
.requested(.none)])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(19)),
|
||||
.requested(.unlimited)])
|
||||
|
||||
publisher2.send(completion: .finished)
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(42))
|
||||
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(10)),
|
||||
.requested(.none)])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(19)),
|
||||
.requested(.unlimited),
|
||||
.requested(.max(42))])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let subscription1 = CustomSubscription()
|
||||
let subscription2 = CustomSubscription()
|
||||
let publisher1 = CustomPublisher(subscription: subscription1)
|
||||
let publisher2 = CustomPublisher(subscription: subscription2)
|
||||
|
||||
let append = publisher1.append(publisher2)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { downstreamSubscription = $0 },
|
||||
receiveValue: { .max($0) },
|
||||
receiveCompletion: { _ in }
|
||||
)
|
||||
append.subscribe(tracking)
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(2)) // total demand is 2
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(3)) // total demand is 5
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate")])
|
||||
XCTAssertEqual(subscription1.history, [.cancelled])
|
||||
XCTAssertEqual(subscription2.history, [])
|
||||
|
||||
XCTAssertEqual(publisher1.send(0), .none) // total demand is 4
|
||||
publisher1.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.value(0)])
|
||||
XCTAssertEqual(subscription1.history, [.cancelled])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(4))])
|
||||
|
||||
XCTAssertEqual(publisher1.send(0), .none) // total demand is 3
|
||||
publisher1.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.value(0),
|
||||
.value(0),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(subscription1.history, [.cancelled])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(4))])
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.value(0),
|
||||
.value(0),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(subscription1.history, [.cancelled])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(4)),
|
||||
.cancelled])
|
||||
|
||||
let subscription3 = CustomSubscription()
|
||||
publisher2.send(subscription: subscription3)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Concatenate"),
|
||||
.value(0),
|
||||
.value(0),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(subscription1.history, [.cancelled])
|
||||
XCTAssertEqual(subscription2.history, [.requested(.max(4)),
|
||||
.cancelled])
|
||||
XCTAssertEqual(subscription3.history, [.cancelled])
|
||||
}
|
||||
|
||||
func testRecursivelyReceiveValue() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.append() })
|
||||
|
||||
var recursion = 10
|
||||
helper.tracking.onValue = {
|
||||
if recursion == 0 { return }
|
||||
recursion -= 1
|
||||
XCTAssertEqual(helper.publisher.send($0 + 1), .none)
|
||||
}
|
||||
|
||||
assertCrashes {
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
}
|
||||
}
|
||||
|
||||
func testRecursivelyReceiveFailure() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.append() })
|
||||
|
||||
var recursion = 10
|
||||
helper.tracking.onFailure = { _ in
|
||||
if recursion == 0 { return }
|
||||
recursion -= 1
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
}
|
||||
|
||||
assertCrashes {
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
}
|
||||
}
|
||||
|
||||
func testHelperMethods() {
|
||||
let publisher = CustomPublisher(subscription: nil)
|
||||
XCTAssertEqual(publisher.append(2, 3, 5, 7).suffix.sequence, [2, 3, 5, 7])
|
||||
XCTAssertEqual(publisher.append(CollectionOfOne(42)).suffix.sequence.first, 42)
|
||||
XCTAssertEqual(publisher.prepend(7, 5, 3, 2).prefix.sequence, [7, 5, 3, 2])
|
||||
XCTAssertEqual(publisher.prepend(CollectionOfOne(42)).prefix.sequence.first, 42)
|
||||
}
|
||||
|
||||
func testConcatenateReceiveValueBeforeSubscription() {
|
||||
testReceiveValueBeforeSubscription(value: 12,
|
||||
expected: .crash,
|
||||
{ $0.append(1, 2, 3) })
|
||||
|
||||
testReceiveValueBeforeSubscription(value: 12,
|
||||
expected: .crash,
|
||||
{ $0.prepend(1, 2, 3) })
|
||||
}
|
||||
|
||||
func testConcatenateReceiveCompletionBeforeSubscription() {
|
||||
testReceiveCompletionBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([.subscription("Concatenate")]),
|
||||
{ $0.append(1, 2, 3) }
|
||||
)
|
||||
|
||||
testReceiveCompletionBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([.subscription("Concatenate")]),
|
||||
{ $0.prepend(1, 2, 3) }
|
||||
)
|
||||
}
|
||||
|
||||
func testConcatenateRequestBeforeSubscription() {
|
||||
testRequestBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
{ $0.append(1, 2, 3) })
|
||||
}
|
||||
|
||||
func testConcatenateCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
{ $0.append(1, 2, 3) })
|
||||
}
|
||||
|
||||
func testConcatenateReceiveSubscriptionTwice() throws {
|
||||
try testReceiveSubscriptionTwice { $0.append(1, 2, 3) }
|
||||
}
|
||||
|
||||
func testConcatenateReflection() throws {
|
||||
try testReflection(parentInput: Float.self,
|
||||
parentFailure: TestingError.self,
|
||||
description: "Concatenate",
|
||||
customMirror: expectedChildren(
|
||||
("downstream", .contains("TrackingSubscriberBase")),
|
||||
("upstreamSubscription", "nil"),
|
||||
("suffix", .contains("(sequence: [2.0, 3.0, 5.0, 7.0])")),
|
||||
("demand", "max(0)")
|
||||
),
|
||||
playgroundDescription: "Concatenate",
|
||||
{ $0.append(2, 3, 5, 7) })
|
||||
}
|
||||
|
||||
func testConcatenateLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: true,
|
||||
{ $0.append() })
|
||||
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
finishingIsPassedThrough: false,
|
||||
{ $0.prepend(1, 2, 3) })
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
import XCTest
|
||||
|
||||
//import Combine
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
|
||||
@@ -0,0 +1,466 @@
|
||||
//
|
||||
// DelayTests.swift
|
||||
// OpenCombineTests
|
||||
//
|
||||
// Created by Евгений Богомолов on 08/09/2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class DelayTests: XCTestCase {
|
||||
|
||||
// Delay's Inner doesn't conform to CustomStringConvertible, so we can't compare
|
||||
// subscriptions using their descriptions
|
||||
let delaySubscription: StringSubscription = {
|
||||
let tracking = TrackingSubscriber()
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
CustomPublisher(subscription: CustomSubscription())
|
||||
.delay(for: 0, scheduler: scheduler)
|
||||
.subscribe(tracking)
|
||||
scheduler.executeScheduledActions()
|
||||
return tracking.subscriptions.first.map(StringSubscription.subscription)
|
||||
?? "Delay"
|
||||
}()
|
||||
|
||||
func testBasicBehavior() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(100),
|
||||
receiveValueDemand: .max(12)) {
|
||||
$0.delay(for: .nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
scheduler: scheduler,
|
||||
options: .nontrivialOptions)
|
||||
}
|
||||
XCTAssertNotNil(helper.publisher.subscriber)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: .nontrivialOptions)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100))])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: .nontrivialOptions)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100))])
|
||||
XCTAssertEqual(scheduler.scheduledDates, [.nanoseconds(200),
|
||||
.nanoseconds(200),
|
||||
.nanoseconds(200)])
|
||||
|
||||
XCTAssertEqual(scheduler.history,
|
||||
[.schedule(options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100)),
|
||||
.requested(.max(12)),
|
||||
.requested(.max(12)),
|
||||
.requested(.max(12))])
|
||||
|
||||
XCTAssertEqual(scheduler.history,
|
||||
[.schedule(options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions)])
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
helper.publisher.send(completion: .finished)
|
||||
XCTAssertEqual(helper.publisher.send(4), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100)),
|
||||
.requested(.max(12)),
|
||||
.requested(.max(12)),
|
||||
.requested(.max(12))])
|
||||
XCTAssertEqual(scheduler.scheduledDates, [.nanoseconds(400)])
|
||||
XCTAssertEqual(scheduler.history,
|
||||
[.schedule(options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(400),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100)),
|
||||
.requested(.max(12)),
|
||||
.requested(.max(12)),
|
||||
.requested(.max(12))])
|
||||
XCTAssertEqual(scheduler.history,
|
||||
[.schedule(options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(400),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions)])
|
||||
XCTAssertEqual(scheduler.now, .nanoseconds(400))
|
||||
}
|
||||
|
||||
func testRequest() throws {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none) {
|
||||
$0.delay(for: .nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
scheduler: scheduler,
|
||||
options: .nontrivialOptions)
|
||||
}
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription)])
|
||||
|
||||
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(delaySubscription)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(10)),
|
||||
.requested(.max(4)),
|
||||
.requested(.max(5)),
|
||||
.requested(.none)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription),
|
||||
.value(2000)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(10)),
|
||||
.requested(.max(4)),
|
||||
.requested(.max(5)),
|
||||
.requested(.none)])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none) {
|
||||
$0.delay(for: .nanoseconds(200),
|
||||
tolerance: .nanoseconds(5),
|
||||
scheduler: scheduler,
|
||||
options: .nontrivialOptions)
|
||||
}
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription)])
|
||||
|
||||
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(delaySubscription)])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: .nontrivialOptions)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(0), .none)
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription)])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: .nontrivialOptions)])
|
||||
}
|
||||
|
||||
func testReceiveCompletionImmediatelyAfterSubscription() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none) {
|
||||
$0.delay(for: .nanoseconds(123),
|
||||
tolerance: .nanoseconds(5),
|
||||
scheduler: scheduler,
|
||||
options: .nontrivialOptions)
|
||||
}
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(scheduler.history,
|
||||
[.schedule(options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(123),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
}
|
||||
|
||||
func testReceiveCompletionImmediatelyAfterValue() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .max(418)) {
|
||||
$0.delay(for: .nanoseconds(123),
|
||||
tolerance: .nanoseconds(5),
|
||||
scheduler: scheduler,
|
||||
options: .nontrivialOptions)
|
||||
}
|
||||
XCTAssertEqual(helper.publisher.send(-1), .none)
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1000), .none)
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription),
|
||||
.value(-1)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.max(418))])
|
||||
XCTAssertEqual(scheduler.history,
|
||||
[.schedule(options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(123),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(246),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions),
|
||||
.now,
|
||||
.scheduleAfterDate(.nanoseconds(246),
|
||||
tolerance: .nanoseconds(5),
|
||||
options: .nontrivialOptions)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(delaySubscription),
|
||||
.value(-1),
|
||||
.value(1000),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.max(418))])
|
||||
}
|
||||
|
||||
func testCrashesWhenReceivingInputRecursively() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .max(418)) {
|
||||
$0.delay(for: .nanoseconds(123), scheduler: ImmediateScheduler.shared)
|
||||
}
|
||||
|
||||
helper.tracking.onValue = { _ in
|
||||
_ = helper.publisher.send(-1)
|
||||
}
|
||||
|
||||
assertCrashes {
|
||||
_ = helper.publisher.send(0)
|
||||
}
|
||||
}
|
||||
|
||||
func testReceiveCompletionRecursively() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .max(418)) {
|
||||
$0.delay(for: .nanoseconds(123), scheduler: ImmediateScheduler.shared)
|
||||
}
|
||||
helper.tracking.onFinish = {
|
||||
helper.publisher.send(completion: .finished)
|
||||
}
|
||||
helper.publisher.send(completion: .finished)
|
||||
}
|
||||
|
||||
func testWeakCaptureWhenSchedulingSubscription() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
var subscription: Subscription?
|
||||
var subscriberReleased = false
|
||||
do {
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
let delay = publisher.delay(for: 0.35, scheduler: scheduler)
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { subscription = $0 },
|
||||
onDeinit: { subscriberReleased = true })
|
||||
delay.subscribe(tracking)
|
||||
XCTAssertEqual(tracking.history, [])
|
||||
XCTAssertEqual(scheduler.history, [.minimumTolerance,
|
||||
.schedule(options: nil)])
|
||||
publisher.cancel()
|
||||
}
|
||||
XCTAssertTrue(subscriberReleased)
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertNil(subscription)
|
||||
}
|
||||
|
||||
func testWeakCaptureWhenSchedulingValue() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
var value: Int?
|
||||
var subscriberReleased = false
|
||||
do {
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
let delay = publisher.delay(for: 0.35, scheduler: scheduler)
|
||||
let tracking = TrackingSubscriber(receiveValue: { value = $0; return .none },
|
||||
onDeinit: { subscriberReleased = true })
|
||||
delay.subscribe(tracking)
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(tracking.history, [.subscription(delaySubscription)])
|
||||
XCTAssertEqual(publisher.send(42), .none)
|
||||
XCTAssertEqual(tracking.history, [.subscription(delaySubscription)])
|
||||
XCTAssertEqual(scheduler.history,
|
||||
[.minimumTolerance,
|
||||
.schedule(options: nil),
|
||||
.now,
|
||||
.scheduleAfterDate(.seconds(0.35),
|
||||
tolerance: 0,
|
||||
options: nil)])
|
||||
tracking.cancel()
|
||||
publisher.cancel()
|
||||
}
|
||||
XCTAssertFalse(subscriberReleased)
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertNil(value)
|
||||
XCTAssertTrue(subscriberReleased)
|
||||
}
|
||||
|
||||
func testWeakCaptureWhenSchedulingCompletion() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
var completion: Subscribers.Completion<TestingError>?
|
||||
var subscriberReleased = false
|
||||
do {
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
let delay = publisher.delay(for: 0.35, scheduler: scheduler)
|
||||
let tracking = TrackingSubscriber(receiveCompletion: { completion = $0 },
|
||||
onDeinit: { subscriberReleased = true })
|
||||
delay.subscribe(tracking)
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(tracking.history, [.subscription(delaySubscription)])
|
||||
publisher.send(completion: .finished)
|
||||
XCTAssertEqual(tracking.history, [.subscription(delaySubscription)])
|
||||
XCTAssertEqual(scheduler.history,
|
||||
[.minimumTolerance,
|
||||
.schedule(options: nil),
|
||||
.now,
|
||||
.scheduleAfterDate(.seconds(0.35),
|
||||
tolerance: 0,
|
||||
options: nil)])
|
||||
tracking.cancel()
|
||||
publisher.cancel()
|
||||
}
|
||||
XCTAssertFalse(subscriberReleased)
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertNil(completion)
|
||||
XCTAssertTrue(subscriberReleased)
|
||||
}
|
||||
|
||||
func testDelayReceiveSubscriptionTwice() throws {
|
||||
try testReceiveSubscriptionTwice {
|
||||
$0.delay(for: 0.35, scheduler: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testDelayReceiveValueBeforeSubscription() {
|
||||
testReceiveValueBeforeSubscription(value: 213,
|
||||
expected: .history([], demand: .none)) {
|
||||
$0.delay(for: 0.35, scheduler: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testDelayReceiveCompletionBeforeSubscription() {
|
||||
testReceiveCompletionBeforeSubscription(inputType: Int.self,
|
||||
expected: .history([])) {
|
||||
$0.delay(for: 0.35, scheduler: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testDelayRequestBeforeSubscription() {
|
||||
testRequestBeforeSubscription(inputType: Int.self, shouldCrash: false) {
|
||||
$0.delay(for: 0.35, scheduler: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testDelayCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self, shouldCrash: false) {
|
||||
$0.delay(for: 0.35, scheduler: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testDelayReflection() throws {
|
||||
/// Delay's Inner doesn't customize its reflection
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: nil,
|
||||
customMirror: nil,
|
||||
playgroundDescription: nil) {
|
||||
$0.delay(for: 42, scheduler: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testDelayLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: true) {
|
||||
$0.delay(for: 42, scheduler: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ final class FlatMapTests: XCTestCase {
|
||||
XCTAssertNil(upstream.erasedSubscriber)
|
||||
}
|
||||
)
|
||||
upstream.onSubscribe = { _ in
|
||||
upstream.willSubscribe = { _ in
|
||||
upstreamReceivedSubscriber = true
|
||||
XCTAssertEqual(tracking.history, [.subscription("FlatMap")])
|
||||
}
|
||||
@@ -564,7 +564,7 @@ final class FlatMapTests: XCTestCase {
|
||||
createSut: { $0.flatMap(maxPublishers: .max(1)) { $0 } }
|
||||
)
|
||||
|
||||
child.onSubscribe = { subscriber in
|
||||
child.willSubscribe = { subscriber in
|
||||
helper.publisher.send(completion: .finished)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
//
|
||||
// HandleEventsTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 03.12.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class HandleEventsTests: XCTestCase {
|
||||
|
||||
func testBasicBehavior() throws {
|
||||
var history = [Event<TestingError>]()
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let handleEvents = publisher.handleAllEvents { history.append($0) }
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
XCTAssertNil(publisher.subscriber)
|
||||
downstreamSubscription = $0
|
||||
},
|
||||
receiveValue: { .max($0) }
|
||||
)
|
||||
handleEvents.subscribe(tracking)
|
||||
|
||||
XCTAssertNotNil(publisher.subscriber)
|
||||
XCTAssertEqual(tracking.history, [.subscription("HandleEvents")])
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
XCTAssertEqual(history, [.receiveSubscription("CustomSubscription")])
|
||||
|
||||
XCTAssertEqual(publisher.send(0), .none)
|
||||
XCTAssertEqual(publisher.send(1), .max(1))
|
||||
XCTAssertEqual(publisher.send(2), .max(2))
|
||||
XCTAssertEqual(publisher.send(3), .max(3))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("HandleEvents"),
|
||||
.value(0),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
XCTAssertEqual(history, [.receiveSubscription("CustomSubscription"),
|
||||
.receiveOutput(0),
|
||||
.receiveOutput(1),
|
||||
.receiveRequest(.max(1)),
|
||||
.receiveOutput(2),
|
||||
.receiveRequest(.max(2)),
|
||||
.receiveOutput(3),
|
||||
.receiveRequest(.max(3))])
|
||||
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(14))
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(10))
|
||||
try XCTUnwrap(downstreamSubscription).request(.none)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("HandleEvents"),
|
||||
.value(0),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(14)),
|
||||
.requested(.max(10)),
|
||||
.requested(.none)])
|
||||
XCTAssertEqual(history, [.receiveSubscription("CustomSubscription"),
|
||||
.receiveOutput(0),
|
||||
.receiveOutput(1),
|
||||
.receiveRequest(.max(1)),
|
||||
.receiveOutput(2),
|
||||
.receiveRequest(.max(2)),
|
||||
.receiveOutput(3),
|
||||
.receiveRequest(.max(3)),
|
||||
.receiveRequest(.max(14)),
|
||||
.receiveRequest(.max(10)),
|
||||
.receiveRequest(.none)])
|
||||
|
||||
publisher.send(completion: .finished)
|
||||
publisher.send(completion: .failure(.oops))
|
||||
publisher.send(completion: .finished)
|
||||
XCTAssertEqual(publisher.send(144), .max(144))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("HandleEvents"),
|
||||
.value(0),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.completion(.finished),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.finished),
|
||||
.value(144)])
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(14)),
|
||||
.requested(.max(10)),
|
||||
.requested(.none)])
|
||||
XCTAssertEqual(history, [.receiveSubscription("CustomSubscription"),
|
||||
.receiveOutput(0),
|
||||
.receiveOutput(1),
|
||||
.receiveRequest(.max(1)),
|
||||
.receiveOutput(2),
|
||||
.receiveRequest(.max(2)),
|
||||
.receiveOutput(3),
|
||||
.receiveRequest(.max(3)),
|
||||
.receiveRequest(.max(14)),
|
||||
.receiveRequest(.max(10)),
|
||||
.receiveRequest(.none),
|
||||
.receiveCompletion(.finished)])
|
||||
}
|
||||
|
||||
func testAccumulatesDemandUntilSubscriptionArrives() {
|
||||
var history = [Event<TestingError>]()
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let handleEvents = publisher.handleAllEvents { history.append($0) }
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
XCTAssertNil(publisher.subscriber)
|
||||
XCTAssertEqual(history, [])
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
$0.request(.max(45))
|
||||
$0.request(.none)
|
||||
$0.request(.max(13))
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
downstreamSubscription = $0
|
||||
},
|
||||
receiveValue: { .max($0) }
|
||||
)
|
||||
handleEvents.subscribe(tracking)
|
||||
XCTAssertEqual(tracking.history, [.subscription("HandleEvents")])
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(58))])
|
||||
XCTAssertEqual(history, [.receiveRequest(.max(45)),
|
||||
.receiveRequest(.none),
|
||||
.receiveRequest(.max(13)),
|
||||
.receiveSubscription("CustomSubscription")])
|
||||
XCTAssertNotNil(downstreamSubscription)
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
var history = [Event<TestingError>]()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(2),
|
||||
receiveValueDemand: .max(5)) {
|
||||
$0.handleAllEvents { history.append($0) }
|
||||
}
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("HandleEvents")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(2))])
|
||||
XCTAssertEqual(history, [.receiveRequest(.max(2)),
|
||||
.receiveSubscription("CustomSubscription")])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(1))
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .max(5))
|
||||
helper.publisher.send(completion: .finished)
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("HandleEvents"),
|
||||
.value(1),
|
||||
.completion(.finished),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(2)),
|
||||
.cancelled])
|
||||
XCTAssertEqual(history, [.receiveRequest(.max(2)),
|
||||
.receiveSubscription("CustomSubscription"),
|
||||
.receiveCancel])
|
||||
}
|
||||
|
||||
func testHandleEventsReceiveSubscriptionTwice() throws {
|
||||
var history = [Event<TestingError>]()
|
||||
try testReceiveSubscriptionTwice { $0.handleAllEvents { history.append($0) } }
|
||||
XCTAssertEqual(history, [.receiveSubscription("CustomSubscription"),
|
||||
.receiveSubscription("CustomSubscription"),
|
||||
.receiveSubscription("CustomSubscription"),
|
||||
.receiveCancel])
|
||||
}
|
||||
|
||||
func testHandleEventsReceiveValueBeforeSubscription() {
|
||||
var history = [Event<Never>]()
|
||||
testReceiveValueBeforeSubscription(
|
||||
value: 144,
|
||||
expected: .history([.subscription("HandleEvents"),
|
||||
.value(144)],
|
||||
demand: .max(42)),
|
||||
{ $0.handleAllEvents { history.append($0) } }
|
||||
)
|
||||
XCTAssertEqual(history, [.receiveOutput(144),
|
||||
.receiveRequest(.max(42))])
|
||||
}
|
||||
|
||||
func testHandleEventsReceiveCompletionBeforeSubscription() {
|
||||
var history = [Event<Never>]()
|
||||
testReceiveCompletionBeforeSubscription(
|
||||
inputType: Int.self,
|
||||
expected: .history([.subscription("HandleEvents"), .completion(.finished)]),
|
||||
{ $0.handleAllEvents { history.append($0) } }
|
||||
)
|
||||
XCTAssertEqual(history, [.receiveCompletion(.finished)])
|
||||
}
|
||||
|
||||
func testHandleEventsRequestBeforeSubscription() {
|
||||
var history = [Event<Never>]()
|
||||
testRequestBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
{ $0.handleAllEvents { history.append($0) } })
|
||||
XCTAssertEqual(history, [.receiveRequest(.max(1))])
|
||||
}
|
||||
|
||||
func testHandleEventsCancelBeforeSubscription() {
|
||||
var history = [Event<Never>]()
|
||||
testCancelBeforeSubscription(inputType: Int.self,
|
||||
shouldCrash: false,
|
||||
{ $0.handleAllEvents { history.append($0) } })
|
||||
XCTAssertEqual(history, [.receiveCancel])
|
||||
}
|
||||
|
||||
func testHandleEventsReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "HandleEvents",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "HandleEvents",
|
||||
{ $0.handleEvents() })
|
||||
}
|
||||
|
||||
func testHandleEventsLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.handleEvents() })
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private enum Event<Failure: Error & Equatable>: Equatable {
|
||||
case receiveSubscription(StringSubscription)
|
||||
case receiveOutput(Int)
|
||||
case receiveCompletion(Subscribers.Completion<Failure>)
|
||||
case receiveCancel
|
||||
case receiveRequest(Subscribers.Demand)
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension Publisher where Output == Int, Failure: Equatable {
|
||||
fileprivate func handleAllEvents(
|
||||
_ handle: @escaping (Event<Failure>) -> Void
|
||||
) -> Publishers.HandleEvents<Self> {
|
||||
return handleEvents(
|
||||
receiveSubscription: { handle(.receiveSubscription(.subscription($0))) },
|
||||
receiveOutput: { handle(.receiveOutput($0)) },
|
||||
receiveCompletion: { handle(.receiveCompletion($0)) },
|
||||
receiveCancel: { handle(.receiveCancel) },
|
||||
receiveRequest: { handle(.receiveRequest($0)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -339,4 +339,29 @@ final class JustTests: XCTestCase {
|
||||
func testSetFailureTypeOperatorSpecialization() {
|
||||
XCTAssertEqual(try Sut(73).setFailureType(to: TestingError.self).result.get(), 73)
|
||||
}
|
||||
|
||||
func testPrependOperatorSpecialization() {
|
||||
XCTAssertEqual(Sut<Int>(5).prepend(1, 2, 3, 4), .init(sequence: [1, 2, 3, 4, 5]))
|
||||
|
||||
let trackingCollection = TrackingCollection<Int>([1, 2, 3, 4])
|
||||
XCTAssertEqual(Sut<Int>(5).prepend(trackingCollection),
|
||||
.init(sequence: [1, 2, 3, 4, 5]))
|
||||
|
||||
XCTAssertEqual(trackingCollection.history, [.initFromSequence,
|
||||
.underestimatedCount,
|
||||
.underestimatedCount,
|
||||
.makeIterator])
|
||||
}
|
||||
|
||||
func testAppendOperatorSpecialization() {
|
||||
XCTAssertEqual(Sut<Int>(1).append(2, 3, 4, 5), .init(sequence: [1, 2, 3, 4, 5]))
|
||||
|
||||
let trackingCollection = TrackingCollection<Int>([2, 3, 4, 5])
|
||||
XCTAssertEqual(Sut<Int>(1).append(trackingCollection),
|
||||
.init(sequence: [1, 2, 3, 4, 5]))
|
||||
|
||||
XCTAssertEqual(trackingCollection.history, [.initFromSequence,
|
||||
.underestimatedCount,
|
||||
.makeIterator])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
//
|
||||
// MeasureIntervalTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 03.12.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class MeasureIntervalTests: XCTestCase {
|
||||
|
||||
func testBasicBehavior() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
scheduler.rewind(to: .nanoseconds(3))
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(13),
|
||||
receiveValueDemand: .none) {
|
||||
$0.measureInterval(using: scheduler, options: .nontrivialOptions)
|
||||
}
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("MeasureInterval")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(13))])
|
||||
XCTAssertEqual(scheduler.history, [.now])
|
||||
|
||||
scheduler.rewind(to: .nanoseconds(14))
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
scheduler.rewind(to: .nanoseconds(17))
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
scheduler.rewind(to: .nanoseconds(10))
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
scheduler.rewind(to: .nanoseconds(21))
|
||||
XCTAssertEqual(helper.publisher.send(4), .none)
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("MeasureInterval"),
|
||||
.value(.nanoseconds(11)),
|
||||
.value(.nanoseconds(3)),
|
||||
.value(.nanoseconds(-7)),
|
||||
.value(.nanoseconds(11)),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(13))])
|
||||
XCTAssertEqual(scheduler.history, [.now, .now, .now, .now, .now])
|
||||
}
|
||||
|
||||
func testRequest() throws {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(1)) {
|
||||
$0.measureInterval(using: scheduler, options: .nontrivialOptions)
|
||||
}
|
||||
|
||||
var recursionCounter = 5
|
||||
helper.subscription.onRequest = { _ in
|
||||
if recursionCounter == 0 { return }
|
||||
recursionCounter -= 1
|
||||
XCTAssertEqual(helper.publisher.send(0), .none)
|
||||
}
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(0), .none)
|
||||
|
||||
helper.subscription.onRequest = nil
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(2))
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.unlimited)
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.none)
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("MeasureInterval"),
|
||||
.value(.zero),
|
||||
.value(.zero),
|
||||
.value(.zero),
|
||||
.value(.zero),
|
||||
.value(.zero),
|
||||
.value(.zero),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(1)),
|
||||
.requested(.max(1)),
|
||||
.requested(.max(1)),
|
||||
.requested(.max(1)),
|
||||
.requested(.max(1)),
|
||||
.requested(.max(1)),
|
||||
.requested(.max(2)),
|
||||
.requested(.unlimited),
|
||||
.requested(.none)])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(1)) {
|
||||
$0.measureInterval(using: scheduler, options: .nontrivialOptions)
|
||||
}
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("MeasureInterval")])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(scheduler.history, [.now])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(1000))
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
XCTAssertEqual(helper.publisher.send(1000), .none)
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("MeasureInterval")])
|
||||
XCTAssertEqual(helper.subscription.history, [.cancelled])
|
||||
XCTAssertEqual(scheduler.history, [.now])
|
||||
}
|
||||
|
||||
func testMeasureIntervalReceiveSubscriptionTwice() throws {
|
||||
try testReceiveSubscriptionTwice {
|
||||
$0.measureInterval(using: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testMeasureIntervalReceiveValueBeforeSubscription() {
|
||||
testReceiveValueBeforeSubscription(value: 213,
|
||||
expected: .history([], demand: .none)) {
|
||||
$0.measureInterval(using: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testMeasureIntervalReceiveCompletionBeforeSubscription() {
|
||||
testReceiveCompletionBeforeSubscription(inputType: Int.self,
|
||||
expected: .history([])) {
|
||||
$0.measureInterval(using: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testMeasureIntervalRequestBeforeSubscription() {
|
||||
testRequestBeforeSubscription(inputType: Int.self, shouldCrash: false) {
|
||||
$0.measureInterval(using: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testMeasureIntervalCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self, shouldCrash: false) {
|
||||
$0.measureInterval(using: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testMeasureIntervalReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: TestingError.self,
|
||||
description: "MeasureInterval",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "MeasureInterval",
|
||||
{ $0.measureInterval(using: ImmediateScheduler.shared) })
|
||||
}
|
||||
|
||||
func testMeasureIntervalLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: true,
|
||||
{ $0.measureInterval(using: ImmediateScheduler.shared) })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
//
|
||||
// ReceiveOnTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 02.12.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class ReceiveOnTests: XCTestCase {
|
||||
|
||||
func testBasicBehavior() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(100),
|
||||
receiveValueDemand: .max(12)) {
|
||||
$0.receive(on: scheduler, options: .nontrivialOptions)
|
||||
}
|
||||
XCTAssertNotNil(helper.publisher.subscriber,
|
||||
"Subscription must be performed synchronously")
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: .nontrivialOptions)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReceiveOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100))])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: .nontrivialOptions)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReceiveOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100))])
|
||||
XCTAssertEqual(scheduler.scheduledDates, [.nanoseconds(0),
|
||||
.nanoseconds(0),
|
||||
.nanoseconds(0)])
|
||||
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReceiveOn"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100)),
|
||||
.requested(.max(12)),
|
||||
.requested(.max(12)),
|
||||
.requested(.max(12))])
|
||||
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions)])
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
helper.publisher.send(completion: .finished)
|
||||
XCTAssertEqual(helper.publisher.send(4), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReceiveOn"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100)),
|
||||
.requested(.max(12)),
|
||||
.requested(.max(12)),
|
||||
.requested(.max(12))])
|
||||
XCTAssertEqual(scheduler.scheduledDates, [.nanoseconds(0)])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions)])
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReceiveOn"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(100)),
|
||||
.requested(.max(12)),
|
||||
.requested(.max(12)),
|
||||
.requested(.max(12))])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions)])
|
||||
XCTAssertEqual(scheduler.now, .nanoseconds(0))
|
||||
}
|
||||
|
||||
func testRequest() throws {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none) {
|
||||
$0.receive(on: scheduler)
|
||||
}
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReceiveOn")])
|
||||
|
||||
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("ReceiveOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(10)),
|
||||
.requested(.max(4)),
|
||||
.requested(.max(5)),
|
||||
.requested(.none)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReceiveOn"),
|
||||
.value(2000)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(10)),
|
||||
.requested(.max(4)),
|
||||
.requested(.max(5)),
|
||||
.requested(.none)])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none) {
|
||||
$0.receive(on: scheduler)
|
||||
}
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReceiveOn")])
|
||||
|
||||
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("ReceiveOn")])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(0), .none)
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReceiveOn")])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil)])
|
||||
}
|
||||
|
||||
func testReceiveCompletionImmediatelyAfterSubscription() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none) {
|
||||
$0.receive(on: scheduler)
|
||||
}
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil),
|
||||
.schedule(options: nil)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
}
|
||||
|
||||
func testReceiveCompletionImmediatelyAfterValue() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .max(418)) {
|
||||
$0.receive(on: scheduler)
|
||||
}
|
||||
XCTAssertEqual(helper.publisher.send(-1), .none)
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1000), .none)
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReceiveOn"),
|
||||
.value(-1)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.max(418))])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil),
|
||||
.schedule(options: nil),
|
||||
.schedule(options: nil),
|
||||
.schedule(options: nil)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReceiveOn"),
|
||||
.value(-1),
|
||||
.value(1000),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.requested(.max(418))])
|
||||
}
|
||||
|
||||
func testCrashesWhenReceivingInputRecursively() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .max(418)) {
|
||||
$0.receive(on: ImmediateScheduler.shared)
|
||||
}
|
||||
|
||||
helper.tracking.onValue = { _ in
|
||||
_ = helper.publisher.send(-1)
|
||||
}
|
||||
|
||||
assertCrashes {
|
||||
_ = helper.publisher.send(0)
|
||||
}
|
||||
}
|
||||
|
||||
func testReceiveCompletionRecursively() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .max(418)) {
|
||||
$0.receive(on: ImmediateScheduler.shared)
|
||||
}
|
||||
helper.tracking.onFinish = {
|
||||
helper.publisher.send(completion: .finished)
|
||||
}
|
||||
helper.publisher.send(completion: .finished)
|
||||
}
|
||||
|
||||
func testWeakCaptureWhenSchedulingSubscription() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
var subscription: Subscription?
|
||||
var subscriberReleased = false
|
||||
do {
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
let receiveOn = publisher.receive(on: scheduler)
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { subscription = $0 },
|
||||
onDeinit: { subscriberReleased = true })
|
||||
receiveOn.subscribe(tracking)
|
||||
XCTAssertEqual(tracking.history, [])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil)])
|
||||
publisher.cancel()
|
||||
}
|
||||
XCTAssertTrue(subscriberReleased)
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertNil(subscription)
|
||||
}
|
||||
|
||||
func testWeakCaptureWhenSchedulingValue() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
var value: Int?
|
||||
var subscriberReleased = false
|
||||
do {
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
let receiveOn = publisher.receive(on: scheduler)
|
||||
let tracking = TrackingSubscriber(receiveValue: { value = $0; return .none },
|
||||
onDeinit: { subscriberReleased = true })
|
||||
receiveOn.subscribe(tracking)
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(tracking.history, [.subscription("ReceiveOn")])
|
||||
XCTAssertEqual(publisher.send(42), .none)
|
||||
XCTAssertEqual(tracking.history, [.subscription("ReceiveOn")])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil),
|
||||
.schedule(options: nil)])
|
||||
tracking.cancel()
|
||||
publisher.cancel()
|
||||
}
|
||||
XCTAssertFalse(subscriberReleased)
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertNil(value)
|
||||
XCTAssertTrue(subscriberReleased)
|
||||
}
|
||||
|
||||
func testWeakCaptureWhenSchedulingCompletion() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
var completion: Subscribers.Completion<TestingError>?
|
||||
var subscriberReleased = false
|
||||
do {
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
let receiveOn = publisher.receive(on: scheduler)
|
||||
let tracking = TrackingSubscriber(receiveCompletion: { completion = $0 },
|
||||
onDeinit: { subscriberReleased = true })
|
||||
receiveOn.subscribe(tracking)
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(tracking.history, [.subscription("ReceiveOn")])
|
||||
publisher.send(completion: .finished)
|
||||
XCTAssertEqual(tracking.history, [.subscription("ReceiveOn")])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil),
|
||||
.schedule(options: nil)])
|
||||
tracking.cancel()
|
||||
publisher.cancel()
|
||||
}
|
||||
XCTAssertFalse(subscriberReleased)
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertNil(completion)
|
||||
XCTAssertTrue(subscriberReleased)
|
||||
}
|
||||
|
||||
func testReceiveOnReceiveSubscriptionTwice() throws {
|
||||
try testReceiveSubscriptionTwice {
|
||||
$0.receive(on: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testReceiveOnReceiveValueBeforeSubscription() {
|
||||
testReceiveValueBeforeSubscription(value: 213,
|
||||
expected: .history([], demand: .none)) {
|
||||
$0.receive(on: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testReceiveOnReceiveCompletionBeforeSubscription() {
|
||||
testReceiveCompletionBeforeSubscription(inputType: Int.self,
|
||||
expected: .history([])) {
|
||||
$0.receive(on: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testReceiveOnRequestBeforeSubscription() {
|
||||
testRequestBeforeSubscription(inputType: Int.self, shouldCrash: false) {
|
||||
$0.receive(on: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testReceiveOnCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self, shouldCrash: false) {
|
||||
$0.receive(on: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testReceiveOnReflection() throws {
|
||||
try testReflection(parentInput: String.self,
|
||||
parentFailure: Error.self,
|
||||
description: "ReceiveOn",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "ReceiveOn",
|
||||
{ $0.receive(on: ImmediateScheduler.shared) })
|
||||
}
|
||||
|
||||
func testReceiveOnLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31, cancellingSubscriptionReleasesSubscriber: true) {
|
||||
$0.receive(on: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
//
|
||||
// SubscribeOnTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 03.12.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class SubscribeOnTests: XCTestCase {
|
||||
|
||||
func testSynchronouslySendsEventsDownstream() throws {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let subscribeOn = publisher.subscribe(on: scheduler, options: .nontrivialOptions)
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.max(100)) },
|
||||
receiveValue: { _ in .max(12) }
|
||||
)
|
||||
|
||||
publisher.didSubscribe = { _ in
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("SubscribeOn")],
|
||||
"Subscription object should be sent synchronously")
|
||||
XCTAssertEqual(subscription.history,
|
||||
[],
|
||||
"Demand should be requested asynchronously")
|
||||
}
|
||||
|
||||
subscribeOn.subscribe(tracking)
|
||||
|
||||
XCTAssertNil(publisher.subscriber,
|
||||
"Subscription must be performed asynchronously")
|
||||
|
||||
XCTAssertEqual(tracking.history, [])
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: .nontrivialOptions)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("SubscribeOn")])
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(100))])
|
||||
|
||||
XCTAssertEqual(publisher.send(1), .max(12))
|
||||
XCTAssertEqual(publisher.send(2), .max(12))
|
||||
XCTAssertEqual(publisher.send(3), .max(12))
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("SubscribeOn"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(100))])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions)])
|
||||
|
||||
publisher.send(completion: .finished)
|
||||
publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(publisher.send(-1), .none)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("SubscribeOn"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(100))])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions)])
|
||||
}
|
||||
|
||||
func testAsynchronouslySendsEventsUpstream() throws {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .unlimited) {
|
||||
$0.subscribe(on: scheduler, options: .nontrivialOptions)
|
||||
}
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("SubscribeOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: .nontrivialOptions)])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(17))
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.unlimited)
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("SubscribeOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("SubscribeOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(17)),
|
||||
.requested(.unlimited),
|
||||
.requested(.none)])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("SubscribeOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(17)),
|
||||
.requested(.unlimited),
|
||||
.requested(.none)])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions),
|
||||
.schedule(options: .nontrivialOptions)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("SubscribeOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(17)),
|
||||
.requested(.unlimited),
|
||||
.requested(.none),
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(42)) {
|
||||
$0.subscribe(on: scheduler)
|
||||
}
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("SubscribeOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil)])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
XCTAssertEqual(helper.publisher.send(1000), .none)
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("SubscribeOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil),
|
||||
.schedule(options: nil)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("SubscribeOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [.cancelled])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil),
|
||||
.schedule(options: nil)])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(20000))
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil),
|
||||
.schedule(options: nil)])
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("SubscribeOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [.cancelled])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil),
|
||||
.schedule(options: nil)])
|
||||
}
|
||||
|
||||
func testCancelImmediatelyAfterRequest() throws {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(42)) {
|
||||
$0.subscribe(on: scheduler)
|
||||
}
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("SubscribeOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil)])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.unlimited)
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("SubscribeOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil),
|
||||
.schedule(options: nil),
|
||||
.schedule(options: nil)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("SubscribeOn")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil),
|
||||
.schedule(options: nil),
|
||||
.schedule(options: nil)])
|
||||
}
|
||||
|
||||
func testCrashWhenRequestingRecursively() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none) {
|
||||
$0.subscribe(on: ImmediateScheduler.shared)
|
||||
}
|
||||
|
||||
helper.subscription.onRequest = { _ in
|
||||
helper.downstreamSubscription?.request(.unlimited)
|
||||
}
|
||||
|
||||
try assertCrashes {
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(1))
|
||||
}
|
||||
}
|
||||
|
||||
func testCancelRecursively() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none) {
|
||||
$0.subscribe(on: ImmediateScheduler.shared)
|
||||
}
|
||||
|
||||
helper.subscription.onCancel = {
|
||||
helper.downstreamSubscription?.cancel()
|
||||
}
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
}
|
||||
|
||||
func testWeakCaptureWhenSchedulingRequest() throws {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
var subscriberReleased = false
|
||||
let subscription = CustomSubscription()
|
||||
do {
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let subscribeOn = publisher.subscribe(on: scheduler)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { downstreamSubscription = $0 },
|
||||
onDeinit: { subscriberReleased = true }
|
||||
)
|
||||
subscribeOn.subscribe(tracking)
|
||||
scheduler.executeScheduledActions()
|
||||
try XCTUnwrap(downstreamSubscription).request(.max(1))
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil),
|
||||
.schedule(options: nil)])
|
||||
publisher.cancel()
|
||||
tracking.cancel()
|
||||
}
|
||||
XCTAssertTrue(subscriberReleased)
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
}
|
||||
|
||||
func testWeakCaptureWhenSchedulingCancel() throws {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
var subscriberReleased = false
|
||||
let subscription = CustomSubscription()
|
||||
do {
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let subscribeOn = publisher.subscribe(on: scheduler)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { downstreamSubscription = $0 },
|
||||
onDeinit: { subscriberReleased = true }
|
||||
)
|
||||
subscribeOn.subscribe(tracking)
|
||||
scheduler.executeScheduledActions()
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
XCTAssertEqual(scheduler.history, [.schedule(options: nil),
|
||||
.schedule(options: nil)])
|
||||
publisher.cancel()
|
||||
tracking.cancel()
|
||||
}
|
||||
XCTAssertTrue(subscriberReleased)
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
}
|
||||
|
||||
func testSubscribeOnReceiveSubscriptionTwice() throws {
|
||||
try testReceiveSubscriptionTwice {
|
||||
$0.subscribe(on: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testSubscribeOnReceiveValueBeforeSubscription() {
|
||||
testReceiveValueBeforeSubscription(value: 213,
|
||||
expected: .history([], demand: .none)) {
|
||||
$0.subscribe(on: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testSubscribeOnReceiveCompletionBeforeSubscription() {
|
||||
testReceiveCompletionBeforeSubscription(inputType: Int.self,
|
||||
expected: .history([])) {
|
||||
$0.subscribe(on: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testSubscribeOnRequestBeforeSubscription() {
|
||||
testRequestBeforeSubscription(inputType: Int.self, shouldCrash: false) {
|
||||
$0.subscribe(on: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testSubscribeOnCancelBeforeSubscription() {
|
||||
testCancelBeforeSubscription(inputType: Int.self, shouldCrash: false) {
|
||||
$0.subscribe(on: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
|
||||
func testSubscribeOnReflection() throws {
|
||||
try testReflection(parentInput: Double.self,
|
||||
parentFailure: Error.self,
|
||||
description: "SubscribeOn",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "SubscribeOn",
|
||||
{ $0.subscribe(on: ImmediateScheduler.shared) })
|
||||
}
|
||||
|
||||
func testSubscribeOnLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31, cancellingSubscriptionReleasesSubscriber: true) {
|
||||
$0.subscribe(on: ImmediateScheduler.shared)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
//
|
||||
// VirtualTimeSchedulerTests.swift
|
||||
// OpenCombineTests
|
||||
//
|
||||
// Created by Евгений Богомолов on 14/09/2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class VirtualTimeSchedulerTests: XCTestCase {
|
||||
|
||||
func testOrder() {
|
||||
var history = [Int]()
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
scheduler.schedule(after: scheduler.now + .nanoseconds(10)) {
|
||||
history.append(5)
|
||||
}
|
||||
scheduler.schedule {
|
||||
history.append(1)
|
||||
}
|
||||
scheduler.schedule(after: scheduler.now + .nanoseconds(5)) {
|
||||
history.append(3)
|
||||
scheduler.schedule(after: scheduler.now + .nanoseconds(2)) {
|
||||
history.append(4)
|
||||
}
|
||||
}
|
||||
scheduler.schedule {
|
||||
history.append(2)
|
||||
}
|
||||
|
||||
XCTAssertEqual(scheduler.now, .nanoseconds(0))
|
||||
XCTAssertEqual(scheduler.scheduledDates, [.nanoseconds(0),
|
||||
.nanoseconds(0),
|
||||
.nanoseconds(5),
|
||||
.nanoseconds(10)])
|
||||
|
||||
scheduler.executeScheduledActions()
|
||||
XCTAssertEqual(history, [1, 2, 3, 4, 5])
|
||||
XCTAssertEqual(scheduler.now, .nanoseconds(10))
|
||||
XCTAssertEqual(scheduler.scheduledDates, [])
|
||||
|
||||
XCTAssertEqual(scheduler.history, [.now,
|
||||
.minimumTolerance,
|
||||
.scheduleAfterDate(.nanoseconds(10),
|
||||
tolerance: 0,
|
||||
options: nil),
|
||||
.schedule(options: nil),
|
||||
.now,
|
||||
.minimumTolerance,
|
||||
.scheduleAfterDate(.nanoseconds(5),
|
||||
tolerance: 0,
|
||||
options: nil),
|
||||
.schedule(options: nil),
|
||||
.now,
|
||||
.now,
|
||||
.minimumTolerance,
|
||||
.scheduleAfterDate(.nanoseconds(7),
|
||||
tolerance: 0,
|
||||
options: nil),
|
||||
.now])
|
||||
}
|
||||
|
||||
func testRepeadedAction() {
|
||||
let scheduler = VirtualTimeScheduler()
|
||||
var history = [Int]()
|
||||
let cancellable = scheduler.schedule(after: scheduler.now + .microseconds(2),
|
||||
interval: .milliseconds(40)) {
|
||||
history.append(Int(scheduler.now.time))
|
||||
}
|
||||
scheduler.schedule(after: scheduler.now + .milliseconds(300)) {
|
||||
cancellable.cancel()
|
||||
}
|
||||
XCTAssertEqual(scheduler.scheduledDates, [.microseconds(2), .milliseconds(300)])
|
||||
scheduler.executeScheduledActions()
|
||||
|
||||
XCTAssertEqual(history, [2000,
|
||||
40002000,
|
||||
80002000,
|
||||
120002000,
|
||||
160002000,
|
||||
200002000,
|
||||
240002000,
|
||||
280002000])
|
||||
XCTAssertEqual(scheduler.now, .microseconds(320002))
|
||||
XCTAssertEqual(scheduler.history,
|
||||
[.now,
|
||||
.minimumTolerance,
|
||||
.scheduleAfterDateWithInterval(.microseconds(2),
|
||||
interval: .milliseconds(40),
|
||||
tolerance: 0,
|
||||
options: nil),
|
||||
.now,
|
||||
.minimumTolerance,
|
||||
.scheduleAfterDate(.milliseconds(300),
|
||||
tolerance: .nanoseconds(0),
|
||||
options: nil),
|
||||
.now,
|
||||
.now,
|
||||
.now,
|
||||
.now,
|
||||
.now,
|
||||
.now,
|
||||
.now,
|
||||
.now,
|
||||
.now])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user