├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── CXExtensions │ ├── AnyScheduler.swift │ ├── Blocking.swift │ ├── DelayedAutoCancellable.swift │ ├── IgnoreError.swift │ ├── Invoke.swift │ ├── SelfRetainedCancellable.swift │ ├── Signal.swift │ └── WeakAssign.swift └── Tests ├── CXExtensionsTests ├── AnySchedulerSpec.swift ├── BlockingSpec.swift ├── CXTest+Extensions.swift ├── DelayedAutoCancellableSpec.swift ├── IgnoreErrorSpec.swift ├── InvokeSpec.swift └── WeakAssignSpec.swift └── LinuxMain.swift /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | paths: 6 | - '.github/workflows/ci.yml' 7 | - 'Package*' 8 | - 'Sources/**' 9 | - 'Tests/**' 10 | pull_request: 11 | paths: 12 | - '.github/workflows/ci.yml' 13 | - 'Package*' 14 | - 'Sources/**' 15 | - 'Tests/**' 16 | 17 | jobs: 18 | mac: 19 | runs-on: macOS-latest 20 | timeout-minutes: 10 21 | env: 22 | CX_COMBINE_IMPLEMENTATION: CombineX 23 | steps: 24 | - uses: actions/checkout@v1 25 | - name: Swift Version 26 | run: | 27 | swift -version 28 | swift package --version 29 | - name: Build and Test 30 | run: swift test 31 | 32 | linux: 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | swift_version: ['5.1', '5.2', '5.3', '5.4'] 37 | runs-on: ubuntu-latest 38 | timeout-minutes: 10 39 | container: 40 | image: swift:${{ matrix.swift_version }} 41 | steps: 42 | - uses: actions/checkout@v1 43 | - name: Swift Version 44 | run: | 45 | swift -version 46 | swift package --version 47 | - name: Build and Test 48 | run: swift test --enable-test-discovery 49 | 50 | combine: 51 | runs-on: macOS-latest 52 | timeout-minutes: 10 53 | env: 54 | CX_COMBINE_IMPLEMENTATION: Combine 55 | steps: 56 | - uses: actions/checkout@v1 57 | - name: Swift Version 58 | run: | 59 | sw_vers -productVersion 60 | swift -version 61 | swift package --version 62 | - name: Build and Test 63 | run: swift test 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Quentin Jin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CXShim", 6 | "repositoryURL": "https://github.com/cx-org/CXShim", 7 | "state": { 8 | "branch": null, 9 | "revision": "82fecc246c7ca9f0ac1656b8199b7956e64d68f3", 10 | "version": "0.4.0" 11 | } 12 | }, 13 | { 14 | "package": "CXTest", 15 | "repositoryURL": "https://github.com/cx-org/CXTest", 16 | "state": { 17 | "branch": null, 18 | "revision": "929ce7cbd3005bc9356ae826bb65ee2611379b17", 19 | "version": "0.4.0" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "CXExtensions", 7 | products: [ 8 | .library(name: "CXExtensions", targets: ["CXExtensions"]), 9 | ], 10 | dependencies: [ 11 | .package(url: "https://github.com/cx-org/CXShim", .upToNextMinor(from: "0.4.0")), 12 | .package(url: "https://github.com/cx-org/CXTest", .upToNextMinor(from: "0.4.0")), 13 | ], 14 | targets: [ 15 | .target(name: "CXExtensions", dependencies: ["CXShim"]), 16 | .testTarget(name: "CXExtensionsTests", dependencies: ["CXExtensions", "CXTest"]), 17 | ] 18 | ) 19 | 20 | enum CombineImplementation { 21 | 22 | case combine 23 | case combineX 24 | case openCombine 25 | 26 | static var `default`: CombineImplementation { 27 | #if canImport(Combine) 28 | return .combine 29 | #else 30 | return .combineX 31 | #endif 32 | } 33 | 34 | init?(_ description: String) { 35 | let desc = description.lowercased().filter { $0.isLetter } 36 | switch desc { 37 | case "combine": self = .combine 38 | case "combinex": self = .combineX 39 | case "opencombine": self = .openCombine 40 | default: return nil 41 | } 42 | } 43 | } 44 | 45 | extension ProcessInfo { 46 | 47 | var combineImplementation: CombineImplementation { 48 | return environment["CX_COMBINE_IMPLEMENTATION"].flatMap(CombineImplementation.init) ?? .default 49 | } 50 | } 51 | 52 | import Foundation 53 | 54 | let combineImpl = ProcessInfo.processInfo.combineImplementation 55 | 56 | if combineImpl == .combine { 57 | package.platforms = [.macOS("10.15"), .iOS("13.0"), .tvOS("13.0"), .watchOS("6.0")] 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CXExtensions 2 | 3 | [![GitHub CI](https://github.com/cx-org/CXExtensions/workflows/CI/badge.svg)](https://github.com/cx-org/CXExtensions/actions) 4 | ![Install](https://img.shields.io/badge/install-Swift_Package_Manager-ff69b4) 5 | ![Supported Platform](https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20iOS%20%7C%20watchOS%20%7C%20tvOS-lightgrey) 6 | [![Discord](https://img.shields.io/badge/chat-discord-9cf)](https://discord.gg/9vzqgZx) 7 | 8 | A collection of useful extensions for Combine. 9 | 10 | CXExtensions is [Combine Compatible Package](https://github.com/cx-org/CombineX/wiki/Combine-Compatible-Package). You're free to switch underlying Combine implementation between [CombineX](https://github.com/cx-org/CombineX) and [Combine](https://developer.apple.com/documentation/combine). 11 | 12 | ## Installation 13 | 14 | Add the following line to the dependencies in your `Package.swift` file: 15 | 16 | ```swift 17 | .package(url: "https://github.com/cx-org/CXExtensions", .upToNextMinor(from: "0.4.0")), 18 | ``` 19 | 20 | #### Requirements 21 | 22 | - Swift 5.0 23 | 24 | ## Operators 25 | 26 | - [IgnoreError](#IgnoreError) 27 | - [WeakAssign](#WeakAssign) 28 | - [Invoke](#Invoke) 29 | - [Signal](#Signal) 30 | - [Blocking](#Blocking) 31 | - [DelayedAutoCancellable](#DelayedAutoCancellable) 32 | 33 | --- 34 | 35 | #### IgnoreError 36 | 37 | Ignore error from upstream and complete. 38 | 39 | ```swift 40 | // Output: (data: Data, response: URLResponse), Failure: URLError 41 | let upstream = URLSession.shared.cx.dataTaskPublisher(for: url) 42 | 43 | // Output: (data: Data, response: URLResponse), Failure: Never 44 | let pub = upstream.ignoreError() 45 | ``` 46 | 47 | #### WeakAssign 48 | 49 | Like `Subscribers.Assign`, but capture its target weakly. 50 | 51 | ```swift 52 | pub.assign(to: \.output, weaklyOn: self) 53 | ``` 54 | 55 | #### Invoke 56 | 57 | Invoke method on an object with each element from a `Publisher`. 58 | 59 | ```swift 60 | pub.invoke(handleOutput, weaklyOn: self) 61 | // Substitute for the following common pattern: 62 | // 63 | // pub.sink { [weak self] output in 64 | // self?.handleOutput(output) 65 | // } 66 | ``` 67 | 68 | #### Signal 69 | 70 | Emits a signal (`Void()`) whenever upstream publisher produce an element. It's useful when you want `Invoke` a parameterless handler. 71 | 72 | ``` 73 | // Transform elements to signal first because `handleSignal` accept no argument. 74 | pub.signal().invoke(handleSignal, weaklyOn: self) 75 | ``` 76 | 77 | #### Blocking 78 | 79 | Get element from a `Publisher` synchronously. It's useful for command line tool and unit testing. 80 | 81 | ```swift 82 | let sequence = pub.blocking() 83 | for value in sequence { 84 | // process value 85 | } 86 | ``` 87 | 88 | #### DelayedAutoCancellable 89 | 90 | Auto cancel after delay. 91 | 92 | ```swift 93 | let delayedCanceller = upstream 94 | .sink { o in 95 | print(o) 96 | } 97 | .cancel(after .second(1), scheduler: DispatchQueue.main.cx) 98 | ``` 99 | -------------------------------------------------------------------------------- /Sources/CXExtensions/AnyScheduler.swift: -------------------------------------------------------------------------------- 1 | import CXShim 2 | 3 | // MARK: - AnyScheduler 4 | 5 | /// A type-erasing scheduler. 6 | /// 7 | /// Do not use `SchedulerTimeType` across different `AnyScheduler` instance. 8 | /// 9 | /// let scheduler1 = AnyScheduler(DispatchQueue.main.cx) 10 | /// let scheduler2 = AnyScheduler(RunLoop.main.cx) 11 | /// 12 | /// // DON'T DO THIS! Will crash. 13 | /// scheduler2.schedule(after: scheduler1.now) { ... } 14 | /// 15 | public final class AnyScheduler: Scheduler { 16 | 17 | public typealias SchedulerOptions = Never 18 | public typealias SchedulerTimeType = AnySchedulerTimeType 19 | 20 | private let _now: () -> SchedulerTimeType 21 | private let _minimumTolerance: () -> SchedulerTimeType.Stride 22 | private let _schedule_action: (@escaping () -> Void) -> Void 23 | private let _schedule_after_tolerance_action: (SchedulerTimeType, SchedulerTimeType.Stride, @escaping () -> Void) -> Void 24 | private let _schedule_after_interval_tolerance_action: (SchedulerTimeType, SchedulerTimeType.Stride, SchedulerTimeType.Stride, @escaping () -> Void) -> Cancellable 25 | 26 | public init(_ scheduler: S, options: S.SchedulerOptions? = nil) { 27 | _now = { 28 | SchedulerTimeType(wrapping: scheduler.now) 29 | } 30 | _minimumTolerance = { 31 | SchedulerTimeType.Stride(wrapping: scheduler.minimumTolerance) 32 | } 33 | _schedule_action = { action in 34 | scheduler.schedule(options: options, action) 35 | } 36 | _schedule_after_tolerance_action = { date, tolerance, action in 37 | scheduler.schedule(after: date.wrapped as! S.SchedulerTimeType, tolerance: tolerance.asType(S.SchedulerTimeType.Stride.self), options: options, action) 38 | } 39 | _schedule_after_interval_tolerance_action = { date, interval, tolerance, action in 40 | scheduler.schedule(after: date.wrapped as! S.SchedulerTimeType, interval: interval.asType(S.SchedulerTimeType.Stride.self), tolerance: tolerance.asType(S.SchedulerTimeType.Stride.self), options: options, action) 41 | } 42 | } 43 | 44 | public var now: SchedulerTimeType { 45 | return _now() 46 | } 47 | 48 | public var minimumTolerance: SchedulerTimeType.Stride { 49 | return _minimumTolerance() 50 | } 51 | 52 | public func schedule(options: SchedulerOptions?, _ action: @escaping () -> Void) { 53 | return _schedule_action(action) 54 | } 55 | 56 | public func schedule(after date: SchedulerTimeType, tolerance: SchedulerTimeType.Stride, options: SchedulerOptions?, _ action: @escaping () -> Void) { 57 | return _schedule_after_tolerance_action(date, tolerance, action) 58 | } 59 | 60 | public func schedule(after date: SchedulerTimeType, interval: SchedulerTimeType.Stride, tolerance: SchedulerTimeType.Stride, options: SchedulerOptions?, _ action: @escaping () -> Void) -> Cancellable { 61 | return _schedule_after_interval_tolerance_action(date, interval, tolerance, action) 62 | } 63 | } 64 | 65 | // MARK: - AnySchedulerTimeType 66 | 67 | /// A type-erasing `SchedulerTimeType` for `AnyScheduler`. 68 | /// 69 | /// Instance of `AnySchedulerTimeType` from different scheduler is NOT 70 | /// interactable 71 | /// 72 | /// let time1 = AnyScheduler(DispatchQueue.main.cx).now 73 | /// let time2 = AnyScheduler(RunLoop.main.cx).now 74 | /// 75 | /// // DON'T DO THIS! Will crash. 76 | /// time1.distance(to: time2) 77 | /// 78 | public struct AnySchedulerTimeType: Strideable { 79 | 80 | fileprivate let wrapped: Any 81 | 82 | private let _distance_to: (Any) -> Stride 83 | private let _advanced_by: (Stride) -> AnySchedulerTimeType 84 | 85 | fileprivate init(wrapping opaque: T) where T.Stride: SchedulerTimeIntervalConvertible { 86 | self.wrapped = opaque 87 | self._distance_to = { other in 88 | return Stride(wrapping: opaque.distance(to: other as! T)) 89 | } 90 | self._advanced_by = { n in 91 | return AnySchedulerTimeType(wrapping: opaque.advanced(by: n.asType(T.Stride.self))) 92 | } 93 | } 94 | 95 | public func distance(to other: AnySchedulerTimeType) -> Stride { 96 | return _distance_to(other) 97 | } 98 | 99 | public func advanced(by n: Stride) -> AnySchedulerTimeType { 100 | return _advanced_by(n) 101 | } 102 | } 103 | 104 | // MARK: - AnySchedulerTimeType.Stride 105 | 106 | extension AnySchedulerTimeType { 107 | 108 | public struct Stride: Comparable, SignedNumeric, SchedulerTimeIntervalConvertible { 109 | 110 | private struct Opaque { 111 | 112 | let wrapped: Any 113 | 114 | let _init: (SchedulerTimeLiteral) -> Opaque 115 | let _lessThan: (Any) -> Bool 116 | let _equalTo: (Any) -> Bool 117 | let _add: (Any) -> Opaque 118 | let _subtract: (Any) -> Opaque 119 | let _multiply: (Any) -> Opaque 120 | let _magnitude: () -> Opaque 121 | 122 | init(_ content: T) { 123 | wrapped = content 124 | _init = { Opaque(T.time(literal: $0)) } 125 | _lessThan = { content < ($0 as! T) } 126 | _equalTo = { content < ($0 as! T) } 127 | _add = { Opaque(content + ($0 as! T)) } 128 | _subtract = { Opaque(content - ($0 as! T)) } 129 | _multiply = { Opaque(content * ($0 as! T)) } 130 | // Get magnitude and create Self from it. It's only possible on 131 | // BinaryInteger or BinaryFloatingPoint, fail fast otherwise. 132 | // 133 | // This is the best we can do for arbitrary SignedNumeric type. 134 | _magnitude = { Opaque(content.magnitudeAsSelfIfBinaryIntegerOrBinaryFloatingPoint!) } 135 | } 136 | } 137 | 138 | private enum Wrapped { 139 | case opaque(Opaque) 140 | case literal(SchedulerTimeLiteral) 141 | } 142 | 143 | private var wrapped: Wrapped 144 | 145 | private init(_ value: Wrapped) { 146 | wrapped = value 147 | } 148 | 149 | fileprivate init(wrapping opaque: T) { 150 | wrapped = .opaque(.init(opaque)) 151 | } 152 | 153 | fileprivate func asType(_ type: T.Type) -> T { 154 | switch wrapped { 155 | case let .opaque(opaque): 156 | guard let result = opaque.wrapped as? T else { 157 | preconditionFailure("Use AnySchedulerTimeType across different AnyScheduler instance is not supported.") 158 | } 159 | return result 160 | case let .literal(literal): 161 | return T.time(literal: literal) 162 | } 163 | } 164 | 165 | public init(integerLiteral value: Int) { 166 | wrapped = .literal(.seconds(value)) 167 | } 168 | 169 | public init?(exactly source: T) { 170 | guard let value = Int(exactly: source) else { 171 | return nil 172 | } 173 | self.init(integerLiteral: value) 174 | } 175 | 176 | public var magnitude: Stride { 177 | switch self.wrapped { 178 | case let .opaque(v): 179 | return .init(.opaque(v._magnitude())) 180 | case let .literal(v): 181 | return .seconds(v.timeInterval.magnitude) 182 | } 183 | } 184 | 185 | private static func withWrapped(_ lhs: Stride, _ rhs: Stride, body: (Opaque, Opaque) -> T, fallback: (SchedulerTimeLiteral, SchedulerTimeLiteral) -> T) -> T { 186 | switch (lhs.wrapped, rhs.wrapped) { 187 | case let (.opaque(l), .opaque(r)): 188 | return body(l, r) 189 | case let (.opaque(l), .literal(r)): 190 | return body(l, l._init(r)) 191 | case let (.literal(l), .opaque(r)): 192 | return body(r._init(l), r) 193 | case let (.literal(l), .literal(r)): 194 | return fallback(l, r) 195 | } 196 | } 197 | 198 | public static func == (lhs: Stride, rhs: Stride) -> Bool { 199 | return withWrapped(lhs, rhs, body: { 200 | $0._equalTo($1.wrapped) 201 | }, fallback: { 202 | // TODO: potential precision loss 203 | $0.timeInterval == $1.timeInterval 204 | }) 205 | } 206 | 207 | public static func < (lhs: Stride, rhs: Stride) -> Bool { 208 | return withWrapped(lhs, rhs, body: { 209 | $0._lessThan($1.wrapped) 210 | }, fallback: { 211 | // TODO: potential precision loss 212 | $0.timeInterval < $1.timeInterval 213 | }) 214 | } 215 | 216 | public static func + (lhs: Stride, rhs: Stride) -> Stride { 217 | return withWrapped(lhs, rhs, body: { 218 | .init(.opaque($0._add($1.wrapped))) 219 | }, fallback: { 220 | // TODO: potential precision loss 221 | .seconds($0.timeInterval + $1.timeInterval) 222 | }) 223 | } 224 | 225 | public static func - (lhs: Stride, rhs: Stride) -> Stride { 226 | return withWrapped(lhs, rhs, body: { 227 | .init(.opaque($0._subtract($1.wrapped))) 228 | }, fallback: { 229 | // TODO: potential precision loss 230 | .seconds($0.timeInterval - $1.timeInterval) 231 | }) 232 | } 233 | 234 | public static func * (lhs: Stride, rhs: Stride) -> Stride { 235 | return withWrapped(lhs, rhs, body: { 236 | .init(.opaque($0._multiply($1.wrapped))) 237 | }, fallback: { 238 | // TODO: potential precision loss 239 | .seconds($0.timeInterval * $1.timeInterval) 240 | }) 241 | } 242 | 243 | public static func += (lhs: inout Stride, rhs: Stride) { 244 | lhs = lhs + rhs 245 | } 246 | 247 | public static func -= (lhs: inout Stride, rhs: Stride) { 248 | lhs = lhs - rhs 249 | } 250 | 251 | public static func *= (lhs: inout Stride, rhs: Stride) { 252 | lhs = lhs * rhs 253 | } 254 | 255 | public static func seconds(_ s: Double) -> Stride { 256 | return Stride(.literal(.interval(s))) 257 | } 258 | 259 | public static func seconds(_ s: Int) -> Stride { 260 | return Stride(.literal(.seconds(s))) 261 | } 262 | 263 | public static func milliseconds(_ ms: Int) -> Stride { 264 | return Stride(.literal(.milliseconds(ms))) 265 | } 266 | 267 | public static func microseconds(_ us: Int) -> Stride { 268 | return Stride(.literal(.microseconds(us))) 269 | } 270 | 271 | public static func nanoseconds(_ ns: Int) -> Stride { 272 | return Stride(.literal(.nanoseconds(ns))) 273 | } 274 | } 275 | } 276 | 277 | // MARK: - SchedulerTimeLiteral 278 | 279 | private enum SchedulerTimeLiteral { 280 | 281 | case seconds(Int) 282 | case milliseconds(Int) 283 | case microseconds(Int) 284 | case nanoseconds(Int) 285 | case interval(Double) 286 | 287 | var timeInterval: Double { 288 | switch self { 289 | case let .seconds(s): return Double(s) 290 | case let .milliseconds(ms): return Double(ms) * 1_000 291 | case let .microseconds(us): return Double(us) * 1_000_000 292 | case let .nanoseconds(ns): return Double(ns) * 1_000_000_000 293 | case let .interval(s): return s 294 | } 295 | } 296 | } 297 | 298 | private extension SchedulerTimeIntervalConvertible { 299 | 300 | static func time(literal: SchedulerTimeLiteral) -> Self { 301 | switch literal { 302 | case let .seconds(s): return .seconds(s) 303 | case let .milliseconds(ms): return .milliseconds(ms) 304 | case let .microseconds(us): return .microseconds(us) 305 | case let .nanoseconds(ns): return .nanoseconds(ns) 306 | case let .interval(s): return .seconds(s) 307 | } 308 | } 309 | } 310 | 311 | // MARK: - Magnitude 312 | 313 | // https://gist.github.com/dabrahams/852dfdb0b628e68567b4d97499f196f9 314 | 315 | private struct Dispatch { 316 | func apply(_ a: A, _ f: (Model)->R0) -> R1 { 317 | f(a as! Model) as! R1 318 | } 319 | } 320 | 321 | private protocol BinaryIntegerDispatch { 322 | func magnitude(_: N) -> N 323 | } 324 | 325 | private protocol BinaryFloatingPointDispatch { 326 | func magnitude(_: N) -> N 327 | } 328 | 329 | extension Dispatch: BinaryIntegerDispatch where Model: BinaryInteger { 330 | func magnitude(_ x: N) -> N { apply(x) { Model($0.magnitude) } } 331 | } 332 | 333 | extension Dispatch: BinaryFloatingPointDispatch where Model: BinaryFloatingPoint { 334 | func magnitude(_ x: N) -> N { apply(x) { Model($0.magnitude) } } 335 | } 336 | 337 | private extension SignedNumeric { 338 | var magnitudeAsSelfIfBinaryIntegerOrBinaryFloatingPoint: Self? { 339 | (Dispatch() as? BinaryIntegerDispatch)?.magnitude(self) ?? 340 | (Dispatch() as? BinaryFloatingPointDispatch)?.magnitude(self) 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /Sources/CXExtensions/Blocking.swift: -------------------------------------------------------------------------------- 1 | import CXShim 2 | import Foundation 3 | import Dispatch 4 | import CoreFoundation 5 | 6 | extension Publisher { 7 | 8 | /// Get element from a publisher synchronously. 9 | /// 10 | /// Calling this method itself is not blocking. Request next value is 11 | /// blocking, however. 12 | /// 13 | /// let subscriber = publisher.blocking() // not blocking 14 | /// 15 | /// let value = subscriber.next() // blocking 16 | /// 17 | /// for value in subscriber { // blocking 18 | /// print(value) 19 | /// } 20 | /// 21 | public func blocking() -> Subscribers.Blocking { 22 | let blocking = Subscribers.Blocking() 23 | self.subscribe(blocking) 24 | return blocking 25 | } 26 | } 27 | 28 | extension Subscribers { 29 | 30 | /// A subscriber that transform asynchronous `Publisher` to synchronous 31 | /// `Sequence`. 32 | /// 33 | /// When you request a value by calling `next()` method, the subscriber 34 | /// request one value from upstream publisher, blocks current thread until 35 | /// upstream publisher produces an element or terminates. 36 | /// 37 | /// Simultaneous access by different threads is not allowed. 38 | public class Blocking: Subscriber, Cancellable, Sequence, IteratorProtocol { 39 | 40 | private enum SubscribingState { 41 | case awaitingSubscription 42 | case connected(Subscription) 43 | case finished(Subscribers.Completion) 44 | case cancelled 45 | } 46 | 47 | private enum DemandingState { 48 | case idle 49 | case demanding(DispatchSemaphore) 50 | case recived(Input?) 51 | } 52 | 53 | private let lock = NSLock() 54 | private var subscribingState = SubscribingState.awaitingSubscription 55 | private var demandingState = DemandingState.idle 56 | 57 | // @testable 58 | var completion: Subscribers.Completion? { 59 | lock.lock() 60 | defer { lock.unlock() } 61 | switch subscribingState { 62 | case let .finished(completion): 63 | return completion 64 | default: 65 | return nil 66 | } 67 | } 68 | 69 | public init() {} 70 | 71 | public func receive(subscription: Subscription) { 72 | lock.lock() 73 | guard case .awaitingSubscription = subscribingState else { 74 | lock.unlock() 75 | subscription.cancel() 76 | return 77 | } 78 | subscribingState = .connected(subscription) 79 | lock.unlock() 80 | } 81 | 82 | public func receive(_ input: Input) -> Subscribers.Demand { 83 | lock.lock() 84 | guard case .demanding = demandingState else { 85 | lock.unlock() 86 | preconditionFailure("upstream publisher send more value than demand") 87 | } 88 | lockedSignal(input) 89 | return .none 90 | } 91 | 92 | public func receive(completion: Subscribers.Completion) { 93 | lock.lock() 94 | switch subscribingState { 95 | case .awaitingSubscription: 96 | lock.unlock() 97 | preconditionFailure("receive completion before subscribing") 98 | case .finished, .cancelled: 99 | lock.unlock() 100 | return 101 | case .connected: 102 | subscribingState = .finished(completion) 103 | switch demandingState { 104 | case .idle, .recived: 105 | lock.unlock() 106 | case .demanding: 107 | lockedSignal(nil) 108 | } 109 | } 110 | } 111 | 112 | public func cancel() { 113 | self.lock.lock() 114 | guard case let .connected(subscription) = subscribingState else { 115 | self.lock.unlock() 116 | return 117 | } 118 | subscribingState = .cancelled 119 | switch demandingState { 120 | case .idle, .recived: 121 | lock.unlock() 122 | case .demanding: 123 | lockedSignal(nil) 124 | } 125 | subscription.cancel() 126 | } 127 | 128 | public func next() -> Input? { 129 | lock.lock() 130 | // TODO: calling next() simultaneously from different thread. 131 | // We need to store multiple `DispatchSemaphore`. 132 | guard case .idle = demandingState else { 133 | lock.unlock() 134 | preconditionFailure("simultaneous access by different threads") 135 | } 136 | switch subscribingState { 137 | case .awaitingSubscription: 138 | lock.unlock() 139 | preconditionFailure("request value before subscribing") 140 | case .finished, .cancelled: 141 | lock.unlock() 142 | return nil 143 | case let .connected(subscription): 144 | return lockedWait(subscription) 145 | } 146 | } 147 | 148 | func lockedWait(_ subscription: Subscription) -> Input? { 149 | let semaphore = DispatchSemaphore(value: 0) 150 | demandingState = .demanding(semaphore) 151 | lock.unlock() 152 | subscription.request(.max(1)) 153 | semaphore.wait() 154 | lock.lock() 155 | guard case let .recived(value) = demandingState else { 156 | fatalError("Internal Inconsistency") 157 | } 158 | demandingState = .idle 159 | lock.unlock() 160 | return value 161 | } 162 | 163 | func lockedSignal(_ value: Input?) { 164 | guard case let .demanding(semaphore) = demandingState else { 165 | fatalError("Internal Inconsistency") 166 | } 167 | demandingState = .recived(value) 168 | lock.unlock() 169 | semaphore.signal() 170 | } 171 | } 172 | } 173 | 174 | #if false 175 | extension Subscribers { 176 | 177 | class AwaitNonBlockingRunLoop: Await { 178 | override func lockedWait(_ subscription: Subscription) -> Input? { 179 | // demandingState = .demanding(???) 180 | lock.unlock() 181 | subscription.request(.max(1)) 182 | #if canImport(Darwin) 183 | let runLoopMode = CFRunLoopMode.defaultMode 184 | let result = CFRunLoopRunResult.stopped 185 | #else 186 | let runLoopMode = kCFRunLoopDefaultMode 187 | let result = kCFRunLoopRunStopped 188 | #endif 189 | while true { 190 | guard CFRunLoopRunInMode(runLoopMode, .infinity, false) == result else { 191 | continue 192 | } 193 | lock.lock() 194 | if case let .recived(value) = demandingState { 195 | demandingState = .idle 196 | lock.unlock() 197 | return value 198 | } 199 | lock.unlock() 200 | } 201 | } 202 | 203 | override func lockedSignal(_ value: Input?) { 204 | 205 | } 206 | } 207 | } 208 | #endif 209 | -------------------------------------------------------------------------------- /Sources/CXExtensions/DelayedAutoCancellable.swift: -------------------------------------------------------------------------------- 1 | import CXShim 2 | 3 | extension Cancellable { 4 | 5 | /// Automatically cancel itself after specified delay. 6 | public func cancel(after interval: S.SchedulerTimeType.Stride, tolerance: S.SchedulerTimeType.Stride? = nil, scheduler: S, options: S.SchedulerOptions? = nil) -> DelayedAutoCancellable { 7 | return DelayedAutoCancellable(cancel: self.cancel, after: interval, tolerance: tolerance, scheduler: scheduler, options: options) 8 | } 9 | } 10 | 11 | /// Automatically cancel itself after specified delay. 12 | public final class DelayedAutoCancellable: Cancellable { 13 | 14 | private var cancelBody: (() -> Void)? 15 | 16 | private var scheduleCanceller: Cancellable? 17 | 18 | public init(cancel: @escaping () -> Void, after interval: S.SchedulerTimeType.Stride, tolerance: S.SchedulerTimeType.Stride? = nil, scheduler: S, options: S.SchedulerOptions? = nil) { 19 | self.cancelBody = cancel 20 | // FIXME: we should schedule non-repeatedly, but it's not cancellable. 21 | self.scheduleCanceller = scheduler.schedule(after: scheduler.now.advanced(by: interval), interval: .seconds(.max), tolerance: tolerance ?? scheduler.minimumTolerance, options: options) { [unowned self] in 22 | self.cancel() 23 | } 24 | } 25 | 26 | public func cancel() { 27 | scheduleCanceller?.cancel() 28 | scheduleCanceller = nil 29 | cancelBody?() 30 | cancelBody = nil 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/CXExtensions/IgnoreError.swift: -------------------------------------------------------------------------------- 1 | import CXShim 2 | 3 | extension Publisher { 4 | 5 | /// Ignores upstream error and complete normally. 6 | public func ignoreError() -> Publishers.IgnoreError { 7 | return .init(upstream: self) 8 | } 9 | } 10 | 11 | extension Publishers { 12 | 13 | /// A publisher that ignores upstream failure, and complete normally. 14 | public struct IgnoreError: Publisher { 15 | 16 | public typealias Output = Upstream.Output 17 | public typealias Failure = Never 18 | 19 | /// The publisher from which this publisher receives elements. 20 | public let upstream: Upstream 21 | 22 | public init(upstream: Upstream) { 23 | self.upstream = upstream 24 | } 25 | 26 | public func receive(subscriber: S) where Failure == S.Failure, Output == S.Input { 27 | self.upstream 28 | .catch { _ in 29 | Empty() 30 | } 31 | .receive(subscriber: subscriber) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/CXExtensions/Invoke.swift: -------------------------------------------------------------------------------- 1 | import CXShim 2 | import Foundation 3 | 4 | extension Publisher where Failure == Never { 5 | 6 | /// Invokes method on an object with a publisher's output. 7 | public func invoke(_ method: @escaping (Target) -> (Output) -> Void, on object: Target) -> AnyCancellable { 8 | let invoke = Subscribers.Invoke(object: object, method: method) 9 | self.subscribe(invoke) 10 | return AnyCancellable(invoke) 11 | } 12 | 13 | /// Invokes method on an object with a publisher's output. The object is 14 | /// weakly captured. 15 | public func invoke(_ method: @escaping (Target) -> (Output) -> Void, weaklyOn object: Target) -> AnyCancellable { 16 | let invoke = Subscribers.Invoke(nonretainedObject: object, method: method) 17 | self.subscribe(invoke) 18 | return AnyCancellable(invoke) 19 | } 20 | } 21 | 22 | extension Publisher where Output == Void, Failure == Never { 23 | 24 | /// Invokes method on an object whenever a publisher produces an output. 25 | public func invoke(_ method: @escaping (Target) -> () -> Void, on object: Target) -> AnyCancellable { 26 | let invoke = Subscribers.Invoke(object: object, method: method) 27 | self.subscribe(invoke) 28 | return AnyCancellable(invoke) 29 | } 30 | 31 | /// Invokes method on an object whenever a publisher produces an output. The 32 | /// object is weakly captured. 33 | public func invoke(_ method: @escaping (Target) -> () -> Void, weaklyOn object: Target) -> AnyCancellable { 34 | let invoke = Subscribers.Invoke(nonretainedObject: object, method: method) 35 | self.subscribe(invoke) 36 | return AnyCancellable(invoke) 37 | } 38 | } 39 | 40 | extension Subscribers { 41 | 42 | /// A simple subscriber that invokes method on an object with each element 43 | /// received from a `Publisher`. 44 | public final class Invoke: Subscriber, Cancellable, CustomStringConvertible, CustomReflectable, CustomPlaygroundDisplayConvertible { 45 | 46 | public typealias Failure = Never 47 | 48 | private enum RefBox { 49 | 50 | final class WeakBox { 51 | weak var object: Target? 52 | init(_ object: Target) { 53 | self.object = object 54 | } 55 | } 56 | 57 | case _strong(Target) 58 | case _weak(WeakBox) 59 | 60 | static func strong(_ object: Target) -> RefBox { 61 | return ._strong(object) 62 | } 63 | 64 | static func weak(_ object: Target) -> RefBox { 65 | return ._weak(WeakBox(object)) 66 | } 67 | 68 | var object: Target? { 69 | switch self { 70 | case let ._strong(object): return object 71 | case let ._weak(box): return box.object 72 | } 73 | } 74 | } 75 | 76 | private enum Method { 77 | case withParameter((Target) -> (Input) -> Void) 78 | case withoutParameter((Target) -> () -> Void) 79 | 80 | var body: Any { 81 | switch self { 82 | case let .withParameter(body): return body 83 | case let .withoutParameter(body): return body 84 | } 85 | } 86 | } 87 | 88 | private var object: RefBox? 89 | 90 | private let method: Method 91 | 92 | private let lock = NSLock() 93 | private var subscription: Subscription? 94 | 95 | private init(object: RefBox, method: Method) { 96 | self.object = object 97 | self.method = method 98 | } 99 | 100 | public func receive(subscription: Subscription) { 101 | self.lock.lock() 102 | if self.subscription == nil { 103 | self.subscription = subscription 104 | self.lock.unlock() 105 | subscription.request(.unlimited) 106 | } else { 107 | self.lock.unlock() 108 | subscription.cancel() 109 | } 110 | } 111 | 112 | public func receive(_ value: Input) -> Subscribers.Demand { 113 | self.lock.lock() 114 | guard self.subscription != nil, let obj = self.object?.object else { 115 | self.lock.unlock() 116 | return .none 117 | } 118 | self.lock.unlock() 119 | switch method { 120 | case let .withParameter(body): 121 | body(obj)(value) 122 | case let .withoutParameter(body): 123 | body(obj)() 124 | } 125 | 126 | return .none 127 | } 128 | 129 | public func receive(completion: Subscribers.Completion) { 130 | self.cancel() 131 | } 132 | 133 | public func cancel() { 134 | self.lock.lock() 135 | guard let subscription = self.subscription else { 136 | self.lock.unlock() 137 | return 138 | } 139 | 140 | self.subscription = nil 141 | self.object = nil 142 | self.lock.unlock() 143 | 144 | subscription.cancel() 145 | } 146 | 147 | public var description: String { 148 | return "Invoke \(Target.self)" 149 | } 150 | 151 | public var customMirror: Mirror { 152 | return Mirror(self, children: [ 153 | "object": self.object?.object as Any, 154 | "method": self.method.body, 155 | "upstreamSubscription": self.subscription as Any 156 | ]) 157 | } 158 | 159 | public var playgroundDescription: Any { 160 | return self.description 161 | } 162 | } 163 | } 164 | 165 | extension Subscribers.Invoke { 166 | 167 | public convenience init(object: Target, method: @escaping (Target) -> (Input) -> Void) { 168 | self.init(object: .strong(object), method: .withParameter(method)) 169 | } 170 | 171 | public convenience init(nonretainedObject object: Target, method: @escaping (Target) -> (Input) -> Void) { 172 | self.init(object: .weak(object), method: .withParameter(method)) 173 | } 174 | } 175 | 176 | extension Subscribers.Invoke where Input == Void { 177 | 178 | public convenience init(object: Target, method: @escaping (Target) -> () -> Void) { 179 | self.init(object: .strong(object), method: .withoutParameter(method)) 180 | } 181 | 182 | public convenience init(nonretainedObject object: Target, method: @escaping (Target) -> () -> Void) { 183 | self.init(object: .weak(object), method: .withoutParameter(method)) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /Sources/CXExtensions/SelfRetainedCancellable.swift: -------------------------------------------------------------------------------- 1 | import CXShim 2 | 3 | extension Publisher { 4 | 5 | @discardableResult 6 | public func selfRetained(subscribing: (AnyPublisher) -> AnyCancellable) -> AnyCancellable { 7 | var retainCycle: AnyCancellable? 8 | let pub = handleEvents(receiveCompletion: { _ in 9 | retainCycle?.cancel() 10 | retainCycle = nil 11 | }, receiveCancel: { 12 | retainCycle?.cancel() 13 | retainCycle = nil 14 | }).eraseToAnyPublisher() 15 | let canceller = subscribing(pub) 16 | retainCycle = canceller 17 | return canceller 18 | } 19 | } 20 | 21 | extension Cancellable where Self: AnyObject { 22 | 23 | public func selfRetained() -> SelfRetainedCancellable { 24 | return SelfRetainedCancellable(wrapping: self) 25 | } 26 | } 27 | 28 | public final class SelfRetainedCancellable: Cancellable { 29 | 30 | private var wrapped: Child? 31 | 32 | private var retainCycle: SelfRetainedCancellable? 33 | 34 | public init(wrapping wrapped: Child) { 35 | self.wrapped = wrapped 36 | retainCycle = self 37 | } 38 | 39 | public func cancel() { 40 | wrapped?.cancel() 41 | wrapped = nil 42 | retainCycle = nil 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/CXExtensions/Signal.swift: -------------------------------------------------------------------------------- 1 | import CXShim 2 | 3 | extension Publisher { 4 | 5 | /// Emits a signal (`Void()`) whenever upstream publisher produce an element. 6 | public func signal() -> Publishers.Signal { 7 | return .init(upstream: self) 8 | } 9 | } 10 | 11 | extension Publishers { 12 | 13 | /// A publisher that emits a signal (`Void()`) whenever upstream publisher 14 | /// produce an element. 15 | public struct Signal: Publisher { 16 | 17 | public typealias Output = Void 18 | public typealias Failure = Upstream.Failure 19 | 20 | public let upstream: Upstream 21 | 22 | public init(upstream: Upstream) { 23 | self.upstream = upstream 24 | } 25 | 26 | public func receive(subscriber: S) where Failure == S.Failure, Output == S.Input { 27 | self.upstream 28 | .map { _ in Void() } 29 | .receive(subscriber: subscriber) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/CXExtensions/WeakAssign.swift: -------------------------------------------------------------------------------- 1 | import CXShim 2 | import Foundation 3 | 4 | extension Publisher where Failure == Never { 5 | 6 | /// Assigns a publisher’s output to a property of an object. The object is 7 | /// weakly captured. 8 | public func assign(to keyPath: ReferenceWritableKeyPath, weaklyOn object: Root) -> AnyCancellable { 9 | let assign = Subscribers.WeakAssign(object: object, keyPath: keyPath) 10 | self.subscribe(assign) 11 | return AnyCancellable(assign) 12 | } 13 | } 14 | 15 | extension Subscribers { 16 | 17 | /// Similar to `Subscribers.Assign`, but captures its target weakly. 18 | public final class WeakAssign: Subscriber, Cancellable, CustomStringConvertible, CustomReflectable, CustomPlaygroundDisplayConvertible { 19 | 20 | public typealias Failure = Never 21 | 22 | /// The object that contains the property to assign. 23 | public private(set) weak var object: Root? 24 | 25 | /// The key path that indicates the property to assign. 26 | public let keyPath: ReferenceWritableKeyPath 27 | 28 | private let lock = NSLock() 29 | private var subscription: Subscription? 30 | 31 | public init(object: Root, keyPath: ReferenceWritableKeyPath) { 32 | self.object = object 33 | self.keyPath = keyPath 34 | } 35 | 36 | public func receive(subscription: Subscription) { 37 | self.lock.lock() 38 | if self.subscription == nil { 39 | self.subscription = subscription 40 | self.lock.unlock() 41 | subscription.request(.unlimited) 42 | } else { 43 | self.lock.unlock() 44 | subscription.cancel() 45 | } 46 | } 47 | 48 | public func receive(_ value: Input) -> Subscribers.Demand { 49 | self.lock.lock() 50 | if self.subscription == nil { 51 | self.lock.unlock() 52 | } else { 53 | let obj = self.object 54 | self.lock.unlock() 55 | 56 | obj?[keyPath: self.keyPath] = value 57 | } 58 | return .none 59 | } 60 | 61 | public func receive(completion: Subscribers.Completion) { 62 | self.cancel() 63 | } 64 | 65 | public func cancel() { 66 | self.lock.lock() 67 | guard let subscription = self.subscription else { 68 | self.lock.unlock() 69 | return 70 | } 71 | 72 | self.subscription = nil 73 | self.object = nil 74 | self.lock.unlock() 75 | 76 | subscription.cancel() 77 | } 78 | 79 | public var description: String { 80 | return "WeakAssign \(Root.self)" 81 | } 82 | 83 | public var customMirror: Mirror { 84 | return Mirror(self, children: [ 85 | "object": self.object as Any, 86 | "keyPath": self.keyPath, 87 | "upstreamSubscription": self.subscription as Any 88 | ]) 89 | } 90 | 91 | public var playgroundDescription: Any { 92 | return self.description 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Tests/CXExtensionsTests/AnySchedulerSpec.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import CXTest 3 | import CXShim 4 | import CXExtensions 5 | 6 | class AnySchedulerTests: XCTestCase { 7 | 8 | func testAnyScheduler() { 9 | let scheduler = VirtualTimeScheduler() 10 | let anyScheduler = AnyScheduler(scheduler) 11 | var events: [Int] = [] 12 | var cancellers = Set() 13 | anyScheduler.schedule { 14 | events.append(1) 15 | } 16 | anyScheduler.schedule(after: anyScheduler.now.advanced(by: .seconds(10))) { 17 | events.append(2) 18 | anyScheduler.schedule(after: anyScheduler.now.advanced(by: .seconds(20))) { 19 | events.append(3) 20 | cancellers.removeAll() 21 | } 22 | } 23 | anyScheduler.schedule { 24 | events.append(4) 25 | } 26 | anyScheduler.schedule(after: anyScheduler.now.advanced(by: .seconds(5)), interval: .seconds(10)) { 27 | events.append(5) 28 | }.store(in: &cancellers) 29 | scheduler.advance(by: 0) 30 | XCTAssertEqual(events, [1, 4]) 31 | scheduler.advance(by: 40) 32 | XCTAssertEqual(events, [1, 4, 5, 2, 5, 5, 3]) 33 | // time: 0, 0, 5, 10,15,25,30 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Tests/CXExtensionsTests/BlockingSpec.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Dispatch 3 | import XCTest 4 | import CXTest 5 | import CXShim 6 | @testable import CXExtensions 7 | 8 | class BlockingTests: XCTestCase { 9 | 10 | // TODO: `Thread.detachNewThread(_:)` has system requirement. 11 | @available(macOS 10.12, iOS 10.10, tvOS 10.10, watchOS 3.0, *) 12 | func testNextValue() { 13 | let pub = PassthroughSubject() 14 | Thread.detachNewThread { 15 | Thread.sleep(forTimeInterval: 0.01) 16 | pub.send(1) 17 | pub.send(2) 18 | } 19 | let sub = pub.blocking() 20 | let value = sub.next() 21 | XCTAssertEqual(value, 1) 22 | XCTAssertNil(sub.completion) 23 | } 24 | 25 | @available(macOS 10.12, iOS 10.10, tvOS 10.10, watchOS 3.0, *) 26 | func testNextFailure() { 27 | let pub = PassthroughSubject() 28 | Thread.detachNewThread { 29 | Thread.sleep(forTimeInterval: 0.01) 30 | pub.send(completion: .failure(.e0)) 31 | } 32 | let sub = pub.blocking() 33 | let value = sub.next() 34 | XCTAssertNil(value) 35 | XCTAssertEqual(sub.completion, .failure(.e0)) 36 | } 37 | 38 | @available(macOS 10.12, iOS 10.10, tvOS 10.10, watchOS 3.0, *) 39 | func testSequenceConformance() { 40 | let source = Array(0..<10) 41 | let pub = source.cx.publisher 42 | let sub = pub.blocking() 43 | Thread.detachNewThread { 44 | Thread.sleep(forTimeInterval: 1) 45 | // in case of deadlock 46 | sub.cancel() 47 | } 48 | let result = Array(sub) 49 | XCTAssertEqual(result, source) 50 | XCTAssertEqual(sub.completion, .finished) 51 | } 52 | 53 | // func testRunloopBlocking() { 54 | // let pub = PassthroughSubject() 55 | // RunLoop.current.cx.schedule { 56 | // pub.send(1) 57 | // } 58 | // let value = pub.blocking().next() 59 | // expect(value) == 1 60 | // } 61 | } 62 | 63 | private enum E: Error { 64 | case e0 65 | case e1 66 | } 67 | -------------------------------------------------------------------------------- /Tests/CXExtensionsTests/CXTest+Extensions.swift: -------------------------------------------------------------------------------- 1 | import CXShim 2 | import CXTest 3 | 4 | extension TracingSubscriber { 5 | 6 | convenience init(initialDemand: Subscribers.Demand) { 7 | self.init(receiveSubscription: { subscription in 8 | subscription.request(initialDemand) 9 | }) 10 | } 11 | } 12 | 13 | extension TracingSubscriber: Cancellable { 14 | public func cancel() { 15 | self.subscription?.cancel() 16 | self.releaseSubscription() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/CXExtensionsTests/DelayedAutoCancellableSpec.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import CXTest 3 | import CXShim 4 | import CXExtensions 5 | 6 | class DelayedAutoCancellableTests: XCTestCase { 7 | 8 | func testCancel() { 9 | let pub = PassthroughSubject() 10 | let sub = TracingSubscriber(initialDemand: .unlimited) 11 | let scheduler = VirtualTimeScheduler() 12 | pub.subscribe(sub) 13 | let canceller = sub.cancel(after: 5, scheduler: scheduler) 14 | XCTAssertEqual(sub.events.count, 1) 15 | pub.send(1) 16 | XCTAssertEqual(sub.events.count, 2) 17 | scheduler.advance(by: 2) 18 | XCTAssertNotNil(sub.subscription) 19 | pub.send(2) 20 | XCTAssertEqual(sub.events.count, 3) 21 | scheduler.advance(by: 3) 22 | XCTAssertNil(sub.subscription) // auto cancel here 23 | pub.send(3) 24 | XCTAssertEqual(sub.events.count, 3) 25 | canceller.cancel() 26 | XCTAssertNil(sub.subscription) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/CXExtensionsTests/IgnoreErrorSpec.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import CXTest 3 | import CXShim 4 | import CXExtensions 5 | 6 | class IgnoreErrorTests: XCTestCase { 7 | 8 | func testIgnoreError() { 9 | let pub = PassthroughSubject() 10 | let sub = TracingSubscriber(initialDemand: .unlimited) 11 | pub.ignoreError().subscribe(sub) 12 | XCTAssertEqual(sub.events.count, 1) 13 | pub.send(1) 14 | XCTAssertEqual(sub.events.count, 2) 15 | pub.send(completion: .failure(.e0)) 16 | XCTAssertEqual(sub.events.dropFirst(), [.value(1), .completion(.finished)]) 17 | } 18 | } 19 | 20 | private enum E: Error { 21 | case e0 22 | case e1 23 | } 24 | -------------------------------------------------------------------------------- /Tests/CXExtensionsTests/InvokeSpec.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import CXShim 3 | import CXExtensions 4 | 5 | class InvokeTests: XCTestCase { 6 | 7 | func testStrongRef() { 8 | let pub = PassthroughSubject() 9 | weak var weakObj: AnyObject? 10 | let connection: AnyCancellable 11 | do { 12 | let obj = A() 13 | weakObj = obj 14 | connection = pub.invoke(A.action, on: obj) 15 | XCTAssertEqual(obj.events, []) 16 | pub.send(1) 17 | XCTAssertEqual(obj.events, [1]) 18 | pub.send(2) 19 | XCTAssertEqual(obj.events, [1, 2]) 20 | } 21 | XCTAssertNotNil(weakObj) 22 | connection.cancel() 23 | XCTAssertNil(weakObj) 24 | } 25 | 26 | func testWeakRef() { 27 | let pub = PassthroughSubject() 28 | weak var weakObj: AnyObject? 29 | let connection: AnyCancellable 30 | do { 31 | let obj = A() 32 | weakObj = obj 33 | connection = pub.invoke(A.action, weaklyOn: obj) 34 | XCTAssertEqual(obj.events, []) 35 | pub.send(1) 36 | XCTAssertEqual(obj.events, [1]) 37 | } 38 | XCTAssertNil(weakObj) 39 | connection.cancel() 40 | } 41 | 42 | func testVoidOutput() { 43 | let pub = PassthroughSubject() 44 | let obj = B() 45 | let connection = pub.invoke(B.action, on: obj) 46 | XCTAssertEqual(obj.eventCount, 0) 47 | pub.send() 48 | XCTAssertEqual(obj.eventCount, 1) 49 | connection.cancel() 50 | } 51 | } 52 | 53 | private class A { 54 | 55 | var events: [T] = [] 56 | 57 | func action(_ v: T) { 58 | events.append(v) 59 | } 60 | } 61 | 62 | private class B { 63 | 64 | var eventCount = 0 65 | 66 | func action() { 67 | eventCount += 1 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Tests/CXExtensionsTests/WeakAssignSpec.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import CXShim 3 | import CXExtensions 4 | 5 | class WeakAssignTests: XCTestCase { 6 | 7 | func testWeakRef() { 8 | let pub = PassthroughSubject() 9 | weak var weakObj: AnyObject? 10 | let connection: AnyCancellable 11 | do { 12 | let obj = A() 13 | weakObj = obj 14 | connection = pub.assign(to: \A.x, weaklyOn: obj) 15 | XCTAssertEqual(obj.events, []) 16 | pub.send(1) 17 | XCTAssertEqual(obj.events, [1]) 18 | } 19 | XCTAssertNil(weakObj) 20 | connection.cancel() 21 | } 22 | } 23 | 24 | private class A { 25 | 26 | var events: [Int] = [] 27 | 28 | var x = 0 { 29 | didSet { 30 | events.append(x) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | #error("Run the tests with `swift test --enable-test-discovery`.") 2 | --------------------------------------------------------------------------------