Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f92ee05d2 | |||
| bdd703abb3 | |||
| e41c48a5cd | |||
| df0b8b08db | |||
| 7056143b99 | |||
| 0a965ba60a | |||
| 7dfaa4edea | |||
| 3e8f2774a4 | |||
| 68e9bbe164 | |||
| 0f71c33d72 | |||
| 3f61648f82 | |||
| c621ceb267 | |||
| 2aa297ec39 | |||
| 9cb27bb91b | |||
| d74f68da86 | |||
| f68dcd520f | |||
| 432fd4f48f | |||
| 9c6bbda0c4 | |||
| 3990ec2afb | |||
| 39dd9e40bf | |||
| fd7c0459b9 | |||
| f7145e7fa5 | |||
| ecd4766129 | |||
| e00a6f06fc | |||
| 23ee3a4b7b | |||
| 9c913124eb | |||
| 7ddd15b334 | |||
| 72753ef93c | |||
| 816426b48c | |||
| 47fb390081 | |||
| 1d3327f6bf | |||
| eb7478d430 | |||
| f69621f0e2 | |||
| 7f3cccf1ae | |||
| ec037dbb3d | |||
| 8a39f35d3f | |||
| 7fb92bffc6 | |||
| e441ea3048 | |||
| 22f7b6d10d | |||
| 7431d21c9c | |||
| 1d901fca7f | |||
| 9834eab0ea | |||
| 1ce9660ce9 | |||
| 313d6befa6 | |||
| 8c7f061892 | |||
| 2ac2470579 | |||
| 57c9ae8590 |
@@ -2,6 +2,7 @@
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
/.swiftpm
|
||||
|
||||
# Created by https://www.gitignore.io/api/Xcode
|
||||
# Edit at https://www.gitignore.io/?templates=Xcode
|
||||
|
||||
+2
-2
@@ -78,14 +78,14 @@ attributes:
|
||||
|
||||
custom_rules:
|
||||
no_foundation_dependency:
|
||||
included: Sources/OpenCombine
|
||||
included: Sources/OpenCombine/
|
||||
name: "No Foundation Dependency"
|
||||
regex: "^import.*Foundation.*$"
|
||||
message: "We don't want to depend on Foundation"
|
||||
severity: error
|
||||
|
||||
no_dispatch_dependency:
|
||||
included: Sources/OpenCombine
|
||||
included: Sources/OpenCombine/
|
||||
name: "No Dispatch Dependency"
|
||||
regex: "^import.*Dispatch.*$"
|
||||
message: "We don't want to depend on Dispatch"
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,102 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1100"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "OpenCombine"
|
||||
BuildableName = "OpenCombine"
|
||||
BlueprintName = "OpenCombine"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "OpenCombineTests"
|
||||
BuildableName = "OpenCombineTests"
|
||||
BlueprintName = "OpenCombineTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
enableThreadSanitizer = "YES"
|
||||
codeCoverageEnabled = "YES">
|
||||
<CodeCoverageTargets>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "OpenCombine"
|
||||
BuildableName = "OpenCombine"
|
||||
BlueprintName = "OpenCombine"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</CodeCoverageTargets>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "OpenCombineTests"
|
||||
BuildableName = "OpenCombineTests"
|
||||
BlueprintName = "OpenCombineTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "OpenCombine"
|
||||
BuildableName = "OpenCombine"
|
||||
BlueprintName = "OpenCombine"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -58,6 +58,13 @@ script:
|
||||
make test-debug;
|
||||
fi
|
||||
fi
|
||||
- if [[ $OPENCOMBINE_TEST == "YES" ]]; then
|
||||
if [[ $TRAVIS_OS_NAME == "linux" ]]; then
|
||||
make SWIFT_TEST_FLAGS="--enable-test-discovery --enable-index-store" test-debug-sanitize-thread;
|
||||
else
|
||||
make test-debug-sanitize-thread;
|
||||
fi
|
||||
fi
|
||||
- if [[ $OPENCOMBINE_TEST == "YES" ]]; then
|
||||
if [[ $TRAVIS_OS_NAME == "linux" ]]; then
|
||||
make SWIFT_TEST_FLAGS="--enable-test-discovery --enable-index-store" test-release;
|
||||
|
||||
@@ -7,7 +7,10 @@ release:
|
||||
swift build -c release
|
||||
|
||||
test-debug:
|
||||
swift test -c debug --sanitize thread $(SWIFT_TEST_FLAGS)
|
||||
swift test -c debug $(SWIFT_TEST_FLAGS)
|
||||
|
||||
test-debug-sanitize-thread:
|
||||
swift test -c debug --sanitize thread $(SWIFT_TEST_FLAGS)
|
||||
|
||||
test-release:
|
||||
swift test -c release $(SWIFT_TEST_FLAGS)
|
||||
|
||||
+9
-3
@@ -6,14 +6,20 @@ let package = Package(
|
||||
name: "OpenCombine",
|
||||
products: [
|
||||
.library(name: "OpenCombine", targets: ["OpenCombine"]),
|
||||
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/broadwaylamb/GottaGoFast.git", from: "0.1.0")
|
||||
],
|
||||
targets: [
|
||||
.target(name: "OpenCombine"),
|
||||
.target(name: "COpenCombineHelpers"),
|
||||
.target(name: "OpenCombine", dependencies: ["COpenCombineHelpers"]),
|
||||
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
|
||||
.testTarget(name: "OpenCombineTests",
|
||||
dependencies: ["OpenCombine", "GottaGoFast"],
|
||||
dependencies: ["OpenCombine",
|
||||
"OpenCombineDispatch",
|
||||
"GottaGoFast"],
|
||||
swiftSettings: [.unsafeFlags(["-enable-testing"])])
|
||||
]
|
||||
],
|
||||
cxxLanguageStandard: .cxx1z
|
||||
)
|
||||
|
||||
@@ -1,22 +1,7 @@
|
||||
// This file contains parts of Apple's Combine that remain unimplemented in OpenCombine
|
||||
// Please remove the corresponding piece from this file if you implment something,
|
||||
// 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 ConnectablePublisher {
|
||||
|
||||
/// Automates the process of connecting or disconnecting from this connectable publisher.
|
||||
///
|
||||
/// Use `autoconnect()` to simplify working with `ConnectablePublisher` instances, such as those created with `makeConnectable()`.
|
||||
///
|
||||
/// let autoconnectedPublisher = somePublisher
|
||||
/// .makeConnectable()
|
||||
/// .autoconnect()
|
||||
/// .subscribe(someSubscriber)
|
||||
///
|
||||
/// - Returns: A publisher which automatically connects to its upstream connectable publisher.
|
||||
public func autoconnect() -> Publishers.Autoconnect<Self>
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that receives elements from an upstream publisher on a specific scheduler.
|
||||
@@ -536,34 +521,6 @@ extension Publisher {
|
||||
public func combineLatest<P, Q, R, T>(_ publisher1: P, _ publisher2: Q, _ publisher3: R, _ transform: @escaping (Self.Output, P.Output, Q.Output, R.Output) -> T) -> Publishers.Map<Publishers.CombineLatest4<Self, P, Q, R>, T> where P : Publisher, Q : Publisher, R : Publisher, Self.Failure == P.Failure, P.Failure == Q.Failure, Q.Failure == R.Failure
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that automatically connects and disconnects from this connectable publisher.
|
||||
public class Autoconnect<Upstream> : Publisher where Upstream : ConnectablePublisher {
|
||||
|
||||
/// 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.
|
||||
final public let upstream: Upstream
|
||||
|
||||
public init(upstream: Upstream)
|
||||
|
||||
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
|
||||
///
|
||||
/// - SeeAlso: `subscribe(_:)`
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<S>(subscriber: S) where S : Subscriber, Upstream.Failure == S.Failure, Upstream.Output == S.Input
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that republishes elements while a predicate closure indicates publishing should continue.
|
||||
@@ -743,43 +700,6 @@ extension Publisher {
|
||||
public func tryContains(where predicate: @escaping (Self.Output) throws -> Bool) -> Publishers.TryContainsWhere<Self>
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
public struct MakeConnectable<Upstream> : ConnectablePublisher 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
|
||||
|
||||
public init(upstream: Upstream)
|
||||
|
||||
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
|
||||
///
|
||||
/// - SeeAlso: `subscribe(_:)`
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<S>(subscriber: S) where S : Subscriber, Upstream.Failure == S.Failure, Upstream.Output == S.Input
|
||||
|
||||
/// Connects to the publisher and returns a `Cancellable` instance with which to cancel publishing.
|
||||
///
|
||||
/// - Returns: A `Cancellable` instance that can be used to cancel publishing.
|
||||
public func connect() -> Cancellable
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher where Self.Failure == Never {
|
||||
|
||||
/// Creates a connectable wrapper around the publisher.
|
||||
///
|
||||
/// - Returns: A `ConnectablePublisher` wrapping this publisher.
|
||||
public func makeConnectable() -> Publishers.MakeConnectable<Self>
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A strategy for collecting received elements.
|
||||
@@ -1782,53 +1702,6 @@ extension Publisher {
|
||||
public func throttle<S>(for interval: S.SchedulerTimeType.Stride, scheduler: S, latest: Bool) -> Publishers.Throttle<Self, S> where S : Scheduler
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher implemented as a class, which otherwise behaves like its upstream publisher.
|
||||
final public class Share<Upstream> : Publisher, Equatable 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
|
||||
|
||||
final public let upstream: Upstream
|
||||
|
||||
public init(upstream: Upstream)
|
||||
|
||||
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
|
||||
///
|
||||
/// - SeeAlso: `subscribe(_:)`
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
final public func receive<S>(subscriber: S) where S : Subscriber, Upstream.Failure == S.Failure, Upstream.Output == S.Input
|
||||
|
||||
/// Returns a Boolean value indicating whether two values are equal.
|
||||
///
|
||||
/// Equality is the inverse of inequality. For any values `a` and `b`,
|
||||
/// `a == b` implies that `a != b` is `false`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - lhs: A value to compare.
|
||||
/// - rhs: Another value to compare.
|
||||
public static func == (lhs: Publishers.Share<Upstream>, rhs: Publishers.Share<Upstream>) -> Bool
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Returns a publisher as a class instance.
|
||||
///
|
||||
/// The downstream subscriber receieves elements and completion states unchanged from the upstream publisher. Use this operator when you want to use reference semantics, such as storing a publisher instance in a property.
|
||||
///
|
||||
/// - Returns: A class instance that republishes its upstream publisher.
|
||||
public func share() -> Publishers.Share<Self>
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that republishes items from another publisher only if each new item is in increasing order from the previously-published item.
|
||||
@@ -3037,17 +2910,6 @@ extension Publishers.ReplaceEmpty : Equatable where Upstream : Equatable, Upstre
|
||||
public static func == (lhs: Publishers.ReplaceEmpty<Upstream>, rhs: Publishers.ReplaceEmpty<Upstream>) -> Bool
|
||||
}
|
||||
|
||||
extension Publishers.ReplaceError : Equatable where Upstream : Equatable, Upstream.Output : Equatable {
|
||||
|
||||
/// Returns a Boolean value that indicates whether two publishers are equivalent.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - lhs: A replace error publisher to compare for equality.
|
||||
/// - rhs: Another replace error publisher to compare for equality.
|
||||
/// - Returns: `true` if the two publishers have equal upstream publishers and output elements, `false` otherwise.
|
||||
public static func == (lhs: Publishers.ReplaceError<Upstream>, rhs: Publishers.ReplaceError<Upstream>) -> Bool
|
||||
}
|
||||
|
||||
extension Publishers.DropUntilOutput : Equatable where Upstream : Equatable, Other : Equatable {
|
||||
|
||||
/// Returns a Boolean value indicating whether two values are equal.
|
||||
@@ -3156,17 +3018,6 @@ extension Publishers.Drop : Equatable where Upstream : Equatable {
|
||||
public static func == (lhs: Publishers.Drop<Upstream>, rhs: Publishers.Drop<Upstream>) -> Bool
|
||||
}
|
||||
|
||||
extension Publishers.First : Equatable where Upstream : Equatable {
|
||||
|
||||
/// Returns a Boolean value that indicates whether two first publishers have equal upstream publishers.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - lhs: A drop publisher to compare for equality.
|
||||
/// - rhs: Another drop publisher to compare for equality.
|
||||
/// - Returns: `true` if the two publishers have equal upstream publishers, `false` otherwise.
|
||||
public static func == (lhs: Publishers.First<Upstream>, rhs: Publishers.First<Upstream>) -> Bool
|
||||
}
|
||||
|
||||
/// A type of object with a publisher that emits before the object has changed.
|
||||
///
|
||||
/// By default an `ObservableObject` will synthesize an `objectWillChange`
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// COpenCombineHelpers.cpp
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 23/09/2019.
|
||||
//
|
||||
|
||||
#include "COpenCombineHelpers.h"
|
||||
|
||||
#include <atomic>
|
||||
|
||||
extern "C" uint64_t opencombine_next_combine_identifier() {
|
||||
static std::atomic<uint64_t> next_combine_identifier;
|
||||
return next_combine_identifier.fetch_add(1);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// COpenCombineHelpers.h
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 23/09/2019.
|
||||
//
|
||||
|
||||
#ifndef COPENCOMBINEHELPERS_H
|
||||
#define COPENCOMBINEHELPERS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
uint64_t opencombine_next_combine_identifier();
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
||||
|
||||
#endif /* COPENCOMBINEHELPERS_H */
|
||||
@@ -5,6 +5,12 @@
|
||||
// Created by Sergej Jaskiewicz on 10.06.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
public func eraseToAnyPublisher() -> AnyPublisher<Output, Failure> {
|
||||
return .init(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// A type-erasing publisher.
|
||||
///
|
||||
/// Use `AnyPublisher` to wrap a publisher whose type has details you don’t want to expose
|
||||
@@ -13,7 +19,6 @@ public struct AnyPublisher<Output, Failure: Error>
|
||||
: CustomStringConvertible,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
|
||||
@usableFromInline
|
||||
internal let box: PublisherBoxBase<Output, Failure>
|
||||
|
||||
@@ -47,8 +52,8 @@ extension AnyPublisher: Publisher {
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
@inlinable
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Output == SubscriberType.Input, Failure == SubscriberType.Failure
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Failure == Downstream.Failure
|
||||
{
|
||||
box.subscribe(subscriber)
|
||||
}
|
||||
@@ -62,11 +67,11 @@ internal class PublisherBoxBase<Output, Failure: Error>: Publisher {
|
||||
@inlinable
|
||||
internal init() {}
|
||||
|
||||
@inlinable
|
||||
internal func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Failure == SubscriberType.Failure, Output == SubscriberType.Input
|
||||
@usableFromInline
|
||||
internal func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
fatalError()
|
||||
abstractMethod()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,8 +89,8 @@ internal final class PublisherBox<PublisherType: Publisher>
|
||||
}
|
||||
|
||||
@inlinable
|
||||
override internal func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Failure == SubscriberType.Failure, Output == SubscriberType.Input
|
||||
override internal func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
base.subscribe(subscriber)
|
||||
}
|
||||
|
||||
@@ -137,17 +137,17 @@ internal class AnySubscriberBase<Input, Failure: Error>: Subscriber {
|
||||
|
||||
@usableFromInline
|
||||
internal func receive(subscription: Subscription) {
|
||||
fatalError()
|
||||
abstractMethod()
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
internal func receive(_ input: Input) -> Subscribers.Demand {
|
||||
fatalError()
|
||||
abstractMethod()
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
internal func receive(completion: Subscribers.Completion<Failure>) {
|
||||
fatalError()
|
||||
abstractMethod()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,53 +222,3 @@ internal final class ClosureBasedAnySubscriber<Input, Failure: Error>
|
||||
receiveCompletionThunk(completion)
|
||||
}
|
||||
}
|
||||
|
||||
internal final class SubjectSubscriber<Downstream: Subject>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
Subscription
|
||||
{
|
||||
internal var downstreamSubject: Downstream?
|
||||
internal var upstreamSubscription: Subscription?
|
||||
|
||||
internal init(_ parent: Downstream) {
|
||||
self.downstreamSubject = parent
|
||||
}
|
||||
|
||||
internal func receive(subscription: Subscription) {
|
||||
guard upstreamSubscription == nil else { return }
|
||||
upstreamSubscription = subscription
|
||||
downstreamSubject?.send(subscription: self)
|
||||
}
|
||||
|
||||
internal func receive(_ input: Downstream.Output) -> Subscribers.Demand {
|
||||
downstreamSubject?.send(input)
|
||||
return .none
|
||||
}
|
||||
|
||||
internal func receive(completion: Subscribers.Completion<Downstream.Failure>) {
|
||||
downstreamSubject?.send(completion: completion)
|
||||
downstreamSubject = nil
|
||||
}
|
||||
|
||||
internal var description: String { return "Subject" }
|
||||
|
||||
internal var customMirror: Mirror {
|
||||
let children: [(label: String?, value: Any)] = [
|
||||
(label: "downstreamSubject", value: downstreamSubject as Any),
|
||||
(label: "upstreamSubscription", value: upstreamSubscription as Any)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
internal func request(_ demand: Subscribers.Demand) {
|
||||
upstreamSubscription?.request(demand)
|
||||
}
|
||||
|
||||
internal func cancel() {
|
||||
upstreamSubscription?.cancel()
|
||||
upstreamSubscription = nil
|
||||
downstreamSubject = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,35 +5,21 @@
|
||||
// Created by Sergej Jaskiewicz on 10.06.2019.
|
||||
//
|
||||
|
||||
import func COpenCombineHelpers.opencombine_next_combine_identifier
|
||||
|
||||
public struct CombineIdentifier: Hashable, CustomStringConvertible {
|
||||
|
||||
@usableFromInline
|
||||
internal static var _counter: UInt = 0
|
||||
private let id: UInt64
|
||||
|
||||
@usableFromInline
|
||||
internal static var _counterLock = Lock(recursive: false)
|
||||
|
||||
@usableFromInline
|
||||
internal let _id: UInt
|
||||
|
||||
@inlinable
|
||||
public init() {
|
||||
|
||||
var id: UInt = 0
|
||||
|
||||
CombineIdentifier._counterLock.do {
|
||||
id = CombineIdentifier._counter
|
||||
CombineIdentifier._counter += 1
|
||||
}
|
||||
|
||||
_id = id
|
||||
self.id = opencombine_next_combine_identifier()
|
||||
}
|
||||
|
||||
public init(_ obj: AnyObject) {
|
||||
_id = UInt(bitPattern: ObjectIdentifier(obj))
|
||||
id = UInt64(UInt(bitPattern: ObjectIdentifier(obj)))
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
return "0x\(String(_id, radix: 16))"
|
||||
return "0x\(String(id, radix: 16))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
/// changes.
|
||||
public final class CurrentValueSubject<Output, Failure: Error>: Subject {
|
||||
|
||||
private let _lock = Lock(recursive: true)
|
||||
private let _lock = unfairRecursiveLock()
|
||||
|
||||
// TODO: Combine uses bag data structure
|
||||
private var _subscriptions: [Conduit] = []
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
//
|
||||
// Locking.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
#if canImport(Darwin)
|
||||
import Darwin
|
||||
#elseif canImport(Glibc)
|
||||
import Glibc
|
||||
#else
|
||||
#error("How to do locking on this platform?")
|
||||
#endif
|
||||
|
||||
@usableFromInline
|
||||
internal protocol UnfairLock: AnyObject {
|
||||
func lock()
|
||||
func unlock()
|
||||
}
|
||||
|
||||
extension UnfairLock {
|
||||
|
||||
@inlinable
|
||||
internal func `do`<Result>(_ body: () throws -> Result) rethrows -> Result {
|
||||
lock()
|
||||
defer { unlock() }
|
||||
return try body()
|
||||
}
|
||||
}
|
||||
|
||||
internal protocol UnfairRecursiveLock: UnfairLock {}
|
||||
|
||||
internal func unfairLock() -> UnfairLock {
|
||||
#if canImport(Darwin)
|
||||
if #available(macOS 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *) {
|
||||
return OSUnfairLock()
|
||||
} else {
|
||||
return PThreadMutexLock()
|
||||
}
|
||||
#else
|
||||
return PThreadMutexLock()
|
||||
#endif
|
||||
}
|
||||
|
||||
internal func unfairRecursiveLock() -> UnfairRecursiveLock {
|
||||
// TODO: Use os_unfair_recursive_lock on Darwin as soon as it becomes public API.
|
||||
return PThreadMutexRecursiveLock()
|
||||
}
|
||||
|
||||
private class PThreadMutexLock
|
||||
: UnfairLock,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
private let mutex = UnsafeMutablePointer<pthread_mutex_t>.allocate(capacity: 1)
|
||||
|
||||
init() {
|
||||
var status: CInt
|
||||
var attributes = pthread_mutexattr_t()
|
||||
status = pthread_mutexattr_init(&attributes)
|
||||
precondition(status == 0,
|
||||
"pthread_mutexattr_init returned non-zero status: \(status)")
|
||||
|
||||
// Enable error detection
|
||||
status = pthread_mutexattr_settype(&attributes, CInt(PTHREAD_MUTEX_ERRORCHECK))
|
||||
precondition(status == 0,
|
||||
"pthread_mutexattr_settype returned non-zero status: \(status)")
|
||||
|
||||
setAdditionalAttributes(&attributes)
|
||||
|
||||
status = pthread_mutex_init(mutex, &attributes)
|
||||
precondition(status == 0,
|
||||
"pthread_mutex_init returned non-zero status: \(status)")
|
||||
}
|
||||
|
||||
fileprivate func setAdditionalAttributes(
|
||||
_ attributes: UnsafeMutablePointer<pthread_mutexattr_t>
|
||||
) {
|
||||
// Do nothing for non-recursive locks
|
||||
}
|
||||
|
||||
final func lock() {
|
||||
let status = pthread_mutex_lock(mutex)
|
||||
precondition(status == 0,
|
||||
"pthread_mutex_lock returned non-zero status: \(status)")
|
||||
}
|
||||
|
||||
final func unlock() {
|
||||
let status = pthread_mutex_unlock(mutex)
|
||||
precondition(status == 0,
|
||||
"pthread_mutex_lock returned non-zero status: \(status)")
|
||||
}
|
||||
|
||||
final var description: String { return String(describing: mutex.pointee) }
|
||||
|
||||
final var customMirror: Mirror { return Mirror(reflecting: mutex.pointee) }
|
||||
|
||||
final var playgroundDescription: Any { return description }
|
||||
|
||||
deinit {
|
||||
let status = pthread_mutex_destroy(mutex)
|
||||
precondition(status == 0,
|
||||
"pthread_mutex_destroy returned non-zero status: \(status)")
|
||||
mutex.deallocate()
|
||||
}
|
||||
}
|
||||
|
||||
private final class PThreadMutexRecursiveLock: PThreadMutexLock, UnfairRecursiveLock {
|
||||
override func setAdditionalAttributes(
|
||||
_ attributes: UnsafeMutablePointer<pthread_mutexattr_t>
|
||||
) {
|
||||
let status = pthread_mutexattr_settype(attributes, CInt(PTHREAD_MUTEX_RECURSIVE))
|
||||
precondition(status == 0,
|
||||
"pthread_mutexattr_settype returned non-zero status: \(status)")
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(Darwin)
|
||||
|
||||
@available(macOS 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *)
|
||||
private final class OSUnfairLock
|
||||
: UnfairLock,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
private var mutex = os_unfair_lock()
|
||||
|
||||
func lock() {
|
||||
os_unfair_lock_lock(&mutex)
|
||||
}
|
||||
|
||||
func unlock() {
|
||||
os_unfair_lock_unlock(&mutex)
|
||||
}
|
||||
|
||||
var description: String { return String(describing: mutex) }
|
||||
|
||||
var customMirror: Mirror { return Mirror(reflecting: mutex) }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
|
||||
#endif // canImport(Darwin)
|
||||
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// SubjectSubscriber.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 16/09/2019.
|
||||
//
|
||||
|
||||
// NOTE: This class has been audited for thread safety.
|
||||
internal final class SubjectSubscriber<Downstream: Subject>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible,
|
||||
Subscription
|
||||
{
|
||||
private let lock = unfairLock()
|
||||
private var downstreamSubject: Downstream?
|
||||
private var upstreamSubscription: Subscription?
|
||||
|
||||
private var isCancelled: Bool { return downstreamSubject == nil }
|
||||
|
||||
internal init(_ parent: Downstream) {
|
||||
self.downstreamSubject = parent
|
||||
}
|
||||
|
||||
internal func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard upstreamSubscription == nil, let subject = downstreamSubject else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
upstreamSubscription = subscription
|
||||
lock.unlock()
|
||||
subject.send(subscription: self)
|
||||
}
|
||||
|
||||
internal func receive(_ input: Downstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard let downstreamSubject = downstreamSubject else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
guard upstreamSubscription != nil else { APIViolationValueBeforeSubscription() }
|
||||
lock.unlock()
|
||||
downstreamSubject.send(input)
|
||||
return .none
|
||||
}
|
||||
|
||||
internal func receive(completion: Subscribers.Completion<Downstream.Failure>) {
|
||||
lock.lock()
|
||||
guard let subject = downstreamSubject else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
guard upstreamSubscription != nil else { APIViolationUnexpectedCompletion() }
|
||||
lock.unlock()
|
||||
subject.send(completion: completion)
|
||||
downstreamSubject = nil
|
||||
}
|
||||
|
||||
internal var description: String { return "Subject" }
|
||||
|
||||
internal var playgroundDescription: Any { return description }
|
||||
|
||||
internal var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("downstreamSubject", downstreamSubject as Any),
|
||||
("upstreamSubscription", upstreamSubscription as Any),
|
||||
("subject", downstreamSubject as Any)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
internal func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard let subscription = upstreamSubscription else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
internal func cancel() {
|
||||
lock.lock()
|
||||
if isCancelled {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
guard let subscription = upstreamSubscription else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
upstreamSubscription = nil
|
||||
downstreamSubject = nil
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// SubscriptionStatus.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 21.09.2019.
|
||||
//
|
||||
|
||||
internal enum SubscriptionStatus {
|
||||
case awaitingSubscription
|
||||
case subscribed(Subscription)
|
||||
case terminal
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// Unreachable.swift
|
||||
// Unreachable
|
||||
//
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2017 Nikolai Vazquez
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
// All the credits go to https://github.com/nvzqz/Unreachable
|
||||
|
||||
/// An unreachable code path.
|
||||
///
|
||||
/// This can be used for whenever the compiler can't determine that a
|
||||
/// path is unreachable, such as dynamically terminating an iterator.
|
||||
@inline(__always)
|
||||
private func unsafeUnreachable() -> Never {
|
||||
return unsafeBitCast((), to: Never.self)
|
||||
}
|
||||
|
||||
/// Asserts that the code path is unreachable.
|
||||
///
|
||||
/// Calls `assertionFailure(_:file:line:)` in unoptimized builds and `unreachable()`
|
||||
/// otherwise.
|
||||
///
|
||||
/// - parameter message: The message to print. The default is
|
||||
/// "Encountered unreachable path".
|
||||
/// - parameter file: The file name to print with the message. The default is the file
|
||||
/// where this function is called.
|
||||
/// - parameter line: The line number to print with the message. The default is the line
|
||||
/// where this function is called.
|
||||
@inline(__always)
|
||||
internal func unreachable(
|
||||
_ message: @autoclosure () -> String = "Encountered unreachable path",
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line
|
||||
) -> Never {
|
||||
var isDebug = false
|
||||
assert({ isDebug = true; return true }())
|
||||
if isDebug {
|
||||
fatalError(message(), file: file, line: line)
|
||||
} else {
|
||||
unsafeUnreachable()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// Violations.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 16/09/2019.
|
||||
//
|
||||
|
||||
internal func APIViolationValueBeforeSubscription(file: StaticString = #file,
|
||||
line: UInt = #line) -> Never {
|
||||
fatalError("""
|
||||
API Violation: received an unexpected value before receiving a Subscription
|
||||
""",
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
|
||||
internal func APIViolationUnexpectedCompletion(file: StaticString = #file,
|
||||
line: UInt = #line) -> Never {
|
||||
fatalError("API Violation: received an unexpected completion", file: file, line: line)
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
internal func abstractMethod(file: StaticString = #file, line: UInt = #line) -> Never {
|
||||
unreachable("Abstract method call", file: file, line: line)
|
||||
}
|
||||
|
||||
extension Subscribers.Demand {
|
||||
internal func assertNonZero(file: StaticString = #file,
|
||||
line: UInt = #line) {
|
||||
if self == .none {
|
||||
fatalError("API Violation: demand must not be zero", file: file, line: line)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
//
|
||||
// Locking.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
#if canImport(Darwin)
|
||||
import Darwin
|
||||
#elseif canImport(Glibc)
|
||||
import Glibc
|
||||
#else
|
||||
#error("How to do locking on this platform?")
|
||||
#endif
|
||||
|
||||
@usableFromInline
|
||||
internal final class Lock {
|
||||
|
||||
@usableFromInline
|
||||
internal var _mutex = pthread_mutex_t()
|
||||
|
||||
@inlinable
|
||||
internal init(recursive: Bool) {
|
||||
var attrib = pthread_mutexattr_t()
|
||||
pthread_mutexattr_init(&attrib)
|
||||
if recursive {
|
||||
pthread_mutexattr_settype(&attrib, Int32(PTHREAD_MUTEX_RECURSIVE))
|
||||
}
|
||||
pthread_mutex_init(&_mutex, &attrib)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
deinit {
|
||||
pthread_mutex_destroy(&_mutex)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
internal func _lock() {
|
||||
pthread_mutex_lock(&_mutex)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
internal func _unlock() {
|
||||
pthread_mutex_unlock(&_mutex)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
internal func `do`<Result>(_ body: () throws -> Result) rethrows -> Result {
|
||||
_lock()
|
||||
defer { _unlock() }
|
||||
return try body()
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
/// specific values on-demand during tests.
|
||||
public final class PassthroughSubject<Output, Failure: Error>: Subject {
|
||||
|
||||
private let _lock = Lock(recursive: true)
|
||||
private let _lock = unfairRecursiveLock()
|
||||
|
||||
private var _completion: Subscribers.Completion<Failure>?
|
||||
|
||||
@@ -39,8 +39,8 @@ public final class PassthroughSubject<Output, Failure: Error>: Subject {
|
||||
}
|
||||
}
|
||||
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Output == SubscriberType.Input, Failure == SubscriberType.Failure
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Failure == Downstream.Failure
|
||||
{
|
||||
_lock.do {
|
||||
if let completion = _completion {
|
||||
@@ -119,7 +119,7 @@ extension PassthroughSubject {
|
||||
fileprivate func request(_ demand: Subscribers.Demand) {
|
||||
precondition(demand > 0, "demand must not be zero")
|
||||
_parent?._lock.do {
|
||||
_demand = demand
|
||||
_demand += demand
|
||||
}
|
||||
_parent?._acknowledgeDownstreamDemand()
|
||||
}
|
||||
|
||||
@@ -38,9 +38,9 @@ public struct Deferred<DeferredPublisher: Publisher>: Publisher {
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Failure == SubscriberType.Failure,
|
||||
Output == SubscriberType.Input
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure,
|
||||
Output == Downstream.Input
|
||||
{
|
||||
let deferredPublisher = createPublisher()
|
||||
deferredPublisher.subscribe(subscriber)
|
||||
|
||||
@@ -42,8 +42,8 @@ public struct Empty<Output, Failure: Error>: Publisher, Equatable {
|
||||
/// to the subscriber. If `false`, it never completes.
|
||||
public let completeImmediately: Bool
|
||||
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Output == SubscriberType.Input, Failure == SubscriberType.Failure
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Failure == Downstream.Failure
|
||||
{
|
||||
subscriber.receive(subscription: Subscriptions.empty)
|
||||
if completeImmediately {
|
||||
|
||||
@@ -30,8 +30,8 @@ public struct Fail<Output, Failure: Error>: Publisher {
|
||||
/// The failure to send when terminating the publisher.
|
||||
public let error: Failure
|
||||
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Output == SubscriberType.Input, Failure == SubscriberType.Failure
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Failure == Downstream.Failure
|
||||
{
|
||||
subscriber.receive(subscription: Subscriptions.empty)
|
||||
subscriber.receive(completion: .failure(error))
|
||||
|
||||
@@ -25,8 +25,8 @@ public struct Just<Output>: Publisher {
|
||||
self.output = output
|
||||
}
|
||||
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where SubscriberType.Input == Output, SubscriberType.Failure == Never
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Never
|
||||
{
|
||||
subscriber.receive(subscription: Inner(value: output, downstream: subscriber))
|
||||
}
|
||||
@@ -251,33 +251,43 @@ extension Just {
|
||||
}
|
||||
}
|
||||
|
||||
private final class Inner<SubscriberType: Subscriber>: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable
|
||||
{
|
||||
private let _output: SubscriberType.Input
|
||||
private var _downstream: SubscriberType?
|
||||
extension Just {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Output
|
||||
{
|
||||
// NOTE: this class has been audited for thread safety.
|
||||
// Combine doesn't use any locking here.
|
||||
|
||||
init(value: SubscriberType.Input, downstream: SubscriberType) {
|
||||
_output = value
|
||||
_downstream = downstream
|
||||
}
|
||||
private var downstream: Downstream?
|
||||
private let value: Output
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
if let downstream = _downstream, demand > 0 {
|
||||
_ = downstream.receive(_output)
|
||||
downstream.receive(completion: .finished)
|
||||
_downstream = nil
|
||||
fileprivate init(value: Output, downstream: Downstream) {
|
||||
self.downstream = downstream
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
_downstream = nil
|
||||
}
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
guard let downstream = self.downstream else { return }
|
||||
self.downstream = nil
|
||||
_ = downstream.receive(value)
|
||||
downstream.receive(completion: .finished)
|
||||
}
|
||||
|
||||
var description: String { return "Just" }
|
||||
func cancel() {
|
||||
downstream = nil
|
||||
}
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, unlabeledChildren: CollectionOfOne(_output))
|
||||
var description: String { return "Just" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, unlabeledChildren: CollectionOfOne(value))
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,8 +50,8 @@ extension Optional {
|
||||
self.output = output
|
||||
}
|
||||
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Output == SubscriberType.Input, Failure == SubscriberType.Failure
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Failure == Downstream.Failure
|
||||
{
|
||||
if let output = output {
|
||||
subscriber.receive(subscription: Inner(value: output,
|
||||
@@ -74,34 +74,44 @@ extension Optional {
|
||||
#endif
|
||||
}
|
||||
|
||||
private final class Inner<SubscriberType: Subscriber>: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable
|
||||
{
|
||||
private let _output: SubscriberType.Input
|
||||
private var _downstream: SubscriberType?
|
||||
extension Optional.OCombine {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Wrapped
|
||||
{
|
||||
// NOTE: this class has been audited for thread safety.
|
||||
// Combine doesn't use any locking here.
|
||||
|
||||
init(value: SubscriberType.Input, downstream: SubscriberType) {
|
||||
_output = value
|
||||
_downstream = downstream
|
||||
}
|
||||
private var downstream: Downstream?
|
||||
private let output: Wrapped
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
if let downstream = _downstream, demand > 0 {
|
||||
_ = downstream.receive(_output)
|
||||
downstream.receive(completion: .finished)
|
||||
_downstream = nil
|
||||
init(value: Wrapped, downstream: Downstream) {
|
||||
self.output = value
|
||||
self.downstream = downstream
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
_downstream = nil
|
||||
}
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
guard let downstream = self.downstream else { return }
|
||||
self.downstream = nil
|
||||
_ = downstream.receive(output)
|
||||
downstream.receive(completion: .finished)
|
||||
}
|
||||
|
||||
var description: String { return "Optional" }
|
||||
func cancel() {
|
||||
downstream = nil
|
||||
}
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, unlabeledChildren: CollectionOfOne(_output))
|
||||
var description: String { return "Optional" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, unlabeledChildren: CollectionOfOne(output))
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
//
|
||||
// Publishers.Autoconnect.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 18/09/2019.
|
||||
//
|
||||
|
||||
extension ConnectablePublisher {
|
||||
|
||||
/// Automates the process of connecting or disconnecting from this connectable
|
||||
/// publisher.
|
||||
///
|
||||
/// Use `autoconnect()` to simplify working with `ConnectablePublisher` instances,
|
||||
/// such as those created with `makeConnectable()`.
|
||||
///
|
||||
/// let autoconnectedPublisher = somePublisher
|
||||
/// .makeConnectable()
|
||||
/// .autoconnect()
|
||||
/// .subscribe(someSubscriber)
|
||||
///
|
||||
/// - Returns: A publisher which automatically connects to its upstream connectable
|
||||
/// publisher.
|
||||
public func autoconnect() -> Publishers.Autoconnect<Self> {
|
||||
return .init(upstream: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that automatically connects and disconnects from this connectable
|
||||
/// publisher.
|
||||
public class Autoconnect<Upstream: ConnectablePublisher>: Publisher {
|
||||
|
||||
// NOTE: This class has been audited for thread safety
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
private enum State {
|
||||
case disconnected
|
||||
case connected(refcount: Int, connection: Cancellable)
|
||||
}
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public final let upstream: Upstream
|
||||
|
||||
private let lock = unfairLock()
|
||||
|
||||
private var state = State.disconnected
|
||||
|
||||
public init(upstream: Upstream) {
|
||||
self.upstream = upstream
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure
|
||||
{
|
||||
let inner = Inner(parent: self, downstream: subscriber)
|
||||
lock.lock()
|
||||
switch state {
|
||||
case let .connected(refcount, connection):
|
||||
state = .connected(refcount: refcount + 1, connection: connection)
|
||||
lock.unlock()
|
||||
upstream.subscribe(inner)
|
||||
case .disconnected:
|
||||
lock.unlock()
|
||||
upstream.subscribe(inner)
|
||||
let connection = upstream.connect()
|
||||
lock.lock()
|
||||
state = .connected(refcount: 1, connection: connection)
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func willCancel() {
|
||||
lock.lock()
|
||||
switch state {
|
||||
case let .connected(refcount, connection):
|
||||
if refcount <= 1 {
|
||||
self.state = .disconnected
|
||||
lock.unlock()
|
||||
connection.cancel()
|
||||
} else {
|
||||
state = .connected(refcount: refcount - 1, connection: connection)
|
||||
lock.unlock()
|
||||
}
|
||||
case .disconnected:
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Autoconnect {
|
||||
|
||||
private struct Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
fileprivate let combineIdentifier = CombineIdentifier()
|
||||
|
||||
private let parent: Publishers.Autoconnect<Upstream>
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
fileprivate init(parent: Publishers.Autoconnect<Upstream>,
|
||||
downstream: Downstream) {
|
||||
self.parent = parent
|
||||
self.downstream = downstream
|
||||
}
|
||||
|
||||
fileprivate func receive(subscription: Subscription) {
|
||||
let sideEffectSubscription = SideEffectSubscription(subscription,
|
||||
parent: parent)
|
||||
downstream.receive(subscription: sideEffectSubscription)
|
||||
}
|
||||
|
||||
fileprivate func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
return downstream.receive(input)
|
||||
}
|
||||
|
||||
fileprivate func receive(completion: Subscribers.Completion<Failure>) {
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
fileprivate var description: String { return "Autoconnect" }
|
||||
|
||||
fileprivate var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("parent", parent),
|
||||
("downstream", downstream)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
fileprivate var playgroundDescription: Any { return description }
|
||||
}
|
||||
|
||||
private struct SideEffectSubscription
|
||||
: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
private let parent: Publishers.Autoconnect<Upstream>
|
||||
|
||||
private let upstreamSubscription: Subscription
|
||||
|
||||
fileprivate init(_ upstreamSubscription: Subscription,
|
||||
parent: Publishers.Autoconnect<Upstream>) {
|
||||
self.parent = parent
|
||||
self.upstreamSubscription = upstreamSubscription
|
||||
}
|
||||
|
||||
fileprivate func request(_ demand: Subscribers.Demand) {
|
||||
upstreamSubscription.request(demand)
|
||||
}
|
||||
|
||||
fileprivate func cancel() {
|
||||
parent.willCancel()
|
||||
upstreamSubscription.cancel()
|
||||
}
|
||||
|
||||
fileprivate var combineIdentifier: CombineIdentifier {
|
||||
return upstreamSubscription.combineIdentifier
|
||||
}
|
||||
|
||||
fileprivate var description: String {
|
||||
return String(describing: upstreamSubscription)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any {
|
||||
return description
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,11 +33,11 @@ extension Publishers {
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Upstream.Failure == SubscriberType.Failure,
|
||||
SubscriberType.Input == Output
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Downstream.Input == Output
|
||||
{
|
||||
let count = _Count<Upstream, SubscriberType>(downstream: subscriber)
|
||||
let count = _Count<Upstream, Downstream>(downstream: subscriber)
|
||||
upstream.subscribe(count)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +35,10 @@ extension Publishers {
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Failure == SubscriberType.Failure, Output == SubscriberType.Input
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
let decodeSubscriber = _Decode<Upstream, SubscriberType, Coder>(
|
||||
let decodeSubscriber = _Decode<Upstream, Downstream, Coder>(
|
||||
downstream: subscriber,
|
||||
decoder: _decoder
|
||||
)
|
||||
|
||||
@@ -26,8 +26,8 @@ extension Publishers {
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Failure == SubscriberType.Failure, Output == SubscriberType.Input
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, predicate: catching(predicate))
|
||||
upstream.subscribe(inner)
|
||||
@@ -53,8 +53,8 @@ extension Publishers {
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Output == SubscriberType.Input, SubscriberType.Failure == Error
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Downstream.Failure == Error
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, predicate: catching(predicate))
|
||||
upstream.subscribe(inner)
|
||||
|
||||
@@ -37,10 +37,10 @@ extension Publishers {
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Failure == SubscriberType.Failure, Output == SubscriberType.Input
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
let encodeSubscriber = _Encode<Upstream, SubscriberType, Coder>(
|
||||
let encodeSubscriber = _Encode<Upstream, Downstream, Coder>(
|
||||
downstream: subscriber,
|
||||
encoder: encoder
|
||||
)
|
||||
|
||||
@@ -93,9 +93,9 @@ extension Publishers {
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Upstream.Failure == SubscriberType.Failure,
|
||||
Upstream.Output == SubscriberType.Input
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
let filter = Inner(downstream: subscriber, isIncluded: catching(isIncluded))
|
||||
upstream.receive(subscriber: filter)
|
||||
|
||||
@@ -130,6 +130,8 @@ extension Publishers {
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.First: Equatable where Upstream: Equatable {}
|
||||
|
||||
private class _FirstWhere<Upstream: Publisher, Downstream: Subscriber>
|
||||
: OperatorSubscription<Downstream>,
|
||||
Subscription
|
||||
|
||||
@@ -15,13 +15,14 @@ extension Publisher {
|
||||
/// - Parameters:
|
||||
/// - maxPublishers: The maximum number of publishers produced by this method.
|
||||
/// - transform: A closure that takes an element as a parameter and returns a
|
||||
/// publisher that produces elements of that type.
|
||||
/// publisher that produces elements of that type.
|
||||
/// - Returns: A publisher that transforms elements from an upstream publisher into
|
||||
/// a publisher of that element’s type.
|
||||
public func flatMap<Result, Child>(maxPublishers: Subscribers.Demand = .unlimited,
|
||||
_ transform: @escaping (Self.Output) -> Child)
|
||||
-> Publishers.FlatMap<Child, Self>
|
||||
where Result == Child.Output, Child: Publisher, Self.Failure == Child.Failure {
|
||||
/// a publisher of that element’s type.
|
||||
public func flatMap<Result, Child: Publisher>(
|
||||
maxPublishers: Subscribers.Demand = .unlimited,
|
||||
_ transform: @escaping (Output) -> Child
|
||||
) -> Publishers.FlatMap<Child, Self>
|
||||
where Result == Child.Output, Failure == Child.Failure {
|
||||
return Publishers.FlatMap(upstream: self,
|
||||
maxPublishers: maxPublishers,
|
||||
transform: transform)
|
||||
@@ -29,9 +30,10 @@ extension Publisher {
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
public struct FlatMap<Child, Upstream>: Publisher
|
||||
where Child: Publisher, Upstream: Publisher, Child.Failure == Upstream.Failure {
|
||||
|
||||
public struct FlatMap<Child: Publisher, Upstream: Publisher>: Publisher
|
||||
where Child.Failure == Upstream.Failure
|
||||
{
|
||||
/// The kind of values published by this publisher.
|
||||
public typealias Output = Child.Output
|
||||
|
||||
@@ -60,23 +62,24 @@ extension Publishers {
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<Downstream>(subscriber: Downstream)
|
||||
where Downstream: Subscriber,
|
||||
Child.Output == Downstream.Input,
|
||||
Upstream.Failure == Downstream.Failure {
|
||||
let inner = Inner(downstream: subscriber,
|
||||
maxPublishers: maxPublishers,
|
||||
transform: transform)
|
||||
upstream.subscribe(inner)
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Child.Output == Downstream.Input, Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
let inner = Inner(downstream: subscriber,
|
||||
maxPublishers: maxPublishers,
|
||||
transform: transform)
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.FlatMap {
|
||||
|
||||
fileprivate final class Inner<Downstream: Subscriber>
|
||||
: CustomStringConvertible,
|
||||
Cancellable
|
||||
where Downstream.Input == Child.Output, Downstream.Failure == Upstream.Failure {
|
||||
Cancellable
|
||||
where Downstream.Input == Child.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
@@ -85,11 +88,13 @@ extension Publishers.FlatMap {
|
||||
// If the value was buffered at the time it became available, and the child's
|
||||
// demand was left at `.none` we keep track of the child in `pausedChild` so
|
||||
// that we can demand some more of it after sending this value.
|
||||
pausedChild: ChildSubscriber?)
|
||||
pausedChild: ChildSubscriber?
|
||||
)
|
||||
|
||||
private let lock = Lock(recursive: false)
|
||||
private let lock = unfairLock()
|
||||
private let maxPublishers: Subscribers.Demand
|
||||
private let transform: (Upstream.Output) -> Child
|
||||
private let transform: (Input) -> Child
|
||||
|
||||
// Locking rules for this class.
|
||||
// - All mutable state must only be accessed while `lock` is held.
|
||||
// - In order to avoid any deadlock potential, it is absolutely forbidden to have
|
||||
@@ -114,12 +119,14 @@ extension Publishers.FlatMap {
|
||||
}
|
||||
|
||||
final func cancel() {
|
||||
let (upstreamToCancel, childrenToCancel) = lock.do { ()
|
||||
-> (Subscription?, Set<ChildSubscriber>) in
|
||||
let upstreamToCancel = upstreamSubscription
|
||||
upstreamSubscription = nil
|
||||
return (upstreamToCancel, lockedDeactivateAndReturnChildToCancel())
|
||||
}
|
||||
|
||||
let (upstreamToCancel, childrenToCancel) = lock
|
||||
.do { () -> (Subscription?, Set<ChildSubscriber>) in
|
||||
let upstreamToCancel = upstreamSubscription
|
||||
upstreamSubscription = nil
|
||||
return (upstreamToCancel, lockedDeactivateAndReturnChildrenToCancel())
|
||||
}
|
||||
|
||||
upstreamToCancel?.cancel()
|
||||
cancelChildren(childrenToCancel)
|
||||
}
|
||||
@@ -128,12 +135,13 @@ extension Publishers.FlatMap {
|
||||
|
||||
// Private implementation
|
||||
extension Publishers.FlatMap.Inner {
|
||||
|
||||
private func deactivate() {
|
||||
cancelChildren(lock.do(lockedDeactivateAndReturnChildToCancel))
|
||||
cancelChildren(lock.do(lockedDeactivateAndReturnChildrenToCancel))
|
||||
}
|
||||
|
||||
// Must be called with lock held.
|
||||
private func lockedDeactivateAndReturnChildToCancel() -> Set<ChildSubscriber> {
|
||||
private func lockedDeactivateAndReturnChildrenToCancel() -> Set<ChildSubscriber> {
|
||||
downstream = nil
|
||||
downstreamDemand = .none
|
||||
let result = childSubscribers
|
||||
@@ -212,7 +220,8 @@ extension Publishers.FlatMap.Inner {
|
||||
}
|
||||
|
||||
let demandResult = surplusAvailable || demandForChild() == .unlimited
|
||||
? Subscribers.Demand.none : Subscribers.Demand.max(1)
|
||||
? Subscribers.Demand.none
|
||||
: .max(1)
|
||||
|
||||
if processTheQueue {
|
||||
processQueue()
|
||||
@@ -222,7 +231,7 @@ extension Publishers.FlatMap.Inner {
|
||||
}
|
||||
|
||||
private func demandForChild() -> Subscribers.Demand {
|
||||
return self.downstreamDemand == .unlimited ? .unlimited : .max(1)
|
||||
return downstreamDemand == .unlimited ? .unlimited : .max(1)
|
||||
}
|
||||
|
||||
private enum QueueWorkStatus {
|
||||
@@ -284,7 +293,7 @@ extension Publishers.FlatMap.Inner {
|
||||
|
||||
// This `Subscriber` implementation is for `FlatMap`'s upstream subscription
|
||||
extension Publishers.FlatMap.Inner: Subscriber {
|
||||
// FlatMap received the upstream subscription
|
||||
|
||||
fileprivate func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
downstream?.receive(subscription: self)
|
||||
@@ -304,7 +313,6 @@ extension Publishers.FlatMap.Inner: Subscriber {
|
||||
return .none
|
||||
}
|
||||
|
||||
// Upstream subscription completed
|
||||
fileprivate func receive(completion: Subscribers.Completion<Failure>) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
@@ -320,8 +328,7 @@ extension Publishers.FlatMap.Inner: Subscriber {
|
||||
extension Publishers.FlatMap.Inner: Subscription {
|
||||
fileprivate func request(_ demand: Subscribers.Demand) {
|
||||
let (drainTheQueue, becameUnlimited) = lock.do { () -> (Bool, Bool) in
|
||||
let becameUnlimited =
|
||||
(demand == .unlimited) && (downstreamDemand != .unlimited)
|
||||
let becameUnlimited = demand == .unlimited && downstreamDemand != .unlimited
|
||||
downstreamDemand = demand
|
||||
defer { queueIsBeingProcessed = true }
|
||||
return (!queueIsBeingProcessed, becameUnlimited)
|
||||
@@ -377,14 +384,15 @@ extension Publishers.FlatMap.Inner {
|
||||
}
|
||||
|
||||
extension Publishers.FlatMap.Inner.ChildSubscriber: Cancellable {
|
||||
internal func cancel() {
|
||||
fileprivate func cancel() {
|
||||
_upstreamSubscription?.cancel()
|
||||
_upstreamSubscription = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.FlatMap.Inner.ChildSubscriber: Subscriber {
|
||||
internal func receive(subscription: Subscription) {
|
||||
|
||||
fileprivate func receive(subscription: Subscription) {
|
||||
if _upstreamSubscription == nil {
|
||||
_upstreamSubscription = subscription
|
||||
subscription.request(_parent.demandForChild())
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// Publishers.MakeConnectable.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 18/09/2019.
|
||||
//
|
||||
|
||||
extension Publisher where Failure == Never {
|
||||
|
||||
/// Creates a connectable wrapper around the publisher.
|
||||
///
|
||||
/// - Returns: A `ConnectablePublisher` wrapping this publisher.
|
||||
public func makeConnectable() -> Publishers.MakeConnectable<Self> {
|
||||
return .init(upstream: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
public struct MakeConnectable<Upstream: Publisher>: ConnectablePublisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
private let inner: Multicast<Upstream, PassthroughSubject<Output, Failure>>
|
||||
|
||||
public init(upstream: Upstream) {
|
||||
inner = upstream.multicast(subject: .init())
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == Failure, Downstream.Input == Output
|
||||
{
|
||||
inner.subscribe(subscriber)
|
||||
}
|
||||
|
||||
public func connect() -> Cancellable {
|
||||
return inner.connect()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,12 @@ extension Publishers {
|
||||
self.upstream = upstream
|
||||
self.transform = transform
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, map: transform))
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that transforms all elements from the upstream publisher
|
||||
@@ -78,12 +84,6 @@ extension Publishers {
|
||||
}
|
||||
|
||||
extension Publishers.Map {
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, transform: catching(transform))
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
|
||||
public func map<Result>(
|
||||
_ transform: @escaping (Output) -> Result
|
||||
@@ -103,8 +103,7 @@ extension Publishers.TryMap {
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Downstream.Failure == Error
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, transform: catching(transform))
|
||||
upstream.subscribe(inner)
|
||||
upstream.subscribe(Inner(downstream: subscriber, map: transform))
|
||||
}
|
||||
|
||||
public func map<Result>(
|
||||
@@ -120,89 +119,154 @@ extension Publishers.TryMap {
|
||||
}
|
||||
}
|
||||
|
||||
private class _Map<Upstream: Publisher, Downstream: Subscriber>
|
||||
: OperatorSubscription<Downstream>
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
typealias Transform = (Input) -> Result<Downstream.Input, Downstream.Failure>
|
||||
|
||||
fileprivate var _transform: Transform?
|
||||
|
||||
var isCompleted: Bool {
|
||||
return _transform == nil
|
||||
}
|
||||
|
||||
init(downstream: Downstream, transform: @escaping Transform) {
|
||||
_transform = transform
|
||||
super.init(downstream: downstream)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
switch _transform?(input) {
|
||||
case .success(let output)?:
|
||||
return downstream.receive(output)
|
||||
case .failure(let error)?:
|
||||
downstream.receive(completion: .failure(error))
|
||||
_transform = nil
|
||||
return .none
|
||||
case nil:
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Map {
|
||||
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: _Map<Upstream, Downstream>,
|
||||
private struct Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
Subscriber
|
||||
where Downstream.Failure == Upstream.Failure
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let map: (Input) -> Output
|
||||
|
||||
let combineIdentifier = CombineIdentifier()
|
||||
|
||||
fileprivate init(downstream: Downstream, map: @escaping (Input) -> Output) {
|
||||
self.downstream = downstream
|
||||
self.map = map
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
downstream.receive(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return downstream.receive(map(input))
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
var description: String { return "Map" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryMap {
|
||||
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: _Map<Upstream, Downstream>,
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
Subscriber
|
||||
where Downstream.Failure == Error
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Output, Downstream.Failure == Error
|
||||
{
|
||||
// NOTE: This class has been audited for thread-safety
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let map: (Input) throws -> Output
|
||||
|
||||
private var status = SubscriptionStatus.awaitingSubscription
|
||||
|
||||
private let lock = unfairLock()
|
||||
|
||||
let combineIdentifier = CombineIdentifier()
|
||||
|
||||
fileprivate init(downstream: Downstream,
|
||||
map: @escaping (Input) throws -> Output) {
|
||||
self.downstream = downstream
|
||||
self.map = map
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = status else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
status = .subscribed(subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
if !isCompleted {
|
||||
_transform = nil
|
||||
downstream.receive(completion: completion.eraseError())
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
do {
|
||||
return try downstream.receive(map(input))
|
||||
} catch {
|
||||
lock.lock()
|
||||
let subscription: Subscription?
|
||||
switch status {
|
||||
case let .subscribed(upstreamSubscription):
|
||||
subscription = upstreamSubscription
|
||||
case .awaitingSubscription, .terminal:
|
||||
subscription = nil
|
||||
}
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
subscription?.cancel()
|
||||
downstream.receive(completion: .failure(error))
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
upstreamSubscription?.request(demand)
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
guard case .subscribed = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion.eraseError())
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
_transform = nil
|
||||
super.cancel()
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "TryMap" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,15 +32,11 @@ extension Publishers {
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Failure == SubscriberType.Failure,
|
||||
Upstream.Output == SubscriberType.Input
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
let mapErrorSubscriber = _MapError<Upstream, SubscriberType>(
|
||||
downstream: subscriber,
|
||||
transform: transform
|
||||
)
|
||||
upstream.subscribe(mapErrorSubscriber)
|
||||
upstream.subscribe(Inner(downstream: subscriber, map: transform))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,41 +60,50 @@ extension Publisher {
|
||||
}
|
||||
}
|
||||
|
||||
private final class _MapError<Upstream: Publisher, Downstream: Subscriber>
|
||||
: OperatorSubscription<Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible
|
||||
where Upstream.Output == Downstream.Input
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
typealias Output = Downstream.Input
|
||||
extension Publishers.MapError {
|
||||
|
||||
private let _transform: (Upstream.Failure) -> Downstream.Failure
|
||||
private struct Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Upstream.Output == Downstream.Input
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
var description: String { return "MapError" }
|
||||
private let downstream: Downstream
|
||||
private let map: (Upstream.Failure) -> Downstream.Failure
|
||||
|
||||
init(downstream: Downstream,
|
||||
transform: @escaping (Upstream.Failure) -> Downstream.Failure) {
|
||||
self._transform = transform
|
||||
super.init(downstream: downstream)
|
||||
}
|
||||
let combineIdentifier = CombineIdentifier()
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
downstream.receive(subscription: subscription)
|
||||
}
|
||||
var description: String { return "MapError" }
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return downstream.receive(input)
|
||||
}
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
downstream.receive(completion: .finished)
|
||||
case .failure(let error):
|
||||
downstream.receive(completion: .failure(_transform(error)))
|
||||
var playgroundDescription: Any { return description }
|
||||
|
||||
init(downstream: Downstream,
|
||||
map: @escaping (Upstream.Failure) -> Downstream.Failure) {
|
||||
self.downstream = downstream
|
||||
self.map = map
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
downstream.receive(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return downstream.receive(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
downstream.receive(completion: .finished)
|
||||
case .failure(let error):
|
||||
downstream.receive(completion: .failure(map(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,9 +28,10 @@ extension Publishers {
|
||||
|
||||
public final class Multicast<Upstream: Publisher, SubjectType: Subject>
|
||||
: ConnectablePublisher
|
||||
where Upstream.Failure == SubjectType.Failure,
|
||||
Upstream.Output == SubjectType.Output
|
||||
where Upstream.Failure == SubjectType.Failure,
|
||||
Upstream.Output == SubjectType.Output
|
||||
{
|
||||
// NOTE: This class has been audited for thread safety
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
@@ -40,7 +41,22 @@ extension Publishers {
|
||||
|
||||
public let createSubject: () -> SubjectType
|
||||
|
||||
private lazy var _subject: SubjectType = self.createSubject()
|
||||
private let lock = unfairLock()
|
||||
|
||||
private var subject: SubjectType?
|
||||
|
||||
private var lazySubject: SubjectType {
|
||||
lock.lock()
|
||||
if let subject = subject {
|
||||
lock.unlock()
|
||||
return subject
|
||||
}
|
||||
|
||||
let subject = createSubject()
|
||||
self.subject = subject
|
||||
lock.unlock()
|
||||
return subject
|
||||
}
|
||||
|
||||
public init(upstream: Upstream, createSubject: @escaping () -> SubjectType) {
|
||||
self.upstream = upstream
|
||||
@@ -51,18 +67,11 @@ extension Publishers {
|
||||
where SubjectType.Failure == Downstream.Failure,
|
||||
SubjectType.Output == Downstream.Input
|
||||
{
|
||||
_subject.subscribe(Inner(downstream: subscriber))
|
||||
lazySubject.subscribe(Inner(parent: self, downstream: subscriber))
|
||||
}
|
||||
|
||||
public func connect() -> Cancellable {
|
||||
|
||||
let subscriber = SubjectSubscriber(_subject)
|
||||
|
||||
upstream.subscribe(subscriber)
|
||||
|
||||
return AnyCancellable {
|
||||
subscriber.downstreamSubject = nil
|
||||
}
|
||||
return upstream.subscribe(lazySubject)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,32 +79,101 @@ extension Publishers {
|
||||
extension Publishers.Multicast {
|
||||
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: OperatorSubscription<Downstream>,
|
||||
Subscriber,
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
Subscription
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Upstream.Output == Downstream.Input, Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
// NOTE: This class has been audited for thread safety
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
var description: String { return "Multicast" }
|
||||
private enum State {
|
||||
case ready(upstream: Upstream, downstream: Downstream)
|
||||
case subscribed(upstream: Upstream,
|
||||
downstream: Downstream,
|
||||
subjectSubscription: Subscription)
|
||||
case terminal
|
||||
}
|
||||
|
||||
private let lock = unfairLock()
|
||||
|
||||
private var state: State
|
||||
|
||||
fileprivate init(parent: Publishers.Multicast<Upstream, SubjectType>,
|
||||
downstream: Downstream) {
|
||||
state = .ready(upstream: parent.upstream, downstream: downstream)
|
||||
}
|
||||
|
||||
fileprivate var description: String { return "Multicast" }
|
||||
|
||||
fileprivate var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
fileprivate var playgroundDescription: Any { return description }
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
lock.lock()
|
||||
guard case let .ready(upstream, downstream) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .subscribed(upstream: upstream,
|
||||
downstream: downstream,
|
||||
subjectSubscription: subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return downstream.receive(input)
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, downstream, subjectSubscription) = state else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
lock.unlock()
|
||||
let newDemand = downstream.receive(input)
|
||||
if newDemand > 0 {
|
||||
subjectSubscription.request(newDemand)
|
||||
}
|
||||
return .none
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<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) {
|
||||
upstreamSubscription?.request(demand)
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subjectSubscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subjectSubscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(_, _, subjectSubscription) = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .terminal
|
||||
lock.unlock()
|
||||
subjectSubscription.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,8 +43,8 @@ extension Publishers {
|
||||
self.stream = stream
|
||||
}
|
||||
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Failure == SubscriberType.Failure, Output == SubscriberType.Input
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, prefix: prefix, stream: stream)
|
||||
upstream.subscribe(inner)
|
||||
@@ -65,99 +65,93 @@ extension Publisher {
|
||||
}
|
||||
}
|
||||
|
||||
private final class Inner<Downstream: Subscriber>: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable
|
||||
{
|
||||
typealias Input = Downstream.Input
|
||||
typealias Failure = Downstream.Failure
|
||||
extension Publishers.Print {
|
||||
private final class Inner<Downstream: Subscriber>: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable
|
||||
{
|
||||
typealias Input = Downstream.Input
|
||||
typealias Failure = Downstream.Failure
|
||||
|
||||
private var _downstream: Downstream
|
||||
private let _prefix: String
|
||||
private var _stream: TextOutputStream
|
||||
private var _upstreamSubscription: Subscription?
|
||||
private let _printerLock = Lock(recursive: false)
|
||||
/// A concrete type wrapper around an abstract stream.
|
||||
private struct PrintTarget: TextOutputStream {
|
||||
|
||||
init(downstream: Downstream, prefix: String, stream: TextOutputStream?) {
|
||||
_downstream = downstream
|
||||
_prefix = prefix
|
||||
_stream = stream ?? StdoutStream()
|
||||
}
|
||||
var stream: TextOutputStream
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
_log("receive subscription", value: subscription)
|
||||
_upstreamSubscription = subscription
|
||||
_downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
_log("receive value", value: input)
|
||||
let demand = _downstream.receive(input)
|
||||
_logDemand(demand, synchronous: true)
|
||||
return demand
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
_log("receive finished")
|
||||
case .failure(let error):
|
||||
_log("receive error", value: error)
|
||||
mutating func write(_ string: String) {
|
||||
stream.write(string)
|
||||
}
|
||||
}
|
||||
_downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
_logDemand(demand, synchronous: false)
|
||||
_upstreamSubscription?.request(demand)
|
||||
}
|
||||
private var downstream: Downstream
|
||||
private let prefix: String
|
||||
private var stream: PrintTarget?
|
||||
private var subscription: Subscription?
|
||||
private let lock = unfairLock()
|
||||
|
||||
func cancel() {
|
||||
_log("receive cancel")
|
||||
_upstreamSubscription?.cancel()
|
||||
_upstreamSubscription = nil
|
||||
}
|
||||
|
||||
var description: String { return "Print" }
|
||||
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
private func _log(_ description: String,
|
||||
value: Any? = nil,
|
||||
additionalInfo: String = "") {
|
||||
_printerLock.do {
|
||||
if !_prefix.isEmpty {
|
||||
_stream.write(_prefix)
|
||||
_stream.write(": ")
|
||||
}
|
||||
_stream.write(description)
|
||||
if let value = value {
|
||||
_stream.write(": (")
|
||||
_stream.write(String(describing: value))
|
||||
_stream.write(")")
|
||||
}
|
||||
if !additionalInfo.isEmpty {
|
||||
_stream.write(" (")
|
||||
_stream.write(additionalInfo)
|
||||
_stream.write(")")
|
||||
}
|
||||
_stream.write("\n")
|
||||
init(downstream: Downstream, prefix: String, stream: TextOutputStream?) {
|
||||
self.downstream = downstream
|
||||
self.prefix = prefix.isEmpty ? "" : "\(prefix): "
|
||||
self.stream = stream.map(PrintTarget.init)
|
||||
}
|
||||
}
|
||||
|
||||
private func _logDemand(_ demand: Subscribers.Demand, synchronous: Bool) {
|
||||
let synchronouslyStr = synchronous ? "synchronous" : ""
|
||||
if let max = demand.max {
|
||||
_log("request max", value: max, additionalInfo: synchronouslyStr)
|
||||
} else {
|
||||
_log("request unlimited", additionalInfo: synchronouslyStr)
|
||||
func receive(subscription: Subscription) {
|
||||
log("\(prefix)receive subscription: (\(subscription))")
|
||||
lock.do {
|
||||
self.subscription = subscription
|
||||
}
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
log("\(prefix)receive value: (\(input))")
|
||||
let demand = downstream.receive(input)
|
||||
|
||||
if let max = demand.max {
|
||||
log("\(prefix)request max: (\(max)) (synchronous)")
|
||||
} else {
|
||||
log("\(prefix)request unlimited (synchronous)")
|
||||
}
|
||||
|
||||
return demand
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
log("\(prefix)receive finished")
|
||||
case .failure(let error):
|
||||
log("\(prefix)receive error: (\(error))")
|
||||
}
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
if let max = demand.max {
|
||||
log("\(prefix)request max: (\(max))")
|
||||
} else {
|
||||
log("\(prefix)request unlimited")
|
||||
}
|
||||
subscription?.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
log("\(prefix)receive cancel")
|
||||
subscription?.cancel()
|
||||
subscription = nil
|
||||
}
|
||||
|
||||
var description: String { return "Print" }
|
||||
|
||||
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
|
||||
|
||||
private func log(_ text: String) {
|
||||
if var stream = stream {
|
||||
Swift.print(text, to: &stream)
|
||||
} else {
|
||||
Swift.print("", text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct StdoutStream: TextOutputStream {
|
||||
mutating func write(_ string: String) {
|
||||
print(string, terminator: "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,19 @@
|
||||
// Created by Bogdan Vlad on 8/29/19.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
/// Replaces any errors in the stream with the provided element.
|
||||
///
|
||||
/// If the upstream publisher fails with an error, this publisher emits the provided
|
||||
/// element, then finishes normally.
|
||||
/// - Parameter output: An element to emit when the upstream publisher fails.
|
||||
/// - Returns: A publisher that replaces an error from the upstream publisher with
|
||||
/// the provided output element.
|
||||
public func replaceError(with output: Output) -> Publishers.ReplaceError<Self> {
|
||||
return .init(upstream: self, output: output)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
/// A publisher that replaces any errors in the stream with a provided element.
|
||||
public struct ReplaceError<Upstream: Publisher>: Publisher {
|
||||
@@ -36,97 +49,131 @@ extension Publishers {
|
||||
/// - Parameters:
|
||||
/// - subscriber: The subscriber to attach to this `Publisher`.
|
||||
/// once attached it can begin to receive values.
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Upstream.Output == SubscriberType.Input,
|
||||
SubscriberType.Failure == Failure
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Output == Downstream.Input, Downstream.Failure == Failure
|
||||
{
|
||||
let replaceErrorSubscriber = _ReplaceError<Upstream, SubscriberType>(
|
||||
downstream: subscriber,
|
||||
output: output
|
||||
)
|
||||
upstream.subscribe(replaceErrorSubscriber)
|
||||
upstream.subscribe(Inner(downstream: subscriber, output: output))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
/// Replaces any errors in the stream with the provided element.
|
||||
///
|
||||
/// If the upstream publisher fails with an error, this publisher emits the provided
|
||||
/// element, then finishes normally.
|
||||
/// - Parameter output: An element to emit when the upstream publisher fails.
|
||||
/// - Returns: A publisher that replaces an error from the upstream publisher with
|
||||
/// the provided output element.
|
||||
public func replaceError(with output: Output) -> Publishers.ReplaceError<Self> {
|
||||
return Publishers.ReplaceError(upstream: self, output: output)
|
||||
}
|
||||
}
|
||||
extension Publishers.ReplaceError: Equatable
|
||||
where Upstream: Equatable, Upstream.Output: Equatable
|
||||
{}
|
||||
|
||||
private final class _ReplaceError<Upstream: Publisher, Downstream: Subscriber>
|
||||
: OperatorSubscription<Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible,
|
||||
Subscription
|
||||
where Upstream.Output == Downstream.Input
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
extension Publishers.ReplaceError {
|
||||
|
||||
var downstreamDemandCounter: Subscribers.Demand = .none
|
||||
var hasFailed: Bool = false
|
||||
var description: String { return "ReplaceError" }
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Upstream.Output == Downstream.Input
|
||||
{
|
||||
// NOTE: this class has been audited for thread safety.
|
||||
|
||||
private let output: Downstream.Input
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
init(downstream: Downstream,
|
||||
output: Downstream.Input) {
|
||||
self.output = output
|
||||
super.init(downstream: downstream)
|
||||
}
|
||||
private let output: Upstream.Output
|
||||
private let downstream: Downstream
|
||||
private var status = SubscriptionStatus.awaitingSubscription
|
||||
private var terminated = false
|
||||
private var pendingDemand = Subscribers.Demand.none
|
||||
private var lock = unfairLock()
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
if hasFailed {
|
||||
_ = downstream.receive(output)
|
||||
downstream?.receive(completion: .finished)
|
||||
} else {
|
||||
downstreamDemandCounter += demand
|
||||
upstreamSubscription?.request(demand)
|
||||
fileprivate init(downstream: Downstream, output: Upstream.Output) {
|
||||
self.downstream = downstream
|
||||
self.output = output
|
||||
}
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = status else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
status = .subscribed(subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
downstreamDemandCounter -= 1
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case .subscribed = status else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
pendingDemand -= 1
|
||||
lock.unlock()
|
||||
let demand = downstream.receive(input)
|
||||
guard demand > 0 else {
|
||||
return .none
|
||||
}
|
||||
lock.lock()
|
||||
pendingDemand += demand
|
||||
lock.unlock()
|
||||
return demand
|
||||
}
|
||||
|
||||
let demand = downstream.receive(input)
|
||||
downstreamDemandCounter += demand
|
||||
|
||||
return demand
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
downstream.receive(completion: .finished)
|
||||
case .failure:
|
||||
hasFailed = true
|
||||
|
||||
// If there was no demand from downstream,
|
||||
// ReplaceError does not forward the value that
|
||||
// replaces the error until it is requested.
|
||||
if downstreamDemandCounter > 0 {
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
downstream.receive(completion: .finished)
|
||||
case .failure:
|
||||
lock.lock()
|
||||
// If there was no demand from downstream,
|
||||
// ReplaceError does not forward the value that
|
||||
// replaces the error until it is requested.
|
||||
guard pendingDemand > 0 else {
|
||||
terminated = true
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
_ = downstream.receive(output)
|
||||
downstream?.receive(completion: .finished)
|
||||
downstream.receive(completion: .finished)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
upstreamSubscription?.cancel()
|
||||
upstreamSubscription = nil
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
lock.lock()
|
||||
if terminated {
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
_ = downstream.receive(output)
|
||||
downstream.receive(completion: .finished)
|
||||
return
|
||||
}
|
||||
pendingDemand += demand
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "ReplaceError" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,11 +25,13 @@ extension Publishers {
|
||||
self.sequence = sequence
|
||||
}
|
||||
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Failure == SubscriberType.Failure,
|
||||
Elements.Element == SubscriberType.Input
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure,
|
||||
Elements.Element == Downstream.Input
|
||||
{
|
||||
if let inner = Inner(downstream: subscriber, sequence: sequence) {
|
||||
var iterator = sequence.makeIterator()
|
||||
if iterator.next() != nil {
|
||||
let inner = Inner(downstream: subscriber, sequence: sequence)
|
||||
subscriber.receive(subscription: inner)
|
||||
} else {
|
||||
subscriber.receive(subscription: Subscriptions.empty)
|
||||
@@ -44,66 +46,89 @@ extension Publishers.Sequence {
|
||||
private final class Inner<Downstream: Subscriber, Elements: Sequence, Failure>
|
||||
: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Elements.Element,
|
||||
Downstream.Failure == Failure
|
||||
{
|
||||
// NOTE: This class has been audited for thread-safety
|
||||
|
||||
typealias Iterator = Elements.Iterator
|
||||
typealias Element = Elements.Element
|
||||
|
||||
private var _downstream: Downstream?
|
||||
private var _sequence: Elements?
|
||||
private var _iterator: Iterator?
|
||||
private var _nextValue: Element?
|
||||
private var sequence: Elements?
|
||||
private var downstream: Downstream?
|
||||
private var iterator: Iterator
|
||||
private var next: Element?
|
||||
private var pendingDemand = Subscribers.Demand.none
|
||||
private var recursion = false
|
||||
private var lock = unfairLock()
|
||||
|
||||
init?(downstream: Downstream, sequence: Elements) {
|
||||
|
||||
// Early exit if the sequence is empty
|
||||
var iterator = sequence.makeIterator()
|
||||
guard iterator.next() != nil else { return nil }
|
||||
|
||||
_downstream = downstream
|
||||
_sequence = sequence
|
||||
_iterator = sequence.makeIterator()
|
||||
_nextValue = iterator.next()
|
||||
fileprivate init(downstream: Downstream, sequence: Elements) {
|
||||
self.sequence = sequence
|
||||
self.downstream = downstream
|
||||
self.iterator = sequence.makeIterator()
|
||||
next = iterator.next()
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return _sequence.map(String.init(describing:)) ?? "Sequence"
|
||||
return sequence.map(String.init(describing:)) ?? "Sequence"
|
||||
}
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children: CollectionOfOne<(label: String?, value: Any)> =
|
||||
CollectionOfOne(("sequence", _sequence ?? [Element]()))
|
||||
let children =
|
||||
CollectionOfOne<Mirror.Child>(("sequence", sequence ?? [Element]()))
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard downstream != nil else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
pendingDemand += demand
|
||||
if recursion {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
|
||||
guard let downstream = _downstream else { return }
|
||||
while let downstream = self.downstream, pendingDemand > 0 {
|
||||
if let current = self.next {
|
||||
pendingDemand -= 1
|
||||
|
||||
var demand = demand
|
||||
|
||||
while demand > 0 {
|
||||
if let nextValue = _nextValue {
|
||||
demand += downstream.receive(nextValue)
|
||||
demand -= 1
|
||||
// Combine calls next() while the lock is held.
|
||||
// It is possible to engineer a custom Sequence that would cause
|
||||
// a dedlock here, but it would be something insane.
|
||||
let next = iterator.next()
|
||||
recursion = true
|
||||
lock.unlock()
|
||||
let additionalDemand = downstream.receive(current)
|
||||
lock.lock()
|
||||
recursion = false
|
||||
pendingDemand += additionalDemand
|
||||
self.next = next
|
||||
}
|
||||
|
||||
_nextValue = _iterator?.next()
|
||||
|
||||
if _nextValue == nil {
|
||||
_downstream?.receive(completion: .finished)
|
||||
cancel()
|
||||
break
|
||||
if next == nil {
|
||||
self.downstream = nil
|
||||
self.sequence = nil
|
||||
lock.unlock()
|
||||
downstream.receive(completion: .finished)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
_downstream = nil
|
||||
_iterator = nil
|
||||
_sequence = nil
|
||||
lock.lock()
|
||||
downstream = nil
|
||||
sequence = nil
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,8 +31,7 @@ extension Publishers {
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == Failure, Downstream.Input == Output
|
||||
{
|
||||
let inner = Inner(downstream: subscriber)
|
||||
upstream.subscribe(inner)
|
||||
upstream.subscribe(Inner(downstream: subscriber))
|
||||
}
|
||||
|
||||
public func setFailureType<NewFailure: Error>(
|
||||
@@ -63,12 +62,20 @@ extension Publisher where Failure == Never {
|
||||
}
|
||||
|
||||
extension Publishers.SetFailureType {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: OperatorSubscription<Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible
|
||||
where Upstream.Output == Downstream.Input
|
||||
private struct Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Upstream.Output == Downstream.Input, Failure == Downstream.Failure
|
||||
{
|
||||
private let downstream: Downstream
|
||||
let combineIdentifier = CombineIdentifier()
|
||||
|
||||
fileprivate init(downstream: Downstream) {
|
||||
self.downstream = downstream
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
downstream.receive(subscription: subscription)
|
||||
}
|
||||
@@ -82,5 +89,11 @@ extension Publishers.SetFailureType {
|
||||
}
|
||||
|
||||
var description: String { return "SetFailureType" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// Publishers.Share
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 18/09/2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Returns a publisher as a class instance.
|
||||
///
|
||||
/// The downstream subscriber receieves elements and completion states unchanged from
|
||||
/// the upstream publisher. Use this operator when you want to use
|
||||
/// reference semantics, such as storing a publisher instance in a property.
|
||||
///
|
||||
/// - Returns: A class instance that republishes its upstream publisher.
|
||||
public func share() -> Publishers.Share<Self> {
|
||||
return .init(upstream: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher implemented as a class, which otherwise behaves like its upstream
|
||||
/// publisher.
|
||||
public final class Share<Upstream: Publisher>: Publisher, Equatable {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
private typealias MulticastSubject = PassthroughSubject<Output, Failure>
|
||||
|
||||
private let inner: Autoconnect<Multicast<Upstream, MulticastSubject>>
|
||||
|
||||
public let upstream: Upstream
|
||||
|
||||
public init(upstream: Upstream) {
|
||||
self.inner = upstream.multicast(subject: .init()).autoconnect()
|
||||
self.upstream = upstream
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure
|
||||
{
|
||||
inner.subscribe(subscriber)
|
||||
}
|
||||
|
||||
public static func == (lhs: Share, rhs: Share) -> Bool {
|
||||
return lhs === rhs
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,8 +77,8 @@ extension Result {
|
||||
self.init(.failure(failure))
|
||||
}
|
||||
|
||||
public func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where SubscriberType.Input == Success, SubscriberType.Failure == Failure
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Success, Downstream.Failure == Failure
|
||||
{
|
||||
switch result {
|
||||
case .success(let value):
|
||||
@@ -115,34 +115,44 @@ extension Result {
|
||||
#endif
|
||||
}
|
||||
|
||||
private final class Inner<SubscriberType: Subscriber>: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable
|
||||
{
|
||||
private let _output: SubscriberType.Input
|
||||
private var _downstream: SubscriberType?
|
||||
extension Result.OCombine {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Success, Downstream.Failure == Failure
|
||||
{
|
||||
// NOTE: this class has been audited for thread safety.
|
||||
// Combine doesn't use any locking here.
|
||||
|
||||
init(value: SubscriberType.Input, downstream: SubscriberType) {
|
||||
_output = value
|
||||
_downstream = downstream
|
||||
}
|
||||
private var downstream: Downstream?
|
||||
private let output: Success
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
if let downstream = _downstream, demand > 0 {
|
||||
_ = downstream.receive(_output)
|
||||
downstream.receive(completion: .finished)
|
||||
_downstream = nil
|
||||
init(value: Success, downstream: Downstream) {
|
||||
self.output = value
|
||||
self.downstream = downstream
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
_downstream = nil
|
||||
}
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
guard let downstream = self.downstream else { return }
|
||||
self.downstream = nil
|
||||
_ = downstream.receive(output)
|
||||
downstream.receive(completion: .finished)
|
||||
}
|
||||
|
||||
var description: String { return "Once" }
|
||||
func cancel() {
|
||||
downstream = nil
|
||||
}
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, unlabeledChildren: CollectionOfOne(_output))
|
||||
var description: String { return "Once" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, unlabeledChildren: CollectionOfOne(output))
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,21 +13,24 @@ extension Subscribers {
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
// NOTE: this class has been audited for thread safety.
|
||||
// Combine doesn't use any locking here.
|
||||
|
||||
public typealias Failure = Never
|
||||
|
||||
public private(set) var object: Root?
|
||||
|
||||
public let keyPath: ReferenceWritableKeyPath<Root, Input>
|
||||
|
||||
private var _upstreamSubscription: Subscription?
|
||||
private var status = SubscriptionStatus.awaitingSubscription
|
||||
|
||||
public var description: String { return "Assign \(Root.self)." }
|
||||
|
||||
public var customMirror: Mirror {
|
||||
let children: [(label: String?, value: Any)] = [
|
||||
(label: "object", value: object as Any),
|
||||
(label: "keyPath", value: keyPath),
|
||||
(label: "status", value: _upstreamSubscription as Any)
|
||||
let children: [Mirror.Child] = [
|
||||
("object", object as Any),
|
||||
("keyPath", keyPath),
|
||||
("status", status as Any)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
@@ -40,17 +43,21 @@ extension Subscribers {
|
||||
}
|
||||
|
||||
public func receive(subscription: Subscription) {
|
||||
if _upstreamSubscription == nil {
|
||||
_upstreamSubscription = subscription
|
||||
subscription.request(.unlimited)
|
||||
} else {
|
||||
switch status {
|
||||
case .subscribed, .terminal:
|
||||
subscription.cancel()
|
||||
case .awaitingSubscription:
|
||||
status = .subscribed(subscription)
|
||||
subscription.request(.unlimited)
|
||||
}
|
||||
}
|
||||
|
||||
public func receive(_ value: Input) -> Subscribers.Demand {
|
||||
if _upstreamSubscription != nil {
|
||||
switch status {
|
||||
case .subscribed:
|
||||
object?[keyPath: keyPath] = value
|
||||
case .awaitingSubscription, .terminal:
|
||||
break
|
||||
}
|
||||
return .none
|
||||
}
|
||||
@@ -60,8 +67,11 @@ extension Subscribers {
|
||||
}
|
||||
|
||||
public func cancel() {
|
||||
_upstreamSubscription?.cancel()
|
||||
_upstreamSubscription = nil
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
return
|
||||
}
|
||||
subscription.cancel()
|
||||
status = .terminal
|
||||
object = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,13 +15,16 @@ extension Subscribers {
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
// NOTE: this class has been audited for thread safety.
|
||||
// Combine doesn't use any locking here.
|
||||
|
||||
/// The closure to execute on receipt of a value.
|
||||
public let receiveValue: (Input) -> Void
|
||||
|
||||
/// The closure to execute on completion.
|
||||
public let receiveCompletion: (Subscribers.Completion<Failure>) -> Void
|
||||
|
||||
private var _upstreamSubscription: Subscription?
|
||||
private var status = SubscriptionStatus.awaitingSubscription
|
||||
|
||||
public var description: String { return "Sink" }
|
||||
|
||||
@@ -45,11 +48,12 @@ extension Subscribers {
|
||||
}
|
||||
|
||||
public func receive(subscription: Subscription) {
|
||||
if _upstreamSubscription == nil {
|
||||
_upstreamSubscription = subscription
|
||||
subscription.request(.unlimited)
|
||||
} else {
|
||||
switch status {
|
||||
case .subscribed, .terminal:
|
||||
subscription.cancel()
|
||||
case .awaitingSubscription:
|
||||
status = .subscribed(subscription)
|
||||
subscription.request(.unlimited)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,11 +64,15 @@ extension Subscribers {
|
||||
|
||||
public func receive(completion: Subscribers.Completion<Failure>) {
|
||||
receiveCompletion(completion)
|
||||
status = .terminal
|
||||
}
|
||||
|
||||
public func cancel() {
|
||||
_upstreamSubscription?.cancel()
|
||||
_upstreamSubscription = nil
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
return
|
||||
}
|
||||
subscription.cancel()
|
||||
status = .terminal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
//
|
||||
// DispatchQueue.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 21.08.2019.
|
||||
//
|
||||
|
||||
import Dispatch
|
||||
import OpenCombine
|
||||
|
||||
extension DispatchQueue {
|
||||
|
||||
/// A namespace for disambiguation when both OpenCombine and Combine are imported.
|
||||
///
|
||||
/// Combine extends `DispatchQueue` with new methods and nested types.
|
||||
/// If you import both OpenCombine and Combine (either explicitly or implicitly,
|
||||
/// e. g. when importing Foundation), you will not be able
|
||||
/// to write `DispatchQueue.SchedulerTimeType`,
|
||||
/// because Swift is unable to understand which `SchedulerTimeType`
|
||||
/// you're referring to.
|
||||
///
|
||||
/// So you have to write `DispatchQueue.OCombine.SchedulerTimeType`.
|
||||
///
|
||||
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
|
||||
///
|
||||
/// You can omit this whenever Combine is not available (e. g. on Linux).
|
||||
public struct OCombine: Scheduler {
|
||||
|
||||
public let queue: DispatchQueue
|
||||
|
||||
public init(_ queue: DispatchQueue) {
|
||||
self.queue = queue
|
||||
}
|
||||
|
||||
/// The scheduler time type used by the dispatch queue.
|
||||
public struct SchedulerTimeType: Strideable, Codable, Hashable {
|
||||
|
||||
/// The dispatch time represented by this type.
|
||||
public var dispatchTime: DispatchTime
|
||||
|
||||
/// Creates a dispatch queue time type instance.
|
||||
///
|
||||
/// - Parameter time: The dispatch time to represent.
|
||||
public init(_ time: DispatchTime) {
|
||||
dispatchTime = time
|
||||
}
|
||||
|
||||
/// Returns the distance to another dispatch queue time.
|
||||
///
|
||||
/// - Parameter other: Another dispatch queue time.
|
||||
/// - Returns: The time interval between this time and the provided time.
|
||||
public func distance(to other: SchedulerTimeType) -> Stride {
|
||||
return .nanoseconds(
|
||||
Int(other.dispatchTime.rawValue - dispatchTime.rawValue)
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns a dispatch queue scheduler time calculated by advancing
|
||||
/// this instance’s time by the given interval.
|
||||
///
|
||||
/// - Parameter n: A time interval to advance.
|
||||
/// - Returns: A dispatch queue time advanced by the given
|
||||
/// interval from this instance’s time.
|
||||
public func advanced(by stride: Stride) -> SchedulerTimeType {
|
||||
return .init(dispatchTime + stride.timeInterval)
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(dispatchTime.rawValue)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
try container.encode(dispatchTime.uptimeNanoseconds)
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
dispatchTime = try .init(uptimeNanoseconds: container.decode(UInt64.self))
|
||||
}
|
||||
|
||||
/// A type that represents the distance between two values.
|
||||
public struct Stride: SchedulerTimeIntervalConvertible,
|
||||
Comparable,
|
||||
SignedNumeric,
|
||||
ExpressibleByFloatLiteral,
|
||||
Hashable,
|
||||
Codable {
|
||||
|
||||
/// If created via floating point literal, the value is
|
||||
/// converted to nanoseconds via multiplication.
|
||||
public typealias FloatLiteralType = Double
|
||||
|
||||
/// Nanoseconds, same as DispatchTimeInterval.
|
||||
public typealias IntegerLiteralType = Int
|
||||
|
||||
/// A type that can represent the absolute value of any possible
|
||||
/// value of the conforming type.
|
||||
public typealias Magnitude = Int
|
||||
|
||||
/// The value of this time interval in nanoseconds.
|
||||
public var magnitude: Int
|
||||
|
||||
/// A `DispatchTimeInterval` created with the value of this type
|
||||
/// in nanoseconds.
|
||||
public var timeInterval: DispatchTimeInterval {
|
||||
return .nanoseconds(magnitude)
|
||||
}
|
||||
|
||||
private init(magnitude: Int) {
|
||||
self.magnitude = magnitude
|
||||
}
|
||||
|
||||
/// Creates a dispatch queue time interval from the given
|
||||
/// dispatch time interval.
|
||||
///
|
||||
/// - Parameter timeInterval: A dispatch time interval.
|
||||
public init(_ timeInterval: DispatchTimeInterval) {
|
||||
switch timeInterval {
|
||||
case .seconds(let seconds):
|
||||
self = .seconds(seconds)
|
||||
case .milliseconds(let milliseconds):
|
||||
self = .milliseconds(milliseconds)
|
||||
case .microseconds(let microseconds):
|
||||
self = .microseconds(microseconds)
|
||||
case .nanoseconds(let nanoseconds):
|
||||
self = .nanoseconds(nanoseconds)
|
||||
case .never:
|
||||
fallthrough
|
||||
@unknown default:
|
||||
self = .nanoseconds(.max)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a dispatch queue time interval from a floating-point
|
||||
/// seconds value.
|
||||
///
|
||||
/// - Parameter value: The number of seconds, as a `Double`.
|
||||
public init(floatLiteral value: Double) {
|
||||
self = .seconds(value)
|
||||
}
|
||||
|
||||
/// Creates a dispatch queue time interval from an integer seconds value.
|
||||
///
|
||||
/// - Parameter value: The number of seconds, as an `Int`.
|
||||
public init(integerLiteral value: Int) {
|
||||
self = .seconds(value)
|
||||
}
|
||||
|
||||
/// Creates a dispatch queue time interval from a binary integer type.
|
||||
///
|
||||
/// If `exactly` cannot convert to an `Int`, the resulting time interval
|
||||
/// is `nil`.
|
||||
///
|
||||
/// - Parameter exactly: A binary integer representing a time interval.
|
||||
public init?<Source: BinaryInteger>(exactly source: Source) {
|
||||
guard let value = Int(exactly: source) else { return nil }
|
||||
self = .nanoseconds(value)
|
||||
}
|
||||
|
||||
public static func < (lhs: Stride, rhs: Stride) -> Bool {
|
||||
return lhs.magnitude < rhs.magnitude
|
||||
}
|
||||
|
||||
public static func * (lhs: Stride, rhs: Stride) -> Stride {
|
||||
// A bug in Combine, should be nanoseconds (FB7189676)
|
||||
return .seconds(lhs.magnitude * rhs.magnitude)
|
||||
}
|
||||
|
||||
public static func + (lhs: Stride, rhs: Stride) -> Stride {
|
||||
// A bug in Combine, should be nanoseconds (FB7189676)
|
||||
return .seconds(lhs.magnitude + rhs.magnitude)
|
||||
}
|
||||
|
||||
public static func - (lhs: Stride, rhs: Stride) -> Stride {
|
||||
// A bug in Combine, should be nanoseconds (FB7189676)
|
||||
return .seconds(lhs.magnitude - rhs.magnitude)
|
||||
}
|
||||
|
||||
// swiftlint:disable shorthand_operator
|
||||
|
||||
public static func -= (lhs: inout Stride, rhs: Stride) {
|
||||
lhs = lhs - rhs
|
||||
}
|
||||
|
||||
public static func *= (lhs: inout Stride, rhs: Stride) {
|
||||
lhs = lhs * rhs
|
||||
}
|
||||
|
||||
public static func += (lhs: inout Stride, rhs: Stride) {
|
||||
lhs = lhs + rhs
|
||||
}
|
||||
|
||||
// swiftlint:enable shorthand_operator
|
||||
|
||||
public static func seconds(_ value: Double) -> Stride {
|
||||
return Stride(magnitude: Int(value * 1_000_000_000))
|
||||
}
|
||||
|
||||
public static func seconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: value * 1_000_000_000)
|
||||
}
|
||||
|
||||
public static func milliseconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: value * 1_000_000)
|
||||
}
|
||||
|
||||
public static func microseconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: value * 1_000)
|
||||
}
|
||||
|
||||
public static func nanoseconds(_ value: Int) -> Stride {
|
||||
return Stride(magnitude: value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Options that affect the operation of the dispatch queue scheduler.
|
||||
public struct SchedulerOptions {
|
||||
|
||||
/// The dispatch queue quality of service.
|
||||
public var qos: DispatchQoS
|
||||
|
||||
/// The dispatch queue work item flags.
|
||||
public var flags: DispatchWorkItemFlags
|
||||
|
||||
/// The dispatch group, if any, that should be used for performing actions.
|
||||
public var group: DispatchGroup?
|
||||
|
||||
public init(qos: DispatchQoS = .unspecified,
|
||||
flags: DispatchWorkItemFlags = [],
|
||||
group: DispatchGroup? = nil) {
|
||||
self.qos = qos
|
||||
self.flags = flags
|
||||
self.group = group
|
||||
}
|
||||
}
|
||||
|
||||
public var minimumTolerance: SchedulerTimeType.Stride {
|
||||
return .nanoseconds(0)
|
||||
}
|
||||
|
||||
public var now: SchedulerTimeType {
|
||||
return .init(.now())
|
||||
}
|
||||
|
||||
public func schedule(options: SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) {
|
||||
let options = options ?? .init()
|
||||
queue.async(group: options.group,
|
||||
qos: options.qos,
|
||||
flags: options.flags,
|
||||
execute: action)
|
||||
}
|
||||
|
||||
public func schedule(after date: SchedulerTimeType,
|
||||
tolerance: SchedulerTimeType.Stride,
|
||||
options: SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) {
|
||||
let options = options ?? .init()
|
||||
queue.asyncAfter(deadline: date.dispatchTime,
|
||||
qos: options.qos,
|
||||
flags: options.flags,
|
||||
execute: action)
|
||||
}
|
||||
|
||||
/// Performs the action at some time after the specified date, at the specified
|
||||
/// frequency, optionally taking into account tolerance if possible.
|
||||
public func schedule(after date: SchedulerTimeType,
|
||||
interval: SchedulerTimeType.Stride,
|
||||
tolerance: SchedulerTimeType.Stride,
|
||||
options: SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) -> Cancellable {
|
||||
let options = options ?? .init()
|
||||
let timer = DispatchSource.makeTimerSource(queue: queue)
|
||||
timer.setEventHandler(qos: options.qos,
|
||||
flags: options.flags,
|
||||
handler: action)
|
||||
timer.schedule(deadline: date.dispatchTime,
|
||||
repeating: interval.timeInterval,
|
||||
leeway: tolerance.timeInterval)
|
||||
timer.resume()
|
||||
return AnyCancellable(timer.cancel)
|
||||
}
|
||||
}
|
||||
|
||||
/// A namespace for disambiguation when both OpenCombine and Combine are imported.
|
||||
///
|
||||
/// Combine extends `DispatchQueue` with new methods and nested types.
|
||||
/// If you import both OpenCombine and Combine (either explicitly or implicitly,
|
||||
/// e. g. when importing Foundation), you will not be able
|
||||
/// to write `DispatchQueue.main.schedule { doThings() }`,
|
||||
/// because Swift is unable to understand which `schedule` method
|
||||
/// you're referring to.
|
||||
///
|
||||
/// So you have to write `DispatchQueue.main.ocombine.schedule { doThings() }`.
|
||||
///
|
||||
/// This bug is tracked [here](https://bugs.swift.org/browse/SR-11183).
|
||||
///
|
||||
/// You can omit this whenever Combine is not available (e. g. on Linux).
|
||||
public var ocombine: OCombine {
|
||||
return OCombine(self)
|
||||
}
|
||||
}
|
||||
|
||||
#if !canImport(Combine)
|
||||
extension DispatchQueue: OpenCombine.Scheduler {
|
||||
|
||||
public typealias SchedulerOptions = OCombine.SchedulerOptions
|
||||
|
||||
public typealias SchedulerTimeType = OCombine.SchedulerTimeType
|
||||
|
||||
public var minimumTolerance: OCombine.SchedulerTimeType.Stride {
|
||||
return ocombine.minimumTolerance
|
||||
}
|
||||
|
||||
public var now: OCombine.SchedulerTimeType {
|
||||
return ocombine.now
|
||||
}
|
||||
|
||||
public func schedule(options: OCombine.SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) {
|
||||
ocombine.schedule(options: options, action)
|
||||
}
|
||||
|
||||
public func schedule(after date: OCombine.SchedulerTimeType,
|
||||
tolerance: OCombine.SchedulerTimeType.Stride,
|
||||
options: OCombine.SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) {
|
||||
ocombine.schedule(after: date, tolerance: tolerance, options: options, action)
|
||||
}
|
||||
|
||||
public func schedule(after date: OCombine.SchedulerTimeType,
|
||||
interval: OCombine.SchedulerTimeType.Stride,
|
||||
tolerance: OCombine.SchedulerTimeType.Stride,
|
||||
options: OCombine.SchedulerOptions?,
|
||||
_ action: @escaping () -> Void) -> Cancellable {
|
||||
return ocombine.schedule(after: date,
|
||||
interval: interval,
|
||||
tolerance: tolerance,
|
||||
options: options,
|
||||
action)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -26,7 +26,7 @@ final class AnyPublisherTests: XCTestCase {
|
||||
XCTAssertEqual($0.combineIdentifier, subscriber.combineIdentifier)
|
||||
}
|
||||
)
|
||||
let erased = AnyPublisher(publisher)
|
||||
let erased = publisher.eraseToAnyPublisher()
|
||||
|
||||
erased.subscribe(subscriber)
|
||||
XCTAssertEqual(publisher.history, [.subscriber])
|
||||
|
||||
@@ -44,9 +44,9 @@ final class CombineIdentifierTests: PerformanceTestCase {
|
||||
}
|
||||
|
||||
func testDefaultInitializedPerformance() throws {
|
||||
try benchmark(allowFailure: isDebug, executionCount: 100) {
|
||||
try benchmark(allowFailure: isDebug, executionCount: 500) {
|
||||
for _ in 0..<2000 {
|
||||
_ = CombineIdentifier()
|
||||
blackHole(CombineIdentifier())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,461 @@
|
||||
//
|
||||
// DispatchQueueSchedulerTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 26.08.2019.
|
||||
//
|
||||
|
||||
import Dispatch
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
import OpenCombineDispatch
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
|
||||
// MARK: - Scheduler.SchedulerTimeType
|
||||
|
||||
func testSchedulerTimeTypeDistance() {
|
||||
let time1 = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10000))
|
||||
let time2 = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10431))
|
||||
|
||||
XCTAssertEqual(time1.distance(to: time2), .nanoseconds(431))
|
||||
|
||||
// A bug in Combine (FB7127210), caused by overflow on subtraction.
|
||||
// It should not crash. When they fix it, this test will fail and we'll know
|
||||
// that we need to update our implementation.
|
||||
assertCrashes {
|
||||
_ = time2.distance(to: time1)
|
||||
}
|
||||
}
|
||||
|
||||
func testSchedulerTimeTypeAdvanced() {
|
||||
let time = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10000))
|
||||
let stride1 = Scheduler.SchedulerTimeType.Stride.nanoseconds(431)
|
||||
let stride2 = Scheduler.SchedulerTimeType.Stride.nanoseconds(-220)
|
||||
|
||||
XCTAssertEqual(time.advanced(by: stride1),
|
||||
Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10431)))
|
||||
|
||||
XCTAssertEqual(time.advanced(by: stride2),
|
||||
Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 9780)))
|
||||
}
|
||||
|
||||
func testSchedulerTimeTypeEquatable() {
|
||||
let time1 = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10000))
|
||||
let time2 = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10000))
|
||||
let time3 = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10001))
|
||||
|
||||
XCTAssertEqual(time1, time1)
|
||||
XCTAssertEqual(time2, time2)
|
||||
XCTAssertEqual(time3, time3)
|
||||
|
||||
XCTAssertEqual(time1, time2)
|
||||
XCTAssertEqual(time2, time1)
|
||||
XCTAssertNotEqual(time1, time3)
|
||||
|
||||
assertCrashes {
|
||||
XCTAssertNotEqual(time3, time1)
|
||||
}
|
||||
}
|
||||
|
||||
func testSchedulerTimeTypeHashable() {
|
||||
let time1 = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10000))
|
||||
let time2 = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 10001))
|
||||
|
||||
XCTAssertEqual(time1.hashValue, time1.dispatchTime.rawValue.hashValue)
|
||||
XCTAssertEqual(time2.hashValue, time2.dispatchTime.rawValue.hashValue)
|
||||
}
|
||||
|
||||
func testSchedulerTimeTypeCodable() throws {
|
||||
let encoder = JSONEncoder()
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
let time = Scheduler.SchedulerTimeType(.init(uptimeNanoseconds: 42))
|
||||
let encodedData = try encoder
|
||||
.encode(KeyedWrapper(value: time))
|
||||
let encodedString = String(decoding: encodedData, as: UTF8.self)
|
||||
|
||||
XCTAssertEqual(encodedString, #"{"value":42}"#)
|
||||
|
||||
let decodedTime = try decoder
|
||||
.decode(KeyedWrapper<Scheduler.SchedulerTimeType>.self, from: encodedData)
|
||||
.value
|
||||
|
||||
XCTAssertEqual(decodedTime, time)
|
||||
}
|
||||
|
||||
// MARK: - Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
func testStrideToDispatchTimeInterval() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
switch (Stride.seconds(12).timeInterval,
|
||||
Stride.milliseconds(34).timeInterval,
|
||||
Stride.microseconds(56).timeInterval,
|
||||
Stride.nanoseconds(78).timeInterval) {
|
||||
case (.nanoseconds(12000000000),
|
||||
.nanoseconds(34000000),
|
||||
.nanoseconds(56000),
|
||||
.nanoseconds(78)):
|
||||
break // pass
|
||||
case let intervals:
|
||||
XCTFail("Unexpected DispatchTimeInterval: \(intervals)")
|
||||
}
|
||||
}
|
||||
|
||||
func testStrideFromDispatchTimeInterval() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
XCTAssertEqual(Stride(.seconds(12)).magnitude, 12000000000)
|
||||
XCTAssertEqual(Stride(.milliseconds(34)).magnitude, 34000000)
|
||||
XCTAssertEqual(Stride(.microseconds(56)).magnitude, 56000)
|
||||
XCTAssertEqual(Stride(.nanoseconds(78)).magnitude, 78)
|
||||
XCTAssertEqual(Stride(.never).magnitude, .max)
|
||||
}
|
||||
|
||||
func testStrideFromNumericValue() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
XCTAssertEqual(Stride.seconds(12.756).magnitude, 12756000000)
|
||||
XCTAssertEqual(Stride.seconds(34).magnitude, 34000000000)
|
||||
XCTAssertEqual(Stride.milliseconds(56).magnitude, 56000000)
|
||||
XCTAssertEqual(Stride.microseconds(78).magnitude, 78000)
|
||||
XCTAssertEqual(Stride.nanoseconds(90).magnitude, 90)
|
||||
|
||||
XCTAssertEqual((12.756 as Stride).magnitude, 12756000000)
|
||||
XCTAssertEqual((34 as Stride).magnitude, 34000000000)
|
||||
|
||||
XCTAssertNil(Stride(exactly: UInt64.max))
|
||||
XCTAssertEqual(Stride(exactly: 871 as UInt64)?.magnitude, 871)
|
||||
}
|
||||
|
||||
func testStrideComparable() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
XCTAssertLessThan(Stride.nanoseconds(1), .nanoseconds(2))
|
||||
XCTAssertGreaterThan(Stride.nanoseconds(-2), .microseconds(-10))
|
||||
XCTAssertLessThan(Stride.milliseconds(29), .seconds(29))
|
||||
}
|
||||
|
||||
func testStrideMultiplication() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
XCTAssertEqual((Stride.nanoseconds(0) * .nanoseconds(61346)).magnitude, 0)
|
||||
XCTAssertEqual((Stride.nanoseconds(61346) * .nanoseconds(0)).magnitude, 0)
|
||||
XCTAssertEqual((Stride.nanoseconds(18) * .nanoseconds(1)).magnitude,
|
||||
18000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(18) * .microseconds(1)).magnitude,
|
||||
18000000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(1) * .nanoseconds(18)).magnitude,
|
||||
18000000000)
|
||||
XCTAssertEqual((Stride.microseconds(1) * .nanoseconds(18)).magnitude,
|
||||
18000000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(15) * .nanoseconds(2)).magnitude,
|
||||
30000000000)
|
||||
XCTAssertEqual((Stride.microseconds(-3) * .nanoseconds(10)).magnitude,
|
||||
-30000000000000)
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(0)
|
||||
stride *= .nanoseconds(61346)
|
||||
XCTAssertEqual(stride.magnitude, 0)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(61346)
|
||||
stride *= .nanoseconds(0)
|
||||
XCTAssertEqual(stride.magnitude, 0)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(18)
|
||||
stride *= .nanoseconds(1)
|
||||
XCTAssertEqual(stride.magnitude, 18000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(18)
|
||||
stride *= .microseconds(1)
|
||||
XCTAssertEqual(stride.magnitude, 18000000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(1)
|
||||
stride *= .nanoseconds(18)
|
||||
XCTAssertEqual(stride.magnitude, 18000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.microseconds(1)
|
||||
stride *= .nanoseconds(18)
|
||||
XCTAssertEqual(stride.magnitude, 18000000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(15)
|
||||
stride *= .nanoseconds(2)
|
||||
XCTAssertEqual(stride.magnitude, 30000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.microseconds(-3)
|
||||
stride *= .nanoseconds(10)
|
||||
XCTAssertEqual(stride.magnitude, -30000000000000)
|
||||
}
|
||||
}
|
||||
|
||||
func testStrideAddition() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
XCTAssertEqual((Stride.nanoseconds(0) + .microseconds(2)).magnitude,
|
||||
2000000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(2) + .microseconds(0)).magnitude,
|
||||
2000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(7) + .nanoseconds(12)).magnitude,
|
||||
19000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(12) + .nanoseconds(7)).magnitude,
|
||||
19000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(7) + .nanoseconds(-12)).magnitude,
|
||||
-5000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(-12) + .nanoseconds(7)).magnitude,
|
||||
-5000000000)
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(0)
|
||||
stride += .microseconds(2)
|
||||
XCTAssertEqual(stride.magnitude, 2000000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(2)
|
||||
stride += .microseconds(0)
|
||||
XCTAssertEqual(stride.magnitude, 2000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(7)
|
||||
stride += .nanoseconds(12)
|
||||
XCTAssertEqual(stride.magnitude, 19000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(12)
|
||||
stride += .nanoseconds(7)
|
||||
XCTAssertEqual(stride.magnitude, 19000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(7)
|
||||
stride += .nanoseconds(-12)
|
||||
XCTAssertEqual(stride.magnitude, -5000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(-12)
|
||||
stride += .nanoseconds(7)
|
||||
XCTAssertEqual(stride.magnitude, -5000000000)
|
||||
}
|
||||
}
|
||||
|
||||
func testStrideSubtraction() {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
XCTAssertEqual((Stride.nanoseconds(0) - .microseconds(2)).magnitude,
|
||||
-2000000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(2) - .microseconds(0)).magnitude,
|
||||
2000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(7) - .nanoseconds(12)).magnitude,
|
||||
-5000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(12) - .nanoseconds(7)).magnitude,
|
||||
5000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(7) - .nanoseconds(-12)).magnitude,
|
||||
19000000000)
|
||||
XCTAssertEqual((Stride.nanoseconds(-12) - .nanoseconds(7)).magnitude,
|
||||
-19000000000)
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(0)
|
||||
stride -= .microseconds(2)
|
||||
XCTAssertEqual(stride.magnitude, -2000000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(2)
|
||||
stride -= .microseconds(0)
|
||||
XCTAssertEqual(stride.magnitude, 2000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(7)
|
||||
stride -= .nanoseconds(12)
|
||||
XCTAssertEqual(stride.magnitude, -5000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(12)
|
||||
stride -= .nanoseconds(7)
|
||||
XCTAssertEqual(stride.magnitude, 5000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(7)
|
||||
stride -= .nanoseconds(-12)
|
||||
XCTAssertEqual(stride.magnitude, 19000000000)
|
||||
}
|
||||
|
||||
do {
|
||||
var stride = Stride.nanoseconds(-12)
|
||||
stride -= .nanoseconds(7)
|
||||
XCTAssertEqual(stride.magnitude, -19000000000)
|
||||
}
|
||||
}
|
||||
|
||||
func testStrideCodable() throws {
|
||||
typealias Stride = Scheduler.SchedulerTimeType.Stride
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
let stride = Stride.nanoseconds(419872)
|
||||
let encodedData = try encoder
|
||||
.encode(KeyedWrapper(value: stride))
|
||||
let encodedString = String(decoding: encodedData, as: UTF8.self)
|
||||
|
||||
XCTAssertEqual(encodedString, #"{"value":{"magnitude":419872}}"#)
|
||||
|
||||
let decodedStride = try decoder
|
||||
.decode(KeyedWrapper<Stride>.self, from: encodedData)
|
||||
.value
|
||||
|
||||
XCTAssertEqual(decodedStride, stride)
|
||||
}
|
||||
|
||||
// MARK: - Scheduler
|
||||
|
||||
func testMinimumTolerance() {
|
||||
XCTAssertEqual(mainScheduler.minimumTolerance, .nanoseconds(0))
|
||||
XCTAssertEqual(backgroundScheduler.minimumTolerance, .nanoseconds(0))
|
||||
}
|
||||
|
||||
func testNow() {
|
||||
let expectedNow = DispatchTime.now().uptimeNanoseconds
|
||||
let actualNowMainScheduler = mainScheduler
|
||||
.now
|
||||
.dispatchTime
|
||||
.uptimeNanoseconds
|
||||
let actualNowBackgroundScheduler = backgroundScheduler
|
||||
.now
|
||||
.dispatchTime
|
||||
.uptimeNanoseconds
|
||||
XCTAssertLessThan(abs(actualNowMainScheduler.distance(to: expectedNow)),
|
||||
1_000_000/*nanoseconds*/)
|
||||
XCTAssertLessThan(abs(actualNowBackgroundScheduler.distance(to: expectedNow)),
|
||||
1_000_000/*nanoseconds*/)
|
||||
}
|
||||
|
||||
func testDefaultSchedulerOptions() {
|
||||
let options = Scheduler.SchedulerOptions()
|
||||
XCTAssertEqual(options.flags, [])
|
||||
XCTAssertEqual(options.qos, .unspecified)
|
||||
XCTAssertNil(options.group)
|
||||
}
|
||||
|
||||
func testScheduleActionOnceNow() {
|
||||
let main = expectation(description: "scheduled on main queue")
|
||||
main.assertForOverFulfill = true
|
||||
|
||||
var didExecuteMainAction = false
|
||||
let didExecuteBackgroundAction = Atomic(false)
|
||||
|
||||
mainScheduler.schedule {
|
||||
didExecuteMainAction = true
|
||||
main.fulfill()
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
backgroundScheduler
|
||||
.schedule(options: .init(qos: .userInteractive, group: group)) {
|
||||
didExecuteBackgroundAction.do { $0 = true }
|
||||
}
|
||||
|
||||
XCTAssertFalse(didExecuteMainAction, "action should be executed asynchronously")
|
||||
|
||||
// Wait for the background scheduler to execute the work.
|
||||
XCTAssertEqual(group.wait(timeout: .now() + 0.1), .success)
|
||||
|
||||
XCTAssertFalse(didExecuteMainAction, "action should be executed asynchronously")
|
||||
XCTAssertTrue(didExecuteBackgroundAction.value)
|
||||
|
||||
wait(for: [main], timeout: 0.1)
|
||||
}
|
||||
|
||||
func testScheduleActionOnceLater() {
|
||||
|
||||
let main = expectation(description: "scheduled on main queue")
|
||||
main.assertForOverFulfill = true
|
||||
|
||||
var didExecuteAction = false
|
||||
|
||||
let delay = Scheduler.SchedulerTimeType.Stride.milliseconds(200)
|
||||
|
||||
mainScheduler.schedule(after: mainScheduler.now.advanced(by: delay)) {
|
||||
didExecuteAction = true
|
||||
main.fulfill()
|
||||
}
|
||||
|
||||
XCTAssertFalse(didExecuteAction, "action should be executed asynchronously")
|
||||
|
||||
wait(for: [main], timeout: 3/*seconds*/)
|
||||
}
|
||||
|
||||
func testScheduleRepeating() {
|
||||
let main = expectation(description: "scheduled on main queue")
|
||||
main.expectedFulfillmentCount = 4
|
||||
main.assertForOverFulfill = true
|
||||
|
||||
let delay = Scheduler.SchedulerTimeType.Stride.milliseconds(100)
|
||||
let interval = Scheduler.SchedulerTimeType.Stride.milliseconds(50)
|
||||
|
||||
var didExecuteAction = false
|
||||
|
||||
let token = mainScheduler
|
||||
.schedule(after: mainScheduler.now.advanced(by: delay),
|
||||
interval: interval) {
|
||||
didExecuteAction = true
|
||||
main.fulfill()
|
||||
}
|
||||
|
||||
XCTAssert(token is AnyCancellable)
|
||||
XCTAssertFalse(didExecuteAction, "action should be executed asynchronously")
|
||||
|
||||
wait(for: [main], timeout: 3/*seconds*/)
|
||||
}
|
||||
}
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private typealias Scheduler = DispatchQueue
|
||||
|
||||
private let mainScheduler = DispatchQueue.main
|
||||
private let backgroundScheduler = DispatchQueue.global(qos: .background)
|
||||
|
||||
#else
|
||||
|
||||
private typealias Scheduler = DispatchQueue.OCombine
|
||||
|
||||
private let mainScheduler = DispatchQueue.main.ocombine
|
||||
private let backgroundScheduler = DispatchQueue.global(qos: .background).ocombine
|
||||
|
||||
#endif
|
||||
|
||||
private struct KeyedWrapper<Value: Codable & Equatable>: Codable, Equatable {
|
||||
let value: Value
|
||||
}
|
||||
@@ -34,13 +34,13 @@ import OpenCombine
|
||||
typealias CustomPublisher = CustomPublisherBase<Int, TestingError>
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class CustomPublisherBase<Output: Equatable, Failure: Error>: Publisher {
|
||||
class CustomPublisherBase<Output: Equatable, Failure: Error>: Publisher {
|
||||
|
||||
private(set) var subscriber: AnySubscriber<Output, Failure>?
|
||||
private(set) var erasedSubscriber: Any?
|
||||
private let subscription: Subscription?
|
||||
|
||||
init(subscription: Subscription?) {
|
||||
required init(subscription: Subscription?) {
|
||||
self.subscription = subscription
|
||||
}
|
||||
|
||||
@@ -60,3 +60,42 @@ final class CustomPublisherBase<Output: Equatable, Failure: Error>: Publisher {
|
||||
subscriber!.receive(completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
typealias CustomConnectablePublisher = CustomConnectablePublisherBase<Int, TestingError>
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class CustomConnectablePublisherBase<Output: Equatable, Failure: Error>
|
||||
: CustomPublisherBase<Output, Failure>,
|
||||
ConnectablePublisher
|
||||
{
|
||||
|
||||
enum Event: CustomStringConvertible {
|
||||
case connected, disconnected
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .connected:
|
||||
return ".connected"
|
||||
case .disconnected:
|
||||
return ".disconnected"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Connection: Cancellable {
|
||||
|
||||
let onCancel: () -> Void
|
||||
|
||||
func cancel() {
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var connectionHistory: [Event] = []
|
||||
|
||||
func connect() -> Cancellable {
|
||||
connectionHistory.append(.connected)
|
||||
return Connection { self.connectionHistory.append(.disconnected) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,9 @@ class PerformanceTestCase: GottaGoFast.PerformanceTestCase {
|
||||
block)
|
||||
#endif
|
||||
}
|
||||
|
||||
@inline(never)
|
||||
func blackHole<Value>(_: Value) {}
|
||||
}
|
||||
|
||||
extension XCTestCase {
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
//
|
||||
// TestReflection.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 21/09/2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
func childrenIsEmpty(_ mirror: Mirror) -> Bool {
|
||||
return mirror.children.isEmpty
|
||||
}
|
||||
|
||||
enum ExpectedMirrorChildValue: Equatable, ExpressibleByStringLiteral {
|
||||
case anything
|
||||
case matches(String)
|
||||
case contains(String)
|
||||
|
||||
typealias StringLiteralType = String
|
||||
|
||||
init(stringLiteral value: String) {
|
||||
self = .matches(value)
|
||||
}
|
||||
}
|
||||
|
||||
func expectedChildren(_ expectedChildren: (String?, ExpectedMirrorChildValue)...,
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line) -> (Mirror) -> Bool {
|
||||
return { mirror in
|
||||
|
||||
let actualChildren = mirror
|
||||
.children
|
||||
.map { ($0, String(describing: $1)) }
|
||||
|
||||
for (actualChild, expectedChild) in zip(actualChildren, expectedChildren) {
|
||||
XCTAssertEqual(actualChild.0, expectedChild.0, file: file, line: line)
|
||||
switch (actualChild.1, expectedChild.1) {
|
||||
case (_, .anything):
|
||||
continue
|
||||
case let (lhs, .matches(rhs)):
|
||||
XCTAssertEqual(lhs, rhs, file: file, line: line)
|
||||
case let (lhs, .contains(rhs)):
|
||||
XCTAssert(lhs.contains(rhs),
|
||||
"\"\(lhs)\" doesn't contain substring \"\(rhs)\"",
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
internal func testReflection<Output, Failure: Error, Operator: Publisher>(
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line,
|
||||
parentInput: Output.Type,
|
||||
parentFailure: Failure.Type,
|
||||
description expectedDescription: String,
|
||||
customMirror customMirrorPredicate: ((Mirror) -> Bool)?,
|
||||
playgroundDescription: String,
|
||||
_ makeOperator: (CustomConnectablePublisherBase<Output, Failure>) -> Operator
|
||||
) throws where Operator.Output: Equatable {
|
||||
let publisher = CustomConnectablePublisherBase<Output, Failure>(subscription: nil)
|
||||
let operatorPublisher = makeOperator(publisher)
|
||||
let tracking = TrackingSubscriberBase<Operator.Output, Operator.Failure>()
|
||||
operatorPublisher.subscribe(tracking)
|
||||
|
||||
let erasedSubscriber =
|
||||
try XCTUnwrap(publisher.erasedSubscriber, file: file, line: line)
|
||||
|
||||
XCTAssertEqual((erasedSubscriber as? CustomStringConvertible)?.description,
|
||||
expectedDescription,
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
let customMirror =
|
||||
try XCTUnwrap((erasedSubscriber as? CustomReflectable)?.customMirror,
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
if let customMirrorPredicate = customMirrorPredicate {
|
||||
XCTAssert(customMirrorPredicate(customMirror),
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
|
||||
XCTAssertEqual(
|
||||
((erasedSubscriber as? CustomPlaygroundDisplayConvertible)?
|
||||
.playgroundDescription as? String),
|
||||
playgroundDescription,
|
||||
file: file,
|
||||
line: line
|
||||
)
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
internal func testSubscriptionReflection<Sut: Publisher>(
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line,
|
||||
description expectedDescription: String,
|
||||
customMirror customMirrorPredicate: ((Mirror) -> Bool)?,
|
||||
playgroundDescription: String,
|
||||
sut: Sut
|
||||
) throws where Sut.Output: Equatable {
|
||||
let tracking = TrackingSubscriberBase<Sut.Output, Sut.Failure>()
|
||||
sut.subscribe(tracking)
|
||||
|
||||
let subscription = try XCTUnwrap(tracking.subscriptions.first?.underlying)
|
||||
|
||||
XCTAssertEqual((subscription as? CustomStringConvertible)?.description,
|
||||
expectedDescription,
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
if let customMirrorPredicate = customMirrorPredicate {
|
||||
let customMirror =
|
||||
try XCTUnwrap((subscription as? CustomReflectable)?.customMirror,
|
||||
"Subscription doesn't conform to CustomReflectable",
|
||||
file: file,
|
||||
line: line)
|
||||
XCTAssert(customMirrorPredicate(customMirror),
|
||||
file: file,
|
||||
line: line)
|
||||
} else {
|
||||
XCTAssertFalse(subscription is CustomReflectable,
|
||||
"Subscription shouldn't conform to CustomReflectable",
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
|
||||
XCTAssertEqual(
|
||||
((subscription as? CustomPlaygroundDisplayConvertible)?
|
||||
.playgroundDescription as? String),
|
||||
playgroundDescription,
|
||||
file: file,
|
||||
line: line
|
||||
)
|
||||
}
|
||||
@@ -263,8 +263,8 @@ final class TrackingSubjectBase<Output: Equatable, Failure: Error>
|
||||
_passthrough.send(completion: completion)
|
||||
}
|
||||
|
||||
func receive<SubscriberType: Subscriber>(subscriber: SubscriberType)
|
||||
where Failure == SubscriberType.Failure, Output == SubscriberType.Input
|
||||
func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
_receiveSubscriber?(subscriber)
|
||||
history.append(.subscriber)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import XCTest
|
||||
|
||||
// FIXME: Remove this shim as soon is XCTUnwrap is added to swift-corelibs-xctest
|
||||
// FIXME: XCTUnwrap is unavailable in Swift Package Manager yet.
|
||||
|
||||
private struct UnwrappingFailure: Error {}
|
||||
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
//
|
||||
// AutoconnectTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 25/09/2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class AutoconnectTests: XCTestCase {
|
||||
|
||||
func testBasicRefcountBehavior() throws {
|
||||
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomConnectablePublisher(subscription: subscription)
|
||||
|
||||
let autoconnect = publisher.autoconnect()
|
||||
|
||||
XCTAssertEqual(publisher.connectionHistory, [])
|
||||
|
||||
let subscriber1 = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.max(101)) },
|
||||
receiveValue: { _ in .max(201) }
|
||||
)
|
||||
let subscriber2 = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.max(102)) },
|
||||
receiveValue: { _ in .max(202) }
|
||||
)
|
||||
let subscriber3 = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.max(103)) },
|
||||
receiveValue: { _ in .max(203) }
|
||||
)
|
||||
|
||||
autoconnect.subscribe(subscriber1) // refcount = 1
|
||||
XCTAssertEqual(publisher.connectionHistory, [.connected])
|
||||
|
||||
autoconnect.subscribe(subscriber2) // refcount = 2
|
||||
XCTAssertEqual(publisher.connectionHistory, [.connected])
|
||||
|
||||
autoconnect.subscribe(subscriber3) // refcount = 3
|
||||
XCTAssertEqual(publisher.connectionHistory, [.connected])
|
||||
|
||||
// Autoconnect should just forward events downstream
|
||||
XCTAssertEqual(publisher.send(1), .max(203))
|
||||
XCTAssertEqual(publisher.send(2), .max(203))
|
||||
publisher.send(completion: .finished)
|
||||
XCTAssertEqual(publisher.send(3), .max(203))
|
||||
publisher.send(completion: .failure(.oops))
|
||||
publisher.send(completion: .failure(.oops))
|
||||
|
||||
let subscription1 = try XCTUnwrap(subscriber1.subscriptions.first?.underlying)
|
||||
let subscription2 = try XCTUnwrap(subscriber2.subscriptions.first?.underlying)
|
||||
let subscription3 = try XCTUnwrap(subscriber3.subscriptions.first?.underlying)
|
||||
|
||||
subscription2.cancel() // refcount = 2
|
||||
XCTAssertEqual(publisher.connectionHistory, [.connected])
|
||||
|
||||
subscription3.cancel() // refcount = 1
|
||||
XCTAssertEqual(publisher.connectionHistory, [.connected])
|
||||
|
||||
subscription1.cancel() // refcount = 0
|
||||
XCTAssertEqual(publisher.connectionHistory, [.connected, .disconnected])
|
||||
|
||||
// Cancelling the same subscription twice shouldn't matter
|
||||
subscription1.cancel()
|
||||
XCTAssertEqual(publisher.connectionHistory, [.connected, .disconnected])
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(101)),
|
||||
.requested(.max(102)),
|
||||
.requested(.max(103)),
|
||||
.cancelled,
|
||||
.cancelled,
|
||||
.cancelled,
|
||||
.cancelled])
|
||||
|
||||
XCTAssertEqual(subscriber3.history, [.subscription("CustomSubscription"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.completion(.finished),
|
||||
.value(3),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops))])
|
||||
}
|
||||
|
||||
func testReentranceWhenConnecting() throws {
|
||||
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomConnectablePublisher(subscription: subscription)
|
||||
|
||||
let autoconnect = publisher.autoconnect()
|
||||
|
||||
let subscriber1 = TrackingSubscriber()
|
||||
|
||||
let subscriber2 = TrackingSubscriber(
|
||||
receiveSubscription: { _ in autoconnect.subscribe(subscriber1) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(publisher.connectionHistory, [])
|
||||
|
||||
autoconnect.subscribe(subscriber2)
|
||||
XCTAssertEqual(publisher.connectionHistory, [.connected,
|
||||
.connected])
|
||||
|
||||
try XCTUnwrap(subscriber2.subscriptions.first?.underlying).cancel()
|
||||
XCTAssertEqual(publisher.connectionHistory, [.connected,
|
||||
.connected,
|
||||
.disconnected])
|
||||
|
||||
try XCTUnwrap(subscriber1.subscriptions.first?.underlying).cancel()
|
||||
XCTAssertEqual(publisher.connectionHistory, [.connected,
|
||||
.connected,
|
||||
.disconnected])
|
||||
}
|
||||
|
||||
func testAutoconnectReflection() throws {
|
||||
|
||||
let customMirrorPredicate = expectedChildren(
|
||||
("parent", .contains("""
|
||||
Publishers.Autoconnect<\
|
||||
OpenCombineTests.\
|
||||
CustomConnectablePublisherBase<Swift.Int, \
|
||||
OpenCombineTests.TestingError>
|
||||
""")),
|
||||
("downstream", "TrackingSubscriberBase<Int, TestingError>: []")
|
||||
)
|
||||
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: TestingError.self,
|
||||
description: "Autoconnect",
|
||||
customMirror: customMirrorPredicate,
|
||||
playgroundDescription: "Autoconnect",
|
||||
{ $0.autoconnect() })
|
||||
|
||||
let subscription = CustomSubscription()
|
||||
let autoconnect = CustomConnectablePublisher(subscription: subscription)
|
||||
.autoconnect()
|
||||
|
||||
try testSubscriptionReflection(
|
||||
description: "CustomSubscription",
|
||||
customMirror: nil,
|
||||
playgroundDescription: "CustomSubscription",
|
||||
sut: autoconnect
|
||||
)
|
||||
|
||||
var autoconnectSubscriptionCombineID: CombineIdentifier?
|
||||
autoconnect.subscribe(
|
||||
TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
autoconnectSubscriptionCombineID = $0.combineIdentifier
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
XCTAssertEqual(autoconnectSubscriptionCombineID, subscription.combineIdentifier)
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ import OpenCombine
|
||||
/// I understand that timeouts like this are a smell. I'd be happy to entertain other ways
|
||||
/// to deterministically test concurrency/race conditions.
|
||||
|
||||
func performConcurrentBlock(_ block: @escaping () -> Void) {
|
||||
private func performConcurrentBlock(_ block: @escaping () -> Void) {
|
||||
let sem = DispatchSemaphore(value: 0)
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
block()
|
||||
@@ -46,34 +46,6 @@ func performConcurrentBlock(_ block: @escaping () -> Void) {
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class FlatMapTests: XCTestCase {
|
||||
|
||||
static let allTests = [
|
||||
("testSendsChildValues", testSendsChildValues),
|
||||
("testChildSubscribeDeadlock", testChildSubscribeDeadlock),
|
||||
("testCancelCancels", testCancelCancels),
|
||||
("testUpstreamDemandWithMaxPublishers", testUpstreamDemandWithMaxPublishers),
|
||||
("testUpstreamDemandWithNoMaxPublishers", testUpstreamDemandWithNoMaxPublishers),
|
||||
("testChildDemandWhenUnlimited", testChildDemandWhenUnlimited),
|
||||
("testChildDemandWhenLimited", testChildDemandWhenLimited),
|
||||
("testDemandFromLimitedtoUnlimited", testDemandFromLimitedToUnlimited),
|
||||
("testChildValueReceivedWhileSendingValue",
|
||||
testChildValueReceivedWhileSendingValue),
|
||||
("testCompletesProperlyWhenChildrenOutliveUpstream",
|
||||
testCompletesProperlyWhenChildrenOutliveUpstream),
|
||||
("testCompletesProperlyWhenUpstreamOutlivesChildren",
|
||||
testCompletesProperlyWhenUpstreamOutlivesChildren),
|
||||
("testDoesNotCompleteWithBufferedValues", testDoesNotCompleteWithBufferedValues),
|
||||
("testFailsIfUpstreamFails", testFailsIfUpstreamFails),
|
||||
("testFailsIfChildFails", testFailsIfChildFails),
|
||||
("testFailsWithoutSendingBufferedValues", testFailsWithoutSendingBufferedValues),
|
||||
("testAllSubscriptionsReleasedOnUpstreamFailure",
|
||||
testAllSubscriptionsReleasedOnUpstreamFailure),
|
||||
("testAllSubscriptionsReleasedOnChildFailure",
|
||||
testAllSubscriptionsReleasedOnChildFailure),
|
||||
("testSendsSubcriptionDownstreamBeforeDemandUpstream",
|
||||
testSendsSubcriptionDownstreamBeforeDemandUpstream),
|
||||
("testTestSuiteIncludesAllTests", testTestSuiteIncludesAllTests)
|
||||
]
|
||||
|
||||
func testSendsChildValues() {
|
||||
let upstreamPublisher = PassthroughSubject<
|
||||
PassthroughSubject<Int, TestingError>,
|
||||
@@ -609,17 +581,4 @@ final class FlatMapTests: XCTestCase {
|
||||
XCTAssertEqual(receiveOrder, [sentSubscriptionDownstream,
|
||||
sentDemandRequestUpstream])
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
func testTestSuiteIncludesAllTests() {
|
||||
// https://oleb.net/blog/2017/03/keeping-xctest-in-sync/
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
let thisClass = type(of: self)
|
||||
let allTestsCount = thisClass.allTests.count
|
||||
let darwinCount = thisClass.defaultTestSuite.testCaseCount
|
||||
XCTAssertEqual(allTestsCount,
|
||||
darwinCount,
|
||||
"\(darwinCount - allTestsCount) tests are missing from allTests")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,22 +33,22 @@ final class JustTests: XCTestCase {
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testCustomMirror() throws {
|
||||
let just = Sut(42)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriberBase<Int, Never>(
|
||||
receiveSubscription: { downstreamSubscription = $0 }
|
||||
)
|
||||
just.subscribe(tracking)
|
||||
func testReflection() throws {
|
||||
try testSubscriptionReflection(description: "Just",
|
||||
customMirror: expectedChildren((nil, "42")),
|
||||
playgroundDescription: "Just",
|
||||
sut: Sut(42))
|
||||
}
|
||||
|
||||
var reflected = ""
|
||||
try dump(XCTUnwrap(downstreamSubscription), to: &reflected)
|
||||
|
||||
XCTAssertEqual(reflected, """
|
||||
▿ Just #0
|
||||
- 42
|
||||
|
||||
""")
|
||||
func testCrashesOnZeroDemand() {
|
||||
assertCrashes {
|
||||
let just = Sut(42)
|
||||
let tracking =
|
||||
TrackingSubscriberBase<Int, Never>(receiveSubscription: {
|
||||
$0.request(.none)
|
||||
})
|
||||
just.subscribe(tracking)
|
||||
}
|
||||
}
|
||||
|
||||
func testJustWithInitialDemand() {
|
||||
@@ -67,13 +67,27 @@ final class JustTests: XCTestCase {
|
||||
func testCancelOnSubscription() {
|
||||
let just = Sut(42)
|
||||
let tracking = TrackingSubscriberBase<Int, Never>(
|
||||
receiveSubscription: { $0.request(.max(1)); $0.cancel() }
|
||||
receiveSubscription: { $0.cancel(); $0.request(.max(1)) }
|
||||
)
|
||||
just.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Just"),
|
||||
.value(42),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(tracking.history, [.subscription("Just")])
|
||||
}
|
||||
|
||||
func testRecursion() {
|
||||
let just = Sut(42)
|
||||
var subscription: Subscription?
|
||||
let tracking = TrackingSubscriberBase<Int, Never>(
|
||||
receiveSubscription: {
|
||||
subscription = $0
|
||||
$0.request(.unlimited)
|
||||
},
|
||||
receiveValue: { _ in
|
||||
subscription?.request(.unlimited)
|
||||
return .none
|
||||
}
|
||||
)
|
||||
just.subscribe(tracking)
|
||||
}
|
||||
|
||||
func testLifecycle() {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// MakeConnectableTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 19/09/2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class MakeConnectableTests: XCTestCase {
|
||||
|
||||
// MakeConnectable is just Multicast that uses PassthroughSubject,
|
||||
// so we can reuse our existing tests
|
||||
|
||||
func testMulticast() throws {
|
||||
try MulticastTests.testGenericMulticast {
|
||||
$0.makeConnectable()
|
||||
}
|
||||
}
|
||||
|
||||
func testMulticastConnectTwice() {
|
||||
MulticastTests.testGenericMulticastConnectTwice {
|
||||
$0.makeConnectable()
|
||||
}
|
||||
}
|
||||
|
||||
func testMulticastDisconnect() {
|
||||
MulticastTests.testGenericMulticastDisconnect {
|
||||
$0.makeConnectable()
|
||||
}
|
||||
}
|
||||
|
||||
func testReflection() throws {
|
||||
try MulticastTests.testGenericMulticastReflection {
|
||||
$0.makeConnectable()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,6 +179,15 @@ final class MapErrorTests: XCTestCase {
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
func testMapErrorReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "MapError",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "MapError",
|
||||
{ $0.mapError { $0 } })
|
||||
}
|
||||
|
||||
func testLifecycle() throws {
|
||||
|
||||
var deinitCounter = 0
|
||||
|
||||
@@ -52,11 +52,12 @@ final class MapTests: XCTestCase {
|
||||
func testTryMapFailureBecauseOfThrow() {
|
||||
var counter = 0 // How many times the transform is called?
|
||||
|
||||
let publisher = PassthroughSubject<Int, Error>()
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisherBase<Int, Error>(subscription: subscription)
|
||||
let map = publisher.tryMap { value -> Int in
|
||||
counter += 1
|
||||
if value == 100 {
|
||||
throw "too much" as TestingError
|
||||
throw TestingError.oops
|
||||
}
|
||||
return value * 2
|
||||
}
|
||||
@@ -64,21 +65,28 @@ final class MapTests: XCTestCase {
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
|
||||
publisher.send(1)
|
||||
XCTAssertEqual(publisher.send(1), .none)
|
||||
map.subscribe(tracking)
|
||||
publisher.send(2)
|
||||
publisher.send(3)
|
||||
publisher.send(100)
|
||||
publisher.send(9)
|
||||
XCTAssertEqual(publisher.send(2), .none)
|
||||
XCTAssertEqual(publisher.send(3), .none)
|
||||
XCTAssertEqual(publisher.send(100), .none)
|
||||
XCTAssertEqual(publisher.send(9), .none)
|
||||
XCTAssertEqual(publisher.send(100), .none)
|
||||
publisher.send(completion: .finished)
|
||||
XCTAssertEqual(publisher.send(100), .none)
|
||||
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("TryMap"),
|
||||
.value(4),
|
||||
.value(6),
|
||||
.completion(.failure("too much" as TestingError))])
|
||||
.completion(.failure(TestingError.oops)),
|
||||
.value(18),
|
||||
.completion(.failure(TestingError.oops)),
|
||||
.completion(.failure(TestingError.oops))])
|
||||
|
||||
XCTAssertEqual(counter, 3)
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited), .cancelled])
|
||||
|
||||
XCTAssertEqual(counter, 6)
|
||||
}
|
||||
|
||||
func testTryMapFailureOnCompletion() {
|
||||
@@ -218,7 +226,7 @@ final class MapTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testTryMapCancel() throws {
|
||||
// Given
|
||||
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let map = publisher.tryMap { $0 * 2 }
|
||||
@@ -227,16 +235,17 @@ final class MapTests: XCTestCase {
|
||||
$0.request(.unlimited)
|
||||
downstreamSubscription = $0
|
||||
})
|
||||
// When
|
||||
|
||||
map.subscribe(tracking)
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
XCTAssertEqual(publisher.send(1), .none)
|
||||
publisher.send(completion: .finished)
|
||||
// Then
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited), .cancelled])
|
||||
XCTAssertEqual(tracking.history, [.subscription("TryMap"), .value(2)])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
func testMapCancelAlreadyCancelled() throws {
|
||||
// Given
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
@@ -258,6 +267,73 @@ final class MapTests: XCTestCase {
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
func testTryMapCancelAlreadyCancelled() throws {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let map = publisher.tryMap { $0 * 2 }
|
||||
let tracking = TrackingSubscriberBase<Int, Error>()
|
||||
map.subscribe(tracking)
|
||||
|
||||
let downstreamSubscription =
|
||||
try XCTUnwrap(tracking.subscriptions.first?.underlying)
|
||||
|
||||
downstreamSubscription.cancel()
|
||||
downstreamSubscription.request(.unlimited)
|
||||
downstreamSubscription.cancel()
|
||||
|
||||
XCTAssertEqual(subscription.history, [.cancelled])
|
||||
}
|
||||
|
||||
func testTryMapReceiveSubscriptionTwice() throws {
|
||||
|
||||
let firstSubscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: firstSubscription)
|
||||
let map = publisher.tryMap { _ -> Int in throw TestingError.oops }
|
||||
let tracking = TrackingSubscriberBase<Int, Error>()
|
||||
map.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(firstSubscription.history, [])
|
||||
XCTAssertEqual(tracking.history, [.subscription("TryMap")])
|
||||
|
||||
let secondSubscription = CustomSubscription()
|
||||
try XCTUnwrap(publisher.subscriber).receive(subscription: secondSubscription)
|
||||
|
||||
XCTAssertEqual(firstSubscription.history, [])
|
||||
XCTAssertEqual(secondSubscription.history, [.cancelled])
|
||||
XCTAssertEqual(tracking.history, [.subscription("TryMap")])
|
||||
|
||||
XCTAssertEqual(publisher.send(0), .none) // Throws an error
|
||||
|
||||
XCTAssertEqual(firstSubscription.history, [.cancelled])
|
||||
XCTAssertEqual(secondSubscription.history, [.cancelled])
|
||||
XCTAssertEqual(tracking.history, [.subscription("TryMap"),
|
||||
.completion(.failure(TestingError.oops))])
|
||||
try XCTUnwrap(publisher.subscriber).receive(subscription: secondSubscription)
|
||||
|
||||
XCTAssertEqual(firstSubscription.history, [.cancelled])
|
||||
XCTAssertEqual(secondSubscription.history, [.cancelled, .cancelled])
|
||||
XCTAssertEqual(tracking.history, [.subscription("TryMap"),
|
||||
.completion(.failure(TestingError.oops))])
|
||||
}
|
||||
|
||||
func testMapReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Never.self,
|
||||
description: "Map",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "Map",
|
||||
{ $0.map { $0 * 2 } })
|
||||
}
|
||||
|
||||
func testTryMapReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Never.self,
|
||||
description: "TryMap",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "TryMap",
|
||||
{ $0.tryMap { $0 * 2 } })
|
||||
}
|
||||
|
||||
func testLifecycle() throws {
|
||||
|
||||
var deinitCounter = 0
|
||||
|
||||
@@ -17,100 +17,19 @@ import OpenCombine
|
||||
final class MulticastTests: XCTestCase {
|
||||
|
||||
func testMulticast() throws {
|
||||
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
let multicast = publisher.multicast(PassthroughSubject.init)
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.unlimited) })
|
||||
|
||||
multicast.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(publisher.send(0), .none)
|
||||
XCTAssertEqual(publisher.send(12), .none)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Multicast")])
|
||||
|
||||
var connection = multicast.connect()
|
||||
|
||||
XCTAssertEqual(publisher.send(-1), .none)
|
||||
XCTAssertEqual(publisher.send(42), .none)
|
||||
|
||||
connection.cancel()
|
||||
|
||||
XCTAssertEqual(publisher.send(14), .none)
|
||||
|
||||
connection = multicast.connect()
|
||||
|
||||
XCTAssertEqual(publisher.send(15), .none)
|
||||
publisher.send(completion: .finished)
|
||||
publisher.send(completion: .finished)
|
||||
|
||||
connection.cancel()
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Multicast"),
|
||||
.value(-1),
|
||||
.value(42),
|
||||
.value(15),
|
||||
.completion(.finished)])
|
||||
try MulticastTests.testGenericMulticast { $0.multicast(PassthroughSubject.init) }
|
||||
}
|
||||
|
||||
func testMulticastConnectTwice() {
|
||||
|
||||
let publisher = TrackingSubject<Int>()
|
||||
let multicastSubject = TrackingSubject<Int>()
|
||||
let multicast = publisher.multicast(subject: multicastSubject)
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.max(10)) }
|
||||
)
|
||||
|
||||
multicast.subscribe(tracking)
|
||||
|
||||
publisher.send(-1)
|
||||
|
||||
let connection1 = multicast.connect()
|
||||
let connection2 = multicast.connect()
|
||||
|
||||
publisher.send(42)
|
||||
publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Multicast"),
|
||||
.value(42),
|
||||
.value(42),
|
||||
.completion(.finished)])
|
||||
|
||||
connection1.cancel()
|
||||
connection2.cancel()
|
||||
MulticastTests.testGenericMulticastConnectTwice {
|
||||
$0.multicast { TrackingSubjectBase<Int, Never>() }
|
||||
}
|
||||
}
|
||||
|
||||
func testMulticastDisconnect() {
|
||||
|
||||
let publisher = PassthroughSubject<Int, TestingError>()
|
||||
let multicast = publisher.multicast(PassthroughSubject.init)
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.unlimited) })
|
||||
|
||||
multicast.subscribe(tracking)
|
||||
|
||||
publisher.send(-1)
|
||||
|
||||
var connection = multicast.connect()
|
||||
|
||||
publisher.send(42)
|
||||
connection.cancel()
|
||||
publisher.send(100)
|
||||
|
||||
multicast.subscribe(tracking)
|
||||
connection = multicast.connect()
|
||||
publisher.send(2)
|
||||
publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Multicast"),
|
||||
.value(42),
|
||||
.subscription("Multicast"),
|
||||
.value(2),
|
||||
.value(2),
|
||||
.completion(.finished),
|
||||
.completion(.finished)])
|
||||
|
||||
connection.cancel()
|
||||
MulticastTests.testGenericMulticastDisconnect {
|
||||
$0.multicast(PassthroughSubject.init)
|
||||
}
|
||||
}
|
||||
|
||||
func testLateSubscriber() {
|
||||
@@ -231,37 +150,9 @@ final class MulticastTests: XCTestCase {
|
||||
|
||||
func testSubscribeAfterCompletion() {
|
||||
|
||||
final class Subj: Subject {
|
||||
|
||||
typealias Output = Int
|
||||
|
||||
typealias Failure = TestingError
|
||||
|
||||
var subscriber: AnySubscriber<Int, TestingError>?
|
||||
|
||||
func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == TestingError, Downstream.Input == Int
|
||||
{
|
||||
subscriber.receive(subscription: CustomSubscription())
|
||||
self.subscriber = AnySubscriber(subscriber)
|
||||
}
|
||||
|
||||
func send(subscription: Subscription) {
|
||||
subscriber?.receive(subscription: subscription)
|
||||
}
|
||||
|
||||
func send(_ value: Int) {
|
||||
_ = subscriber?.receive(value)
|
||||
}
|
||||
|
||||
func send(completion: Subscribers.Completion<TestingError>) {
|
||||
subscriber?.receive(completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
|
||||
let subject = Subj()
|
||||
let subject = MulticastTestingSubject()
|
||||
|
||||
let multicast = publisher.multicast(subject: subject)
|
||||
|
||||
@@ -277,4 +168,293 @@ final class MulticastTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(lateSubscriber.history, [.subscription("Multicast")])
|
||||
}
|
||||
|
||||
func testInnerSubscriber() throws {
|
||||
|
||||
let publisher = PassthroughSubject<Int, TestingError>()
|
||||
|
||||
let subject = MulticastTestingSubject()
|
||||
|
||||
let multicast = publisher.multicast(subject: subject)
|
||||
let subscriber = TrackingSubscriberBase<Int, TestingError>(
|
||||
receiveSubscription: { $0.request(.max(42)) },
|
||||
receiveValue: { $0 < 0 ? .unlimited : .max($0) }
|
||||
)
|
||||
|
||||
try withExtendedLifetime(multicast.connect()) {
|
||||
multicast.subscribe(subscriber)
|
||||
|
||||
XCTAssertEqual(subscriber.history, [.subscription("Multicast")])
|
||||
XCTAssertEqual(subject.subscription.history, [.requested(.max(42))])
|
||||
|
||||
// Steal the underlying Multicast subscriber/subscription
|
||||
let innerSubscriber = try XCTUnwrap(subject.subscriber)
|
||||
|
||||
innerSubscriber.receive(subscription: Subscriptions.empty)
|
||||
innerSubscriber.receive(subscription: Subscriptions.empty)
|
||||
|
||||
XCTAssertEqual(subscriber.history,
|
||||
[.subscription("Multicast")],
|
||||
"Downstream subscriber should receive subscription once")
|
||||
XCTAssertEqual(subject.subscription.history, [.requested(.max(42))])
|
||||
|
||||
XCTAssertEqual(innerSubscriber.receive(0), .none)
|
||||
XCTAssertEqual(innerSubscriber.receive(0), .none)
|
||||
XCTAssertEqual(innerSubscriber.receive(1), .none)
|
||||
XCTAssertEqual(innerSubscriber.receive(2), .none)
|
||||
XCTAssertEqual(innerSubscriber.receive(-1), .none)
|
||||
XCTAssertEqual(innerSubscriber.receive(3), .none)
|
||||
XCTAssertEqual(innerSubscriber.receive(-1), .none)
|
||||
XCTAssertEqual(innerSubscriber.receive(0), .none)
|
||||
|
||||
XCTAssertEqual(subscriber.history, [.subscription("Multicast"),
|
||||
.value(0),
|
||||
.value(0),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(-1),
|
||||
.value(3),
|
||||
.value(-1),
|
||||
.value(0)])
|
||||
XCTAssertEqual(subject.subscription.history, [.requested(.max(42)),
|
||||
.requested(.max(1)),
|
||||
.requested(.max(2)),
|
||||
.requested(.unlimited),
|
||||
.requested(.max(3)),
|
||||
.requested(.unlimited)])
|
||||
|
||||
innerSubscriber.receive(completion: .failure(.oops))
|
||||
innerSubscriber.receive(completion: .finished)
|
||||
XCTAssertEqual(innerSubscriber.receive(123), .none)
|
||||
innerSubscriber.receive(subscription: Subscriptions.empty)
|
||||
|
||||
XCTAssertEqual(subscriber.history, [.subscription("Multicast"),
|
||||
.value(0),
|
||||
.value(0),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(-1),
|
||||
.value(3),
|
||||
.value(-1),
|
||||
.value(0),
|
||||
.completion(.failure(.oops))])
|
||||
}
|
||||
}
|
||||
|
||||
func testInnerSubscription() throws {
|
||||
let publisher = PassthroughSubject<Int, TestingError>()
|
||||
|
||||
let subject = MulticastTestingSubject()
|
||||
|
||||
let multicast = publisher.multicast(subject: subject)
|
||||
let subscriber = TrackingSubscriberBase<Int, TestingError>()
|
||||
|
||||
try withExtendedLifetime(multicast.connect()) {
|
||||
multicast.subscribe(subscriber)
|
||||
|
||||
XCTAssertEqual(subscriber.history, [.subscription("Multicast")])
|
||||
XCTAssertEqual(subject.subscription.history, [])
|
||||
|
||||
// Steal the underlying Multicast subscriber/subscription
|
||||
let innerSubscriber = try XCTUnwrap(subject.subscriber)
|
||||
let innerSubscription =
|
||||
try XCTUnwrap(subscriber.subscriptions.first?.underlying)
|
||||
|
||||
innerSubscription.request(.max(42))
|
||||
innerSubscription.request(.max(43))
|
||||
innerSubscription.request(.unlimited)
|
||||
innerSubscription.request(.max(44))
|
||||
|
||||
XCTAssertEqual(subject.subscription.history, [.requested(.max(42)),
|
||||
.requested(.max(43)),
|
||||
.requested(.unlimited),
|
||||
.requested(.max(44))])
|
||||
XCTAssertEqual(subscriber.history, [.subscription("Multicast")])
|
||||
|
||||
innerSubscription.cancel()
|
||||
innerSubscription.cancel()
|
||||
innerSubscription.request(.max(30))
|
||||
innerSubscription.request(.unlimited)
|
||||
innerSubscriber.receive(subscription: Subscriptions.empty)
|
||||
XCTAssertEqual(innerSubscriber.receive(1000), .none)
|
||||
innerSubscriber.receive(completion: .finished)
|
||||
|
||||
XCTAssertEqual(subject.subscription.history, [.requested(.max(42)),
|
||||
.requested(.max(43)),
|
||||
.requested(.unlimited),
|
||||
.requested(.max(44)),
|
||||
.cancelled])
|
||||
XCTAssertEqual(subscriber.history, [.subscription("Multicast")])
|
||||
}
|
||||
}
|
||||
|
||||
func testReflection() throws {
|
||||
try MulticastTests.testGenericMulticastReflection {
|
||||
$0.multicast(PassthroughSubject.init)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Generic tests for Multicast & MakeConnectable
|
||||
|
||||
static func testGenericMulticast<Multicast: ConnectablePublisher>(
|
||||
_ makeMulticast: (CustomPublisherBase<Int, Never>) -> Multicast
|
||||
) throws where Multicast.Output == Int, Multicast.Failure == Never {
|
||||
|
||||
let publisher =
|
||||
CustomPublisherBase<Int, Never>(subscription: CustomSubscription())
|
||||
let multicast = makeMulticast(publisher)
|
||||
let tracking = TrackingSubscriberBase<Int, Never>(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
|
||||
multicast.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(publisher.send(0), .none)
|
||||
XCTAssertEqual(publisher.send(12), .none)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Multicast")])
|
||||
|
||||
var connection = multicast.connect()
|
||||
|
||||
XCTAssertEqual(publisher.send(-1), .none)
|
||||
XCTAssertEqual(publisher.send(42), .none)
|
||||
|
||||
connection.cancel()
|
||||
|
||||
XCTAssertEqual(publisher.send(14), .none)
|
||||
|
||||
connection = multicast.connect()
|
||||
|
||||
XCTAssertEqual(publisher.send(15), .none)
|
||||
publisher.send(completion: .finished)
|
||||
publisher.send(completion: .finished)
|
||||
|
||||
connection.cancel()
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Multicast"),
|
||||
.value(-1),
|
||||
.value(42),
|
||||
.value(15),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
static func testGenericMulticastConnectTwice<Multicast: ConnectablePublisher>(
|
||||
_ makeMulticast: (TrackingSubjectBase<Int, Never>) -> Multicast
|
||||
) where Multicast.Output == Int, Multicast.Failure == Never {
|
||||
let publisher = TrackingSubjectBase<Int, Never>()
|
||||
let multicast = makeMulticast(publisher)
|
||||
let tracking = TrackingSubscriberBase<Int, Never>(
|
||||
receiveSubscription: { $0.request(.max(10)) }
|
||||
)
|
||||
|
||||
multicast.subscribe(tracking)
|
||||
|
||||
publisher.send(-1)
|
||||
|
||||
let connection1 = multicast.connect()
|
||||
let connection2 = multicast.connect()
|
||||
|
||||
publisher.send(42)
|
||||
publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Multicast"),
|
||||
.value(42),
|
||||
.value(42),
|
||||
.completion(.finished)])
|
||||
|
||||
connection1.cancel()
|
||||
connection2.cancel()
|
||||
}
|
||||
|
||||
static func testGenericMulticastDisconnect<Multicast: ConnectablePublisher>(
|
||||
_ makeMulticast: (PassthroughSubject<Int, Never>) -> Multicast
|
||||
) where Multicast.Output == Int, Multicast.Failure == Never {
|
||||
|
||||
let publisher = PassthroughSubject<Int, Never>()
|
||||
let multicast = makeMulticast(publisher)
|
||||
let tracking = TrackingSubscriberBase<Int, Never>(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
|
||||
multicast.subscribe(tracking)
|
||||
|
||||
publisher.send(-1)
|
||||
|
||||
var connection = multicast.connect()
|
||||
|
||||
publisher.send(42)
|
||||
connection.cancel()
|
||||
publisher.send(100)
|
||||
|
||||
multicast.subscribe(tracking)
|
||||
connection = multicast.connect()
|
||||
publisher.send(2)
|
||||
publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Multicast"),
|
||||
.value(42),
|
||||
.subscription("Multicast"),
|
||||
.value(2),
|
||||
.value(2),
|
||||
.completion(.finished),
|
||||
.completion(.finished)])
|
||||
|
||||
connection.cancel()
|
||||
}
|
||||
|
||||
static func testGenericMulticastReflection<Multicast: ConnectablePublisher>(
|
||||
_ makeMulticast: (PassthroughSubject<Int, Never>) -> Multicast
|
||||
) throws where Multicast.Output == Int, Multicast.Failure == Never {
|
||||
|
||||
let publisher = PassthroughSubject<Int, Never>()
|
||||
let multicast = makeMulticast(publisher)
|
||||
let tracking = TrackingSubscriberBase<Int, Never>()
|
||||
|
||||
multicast.subscribe(tracking)
|
||||
|
||||
let multicastSubscription =
|
||||
try XCTUnwrap(tracking.subscriptions.first?.underlying)
|
||||
|
||||
let mirror =
|
||||
try XCTUnwrap((multicastSubscription as? CustomReflectable)?.customMirror)
|
||||
|
||||
XCTAssert(mirror.children.isEmpty)
|
||||
|
||||
let playgroundDescription =
|
||||
(multicastSubscription as? CustomPlaygroundDisplayConvertible)?
|
||||
.playgroundDescription as? String
|
||||
|
||||
XCTAssertEqual(playgroundDescription, "Multicast")
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private final class MulticastTestingSubject: Subject {
|
||||
|
||||
typealias Output = Int
|
||||
|
||||
typealias Failure = TestingError
|
||||
|
||||
let subscription = CustomSubscription()
|
||||
|
||||
var subscriber: AnySubscriber<Int, TestingError>?
|
||||
|
||||
func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == TestingError, Downstream.Input == Int
|
||||
{
|
||||
subscriber.receive(subscription: subscription)
|
||||
self.subscriber = AnySubscriber(subscriber)
|
||||
}
|
||||
|
||||
func send(subscription: Subscription) {
|
||||
subscriber?.receive(subscription: subscription)
|
||||
}
|
||||
|
||||
func send(_ value: Int) {
|
||||
_ = subscriber?.receive(value)
|
||||
}
|
||||
|
||||
func send(completion: Subscribers.Completion<TestingError>) {
|
||||
subscriber?.receive(completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,9 +23,9 @@ final class OptionalPublisherTests: XCTestCase {
|
||||
#endif
|
||||
|
||||
func testSuccessNoInitialDemand() {
|
||||
let success = Sut(42)
|
||||
let optional = Sut(42)
|
||||
let tracking = TrackingSubscriberBase<Int, Never>()
|
||||
success.subscribe(tracking)
|
||||
optional.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Optional")])
|
||||
|
||||
@@ -38,11 +38,11 @@ final class OptionalPublisherTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testSuccessWithInitialDemand() {
|
||||
let just = Sut(42)
|
||||
let optional = Sut(42)
|
||||
let tracking = TrackingSubscriberBase<Int, Never>(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
just.subscribe(tracking)
|
||||
optional.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Optional"),
|
||||
.value(42),
|
||||
@@ -70,6 +70,39 @@ final class OptionalPublisherTests: XCTestCase {
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testRecursion() {
|
||||
let optional = Sut(42)
|
||||
var subscription: Subscription?
|
||||
let tracking = TrackingSubscriberBase<Int, Never>(
|
||||
receiveSubscription: {
|
||||
subscription = $0
|
||||
$0.request(.unlimited)
|
||||
},
|
||||
receiveValue: { _ in
|
||||
subscription?.request(.unlimited)
|
||||
return .none
|
||||
}
|
||||
)
|
||||
optional.subscribe(tracking)
|
||||
}
|
||||
|
||||
func testReflection() throws {
|
||||
try testSubscriptionReflection(description: "Optional",
|
||||
customMirror: expectedChildren((nil, "42")),
|
||||
playgroundDescription: "Optional",
|
||||
sut: Sut(42))
|
||||
}
|
||||
|
||||
func testCrashesOnZeroDemand() {
|
||||
assertCrashes {
|
||||
let tracking =
|
||||
TrackingSubscriberBase<Int, Never>(receiveSubscription: {
|
||||
$0.request(.none)
|
||||
})
|
||||
Sut(42).subscribe(tracking)
|
||||
}
|
||||
}
|
||||
|
||||
func testLifecycle() {
|
||||
var deinitCount = 0
|
||||
do {
|
||||
|
||||
@@ -18,7 +18,7 @@ final class PrintTests: XCTestCase {
|
||||
|
||||
func testPrintWithoutPrefix() {
|
||||
|
||||
let stream = StringStream()
|
||||
let stream = HistoryStream()
|
||||
let subscription = CustomSubscription(
|
||||
onRequest: { _ in stream.write("callback request demand\n") },
|
||||
onCancel: { stream.write("callback cancel subscription\n") }
|
||||
@@ -68,44 +68,81 @@ final class PrintTests: XCTestCase {
|
||||
.requested(.max(30)),
|
||||
.cancelled])
|
||||
|
||||
let expectedOutput = """
|
||||
receive subscription: (CustomSubscription)
|
||||
callback subscription
|
||||
request unlimited
|
||||
callback request demand
|
||||
request max: (30)
|
||||
callback request demand
|
||||
receive cancel
|
||||
callback cancel subscription
|
||||
receive value: (1)
|
||||
callback value
|
||||
request max: (100)
|
||||
request max: (2) (synchronous)
|
||||
request max: (42)
|
||||
receive value: (2)
|
||||
callback value
|
||||
request max: (100)
|
||||
request max: (2) (synchronous)
|
||||
receive finished
|
||||
callback completion
|
||||
request max: (12)
|
||||
receive error: (failure)
|
||||
callback completion
|
||||
request max: (12)
|
||||
receive value: (10)
|
||||
callback value
|
||||
request max: (100)
|
||||
request unlimited (synchronous)
|
||||
receive cancel
|
||||
|
||||
"""
|
||||
let expectedOutput = [
|
||||
"",
|
||||
"receive subscription: (CustomSubscription)",
|
||||
"\n",
|
||||
"callback subscription\n",
|
||||
"",
|
||||
"request unlimited",
|
||||
"\n",
|
||||
"callback request demand\n",
|
||||
"",
|
||||
"request max: (30)",
|
||||
"\n",
|
||||
"callback request demand\n",
|
||||
"",
|
||||
"receive cancel",
|
||||
"\n",
|
||||
"callback cancel subscription\n",
|
||||
"",
|
||||
"receive value: (1)",
|
||||
"\n",
|
||||
"callback value\n",
|
||||
"",
|
||||
"request max: (100)",
|
||||
"\n",
|
||||
"",
|
||||
"request max: (2) (synchronous)",
|
||||
"\n",
|
||||
"",
|
||||
"request max: (42)",
|
||||
"\n",
|
||||
"",
|
||||
"receive value: (2)",
|
||||
"\n",
|
||||
"callback value\n",
|
||||
"",
|
||||
"request max: (100)",
|
||||
"\n",
|
||||
"",
|
||||
"request max: (2) (synchronous)",
|
||||
"\n",
|
||||
"",
|
||||
"receive finished",
|
||||
"\n",
|
||||
"callback completion\n",
|
||||
"",
|
||||
"request max: (12)",
|
||||
"\n",
|
||||
"",
|
||||
"receive error: (failure)",
|
||||
"\n",
|
||||
"callback completion\n",
|
||||
"",
|
||||
"request max: (12)",
|
||||
"\n",
|
||||
"",
|
||||
"receive value: (10)",
|
||||
"\n",
|
||||
"callback value\n",
|
||||
"",
|
||||
"request max: (100)",
|
||||
"\n",
|
||||
"",
|
||||
"request unlimited (synchronous)",
|
||||
"\n",
|
||||
"",
|
||||
"receive cancel",
|
||||
"\n"
|
||||
]
|
||||
|
||||
XCTAssertEqual(stream.output.value, expectedOutput)
|
||||
}
|
||||
|
||||
func testPrintWithPrefix() {
|
||||
|
||||
let stream = StringStream()
|
||||
let stream = HistoryStream()
|
||||
let subscription = CustomSubscription(
|
||||
onRequest: { _ in stream.write("callback request demand\n") },
|
||||
onCancel: { stream.write("callback cancel subscription\n") }
|
||||
@@ -155,44 +192,81 @@ final class PrintTests: XCTestCase {
|
||||
.requested(.max(30)),
|
||||
.cancelled])
|
||||
|
||||
let expectedOutput = """
|
||||
👉: receive subscription: (CustomSubscription)
|
||||
callback subscription
|
||||
👉: request unlimited
|
||||
callback request demand
|
||||
👉: request max: (30)
|
||||
callback request demand
|
||||
👉: receive cancel
|
||||
callback cancel subscription
|
||||
👉: receive value: (1)
|
||||
callback value
|
||||
👉: request max: (100)
|
||||
👉: request max: (2) (synchronous)
|
||||
👉: request max: (42)
|
||||
👉: receive value: (2)
|
||||
callback value
|
||||
👉: request max: (100)
|
||||
👉: request max: (2) (synchronous)
|
||||
👉: receive finished
|
||||
callback completion
|
||||
👉: request max: (12)
|
||||
👉: receive error: (failure)
|
||||
callback completion
|
||||
👉: request max: (12)
|
||||
👉: receive value: (10)
|
||||
callback value
|
||||
👉: request max: (100)
|
||||
👉: request unlimited (synchronous)
|
||||
👉: receive cancel
|
||||
|
||||
"""
|
||||
let expectedOutput = [
|
||||
"",
|
||||
"👉: receive subscription: (CustomSubscription)",
|
||||
"\n",
|
||||
"callback subscription\n",
|
||||
"",
|
||||
"👉: request unlimited",
|
||||
"\n",
|
||||
"callback request demand\n",
|
||||
"",
|
||||
"👉: request max: (30)",
|
||||
"\n",
|
||||
"callback request demand\n",
|
||||
"",
|
||||
"👉: receive cancel",
|
||||
"\n",
|
||||
"callback cancel subscription\n",
|
||||
"",
|
||||
"👉: receive value: (1)",
|
||||
"\n",
|
||||
"callback value\n",
|
||||
"",
|
||||
"👉: request max: (100)",
|
||||
"\n",
|
||||
"",
|
||||
"👉: request max: (2) (synchronous)",
|
||||
"\n",
|
||||
"",
|
||||
"👉: request max: (42)",
|
||||
"\n",
|
||||
"",
|
||||
"👉: receive value: (2)",
|
||||
"\n",
|
||||
"callback value\n",
|
||||
"",
|
||||
"👉: request max: (100)",
|
||||
"\n",
|
||||
"",
|
||||
"👉: request max: (2) (synchronous)",
|
||||
"\n",
|
||||
"",
|
||||
"👉: receive finished",
|
||||
"\n",
|
||||
"callback completion\n",
|
||||
"",
|
||||
"👉: request max: (12)",
|
||||
"\n",
|
||||
"",
|
||||
"👉: receive error: (failure)",
|
||||
"\n",
|
||||
"callback completion\n",
|
||||
"",
|
||||
"👉: request max: (12)",
|
||||
"\n",
|
||||
"",
|
||||
"👉: receive value: (10)",
|
||||
"\n",
|
||||
"callback value\n",
|
||||
"",
|
||||
"👉: request max: (100)",
|
||||
"\n",
|
||||
"",
|
||||
"👉: request unlimited (synchronous)",
|
||||
"\n",
|
||||
"",
|
||||
"👉: receive cancel",
|
||||
"\n"
|
||||
]
|
||||
|
||||
XCTAssertEqual(stream.output.value, expectedOutput)
|
||||
}
|
||||
|
||||
func testSynchronization() {
|
||||
|
||||
let stream = StringStream()
|
||||
let stream = HistoryStream()
|
||||
let publisher = CustomPublisherBase<Int, Never>(subscription: nil)
|
||||
let printer = publisher.print(to: stream)
|
||||
|
||||
@@ -208,11 +282,11 @@ final class PrintTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
private final class StringStream: TextOutputStream {
|
||||
private final class HistoryStream: TextOutputStream {
|
||||
|
||||
var output = Atomic("")
|
||||
let output = Atomic([String]())
|
||||
|
||||
func write(_ string: String) {
|
||||
output.do { $0.write(string) }
|
||||
output.do { $0.append(string) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,11 @@ final class ReplaceErrorTests: XCTestCase {
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceError"),
|
||||
.value(42),
|
||||
.completion(.finished)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(-1), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceError"),
|
||||
.value(42),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testLifecycle() throws {
|
||||
@@ -154,8 +159,7 @@ final class ReplaceErrorTests: XCTestCase {
|
||||
helper.downstreamSubscription?.request(.unlimited)
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.cancelled])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
}
|
||||
|
||||
func testErrorWhileDownstreamDemandIsZero() {
|
||||
@@ -165,7 +169,7 @@ final class ReplaceErrorTests: XCTestCase {
|
||||
createSut: { $0.replaceError(with: 42) })
|
||||
|
||||
// Send demanded value
|
||||
_ = helper.publisher.send(9)
|
||||
XCTAssertEqual(helper.publisher.send(9), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceError"),
|
||||
.value(9)])
|
||||
|
||||
@@ -179,12 +183,71 @@ final class ReplaceErrorTests: XCTestCase {
|
||||
.value(42),
|
||||
.completion(.finished)])
|
||||
}
|
||||
}
|
||||
|
||||
private struct OtherError: Error {
|
||||
let original: Error
|
||||
func testCrashOnReceiveValueWithZeroPendingDemand() {
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
let replaceError = publisher.replaceError(with: 0)
|
||||
let tracking = TrackingSubscriberBase<Int, Never>()
|
||||
replaceError.subscribe(tracking)
|
||||
|
||||
init(_ original: Error) {
|
||||
self.original = original
|
||||
assertCrashes {
|
||||
XCTAssertEqual(publisher.send(1), .none)
|
||||
}
|
||||
}
|
||||
|
||||
func testReplaceErrorReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "ReplaceError",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "ReplaceError",
|
||||
{ $0.replaceError(with: 0) })
|
||||
}
|
||||
|
||||
func testReceiveSubscriptionTwice() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(1),
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.replaceError(with: 42) })
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(1))])
|
||||
|
||||
let anotherSubscription = CustomSubscription()
|
||||
helper.publisher.subscriber?.receive(subscription: anotherSubscription)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(1))])
|
||||
XCTAssertEqual(anotherSubscription.history, [.cancelled])
|
||||
|
||||
helper.downstreamSubscription?.cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(1)), .cancelled])
|
||||
|
||||
helper.publisher.subscriber?.receive(subscription: anotherSubscription)
|
||||
|
||||
XCTAssertEqual(anotherSubscription.history, [.cancelled, .cancelled])
|
||||
}
|
||||
|
||||
func testReceiveCompletionTwice() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(1),
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.replaceError(with: 42) })
|
||||
|
||||
helper.publisher.send(completion: .finished)
|
||||
helper.publisher.send(completion: .finished)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceError"),
|
||||
.completion(.finished),
|
||||
.completion(.finished)])
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.publisher.send(-1), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceError"),
|
||||
.completion(.finished),
|
||||
.completion(.finished),
|
||||
.value(42),
|
||||
.completion(.finished),
|
||||
.value(42),
|
||||
.completion(.finished),
|
||||
.value(-1)])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,4 +86,13 @@ final class ReplaceNilTests: XCTestCase {
|
||||
.value(42),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testReplaceNilReflection() throws {
|
||||
try testReflection(parentInput: Int?.self,
|
||||
parentFailure: Never.self,
|
||||
description: "Map",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "Map",
|
||||
{ $0.replaceNil(with: 0) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,46 @@ final class ResultPublisherTests: XCTestCase {
|
||||
.completion(.failure("failure"))])
|
||||
}
|
||||
|
||||
func testRecursion() {
|
||||
let success = makePublisher(42)
|
||||
var subscription: Subscription?
|
||||
let tracking = TrackingSubscriberBase<Int, TestingError>(
|
||||
receiveSubscription: {
|
||||
subscription = $0
|
||||
$0.request(.unlimited)
|
||||
},
|
||||
receiveValue: { _ in
|
||||
subscription?.request(.unlimited)
|
||||
return .none
|
||||
}
|
||||
)
|
||||
success.subscribe(tracking)
|
||||
}
|
||||
|
||||
func testReflection() throws {
|
||||
|
||||
func testCustomMirror(_ mirror: Mirror) -> Bool {
|
||||
return mirror.children.count == 1 &&
|
||||
mirror.children.first!.label == nil &&
|
||||
(mirror.children.first!.value as? Int) == 42
|
||||
}
|
||||
|
||||
try testSubscriptionReflection(description: "Once",
|
||||
customMirror: expectedChildren((nil, "42")),
|
||||
playgroundDescription: "Once",
|
||||
sut: Sut(42))
|
||||
}
|
||||
|
||||
func testCrashesOnZeroDemand() {
|
||||
assertCrashes {
|
||||
let tracking =
|
||||
TrackingSubscriberBase<Int, TestingError>(receiveSubscription: {
|
||||
$0.request(.none)
|
||||
})
|
||||
makePublisher(42).subscribe(tracking)
|
||||
}
|
||||
}
|
||||
|
||||
func testLifecycle() {
|
||||
var deinitCount = 0
|
||||
do {
|
||||
|
||||
@@ -166,6 +166,75 @@ final class SequenceTests: XCTestCase {
|
||||
.value(1)])
|
||||
}
|
||||
|
||||
func testCancelOnValue() {
|
||||
let counter = Counter(upperBound: 3)
|
||||
let publisher = makePublisher(counter)
|
||||
var subscription: Subscription?
|
||||
let subscriber = TrackingSubscriberBase<Int, Never>(
|
||||
receiveSubscription: {
|
||||
subscription = $0
|
||||
$0.request(.unlimited)
|
||||
},
|
||||
receiveValue: { _ in
|
||||
subscription?.cancel()
|
||||
return .unlimited
|
||||
}
|
||||
)
|
||||
publisher.subscribe(subscriber)
|
||||
|
||||
XCTAssertEqual(subscriber.history, [.subscription("Sequence"),
|
||||
.value(1)])
|
||||
|
||||
subscriber.subscriptions.first?.request(.max(1))
|
||||
|
||||
XCTAssertEqual(subscriber.history, [.subscription("Sequence"),
|
||||
.value(1)])
|
||||
}
|
||||
|
||||
func testPublishesCorrectValues() {
|
||||
let sequence = makePublisher(1...5)
|
||||
|
||||
var history = [Int]()
|
||||
_ = sequence.sink {
|
||||
history.append($0)
|
||||
}
|
||||
|
||||
XCTAssertEqual(history, [1, 2, 3, 4, 5])
|
||||
}
|
||||
|
||||
func testRecursion() {
|
||||
let sequence = makePublisher(1...5)
|
||||
|
||||
var history = [Int]()
|
||||
var storedSubscription: Subscription?
|
||||
|
||||
let tracking = TrackingSubscriberBase<Int, Never>(
|
||||
receiveSubscription: { subscription in
|
||||
storedSubscription = subscription
|
||||
subscription.request(.none) // Shouldn't crash
|
||||
subscription.request(.max(1))
|
||||
},
|
||||
receiveValue: { value in
|
||||
storedSubscription?.request(.max(1))
|
||||
history.append(value)
|
||||
return .none
|
||||
}
|
||||
)
|
||||
|
||||
sequence.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(history, [1, 2, 3, 4, 5])
|
||||
}
|
||||
|
||||
func testReflection() throws {
|
||||
|
||||
try testSubscriptionReflection(description: "1...5",
|
||||
customMirror:
|
||||
expectedChildren(("sequence", "1...5")),
|
||||
playgroundDescription: "1...5",
|
||||
sut: makePublisher(1...5))
|
||||
}
|
||||
|
||||
func testLifecycle() throws {
|
||||
|
||||
var deinitCounter = 0
|
||||
|
||||
@@ -158,4 +158,13 @@ final class SetFailureTypeTests: XCTestCase {
|
||||
.requested(.unlimited),
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
func testSetFailureTypeReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Never.self,
|
||||
description: "SetFailureType",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "SetFailureType",
|
||||
{ $0.setFailureType(to: Error.self) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
//
|
||||
// ShareTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 03/10/2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class ShareTests: XCTestCase {
|
||||
|
||||
func testBasicBehavior() {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
|
||||
let share = publisher.share()
|
||||
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.max(42)) },
|
||||
receiveValue: { .max($0) }
|
||||
)
|
||||
|
||||
share.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(tracking.history, [.subscription("Multicast")])
|
||||
|
||||
XCTAssertEqual(publisher.send(1), .none)
|
||||
XCTAssertEqual(publisher.send(2), .none)
|
||||
XCTAssertEqual(publisher.send(3), .none)
|
||||
publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(tracking.history, [.subscription("Multicast"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.completion(.failure(.oops))])
|
||||
}
|
||||
|
||||
func testLateSubscriber() {
|
||||
let subscription = CustomSubscription()
|
||||
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let share = publisher.share()
|
||||
|
||||
let earlySubscriber = TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
$0.request(.max(3))
|
||||
}
|
||||
)
|
||||
|
||||
share.subscribe(earlySubscriber)
|
||||
|
||||
XCTAssertNotNil(publisher.subscriber)
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(earlySubscriber.history, [.subscription("Multicast")])
|
||||
|
||||
XCTAssertEqual(publisher.send(1), .none)
|
||||
XCTAssertEqual(publisher.send(2), .none)
|
||||
XCTAssertEqual(publisher.send(3), .none)
|
||||
XCTAssertEqual(publisher.send(4), .none)
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(earlySubscriber.history, [.subscription("Multicast"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
|
||||
let lateSubscriber = TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
$0.request(.max(2))
|
||||
}
|
||||
)
|
||||
|
||||
share.subscribe(lateSubscriber)
|
||||
|
||||
XCTAssertEqual(publisher.send(5), .none)
|
||||
XCTAssertEqual(publisher.send(6), .none)
|
||||
XCTAssertEqual(publisher.send(7), .none)
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(earlySubscriber.history, [.subscription("Multicast"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
XCTAssertEqual(lateSubscriber.history, [.subscription("Multicast"),
|
||||
.value(5),
|
||||
.value(6)])
|
||||
|
||||
publisher.send(completion: .finished)
|
||||
|
||||
let latestSubscriber = TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
$0.request(.none)
|
||||
}
|
||||
)
|
||||
|
||||
share.subscribe(latestSubscriber)
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(earlySubscriber.history, [.subscription("Multicast"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(lateSubscriber.history, [.subscription("Multicast"),
|
||||
.value(5),
|
||||
.value(6),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(latestSubscriber.history, [.subscription("Multicast"),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testEquatable() {
|
||||
|
||||
let publisher = CustomPublisher(subscription: nil)
|
||||
let share1 = publisher.share()
|
||||
let share2 = publisher.share()
|
||||
|
||||
XCTAssertEqual(share1, share1)
|
||||
XCTAssertEqual(share2, share2)
|
||||
XCTAssertNotEqual(share1, share2)
|
||||
XCTAssertNotEqual(share2, share1)
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ final class AssignTests: XCTestCase {
|
||||
|
||||
let subscription1 = CustomSubscription()
|
||||
assign.receive(subscription: subscription1)
|
||||
XCTAssertEqual(subscription1.lastRequested, .unlimited)
|
||||
XCTAssertEqual(subscription1.history, [.requested(.unlimited)])
|
||||
XCTAssertFalse(subscription1.cancelled)
|
||||
|
||||
let subscription2 = CustomSubscription()
|
||||
@@ -75,6 +75,11 @@ final class AssignTests: XCTestCase {
|
||||
subscription1.cancelled = false
|
||||
assign.receive(completion: .finished)
|
||||
XCTAssertTrue(subscription1.cancelled)
|
||||
|
||||
let subscription3 = CustomSubscription()
|
||||
assign.receive(subscription: subscription3)
|
||||
XCTAssertEqual(subscription3.history, [.cancelled])
|
||||
XCTAssertTrue(subscription3.cancelled)
|
||||
}
|
||||
|
||||
func testReceiveValue() {
|
||||
|
||||
@@ -36,7 +36,7 @@ final class SinkTests: XCTestCase {
|
||||
|
||||
let subscription1 = CustomSubscription()
|
||||
sink.receive(subscription: subscription1)
|
||||
XCTAssertEqual(subscription1.lastRequested, .unlimited)
|
||||
XCTAssertEqual(subscription1.history, [.requested(.unlimited)])
|
||||
XCTAssertFalse(subscription1.cancelled)
|
||||
|
||||
let subscription2 = CustomSubscription()
|
||||
@@ -50,6 +50,11 @@ final class SinkTests: XCTestCase {
|
||||
subscription1.cancelled = false
|
||||
sink.receive(completion: .finished)
|
||||
XCTAssertFalse(subscription1.cancelled)
|
||||
|
||||
let subscription3 = CustomSubscription()
|
||||
sink.receive(subscription: subscription3)
|
||||
XCTAssertEqual(subscription3.history, [.cancelled])
|
||||
XCTAssertTrue(subscription3.cancelled)
|
||||
}
|
||||
|
||||
func testReceiveValue() {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
CombineIdentifierTests:
|
||||
testDefaultInitializedPerformance:
|
||||
maxPercentRelativeStandardDeviation: 1e+1
|
||||
measurement: 1.5804951544851065e-05
|
||||
strategy: minimum
|
||||
PassthroughSubjectTests:
|
||||
testCancelSubscriptionPerformance:
|
||||
maxPercentRelativeStandardDeviation: 1e+1
|
||||
measurement: 6.59683800768107e-3
|
||||
strategy: minimum
|
||||
testSubscriptionPerformance:
|
||||
maxPercentRelativeStandardDeviation: 1e+1
|
||||
measurement: 1.56868119956926e-2
|
||||
strategy: minimum
|
||||
@@ -1,5 +1,5 @@
|
||||
CombineIdentifierTests:
|
||||
testDefaultInitializedPerformance:
|
||||
maxPercentRelativeStandardDeviation: 1e+1
|
||||
measurement: 1.06726900003764e-4
|
||||
measurement: 1.6446406001023208e-05
|
||||
strategy: minimum
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
CombineIdentifierTests:
|
||||
testDefaultInitializedPerformance:
|
||||
maxPercentRelativeStandardDeviation: 1e+1
|
||||
measurement: 1.47168469999883e-4
|
||||
measurement: 2.1368432000258509e-05
|
||||
strategy: minimum
|
||||
|
||||
Reference in New Issue
Block a user