├── Sources ├── _CXShim ├── _CXTest ├── _CXCompatible ├── CombineX │ ├── Subscriptions │ │ ├── Subscriptions.swift │ │ └── EmptySubscription.swift │ ├── Subscribers │ │ ├── Subscribers.swift │ │ └── Completion.swift │ ├── Coding.swift │ ├── CustomCombineIdentifierConvertible.swift │ ├── Publishers │ │ ├── B │ │ │ ├── Publishers.swift │ │ │ ├── Deferred.swift │ │ │ ├── Combined │ │ │ │ ├── Last.swift │ │ │ │ ├── Encode.swift │ │ │ │ ├── Decode.swift │ │ │ │ ├── First.swift │ │ │ │ ├── IgnoreOutput.swift │ │ │ │ ├── Drop.swift │ │ │ │ ├── Count.swift │ │ │ │ ├── Collect.swift │ │ │ │ ├── Filter.swift │ │ │ │ ├── LastWhere.swift │ │ │ │ ├── DropWhile.swift │ │ │ │ ├── FirstWhere.swift │ │ │ │ ├── PrefixWhile.swift │ │ │ │ ├── ContainsWhere.swift │ │ │ │ ├── Contains.swift │ │ │ │ ├── TryFirstWhere.swift │ │ │ │ ├── TryLastWhere.swift │ │ │ │ ├── TryContainsWhere.swift │ │ │ │ ├── Scan.swift │ │ │ │ ├── Reduce.swift │ │ │ │ ├── CompactMap.swift │ │ │ │ ├── AllSatisfy.swift │ │ │ │ ├── AssertNoFailure.swift │ │ │ │ ├── TryFilter.swift │ │ │ │ ├── Map.swift │ │ │ │ ├── TryMap.swift │ │ │ │ └── RemoveDuplicates.swift │ │ │ ├── Fail.swift │ │ │ ├── Empty.swift │ │ │ ├── SetFailureType.swift │ │ │ └── TryCombineLatest.swift │ │ └── C │ │ │ ├── MakeConnectable.swift │ │ │ ├── Future.swift │ │ │ ├── Autoconnect.swift │ │ │ └── Share.swift │ ├── SchedulerTimeIntervalConvertible.swift │ ├── ConnectablePublisher.swift │ ├── Internal │ │ ├── WeakHashBox.swift │ │ ├── PeekableIterator.swift │ │ ├── Extensions │ │ │ ├── Completion+extensions.swift │ │ │ ├── Never+reasons.swift │ │ │ └── Result+extensions.swift │ │ ├── OptionalProtocol.swift │ │ ├── ObserableObjectCache.swift │ │ ├── RelayState.swift │ │ └── DemandState.swift │ ├── Subscription.swift │ ├── CombineIdentifier.swift │ ├── Cancellable.swift │ ├── CXNamespace.swift │ ├── Subject.swift │ ├── Subscriber.swift │ ├── AnyCancellable.swift │ ├── Publisher.swift │ └── AnyPublisher.swift ├── CXTestUtility │ ├── @_exported.swift │ ├── Common.swift │ ├── Extensions │ │ ├── Subject+send.swift │ │ ├── Int+loop.swift │ │ ├── Sequence+scan.swift │ │ ├── DispatchQueue+extensions.swift │ │ └── TracingSubscriber+extensions.swift │ ├── TestError.swift │ ├── Inconsistent │ │ └── BranchExpectation.swift │ ├── Predicate.swift │ └── TestTimeline.swift ├── CXUtility │ ├── Const.swift │ ├── Math.swift │ └── LockedAtomic.swift └── CXFoundation │ ├── JSONEncoder.swift │ ├── JSONDecoder.swift │ ├── PropertyListEncoder.swift │ ├── PropertyListDecoder.swift │ └── NSObject.swift ├── CHANGELOG.md ├── .gitignore ├── .gitmodules ├── Tests ├── CombineXTests │ ├── Typealias.swift │ ├── Publishers │ │ ├── HandleEventsSpec.swift │ │ ├── AssertNoFailureSpec.swift │ │ ├── RemoveDuplicatesSpec.swift │ │ ├── ReplaceErrorSpec.swift │ │ ├── AutoconnectSpec.swift │ │ ├── MulticastSpec.swift │ │ ├── ShareSpec.swift │ │ ├── EmptySpec.swift │ │ ├── ReplaceEmptySpec.swift │ │ ├── MergeSpec.swift │ │ ├── BufferSpec.swift │ │ ├── MeasureIntervalSpec.swift │ │ ├── PrefixUntilOutputSpec.swift │ │ ├── OptionalSpec.swift │ │ ├── CollectByCountSpec.swift │ │ ├── MapErrorSpec.swift │ │ ├── TryRemoveDuplicatesSpec.swift │ │ ├── TryAllSatisfySpec.swift │ │ ├── RetrySpec.swift │ │ ├── TimeoutSpec.swift │ │ ├── DropUntilOutputSpec.swift │ │ ├── RecordSpec.swift │ │ ├── TryCatchSpec.swift │ │ ├── ResultSpec.swift │ │ └── ReceiveOnSpec.swift │ ├── Scheduler │ │ └── ImmediateSchedulerSpec.swift │ └── CombineIdentifierSpec.swift ├── CXInconsistentTests │ ├── README.md │ ├── Fixed │ │ └── FixedSpec.swift │ ├── SuspiciousBehaviour │ │ ├── SuspiciousDemandSpec.swift │ │ ├── SuspiciousBufferSpec.swift │ │ └── SuspiciousSwitchToLatestSpec.swift │ ├── Versioning │ │ ├── VersioningAssignSpec.swift │ │ ├── VersioningReceiveOnSpec.swift │ │ ├── VersioningDelaySpec.swift │ │ ├── VersioningSinkSpec.swift │ │ ├── VersioningFutureSpec.swift │ │ └── VersioningSwitchToLatestSpec.swift │ └── FailingTests │ │ ├── FailingFlatMapSpec.swift │ │ ├── FailingTimerSpec.swift │ │ └── FailingBufferSpec.swift ├── CXFoundationTests │ ├── URLSessionSpec.swift │ ├── CoderSpec.swift │ ├── TimerSpec.swift │ ├── NotificationCenterSpec.swift │ └── SchedulerSpec.swift └── LinuxMain.swift ├── CombineX-Carthage.xcodeproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Scripts └── generate-xcodeproj-for-carthage ├── Package.swift ├── Info-carthage.plist ├── project.yml ├── LICENSE ├── CombineX.podspec ├── .swiftlint.yml ├── Package.resolved └── README_zh-Hans.md /Sources/_CXShim: -------------------------------------------------------------------------------- 1 | ../CXShim/Sources/CXShim -------------------------------------------------------------------------------- /Sources/_CXTest: -------------------------------------------------------------------------------- 1 | ../CXTest/Sources/CXTest -------------------------------------------------------------------------------- /Sources/_CXCompatible: -------------------------------------------------------------------------------- 1 | ../CXShim/Sources/CXCompatible -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## Next Version 4 | -------------------------------------------------------------------------------- /Sources/CombineX/Subscriptions/Subscriptions.swift: -------------------------------------------------------------------------------- 1 | public enum Subscriptions { 2 | } 3 | -------------------------------------------------------------------------------- /Sources/CXTestUtility/@_exported.swift: -------------------------------------------------------------------------------- 1 | @_exported import _CXShim 2 | @_exported import _CXTest 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .swiftpm 2 | .build 3 | 4 | xcuserdata/ 5 | 6 | Carthage/Build/ 7 | 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /Sources/CombineX/Subscribers/Subscribers.swift: -------------------------------------------------------------------------------- 1 | /// A namespace for types related to the `Subscriber` protocol. 2 | public enum Subscribers { 3 | } 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "CXShim"] 2 | path = CXShim 3 | url = https://github.com/cx-org/CXShim 4 | [submodule "CXTest"] 5 | path = CXTest 6 | url = https://github.com/cx-org/CXTest 7 | -------------------------------------------------------------------------------- /Sources/CXTestUtility/Common.swift: -------------------------------------------------------------------------------- 1 | @inline(never) 2 | public func blackHole(_ x: T) { 3 | } 4 | 5 | @inline(never) 6 | public func identity(_ x: T) -> T { 7 | return x 8 | } 9 | -------------------------------------------------------------------------------- /Tests/CombineXTests/Typealias.swift: -------------------------------------------------------------------------------- 1 | #if swift(>=5.1) 2 | 3 | import CXTestUtility 4 | 5 | typealias Published = CXTestUtility.Published 6 | typealias ObservableObject = CXTestUtility.ObservableObject 7 | 8 | #endif 9 | -------------------------------------------------------------------------------- /CombineX-Carthage.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Scripts/generate-xcodeproj-for-carthage: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Generate "xcodeproj" for Carthage 4 | 5 | if which xcodegen >/dev/null; then 6 | xcodegen generate 7 | else 8 | echo "ERROR: [XcodeGen](https://github.com/yonaskolb/XcodeGen) is not installed." 9 | fi 10 | -------------------------------------------------------------------------------- /Sources/CXTestUtility/Extensions/Subject+send.swift: -------------------------------------------------------------------------------- 1 | 2 | extension Subject { 3 | 4 | public func send(contentsOf values: S) where S.Element == Output { 5 | for value in values { 6 | send(value) 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /CombineX-Carthage.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/CombineX/Coding.swift: -------------------------------------------------------------------------------- 1 | public protocol TopLevelDecoder { 2 | 3 | associatedtype Input 4 | 5 | func decode(_ type: T.Type, from: Input) throws -> T 6 | } 7 | 8 | public protocol TopLevelEncoder { 9 | 10 | associatedtype Output 11 | 12 | func encode(_ value: T) throws -> Output 13 | } 14 | -------------------------------------------------------------------------------- /Sources/CXUtility/Const.swift: -------------------------------------------------------------------------------- 1 | public enum Const { 2 | 3 | public static let nsec_per_sec = 1000_000_000 4 | public static let nsec_per_msec = 1000_000 5 | public static let nsec_per_usec = 1000 6 | 7 | public static let usec_per_sec = 1000_000 8 | public static let usec_per_msec = 1000 9 | 10 | public static let msec_per_sec = 1000 11 | } 12 | -------------------------------------------------------------------------------- /Sources/CombineX/CustomCombineIdentifierConvertible.swift: -------------------------------------------------------------------------------- 1 | public protocol CustomCombineIdentifierConvertible { 2 | 3 | var combineIdentifier: CombineIdentifier { get } 4 | } 5 | 6 | extension CustomCombineIdentifierConvertible where Self: AnyObject { 7 | 8 | public var combineIdentifier: CombineIdentifier { 9 | return CombineIdentifier(self) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/Publishers.swift: -------------------------------------------------------------------------------- 1 | /// A namespace for types related to the Publisher protocol. 2 | /// 3 | /// The various operators defined as extensions on `Publisher` implement their functionality as classes 4 | /// or structures that extend this enumeration. For example, the `contains()` operator returns a 5 | /// `Publishers.Contains` instance. 6 | public enum Publishers { 7 | } 8 | -------------------------------------------------------------------------------- /Tests/CombineXTests/Publishers/HandleEventsSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Nimble 3 | import Quick 4 | 5 | class HandleEventsSpec: QuickSpec { 6 | 7 | override func spec() { 8 | 9 | // MARK: - Relay 10 | describe("Relay") { 11 | 12 | it("should") { 13 | expect(1989) != 0614 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/CXTestUtility/Extensions/Int+loop.swift: -------------------------------------------------------------------------------- 1 | extension Int { 2 | 3 | public func times(_ body: (Int) -> Void) { 4 | guard self > 0 else { 5 | return 6 | } 7 | for i in 0.. Void) { 13 | self.times { _ in 14 | body() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/CXTestUtility/Extensions/Sequence+scan.swift: -------------------------------------------------------------------------------- 1 | extension Sequence { 2 | 3 | public func scan(_ initialResult: Result, _ nextPartialResult: (Result, Element) -> Result) -> [Result] { 4 | var partialResult = initialResult 5 | return map { element in 6 | partialResult = nextPartialResult(partialResult, element) 7 | return partialResult 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Sources/CXTestUtility/TestError.swift: -------------------------------------------------------------------------------- 1 | public enum TestError: Int, Error, Equatable, CustomStringConvertible, Codable { 2 | case e0 3 | case e1 4 | case e2 5 | 6 | public var description: String { 7 | switch self { 8 | case .e0: return "TestError.e0" 9 | case .e1: return "TestError.e1" 10 | case .e2: return "TestError.e2" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/CXUtility/Math.swift: -------------------------------------------------------------------------------- 1 | extension FixedWidthInteger { 2 | 3 | public func multipliedClamping(by rhs: Self) -> Self { 4 | let (value, overflow) = multipliedReportingOverflow(by: rhs) 5 | return overflow ? .max : value 6 | } 7 | 8 | public func addingClamping(by rhs: Self) -> Self { 9 | let (value, overflow) = addingReportingOverflow(rhs) 10 | return overflow ? .max : value 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/CombineX/SchedulerTimeIntervalConvertible.swift: -------------------------------------------------------------------------------- 1 | /// A protocol that provides a scheduler with an expression for relative time. 2 | public protocol SchedulerTimeIntervalConvertible { 3 | 4 | static func seconds(_ s: Int) -> Self 5 | 6 | static func seconds(_ s: Double) -> Self 7 | 8 | static func milliseconds(_ ms: Int) -> Self 9 | 10 | static func microseconds(_ us: Int) -> Self 11 | 12 | static func nanoseconds(_ ns: Int) -> Self 13 | } 14 | -------------------------------------------------------------------------------- /Sources/CombineX/ConnectablePublisher.swift: -------------------------------------------------------------------------------- 1 | /// A publisher that provides an explicit means of connecting and canceling publication. 2 | /// 3 | /// Use `makeConnectable()` to create a `ConnectablePublisher` from any publisher whose failure type is `Never`. 4 | public protocol ConnectablePublisher: Publisher { 5 | 6 | /// Connects to the publisher and returns a `Cancellable` instance with which to cancel publishing. 7 | /// 8 | /// - Returns: A `Cancellable` instance that can be used to cancel publishing. 9 | func connect() -> Cancellable 10 | } 11 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "CombineX", 7 | products: [ 8 | .library(name: "CombineX", targets: ["CombineX", "CXFoundation"]), 9 | ], 10 | dependencies: [ 11 | // TODO: use swift-atomics which requires swift 5.1 12 | ], 13 | targets: [ 14 | .target(name: "CXUtility"), 15 | .target(name: "CombineX", dependencies: ["CXUtility"]), 16 | .target(name: "CXFoundation", dependencies: ["CXUtility", "CombineX"]), 17 | ] 18 | ) 19 | -------------------------------------------------------------------------------- /Sources/CombineX/Internal/WeakHashBox.swift: -------------------------------------------------------------------------------- 1 | class WeakHashBox { 2 | 3 | private(set) weak var value: Value? 4 | 5 | private var id: ObjectIdentifier 6 | 7 | init(_ value: Value) { 8 | self.value = value 9 | self.id = ObjectIdentifier(value) 10 | } 11 | } 12 | 13 | extension WeakHashBox: Equatable, Hashable { 14 | 15 | static func == (lhs: WeakHashBox, rhs: WeakHashBox) -> Bool { 16 | return lhs.id == rhs.id 17 | } 18 | 19 | func hash(into hasher: inout Hasher) { 20 | hasher.combine(id) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/CombineX/Subscriptions/EmptySubscription.swift: -------------------------------------------------------------------------------- 1 | extension Subscriptions { 2 | 3 | /// Returns the 'empty' subscription. 4 | /// 5 | /// Use the empty subscription when you need a `Subscription` that ignores requests and cancellation. 6 | public static let empty: Subscription = EmptySubscription() 7 | } 8 | 9 | extension Subscriptions { 10 | 11 | private final class EmptySubscription: Subscription, Cancellable, CustomCombineIdentifierConvertible { 12 | 13 | func request(_ demand: Subscribers.Demand) { 14 | } 15 | 16 | func cancel() { 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/CombineX/Subscription.swift: -------------------------------------------------------------------------------- 1 | /// A protocol representing the connection of a subscriber to a publisher. 2 | /// 3 | /// Subcriptions are class constrained because a `Subscription` has identity - 4 | /// defined by the moment in time a particular subscriber attached to a publisher. 5 | /// Canceling a `Subscription` must be thread-safe. 6 | /// 7 | /// You can only cancel a `Subscription` once. 8 | /// 9 | /// Canceling a subscription frees up any resources previously allocated by attaching the `Subscriber`. 10 | public protocol Subscription: Cancellable, CustomCombineIdentifierConvertible { 11 | 12 | /// Tells a publisher that it may send more values to the subscriber. 13 | func request(_ demand: Subscribers.Demand) 14 | } 15 | -------------------------------------------------------------------------------- /Sources/CombineX/Internal/PeekableIterator.swift: -------------------------------------------------------------------------------- 1 | struct PeekableIterator: IteratorProtocol { 2 | 3 | private var iterator: AnyIterator 4 | private var nextValue: Element? 5 | 6 | init(_ iterator: I) where I.Element == Element { 7 | self.iterator = AnyIterator(iterator) 8 | self.nextValue = self.iterator.next() 9 | } 10 | 11 | var isEmpty: Bool { 12 | return self.nextValue == nil 13 | } 14 | 15 | func peek() -> Element? { 16 | return self.nextValue 17 | } 18 | 19 | mutating func next() -> Element? { 20 | defer { 21 | self.nextValue = self.iterator.next() 22 | } 23 | return self.nextValue 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/CXFoundation/JSONEncoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if !COCOAPODS 4 | import CombineX 5 | #endif 6 | 7 | extension CXWrappers { 8 | 9 | public final class JSONEncoder: CXWrapper { 10 | 11 | public typealias Base = Foundation.JSONEncoder 12 | 13 | public let base: Base 14 | 15 | public init(wrapping base: Base) { 16 | self.base = base 17 | } 18 | } 19 | } 20 | 21 | extension JSONEncoder: CXWrapping { 22 | 23 | public typealias CX = CXWrappers.JSONEncoder 24 | } 25 | 26 | extension JSONEncoder.CX: CombineX.TopLevelEncoder { 27 | 28 | public func encode(_ value: T) throws -> Data { 29 | return try self.base.encode(value) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Tests/CombineXTests/Publishers/AssertNoFailureSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Nimble 3 | import Quick 4 | 5 | class AssertNoFailureSpec: QuickSpec { 6 | 7 | override func spec() { 8 | 9 | // MARK: - No Failure 10 | describe("No Failure") { 11 | 12 | #if arch(x86_64) && canImport(Darwin) 13 | it("should throw assertion if there is an error") { 14 | let pub = Fail(error: .e0) 15 | .assertNoFailure() 16 | 17 | expect { 18 | pub.subscribeTracingSubscriber(initialDemand: .max(0)) 19 | }.to(throwAssertion()) 20 | } 21 | #endif 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/CXFoundation/JSONDecoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if !COCOAPODS 4 | import CombineX 5 | #endif 6 | 7 | extension CXWrappers { 8 | 9 | public final class JSONDecoder: CXWrapper { 10 | 11 | public typealias Base = Foundation.JSONDecoder 12 | 13 | public let base: Base 14 | 15 | public init(wrapping base: Base) { 16 | self.base = base 17 | } 18 | } 19 | } 20 | 21 | extension JSONDecoder: CXWrapping { 22 | 23 | public typealias CX = CXWrappers.JSONDecoder 24 | } 25 | 26 | extension JSONDecoder.CX: CombineX.TopLevelDecoder { 27 | 28 | public func decode(_ type: T.Type, from: Data) throws -> T { 29 | return try self.base.decode(type, from: from) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Info-carthage.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tests/CXInconsistentTests/README.md: -------------------------------------------------------------------------------- 1 | This target contains inconsistent behaviour between `CombineX` and Apple's `Combine`, including: 2 | 3 | ### SuspiciousBehaviour 4 | 5 | The behaviour of `Combine` is suspicious or inconsistent with documentation. `CombineX` will not attempt to match these behaviour. 6 | 7 | All suspicious tests will ultimately 8 | 9 | 1. Move to Versioning tests if Apple's `Combine` fix it. Or, 10 | 2. Move to FailingTests if Apple change its documentation, or it proves to be our fault. 11 | 12 | ### Versioning 13 | 14 | The behaviour of `Combine` changed over time. `CombineX` follows the latest behaviour. 15 | 16 | ### FailingTests 17 | 18 | The behaviour of `CombineX` is wrong and needs to be fixed. 19 | 20 | ### Fixed 21 | 22 | Was failing, now fixed and consistent with `Combine`. 23 | -------------------------------------------------------------------------------- /Tests/CXInconsistentTests/Fixed/FixedSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Foundation 3 | import Nimble 4 | import Quick 5 | 6 | class FixedSpec: QuickSpec { 7 | 8 | override func spec() { 9 | 10 | // MARK: should fix https://github.com/cx-org/CombineX/issues/44 11 | it("should fix #44") { 12 | let pub = PassthroughSubject() 13 | let sub = pub 14 | .debounce(for: 0.5, scheduler: DispatchQueue.global().cx) 15 | .subscribeTracingSubscriber(initialDemand: .unlimited) 16 | 17 | (0...10).forEach(pub.send) 18 | 19 | expect(sub.eventsWithoutSubscription).to(beEmpty()) 20 | expect(sub.eventsWithoutSubscription).toEventually(equal([.value(10)])) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/CXInconsistentTests/SuspiciousBehaviour/SuspiciousDemandSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Foundation 3 | import Nimble 4 | import Quick 5 | 6 | class SuspiciousDemandSpec: QuickSpec { 7 | 8 | typealias Demand = Subscribers.Demand 9 | 10 | override func spec() { 11 | 12 | // SUSPICIOUS: Doc says "any operation that would result in a negative 13 | // value is clamped to .max(0)", but it will actually crash in Combine. 14 | it("result should clamped to .max(0) as documented") { 15 | #if arch(x86_64) && canImport(Darwin) 16 | expect { 17 | Demand.max(1) - .max(2) 18 | }.toBranch( 19 | combine: throwAssertion(), 20 | cx: equal(.max(0))) 21 | #endif 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /project.yml: -------------------------------------------------------------------------------- 1 | name: CombineX-Carthage 2 | options: 3 | bundleIdPrefix: com.github.cx-org 4 | deploymentTarget: 5 | iOS: 9.0 6 | macOS: !!str '10.10' 7 | tvOS: 9.0 8 | watchOS: 2.0 9 | targets: 10 | CXUtility: 11 | templates: [Framework] 12 | CombineX: 13 | templates: [Framework] 14 | dependencies: 15 | - target: CXUtility_${platform} 16 | CXFoundation: 17 | templates: [Framework] 18 | dependencies: 19 | - target: CXUtility_${platform} 20 | - target: CombineX_${platform} 21 | targetTemplates: 22 | Framework: 23 | type: framework 24 | platform: [iOS, macOS, tvOS, watchOS] 25 | scheme: {} 26 | settings: 27 | APPLICATION_EXTENSION_API_ONLY: true 28 | info: 29 | path: Info-carthage.plist 30 | sources: 31 | - Sources/${target_name} 32 | 33 | -------------------------------------------------------------------------------- /Sources/CXFoundation/PropertyListEncoder.swift: -------------------------------------------------------------------------------- 1 | #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) 2 | 3 | import Foundation 4 | 5 | #if !COCOAPODS 6 | import CombineX 7 | #endif 8 | 9 | extension CXWrappers { 10 | 11 | public final class PropertyListEncoder: CXWrapper { 12 | 13 | public typealias Base = Foundation.PropertyListEncoder 14 | 15 | public let base: Base 16 | 17 | public init(wrapping base: Base) { 18 | self.base = base 19 | } 20 | } 21 | } 22 | 23 | extension PropertyListEncoder: CXWrapping { 24 | 25 | public typealias CX = CXWrappers.PropertyListEncoder 26 | } 27 | 28 | extension PropertyListEncoder.CX: CombineX.TopLevelEncoder { 29 | 30 | public func encode(_ value: T) throws -> Data { 31 | return try self.base.encode(value) 32 | } 33 | } 34 | 35 | #endif 36 | -------------------------------------------------------------------------------- /Sources/CXFoundation/PropertyListDecoder.swift: -------------------------------------------------------------------------------- 1 | #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) 2 | 3 | import Foundation 4 | 5 | #if !COCOAPODS 6 | import CombineX 7 | #endif 8 | 9 | extension CXWrappers { 10 | 11 | public final class PropertyListDecoder: CXWrapper { 12 | 13 | public typealias Base = Foundation.PropertyListDecoder 14 | 15 | public let base: Base 16 | 17 | public init(wrapping base: Base) { 18 | self.base = base 19 | } 20 | } 21 | } 22 | 23 | extension PropertyListDecoder: CXWrapping { 24 | 25 | public typealias CX = CXWrappers.PropertyListDecoder 26 | } 27 | 28 | extension PropertyListDecoder.CX: CombineX.TopLevelDecoder { 29 | 30 | public func decode(_ type: T.Type, from: Data) throws -> T { 31 | return try self.base.decode(type, from: from) 32 | } 33 | } 34 | 35 | #endif 36 | -------------------------------------------------------------------------------- /Sources/CombineX/CombineIdentifier.swift: -------------------------------------------------------------------------------- 1 | #if CX_LOCK_FREE_ATOMIC 2 | @_implementationOnly import Atomics 3 | private let counter = UnsafeAtomic.create(0) 4 | #else 5 | #if !COCOAPODS 6 | import CXUtility 7 | #endif 8 | private let counter = LockedAtomic(0) 9 | #endif 10 | 11 | public struct CombineIdentifier: Hashable, CustomStringConvertible { 12 | 13 | private let value: UInt64 14 | 15 | public init() { 16 | #if CX_LOCK_FREE_ATOMIC 17 | self.value = counter.loadThenWrappingIncrement(ordering: .relaxed) 18 | #else 19 | self.value = counter.loadThenWrappingIncrement() 20 | #endif 21 | } 22 | 23 | public init(_ obj: AnyObject) { 24 | self.value = UInt64(truncatingIfNeeded: UInt(bitPattern: ObjectIdentifier(obj))) 25 | } 26 | 27 | public var description: String { 28 | return "0x" + String(self.value, radix: 16) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Tests/CXInconsistentTests/Versioning/VersioningAssignSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Nimble 3 | import Quick 4 | 5 | class VersioningAssignSpec: QuickSpec { 6 | 7 | class Object { 8 | var value = 0 9 | } 10 | 11 | override func spec() { 12 | 13 | it("should not cancel when receiving completion") { 14 | let obj = Object() 15 | let assign = Subscribers.Assign(object: obj, keyPath: \Object.value) 16 | var cancelled = false 17 | let subscription = TracingSubscription(receiveCancel: { 18 | cancelled = true 19 | }) 20 | assign.receive(subscription: subscription) 21 | assign.receive(completion: .finished) 22 | expect(cancelled).toVersioning([ 23 | .v11_0: beTrue(), 24 | .v12_0: beFalse() 25 | ]) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/CXInconsistentTests/FailingTests/FailingFlatMapSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Nimble 3 | import Quick 4 | 5 | class FailingFlatMapSpec: QuickSpec { 6 | 7 | override func spec() { 8 | 9 | it("should forward unlimited request") { 10 | let subj1 = TracingSubject() 11 | let subj2 = TracingSubject() 12 | let pub = [subj1, subj2].cx.publisher.flatMap { $0 } 13 | let sub = pub.subscribeTracingSubscriber(initialDemand: .unlimited) 14 | 15 | expect(subj1.subscription.demandRecords).toBranch( 16 | combine: equal([.unlimited]), 17 | cx: equal([.max(1)])) 18 | expect(subj2.subscription.demandRecords).toBranch( 19 | combine: equal([.unlimited]), 20 | cx: equal([.max(1)])) 21 | 22 | _ = sub 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Tests/CombineXTests/Publishers/RemoveDuplicatesSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Nimble 3 | import Quick 4 | 5 | class RemoveDuplicatesSpec: QuickSpec { 6 | 7 | override func spec() { 8 | 9 | it("should ignore duplicate value") { 10 | let subject = PassthroughSubject() 11 | let pub = subject.removeDuplicates() 12 | let sub = pub.subscribeTracingSubscriber(initialDemand: .unlimited) 13 | 14 | subject.send(1) 15 | subject.send(1) 16 | subject.send(2) 17 | subject.send(2) 18 | subject.send(1) 19 | subject.send(1) 20 | subject.send(completion: .finished) 21 | 22 | let events = [1, 2, 1].map(TracingSubscriber.Event.value) 23 | let expected = events + [.completion(.finished)] 24 | expect(sub.eventsWithoutSubscription) == expected 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/CXTestUtility/Inconsistent/BranchExpectation.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Nimble 3 | 4 | public extension Expectation { 5 | 6 | func toFail(_ predicate: Predicate, description: String? = nil) { 7 | #if USE_COMBINE 8 | to(predicate, description: description) 9 | #else 10 | toNot(predicate, description: description) 11 | #endif 12 | } 13 | 14 | func toFix(_ predicate: Predicate, description: String? = nil) { 15 | #if USE_COMBINE 16 | toNot(predicate, description: description) 17 | #else 18 | to(predicate, description: description) 19 | #endif 20 | } 21 | 22 | func toBranch(combine combinePredicate: Predicate, cx cxPredicate: Predicate, description: String? = nil) { 23 | #if USE_COMBINE 24 | to(combinePredicate, description: description) 25 | #else 26 | to(cxPredicate, description: description) 27 | #endif 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/CombineX/Cancellable.swift: -------------------------------------------------------------------------------- 1 | /// A protocol indicating that an activity or action may be canceled. 2 | /// 3 | /// Calling `cancel()` frees up any allocated resources. It also stops side effects such as timers, network access, or disk I/O. 4 | public protocol Cancellable { 5 | 6 | /// Cancel the activity. 7 | func cancel() 8 | } 9 | 10 | extension Cancellable { 11 | 12 | /// Stores this Cancellable in the specified collection. 13 | /// Parameters: 14 | /// - collection: The collection to store this Cancellable. 15 | public func store(in collection: inout C) where C.Element == AnyCancellable { 16 | collection.append(AnyCancellable(self)) 17 | } 18 | 19 | /// Stores this Cancellable in the specified set. 20 | /// Parameters: 21 | /// - collection: The set to store this Cancellable. 22 | public func store(in set: inout Set) { 23 | set.insert(AnyCancellable(self)) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Tests/CXFoundationTests/URLSessionSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Foundation 3 | import Nimble 4 | import Quick 5 | 6 | #if canImport(FoundationNetworking) 7 | import FoundationNetworking 8 | #endif 9 | 10 | private let testURL = Foundation.URL(string: "https://github.com/repos/cx-org/CXFoundation/releases/latest")! 11 | 12 | class URLSessionSpec: QuickSpec { 13 | 14 | override func spec() { 15 | 16 | // MARK: 1.1 should receive response from session 17 | it("should receive response from session") { 18 | var response: URLResponse? 19 | let pub = URLSession.shared.cx.dataTaskPublisher(for: testURL) 20 | let sink = pub 21 | .sink(receiveCompletion: { _ in 22 | }, receiveValue: { v in 23 | response = v.response 24 | }) 25 | 26 | expect(response).toEventuallyNot(beNil(), timeout: .seconds(5)) 27 | _ = sink 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/CombineX/CXNamespace.swift: -------------------------------------------------------------------------------- 1 | public protocol CXWrapper { 2 | 3 | associatedtype Base 4 | 5 | var base: Base { get } 6 | 7 | init(wrapping base: Base) 8 | } 9 | 10 | public protocol CXWrapping { 11 | 12 | associatedtype CX 13 | 14 | var cx: CX { get } 15 | } 16 | 17 | public extension CXWrapping where CX: CXWrapper, CX.Base == Self { 18 | 19 | var cx: CX { 20 | return CX(wrapping: self) 21 | } 22 | } 23 | 24 | public enum CXWrappers {} 25 | 26 | // MARK: - Compatible 27 | 28 | // Expected Warning: Redundant conformance constraint 'Self': 'CXWrapper' 29 | // https://bugs.swift.org/browse/SR-11670 30 | public protocol CXSelfWrapping: CXWrapping, CXWrapper where Base == Self, CX == Self {} 31 | 32 | public extension CXSelfWrapping { 33 | 34 | var cx: Self { 35 | return self 36 | } 37 | 38 | var base: Self { 39 | return self 40 | } 41 | 42 | init(wrapping base: Self) { 43 | self = base 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/CombineX/Internal/Extensions/Completion+extensions.swift: -------------------------------------------------------------------------------- 1 | extension Subscribers.Completion { 2 | 3 | func mapError(_ transform: (Failure) -> NewFailure) -> Subscribers.Completion { 4 | switch self { 5 | case .finished: 6 | return .finished 7 | case .failure(let error): 8 | return .failure(transform(error)) 9 | } 10 | } 11 | 12 | var isFinished: Bool { 13 | switch self { 14 | case .finished: 15 | return true 16 | case .failure: 17 | return false 18 | } 19 | } 20 | 21 | var isFailure: Bool { 22 | switch self { 23 | case .finished: 24 | return false 25 | case .failure: 26 | return true 27 | } 28 | } 29 | 30 | var error: Failure? { 31 | switch self { 32 | case .finished: 33 | return nil 34 | case .failure(let error): 35 | return error 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/CombineX/Internal/OptionalProtocol.swift: -------------------------------------------------------------------------------- 1 | #if !COCOAPODS 2 | import CXUtility 3 | #endif 4 | 5 | protocol OptionalProtocol { 6 | 7 | associatedtype Wrapped 8 | 9 | var optional: Wrapped? { 10 | get set 11 | } 12 | } 13 | 14 | extension Optional: OptionalProtocol { 15 | 16 | var optional: Wrapped? { 17 | get { return self } 18 | set { self = newValue } 19 | } 20 | } 21 | 22 | extension Optional { 23 | 24 | func filter(_ isIncluded: (Wrapped) -> Bool) -> Wrapped? { 25 | guard let val = self, isIncluded(val) else { 26 | return nil 27 | } 28 | return val 29 | } 30 | } 31 | 32 | extension LockedAtomic where Value: OptionalProtocol { 33 | 34 | func setIfNil(_ value: Value.Wrapped) -> Bool { 35 | return self.withLockMutating { 36 | if $0.optional == nil { 37 | $0.optional = value 38 | return true 39 | } else { 40 | return false 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/CXTestUtility/Extensions/DispatchQueue+extensions.swift: -------------------------------------------------------------------------------- 1 | import CXUtility 2 | import Foundation 3 | import Dispatch 4 | 5 | extension DispatchQueue { 6 | 7 | public func concurrentPerform(iterations: Int, execute work: (Int) -> Void) { 8 | withoutActuallyEscaping(work) { w in 9 | let g = DispatchGroup() 10 | for i in 0..() 21 | self.setSpecific(key: key, value: ()) 22 | defer { 23 | self.setSpecific(key: key, value: nil) 24 | } 25 | return DispatchQueue.getSpecific(key: key) != nil 26 | } 27 | } 28 | 29 | extension CXWrappers.DispatchQueue.SchedulerTimeType.Stride { 30 | 31 | public var seconds: TimeInterval { 32 | return TimeInterval(magnitude) / TimeInterval(Const.nsec_per_sec) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/CXInconsistentTests/Versioning/VersioningReceiveOnSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Foundation 3 | import Nimble 4 | import Quick 5 | 6 | class VersioningReceiveOnSpec: QuickSpec { 7 | 8 | override func spec() { 9 | 10 | it("should not schedule subscription since iOS 13.3") { 11 | let subject = PassthroughSubject() 12 | let scheduler = DispatchQueue(label: UUID().uuidString).cx 13 | let pub = subject.receive(on: scheduler) 14 | let sub = TracingSubscriber(receiveSubscription: { s in 15 | expect(scheduler.base.isCurrent).toVersioning([ 16 | .v11_0: beTrue(), 17 | .v11_3: beFalse(), 18 | ]) 19 | }) 20 | pub.subscribe(sub) 21 | 22 | expect(sub.subscription).toVersioning([ 23 | .v11_0: beNil(), 24 | .v11_3: beNotNil(), 25 | ]) 26 | expect(sub.subscription).toEventuallyNot(beNil()) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tests/CXInconsistentTests/Versioning/VersioningDelaySpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Foundation 3 | import Nimble 4 | import Quick 5 | 6 | class VersioningDelaySpec: QuickSpec { 7 | 8 | override func spec() { 9 | 10 | it("should not schedule subscription since iOS 13.3") { 11 | let subject = PassthroughSubject() 12 | let scheduler = DispatchQueue(label: UUID().uuidString).cx 13 | let pub = subject.delay(for: .seconds(1), scheduler: scheduler) 14 | let sub = TracingSubscriber(receiveSubscription: { s in 15 | expect(scheduler.base.isCurrent).toVersioning([ 16 | .v11_0: beTrue(), 17 | .v11_3: beFalse(), 18 | ]) 19 | }) 20 | pub.subscribe(sub) 21 | 22 | expect(sub.subscription).toVersioning([ 23 | .v11_0: beNil(), 24 | .v11_3: beNotNil(), 25 | ]) 26 | expect(sub.subscription).toEventuallyNot(beNil()) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tests/CXInconsistentTests/SuspiciousBehaviour/SuspiciousBufferSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Nimble 3 | import Quick 4 | 5 | class SuspiciousBufferSpec: QuickSpec { 6 | 7 | override func spec() { 8 | 9 | // MARK: 1.4 should throw an error when full 10 | it("should throw an error when full") { 11 | let subject = PassthroughSubject() 12 | let pub = subject.buffer(size: 5, prefetch: .byRequest, whenFull: .customError({ TestError.e1 })) 13 | let sub = pub.subscribeTracingSubscriber(initialDemand: .max(5)) 14 | 15 | subject.send(contentsOf: 0..<100) 16 | 17 | // SUSPICIOUS: Apple's combine doesn't receive error. 18 | let valueEvents = (0..<5).map(TracingSubscriber.Event.value) 19 | let expected = valueEvents + [.completion(.failure(.e1))] 20 | expect(sub.eventsWithoutSubscription).toBranch( 21 | combine: equal(valueEvents), 22 | cx: equal(expected)) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Tests/CXInconsistentTests/Versioning/VersioningSinkSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Nimble 3 | import Quick 4 | 5 | class VersioningSinkSpec: QuickSpec { 6 | 7 | override func spec() { 8 | 9 | it("should receive values even if it has received completion") { 10 | let pub = AnyPublisher { s in 11 | _ = s.receive(1) 12 | s.receive(completion: .finished) 13 | _ = s.receive(2) 14 | } 15 | 16 | var events: [TracingSubscriber.Event] = [] 17 | let sink = pub.sink(receiveCompletion: { c in 18 | events.append(.completion(c)) 19 | }, receiveValue: { v in 20 | events.append(.value(v)) 21 | }) 22 | 23 | expect(events).toVersioning([ 24 | .v11_0: equal([.value(1), .completion(.finished), .value(2)]), 25 | .v12_0: equal([.value(1), .completion(.finished)]), 26 | ]) 27 | 28 | _ = sink 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Tests/CombineXTests/Scheduler/ImmediateSchedulerSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Nimble 3 | import Quick 4 | 5 | class ImmediateSchedulerSpec: QuickSpec { 6 | 7 | typealias Time = ImmediateScheduler.SchedulerTimeType 8 | typealias Stride = ImmediateScheduler.SchedulerTimeType.Stride 9 | 10 | override func spec() { 11 | 12 | // MARK: It should have a zero magnitude stride 13 | it("should have a zero magnitude stride") { 14 | 15 | let s0 = Stride.seconds(1) 16 | let s1 = Stride.nanoseconds(2) 17 | 18 | expect(s0.magnitude) == 0 19 | expect(s1.magnitude) == 0 20 | } 21 | 22 | // MARK: It should have a lazy scheduler time 23 | it("should have a lazy scheduler time") { 24 | let time = ImmediateScheduler.shared.now 25 | let advanced = time.advanced(by: .seconds(10)) 26 | 27 | expect(advanced.distance(to: time)) == .seconds(0) 28 | expect((time..: Publisher where DeferredPublisher: Publisher { 3 | 4 | public typealias Output = DeferredPublisher.Output 5 | 6 | public typealias Failure = DeferredPublisher.Failure 7 | 8 | /// The closure to execute when it receives a subscription. 9 | /// 10 | /// The publisher returned by this closure immediately receives the incoming subscription. 11 | public let createPublisher: () -> DeferredPublisher 12 | 13 | /// Creates a deferred publisher. 14 | /// 15 | /// - Parameter createPublisher: The closure to execute when calling `subscribe(_:)`. 16 | public init(createPublisher: @escaping () -> DeferredPublisher) { 17 | self.createPublisher = createPublisher 18 | } 19 | 20 | public func receive(subscriber: S) where DeferredPublisher.Failure == S.Failure, DeferredPublisher.Output == S.Input { 21 | self.createPublisher().receive(subscriber: subscriber) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /CombineX.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = "CombineX" 4 | s.version = "0.4.0" 5 | s.summary = "Open source implementation for Apple's Combine." 6 | s.homepage = "https://github.com/cx-org/CombineX" 7 | s.license = { :type => "MIT", :file => "LICENSE" } 8 | s.authors = { "Quentin Jin" => "luoxiustm@gmail.com", "ddddxxx" => "dengxiang2010@gmail.com" } 9 | 10 | s.swift_versions = ['5.0'] 11 | s.osx.deployment_target = "10.10" 12 | s.ios.deployment_target = "8.0" 13 | s.tvos.deployment_target = "9.0" 14 | s.watchos.deployment_target = "2.0" 15 | 16 | s.source = { :git => "https://github.com/cx-org/CombineX.git", :tag => "#{s.version}" } 17 | 18 | s.subspec "CXUtility" do |ss| 19 | ss.source_files = "Sources/CXUtility/**/*.swift" 20 | end 21 | 22 | s.subspec "Main" do |ss| 23 | ss.source_files = "Sources/CombineX/**/*.swift" 24 | ss.dependency "CombineX/CXUtility" 25 | end 26 | 27 | s.subspec "CXFoundation" do |ss| 28 | ss.source_files = "Sources/CXFoundation/**/*.swift" 29 | ss.dependency "CombineX/Main" 30 | end 31 | 32 | s.default_subspec = 'Main' 33 | 34 | end 35 | -------------------------------------------------------------------------------- /Tests/CombineXTests/Publishers/ReplaceErrorSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Nimble 3 | import Quick 4 | 5 | class ReplaceErrorSpec: QuickSpec { 6 | 7 | override func spec() { 8 | 9 | // MARK: - Relay 10 | describe("Relay") { 11 | 12 | // MARK: 1.1 should send default value if error 13 | it("should send default value if error") { 14 | let pub = Fail(error: .e0).replaceError(with: 1) 15 | let sub = pub.subscribeTracingSubscriber(initialDemand: .unlimited) 16 | expect(sub.eventsWithoutSubscription) == [.value(1), .completion(.finished)] 17 | } 18 | 19 | #if arch(x86_64) && canImport(Darwin) 20 | // MARK: 1.2 should crash when the demand is 0 21 | it("should crash when the demand is 0") { 22 | let pub = Fail(error: .e0).replaceError(with: 1) 23 | expect { 24 | pub.subscribeTracingSubscriber(initialDemand: .max(0)) 25 | }.to(throwAssertion()) 26 | } 27 | #endif 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Tests/CXInconsistentTests/FailingTests/FailingTimerSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Foundation 3 | import Nimble 4 | import Quick 5 | 6 | class FailingTimerSpec: QuickSpec { 7 | 8 | override func spec() { 9 | 10 | context(minimalVersion: .v12_0) { 11 | 12 | it("should not add demands up from multiple subscriber") { 13 | let pub = CXWrappers.Timer.publish(every: 0.1, on: .current, in: .common) 14 | let sub1 = pub.subscribeTracingSubscriber(initialDemand: .max(1)) 15 | let sub2 = pub.subscribeTracingSubscriber(initialDemand: .max(2)) 16 | 17 | let connection = pub.connect() 18 | 19 | RunLoop.current.run(until: Date().addingTimeInterval(1)) 20 | expect(sub1.eventsWithoutSubscription.count).toBranch( 21 | combine: equal(1), 22 | cx: equal(3)) 23 | expect(sub2.eventsWithoutSubscription.count).toBranch( 24 | combine: equal(2), 25 | cx: equal(3)) 26 | connection.cancel() 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Tests/CombineXTests/Publishers/AutoconnectSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Foundation 3 | import Nimble 4 | import Quick 5 | 6 | class AutoconnectSpec: QuickSpec { 7 | 8 | override func spec() { 9 | 10 | // MARK: - Auto Connect 11 | describe("Auto Connect") { 12 | 13 | it("should auto connect and cancel") { 14 | let subject = PassthroughSubject() 15 | let sub = subject 16 | .makeConnectable() 17 | .autoconnect() 18 | .subscribeTracingSubscriber(initialDemand: .unlimited) 19 | 20 | subject.send(1) 21 | subject.send(2) 22 | subject.send(3) 23 | 24 | expect(sub.eventsWithoutSubscription) == [.value(1), .value(2), .value(3)] 25 | 26 | sub.subscription?.cancel() 27 | 28 | subject.send(4) 29 | subject.send(5) 30 | subject.send(6) 31 | 32 | expect(sub.eventsWithoutSubscription) == [.value(1), .value(2), .value(3)] 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/C/MakeConnectable.swift: -------------------------------------------------------------------------------- 1 | extension Publisher where Failure == Never { 2 | 3 | /// Creates a connectable wrapper around the publisher. 4 | /// 5 | /// - Returns: A `ConnectablePublisher` wrapping this publisher. 6 | public func makeConnectable() -> Publishers.MakeConnectable { 7 | return .init(self) 8 | } 9 | } 10 | 11 | extension Publishers { 12 | 13 | public struct MakeConnectable: ConnectablePublisher where Upstream: Publisher { 14 | 15 | public typealias Output = Upstream.Output 16 | 17 | public typealias Failure = Upstream.Failure 18 | 19 | private let multicase: Multicast> 20 | init(_ upstream: Upstream) { 21 | self.multicase = upstream.multicast(subject: PassthroughSubject()) 22 | } 23 | 24 | public func receive(subscriber: S) where Upstream.Failure == S.Failure, Upstream.Output == S.Input { 25 | self.multicase.receive(subscriber: subscriber) 26 | } 27 | 28 | public func connect() -> Cancellable { 29 | return self.multicase.connect() 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/CombineXTests/Publishers/MulticastSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Nimble 3 | import Quick 4 | 5 | class MulticastSpec: QuickSpec { 6 | 7 | override func spec() { 8 | 9 | describe("Relay") { 10 | 11 | // MARK: 1.1 should multicase after connect 12 | it("should multicase after connect") { 13 | let subject = PassthroughSubject() 14 | let pub = subject.multicast(subject: PassthroughSubject()) 15 | 16 | let sub = pub.subscribeTracingSubscriber(initialDemand: .unlimited) 17 | 18 | subject.send(contentsOf: 0..<10) 19 | expect(sub.eventsWithoutSubscription) == [] 20 | 21 | let cancel = pub.connect() 22 | 23 | subject.send(contentsOf: 0..<10) 24 | expect(sub.eventsWithoutSubscription) == (0..<10).map { .value($0) } 25 | 26 | cancel.cancel() 27 | 28 | subject.send(contentsOf: 0..<10) 29 | expect(sub.eventsWithoutSubscription) == (0..<10).map { .value($0) } 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/Combined/Last.swift: -------------------------------------------------------------------------------- 1 | extension Publisher { 2 | 3 | /// Only publishes the last element of a stream, after the stream finishes. 4 | /// - Returns: A publisher that only publishes the last element of a stream. 5 | public func last() -> Publishers.Last { 6 | return .init(upstream: self) 7 | } 8 | } 9 | 10 | extension Publishers.Last: Equatable where Upstream: Equatable {} 11 | 12 | extension Publishers { 13 | 14 | /// A publisher that only publishes the last element of a stream, after the stream finishes. 15 | public struct Last: Publisher { 16 | 17 | public typealias Output = Upstream.Output 18 | 19 | public typealias Failure = Upstream.Failure 20 | 21 | /// The publisher from which this publisher receives elements. 22 | public let upstream: Upstream 23 | 24 | public init(upstream: Upstream) { 25 | self.upstream = upstream 26 | } 27 | 28 | public func receive(subscriber: S) where Upstream.Failure == S.Failure, Upstream.Output == S.Input { 29 | self.upstream 30 | .last { _ in true } 31 | .receive(subscriber: subscriber) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/CXFoundationTests/CoderSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Foundation 3 | import Nimble 4 | import Quick 5 | 6 | class CoderSpec: QuickSpec { 7 | 8 | override func spec() { 9 | 10 | // MARK: 1.1 should encode/decode json as expected 11 | it("should encode/decode json as expected") { 12 | struct User: Codable { 13 | let name: String 14 | } 15 | 16 | let a = User(name: "quentin") 17 | let json = try! JSONEncoder().cx.encode(a) 18 | let b = try! JSONDecoder().cx.decode(User.self, from: json) 19 | 20 | expect(b.name) == a.name 21 | } 22 | 23 | #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) 24 | // MARK: 1.2 should encode/decode plist as expected 25 | it("should encode/decode plist as expected") { 26 | struct User: Codable { 27 | let name: String 28 | } 29 | 30 | let a = User(name: "quentin") 31 | let json = try! PropertyListEncoder().cx.encode(a) 32 | let b = try! PropertyListDecoder().cx.decode(User.self, from: json) 33 | 34 | expect(b.name) == a.name 35 | } 36 | #endif 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/Combined/Encode.swift: -------------------------------------------------------------------------------- 1 | extension Publisher where Output: Encodable { 2 | 3 | /// Encodes the output from upstream using a specified `TopLevelEncoder`. 4 | /// For example, use `JSONEncoder`. 5 | public func encode(encoder: Coder) -> Publishers.Encode where Coder: TopLevelEncoder { 6 | return .init(upstream: self, encoder: encoder) 7 | } 8 | } 9 | 10 | extension Publishers { 11 | 12 | public struct Encode: Publisher where Upstream.Output: Encodable { 13 | 14 | public typealias Failure = Error 15 | 16 | public typealias Output = Coder.Output 17 | 18 | public let upstream: Upstream 19 | 20 | private let encoder: Coder 21 | 22 | public init(upstream: Upstream, encoder: Coder) { 23 | self.upstream = upstream 24 | self.encoder = encoder 25 | } 26 | 27 | public func receive(subscriber: S) where Coder.Output == S.Input, S.Failure == Publishers.Encode.Failure { 28 | self.upstream 29 | .tryMap { 30 | try self.encoder.encode($0) 31 | } 32 | .receive(subscriber: subscriber) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Tests/CXFoundationTests/TimerSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Foundation 3 | import Nimble 4 | import Quick 5 | 6 | class TimerSpec: QuickSpec { 7 | 8 | override func spec() { 9 | 10 | // MARK: 1.1 should not send values before connect 11 | it("should not send values before connect") { 12 | let pub = CXWrappers.Timer.publish(every: 0.1, on: .main, in: .common) 13 | let sub = pub.subscribeTracingSubscriber(initialDemand: .unlimited) 14 | 15 | waitUntil(timeout: .seconds(3)) { done in 16 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) { 17 | done() 18 | expect(sub.eventsWithoutSubscription).to(beEmpty()) 19 | } 20 | } 21 | } 22 | 23 | // MARK: 1.2 should send values repeatedly 24 | it("should send values repeatedly") { 25 | let pub = CXWrappers.Timer.publish(every: 0.1, on: .main, in: .common) 26 | let sub = pub.subscribeTracingSubscriber(initialDemand: .unlimited) 27 | 28 | let connection = pub.connect() 29 | 30 | expect(sub.eventsWithoutSubscription).toEventually(haveCount(4)) 31 | 32 | _ = connection 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/Combined/Decode.swift: -------------------------------------------------------------------------------- 1 | extension Publisher { 2 | 3 | /// Decodes the output from upstream using a specified `TopLevelDecoder`. 4 | /// For example, use `JSONDecoder`. 5 | public func decode(type: Item.Type, decoder: Coder) -> Publishers.Decode where Item: Decodable, Coder: TopLevelDecoder, Output == Coder.Input { 6 | return .init(upstream: self, decoder: decoder) 7 | } 8 | } 9 | 10 | extension Publishers { 11 | 12 | public struct Decode: Publisher where Upstream.Output == Coder.Input { 13 | 14 | public typealias Failure = Error 15 | 16 | public let upstream: Upstream 17 | 18 | private let decoder: Coder 19 | 20 | public init(upstream: Upstream, decoder: Coder) { 21 | self.upstream = upstream 22 | self.decoder = decoder 23 | } 24 | 25 | public func receive(subscriber: S) where Output == S.Input, S.Failure == Publishers.Decode.Failure { 26 | self.upstream 27 | .tryMap { 28 | try self.decoder.decode(Output.self, from: $0) 29 | } 30 | .receive(subscriber: subscriber) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/Combined/First.swift: -------------------------------------------------------------------------------- 1 | extension Publisher { 2 | 3 | /// Publishes the first element of a stream, then finishes. 4 | /// 5 | /// If this publisher doesn’t receive any elements, it finishes without publishing. 6 | /// - Returns: A publisher that only publishes the first element of a stream. 7 | public func first() -> Publishers.First { 8 | return .init(upstream: self) 9 | } 10 | } 11 | 12 | extension Publishers.First: Equatable where Upstream: Equatable {} 13 | 14 | extension Publishers { 15 | 16 | /// A publisher that publishes the first element of a stream, then finishes. 17 | public struct First: Publisher { 18 | 19 | public typealias Output = Upstream.Output 20 | 21 | public typealias Failure = Upstream.Failure 22 | 23 | /// The publisher from which this publisher receives elements. 24 | public let upstream: Upstream 25 | 26 | public init(upstream: Upstream) { 27 | self.upstream = upstream 28 | } 29 | 30 | public func receive(subscriber: S) where Upstream.Failure == S.Failure, Upstream.Output == S.Input { 31 | return self.upstream 32 | .output(at: 0) 33 | .receive(subscriber: subscriber) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/CombineX/Internal/ObserableObjectCache.swift: -------------------------------------------------------------------------------- 1 | #if !COCOAPODS 2 | import CXUtility 3 | #endif 4 | 5 | // Ad-hoc cache for ObservableObject that works on following assumption: 6 | // 7 | // - Once the value is added, it will not be (manually) removed or modified. 8 | class ObservableObjectPublisherCache { 9 | 10 | private var storage: [WeakHashBox: WeakHashBox] = [:] 11 | 12 | private var lock = Lock() 13 | 14 | deinit { 15 | lock.cleanupLock() 16 | } 17 | 18 | private func cleanup() { 19 | for (key, value) in storage where key.value == nil || value.value == nil { 20 | storage.removeValue(forKey: key) 21 | } 22 | } 23 | 24 | func value(for key: Key, make: () throws -> Value) rethrows -> Value { 25 | let wkey = WeakHashBox(key) 26 | if let value = storage[wkey]?.value { 27 | return value 28 | } 29 | lock.lock() 30 | defer { lock.unlock() } 31 | if let value = storage[wkey]?.value { 32 | // avoid double write 33 | return value 34 | } 35 | let value = try make() 36 | if storage.count == storage.capacity { 37 | // will allocate new storage 38 | cleanup() 39 | } 40 | storage[wkey] = WeakHashBox(value) 41 | return value 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tests/CombineXTests/Publishers/ShareSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Nimble 3 | import Quick 4 | 5 | class ShareSpec: QuickSpec { 6 | 7 | override func spec() { 8 | 9 | // MARK: Relay 10 | describe("Relay") { 11 | 12 | it("should share the upstream") { 13 | let subject = PassthroughSubject() 14 | var normalCount = 0 15 | let normal = subject.map { i -> Int in 16 | normalCount += 1 17 | return i 18 | } 19 | _ = normal.subscribeTracingSubscriber(initialDemand: .unlimited) 20 | _ = normal.subscribeTracingSubscriber(initialDemand: .unlimited) 21 | 22 | var shareCount = 0 23 | let share = subject.map { i -> Int in 24 | shareCount += 1 25 | return i 26 | }.share() 27 | 28 | _ = share.subscribeTracingSubscriber(initialDemand: .unlimited) 29 | _ = share.subscribeTracingSubscriber(initialDemand: .unlimited) 30 | 31 | subject.send(1) 32 | subject.send(2) 33 | subject.send(3) 34 | 35 | expect(normalCount) == 6 36 | expect(shareCount) == 3 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/Fail.swift: -------------------------------------------------------------------------------- 1 | /// A publisher that immediately terminates with the specified error. 2 | public struct Fail: Publisher { 3 | 4 | /// Creates a publisher that immediately terminates with the specified failure. 5 | /// 6 | /// - Parameter error: The failure to send when terminating the publisher. 7 | public init(error: Failure) { 8 | self.error = error 9 | } 10 | 11 | /// Creates publisher with the given output type, that immediately terminates with the specified failure. 12 | /// 13 | /// Use this initializer to create a `Fail` publisher that can work with subscribers or publishers that expect a given output type. 14 | /// - Parameters: 15 | /// - outputType: The output type exposed by this publisher. 16 | /// - failure: The failure to send when terminating the publisher. 17 | public init(outputType: Output.Type, failure: Failure) { 18 | self.error = failure 19 | } 20 | 21 | /// The failure to send when terminating the publisher. 22 | public let error: Failure 23 | 24 | public func receive(subscriber: S) where Output == S.Input, Failure == S.Failure { 25 | Result 26 | .failure(self.error) 27 | .cx 28 | .publisher 29 | .receive(subscriber: subscriber) 30 | } 31 | } 32 | 33 | extension Fail: Equatable where Failure: Equatable {} 34 | -------------------------------------------------------------------------------- /Tests/CXInconsistentTests/SuspiciousBehaviour/SuspiciousSwitchToLatestSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Nimble 3 | import Quick 4 | 5 | class SuspiciousSwitchToLatestSpec: QuickSpec { 6 | 7 | override func spec() { 8 | 9 | // MARK: 1.1 should not crash if the child sends more events than initial demand. 10 | xit("should not crash if the child sends more events than initial demand.") { 11 | let subject1 = PassthroughSubject() 12 | 13 | let subject = PassthroughSubject, Never>() 14 | 15 | let pub = subject.switchToLatest() 16 | let sub = pub.subscribeTracingSubscriber(initialDemand: .max(10)) { v in 17 | return [0, 10].contains(v) ? .max(1) : .none 18 | } 19 | 20 | subject.send(subject1) 21 | 22 | (1...10).forEach(subject1.send) 23 | 24 | // TODO: not at macOS 10.15.7, should move to verisoning test 25 | // SUSPICIOUS: Combine will crash here. This should be a bug. 26 | #if arch(x86_64) && canImport(Darwin) 27 | expect { 28 | subject1.send(11) 29 | }.toBranch( 30 | combine: throwAssertion(), 31 | cx: beVoid() 32 | ) 33 | #endif 34 | 35 | _ = sub 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/CombineX/Internal/Extensions/Never+reasons.swift: -------------------------------------------------------------------------------- 1 | enum APIViolation {} 2 | 3 | extension APIViolation { 4 | 5 | static func valueBeforeSubscription(file: StaticString = #file, line: UInt = #line) -> Never { 6 | fatalError("API Violation: received an unexpected value before receiving a Subscription", file: file, line: line) 7 | } 8 | 9 | static func unexpectedCompletion(file: StaticString = #file, line: UInt = #line) -> Never { 10 | fatalError("API Violation: received an unexpected completion", file: file, line: line) 11 | } 12 | } 13 | 14 | extension Never { 15 | 16 | static func requiresImplementation(_ fn: String = #function, file: StaticString = #file, line: UInt = #line) -> Never { 17 | fatalError("\(fn) is not yet implemented", file: file, line: line) 18 | } 19 | 20 | static func requiresConcreteImplementation(_ fn: String = #function, file: StaticString = #file, line: UInt = #line) -> Never { 21 | fatalError("\(fn) must be overriden in subclass", file: file, line: line) 22 | } 23 | 24 | static func unsupported(_ fn: String = #function, file: StaticString = #file, line: UInt = #line) -> Never { 25 | fatalError("\(fn) is not supported on this platform", file: file, line: line) 26 | } 27 | 28 | static func never(_ fn: String = #function, file: StaticString = #file, line: UInt = #line) -> Never { 29 | fatalError("Never", file: file, line: line) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/CombineX/Subject.swift: -------------------------------------------------------------------------------- 1 | /// A publisher that exposes a method for outside callers to publish elements. 2 | /// 3 | /// A subject is a publisher that you can use to ”inject” values into a stream, by calling its `send()` method. This can be useful for adapting existing imperative code to the Combine model. 4 | public protocol Subject: AnyObject, Publisher { 5 | 6 | /// Sends a value to the subscriber. 7 | /// 8 | /// - Parameter value: The value to send. 9 | func send(_ value: Output) 10 | 11 | /// Sends a completion signal to the subscriber. 12 | /// 13 | /// - Parameter completion: A `Completion` instance which indicates whether publishing has finished normally or failed with an error. 14 | func send(completion: Subscribers.Completion) 15 | 16 | /// Provides this Subject an opportunity to establish demand for any new upstream subscriptions (say via, `Publisher.subscribe(_: Subject)` 17 | func send(subscription: Subscription) 18 | } 19 | 20 | extension Subject where Output == Void { 21 | 22 | /// Signals subscribers. 23 | public func send() { 24 | self.send(()) 25 | } 26 | } 27 | 28 | extension Publisher { 29 | 30 | public func subscribe(_ subject: S) -> AnyCancellable where Failure == S.Failure, Output == S.Output { 31 | let sub = SubjectSubscriberBox(subject) 32 | self.subscribe(sub) 33 | return AnyCancellable(sub.cancel) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/Combined/IgnoreOutput.swift: -------------------------------------------------------------------------------- 1 | extension Publisher { 2 | 3 | /// Ingores all upstream elements, but passes along a completion state (finished or failed). 4 | /// 5 | /// The output type of this publisher is `Never`. 6 | /// - Returns: A publisher that ignores all upstream elements. 7 | public func ignoreOutput() -> Publishers.IgnoreOutput { 8 | return .init(upstream: self) 9 | } 10 | } 11 | 12 | extension Publishers.IgnoreOutput: Equatable where Upstream: Equatable {} 13 | 14 | extension Publishers { 15 | 16 | /// A publisher that ignores all upstream elements, but passes along a completion state (finish or failed). 17 | public struct IgnoreOutput: Publisher { 18 | 19 | public typealias Output = Never 20 | 21 | public typealias Failure = Upstream.Failure 22 | 23 | /// The publisher from which this publisher receives elements. 24 | public let upstream: Upstream 25 | 26 | public init(upstream: Upstream) { 27 | self.upstream = upstream 28 | } 29 | 30 | public func receive(subscriber: S) where Upstream.Failure == S.Failure, S.Input == Publishers.IgnoreOutput.Output { 31 | self.upstream 32 | .filter { _ in false } 33 | .map { _ in Never.never() } 34 | .receive(subscriber: subscriber) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/CombineX/Internal/Extensions/Result+extensions.swift: -------------------------------------------------------------------------------- 1 | extension Result { 2 | 3 | func tryMap(_ transform: (Success) throws -> NewSuccess) -> Result { 4 | switch self { 5 | case .success(let success): 6 | do { 7 | return .success(try transform(success)) 8 | } catch { 9 | return .failure(error) 10 | } 11 | case .failure(let error): 12 | return .failure(error) 13 | } 14 | } 15 | 16 | func replaceError(with output: Success) -> Result { 17 | switch self { 18 | case let .success(success): 19 | return .success(success) 20 | case .failure: 21 | return .success(output) 22 | } 23 | } 24 | 25 | var erasedError: Result { 26 | switch self { 27 | case let .success(success): 28 | return .success(success) 29 | case let .failure(error): 30 | return .failure(error) 31 | } 32 | } 33 | } 34 | 35 | extension Result where Failure == Never { 36 | 37 | var success: Success { 38 | switch self { 39 | case let .success(success): 40 | return success 41 | } 42 | } 43 | } 44 | 45 | extension Result where Success == Never { 46 | 47 | var error: Failure { 48 | switch self { 49 | case let .failure(error): 50 | return error 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/Combined/Drop.swift: -------------------------------------------------------------------------------- 1 | extension Publisher { 2 | 3 | /// Omits the specified number of elements before republishing subsequent elements. 4 | /// 5 | /// - Parameter count: The number of elements to omit. 6 | /// - Returns: A publisher that does not republish the first `count` elements. 7 | public func dropFirst(_ count: Int = 1) -> Publishers.Drop { 8 | return .init(upstream: self, count: count) 9 | } 10 | } 11 | 12 | extension Publishers.Drop: Equatable where Upstream: Equatable {} 13 | 14 | extension Publishers { 15 | 16 | /// A publisher that omits a specified number of elements before republishing later elements. 17 | public struct Drop: Publisher { 18 | 19 | public typealias Output = Upstream.Output 20 | 21 | public typealias Failure = Upstream.Failure 22 | 23 | /// The publisher from which this publisher receives elements. 24 | public let upstream: Upstream 25 | 26 | /// The number of elements to drop. 27 | public let count: Int 28 | 29 | public init(upstream: Upstream, count: Int) { 30 | self.upstream = upstream 31 | self.count = count 32 | } 33 | 34 | public func receive(subscriber: S) where Upstream.Failure == S.Failure, Upstream.Output == S.Input { 35 | return self.upstream 36 | .output(in: self.count...) 37 | .receive(subscriber: subscriber) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Tests/CombineXTests/Publishers/EmptySpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Nimble 3 | import Quick 4 | 5 | class EmptySpec: QuickSpec { 6 | 7 | override func spec() { 8 | 9 | // MARK: - Send Values 10 | describe("Send Values") { 11 | 12 | // MARK: 1.1 should send completion immediately 13 | it("should send completion immediately") { 14 | let pub = Empty() 15 | let sub = pub.subscribeTracingSubscriber(initialDemand: .unlimited) 16 | 17 | expect(sub.eventsWithoutSubscription) == [.completion(.finished)] 18 | } 19 | 20 | // MARK: 1.2 should send nothing 21 | it("should send nothing") { 22 | let pub = Empty(completeImmediately: false) 23 | let sub = pub.subscribeTracingSubscriber(initialDemand: .unlimited) 24 | expect(sub.eventsWithoutSubscription) == [] 25 | } 26 | } 27 | 28 | // MARK: - Equal 29 | describe("Equal") { 30 | 31 | // MARK: 2.1 should equal if 'completeImmediately' are the same 32 | it("should equal if 'completeImmediately' are the same") { 33 | 34 | let e1 = Empty() 35 | let e2 = Empty() 36 | let e3 = Empty(completeImmediately: false) 37 | 38 | expect(e1) == e2 39 | expect(e1) != e3 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tests/CXInconsistentTests/Versioning/VersioningFutureSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Nimble 3 | import Quick 4 | 5 | class VersioningFutureSpec: QuickSpec { 6 | 7 | override func spec() { 8 | 9 | it("should send failure even without demand") { 10 | let future = Future { promise in 11 | promise(.failure(.e0)) 12 | } 13 | 14 | let sub = future.subscribeTracingSubscriber(initialDemand: nil) 15 | 16 | expect(sub.events.first?.isSubscription) == true 17 | expect(sub.eventsWithoutSubscription).toVersioning([ 18 | .v11_0: beEmpty(), 19 | .v12_0: equal([.completion(.failure(.e0))]), 20 | ]) 21 | } 22 | 23 | it("should not leak subscription") { 24 | var promise: Future.Promise? 25 | let future = Future { promise = $0 } 26 | weak var weakSubscription: AnyObject? 27 | 28 | do { 29 | let sub = future.subscribeTracingSubscriber(initialDemand: .max(1)) 30 | weakSubscription = sub.subscription as AnyObject? 31 | 32 | promise?(.success(1)) 33 | 34 | sub.releaseSubscription() 35 | } 36 | 37 | // SUSPICIOUS: Combine leaks subscription 38 | expect(weakSubscription).toVersioning([ 39 | .v11_0: beNotNil(), 40 | .v12_0: beNil(), 41 | ]) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/CombineX/Subscriber.swift: -------------------------------------------------------------------------------- 1 | /// A protocol that declares a type that can receive input from a publisher. 2 | public protocol Subscriber: CustomCombineIdentifierConvertible { 3 | 4 | /// The kind of values this subscriber receives. 5 | associatedtype Input 6 | 7 | /// The kind of errors this subscriber might receive. 8 | /// 9 | /// Use `Never` if this `Subscriber` cannot receive errors. 10 | associatedtype Failure: Error 11 | 12 | /// Tells the subscriber that it has successfully subscribed to the publisher and may request items. 13 | /// 14 | /// Use the received `Subscription` to request items from the publisher. 15 | /// - Parameter subscription: A subscription that represents the connection between publisher and subscriber. 16 | func receive(subscription: Subscription) 17 | 18 | /// Tells the subscriber that the publisher has produced an element. 19 | /// 20 | /// - Parameter input: The published element. 21 | /// - Returns: A `Demand` instance indicating how many more elements the subcriber expects to receive. 22 | func receive(_ input: Input) -> Subscribers.Demand 23 | 24 | /// Tells the subscriber that the publisher has completed publishing, either normally or with an error. 25 | /// 26 | /// - Parameter completion: A `Completion` case indicating whether publishing completed normally or with an error. 27 | func receive(completion: Subscribers.Completion) 28 | } 29 | 30 | extension Subscriber where Input == Void { 31 | 32 | public func receive() -> Subscribers.Demand { 33 | return self.receive(()) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Tests/CombineXTests/Publishers/ReplaceEmptySpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Nimble 3 | import Quick 4 | 5 | class ReplaceEmptySpec: QuickSpec { 6 | 7 | override func spec() { 8 | 9 | // MARK: - Relay 10 | describe("Relay") { 11 | 12 | // MARK: 1.1 should send default value if empty 13 | it("should send default value if empty") { 14 | let pub = Empty().replaceEmpty(with: 1) 15 | let sub = pub.subscribeTracingSubscriber(initialDemand: .unlimited) 16 | 17 | expect(sub.eventsWithoutSubscription) == [.value(1), .completion(.finished)] 18 | } 19 | 20 | // MARK: 1.2 should not send default value if not empty 21 | it("should not send default value if not empty") { 22 | let pub = Just(0).replaceEmpty(with: 1) 23 | let sub = pub.subscribeTracingSubscriber(initialDemand: .unlimited) 24 | 25 | expect(sub.eventsWithoutSubscription) == [.value(0), .completion(.finished)] 26 | } 27 | 28 | #if arch(x86_64) && canImport(Darwin) 29 | // MARK: 1.3 should throw assertion when the demand is 0 30 | it("should throw assertion when the demand is 0") { 31 | let pub = Empty().replaceEmpty(with: 1) 32 | 33 | expect { 34 | pub.subscribeTracingSubscriber(initialDemand: .max(0)) 35 | }.to(throwAssertion()) 36 | } 37 | #endif 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/Combined/Count.swift: -------------------------------------------------------------------------------- 1 | #if !COCOAPODS 2 | import CXUtility 3 | #endif 4 | 5 | extension Publisher { 6 | 7 | /// Publishes the number of elements received from the upstream publisher. 8 | /// 9 | /// - Returns: A publisher that consumes all elements until the upstream publisher finishes, then emits a single 10 | /// value with the total number of elements received. 11 | public func count() -> Publishers.Count { 12 | return .init(upstream: self) 13 | } 14 | } 15 | 16 | extension Publishers.Count: Equatable where Upstream: Equatable {} 17 | 18 | extension Publishers { 19 | 20 | /// A publisher that publishes the number of elements received from the upstream publisher. 21 | public struct Count: Publisher { 22 | 23 | public typealias Output = Int 24 | 25 | public typealias Failure = Upstream.Failure 26 | 27 | /// The publisher from which this publisher receives elements. 28 | public let upstream: Upstream 29 | 30 | public init(upstream: Upstream) { 31 | self.upstream = upstream 32 | } 33 | 34 | public func receive(subscriber: S) where Upstream.Failure == S.Failure, S.Input == Publishers.Count.Output { 35 | self.upstream 36 | .reduce(LockedAtomic(0)) { counter, _ in 37 | _ = counter.loadThenWrappingIncrement() 38 | return counter 39 | } 40 | .map { 41 | $0.load() 42 | } 43 | .receive(subscriber: subscriber) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/CXFoundationTests/NotificationCenterSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Foundation 3 | import Nimble 4 | import Quick 5 | 6 | class NotificationCenterSpec: QuickSpec { 7 | 8 | override func spec() { 9 | 10 | // MARK: 1.1 should send as many notications as demand 11 | it("should send as many notications as demand") { 12 | let name = Notification.Name(rawValue: UUID().uuidString) 13 | let pub = NotificationCenter.default.cx.publisher(for: name) 14 | let sub = pub.subscribeTracingSubscriber(initialDemand: .unlimited) 15 | 16 | NotificationCenter.default.post(name: name, object: nil) 17 | NotificationCenter.default.post(name: name, object: nil) 18 | NotificationCenter.default.post(name: name, object: nil) 19 | 20 | expect(sub.eventsWithoutSubscription).toEventually(haveCount(3)) 21 | } 22 | 23 | // MARK: 1.2 should stop sending values after cancel 24 | it("should stop sending values after cancel") { 25 | let name = Notification.Name(rawValue: UUID().uuidString) 26 | let pub = NotificationCenter.default.cx.publisher(for: name) 27 | let sub = pub.subscribeTracingSubscriber(initialDemand: .unlimited) 28 | 29 | sub.subscription?.cancel() 30 | 31 | NotificationCenter.default.post(name: name, object: nil) 32 | NotificationCenter.default.post(name: name, object: nil) 33 | NotificationCenter.default.post(name: name, object: nil) 34 | 35 | expect(sub.eventsWithoutSubscription).toEventually(beEmpty()) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/C/Future.swift: -------------------------------------------------------------------------------- 1 | #if !COCOAPODS 2 | import CXUtility 3 | #endif 4 | 5 | /// A publisher that eventually produces one value and then finishes or fails. 6 | public final class Future: Publisher { 7 | 8 | public typealias Promise = (Result) -> Void 9 | 10 | private let subject = PassthroughSubject() 11 | 12 | private let lock = Lock() 13 | private var result: Result? 14 | 15 | public init(_ attemptToFulfill: @escaping (@escaping Promise) -> Void) { 16 | attemptToFulfill(self.complete) 17 | } 18 | 19 | deinit { 20 | lock.cleanupLock() 21 | } 22 | 23 | public final func receive(subscriber: S) where Output == S.Input, Failure == S.Failure { 24 | self.lock.lock() 25 | if let result = self.result { 26 | self.lock.unlock() 27 | result.cx.publisher.receive(subscriber: subscriber) 28 | return 29 | } 30 | 31 | self.subject.receive(subscriber: subscriber) 32 | self.lock.unlock() 33 | } 34 | 35 | private func complete(_ result: Result) { 36 | self.lock.lock() 37 | guard self.result == nil else { 38 | self.lock.unlock() 39 | return 40 | } 41 | 42 | self.result = result 43 | self.lock.unlock() 44 | 45 | switch result { 46 | case .success(let output): 47 | self.subject.send(output) 48 | self.subject.send(completion: .finished) 49 | case .failure(let error): 50 | self.subject.send(completion: .failure(error)) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - cyclomatic_complexity 3 | - file_length 4 | - function_body_length 5 | - identifier_name 6 | - trailing_comma 7 | - trailing_whitespace 8 | - type_name 9 | opt_in_rules: 10 | - anyobject_protocol 11 | - array_init 12 | - closure_end_indentation 13 | - closure_spacing 14 | - collection_alignment 15 | - contains_over_filter_count 16 | - contains_over_filter_is_empty 17 | - contains_over_first_not_nil 18 | - contains_over_range_nil_comparison 19 | - convenience_type 20 | - discouraged_object_literal 21 | - empty_collection_literal 22 | - empty_count 23 | - empty_string 24 | - explicit_init 25 | - first_where 26 | - flatmap_over_map_reduce 27 | - identical_operands 28 | - implicitly_unwrapped_optional 29 | - is_disjoint 30 | - joined_default_parameter 31 | - last_where 32 | - legacy_multiple 33 | - legacy_random 34 | - literal_expression_end_indentation 35 | - modifier_order 36 | - multiline_arguments 37 | - multiline_function_chains 38 | - multiline_literal_brackets 39 | - multiline_parameters 40 | - multiple_closures_with_trailing_closure 41 | - nimble_operator 42 | - object_literal 43 | - operator_usage_whitespace 44 | - overridden_super_call 45 | - override_in_extension 46 | - pattern_matching_keywords 47 | - prohibited_super_call 48 | - quick_discouraged_call 49 | - reduce_into 50 | - single_test_class 51 | - sorted_first_last 52 | - sorted_imports 53 | - static_operator 54 | - toggle_bool 55 | - trailing_closure 56 | - unneeded_parentheses_in_closure_argument 57 | - untyped_error_in_catch 58 | - unused_import 59 | - vertical_parameter_alignment_on_call 60 | - vertical_whitespace_closing_braces 61 | - yoda_condition 62 | excluded: 63 | - .build 64 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/Combined/Collect.swift: -------------------------------------------------------------------------------- 1 | extension Publisher { 2 | 3 | /// Collects all received elements, and emits a single array of the collection when the upstream publisher finishes. 4 | /// 5 | /// If the upstream publisher fails with an error, this publisher forwards the error to the downstream receiver instead of sending its output. 6 | /// This publisher requests an unlimited number of elements from the upstream publisher. It only sends the collected array to its downstream after a request whose demand is greater than 0 items. 7 | /// Note: This publisher uses an unbounded amount of memory to store the received values. 8 | /// 9 | /// - Returns: A publisher that collects all received items and returns them as an array upon completion. 10 | public func collect() -> Publishers.Collect { 11 | return .init(upstream: self) 12 | } 13 | } 14 | 15 | extension Publishers.Collect: Equatable where Upstream: Equatable {} 16 | 17 | extension Publishers { 18 | 19 | /// A publisher that buffers items. 20 | public struct Collect: Publisher { 21 | 22 | public typealias Output = [Upstream.Output] 23 | 24 | public typealias Failure = Upstream.Failure 25 | 26 | /// The publisher that this publisher receives elements from. 27 | public let upstream: Upstream 28 | 29 | public init(upstream: Upstream) { 30 | self.upstream = upstream 31 | } 32 | 33 | public func receive(subscriber: S) where Upstream.Failure == S.Failure, S.Input == [Upstream.Output] { 34 | self.upstream 35 | .collect(.max) 36 | .receive(subscriber: subscriber) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/CombineX/Subscribers/Completion.swift: -------------------------------------------------------------------------------- 1 | extension Subscribers { 2 | 3 | /// A signal that a publisher doesn’t produce additional elements, either due to normal completion or an error. 4 | /// 5 | /// - finished: The publisher finished normally. 6 | /// - failure: The publisher stopped publishing due to the indicated error. 7 | public enum Completion { 8 | 9 | case finished 10 | 11 | case failure(Failure) 12 | } 13 | } 14 | 15 | extension Subscribers.Completion: Equatable where Failure: Equatable {} 16 | 17 | extension Subscribers.Completion: Hashable where Failure: Hashable {} 18 | 19 | extension Subscribers.Completion { 20 | 21 | private enum CodingKeys: CodingKey { 22 | case success 23 | case error 24 | } 25 | } 26 | 27 | extension Subscribers.Completion: Encodable where Failure: Encodable { 28 | 29 | public func encode(to encoder: Encoder) throws { 30 | var container = encoder.container(keyedBy: CodingKeys.self) 31 | switch self { 32 | case .finished: 33 | try container.encode(true, forKey: .success) 34 | case .failure(let e): 35 | try container.encode(false, forKey: .success) 36 | try container.encode(e, forKey: .error) 37 | } 38 | } 39 | } 40 | 41 | extension Subscribers.Completion: Decodable where Failure: Decodable { 42 | 43 | public init(from decoder: Decoder) throws { 44 | let container = try decoder.container(keyedBy: CodingKeys.self) 45 | 46 | if try container.decode(Bool.self, forKey: .success) { 47 | self = .finished 48 | } else { 49 | self = .failure(try container.decode(Failure.self, forKey: .error)) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/CXTestUtility/Predicate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Nimble 3 | 4 | public func beAllEqual() -> Predicate 5 | where S.Iterator.Element == T { 6 | return Predicate.simple("element be all equal") { actualExpression in 7 | guard let actualValue = try actualExpression.evaluate() else { 8 | return .fail 9 | } 10 | var actualGenerator = actualValue.makeIterator() 11 | if let first = actualGenerator.next() { 12 | while let next = actualGenerator.next() { 13 | if next != first { 14 | return .doesNotMatch 15 | } 16 | } 17 | } 18 | return .matches 19 | } 20 | } 21 | 22 | public func beNotNil() -> Predicate { 23 | return Predicate.simpleNilable("be not nil") { actualExpression in 24 | let actualValue = try actualExpression.evaluate() 25 | return PredicateStatus(bool: actualValue != nil) 26 | } 27 | } 28 | 29 | public func beNotIdenticalTo(_ expected: Any?) -> Predicate { 30 | return Predicate.define { actualExpression in 31 | let actual = try actualExpression.evaluate() as AnyObject? 32 | 33 | let bool = actual !== (expected as AnyObject?) && actual !== nil 34 | return PredicateResult( 35 | bool: bool, 36 | message: .expectedCustomValueTo( 37 | "be not identical to \(identityAsString(expected))", 38 | actual: "\(identityAsString(actual))" 39 | ) 40 | ) 41 | } 42 | } 43 | 44 | private func identityAsString(_ value: Any?) -> String { 45 | let anyObject = value as AnyObject? 46 | if let value = anyObject { 47 | return NSString(format: "<%p>", unsafeBitCast(value, to: Int.self)).description 48 | } else { 49 | return "nil" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Tests/CombineXTests/Publishers/MergeSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Nimble 3 | import Quick 4 | 5 | class MergeSpec: QuickSpec { 6 | 7 | override func spec() { 8 | 9 | // MARK: - Relay 10 | describe("Relay") { 11 | 12 | // MARK: It should merge 8 upstreams 13 | it("should merge 8 upstreams") { 14 | let subjects = (0..<8).map { _ in PassthroughSubject() } 15 | let pub = Publishers.Merge8( 16 | subjects[0], subjects[1], subjects[2], subjects[3], 17 | subjects[4], subjects[5], subjects[6], subjects[7] 18 | ) 19 | 20 | let sub = pub.subscribeTracingSubscriber(initialDemand: .unlimited) 21 | 22 | 100.times { 23 | subjects.randomElement()!.send($0) 24 | } 25 | 26 | let events = (0..<100).map(TracingSubscriber.Event.value) 27 | expect(sub.eventsWithoutSubscription) == events 28 | } 29 | 30 | // MARK: It should merge many upstreams 31 | it("should merge many upstreams") { 32 | let subjects = (0..<9).map { _ in PassthroughSubject() } 33 | let pub = Publishers.MergeMany(subjects) 34 | 35 | let sub = pub.subscribeTracingSubscriber(initialDemand: .unlimited) 36 | 37 | 100.times { 38 | subjects.randomElement()!.send($0) 39 | } 40 | 41 | let events = (0..<100).map(TracingSubscriber.Event.value) 42 | expect(sub.eventsWithoutSubscription) == events 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/CombineXTests/Publishers/BufferSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Nimble 3 | import Quick 4 | 5 | class BufferSpec: QuickSpec { 6 | 7 | override func spec() { 8 | 9 | // MARK: - Relay-ByRequest 10 | describe("Relay-ByRequest") { 11 | 12 | // MARK: 1.2 should drop oldest 13 | it("should drop oldest") { 14 | let subject = PassthroughSubject() 15 | let pub = subject.buffer(size: 5, prefetch: .byRequest, whenFull: .dropOldest) 16 | let sub = pub.subscribeTracingSubscriber(initialDemand: .max(5)) 17 | 18 | subject.send(contentsOf: 0..<11) 19 | 20 | sub.subscription?.request(.max(5)) 21 | 22 | let expected = (Array(0..<5) + Array(6..<11)) 23 | .map(TracingSubscriber.Event.value) 24 | expect(sub.eventsWithoutSubscription) == expected 25 | } 26 | 27 | // MARK: 1.3 should drop newest 28 | it("should drop newest") { 29 | let subject = PassthroughSubject() 30 | let pub = subject.buffer(size: 5, prefetch: .byRequest, whenFull: .dropNewest) 31 | let sub = pub.subscribeTracingSubscriber(initialDemand: .max(5)) 32 | 33 | subject.send(contentsOf: 0..<11) 34 | 35 | sub.subscription?.request(.max(5)) 36 | 37 | let expected = (0..<10).map(TracingSubscriber.Event.value) 38 | expect(sub.eventsWithoutSubscription) == expected 39 | } 40 | } 41 | 42 | // MARK: - Realy-KeepFull 43 | describe("KeepFull") { 44 | 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/Combined/Filter.swift: -------------------------------------------------------------------------------- 1 | extension Publisher { 2 | 3 | public func filter(_ isIncluded: @escaping (Output) -> Bool) -> Publishers.Filter { 4 | return .init(upstream: self, isIncluded: isIncluded) 5 | } 6 | } 7 | 8 | extension Publishers.Filter { 9 | 10 | public func tryFilter(_ isIncluded: @escaping (Publishers.Filter.Output) throws -> Bool) -> Publishers.TryFilter { 11 | let newIsIncluded: (Upstream.Output) throws -> Bool = { 12 | let lhs = self.isIncluded($0) 13 | let rhs = try isIncluded($0) 14 | return lhs && rhs 15 | } 16 | return self.upstream.tryFilter(newIsIncluded) 17 | } 18 | } 19 | 20 | extension Publishers { 21 | 22 | /// A publisher that republishes all elements that match a provided closure. 23 | public struct Filter: Publisher { 24 | 25 | public typealias Output = Upstream.Output 26 | 27 | public typealias Failure = Upstream.Failure 28 | 29 | /// The publisher from which this publisher receives elements. 30 | public let upstream: Upstream 31 | 32 | /// A closure that indicates whether to republish an element. 33 | public let isIncluded: (Upstream.Output) -> Bool 34 | 35 | public init(upstream: Upstream, isIncluded: @escaping (Upstream.Output) -> Bool) { 36 | self.upstream = upstream 37 | self.isIncluded = isIncluded 38 | } 39 | 40 | public func receive(subscriber: S) where Upstream.Failure == S.Failure, Upstream.Output == S.Input { 41 | self.upstream 42 | .compactMap { 43 | self.isIncluded($0) ? $0 : nil 44 | } 45 | .receive(subscriber: subscriber) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Tests/CombineXTests/CombineIdentifierSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import CXUtility 3 | import Foundation 4 | import Dispatch 5 | import Nimble 6 | import Quick 7 | 8 | class CombineIdentifierSpec: QuickSpec { 9 | 10 | override func spec() { 11 | 12 | // MARK: - Unique 13 | describe("Unique") { 14 | 15 | // MARK: 1.1 should be unique to each other 16 | it("should be unique to each other") { 17 | let set = LockedAtomic>([]) 18 | let g = DispatchGroup() 19 | for _ in 0..<100 { 20 | let id = CombineIdentifier() 21 | DispatchQueue.global().async(group: g) { 22 | _ = set.withLockMutating { $0.insert(id) } 23 | } 24 | } 25 | g.wait() 26 | 27 | expect(set.load().count) == 100 28 | } 29 | 30 | // MARK: 1.2 should use object's address as id 31 | it("should use object's address as id") { 32 | let obj = NSObject() 33 | 34 | let id1 = CombineIdentifier(obj) 35 | let id2 = CombineIdentifier(obj) 36 | 37 | expect(id1) == id2 38 | } 39 | } 40 | } 41 | } 42 | 43 | import XCTest 44 | 45 | class CombineIdentifierTests: XCTestCase { 46 | 47 | func testPerformance() { 48 | measure { 49 | let g = DispatchGroup() 50 | for _ in 0..<1000 { 51 | DispatchQueue.global().async(group: g) { 52 | for _ in 0..<1000 { 53 | blackHole(CombineIdentifier()) 54 | } 55 | } 56 | } 57 | g.wait() 58 | } 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/Combined/LastWhere.swift: -------------------------------------------------------------------------------- 1 | extension Publisher { 2 | 3 | /// Only publishes the last element of a stream that satisfies a predicate closure, after the stream finishes. 4 | /// - Parameter predicate: A closure that takes an element as its parameter and returns a Boolean value indicating whether to publish the element. 5 | /// - Returns: A publisher that only publishes the last element satisfying the given predicate. 6 | public func last(where predicate: @escaping (Output) -> Bool) -> Publishers.LastWhere { 7 | return .init(upstream: self, predicate: predicate) 8 | } 9 | } 10 | 11 | extension Publishers { 12 | 13 | /// A publisher that only publishes the last element of a stream that satisfies a predicate closure, once the stream finishes. 14 | public struct LastWhere: Publisher { 15 | 16 | public typealias Output = Upstream.Output 17 | 18 | public typealias Failure = Upstream.Failure 19 | 20 | /// The publisher from which this publisher receives elements. 21 | public let upstream: Upstream 22 | 23 | /// The closure that determines whether to publish an element. 24 | public let predicate: (Upstream.Output) -> Bool 25 | 26 | public init(upstream: Upstream, predicate: @escaping (Publishers.LastWhere.Output) -> Bool) { 27 | self.upstream = upstream 28 | self.predicate = predicate 29 | } 30 | 31 | public func receive(subscriber: S) where Upstream.Failure == S.Failure, Upstream.Output == S.Input { 32 | self.upstream 33 | .tryLast(where: self.predicate) 34 | .mapError { 35 | $0 as! Failure 36 | } 37 | .receive(subscriber: subscriber) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/CombineX/Internal/RelayState.swift: -------------------------------------------------------------------------------- 1 | enum RelayState { 2 | 3 | case waiting 4 | 5 | case relaying(Subscription) 6 | 7 | case completed 8 | } 9 | 10 | extension RelayState { 11 | 12 | var isWaiting: Bool { 13 | switch self { 14 | case .waiting: return true 15 | default: return false 16 | } 17 | } 18 | 19 | var isRelaying: Bool { 20 | switch self { 21 | case .relaying: return true 22 | default: return false 23 | } 24 | } 25 | 26 | var isCompleted: Bool { 27 | switch self { 28 | case .completed: return true 29 | default: return false 30 | } 31 | } 32 | 33 | var subscription: Subscription? { 34 | switch self { 35 | case .relaying(let s): return s 36 | default: return nil 37 | } 38 | } 39 | } 40 | 41 | extension RelayState { 42 | 43 | func preconditionValue(file: StaticString = #file, line: UInt = #line) { 44 | if self.isWaiting { 45 | fatalError("Received value before receiving subscription", file: file, line: line) 46 | } 47 | } 48 | 49 | func preconditionCompletion(file: StaticString = #file, line: UInt = #line) { 50 | if self.isWaiting { 51 | fatalError("Received completion before receiving subscription", file: file, line: line) 52 | } 53 | } 54 | } 55 | 56 | extension RelayState { 57 | 58 | mutating func relay(_ subscription: Subscription) -> Bool { 59 | guard self.isWaiting else { return false } 60 | self = .relaying(subscription) 61 | return true 62 | } 63 | 64 | mutating func complete() -> Subscription? { 65 | defer { 66 | self = .completed 67 | } 68 | return self.subscription 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Tests/CombineXTests/Publishers/MeasureIntervalSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Foundation 3 | import Nimble 4 | import Quick 5 | 6 | class MeasureIntervalSpec: QuickSpec { 7 | 8 | override func spec() { 9 | 10 | // MARK: Measure Interval 11 | describe("Measure Interval") { 12 | 13 | // MARK: 1.1 should measure interval as expected 14 | it("should measure interval as expected") { 15 | let subject = PassthroughSubject() 16 | 17 | let pub = subject.measureInterval(using: DispatchQueue.main.cx) 18 | var t = Date() 19 | var dts: [TimeInterval] = [] 20 | let sub = TracingSubscriber(receiveSubscription: { s in 21 | s.request(.unlimited) 22 | t = Date() 23 | }, receiveValue: { _ in 24 | dts.append(-t.timeIntervalSinceNow) 25 | t = Date() 26 | return .none 27 | }) 28 | 29 | pub.subscribe(sub) 30 | 31 | Thread.sleep(forTimeInterval: 0.2) 32 | subject.send(1) 33 | 34 | Thread.sleep(forTimeInterval: 0.1) 35 | subject.send(1) 36 | 37 | subject.send(completion: .finished) 38 | 39 | expect(sub.eventsWithoutSubscription).to(haveCount(dts.count + 1)) 40 | expect(sub.eventsWithoutSubscription.last) == .completion(.finished) 41 | for (event, dt) in zip(sub.eventsWithoutSubscription.dropLast(), dts) { 42 | expect(event.value?.seconds).to(beCloseTo(dt, within: 0.1)) 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/Combined/DropWhile.swift: -------------------------------------------------------------------------------- 1 | extension Publisher { 2 | 3 | /// Omits elements from the upstream publisher until a given closure returns false, before republishing all remaining elements. 4 | /// 5 | /// - Parameter predicate: A closure that takes an element as a parameter and returns a Boolean 6 | /// value indicating whether to drop the element from the publisher’s output. 7 | /// - Returns: A publisher that skips over elements until the provided closure returns `false`. 8 | public func drop(while predicate: @escaping (Output) -> Bool) -> Publishers.DropWhile { 9 | return .init(upstream: self, predicate: predicate) 10 | } 11 | } 12 | 13 | extension Publishers { 14 | 15 | /// A publisher that omits elements from an upstream publisher until a given closure returns false. 16 | public struct DropWhile: Publisher { 17 | 18 | public typealias Output = Upstream.Output 19 | 20 | public typealias Failure = Upstream.Failure 21 | 22 | /// The publisher from which this publisher receives elements. 23 | public let upstream: Upstream 24 | 25 | /// The closure that indicates whether to drop the element. 26 | public let predicate: (Upstream.Output) -> Bool 27 | 28 | public init(upstream: Upstream, predicate: @escaping (Publishers.DropWhile.Output) -> Bool) { 29 | self.upstream = upstream 30 | self.predicate = predicate 31 | } 32 | 33 | public func receive(subscriber: S) where Upstream.Failure == S.Failure, Upstream.Output == S.Input { 34 | self.upstream 35 | .tryDrop(while: self.predicate) 36 | .mapError { 37 | $0 as! Failure 38 | } 39 | .receive(subscriber: subscriber) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/CombineX/Internal/DemandState.swift: -------------------------------------------------------------------------------- 1 | enum DemandState { 2 | 3 | case waiting 4 | 5 | case demanding(Subscribers.Demand) 6 | 7 | case completed 8 | } 9 | 10 | extension DemandState { 11 | 12 | var isWaiting: Bool { 13 | switch self { 14 | case .waiting: return true 15 | default: return false 16 | } 17 | } 18 | 19 | var isDemanding: Bool { 20 | switch self { 21 | case .demanding: return true 22 | default: return false 23 | } 24 | } 25 | 26 | var isCompleted: Bool { 27 | switch self { 28 | case .completed: return true 29 | default: return false 30 | } 31 | } 32 | 33 | var demand: Subscribers.Demand? { 34 | switch self { 35 | case .demanding(let d): return d 36 | default: return nil 37 | } 38 | } 39 | } 40 | 41 | extension DemandState { 42 | 43 | /// - Returns: `true` if the previous state is not `completed`. 44 | mutating func complete() -> Bool { 45 | defer { 46 | self = .completed 47 | } 48 | return !self.isCompleted 49 | } 50 | } 51 | 52 | extension DemandState { 53 | 54 | typealias Demands = (old: Subscribers.Demand, new: Subscribers.Demand) 55 | 56 | mutating func add(_ demand: Subscribers.Demand) -> Demands? { 57 | guard let old = self.demand else { 58 | return nil 59 | } 60 | let new = old + demand 61 | self = .demanding(new) 62 | return (old, new) 63 | } 64 | 65 | mutating func sub(_ demand: Subscribers.Demand) -> Demands? { 66 | guard let old = self.demand else { 67 | return nil 68 | } 69 | let new = old - demand 70 | self = .demanding(new) 71 | return (old, new) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CwlCatchException", 6 | "repositoryURL": "https://github.com/mattgallagher/CwlCatchException.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "682841464136f8c66e04afe5dbd01ab51a3a56f2", 10 | "version": "2.1.0" 11 | } 12 | }, 13 | { 14 | "package": "CwlPreconditionTesting", 15 | "repositoryURL": "https://github.com/mattgallagher/CwlPreconditionTesting.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "02b7a39a99c4da27abe03cab2053a9034379639f", 19 | "version": "2.0.0" 20 | } 21 | }, 22 | { 23 | "package": "Nimble", 24 | "repositoryURL": "https://github.com/Quick/Nimble.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "af1730dde4e6c0d45bf01b99f8a41713ce536790", 28 | "version": "9.2.0" 29 | } 30 | }, 31 | { 32 | "package": "Quick", 33 | "repositoryURL": "https://github.com/Quick/Quick.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "8cce6acd38f965f5baa3167b939f86500314022b", 37 | "version": "3.1.2" 38 | } 39 | }, 40 | { 41 | "package": "Semver", 42 | "repositoryURL": "https://github.com/ddddxxx/Semver.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "6094e6f23a02b52b5d211fd114a4750c4f3ecef3", 46 | "version": "0.2.1" 47 | } 48 | }, 49 | { 50 | "package": "swift-atomics", 51 | "repositoryURL": "https://github.com/apple/swift-atomics.git", 52 | "state": { 53 | "branch": null, 54 | "revision": "3e95ba32cd1b4c877f6163e8eea54afc4e63bf9f", 55 | "version": "0.0.3" 56 | } 57 | } 58 | ] 59 | }, 60 | "version": 1 61 | } 62 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/Combined/FirstWhere.swift: -------------------------------------------------------------------------------- 1 | extension Publisher { 2 | 3 | /// Publishes the first element of a stream to satisfy a predicate closure, then finishes. 4 | /// 5 | /// The publisher ignores all elements after the first. If this publisher doesn’t receive any elements, it finishes without publishing. 6 | /// - Parameter predicate: A closure that takes an element as a parameter and returns a Boolean value that indicates whether to publish the element. 7 | /// - Returns: A publisher that only publishes the first element of a stream that satifies the predicate. 8 | public func first(where predicate: @escaping (Output) -> Bool) -> Publishers.FirstWhere { 9 | return .init(upstream: self, predicate: predicate) 10 | } 11 | } 12 | 13 | extension Publishers { 14 | 15 | /// A publisher that only publishes the first element of a stream to satisfy a predicate closure. 16 | public struct FirstWhere: Publisher { 17 | 18 | public typealias Output = Upstream.Output 19 | 20 | public typealias Failure = Upstream.Failure 21 | 22 | /// The publisher from which this publisher receives elements. 23 | public let upstream: Upstream 24 | 25 | /// The closure that determines whether to publish an element. 26 | public let predicate: (Upstream.Output) -> Bool 27 | 28 | public init(upstream: Upstream, predicate: @escaping (Publishers.FirstWhere.Output) -> Bool) { 29 | self.upstream = upstream 30 | self.predicate = predicate 31 | } 32 | 33 | public func receive(subscriber: S) where Upstream.Failure == S.Failure, Upstream.Output == S.Input { 34 | return self.upstream 35 | .filter(self.predicate) 36 | .output(at: 0) 37 | .receive(subscriber: subscriber) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/CXTestUtility/TestTimeline.swift: -------------------------------------------------------------------------------- 1 | import CXUtility 2 | 3 | public class TestTimeline: CustomStringConvertible { 4 | 5 | public class var tolerance: Context.SchedulerTimeType.Stride { 6 | return .seconds(0.01) 7 | } 8 | 9 | private let context: Context 10 | private let _records: LockedAtomic<[Context.SchedulerTimeType]> 11 | 12 | private init(context: Context, records: [Context.SchedulerTimeType]) { 13 | self.context = context 14 | self._records = LockedAtomic(records) 15 | } 16 | 17 | public convenience init(context: Context) { 18 | self.init(context: context, records: []) 19 | } 20 | 21 | public var records: [Context.SchedulerTimeType] { 22 | return self._records.load() 23 | } 24 | 25 | public func record() { 26 | self._records.withLockMutating { 27 | $0.append(self.context.now) 28 | } 29 | } 30 | 31 | public var description: String { 32 | return self.records.description 33 | } 34 | } 35 | 36 | public extension TestTimeline { 37 | 38 | func delayed(_ interval: Context.SchedulerTimeType.Stride) -> TestTimeline { 39 | return .init( 40 | context: self.context, 41 | records: self.records 42 | .map { 43 | $0.advanced(by: interval) 44 | } 45 | ) 46 | } 47 | 48 | func isCloseTo(to other: TestTimeline, tolerance: Context.SchedulerTimeType.Stride = TestTimeline.tolerance) -> Bool { 49 | let recordsA = self.records 50 | let recordsB = other.records 51 | 52 | guard recordsA.count == recordsB.count else { 53 | return false 54 | } 55 | for (a, b) in zip(recordsA, recordsB) { 56 | if Swift.abs(a.distance(to: b)) > tolerance { 57 | return false 58 | } 59 | } 60 | return true 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/Empty.swift: -------------------------------------------------------------------------------- 1 | /// A publisher that never publishes any values, and optionally finishes immediately. 2 | /// 3 | /// You can create a ”Never” publisher — one which never sends values and never finishes or fails — with the initializer `Empty(completeImmediately: false)`. 4 | public struct Empty: Publisher, Equatable { 5 | 6 | /// Creates an empty publisher. 7 | /// 8 | /// - Parameter completeImmediately: A Boolean value that indicates whether the publisher should immediately finish. 9 | public init(completeImmediately: Bool = true) { 10 | self.completeImmediately = completeImmediately 11 | } 12 | 13 | /// Creates an empty publisher with the given completion behavior and output and failure types. 14 | /// 15 | /// Use this initializer to connect the empty publisher to subscribers or other publishers that have specific output and failure types. 16 | /// - Parameters: 17 | /// - completeImmediately: A Boolean value that indicates whether the publisher should immediately finish. 18 | /// - outputType: The output type exposed by this publisher. 19 | /// - failureType: The failure type exposed by this publisher. 20 | public init(completeImmediately: Bool = true, outputType: Output.Type, failureType: Failure.Type) { 21 | self.completeImmediately = completeImmediately 22 | } 23 | 24 | /// A Boolean value that indicates whether the publisher immediately sends a completion. 25 | /// 26 | /// If `true`, the publisher finishes immediately after sending a subscription to the subscriber. If `false`, it never completes. 27 | public let completeImmediately: Bool 28 | 29 | public func receive(subscriber: S) where Output == S.Input, Failure == S.Failure { 30 | subscriber.receive(subscription: Subscriptions.empty) 31 | if completeImmediately { 32 | subscriber.receive(completion: .finished) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/C/Autoconnect.swift: -------------------------------------------------------------------------------- 1 | extension ConnectablePublisher { 2 | 3 | /// Automates the process of connecting or disconnecting from this connectable publisher. 4 | /// 5 | /// Use `autoconnect()` to simplify working with `ConnectablePublisher` instances, such as those created with `makeConnectable()`. 6 | /// 7 | /// let autoconnectedPublisher = somePublisher 8 | /// .makeConnectable() 9 | /// .autoconnect() 10 | /// .subscribe(someSubscriber) 11 | /// 12 | /// - Returns: A publisher which automatically connects to its upstream connectable publisher. 13 | public func autoconnect() -> Publishers.Autoconnect { 14 | return .init(upstream: self) 15 | } 16 | } 17 | 18 | extension Publishers { 19 | 20 | /// A publisher that automatically connects and disconnects from this connectable publisher. 21 | public class Autoconnect: Publisher where Upstream: ConnectablePublisher { 22 | 23 | public typealias Output = Upstream.Output 24 | 25 | public typealias Failure = Upstream.Failure 26 | 27 | /// The publisher from which this publisher receives elements. 28 | public final let upstream: Upstream 29 | 30 | public init(upstream: Upstream) { 31 | self.upstream = upstream 32 | } 33 | 34 | public func receive(subscriber: S) where Upstream.Failure == S.Failure, Upstream.Output == S.Input { 35 | var cancel: Cancellable? 36 | self.upstream 37 | .handleEvents( 38 | receiveSubscription: { _ in 39 | cancel = self.upstream.connect() 40 | }, receiveCancel: { 41 | cancel?.cancel() 42 | cancel = nil 43 | } 44 | ) 45 | .receive(subscriber: subscriber) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/Combined/PrefixWhile.swift: -------------------------------------------------------------------------------- 1 | extension Publisher { 2 | 3 | /// Republishes elements while a predicate closure indicates publishing should continue. 4 | /// 5 | /// The publisher finishes when the closure returns `false`. 6 | /// 7 | /// - Parameter predicate: A closure that takes an element as its parameter and returns a Boolean value indicating whether publishing should continue. 8 | /// - Returns: A publisher that passes through elements until the predicate indicates publishing should finish. 9 | public func prefix(while predicate: @escaping (Output) -> Bool) -> Publishers.PrefixWhile { 10 | return .init(upstream: self, predicate: predicate) 11 | } 12 | } 13 | 14 | extension Publishers { 15 | 16 | /// A publisher that republishes elements while a predicate closure indicates publishing should continue. 17 | public struct PrefixWhile: Publisher { 18 | 19 | public typealias Output = Upstream.Output 20 | 21 | public typealias Failure = Upstream.Failure 22 | 23 | /// The publisher from which this publisher receives elements. 24 | public let upstream: Upstream 25 | 26 | /// The closure that determines whether whether publishing should continue. 27 | public let predicate: (Upstream.Output) -> Bool 28 | 29 | public init(upstream: Upstream, predicate: @escaping (Publishers.PrefixWhile.Output) -> Bool) { 30 | self.upstream = upstream 31 | self.predicate = predicate 32 | } 33 | 34 | public func receive(subscriber: S) where Upstream.Failure == S.Failure, Upstream.Output == S.Input { 35 | self.upstream 36 | .tryPrefix(while: self.predicate) 37 | .mapError { 38 | $0 as! Failure 39 | } 40 | .receive(subscriber: subscriber) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/CombineX/AnyCancellable.swift: -------------------------------------------------------------------------------- 1 | /// A type-erasing cancellable object that executes a provided closure when canceled. 2 | /// 3 | /// Subscriber implementations can use this type to provide a “cancellation token” that makes it possible for a caller to cancel a publisher, but not to use the `Subscription` object to request items. 4 | /// An AnyCancellable instance automatically calls `cancel()` when deinitialized. 5 | public final class AnyCancellable: Cancellable, Hashable { 6 | 7 | private var cancelBody: (() -> Void)? 8 | 9 | /// Initializes the cancellable object with the given cancel-time closure. 10 | /// 11 | /// - Parameter cancel: A closure that the `cancel()` method executes. 12 | public init(_ cancel: @escaping () -> Void) { 13 | self.cancelBody = cancel 14 | } 15 | 16 | public init(_ canceller: C) { 17 | self.cancelBody = canceller.cancel 18 | } 19 | 20 | public final func cancel() { 21 | self.cancelBody?() 22 | self.cancelBody = nil 23 | } 24 | 25 | deinit { 26 | self.cancelBody?() 27 | } 28 | 29 | public final func hash(into hasher: inout Hasher) { 30 | hasher.combine(ObjectIdentifier(self)) 31 | } 32 | 33 | public static func == (lhs: AnyCancellable, rhs: AnyCancellable) -> Bool { 34 | return lhs === rhs 35 | } 36 | } 37 | 38 | extension AnyCancellable { 39 | 40 | /// Stores this AnyCancellable in the specified collection. 41 | /// Parameters: 42 | /// - collection: The collection to store this AnyCancellable. 43 | public final func store(in collection: inout C) where C.Element == AnyCancellable { 44 | collection.append(self) 45 | } 46 | 47 | /// Stores this AnyCancellable in the specified set. 48 | /// Parameters: 49 | /// - collection: The set to store this AnyCancellable. 50 | public final func store(in set: inout Set) { 51 | set.insert(self) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/Combined/ContainsWhere.swift: -------------------------------------------------------------------------------- 1 | extension Publisher { 2 | 3 | /// Publishes a Boolean value upon receiving an element that satisfies the predicate closure. 4 | /// 5 | /// This operator consumes elements produced from the upstream publisher until the upstream publisher produces a matching element. 6 | /// - Parameter predicate: A closure that takes an element as its parameter and returns a Boolean value indicating whether the element satisfies the closure’s comparison logic. 7 | /// - Returns: A publisher that emits the Boolean value `true` when the upstream publisher emits a matching value. 8 | public func contains(where predicate: @escaping (Output) -> Bool) -> Publishers.ContainsWhere { 9 | return .init(upstream: self, predicate: predicate) 10 | } 11 | } 12 | 13 | extension Publishers { 14 | 15 | /// A publisher that emits a Boolean value upon receiving an element that satisfies the predicate closure. 16 | public struct ContainsWhere: Publisher { 17 | 18 | public typealias Output = Bool 19 | 20 | public typealias Failure = Upstream.Failure 21 | 22 | /// The publisher from which this publisher receives elements. 23 | public let upstream: Upstream 24 | 25 | /// The closure that determines whether the publisher should consider an element as a match. 26 | public let predicate: (Upstream.Output) -> Bool 27 | 28 | public init(upstream: Upstream, predicate: @escaping (Upstream.Output) -> Bool) { 29 | self.upstream = upstream 30 | self.predicate = predicate 31 | } 32 | 33 | public func receive(subscriber: S) where Upstream.Failure == S.Failure, S.Input == Publishers.ContainsWhere.Output { 34 | self.upstream 35 | .first(where: self.predicate) 36 | .map { _ in true } 37 | .receive(subscriber: subscriber) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/C/Share.swift: -------------------------------------------------------------------------------- 1 | extension Publisher { 2 | 3 | /// Returns a publisher as a class instance. 4 | /// 5 | /// The downstream subscriber receieves elements and completion states unchanged from the 6 | /// upstream publisher. Use this operator when you want to use reference semantics, such as storing a 7 | /// publisher instance in a property. 8 | /// 9 | /// - Returns: A class instance that republishes its upstream publisher. 10 | public func share() -> Publishers.Share { 11 | return .init(upstream: self) 12 | } 13 | } 14 | 15 | extension Publishers { 16 | 17 | /// A publisher implemented as a class, which otherwise behaves like its upstream publisher. 18 | public final class Share: Publisher, Equatable where Upstream: Publisher { 19 | 20 | public typealias Output = Upstream.Output 21 | 22 | public typealias Failure = Upstream.Failure 23 | 24 | public final let upstream: Upstream 25 | 26 | private lazy var pub = self.upstream 27 | .multicast(subject: PassthroughSubject()) 28 | .autoconnect() 29 | 30 | public init(upstream: Upstream) { 31 | self.upstream = upstream 32 | } 33 | 34 | public final func receive(subscriber: S) where Upstream.Failure == S.Failure, Upstream.Output == S.Input { 35 | self.pub.receive(subscriber: subscriber) 36 | } 37 | 38 | /// Returns a Boolean value indicating whether two values are equal. 39 | /// 40 | /// Equality is the inverse of inequality. For any values `a` and `b`, 41 | /// `a == b` implies that `a != b` is `false`. 42 | /// 43 | /// - Parameters: 44 | /// - lhs: A value to compare. 45 | /// - rhs: Another value to compare. 46 | public static func == (lhs: Publishers.Share, rhs: Publishers.Share) -> Bool { 47 | return lhs === rhs 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/Combined/Contains.swift: -------------------------------------------------------------------------------- 1 | extension Publishers.Contains: Equatable where Upstream: Equatable {} 2 | 3 | extension Publisher where Output: Equatable { 4 | 5 | /// Publishes a Boolean value upon receiving an element equal to the argument. 6 | /// 7 | /// The contains publisher consumes all received elements until the upstream publisher produces a 8 | /// matching element. At that point, it emits `true` and finishes normally. If the upstream finishes 9 | /// normally without producing a matching element, this publisher emits `false`, then finishes. 10 | /// 11 | /// - Parameter output: An element to match against. 12 | /// - Returns: A publisher that emits the Boolean value `true` when the upstream publisher emits a matching value. 13 | public func contains(_ output: Output) -> Publishers.Contains { 14 | return .init(upstream: self, output: output) 15 | } 16 | } 17 | 18 | extension Publishers { 19 | 20 | /// A publisher that emits a Boolean value when a specified element is received from its upstream publisher. 21 | public struct Contains: Publisher where Upstream: Publisher, Upstream.Output: Equatable { 22 | 23 | public typealias Output = Bool 24 | 25 | public typealias Failure = Upstream.Failure 26 | 27 | /// The publisher from which this publisher receives elements. 28 | public let upstream: Upstream 29 | 30 | /// The element to scan for in the upstream publisher. 31 | public let output: Upstream.Output 32 | 33 | public init(upstream: Upstream, output: Upstream.Output) { 34 | self.upstream = upstream 35 | self.output = output 36 | } 37 | 38 | public func receive(subscriber: S) where Upstream.Failure == S.Failure, S.Input == Publishers.Contains.Output { 39 | self.upstream 40 | .contains { 41 | $0 == self.output 42 | } 43 | .receive(subscriber: subscriber) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/Combined/TryFirstWhere.swift: -------------------------------------------------------------------------------- 1 | extension Publisher { 2 | 3 | /// Publishes the first element of a stream to satisfy a throwing predicate closure, then finishes. 4 | /// 5 | /// The publisher ignores all elements after the first. If this publisher doesn’t receive any elements, it 6 | /// finishes without publishing. If the predicate closure throws, the publisher fails with an error. 7 | /// 8 | /// - Parameter predicate: A closure that takes an element as a parameter and returns a 9 | /// Boolean value that indicates whether to publish the element. 10 | /// - Returns: A publisher that only publishes the first element of a stream that satifies the predicate. 11 | public func tryFirst(where predicate: @escaping (Output) throws -> Bool) -> Publishers.TryFirstWhere { 12 | return .init(upstream: self, predicate: predicate) 13 | } 14 | } 15 | 16 | extension Publishers { 17 | 18 | /// A publisher that only publishes the first element of a stream to satisfy a throwing predicate closure. 19 | public struct TryFirstWhere: Publisher { 20 | 21 | public typealias Output = Upstream.Output 22 | 23 | public typealias Failure = Error 24 | 25 | /// The publisher from which this publisher receives elements. 26 | public let upstream: Upstream 27 | 28 | /// The error-throwing closure that determines whether to publish an element. 29 | public let predicate: (Upstream.Output) throws -> Bool 30 | 31 | public init(upstream: Upstream, predicate: @escaping (Publishers.TryFirstWhere.Output) throws -> Bool) { 32 | self.upstream = upstream 33 | self.predicate = predicate 34 | } 35 | 36 | public func receive(subscriber: S) where Upstream.Output == S.Input, S.Failure == Publishers.TryFirstWhere.Failure { 37 | return self.upstream 38 | .tryFilter(self.predicate) 39 | .output(at: 0) 40 | .receive(subscriber: subscriber) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/CombineX/Publisher.swift: -------------------------------------------------------------------------------- 1 | /// Declares that a type can transmit a sequence of values over time. 2 | /// 3 | /// There are four kinds of messages: 4 | /// subscription - A connection between `Publisher` and `Subscriber`. 5 | /// value - An element in the sequence. 6 | /// error - The sequence ended with an error (`.failure(e)`). 7 | /// complete - The sequence ended successfully (`.finished`). 8 | /// 9 | /// Both `.failure` and `.finished` are terminal messages. 10 | /// 11 | /// You can summarize these possibilities with a regular expression: 12 | /// value*(error|finished)? 13 | /// 14 | /// Every `Publisher` must adhere to this contract. 15 | public protocol Publisher { 16 | 17 | /// The kind of values published by this publisher. 18 | associatedtype Output 19 | 20 | /// The kind of errors this publisher might publish. 21 | /// 22 | /// Use `Never` if this `Publisher` does not publish errors. 23 | associatedtype Failure: Error 24 | 25 | /// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)` 26 | /// 27 | /// - SeeAlso: `subscribe(_:)` 28 | /// - Parameters: 29 | /// - subscriber: The subscriber to attach to this `Publisher`. 30 | /// once attached it can begin to receive values. 31 | func receive(subscriber: S) where Failure == S.Failure, Output == S.Input 32 | } 33 | 34 | extension Publisher { 35 | 36 | /// Attaches the specified subscriber to this publisher. 37 | /// 38 | /// Always call this function instead of `receive(subscriber:)`. 39 | /// Adopters of `Publisher` must implement `receive(subscriber:)`. The implementation of `subscribe(_:)` in this extension calls through to `receive(subscriber:)`. 40 | /// - SeeAlso: `receive(subscriber:)` 41 | /// - Parameters: 42 | /// - subscriber: The subscriber to attach to this `Publisher`. After attaching, the subscriber can start to receive values. 43 | public func subscribe(_ subscriber: S) where Failure == S.Failure, Output == S.Input { 44 | self.receive(subscriber: subscriber) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/CombineXTests/Publishers/PrefixUntilOutputSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Nimble 3 | import Quick 4 | 5 | class PrefixUntilOutputSpec: QuickSpec { 6 | 7 | override func spec() { 8 | 9 | // MARK: - Relay 10 | describe("Relay") { 11 | 12 | // MARK: 1.1 should relay until other sends a value 13 | it("should relay until other sends a value") { 14 | 15 | let pub0 = PassthroughSubject() 16 | let pub1 = PassthroughSubject() 17 | 18 | let pub = pub0.prefix(untilOutputFrom: pub1) 19 | let sub = pub.subscribeTracingSubscriber(initialDemand: .unlimited) 20 | 21 | pub0.send(contentsOf: 0..<10) 22 | pub1.send(-1) 23 | 24 | for i in 10..<20 { 25 | pub0.send(i) 26 | } 27 | 28 | let valueEvents = (0..<10).map(TracingSubscriber.Event.value) 29 | let expected = valueEvents + [.completion(.finished)] 30 | expect(sub.eventsWithoutSubscription) == expected 31 | } 32 | 33 | // MARK: 1.2 should complete when other complete 34 | it("should complete when other complete") { 35 | 36 | let pub0 = PassthroughSubject() 37 | let pub1 = PassthroughSubject() 38 | 39 | let pub = pub0.prefix(untilOutputFrom: pub1) 40 | let sub = pub.subscribeTracingSubscriber(initialDemand: .unlimited) 41 | 42 | pub0.send(contentsOf: 0..<10) 43 | pub1.send(completion: .failure(.e0)) 44 | for i in 10..<20 { 45 | pub0.send(i) 46 | } 47 | 48 | let expected = (0..<20).map(TracingSubscriber.Event.value) 49 | expect(sub.eventsWithoutSubscription) == expected 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/Combined/TryLastWhere.swift: -------------------------------------------------------------------------------- 1 | extension Publisher { 2 | 3 | /// Only publishes the last element of a stream that satisfies a error-throwing predicate closure, after the stream finishes. 4 | /// 5 | /// If the predicate closure throws, the publisher fails with the thrown error. 6 | /// - Parameter predicate: A closure that takes an element as its parameter and returns a Boolean value indicating whether to publish the element. 7 | /// - Returns: A publisher that only publishes the last element satisfying the given predicate. 8 | public func tryLast(where predicate: @escaping (Output) throws -> Bool) -> Publishers.TryLastWhere { 9 | return .init(upstream: self, predicate: predicate) 10 | } 11 | } 12 | 13 | extension Publishers { 14 | 15 | /// A publisher that only publishes the last element of a stream that satisfies a error-throwing predicate closure, once the stream finishes. 16 | public struct TryLastWhere: Publisher { 17 | 18 | public typealias Output = Upstream.Output 19 | 20 | public typealias Failure = Error 21 | 22 | /// The publisher from which this publisher receives elements. 23 | public let upstream: Upstream 24 | 25 | /// The error-throwing closure that determines whether to publish an element. 26 | public let predicate: (Upstream.Output) throws -> Bool 27 | 28 | public init(upstream: Upstream, predicate: @escaping (Publishers.TryLastWhere.Output) throws -> Bool) { 29 | self.upstream = upstream 30 | self.predicate = predicate 31 | } 32 | public func receive(subscriber: S) where Upstream.Output == S.Input, S.Failure == Publishers.TryLastWhere.Failure { 33 | self.upstream 34 | .tryReduce(nil as Output?) { 35 | if try self.predicate($1) { 36 | return $1 37 | } 38 | return $0 39 | } 40 | .compactMap { $0 } 41 | .receive(subscriber: subscriber) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/Combined/TryContainsWhere.swift: -------------------------------------------------------------------------------- 1 | extension Publisher { 2 | 3 | /// Publishes a Boolean value upon receiving an element that satisfies the throwing predicate closure. 4 | /// 5 | /// This operator consumes elements produced from the upstream publisher until the upstream publisher produces a matching element. If the closure throws, the stream fails with an error. 6 | /// - Parameter predicate: A closure that takes an element as its parameter and returns a Boolean value indicating whether the element satisfies the closure’s comparison logic. 7 | /// - Returns: A publisher that emits the Boolean value `true` when the upstream publisher emits a matching value. 8 | public func tryContains(where predicate: @escaping (Output) throws -> Bool) -> Publishers.TryContainsWhere { 9 | return .init(upstream: self, predicate: predicate) 10 | } 11 | } 12 | 13 | extension Publishers { 14 | /// A publisher that emits a Boolean value upon receiving an element that satisfies the throwing predicate closure. 15 | public struct TryContainsWhere: Publisher { 16 | 17 | public typealias Output = Bool 18 | 19 | public typealias Failure = Error 20 | 21 | /// The publisher from which this publisher receives elements. 22 | public let upstream: Upstream 23 | 24 | /// The error-throwing closure that determines whether this publisher should emit a `true` element. 25 | public let predicate: (Upstream.Output) throws -> Bool 26 | 27 | public init(upstream: Upstream, predicate: @escaping (Upstream.Output) throws -> Bool) { 28 | self.upstream = upstream 29 | self.predicate = predicate 30 | } 31 | 32 | public func receive(subscriber: S) where S.Failure == Publishers.TryContainsWhere.Failure, S.Input == Publishers.TryContainsWhere.Output { 33 | self.upstream 34 | .tryFirst(where: self.predicate) 35 | .map { _ in 36 | true 37 | } 38 | .receive(subscriber: subscriber) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/CXFoundation/NSObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if !COCOAPODS 4 | import CombineX 5 | #endif 6 | 7 | extension CXWrappers { 8 | 9 | open class NSObject: CXWrapper { 10 | 11 | public let base: Base 12 | 13 | public required init(wrapping base: Base) { 14 | self.base = base 15 | } 16 | } 17 | } 18 | 19 | // The naïve conformance of NSObject to CXWrapping looks like this: 20 | // extension CXWrapping where Self: Foundation.NSObject { 21 | // public typealias CX = CXWrappers.NSObject 22 | // } 23 | // 24 | // extension NSObject: CXWrapping { } 25 | // 26 | // But that produces a cascade of errors, starting with this: 27 | // 28 | // .../CombineX/Sources/CXFoundation/JSONDecoder.swift:22:1: error: type 'JSONDecoder' does not conform to protocol 'CXWrapping' 29 | // extension JSONDecoder: CXWrapping { 30 | // ^ 31 | // .../CombineX/Sources/CXNamespace/CXNamespace.swift:12:20: note: multiple matching types named 'CX' 32 | // associatedtype CX 33 | // ^ 34 | // .../CombineX/Sources/CXFoundation/JSONDecoder.swift:24:22: note: possibly intended match 35 | // public typealias CX = CXWrappers.JSONDecoder 36 | // ^ 37 | // .../CombineX/Sources/CXFoundation/NSObject.swift:21:22: note: possibly intended match 38 | // public typealias CX = CXWrappers.NSObject 39 | // 40 | // I think the root problem might be that JSONDecoder (and several other types) somehow secretly subclass NSObject on Apple platforms, and so it tries to pick up two different definitions of `CX`. 41 | // 42 | // My workaround here is to *not* conform NSObject to CXWrapping at all. 43 | // 44 | // A different fix is to rework CXWrapper/CXWrapping entirely. They are analogous to RxSwift's Reactive/ReactiveCompatible types, and could be implemented the same way RxSwift does (with CXWrapper as a struct rather than a protocol). However, that fix would touch many more files instead of just this one. 45 | 46 | public protocol _CXNSObject { } 47 | 48 | extension NSObject: _CXNSObject { } 49 | 50 | extension _CXNSObject where Self: NSObject { 51 | public var cx: CXWrappers.NSObject { .init(wrapping: self) } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/CXUtility/LockedAtomic.swift: -------------------------------------------------------------------------------- 1 | public final class LockedAtomic { 2 | 3 | private let lock = Lock() 4 | private var value: Value 5 | 6 | public init(_ value: Value) { 7 | self.value = value 8 | } 9 | 10 | deinit { 11 | lock.cleanupLock() 12 | } 13 | 14 | public var isMutating: Bool { 15 | if lock.tryLock() { 16 | lock.unlock() 17 | return false 18 | } 19 | return true 20 | } 21 | 22 | public func load() -> Value { 23 | self.lock.lock() 24 | defer { self.lock.unlock() } 25 | return self.value 26 | } 27 | 28 | public func store(_ desired: Value) { 29 | self.lock.lock() 30 | defer { self.lock.unlock() } 31 | self.value = desired 32 | } 33 | 34 | public func exchange(_ desired: Value) -> Value { 35 | self.lock.lock() 36 | defer { self.lock.unlock() } 37 | let old = self.value 38 | self.value = desired 39 | return old 40 | } 41 | 42 | public func withLockMutating(_ body: (inout Value) throws -> R) rethrows -> R { 43 | self.lock.lock() 44 | defer { self.lock.unlock() } 45 | return try body(&self.value) 46 | } 47 | } 48 | 49 | public extension LockedAtomic where Value: Equatable { 50 | 51 | func compareExchange(expected: Value, desired: Value) -> (exchanged: Bool, original: Value) { 52 | self.lock.lock() 53 | defer { self.lock.unlock() } 54 | let original = value 55 | guard original == expected else { 56 | return (false, original) 57 | } 58 | value = desired 59 | return (true, original) 60 | } 61 | } 62 | 63 | public extension LockedAtomic where Value: Numeric { 64 | 65 | func loadThenWrappingIncrement(by operand: Value = 1) -> Value { 66 | self.lock.lock() 67 | defer { self.lock.unlock() } 68 | let original = value 69 | value += operand 70 | return original 71 | } 72 | 73 | func loadThenWrappingDecrement(by operand: Value = 1) -> Value { 74 | self.lock.lock() 75 | defer { self.lock.unlock() } 76 | let original = value 77 | value -= operand 78 | return original 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/Combined/Scan.swift: -------------------------------------------------------------------------------- 1 | extension Publisher { 2 | 3 | /// Transforms elements from the upstream publisher by providing the current element to a closure along with the last value returned by the closure. 4 | /// 5 | /// let pub = (0...5) 6 | /// .publisher 7 | /// .scan(0, { return $0 + $1 }) 8 | /// .sink(receiveValue: { print ("\($0)", terminator: " ") }) 9 | /// // Prints "0 1 3 6 10 15 ". 10 | /// 11 | /// 12 | /// - Parameters: 13 | /// - initialResult: The previous result returned by the `nextPartialResult` closure. 14 | /// - nextPartialResult: A closure that takes as its arguments the previous value returned by the closure and the next element emitted from the upstream publisher. 15 | /// - Returns: A publisher that transforms elements by applying a closure that receives its previous return value and the next element from the upstream publisher. 16 | public func scan(_ initialResult: T, _ nextPartialResult: @escaping (T, Output) -> T) -> Publishers.Scan { 17 | return .init(upstream: self, initialResult: initialResult, nextPartialResult: nextPartialResult) 18 | } 19 | } 20 | 21 | extension Publishers { 22 | 23 | public struct Scan: Publisher { 24 | 25 | public typealias Failure = Upstream.Failure 26 | 27 | public let upstream: Upstream 28 | 29 | public let initialResult: Output 30 | 31 | public let nextPartialResult: (Output, Upstream.Output) -> Output 32 | 33 | public init(upstream: Upstream, initialResult: Output, nextPartialResult: @escaping (Output, Upstream.Output) -> Output) { 34 | self.upstream = upstream 35 | self.initialResult = initialResult 36 | self.nextPartialResult = nextPartialResult 37 | } 38 | 39 | public func receive(subscriber: S) where Output == S.Input, Upstream.Failure == S.Failure { 40 | self.upstream 41 | .tryScan(self.initialResult, self.nextPartialResult) 42 | .mapError { 43 | $0 as! Failure 44 | } 45 | .receive(subscriber: subscriber) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Tests/CombineXTests/Publishers/OptionalSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Nimble 3 | import Quick 4 | 5 | typealias OptionalPublisher = Optional.CX.Publisher 6 | 7 | class OptionalSpec: QuickSpec { 8 | 9 | override func spec() { 10 | 11 | // MARK: - Send Values 12 | describe("Send Values") { 13 | 14 | // MARK: 1.1 should send a value then send finished 15 | it("should send value then send finished") { 16 | let pub = OptionalPublisher(1) 17 | let sub = pub.subscribeTracingSubscriber(initialDemand: .unlimited) 18 | 19 | expect(sub.eventsWithoutSubscription) == [.value(1), .completion(.finished)] 20 | } 21 | 22 | // MARK: 1.2 should send finished even no demand 23 | it("should send finished") { 24 | let pub = OptionalPublisher(nil) 25 | 26 | let sub = pub.subscribeTracingSubscriber(initialDemand: .max(0)) 27 | 28 | expect(sub.eventsWithoutSubscription) == [.completion(.finished)] 29 | } 30 | 31 | #if arch(x86_64) && canImport(Darwin) 32 | // MARK: 1.3 should throw assertion when none demand is requested 33 | it("should throw assertion when less than one demand is requested") { 34 | let pub = OptionalPublisher(1) 35 | expect { 36 | pub.subscribeTracingSubscriber(initialDemand: .max(0)) 37 | }.to(throwAssertion()) 38 | } 39 | 40 | // MARK: 1.4 should not throw assertion when none demand is requested if is nil 41 | it("should not throw assertion when none demand is requested if is nil") { 42 | let pub = OptionalPublisher(nil) 43 | expect { 44 | pub.subscribeTracingSubscriber(initialDemand: .max(0)) 45 | }.toNot(throwAssertion()) 46 | } 47 | #endif 48 | 49 | it("Optional provide publisher property since macOS 11") { 50 | let pub = Optional(11).cx.publisher 51 | assert(pub == OptionalPublisher(11)) 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/SetFailureType.swift: -------------------------------------------------------------------------------- 1 | extension Publisher where Failure == Never { 2 | 3 | /// Changes the failure type declared by the upstream publisher. 4 | /// 5 | /// The publisher returned by this method cannot actually fail with the specified type and instead just 6 | /// finishes normally. Instead, you use this method when you need to match the error types of two 7 | /// mismatched publishers. 8 | /// 9 | /// - Parameter failureType: The `Failure` type presented by this publisher. 10 | /// - Returns: A publisher that appears to send the specified failure type. 11 | public func setFailureType(to failureType: E.Type) -> Publishers.SetFailureType { 12 | return .init(upstream: self) 13 | } 14 | } 15 | 16 | extension Publishers.SetFailureType: Equatable where Upstream: Equatable {} 17 | 18 | extension Publishers { 19 | 20 | /// A publisher that appears to send a specified failure type. 21 | /// 22 | /// The publisher cannot actually fail with the specified type and instead just finishes normally. Use this 23 | /// publisher type when you need to match the error types for two mismatched publishers. 24 | public struct SetFailureType: Publisher where Upstream.Failure == Never { 25 | 26 | public typealias Output = Upstream.Output 27 | 28 | /// The publisher from which this publisher receives elements. 29 | public let upstream: Upstream 30 | 31 | /// Creates a publisher that appears to send a specified failure type. 32 | /// 33 | /// - Parameter upstream: The publisher from which this publisher receives elements. 34 | public init(upstream: Upstream) { 35 | self.upstream = upstream 36 | } 37 | 38 | public func receive(subscriber: S) where Failure == S.Failure, Upstream.Output == S.Input { 39 | func dummy(_: Never) -> T {} 40 | self.upstream 41 | .mapError(dummy) 42 | .receive(subscriber: subscriber) 43 | } 44 | 45 | public func setFailureType(to failure: E.Type) -> Publishers.SetFailureType { 46 | return Publishers.SetFailureType(upstream: self.upstream) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Tests/CombineXTests/Publishers/CollectByCountSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Nimble 3 | import Quick 4 | 5 | class CollectByCountSpec: QuickSpec { 6 | 7 | override func spec() { 8 | 9 | // MARK: - Relay 10 | describe("Relay") { 11 | 12 | // MARK: 1.1 should relay values by collection 13 | it("should relay values by collection") { 14 | let pub = PassthroughSubject() 15 | let sub = pub.collect(2).subscribeTracingSubscriber(initialDemand: .unlimited) 16 | 17 | pub.send(contentsOf: 0..<5) 18 | pub.send(completion: .failure(.e0)) 19 | 20 | expect(sub.eventsWithoutSubscription) == [ 21 | .value([0, 1]), 22 | .value([2, 3]), 23 | .completion(.failure(.e0)) 24 | ] 25 | } 26 | 27 | // MARK: 1.2 should send unsent values if upstream finishes 28 | it("should send unsent values if upstream finishes") { 29 | let pub = PassthroughSubject() 30 | let sub = pub.collect(2).subscribeTracingSubscriber(initialDemand: .unlimited) 31 | 32 | pub.send(contentsOf: 0..<5) 33 | pub.send(completion: .finished) 34 | 35 | expect(sub.eventsWithoutSubscription) == [ 36 | .value([0, 1]), 37 | .value([2, 3]), 38 | .value([4]), 39 | .completion(.finished) 40 | ] 41 | } 42 | 43 | // MARK: 1.3 should relay as many values as demand 44 | it("should relay as many values as demand") { 45 | let pub = PassthroughSubject() 46 | let sub = pub.collect(2).subscribeTracingSubscriber(initialDemand: .max(1)) { v in 47 | v == [0, 1] ? .max(1) : .none 48 | } 49 | 50 | pub.send(contentsOf: 0..<5) 51 | pub.send(completion: .finished) 52 | 53 | expect(sub.eventsWithoutSubscription) == [.value([0, 1]), .value([2, 3]), .completion(.finished)] 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/CXTestUtility/Extensions/TracingSubscriber+extensions.swift: -------------------------------------------------------------------------------- 1 | import CXUtility 2 | 3 | extension Publisher { 4 | 5 | public func subscribeTracingSubscriber(initialDemand: Subscribers.Demand?, subsequentDemand: ((Output) -> Subscribers.Demand)? = nil) -> TracingSubscriber { 6 | let sub = TracingSubscriber(receiveSubscription: { s in 7 | initialDemand.map(s.request) 8 | }, receiveValue: { v in 9 | return subsequentDemand?(v) ?? .none 10 | }) 11 | subscribe(sub) 12 | return sub 13 | } 14 | 15 | public func subscribeTracingSubscriber(initialDemand: Subscribers.Demand?, subsequentDemand: @autoclosure @escaping () -> Subscribers.Demand) -> TracingSubscriber { 16 | let sub = TracingSubscriber(receiveSubscription: { s in 17 | initialDemand.map(s.request) 18 | }, receiveValue: { _ in 19 | return subsequentDemand() 20 | }) 21 | subscribe(sub) 22 | return sub 23 | } 24 | } 25 | 26 | extension TracingSubscriber { 27 | 28 | public var eventsWithoutSubscription: [Event] { 29 | return self.events.filter { !$0.isSubscription } 30 | } 31 | } 32 | 33 | extension TracingSubscriber.Event { 34 | 35 | public var isSubscription: Bool { 36 | switch self { 37 | case .subscription: 38 | return true 39 | case .value, .completion: 40 | return false 41 | } 42 | } 43 | } 44 | 45 | public protocol TestEventProtocol { 46 | associatedtype Input 47 | associatedtype Failure: Error 48 | 49 | var testEvent: TracingSubscriber.Event { 50 | get set 51 | } 52 | } 53 | 54 | extension TracingSubscriber.Event: TestEventProtocol { 55 | 56 | public var testEvent: TracingSubscriber.Event { 57 | get { 58 | return self 59 | } 60 | set { 61 | self = newValue 62 | } 63 | } 64 | } 65 | 66 | extension Collection where Element: TestEventProtocol { 67 | 68 | public func mapError(_ transform: (Element.Failure) -> NewFailure) -> [TracingSubscriber.Event] { 69 | return self.map { 70 | $0.testEvent.mapError(transform) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /README_zh-Hans.md: -------------------------------------------------------------------------------- 1 | # CombineX 2 | 3 | [![Github CI Status](https://github.com/cx-org/CombineX/workflows/CI/badge.svg)](https://github.com/cx-org/CombineX/actions) 4 | [![Release](https://img.shields.io/github/release-pre/cx-org/combinex)](https://github.com/cx-org/CombineX/releases) 5 | ![Install](https://img.shields.io/badge/install-Swift_PM%20%7C%20CocoaPods%20%7C%20Carthage-ff69b4) 6 | ![Supported Platform](https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20iOS%20%7C%20watchOS%20%7C%20tvOS-lightgrey) 7 | [![Discord](https://img.shields.io/badge/chat-discord-9cf)](https://discord.gg/9vzqgZx) 8 | 9 | 对 Apple [Combine](https://developer.apple.com/documentation/combine) 的开源实现。 10 | 11 | > 虽然 CombineX 已经实现了 Combine 的全部功能,但这一项目仍处于早期开发阶段。 12 | 13 | ## 什么是 Combine 14 | 15 | > Customize handling of asynchronous events by combining event-processing operators. -- Apple 16 | 17 | `Combine` 是 Apple 在 WWDC 2019 上推出的[函数式响应式编程](https://zh.wikipedia.org/wiki/函数式反应式编程)框架。在可预见的将来,它一定会成为 Swift 编程的基石。 18 | 19 | ## 开始使用 20 | 21 | > 如果你开发的是库,建议使用 [`CXShim`](https://github.com/cx-org/CXShim) 以使其兼容 SwiftUI。 22 | 23 | ### 要求 24 | 25 | - Swift 5.0+ (Xcode 10.2+) 26 | 27 | ### 安装 28 | 29 | #### Swift Package Manager (推荐) 30 | 31 | ```swift 32 | package.dependencies += [ 33 | .package(url: "https://github.com/cx-org/CombineX", from: "0.4.0"), 34 | ] 35 | ``` 36 | 37 | #### CocoaPods 38 | 39 | ```ruby 40 | pod 'CombineX', "~> 0.4.0" 41 | 42 | # 或者,如果你想用 `Foundation` 扩展: 43 | pod 'CombineX/CXFoundation', "~> 0.4.0" 44 | ``` 45 | 46 | #### Carthage 47 | 48 | ```carthage 49 | github "cx-org/CombineX" ~> 0.4.0 50 | ``` 51 | 52 | ## 相关项目 53 | 54 | 以下这些库为 Combine 添加了额外功能。它们都是 [Combine 兼容库](https://github.com/cx-org/CombineX/wiki/Combine-Compatible-Package),你可以自由切换底层的 Combine 实现,以使用 CombineX 或是 Apple 提供的 Combine。 55 | 56 | - [CXTest](https://github.com/cx-org/CXTest): 针对 Combine 的单元测试工具,例如 `TracingSubscriber`,`VirtualTimeScheduler` 等。 57 | - [CXExtensions](https://github.com/cx-org/CXExtensions):提供一系列有用的 Combine 扩展,例如:`IgnoreError`,`DelayedAutoCancellable` 等。 58 | - [CXCocoa](https://github.com/cx-org/CXCocoa):提供 `Cocoa` 的 Combine 扩展。例如 `KVO+Publisher`,`Method Interception`,`UIBinding`,`Delegate Proxy` 等。 59 | 60 | ## 许可协议 61 | 62 | CombineX 以 MIT 协议发布. 查看 [LICENSE](LICENSE) 获取更多信息. 63 | 64 | 以下文件取自 Swift 项目: 65 | 66 | - [Publishers+KeyValueObserving](Sources/CXFoundation/Publishers+KeyValueObserving.swift) 67 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/Combined/Reduce.swift: -------------------------------------------------------------------------------- 1 | extension Publisher { 2 | 3 | /// Applies a closure that accumulates each element of a stream and publishes a final result upon completion. 4 | /// 5 | /// - Parameters: 6 | /// - initialResult: The value the closure receives the first time it is called. 7 | /// - nextPartialResult: A closure that takes the previously-accumulated value and the next element from the upstream publisher to produce a new value. 8 | /// - Returns: A publisher that applies the closure to all received elements and produces an accumulated value when the upstream publisher finishes. 9 | public func reduce(_ initialResult: T, _ nextPartialResult: @escaping (T, Output) -> T) -> Publishers.Reduce { 10 | return .init(upstream: self, initial: initialResult, nextPartialResult: nextPartialResult) 11 | } 12 | } 13 | 14 | extension Publishers { 15 | 16 | /// A publisher that applies a closure to all received elements and produces an accumulated value when the upstream publisher finishes. 17 | public struct Reduce: Publisher { 18 | 19 | public typealias Failure = Upstream.Failure 20 | 21 | /// The publisher from which this publisher receives elements. 22 | public let upstream: Upstream 23 | 24 | /// The initial value provided on the first invocation of the closure. 25 | public let initial: Output 26 | 27 | /// A closure that takes the previously-accumulated value and the next element from the upstream publisher to produce a new value. 28 | public let nextPartialResult: (Output, Upstream.Output) -> Output 29 | 30 | public init(upstream: Upstream, initial: Output, nextPartialResult: @escaping (Output, Upstream.Output) -> Output) { 31 | self.upstream = upstream 32 | self.initial = initial 33 | self.nextPartialResult = nextPartialResult 34 | } 35 | 36 | public func receive(subscriber: S) where Output == S.Input, Upstream.Failure == S.Failure { 37 | self.upstream 38 | .tryReduce(self.initial, self.nextPartialResult) 39 | .mapError { 40 | $0 as! Failure 41 | } 42 | .receive(subscriber: subscriber) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Tests/CXInconsistentTests/FailingTests/FailingBufferSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Nimble 3 | import Quick 4 | 5 | class FailingBufferSpec: QuickSpec { 6 | 7 | override func spec() { 8 | 9 | context(minimalVersion: .v12_0) { 10 | 11 | it("should request unlimit at beginning if strategy is by request") { 12 | let subject = TracingSubject() 13 | let pub = subject.buffer(size: 5, prefetch: .byRequest, whenFull: .dropOldest) 14 | let sub = pub.subscribeTracingSubscriber(initialDemand: .max(2), subsequentDemand: { [5].contains($0) ? .max(5) : .max(0) }) 15 | 16 | subject.send(contentsOf: 0..<100) 17 | 18 | sub.subscription?.request(.max(2)) 19 | expect(subject.subscription.requestDemandRecords).toBranch( 20 | combine: equal([.unlimited]), 21 | cx: equal([.unlimited, .max(2), .max(2)])) 22 | expect(subject.subscription.syncDemandRecords) == Array(repeating: .max(0), count: 100) 23 | } 24 | 25 | it("should request buffer count at beginning if strategy is keep full") { 26 | let subject = TracingSubject() 27 | let pub = subject.buffer(size: 10, prefetch: .keepFull, whenFull: .dropOldest) 28 | let sub = pub.subscribeTracingSubscriber(initialDemand: .max(5), subsequentDemand: { [5, 10].contains($0) ? .max(5) : .max(0) }) 29 | 30 | subject.send(contentsOf: 0..<100) 31 | 32 | sub.subscription?.request(.max(9)) 33 | sub.subscription?.request(.max(5)) 34 | 35 | expect(subject.subscription.requestDemandRecords).toBranch( 36 | combine: equal([.max(10), .max(10)]), 37 | cx: equal([.max(10), .max(5), .max(19), .max(5)])) 38 | let max1 = Array(repeating: Subscribers.Demand.max(1), count: 5) 39 | expect(subject.subscription.syncDemandRecords).toBranch( 40 | combine: equal(max1 + Array(repeating: Subscribers.Demand.max(0), count: 10)), 41 | cx: equal(max1 + Array(repeating: Subscribers.Demand.max(0), count: 15))) 42 | } 43 | } 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Tests/CombineXTests/Publishers/MapErrorSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Nimble 3 | import Quick 4 | 5 | class MapErrorSpec: QuickSpec { 6 | 7 | override func spec() { 8 | 9 | // MARK: Relay 10 | describe("Relay") { 11 | 12 | // MARK: 1.1 should map error 13 | it("should map error") { 14 | let pub = PassthroughSubject() 15 | let sub = pub 16 | .mapError { _ in TestError.e2 } 17 | .subscribeTracingSubscriber(initialDemand: .unlimited) 18 | 19 | for i in 0..<100 { 20 | pub.send(i) 21 | } 22 | 23 | pub.send(completion: .failure(.e0)) 24 | 25 | let valueEvents = (0..<100).map(TracingSubscriber.Event.value) 26 | let expected = valueEvents + [.completion(.failure(.e2))] 27 | expect(sub.eventsWithoutSubscription) == expected 28 | } 29 | 30 | #if arch(x86_64) && canImport(Darwin) 31 | // MARK: 1.2 should throw assertion when upstream send values before sending subscription 32 | it("should throw assertion when upstream send values before sending subscription") { 33 | let upstream = AnyPublisher { s in 34 | _ = s.receive(1) 35 | } 36 | let pub = upstream.mapError { $0 as Error } 37 | 38 | expect { 39 | pub.subscribeTracingSubscriber(initialDemand: .unlimited) 40 | }.toNot(throwAssertion()) 41 | } 42 | 43 | // MARK: 1.3 should throw assertion when upstream send completion before sending subscription 44 | it("should throw assertion when upstream send values before sending subscription") { 45 | let upstream = AnyPublisher { s in 46 | s.receive(completion: .finished) 47 | } 48 | let pub = upstream.mapError { $0 as Error } 49 | 50 | expect { 51 | pub.subscribeTracingSubscriber(initialDemand: .unlimited) 52 | }.toNot(throwAssertion()) 53 | } 54 | #endif 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Tests/CombineXTests/Publishers/TryRemoveDuplicatesSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Nimble 3 | import Quick 4 | 5 | class TryRemoveDuplicatesSpec: QuickSpec { 6 | 7 | override func spec() { 8 | 9 | // MARK: - Relay 10 | describe("Relay") { 11 | 12 | // MARK: 1.1 should remove duplicate values from upstream 13 | it("should remove duplicate values from upstream") { 14 | let pub = PassthroughSubject() 15 | let sub = pub 16 | .tryRemoveDuplicates(by: ==) 17 | .subscribeTracingSubscriber(initialDemand: .unlimited) 18 | 19 | pub.send(1) 20 | pub.send(1) 21 | pub.send(2) 22 | pub.send(2) 23 | pub.send(3) 24 | pub.send(3) 25 | 26 | let got = sub.eventsWithoutSubscription.mapError { $0 as! TestError } 27 | 28 | expect(got) == [.value(1), .value(2), .value(3)] 29 | } 30 | 31 | // MARK: 1.2 should send as many values as demand 32 | it("should send as many values as demand") { 33 | let pub = PassthroughSubject() 34 | let sub = pub 35 | .tryRemoveDuplicates(by: ==) 36 | .subscribeTracingSubscriber(initialDemand: .max(10)) 37 | 38 | for _ in 0..<100 { 39 | pub.send(Int.random(in: 0..<100)) 40 | } 41 | 42 | expect(sub.eventsWithoutSubscription.count) == 10 43 | } 44 | 45 | // MARK: 1.3 should fail if closure throws error 46 | it("should fail if closure throws error") { 47 | let pub = PassthroughSubject() 48 | let sub = pub 49 | .tryRemoveDuplicates(by: { _, _ in throw TestError.e0 }) 50 | .subscribeTracingSubscriber(initialDemand: .unlimited) 51 | 52 | pub.send(1) 53 | pub.send(1) 54 | 55 | let got = sub.eventsWithoutSubscription.mapError { $0 as! TestError } 56 | 57 | expect(got) == [.value(1), .completion(.failure(.e0))] 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/CombineX/AnyPublisher.swift: -------------------------------------------------------------------------------- 1 | extension Publisher { 2 | 3 | /// Wraps this publisher with a type eraser. 4 | /// 5 | /// Use `eraseToAnyPublisher()` to expose an instance of AnyPublisher to the downstream subscriber, rather than this publisher’s actual type. 6 | public func eraseToAnyPublisher() -> AnyPublisher { 7 | return AnyPublisher(self) 8 | } 9 | } 10 | 11 | /// A type-erasing publisher. 12 | /// 13 | /// Use `AnyPublisher` to wrap a publisher whose type has details you don’t want to expose to subscribers or other publishers. 14 | public struct AnyPublisher: CustomStringConvertible, CustomPlaygroundDisplayConvertible { 15 | 16 | @usableFromInline 17 | let box: PublisherBoxBase 18 | 19 | /// Creates a type-erasing publisher to wrap the provided publisher. 20 | /// 21 | /// - Parameters: 22 | /// - publisher: A publisher to wrap with a type-eraser. 23 | @inlinable 24 | public init(_ publisher: P) where Output == P.Output, Failure == P.Failure { 25 | box = PublisherBox(publisher) 26 | } 27 | 28 | public var description: String { 29 | return "AnyPublisher" 30 | } 31 | 32 | public var playgroundDescription: Any { 33 | return self.description 34 | } 35 | } 36 | 37 | extension AnyPublisher: Publisher { 38 | 39 | @inlinable 40 | public func receive(subscriber: S) where Output == S.Input, Failure == S.Failure { 41 | self.box.receive(subscriber: subscriber) 42 | } 43 | } 44 | 45 | // MARK: - Implementation 46 | 47 | @usableFromInline 48 | class PublisherBoxBase: Publisher { 49 | 50 | @inlinable 51 | init() {} 52 | 53 | @usableFromInline 54 | func receive(subscriber: S) where Output == S.Input, Failure == S.Failure { 55 | Never.requiresConcreteImplementation() 56 | } 57 | } 58 | 59 | @usableFromInline 60 | final class PublisherBox: PublisherBoxBase { 61 | 62 | @usableFromInline 63 | let base: Base 64 | 65 | @inlinable 66 | init(_ base: Base) { 67 | self.base = base 68 | } 69 | 70 | @inlinable 71 | override func receive(subscriber: S) where Output == S.Input, Failure == S.Failure { 72 | base.receive(subscriber: subscriber) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/Combined/CompactMap.swift: -------------------------------------------------------------------------------- 1 | extension Publisher { 2 | 3 | /// Calls a closure with each received element and publishes any returned optional that has a value. 4 | /// 5 | /// - Parameter transform: A closure that receives a value and returns an optional value. 6 | /// - Returns: A publisher that republishes all non-`nil` results of calling the transform closure. 7 | public func compactMap(_ transform: @escaping (Output) -> T?) -> Publishers.CompactMap { 8 | return .init(upstream: self, transform: transform) 9 | } 10 | } 11 | 12 | extension Publishers.CompactMap { 13 | 14 | public func compactMap(_ transform: @escaping (Output) -> T?) -> Publishers.CompactMap { 15 | return self.upstream.compactMap { 16 | if let output = self.transform($0) { 17 | return transform(output) 18 | } 19 | return nil 20 | } 21 | } 22 | 23 | public func map(_ transform: @escaping (Output) -> T) -> Publishers.CompactMap { 24 | return self.upstream.compactMap { 25 | if let output = self.transform($0) { 26 | return transform(output) 27 | } 28 | return nil 29 | } 30 | } 31 | } 32 | 33 | extension Publishers { 34 | 35 | /// A publisher that republishes all non-`nil` results of calling a closure with each received element. 36 | public struct CompactMap: Publisher { 37 | 38 | public typealias Failure = Upstream.Failure 39 | 40 | /// The publisher from which this publisher receives elements. 41 | public let upstream: Upstream 42 | 43 | /// A closure that receives values from the upstream publisher and returns optional values. 44 | public let transform: (Upstream.Output) -> Output? 45 | 46 | public init(upstream: Upstream, transform: @escaping (Upstream.Output) -> Output?) { 47 | self.upstream = upstream 48 | self.transform = transform 49 | } 50 | 51 | public func receive(subscriber: S) where Output == S.Input, Upstream.Failure == S.Failure { 52 | self.upstream 53 | .tryCompactMap(self.transform) 54 | .mapError { 55 | $0 as! Failure 56 | } 57 | .receive(subscriber: subscriber) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Tests/CombineXTests/Publishers/TryAllSatisfySpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Nimble 3 | import Quick 4 | 5 | class TryAllSatisfySpec: QuickSpec { 6 | 7 | override func spec() { 8 | 9 | // MARK: - Relay 10 | describe("Relay") { 11 | 12 | // MARK: 1.1 should send true then send finished 13 | it("should send true then send finished") { 14 | let subject = PassthroughSubject() 15 | let pub = subject.tryAllSatisfy { $0 < 100 } 16 | let sub = pub.subscribeTracingSubscriber(initialDemand: .unlimited) 17 | 18 | subject.send(contentsOf: 0..<10) 19 | subject.send(completion: .finished) 20 | 21 | let got = sub.eventsWithoutSubscription.mapError { $0 as! TestError } 22 | expect(got) == [.value(true), .completion(.finished)] 23 | } 24 | 25 | // MARK: 1.2 should send false then send finished 26 | it("should send false then send finished") { 27 | let subject = PassthroughSubject() 28 | let pub = subject.tryAllSatisfy { $0 < 5 } 29 | let sub = pub.subscribeTracingSubscriber(initialDemand: .unlimited) 30 | 31 | subject.send(contentsOf: 0..<10) 32 | subject.send(completion: .finished) 33 | 34 | let got = sub.eventsWithoutSubscription.mapError { $0 as! TestError } 35 | expect(got) == [.value(false), .completion(.finished)] 36 | } 37 | 38 | // MARK: 1.3 should fail if closure throws an error 39 | it("should send true then send finished") { 40 | let subject = PassthroughSubject() 41 | let pub = subject.tryAllSatisfy { 42 | if $0 == 5 { 43 | throw TestError.e0 44 | } 45 | return true 46 | } 47 | let sub = pub.subscribeTracingSubscriber(initialDemand: .unlimited) 48 | 49 | subject.send(contentsOf: 0..<10) 50 | subject.send(completion: .finished) 51 | 52 | let got = sub.eventsWithoutSubscription.mapError { $0 as! TestError } 53 | expect(got) == [.completion(.failure(.e0))] 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Tests/CXFoundationTests/SchedulerSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Foundation 3 | import Nimble 4 | import Quick 5 | 6 | class SchedulerSpec: QuickSpec { 7 | 8 | override func spec() { 9 | 10 | // MARK: 1.1 should schedule, (we just need it to compile now, and yes! we did it! 🤣) 11 | it("should schedule") { 12 | _ = Just(1) 13 | .receive(on: RunLoop.main.cx) 14 | .receive(on: DispatchQueue.main.cx) 15 | .receive(on: OperationQueue.main.cx) 16 | .sink { _ in 17 | } 18 | } 19 | 20 | // MARK: 2.1 should clamp overflowing schedule time, instead of crash. 21 | it("should clamp overflowing schedule time") { 22 | let dts: [CXWrappers.DispatchQueue.SchedulerTimeType.Stride] = [ 23 | .seconds(.max), 24 | .milliseconds(.max), 25 | .microseconds(.max), 26 | .nanoseconds(.max), 27 | ] 28 | 29 | expect(dts).to(beAllEqual()) 30 | 31 | #if arch(x86_64) && canImport(Darwin) 32 | expect { 33 | CXWrappers.DispatchQueue.SchedulerTimeType.Stride.seconds(.infinity) 34 | }.to(throwAssertion()) 35 | #endif 36 | } 37 | 38 | // MARK: 3.1 should compute time intervals correctly. 39 | it("should compute time intervals correctly") { 40 | typealias RunLoopSchedulerTimeType = CXWrappers.RunLoop.SchedulerTimeType 41 | 42 | let earlyDate = Date(timeIntervalSinceReferenceDate: 69) 43 | let lateDate = Date(timeIntervalSinceReferenceDate: 420) 44 | 45 | let earlyRLSTT = RunLoopSchedulerTimeType(earlyDate) 46 | let lateRLSTT = RunLoopSchedulerTimeType(lateDate) 47 | let rlDistance = earlyRLSTT.distance(to: lateRLSTT) 48 | expect(rlDistance) > .seconds(0) 49 | expect(lateRLSTT) == earlyRLSTT.advanced(by: rlDistance) 50 | 51 | typealias OperationQueueSchedulerTimeType = CXWrappers.OperationQueue.SchedulerTimeType 52 | 53 | let earlyOQSTT = OperationQueueSchedulerTimeType(earlyDate) 54 | let lateOQSTT = OperationQueueSchedulerTimeType(lateDate) 55 | let oqDistance = earlyOQSTT.distance(to: lateOQSTT) 56 | expect(oqDistance.timeInterval > 0).to(beTrue()) 57 | expect(lateOQSTT) == earlyOQSTT.advanced(by: oqDistance) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Tests/CXInconsistentTests/Versioning/VersioningSwitchToLatestSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Nimble 3 | import Quick 4 | 5 | class VersioningSwitchToLatestSpec: QuickSpec { 6 | 7 | override func spec() { 8 | 9 | // MARK: 1.1 should finish when the last child finish 10 | it("should finish when the last child finish") { 11 | let subject1 = PassthroughSubject() 12 | let subject2 = PassthroughSubject() 13 | 14 | let subject = PassthroughSubject, TestError>() 15 | let pub = subject.switchToLatest() 16 | let sub = pub.subscribeTracingSubscriber(initialDemand: .unlimited) 17 | 18 | subject.send(subject1) 19 | subject1.send(completion: .finished) 20 | expect(sub.eventsWithoutSubscription) == [] 21 | 22 | subject.send(subject2) 23 | expect(sub.eventsWithoutSubscription) == [] 24 | 25 | subject.send(completion: .finished) 26 | expect(sub.eventsWithoutSubscription) == [] 27 | 28 | // VERSIONING: Combine won't get any event when the last child finish. 29 | subject2.send(completion: .finished) 30 | expect(sub.eventsWithoutSubscription).toVersioning([ 31 | .v11_0: beEmpty(), 32 | .v11_4: equal([.completion(.finished)]), 33 | ]) 34 | } 35 | 36 | // MARK: 1.2 should send as many values as demand 37 | it("should send as many values as demand") { 38 | let subject1 = PassthroughSubject() 39 | let subject2 = PassthroughSubject() 40 | 41 | let subject = PassthroughSubject, Never>() 42 | 43 | let pub = subject.switchToLatest() 44 | let sub = pub.subscribeTracingSubscriber(initialDemand: .max(10)) { v in 45 | return [1, 11].contains(v) ? .max(1) : .none 46 | } 47 | 48 | subject.send(subject1) 49 | 50 | (1...10).forEach(subject1.send) 51 | 52 | subject.send(subject2) 53 | 54 | (11...20).forEach(subject2.send) 55 | 56 | expect(sub.eventsWithoutSubscription.count).toVersioning([ 57 | .v11_0: equal(10), 58 | .v11_4: equal(12), 59 | ]) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Tests/CombineXTests/Publishers/RetrySpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Nimble 3 | import Quick 4 | 5 | class RetrySpec: QuickSpec { 6 | 7 | override func spec() { 8 | 9 | // MARK: - Retry 10 | describe("Retry") { 11 | 12 | // MARK: 1.1 should retry specified times then finish 13 | it("should retry specified times then finish") { 14 | var errs: [TestError] = [.e0, .e1, .e2] 15 | let pub = AnyPublisher { s in 16 | s.receive(subscription: Subscriptions.empty) 17 | if errs.isEmpty { 18 | s.receive(completion: .finished) 19 | } else { 20 | s.receive(completion: .failure(errs.removeFirst())) 21 | } 22 | } 23 | let sub = pub.retry(5).subscribeTracingSubscriber(initialDemand: .unlimited) 24 | 25 | expect(sub.eventsWithoutSubscription) == [.completion(.finished)] 26 | } 27 | 28 | // MARK: 1.2 should retry specified times then fail 29 | it("should retry specified times then fail") { 30 | var errs: [TestError] = [.e0, .e1, .e2] 31 | let pub = AnyPublisher { s in 32 | s.receive(subscription: Subscriptions.empty) 33 | if errs.isEmpty { 34 | s.receive(completion: .finished) 35 | } else { 36 | s.receive(completion: .failure(errs.removeFirst())) 37 | } 38 | } 39 | let sub = pub.retry(1).subscribeTracingSubscriber(initialDemand: .unlimited) 40 | 41 | expect(sub.eventsWithoutSubscription) == [.completion(.failure(.e1))] 42 | } 43 | } 44 | 45 | // MARK: - Demand 46 | describe("Demand") { 47 | 48 | // MARK: 2.1 should continue demand after retry 49 | it("should continue demand after retry") { 50 | let pub0 = Publishers.Sequence<[Int], TestError>(sequence: [1, 2, 3]) 51 | let pub1 = Fail(error: .e0) 52 | let pub = pub0.append(pub1) 53 | let sub = pub.retry(1).subscribeTracingSubscriber(initialDemand: .max(5)) 54 | 55 | expect(sub.eventsWithoutSubscription) == [.value(1), .value(2), .value(3), .value(1), .value(2)] 56 | } 57 | } 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/Combined/AllSatisfy.swift: -------------------------------------------------------------------------------- 1 | extension Publisher { 2 | 3 | /// Publishes a single Boolean value that indicates whether all received elements pass a given predicate. 4 | /// 5 | /// When this publisher receives an element, it runs the predicate against the element. If the predicate 6 | /// returns `false`, the publisher produces a `false` value and finishes. If the upstream publisher 7 | /// finishes normally, this publisher produces a `true` value and finishes. 8 | /// 9 | /// As a `reduce`-style operator, this publisher produces at most one value. 10 | /// 11 | /// Backpressure note: Upon receiving any request greater than zero, this publisher requests unlimited 12 | /// elements from the upstream publisher. 13 | /// 14 | /// - Parameter predicate: A closure that evaluates each received element. Return `true` to 15 | /// continue, or `false` to cancel the upstream and complete. 16 | /// - Returns: A publisher that publishes a Boolean value that indicates whether all received 17 | /// elements pass a given predicate. 18 | public func allSatisfy(_ predicate: @escaping (Output) -> Bool) -> Publishers.AllSatisfy { 19 | return .init(upstream: self, predicate: predicate) 20 | } 21 | } 22 | 23 | extension Publishers { 24 | 25 | /// A publisher that publishes a single Boolean value that indicates whether all received elements pass a given predicate. 26 | public struct AllSatisfy: Publisher { 27 | 28 | public typealias Output = Bool 29 | 30 | public typealias Failure = Upstream.Failure 31 | 32 | /// The publisher from which this publisher receives elements. 33 | public let upstream: Upstream 34 | 35 | /// A closure that evaluates each received element. 36 | /// 37 | /// Return `true` to continue, or `false` to cancel the upstream and finish. 38 | public let predicate: (Upstream.Output) -> Bool 39 | 40 | public init(upstream: Upstream, predicate: @escaping (Upstream.Output) -> Bool) { 41 | self.upstream = upstream 42 | self.predicate = predicate 43 | } 44 | 45 | public func receive(subscriber: S) where Upstream.Failure == S.Failure, S.Input == Publishers.AllSatisfy.Output { 46 | self.upstream 47 | .tryAllSatisfy(self.predicate) 48 | .mapError { 49 | $0 as! Failure 50 | } 51 | .receive(subscriber: subscriber) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Tests/CombineXTests/Publishers/TimeoutSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Foundation 3 | import Nimble 4 | import Quick 5 | 6 | class TimeoutSpec: QuickSpec { 7 | 8 | override func spec() { 9 | 10 | // MARK: - Relay 11 | describe("Relay") { 12 | 13 | // MARK: 1.1 should fail after the specified interval 14 | it("should fail after the specified interval") { 15 | let subject = PassthroughSubject() 16 | let scheduler = VirtualTimeScheduler() 17 | 18 | let pub = subject.timeout(.seconds(5), scheduler: scheduler, customError: { TestError.e0 }) 19 | let sub = pub.subscribeTracingSubscriber(initialDemand: .unlimited) 20 | 21 | scheduler.advance(by: .seconds(4)) 22 | expect(sub.eventsWithoutSubscription) == [] 23 | 24 | scheduler.advance(by: .seconds(5)) 25 | expect(sub.eventsWithoutSubscription) == [.completion(.failure(.e0))] 26 | } 27 | 28 | // MARK: 1.2 should finish if `customError` is nil 29 | it("should finish if `customError` is nil") { 30 | let subject = PassthroughSubject() 31 | let scheduler = VirtualTimeScheduler() 32 | 33 | let pub = subject.timeout(.seconds(5), scheduler: scheduler) 34 | let sub = pub.subscribeTracingSubscriber(initialDemand: .unlimited) 35 | 36 | scheduler.advance(by: .seconds(6)) 37 | expect(sub.eventsWithoutSubscription) == [.completion(.finished)] 38 | } 39 | 40 | // MARK: 1.3 should send timeout event in scheduled action 41 | it("should send timeout error in scheduled action") { 42 | let subject = PassthroughSubject() 43 | let scheduler = DispatchQueue(label: UUID().uuidString).cx 44 | 45 | let pub = subject.timeout(.seconds(0.01), scheduler: scheduler, customError: { TestError.e0 }) 46 | let sub = TracingSubscriber(receiveSubscription: { s in 47 | s.request(.unlimited) 48 | }, receiveCompletion: { _ in 49 | expect(scheduler.base.isCurrent) == true 50 | }) 51 | pub.subscribe(sub) 52 | 53 | expect(sub.eventsWithoutSubscription).toEventually(equal([.completion(.failure(TestError.e0))])) 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/Combined/AssertNoFailure.swift: -------------------------------------------------------------------------------- 1 | extension Publisher { 2 | 3 | /// Raises a fatal error when its upstream publisher fails, and otherwise republishes all received input. 4 | /// 5 | /// Use this function for internal sanity checks that are active during testing but do not impact performance of shipping code. 6 | /// 7 | /// - Parameters: 8 | /// - prefix: A string used at the beginning of the fatal error message. 9 | /// - file: A filename used in the error message. This defaults to `#file`. 10 | /// - line: A line number used in the error message. This defaults to `#line`. 11 | /// - Returns: A publisher that raises a fatal error when its upstream publisher fails. 12 | public func assertNoFailure(_ prefix: String = "", file: StaticString = #file, line: UInt = #line) -> Publishers.AssertNoFailure { 13 | return .init(upstream: self, prefix: prefix, file: file, line: line) 14 | } 15 | } 16 | 17 | extension Publishers { 18 | 19 | /// A publisher that raises a fatal error upon receiving any failure, and otherwise republishes all received input. 20 | /// 21 | /// Use this function for internal sanity checks that are active during testing but do not impact performance of shipping code. 22 | public struct AssertNoFailure: Publisher { 23 | 24 | public typealias Output = Upstream.Output 25 | 26 | public typealias Failure = Never 27 | 28 | /// The publisher from which this publisher receives elements. 29 | public let upstream: Upstream 30 | 31 | /// The string used at the beginning of the fatal error message. 32 | public let prefix: String 33 | 34 | /// The filename used in the error message. 35 | public let file: StaticString 36 | 37 | /// The line number used in the error message. 38 | public let line: UInt 39 | 40 | public init(upstream: Upstream, prefix: String, file: StaticString, line: UInt) { 41 | self.upstream = upstream 42 | self.prefix = prefix 43 | self.file = file 44 | self.line = line 45 | } 46 | 47 | public func receive(subscriber: S) where Upstream.Output == S.Input, S.Failure == Publishers.AssertNoFailure.Failure { 48 | self.upstream 49 | .mapError { 50 | fatalError(self.prefix + ": Assert no failure, but got \($0)", file: self.file, line: self.line) 51 | } 52 | .receive(subscriber: subscriber) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Tests/CombineXTests/Publishers/DropUntilOutputSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Nimble 3 | import Quick 4 | 5 | class DropUntilOutputSpec: QuickSpec { 6 | 7 | override func spec() { 8 | 9 | // MARK: - Relay 10 | describe("Relay") { 11 | 12 | // MARK: 1.1 should drop until other sends a value 13 | it("should drop until other sends a value") { 14 | 15 | let pub0 = PassthroughSubject() 16 | let pub1 = PassthroughSubject() 17 | 18 | let pub = pub0.drop(untilOutputFrom: pub1) 19 | let sub = pub.subscribeTracingSubscriber(initialDemand: .unlimited) 20 | 21 | pub0.send(contentsOf: 0..<10) 22 | pub1.send(-1) 23 | 24 | for i in 10..<20 { 25 | pub0.send(i) 26 | } 27 | 28 | let expected = (10..<20).map(TracingSubscriber.Event.value) 29 | expect(sub.eventsWithoutSubscription) == expected 30 | } 31 | 32 | // MARK: 1.2 should complete when other complete 33 | it("should complete when other complete") { 34 | 35 | let pub0 = PassthroughSubject() 36 | let pub1 = PassthroughSubject() 37 | 38 | let pub = pub0.drop(untilOutputFrom: pub1) 39 | let sub = pub.subscribeTracingSubscriber(initialDemand: .unlimited) 40 | 41 | pub0.send(contentsOf: 0..<10) 42 | pub1.send(completion: .finished) 43 | pub0.send(contentsOf: 0..<10) 44 | 45 | expect(sub.eventsWithoutSubscription) == [.completion(.finished)] 46 | } 47 | 48 | // MARK: 1.3 should complete if self complete 49 | it("should complete if self complete") { 50 | 51 | let pub0 = PassthroughSubject() 52 | let pub1 = PassthroughSubject() 53 | 54 | let pub = pub0.drop(untilOutputFrom: pub1) 55 | let sub = pub.subscribeTracingSubscriber(initialDemand: .unlimited) 56 | 57 | pub0.send(contentsOf: 0..<10) 58 | pub0.send(completion: .finished) 59 | 60 | expect(sub.eventsWithoutSubscription) == [.completion(.finished)] 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/Combined/TryFilter.swift: -------------------------------------------------------------------------------- 1 | extension Publisher { 2 | 3 | /// Republishes all elements that match a provided error-throwing closure. 4 | /// 5 | /// If the `isIncluded` closure throws an error, the publisher fails with that error. 6 | /// 7 | /// - Parameter isIncluded: A closure that takes one element and returns a Boolean value indicating whether to republish the element. 8 | /// - Returns: A publisher that republishes all elements that satisfy the closure. 9 | public func tryFilter(_ isIncluded: @escaping (Output) throws -> Bool) -> Publishers.TryFilter { 10 | return .init(upstream: self, isIncluded: isIncluded) 11 | } 12 | } 13 | 14 | extension Publishers.TryFilter { 15 | 16 | public func filter(_ isIncluded: @escaping (Publishers.TryFilter.Output) -> Bool) -> Publishers.TryFilter { 17 | return self.upstream 18 | .tryFilter { 19 | let a = try self.isIncluded($0) 20 | let b = isIncluded($0) 21 | return a && b 22 | } 23 | } 24 | 25 | public func tryFilter(_ isIncluded: @escaping (Publishers.TryFilter.Output) throws -> Bool) -> Publishers.TryFilter { 26 | return self.upstream 27 | .tryFilter { 28 | let a = try self.isIncluded($0) 29 | let b = try isIncluded($0) 30 | return a && b 31 | } 32 | } 33 | } 34 | 35 | extension Publishers { 36 | 37 | /// A publisher that republishes all elements that match a provided error-throwing closure. 38 | public struct TryFilter: Publisher { 39 | 40 | public typealias Output = Upstream.Output 41 | 42 | public typealias Failure = Error 43 | 44 | /// The publisher from which this publisher receives elements. 45 | public let upstream: Upstream 46 | 47 | /// A error-throwing closure that indicates whether to republish an element. 48 | public let isIncluded: (Upstream.Output) throws -> Bool 49 | 50 | public init(upstream: Upstream, isIncluded: @escaping (Upstream.Output) throws -> Bool) { 51 | self.upstream = upstream 52 | self.isIncluded = isIncluded 53 | } 54 | 55 | public func receive(subscriber: S) where Upstream.Output == S.Input, S.Failure == Publishers.TryFilter.Failure { 56 | self.upstream 57 | .tryCompactMap { 58 | try self.isIncluded($0) ? $0 : nil 59 | } 60 | .receive(subscriber: subscriber) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/Combined/Map.swift: -------------------------------------------------------------------------------- 1 | extension Publisher { 2 | 3 | /// Transforms all elements from the upstream publisher with a provided closure. 4 | /// 5 | /// - Parameter transform: A closure that takes one element as its parameter and returns a new element. 6 | /// - Returns: A publisher that uses the provided closure to map elements from the upstream publisher to new elements that it then publishes. 7 | public func map(_ transform: @escaping (Output) -> T) -> Publishers.Map { 8 | return .init(upstream: self, transform: transform) 9 | } 10 | } 11 | 12 | extension Publisher { 13 | 14 | /// Replaces nil elements in the stream with the proviced element. 15 | /// 16 | /// - Parameter output: The element to use when replacing `nil`. 17 | /// - Returns: A publisher that replaces `nil` elements from the upstream publisher with the provided element. 18 | public func replaceNil(with output: T) -> Publishers.Map where Output == T? { 19 | return self.map { $0 ?? output } 20 | } 21 | } 22 | 23 | extension Publishers.Map { 24 | 25 | public func map(_ transform: @escaping (Output) -> T) -> Publishers.Map { 26 | let newTransform: (Upstream.Output) -> T = { 27 | transform(self.transform($0)) 28 | } 29 | return self.upstream.map(newTransform) 30 | } 31 | 32 | public func tryMap(_ transform: @escaping (Output) throws -> T) -> Publishers.TryMap { 33 | let newTransform: (Upstream.Output) throws -> T = { 34 | try transform(self.transform($0)) 35 | } 36 | return self.upstream.tryMap(newTransform) 37 | } 38 | } 39 | 40 | extension Publishers { 41 | 42 | /// A publisher that transforms all elements from the upstream publisher with a provided closure. 43 | public struct Map: Publisher { 44 | 45 | public typealias Failure = Upstream.Failure 46 | 47 | /// The publisher from which this publisher receives elements. 48 | public let upstream: Upstream 49 | 50 | /// The closure that transforms elements from the upstream publisher. 51 | public let transform: (Upstream.Output) -> Output 52 | 53 | public init(upstream: Upstream, transform: @escaping (Upstream.Output) -> Output) { 54 | self.upstream = upstream 55 | self.transform = transform 56 | } 57 | 58 | public func receive(subscriber: S) where Output == S.Input, Upstream.Failure == S.Failure { 59 | self.upstream 60 | .compactMap(self.transform) 61 | .receive(subscriber: subscriber) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Tests/CombineXTests/Publishers/RecordSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Nimble 3 | import Quick 4 | 5 | class RecordSpec: QuickSpec { 6 | 7 | override func spec() { 8 | 9 | // MARK: - Recording 10 | describe("Recording") { 11 | 12 | typealias Recording = Record.Recording 13 | 14 | // MARK: 1.1 should record events 15 | it("should record events") { 16 | var recording = Recording() 17 | 18 | recording.receive(1) 19 | recording.receive(2) 20 | recording.receive(completion: .failure(.e2)) 21 | 22 | expect(recording.output) == [1, 2] 23 | expect(recording.completion) == .failure(.e2) 24 | } 25 | 26 | // MARK: 1.2 should use finish temporarily 27 | it("should use finish temporarily") { 28 | let recording = Recording() 29 | expect(recording.completion) == .finished 30 | } 31 | 32 | #if arch(x86_64) && canImport(Darwin) 33 | // MARK: 1.3 should fatal if receiving value after receiving completion 34 | it("should fatal if receiving value after receiving completion") { 35 | expect { 36 | var recording = Recording() 37 | recording.receive(completion: .finished) 38 | recording.receive(1) 39 | }.to(throwAssertion()) 40 | } 41 | 42 | // MARK: 1.4 should fatal if receiving completion after receiving completion 43 | it("should fatal if receiving completion after receiving completion") { 44 | expect { 45 | var recording = Recording() 46 | recording.receive(completion: .finished) 47 | recording.receive(completion: .finished) 48 | }.to(throwAssertion()) 49 | } 50 | #endif 51 | } 52 | 53 | // MARK: - Replay 54 | describe("Replay") { 55 | 56 | // MARK: 2.1 should replay its events 57 | it("should replay its events") { 58 | let record = Record { 59 | $0.receive(1) 60 | $0.receive(2) 61 | $0.receive(completion: .failure(.e2)) 62 | } 63 | 64 | let sub = record.subscribeTracingSubscriber(initialDemand: .unlimited) 65 | 66 | expect(sub.eventsWithoutSubscription) == [.value(1), .value(2), .completion(.failure(.e2))] 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Tests/CombineXTests/Publishers/TryCatchSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Nimble 3 | import Quick 4 | 5 | class TryCatchSpec: QuickSpec { 6 | 7 | override func spec() { 8 | 9 | // MARK: - Send Values 10 | describe("Send Values") { 11 | 12 | // MARK: 1.1 should use new publisher if upstream ends with error 13 | it("should use new publisher if upstream ends with error") { 14 | let p0 = Fail(error: .e0) 15 | let p1 = Publishers.Sequence<[Int], TestError>(sequence: [1, 2, 3]).append(Fail(error: .e0)) 16 | 17 | let pub = p0.tryCatch { _ in p1 } 18 | let sub = pub.subscribeTracingSubscriber(initialDemand: .unlimited) 19 | 20 | let got = sub.eventsWithoutSubscription.mapError { $0 as! TestError } 21 | 22 | let valueEvents = [1, 2, 3].map(TracingSubscriber.Event.value) 23 | let expected = valueEvents + [.completion(.failure(.e0))] 24 | 25 | expect(got) == expected 26 | } 27 | 28 | // MARK: 1.2 should send as many value as demand 29 | it("should send as many value as demand") { 30 | let p0 = Publishers.Sequence<[Int], TestError>(sequence: Array(0..<10)).append(Fail(error: .e0)) 31 | let p1 = Publishers.Sequence<[Int], TestError>(sequence: Array(10..<20)) 32 | 33 | let pub = p0.tryCatch { _ in p1 } 34 | let sub = pub.subscribeTracingSubscriber(initialDemand: .max(10)) { v in 35 | [0, 10].contains(v) ? .max(1) : .none 36 | } 37 | 38 | let got = sub.eventsWithoutSubscription.mapError { $0 as! TestError } 39 | let events = (0..<12).map(TracingSubscriber.Event.value) 40 | expect(got) == events 41 | } 42 | 43 | // MARK: 1.3 should fail if error handle throws an error 44 | it("should fail if error handle throws an error") { 45 | typealias Pub0 = Fail 46 | typealias Pub1 = Publishers.Sequence<[Int], TestError> 47 | let p0 = Pub0(error: .e0) 48 | 49 | let pub: Publishers.TryCatch = p0.tryCatch { _ in throw TestError.e2 } 50 | let sub = pub.subscribeTracingSubscriber(initialDemand: .unlimited) 51 | 52 | let got = sub.eventsWithoutSubscription.mapError { $0 as! TestError } 53 | expect(got) == [.completion(.failure(.e2))] 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/Combined/TryMap.swift: -------------------------------------------------------------------------------- 1 | extension Publisher { 2 | 3 | /// Transforms all elements from the upstream publisher with a provided error-throwing closure. 4 | /// 5 | /// If the `transform` closure throws an error, the publisher fails with the thrown error. 6 | /// - Parameter transform: A closure that takes one element as its parameter and returns a new element. 7 | /// - Returns: A publisher that uses the provided closure to map elements from the upstream publisher to new elements that it then publishes. 8 | public func tryMap(_ transform: @escaping (Output) throws -> T) -> Publishers.TryMap { 9 | return .init(upstream: self, transform: transform) 10 | } 11 | } 12 | 13 | extension Publishers.TryMap { 14 | 15 | public func map(_ transform: @escaping (Output) -> T) -> Publishers.TryMap { 16 | let newTransform: (Upstream.Output) throws -> T = { 17 | do { 18 | let output = try self.transform($0) 19 | return transform(output) 20 | } catch { 21 | throw error 22 | } 23 | } 24 | return self.upstream.tryMap(newTransform) 25 | } 26 | 27 | public func tryMap(_ transform: @escaping (Output) throws -> T) -> Publishers.TryMap { 28 | let newTransform: (Upstream.Output) throws -> T = { 29 | do { 30 | let output = try self.transform($0) 31 | return try transform(output) 32 | } catch { 33 | throw error 34 | } 35 | } 36 | return self.upstream.tryMap(newTransform) 37 | } 38 | } 39 | 40 | extension Publishers { 41 | 42 | /// A publisher that transforms all elements from the upstream publisher with a provided error-throwing closure. 43 | public struct TryMap: Publisher { 44 | 45 | public typealias Failure = Error 46 | 47 | /// The publisher from which this publisher receives elements. 48 | public let upstream: Upstream 49 | 50 | /// The error-throwing closure that transforms elements from the upstream publisher. 51 | public let transform: (Upstream.Output) throws -> Output 52 | 53 | public init(upstream: Upstream, transform: @escaping (Upstream.Output) throws -> Output) { 54 | self.upstream = upstream 55 | self.transform = transform 56 | } 57 | 58 | public func receive(subscriber: S) where Output == S.Input, S.Failure == Publishers.TryMap.Failure { 59 | self.upstream 60 | .tryCompactMap(self.transform) 61 | .receive(subscriber: subscriber) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Tests/CombineXTests/Publishers/ResultSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Dispatch 3 | import Nimble 4 | import Quick 5 | 6 | typealias ResultPublisher = Result.CX.Publisher 7 | 8 | class ResultSpec: QuickSpec { 9 | 10 | override func spec() { 11 | 12 | // MARK: - Send Values 13 | describe("Send Values") { 14 | 15 | // MARK: 1.1 should send a value then send finished 16 | it("should send value then send finished") { 17 | let pub = ResultPublisher(1) 18 | let sub = pub.subscribeTracingSubscriber(initialDemand: .unlimited) 19 | 20 | expect(sub.eventsWithoutSubscription) == [.value(1), .completion(.finished)] 21 | } 22 | 23 | // MARK: 1.2 should send failure even no demand 24 | it("should send failure") { 25 | let pub = ResultPublisher(.e0) 26 | let sub = pub.subscribeTracingSubscriber(initialDemand: .max(0)) 27 | 28 | expect(sub.eventsWithoutSubscription) == [.completion(.failure(.e0))] 29 | } 30 | 31 | #if arch(x86_64) && canImport(Darwin) 32 | // MARK: 1.3 should throw assertion when none demand is requested 33 | it("should throw assertion when less than one demand is requested") { 34 | let pub = ResultPublisher(1) 35 | expect { 36 | pub.subscribeTracingSubscriber(initialDemand: .max(0)) 37 | }.to(throwAssertion()) 38 | } 39 | 40 | // MARK: 1.4 should not throw assertion when none demand is requested if is nil 41 | it("should not throw assertion when none demand is requested if is failure") { 42 | let pub = ResultPublisher(.e0) 43 | expect { 44 | pub.subscribeTracingSubscriber(initialDemand: .max(0)) 45 | }.toNot(throwAssertion()) 46 | } 47 | #endif 48 | } 49 | 50 | // MARK: - Specializations 51 | describe("Specializations") { 52 | 53 | // MARK: 2.1 54 | it("should capture error on specialized tryMin/tryMax") { 55 | let pub = ResultPublisher(1) 56 | let err = TestError.e0 57 | 58 | let r1 = pub.tryMin(by: { _, _ in throw err }).result 59 | expect { try r1.get() }.to(throwError(err)) 60 | let r2 = pub.tryMax(by: { _, _ in throw err }).result 61 | expect { try r2.get() }.to(throwError(err)) 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/TryCombineLatest.swift: -------------------------------------------------------------------------------- 1 | extension Publisher { 2 | 3 | /// Subscribes to an additional publisher and invokes an error-throwing closure upon receiving output 4 | /// from either publisher. 5 | /// 6 | /// The combined publisher passes through any requests to *all* upstream publishers. However, it still 7 | /// obeys the demand-fulfilling rule of only sending the request amount downstream. If the demand isn’t 8 | /// `.unlimited`, it drops values from upstream publishers. It implements this by using a buffer size 9 | /// of 1 for each upstream, and holds the most recent value in each buffer. 10 | /// 11 | /// If the provided transform throws an error, the publisher fails with the error. `Failure` and 12 | /// `P.Failure` must both be `Swift.Error`. 13 | /// 14 | /// All upstream publishers need to finish for this publisher to finish. If an upstream publisher never 15 | /// publishes a value, this publisher never finishes. 16 | /// 17 | /// If any of the combined publishers terminates with a failure, this publisher also fails. 18 | /// 19 | /// - Parameters: 20 | /// - other: Another publisher to combine with this one. 21 | /// - transform: A closure that receives the most recent value from each publisher and returns 22 | /// a new value to publish. 23 | /// - Returns: A publisher that receives and combines elements from this and another publisher. 24 | public func tryCombineLatest( 25 | _ other: P, 26 | _ transform: @escaping (Output, P.Output) throws -> T 27 | ) -> Publishers.TryCombineLatest where P.Failure == Error { 28 | return .init(self, other, transform: transform) 29 | } 30 | } 31 | 32 | extension Publishers { 33 | 34 | /// A publisher that receives and combines the latest elements from two publishers, using a throwing closure. 35 | public struct TryCombineLatest: Publisher where A: Publisher, B: Publisher, A.Failure == Error, B.Failure == Error { 36 | 37 | public typealias Failure = Error 38 | 39 | public let a: A 40 | 41 | public let b: B 42 | 43 | public let transform: (A.Output, B.Output) throws -> Output 44 | 45 | public init(_ a: A, _ b: B, transform: @escaping (A.Output, B.Output) throws -> Output) { 46 | self.a = a 47 | self.b = b 48 | self.transform = transform 49 | } 50 | 51 | public func receive(subscriber: S) where Output == S.Input, S.Failure == Publishers.TryCombineLatest.Failure { 52 | self.a.combineLatest(self.b) 53 | .tryMap(self.transform) 54 | .receive(subscriber: subscriber) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import Quick 2 | import XCTest 3 | 4 | @testable import CombineXTests 5 | @testable import CXFoundationTests 6 | @testable import CXInconsistentTests 7 | 8 | QCKMain([ 9 | AnyCancellableSpec.self, 10 | AnySubscriberSpec.self, 11 | CombineIdentifierSpec.self, 12 | DemandSpec.self, 13 | 14 | AssertNoFailureSpec.self, 15 | AutoconnectSpec.self, 16 | BufferSpec.self, 17 | CollectByCountSpec.self, 18 | CollectByTimeSpec.self, 19 | ConcatenateSpec.self, 20 | DebounceSpec.self, 21 | DelaySpec.self, 22 | DropUntilOutputSpec.self, 23 | EmptySpec.self, 24 | FlatMapSpec.self, 25 | FutureSpec.self, 26 | HandleEventsSpec.self, 27 | JustSpec.self, 28 | MapErrorSpec.self, 29 | MeasureIntervalSpec.self, 30 | MergeSpec.self, 31 | MulticastSpec.self, 32 | OptionalSpec.self, 33 | OutputSpec.self, 34 | PrefixUntilOutputSpec.self, 35 | PrintSpec.self, 36 | ReceiveOnSpec.self, 37 | PublishedSpec.self, 38 | RecordSpec.self, 39 | RemoveDuplicatesSpec.self, 40 | ReplaceErrorSpec.self, 41 | ReplaceEmptySpec.self, 42 | ResultSpec.self, 43 | RetrySpec.self, 44 | SequenceSpec.self, 45 | ShareSpec.self, 46 | SubscribeOnSpec.self, 47 | SwitchToLatestSpec.self, 48 | ThrottleSpec.self, 49 | TimeoutSpec.self, 50 | TryAllSatisfySpec.self, 51 | TryCatchSpec.self, 52 | TryCompactMapSpec.self, 53 | TryDropWhileSpec.self, 54 | TryPrefixWhileSpec.self, 55 | TryReduceSpec.self, 56 | TryRemoveDuplicatesSpec.self, 57 | TryScanSpec.self, 58 | ZipSpec.self, 59 | 60 | ImmediateSchedulerSpec.self, 61 | 62 | PassthroughSubjectSpec.self, 63 | CurrentValueSubjectSpec.self, 64 | 65 | AssignSpec.self, 66 | SinkSpec.self, 67 | 68 | ObserableObjectSpec.self, 69 | 70 | // MARK: - CXFoundation 71 | 72 | CoderSpec.self, 73 | KeyValueObservingSpec.self, 74 | NotificationCenterSpec.self, 75 | SchedulerSpec.self, 76 | TimerSpec.self, 77 | URLSessionSpec.self, 78 | 79 | // MARK: - CXInconsistentTests 80 | 81 | FailingSubjectSpec.self, 82 | FailingTimerSpec.self, 83 | FailingBufferSpec.self, 84 | FailingFlatMapSpec.self, 85 | 86 | SuspiciousBufferSpec.self, 87 | SuspiciousDemandSpec.self, 88 | SuspiciousSwitchToLatestSpec.self, 89 | 90 | FixedSpec.self, 91 | 92 | VersioningDelaySpec.self, 93 | VersioningReceiveOnSpec.self, 94 | VersioningObserableObjectSpec.self, 95 | VersioningCollectByTimeSpec.self, 96 | VersioningSwitchToLatestSpec.self, 97 | VersioningFutureSpec.self, 98 | VersioningSinkSpec.self, 99 | VersioningAssignSpec.self, 100 | ]) 101 | -------------------------------------------------------------------------------- /Sources/CombineX/Publishers/B/Combined/RemoveDuplicates.swift: -------------------------------------------------------------------------------- 1 | extension Publisher where Output: Equatable { 2 | 3 | /// Publishes only elements that don’t match the previous element. 4 | /// 5 | /// - Returns: A publisher that consumes — rather than publishes — duplicate elements. 6 | public func removeDuplicates() -> Publishers.RemoveDuplicates { 7 | return .init(upstream: self, predicate: ==) 8 | } 9 | } 10 | 11 | extension Publisher { 12 | 13 | /// Publishes only elements that don’t match the previous element, as evaluated by a provided closure. 14 | /// 15 | /// - Parameter predicate: A closure to evaluate whether two elements are equivalent, for 16 | /// purposes of filtering. Return `true` from this closure to indicate that the second element is a duplicate of the first. 17 | public func removeDuplicates(by predicate: @escaping (Output, Output) -> Bool) -> Publishers.RemoveDuplicates { 18 | return .init(upstream: self, predicate: predicate) 19 | } 20 | } 21 | 22 | extension Publishers { 23 | 24 | /// A publisher that publishes only elements that don’t match the previous element. 25 | public struct RemoveDuplicates: Publisher { 26 | 27 | public typealias Output = Upstream.Output 28 | 29 | public typealias Failure = Upstream.Failure 30 | 31 | /// The publisher from which this publisher receives elements. 32 | public let upstream: Upstream 33 | 34 | /// A closure to evaluate whether two elements are equivalent, for purposes of filtering. 35 | public let predicate: (Upstream.Output, Upstream.Output) -> Bool 36 | 37 | /// Creates a publisher that publishes only elements that don’t match the previous element, as 38 | /// evaluated by a provided closure. 39 | /// 40 | /// - Parameter upstream: The publisher from which this publisher receives elements. 41 | /// - Parameter predicate: A closure to evaluate whether two elements are equivalent, 42 | /// for purposes of filtering. Return `true` from this closure to indicate that the second element 43 | /// is a duplicate of the first. 44 | public init(upstream: Upstream, predicate: @escaping (Publishers.RemoveDuplicates.Output, Publishers.RemoveDuplicates.Output) -> Bool) { 45 | self.upstream = upstream 46 | self.predicate = predicate 47 | } 48 | 49 | public func receive(subscriber: S) where Upstream.Failure == S.Failure, Upstream.Output == S.Input { 50 | self.upstream 51 | .tryRemoveDuplicates(by: self.predicate) 52 | .mapError { 53 | $0 as! Failure 54 | } 55 | .receive(subscriber: subscriber) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/CombineXTests/Publishers/ReceiveOnSpec.swift: -------------------------------------------------------------------------------- 1 | import CXTestUtility 2 | import Foundation 3 | import Nimble 4 | import Quick 5 | 6 | class ReceiveOnSpec: QuickSpec { 7 | 8 | override func spec() { 9 | 10 | // MARK: - Relay 11 | describe("Relay") { 12 | 13 | // MARK: 1.1 should receive events on the specified queue 14 | it("should receive events on the specified queue") { 15 | let subject = PassthroughSubject() 16 | let scheduler = DispatchQueue(label: UUID().uuidString).cx 17 | let pub = subject.receive(on: scheduler) 18 | 19 | var received = ( 20 | subscription: false, 21 | value: false, 22 | completion: false 23 | ) 24 | 25 | let sub = TracingSubscriber(receiveSubscription: { s in 26 | s.request(.max(100)) 27 | received.subscription = true 28 | // Versioning: see VersioningReceiveOnSpec 29 | // expect(scheduler.isCurrent) == false 30 | }, receiveValue: { _ in 31 | received.value = true 32 | expect(scheduler.base.isCurrent) == true 33 | return .none 34 | }, receiveCompletion: { _ in 35 | received.completion = true 36 | expect(scheduler.base.isCurrent) == true 37 | }) 38 | 39 | pub.subscribe(sub) 40 | 41 | // Versioning: see VersioningReceiveOnSpec 42 | expect(sub.subscription).toEventuallyNot(beNil()) 43 | 44 | subject.send(contentsOf: 0..<1000) 45 | subject.send(completion: .finished) 46 | 47 | expect([ 48 | received.subscription, 49 | received.value, 50 | received.completion 51 | ]).toEventually(equal([true, true, true])) 52 | } 53 | 54 | // MARK: 1.2 should send values as many as demand 55 | it("should send values as many as demand") { 56 | let subject = PassthroughSubject() 57 | let scheduler = DispatchQueue(label: UUID().uuidString).cx 58 | let pub = subject.receive(on: scheduler) 59 | let sub = pub.subscribeTracingSubscriber(initialDemand: .max(10)) 60 | 61 | expect(sub.subscription).toEventuallyNot(beNil()) 62 | 63 | subject.send(contentsOf: 0..<100) 64 | 65 | expect(sub.eventsWithoutSubscription.count).toEventually(equal(10)) 66 | } 67 | } 68 | } 69 | } 70 | --------------------------------------------------------------------------------