├── .gitignore ├── .github ├── PULL_REQUEST_TEMPLATE │ ├── documentation.md │ └── code.md ├── workflows │ └── tests.yml ├── pull_request_template.md └── ISSUE_TEMPLATE │ ├── question.md │ ├── feature_request.md │ └── bug_report.md ├── sources ├── testing │ ├── XCTestCase.swift │ └── Expectations.swift └── runtime │ ├── operators │ ├── HandleEndOp.swift │ ├── ThenOp.swift │ ├── AwaitOp.swift │ ├── RetryOp.swift │ ├── SinkOp.swift │ ├── ResultOp.swift │ └── AssignOp.swift │ ├── utils │ ├── Buffer.swift │ ├── State.swift │ └── Lock.swift │ ├── subscribers │ ├── GraduatedSink.swift │ └── FixedSink.swift │ └── publishers │ ├── DeferredResult.swift │ ├── DeferredValue.swift │ ├── DeferredTryComplete.swift │ ├── DeferredTryValue.swift │ ├── DeferredComplete.swift │ ├── DeferredFuture.swift │ ├── HandleEnd.swift │ ├── DeferredPassthrough.swift │ ├── Retry.swift │ └── Then.swift ├── Package.swift ├── tests ├── runtime │ ├── operators │ │ ├── AwaitOpTests.swift │ │ ├── AssignOpTests.swift │ │ ├── ThenOpTests.swift │ │ ├── ResultOpTests.swift │ │ ├── DelayedRetryOpTests.swift │ │ └── HandleEndOpTests.swift │ ├── publishers │ │ ├── DeferredValueTests.swift │ │ ├── DeferredTryValueTests.swift │ │ ├── DeferredResultTests.swift │ │ ├── DeferredPassthroughTests.swift │ │ ├── DeferredTryCompleteTests.swift │ │ ├── DeferredFutureTests.swift │ │ └── DeferredCompleteTests.swift │ ├── subscribers │ │ ├── GraduatedSinkTests.swift │ │ └── FixedSinkTests.swift │ └── utils │ │ └── BufferTests.swift └── testing │ └── ExpectationsTests.swift ├── LICENSE ├── docs ├── assets │ ├── badges │ │ ├── License.svg │ │ ├── Swift.svg │ │ └── Apple.svg │ └── Conbini.svg ├── CONTRIBUTING.md ├── SUPPORT.md └── CODE_OF_CONDUCT.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | /.swiftpm 7 | 8 | # Project specific 9 | /Assets/Originals 10 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation 3 | about: Fix typos, rephrase documentation, or add missing context. 4 | 5 | --- 6 | 7 | ## Description 8 | 9 | Brief description of what you have changed/added. -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [ push, pull_request ] 3 | 4 | jobs: 5 | tests_on_macOS: 6 | name: Tests on macOS 7 | runs-on: macos-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Build 11 | run: swift build -v 12 | - name: Run tests 13 | run: swift test -v 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/code.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Code 3 | about: Tweak or add code to Conbini 4 | 5 | --- 6 | 7 | ## Description 8 | 9 | Brief description of what you have changed/added. 10 | 11 | ## Checklist 12 | 13 | - [ ] Include in-code documentation at the top of the property/function/structure/class (if necessary). 14 | - [ ] Merge to [`develop`](https://github.com/dehesa/Conbini/tree/develop). 15 | - [ ] Add to existing tests or create new tests (if necessary). 16 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Brief description of what you have changed/added. 4 | 5 | ## Checklist 6 | 7 | The following list must only be fulfilled by code-changing PRs. If you are making changes on the documentation, ignore these. 8 | 9 | - [ ] Include in-code documentation at the top of the property/function/structure/class (if necessary). 10 | - [ ] Merge to [`develop`](https://github.com/dehesa/Conbini/tree/develop). 11 | - [ ] Add to existing tests or create new tests (if necessary). 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask anything related to Combine or Conbini 4 | title: '' 5 | labels: question 6 | assignees: dehesa 7 | 8 | --- 9 | 10 | ## Question 11 | One or two-liner with your question. 12 | 13 | ## Additional Context 14 | Add any other context about the question here. 15 | 16 | ## System 17 | Delete section if not applicable 18 | - OS: [e.g. macOS 11, iOS 14] 19 | - Conbini: [e.g. 0.6.1] 20 | You can check this in your SPM `Package.swift` file (or `Package.resolved` file). Alternatively, go to Xcode's Source Control Navigator (`⌘+2`) and click on `Conbini`. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: dehesa 7 | 8 | --- 9 | 10 | ## Is your feature request related to a problem? 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | ## Describe the solution you'd like 14 | A clear and concise description of what you want to happen. 15 | 16 | ## Describe alternatives you've considered 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | ## Additional context 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /sources/testing/XCTestCase.swift: -------------------------------------------------------------------------------- 1 | #if !os(watchOS) 2 | import XCTest 3 | import Combine 4 | import Foundation 5 | 6 | extension XCTestCase { 7 | /// Locks the receiving test for `interval` seconds. 8 | /// - parameter interval: The number of seconds waiting (must be greater than zero). 9 | public func wait(seconds interval: TimeInterval) { 10 | precondition(interval > 0) 11 | 12 | let e = self.expectation(description: "Waiting for \(interval) seconds") 13 | let timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { 14 | $0.invalidate() 15 | e.fulfill() 16 | } 17 | 18 | self.wait(for: [e], timeout: interval) 19 | timer.invalidate() 20 | } 21 | } 22 | 23 | #endif 24 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | import PackageDescription 3 | 4 | var package = Package( 5 | name: "package-conbini", 6 | platforms: [ 7 | .macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6) 8 | ], 9 | products: [ 10 | .library(name: "Conbini", targets: ["Conbini"]), 11 | .library(name: "ConbiniForTesting", targets: ["ConbiniForTesting"]) 12 | ], 13 | dependencies: [], 14 | targets: [ 15 | .target(name: "Conbini", path: "sources/runtime"), 16 | .target(name: "ConbiniForTesting", path: "sources/testing"), 17 | .testTarget(name: "ConbiniTests", dependencies: ["Conbini"], path: "tests/runtime"), 18 | .testTarget(name: "ConbiniForTestingTests", dependencies: ["Conbini", "ConbiniForTesting"], path: "tests/testing"), 19 | ] 20 | ) 21 | -------------------------------------------------------------------------------- /sources/runtime/operators/HandleEndOp.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | extension Publisher { 4 | /// Performs the specified closure when the publisher completes (whether successfully or with a failure) or when the publisher gets cancelled. 5 | /// 6 | /// The closure will get executed exactly once. 7 | /// - parameter handle: A closure that executes when the publisher receives a completion event or when the publisher gets cancelled. 8 | /// - parameter completion: A completion event if the publisher completes (whether successfully or not), or `nil` in case the publisher is cancelled. 9 | @inlinable public func handleEnd(_ handle: @escaping (_ completion: Subscribers.Completion?)->Void) -> Publishers.HandleEnd { 10 | .init(upstream: self, handle: handle) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /sources/runtime/utils/Buffer.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | extension Publishers.BufferingStrategy { 4 | /// Unconditionally prints a given message and stops execution when the buffer exhaust its capacity. 5 | /// - parameter message: The string to print. The default is an empty string. 6 | /// - parameter file: The file name to print with `message`. The default is the file where this function is called. 7 | /// - parameter line: The line number to print along with `message`. The default is the line number where `fatalError()` 8 | @_transparent public static func fatalError(_ message: @autoclosure @escaping () -> String = String(), file: StaticString = #file, line: UInt = #line) -> Self { 9 | .customError({ 10 | Swift.fatalError(message(), file: file, line: line) 11 | }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: dehesa 7 | 8 | --- 9 | 10 | ## Describe the bug 11 | A clear and concise description of what the bug is. 12 | 13 | ## To Reproduce 14 | Steps to reproduce the behavior: 15 | 1. Create entity '...' 16 | 2. Perform action '....' 17 | 18 | ## Expected behavior 19 | A clear and concise description of what you expected to happen. 20 | 21 | ## System 22 | - OS: [e.g. macOS 11, iOS 14] 23 | - Conbini: [e.g. 0.6.1] 24 | You can check this in your SPM `Package.swift` file (or `Package.resolved` file). Alternatively, go to Xcode's Source Control Navigator (`⌘+2`) and click on `Conbini`. 25 | 26 | ## Additional context 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /tests/runtime/operators/AwaitOpTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Conbini 3 | import Combine 4 | 5 | /// Tests the correct behavior of the `await` operator. 6 | final class AwaitOpTests: XCTestCase { 7 | override func setUp() { 8 | self.continueAfterFailure = false 9 | } 10 | } 11 | 12 | extension AwaitOpTests { 13 | /// Tests the `await` operator. 14 | func testAwait() { 15 | let publisher = Just("Hello") 16 | .delay(for: 1, scheduler: DispatchQueue.global()) 17 | 18 | let queue = DispatchQueue(label: "io.dehesa.conbini.tests.await") 19 | let cancellable = Just(()) 20 | .delay(for: 10, scheduler: queue) 21 | .sink { XCTFail("The await test failed") } 22 | 23 | let greeting = publisher.await 24 | XCTAssertEqual(greeting, "Hello") 25 | 26 | cancellable.cancel() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /sources/runtime/operators/ThenOp.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | extension Publisher { 4 | /// Ignores all upstream value events and when it completes successfully, the operator switches to the provided publisher. 5 | /// 6 | /// The `transform` closure will only be executed once a successful completion event arrives. If a completion event doesn't arrive or the completion is a failure, the closure is never executed. 7 | /// - parameter maxDemand: The maximum demand requested to the upstream at the same time. For example, if `.max(5)` is requested, a demand of 5 is kept until the upstream completes. 8 | /// - parameter transform: Closure generating the stream to be switched to once a successful completion event is received from upstream. 9 | @inlinable public func then(maxDemand: Subscribers.Demand = .unlimited, _ transform: @escaping ()->Child) -> Publishers.Then where Child:Publisher, Self.Failure==Child.Failure { 10 | Publishers.Then(upstream: self, maxDemand: maxDemand, transform: transform) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /sources/runtime/operators/AwaitOp.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | extension Publisher { 5 | /// Subscribes to the receiving publichser and expects a single value and a subsequent successfull completion. 6 | /// 7 | /// If no values is received, or more than one value is received, or a failure is received, the program will crash. 8 | /// - warning: The publisher must receive the value and completion event in a different queue from the queue where this property is called or the code will never execute. 9 | @inlinable public var await: Output { 10 | let group = DispatchGroup() 11 | group.enter() 12 | 13 | var value: Output? = nil 14 | let cancellable = self.sink(fixedDemand: 1, receiveCompletion: { 15 | switch $0 { 16 | case .failure(let error): fatalError("\(error)") 17 | case .finished: 18 | guard case .some = value else { fatalError() } 19 | group.leave() 20 | } 21 | }, receiveValue: { value = $0 }) 22 | 23 | group.wait() 24 | cancellable.cancel() 25 | return value! 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Marcos Sánchez-Dehesa 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 | -------------------------------------------------------------------------------- /tests/runtime/publishers/DeferredValueTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Conbini 3 | import Combine 4 | 5 | /// Tests the correct behavior of the `DeferredValueTests` publisher. 6 | final class DeferredValueTests: XCTestCase { 7 | /// A convenience storage of cancellables. 8 | private var _cancellables = Set() 9 | 10 | override func setUp() { 11 | self.continueAfterFailure = false 12 | self._cancellables.removeAll() 13 | } 14 | 15 | override func tearDown() { 16 | self._cancellables.removeAll() 17 | } 18 | } 19 | 20 | extension DeferredValueTests { 21 | /// Tests a successful delivery of an emitted value. 22 | func testSuccessfulDelivery() { 23 | let exp = self.expectation(description: "Publisher completes successfully") 24 | 25 | let value = 42 26 | DeferredTryValue { value } 27 | .sink(receiveCompletion: { 28 | guard case .finished = $0 else { return XCTFail("The deffered value publisher has failed!") } 29 | exp.fulfill() 30 | }, receiveValue: { XCTAssertEqual($0, value) }) 31 | .store(in: &self._cancellables) 32 | 33 | self.wait(for: [exp], timeout: 0.2) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /sources/runtime/operators/RetryOp.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | extension Publisher { 5 | /// Attempts to recreate a failed subscription with the upstream publisher using a specified number of attempts to establish the connection and a given amount of seconds between attempts. 6 | /// - parameter scheduler: The scheduler used to wait for the specific intervals. 7 | /// - parameter tolerance: The tolerance used when scheduling a new attempt after a failure. A default implies the minimum tolerance. 8 | /// - parameter options: The options for the given scheduler. 9 | /// - parameter intervals: The amount of seconds to wait after a failure occurrence. Negative values are considered zero. 10 | /// - returns: A publisher that attemps to recreate its subscription to a failed upstream publisher a given amount of times and waiting a given amount of seconds between attemps. 11 | @inlinable public func retry(on scheduler: S, tolerance: S.SchedulerTimeType.Stride? = nil, options: S.SchedulerOptions? = nil, intervals: [TimeInterval]) -> Publishers.DelayedRetry where S:Scheduler { 12 | .init(upstream: self, scheduler: scheduler, options: options, intervals: intervals) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /docs/assets/badges/License.svg: -------------------------------------------------------------------------------- 1 | MITLicense -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | There are several ways you can contribute to [Conbini](https://www.github.com/dehesa/Conbini): 4 | 5 | - Fix typos, rephrase documentation, or add missing context. 6 | - [Create issues](https://github.com/dehesa/Conbini/issues) for bugs, enhacement suggesstions, etc. 7 | - Send [Pull Requests](https://github.com/dehesa/Conbini/pulls) (PRs) with new code. 8 | - Spread awereness 😄 9 | 10 | The only contribution requirements are: 11 | 12 | - Make all your changes on [`develop`](https://github.com/dehesa/Conbini/tree/develop). 13 | - Try to add tests to new enhacements. 14 | - Make sure all tests pass (the CI will check that for you). 15 | 16 | That is it. Thank you for taking the time to read this and for contributing. 17 | 18 | ## Looking where to start 19 | 20 | This repo's [Github projects](https://github.com/dehesa/Conbini/projects) are kept up to date and they are a good place to look if you don't know where to start. 21 | 22 | - The [_features_ project](https://github.com/dehesa/Conbini/projects/1) contains the features that are being worked on or it has been agreed to work on in the near future. 23 | 24 | Check the "Planned" column and pick a task. 25 | 26 | - The [_bugs_ project](https://github.com/dehesa/Conbini/projects/2) contains a list of known issues. 27 | 28 | Feel free to pick any. 29 | 30 | - The [_ideas_ project](https://github.com/dehesa/Conbini/projects/3) contains a list of suggestions or future plans that haven't been properly investigated. 31 | 32 | These are a bit more "futuristics" and many of them may never be undertook. However, they are worth exploring. 33 | -------------------------------------------------------------------------------- /tests/runtime/publishers/DeferredTryValueTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Conbini 3 | import Combine 4 | 5 | /// Tests the correct behavior of the `DeferredTryValue` publisher. 6 | final class DeferredTryValueTests: XCTestCase { 7 | /// A convenience storage of cancellables. 8 | private var _cancellables = Set() 9 | 10 | override func setUp() { 11 | self.continueAfterFailure = false 12 | self._cancellables.removeAll() 13 | } 14 | 15 | override func tearDown() { 16 | self._cancellables.removeAll() 17 | } 18 | 19 | /// A custom error to send as a dummy. 20 | private struct _CustomError: Swift.Error {} 21 | } 22 | 23 | extension DeferredTryValueTests { 24 | /// Tests a successful delivery of an emitted value. 25 | func testSuccessfulDelivery() { 26 | let exp = self.expectation(description: "Publisher completes successfully") 27 | 28 | let value = 42 29 | DeferredTryValue { value } 30 | .sink(receiveCompletion: { 31 | guard case .finished = $0 else { return XCTFail("The deffered value publisher has failed!") } 32 | exp.fulfill() 33 | }, receiveValue: { XCTAssertEqual($0, value) }) 34 | .store(in: &self._cancellables) 35 | 36 | self.wait(for: [exp], timeout: 0.2) 37 | } 38 | 39 | /// Tests a failed value generation. 40 | func testFailedCompletion() { 41 | let exp = self.expectation(description: "Publisher completes with a failure") 42 | 43 | DeferredTryValue { throw _CustomError() } 44 | .sink(receiveCompletion: { 45 | guard case .failure = $0 else { return XCTFail("The deffered value publisher has completed successfully!") } 46 | exp.fulfill() 47 | }, receiveValue: { _ in XCTFail("The deferred complete has emitted a value!") }) 48 | .store(in: &self._cancellables) 49 | 50 | self.wait(for: [exp], timeout: 0.2) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /sources/runtime/operators/SinkOp.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | extension Publisher { 4 | /// Subscribe to the receiving publisher and request exactly `fixedDemand` values. 5 | /// 6 | /// This operator may receive zero to `fixedDemand` of values before completing, but no more. 7 | /// - parameter fixedDemand: The maximum number of values to be received. 8 | /// - parameter receiveCompletion: The closure executed when the provided amount of values are received or a completion event is received. 9 | /// - parameter receiveValue: The closure executed when a value is received. 10 | @inlinable public func sink(fixedDemand: Int, receiveCompletion: ((Subscribers.Completion)->Void)?, receiveValue: ((Output)->Void)?) -> AnyCancellable { 11 | let subscriber = Subscribers.FixedSink(demand: fixedDemand, receiveCompletion: receiveCompletion, receiveValue: receiveValue) 12 | let cancellable = AnyCancellable(subscriber) 13 | self.subscribe(subscriber) 14 | return cancellable 15 | } 16 | 17 | /// Subscribe to the receiving publisher requesting `maxDemand` values and always keeping the same backpressure. 18 | /// - parameter maxDemand: The maximum number of in-flight values. 19 | /// - parameter receiveCompletion: The closure executed when the provided amount of values are received or a completion event is received. 20 | /// - parameter receiveValue: The closure executed when a value is received. 21 | @inlinable public func sink(maxDemand: Subscribers.Demand, receiveCompletion: ((Subscribers.Completion)->Void)?, receiveValue: ((Output)->Void)?) -> AnyCancellable { 22 | let subscriber = Subscribers.GraduatedSink(maxDemand: maxDemand, receiveCompletion: receiveCompletion, receiveValue: receiveValue) 23 | let cancellable = AnyCancellable(subscriber) 24 | self.subscribe(subscriber) 25 | return cancellable 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /sources/runtime/operators/ResultOp.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | extension Publisher { 4 | /// Subscribes to the upstream and expects a single value and a subsequent successful completion. 5 | /// 6 | /// The underlying subscriber is `Subscriber.FixedSink`, which makes it impossible to receive more than one value. That said, here are the possible scenarios: 7 | /// - If a single value is published, the handler is called with such value. 8 | /// - If a failure occurs; the handler is called with such failure. 9 | /// - If a completion occurs and no value has been sent, the subscriber gets cancelled, `onEmpty` is called, and depending on whether an error is generated, the handler is called or not. 10 | /// - parameter onEmpty: Autoclosure generating an optional error to pass to the `handler` when upstream doesn't behave as expected. If `nil`, the `handler` won't be called when no values are published. 11 | /// - parameter handler: Returns the result of the publisher. 12 | /// - parameter result: The value yielded after the subscription. 13 | /// - returns: `Cancellable` able to stop/cancel the subscription. 14 | @inlinable @discardableResult public func result(onEmpty: @escaping @autoclosure ()->Failure? = nil, _ handler: @escaping (_ result: Result)->Void) -> AnyCancellable { 15 | var value: Output? = nil 16 | 17 | let subscriber = Subscribers.FixedSink(demand: 1, receiveCompletion: { 18 | switch $0 { 19 | case .failure(let error): 20 | handler(.failure(error)) 21 | case .finished: 22 | if let value = value { 23 | handler(.success(value)) 24 | } else if let error = onEmpty() { 25 | handler(.failure(error)) 26 | } 27 | } 28 | 29 | value = nil 30 | }, receiveValue: { value = $0 }) 31 | 32 | self.subscribe(subscriber) 33 | return AnyCancellable(subscriber) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/runtime/operators/AssignOpTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Conbini 3 | import Combine 4 | 5 | /// Tests the correct behavior of the `assign(to:on:)` and `invoke(_:on:)` operators. 6 | final class AssignOpTests: XCTestCase { 7 | /// A convenience storage of cancellables. 8 | private var _cancellables = Set() 9 | 10 | override func setUp() { 11 | self.continueAfterFailure = false 12 | self._cancellables.removeAll() 13 | } 14 | 15 | override func tearDown() { 16 | self._cancellables.removeAll() 17 | } 18 | } 19 | 20 | extension AssignOpTests { 21 | /// Tests the `assign(to:onWeak:)` operations. 22 | func testRegularAssign() { 23 | let data = [1, 2, 3, 4] 24 | final class _Custom { var value: Int = 0 } 25 | 26 | let objA = _Custom() 27 | data.publisher.assign(to: \.value, on: objA).store(in: &self._cancellables) 28 | XCTAssert(objA.value == data.last!) 29 | 30 | let objB = _Custom() 31 | data.publisher.assign(to: \.value, onWeak: objB).store(in: &self._cancellables) 32 | XCTAssert(objB.value == data.last!) 33 | 34 | let objC = _Custom() 35 | data.publisher.assign(to: \.value, onWeak: objC).store(in: &self._cancellables) 36 | XCTAssert(objC.value == data.last!) 37 | } 38 | 39 | /// Tests the `invoke(_:on:)` operations. 40 | func testRegularInvocation() { 41 | let data = [1, 2, 3, 4] 42 | final class _Custom { 43 | private(set) var value: Int = 0 44 | func setNumber(value: Int) -> Void { self.value = value } 45 | } 46 | 47 | let objA = _Custom() 48 | data.publisher.invoke(_Custom.setNumber, on: objA).store(in: &self._cancellables) 49 | XCTAssert(objA.value == data.last!) 50 | 51 | let objB = _Custom() 52 | data.publisher.invoke(_Custom.setNumber, onWeak: objB).store(in: &self._cancellables) 53 | XCTAssert(objB.value == data.last!) 54 | 55 | let objC = _Custom() 56 | data.publisher.invoke(_Custom.setNumber, onWeak: objC).store(in: &self._cancellables) 57 | XCTAssert(objC.value == data.last!) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /docs/SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Getting help 2 | 3 | Here is a helpful list of "where to get help" depending on the topic: 4 | 5 | - **How to use Conbini?** 6 | 7 | The first place to look is the [README file](https://github.com/dehesa/Conbini/blob/master/README.md). It is kept updated and there is a wealth of information under each right-pointing [caret](https://en.wikipedia.org/wiki/Caret). 8 | 9 | You can also look for issues labeled as [question](https://github.com/dehesa/Conbini/issues?q=is%3Aissue+label%3Aquestion+). Odds are your question has been asked before. 10 | 11 | If the information is not there, feel free to create a [new issue](https://github.com/dehesa/Conbini/issues/new) labeling it as _question_. 12 | 13 | - **How to use Swift's [Combine](https://developer.apple.com/documentation/combine)?** 14 | 15 | There are many places to learn how to use Combine. 16 | 17 | - [Apple's Article](https://developer.apple.com/documentation/combine/receiving_and_handling_events_with_combine) on receiving and handling events with Combine. 18 | - Ray Wenderlich's [Combine book](https://store.raywenderlich.com/products/combine-asynchronous-programming-with-swift). 19 | 20 | For complex queries, go to the [Swift forums](https://forums.swift.org), specifically the [_Using Swift_](https://forums.swift.org/c/swift-users/15) category, setting the search tag to [_combine_](https://forums.swift.org/tags/c/swift-users/15/combine). 21 | 22 | - **How to contribute to Conbini?** 23 | 24 | This project uses the standard [CONTRIBUTING.md](CONTRIBUTING.md) file explaining the contribution guidelines. This same file also has a section on "where to start contributing", in case you want to contribute but you are unsure where to start. 25 | 26 | Also take a look at the Github's [projects page](https://github.com/dehesa/Conbini/projects) where you can see the project's Kanban board. 27 | 28 | - **Problems with Github**. 29 | 30 | Check their [fantastic documentation](https://help.github.com/en), go to [Github's learning lab](https://lab.github.com), or ask [Github support](https://support.github.com). 31 | -------------------------------------------------------------------------------- /tests/runtime/publishers/DeferredResultTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Conbini 3 | import Combine 4 | 5 | /// Tests the correct behavior of the `Complete` publisher. 6 | final class DeferredResultTests: XCTestCase { 7 | /// A convenience storage of cancellables. 8 | private var _cancellables = Set() 9 | 10 | override func setUp() { 11 | self.continueAfterFailure = false 12 | self._cancellables.removeAll() 13 | } 14 | 15 | override func tearDown() { 16 | self._cancellables.removeAll() 17 | } 18 | 19 | /// A custom error to send as a dummy. 20 | private struct _CustomError: Swift.Error {} 21 | } 22 | 23 | extension DeferredResultTests { 24 | /// Tests a successful completion of the `DeferredCompletion` publisher. 25 | func testSuccessfulCompletion() { 26 | let exp = self.expectation(description: "Publisher completes successfully") 27 | let value = 1 28 | 29 | DeferredResult { .success(value) } 30 | .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) 31 | .sink(receiveCompletion: { 32 | guard case .finished = $0 else { return XCTFail("The successful completion publisher has failed!") } 33 | exp.fulfill() 34 | }, receiveValue: { XCTAssertEqual($0, value) }) 35 | .store(in: &self._cancellables) 36 | 37 | self.wait(for: [exp], timeout: 0.2) 38 | } 39 | 40 | /// Tests a failure completion of the `DeferredCompletion` publisher. 41 | func testFailedCompletion() { 42 | let exp = self.expectation(description: "Publisher completes with a failure") 43 | 44 | DeferredResult { .failure(_CustomError()) } 45 | .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) 46 | .sink(receiveCompletion: { 47 | guard case .failure = $0 else { return XCTFail("The failed completion publisher has completed successfully!") } 48 | exp.fulfill() 49 | }, receiveValue: { _ in XCTFail("The empty complete publisher has emitted a value!") }) 50 | .store(in: &self._cancellables) 51 | 52 | self.wait(for: [exp], timeout: 0.2) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/runtime/operators/ThenOpTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Conbini 3 | import Combine 4 | 5 | /// Tests the correct behavior of the `Then` operator. 6 | final class ThenOpTests: XCTestCase { 7 | override func setUp() { 8 | self.continueAfterFailure = false 9 | } 10 | 11 | /// A custom error to send as a dummy. 12 | private struct _CustomError: Swift.Error {} 13 | } 14 | 15 | extension ThenOpTests { 16 | /// Test a normal "happy path" example for the custom "then" combine operator. 17 | func testThenPassthrough() { 18 | let e = self.expectation(description: "Successful completion") 19 | let queue = DispatchQueue.main 20 | 21 | let subject = PassthroughSubject() 22 | let cancellable = subject.then { 23 | ["A", "B", "C"].publisher.setFailureType(to: _CustomError.self) 24 | }.sink(receiveCompletion: { _ in e.fulfill() }) { _ in return } 25 | 26 | queue.asyncAfter(deadline: .now() + .milliseconds(100)) { subject.send(0) } 27 | queue.asyncAfter(deadline: .now() + .milliseconds(200)) { subject.send(1) } 28 | queue.asyncAfter(deadline: .now() + .milliseconds(300)) { subject.send(2) } 29 | queue.asyncAfter(deadline: .now() + .milliseconds(400)) { subject.send(completion: .finished) } 30 | 31 | self.wait(for: [e], timeout: 1) 32 | cancellable.cancel() 33 | } 34 | 35 | /// Tests the behavior of the `then` operator reacting to a upstream error. 36 | func testThenFailure() { 37 | let e = self.expectation(description: "Failure on origin") 38 | let queue = DispatchQueue.main 39 | 40 | let subject = PassthroughSubject() 41 | let cancellable = subject.then { 42 | Future { (promise) in 43 | queue.asyncAfter(deadline: .now() + .milliseconds(200)) { promise(.success("Completed")) } 44 | } 45 | }.sink(receiveCompletion: { (completion) in 46 | guard case .failure = completion else { return } 47 | e.fulfill() 48 | }) { _ in return } 49 | 50 | queue.asyncAfter(deadline: .now() + .milliseconds(50)) { subject.send(0) } 51 | queue.asyncAfter(deadline: .now() + .milliseconds(100)) { subject.send(completion: .failure(_CustomError())) } 52 | 53 | self.wait(for: [e], timeout: 2) 54 | cancellable.cancel() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /sources/runtime/utils/State.swift: -------------------------------------------------------------------------------- 1 | /// States where conduit can find itself into. 2 | @frozen public enum ConduitState: ExpressibleByNilLiteral { 3 | /// A subscriber has been sent upstream, but a subscription acknowledgement hasn't been received yet. 4 | case awaitingSubscription(WaitConfiguration) 5 | /// The conduit is active and potentially receiving and sending events. 6 | case active(ActiveConfiguration) 7 | /// The conduit has been cancelled or it has been terminated. 8 | case terminated 9 | 10 | public init(nilLiteral: ()) { 11 | self = .terminated 12 | } 13 | } 14 | 15 | extension ConduitState { 16 | /// Returns the `WaitConfiguration` if the receiving state is at `.awaitingSubscription`. `nil` for `.terminated` states, and it produces a fatal error otherwise. 17 | /// 18 | /// It is used on places where `Combine` promises that a subscription might only be in `.awaitingSubscription` or `.terminated` state, but never on `.active`. 19 | @_transparent public var awaitingConfiguration: WaitConfiguration? { 20 | switch self { 21 | case .awaitingSubscription(let config): return config 22 | case .terminated: return nil 23 | case .active: fatalError() 24 | } 25 | } 26 | 27 | /// Returns the `ActiveConfiguration` if the receiving state is at `.active`. `nil` for `.terminated` states, and it produces a fatal error otherwise. 28 | /// 29 | /// It is used on places where `Combine` promises that a subscription might only be in `.active` or `.terminated` state, but never on `.awaitingSubscription`. 30 | @_transparent public var activeConfiguration: ActiveConfiguration? { 31 | switch self { 32 | case .active(let config): return config 33 | case .terminated: return nil 34 | case .awaitingSubscription: fatalError() 35 | } 36 | } 37 | } 38 | 39 | extension ConduitState { 40 | /// Boolean indicating if the state is still active. 41 | @_transparent public var isActive: Bool { 42 | switch self { 43 | case .active: return true 44 | case .awaitingSubscription, .terminated: return false 45 | } 46 | } 47 | 48 | /// Boolean indicating if the state has been terminated. 49 | @_transparent public var isTerminated: Bool { 50 | switch self { 51 | case .terminated: return true 52 | case .awaitingSubscription, .active: return false 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /docs/assets/badges/Swift.svg: -------------------------------------------------------------------------------- 1 | 5.2Swift -------------------------------------------------------------------------------- /tests/runtime/subscribers/GraduatedSinkTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Conbini 3 | import Combine 4 | 5 | /// Tests the correct behavior of the `Then` operator. 6 | final class GraduatedSinkTests: XCTestCase { 7 | override func setUp() { 8 | self.continueAfterFailure = false 9 | } 10 | /// A custom error to send as a dummy. 11 | private struct _CustomError: Swift.Error {} 12 | } 13 | 14 | extension GraduatedSinkTests { 15 | /// Tests a subscription with a graduated sink yielding some values and a successful completion. 16 | func testSuccessfulCompletion() { 17 | let e = self.expectation(description: "Successful completion") 18 | 19 | let input = (0..<10) 20 | var received = [Int]() 21 | 22 | let subscriber = Subscribers.GraduatedSink(maxDemand: .max(3), receiveCompletion: { 23 | guard case .finished = $0 else { return XCTFail("A failure completion was received when a successful completion was expected.")} 24 | e.fulfill() 25 | }, receiveValue: { received.append($0) }) 26 | 27 | input.publisher.setFailureType(to: _CustomError.self) 28 | .map { $0 * 2} 29 | .subscribe(subscriber) 30 | 31 | self.wait(for: [e], timeout: 1) 32 | XCTAssertEqual(input.map { $0 * 2 }, received) 33 | subscriber.cancel() 34 | } 35 | 36 | /// Tests a subscription with a graduated sink yielding some values and a failure completion. 37 | func testFailedCompletion() { 38 | let e = self.expectation(description: "Failure completion") 39 | 40 | let input = (0..<10) 41 | var received = [Int]() 42 | 43 | let subscriber = Subscribers.GraduatedSink(maxDemand: .max(3), receiveCompletion: { 44 | guard case .failure = $0 else { return XCTFail("A succesful completion was received when a failure completion was expected.")} 45 | e.fulfill() 46 | }, receiveValue: { received.append($0) }) 47 | 48 | let subject = PassthroughSubject() 49 | subject.map { $0 * 2} 50 | .subscribe(subscriber) 51 | 52 | let queue = DispatchQueue(label: "io.dehesa.conbini.tests.subscribers.graduatedSink") 53 | for i in input { 54 | queue.asyncAfter(deadline: .now() + .milliseconds(i * 50)) { subject.send(i) } 55 | } 56 | 57 | queue.asyncAfter(deadline: .now() + .milliseconds((input.last! + 1) * 50)) { 58 | subject.send(completion: .failure(_CustomError())) 59 | } 60 | 61 | self.wait(for: [e], timeout: 4) 62 | XCTAssertEqual(input.map { $0 * 2 }, received) 63 | subscriber.cancel() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/runtime/utils/BufferTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Conbini 3 | import Combine 4 | 5 | /// Tests the correct behavior of the `DeferredComplete` publisher. 6 | final class BufferTests: XCTestCase { 7 | /// A convenience storage of cancellables. 8 | private var cancellables = Set() 9 | 10 | override func setUp() { 11 | self.continueAfterFailure = false 12 | self.cancellables.removeAll() 13 | } 14 | 15 | override func tearDown() { 16 | self._cancellables.removeAll() 17 | } 18 | 19 | /// A custom error to send as a dummy. 20 | private struct _CustomError: Swift.Error {} 21 | } 22 | 23 | extension BufferTests { 24 | /// Tests the regular usage of a buffer operator. 25 | func testRegularUsage() { 26 | let exp = self.expectation(description: "Publisher completes successfully") 27 | 28 | let input = [0, 1, 2, 3, 4] 29 | var output = [Int]() 30 | 31 | let subject = PassthroughSubject() 32 | let subscriber = AnySubscriber(receiveSubscription: { $0.request(.max(1)) }, receiveValue: { 33 | output.append($0) 34 | return .max(1) 35 | }, receiveCompletion: { 36 | guard case .finished = $0 else { return XCTFail("The publisher failed when a successful completion was expected")} 37 | exp.fulfill() 38 | }) 39 | 40 | subject.map { $0 * 2 } 41 | .buffer(size: 10, prefetch: .keepFull, whenFull: .fatalError()) 42 | .map { $0 * 2} 43 | .subscribe(subscriber) 44 | 45 | for i in input { subject.send(i) } 46 | subject.send(completion: .finished) 47 | 48 | self.wait(for: [exp], timeout: 0.2) 49 | XCTAssertEqual(input.map { $0 * 4 }, output) 50 | } 51 | 52 | /// Tests a buffer operator when it is filled till the *brim*. 53 | func testBrimUsage() { 54 | let exp = self.expectation(description: "Publisher completes successfully") 55 | 56 | let input = [0, 1, 2, 3, 4] 57 | var output = [Int]() 58 | 59 | let subject = PassthroughSubject() 60 | subject.map { $0 * 2 } 61 | .buffer(size: input.count, prefetch: .keepFull, whenFull: .fatalError()) 62 | .map { $0 * 2} 63 | .sink(receiveCompletion: { 64 | guard case .finished = $0 else { return XCTFail("The publisher failed when a successful completion was expected")} 65 | exp.fulfill() 66 | }, receiveValue: { output.append($0) }) 67 | .store(in: &self.cancellables) 68 | 69 | for i in input { subject.send(i) } 70 | subject.send(completion: .finished) 71 | 72 | self.wait(for: [exp], timeout: 0.2) 73 | XCTAssertEqual(input.map { $0 * 4 }, output) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /sources/runtime/subscribers/GraduatedSink.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | extension Subscribers { 4 | /// A simple subscriber that requests the given number of values upon subscription, always maintaining the same demand. 5 | public final class GraduatedSink: Subscriber, Cancellable where Failure:Error { 6 | /// The maximum allowed in-flight events. 7 | public let maxDemand: Subscribers.Demand 8 | /// The closure executed when a value is received. 9 | public private(set) var receiveValue: ((Input)->Void)? 10 | /// The closure executed when a completion event is received. 11 | public private(set) var receiveCompletion: ((Subscribers.Completion)->Void)? 12 | /// The subscriber's state. 13 | @ConduitLock private var state: ConduitState 14 | 15 | /// Designated initializer specifying the maximum in-flight events. 16 | /// - precondition: `maxDemand` must be greater than zero. 17 | /// - parameter maxDemand: The maximum allowed in-flight events. 18 | /// - parameter receiveCompletion: The closure executed when a completion event is received. 19 | /// - parameter receiveValue: The closure executed when a value is received. 20 | public init(maxDemand: Subscribers.Demand, receiveCompletion: ((Subscribers.Completion)->Void)? = nil, receiveValue: ((Input)->Void)? = nil) { 21 | precondition(maxDemand > 0) 22 | self.maxDemand = maxDemand 23 | self.receiveValue = receiveValue 24 | self.receiveCompletion = receiveCompletion 25 | self.state = .awaitingSubscription(()) 26 | } 27 | 28 | deinit { 29 | self.cancel() 30 | self._state.invalidate() 31 | } 32 | 33 | public func receive(subscription: Subscription) { 34 | guard case .some = self._state.activate(atomic: { _ in .init(upstream: subscription) }) else { 35 | return subscription.cancel() 36 | } 37 | subscription.request(self.maxDemand) 38 | } 39 | 40 | public func receive(_ input: Input) -> Subscribers.Demand { 41 | self.receiveValue?(input) 42 | return .max(1) 43 | } 44 | 45 | public func receive(completion: Subscribers.Completion) { 46 | guard case .active = self._state.terminate() else { return } 47 | self.receiveValue = nil 48 | self.receiveCompletion?(completion) 49 | self.receiveCompletion = nil 50 | } 51 | 52 | public func cancel() { 53 | guard case .active = self._state.terminate() else { return } 54 | self.receiveValue = nil 55 | self.receiveCompletion = nil 56 | } 57 | } 58 | } 59 | 60 | private extension Subscribers.GraduatedSink { 61 | /// Variables required during the *active* stage. 62 | struct _Configuration { 63 | /// Upstream subscription. 64 | let upstream: Subscription 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /docs/assets/Conbini.svg: -------------------------------------------------------------------------------- 1 | Conbini -------------------------------------------------------------------------------- /sources/runtime/utils/Lock.swift: -------------------------------------------------------------------------------- 1 | import Darwin 2 | 3 | /// Property Wrapper used to guard a combine conduit state behind a unfair lock. 4 | /// 5 | /// - attention: Always make sure to deinitialize the lock. 6 | @propertyWrapper public struct ConduitLock { 7 | /// Performant non-reentrant unfair lock. 8 | private var _lock: UnsafeMutablePointer 9 | /// Generic variable being guarded by the lock. 10 | public var value: Value 11 | 12 | public init(wrappedValue: Value) { 13 | self._lock = UnsafeMutablePointer.allocate(capacity: 1) 14 | self._lock.initialize(to: os_unfair_lock()) 15 | self.value = wrappedValue 16 | } 17 | 18 | /// Provide thread-safe storage access (within the lock). 19 | public var wrappedValue: Value { 20 | get { 21 | self.lock() 22 | let content = self.value 23 | self.unlock() 24 | return content 25 | } 26 | set { 27 | self.lock() 28 | self.value = newValue 29 | self.unlock() 30 | } 31 | } 32 | } 33 | 34 | extension ConduitLock { 35 | /// Locks the state to other threads. 36 | public func lock() { 37 | os_unfair_lock_lock(self._lock) 38 | } 39 | 40 | /// Unlocks the state for other threads. 41 | public func unlock() { 42 | os_unfair_lock_unlock(self._lock) 43 | } 44 | 45 | public func invalidate() { 46 | self._lock.deinitialize(count: 1) 47 | self._lock.deallocate() 48 | } 49 | } 50 | 51 | extension ConduitLock { 52 | /// The type of the value being guarded by the lock. 53 | public typealias Value = ConduitState 54 | 55 | /// Switches the state from `.awaitingSubscription` to `.active` by providing the active configuration parameters. 56 | /// - If the state is already in `.active`, this function crashes. 57 | /// - If the state is `.terminated`, no work is performed. 58 | /// - parameter atomic: Code executed within the unfair locks. Don't call anywhere here; just perform computations. 59 | /// - returns: The active configuration set after the call of this function. 60 | public mutating func activate(atomic: (WaitConfiguration)->ActiveConfiguration) -> ActiveConfiguration? { 61 | let result: ActiveConfiguration? 62 | 63 | self.lock() 64 | switch self.value { 65 | case .awaitingSubscription(let awaitingConfiguration): 66 | result = atomic(awaitingConfiguration) 67 | self.value = .active(result.unsafelyUnwrapped) 68 | case .terminated: result = nil 69 | case .active: fatalError() 70 | } 71 | self.unlock() 72 | 73 | return result 74 | } 75 | 76 | /// Nullify the state and returns the previous state value. 77 | @discardableResult public mutating func terminate() -> Value { 78 | self.lock() 79 | let result = self.value 80 | self.value = .terminated 81 | self.unlock() 82 | return result 83 | } 84 | } 85 | 86 | -------------------------------------------------------------------------------- /sources/runtime/publishers/DeferredResult.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | /// A publisher returning the result of a given closure only executed on the first positive demand. 4 | /// 5 | /// This publisher is used at the origin of a publisher chain and it only provides the value when it receives a request with a demand greater than zero. 6 | public struct DeferredResult: Publisher { 7 | /// The closure type being store for delayed execution. 8 | public typealias Closure = () -> Result 9 | /// Deferred closure. 10 | /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). 11 | public let closure: Closure 12 | 13 | /// Creates a publisher that send a value and completes successfully or just fails depending on the result of the given closure. 14 | /// - parameter closure: Closure in charge of generating the value to be emitted. 15 | /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). 16 | @inlinable public init(closure: @escaping Closure) { 17 | self.closure = closure 18 | } 19 | 20 | public func receive(subscriber: S) where S: Subscriber, S.Input==Output, S.Failure==Failure { 21 | let subscription = _Conduit(downstream: subscriber, closure: self.closure) 22 | subscriber.receive(subscription: subscription) 23 | } 24 | } 25 | 26 | private extension DeferredResult { 27 | /// The shadow subscription chain's origin. 28 | final class _Conduit: Subscription where Downstream:Subscriber, Downstream.Input==Output, Downstream.Failure==Failure { 29 | /// Enum listing all possible conduit states. 30 | @ConduitLock private var state: ConduitState 31 | 32 | /// Designated initializer passing the state configuration values. 33 | init(downstream: Downstream, closure: @escaping Closure) { 34 | self.state = .active(_Configuration(downstream: downstream, closure: closure)) 35 | } 36 | 37 | deinit { 38 | self._state.invalidate() 39 | } 40 | 41 | func request(_ demand: Subscribers.Demand) { 42 | guard demand > 0, case .active(let config) = self._state.terminate() else { return } 43 | 44 | switch config.closure() { 45 | case .success(let value): 46 | _ = config.downstream.receive(value) 47 | config.downstream.receive(completion: .finished) 48 | case .failure(let error): 49 | config.downstream.receive(completion: .failure(error)) 50 | } 51 | } 52 | 53 | func cancel() { 54 | self._state.terminate() 55 | } 56 | } 57 | } 58 | 59 | private extension DeferredResult._Conduit { 60 | /// Values needed for the subscription's active state. 61 | struct _Configuration { 62 | /// The downstream subscriber awaiting any value and/or completion events. 63 | let downstream: Downstream 64 | /// The closure generating the successful/failure completion. 65 | let closure: DeferredResult.Closure 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/runtime/operators/ResultOpTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Conbini 3 | import Combine 4 | 5 | /// Tests the correct behavior of the `result(onEmpty:)` operator. 6 | final class ResultOpTests: XCTestCase { 7 | /// A convenience storage of cancellables. 8 | private var _cancellables = Set() 9 | 10 | override func setUp() { 11 | self.continueAfterFailure = false 12 | self._cancellables = .init() 13 | } 14 | 15 | override func tearDown() { 16 | self._cancellables.removeAll() 17 | } 18 | 19 | /// A custom error to send as a dummy. 20 | private struct _CustomError: Swift.Error {} 21 | } 22 | 23 | extension ResultOpTests { 24 | /// Test the `result` operator with completions and no values. 25 | func testCompletionWithoutValue() { 26 | Empty(completeImmediately: true).result { _ in 27 | XCTFail("The handler has been called although it was not expected to") 28 | }.store(in: &self._cancellables) 29 | 30 | Empty(completeImmediately: false).result { _ in 31 | XCTFail("The handler has been called although it was not expected to") 32 | }.store(in: &self._cancellables) 33 | } 34 | 35 | /// Tests the `result` operator with one value and completion. 36 | func testRegularUsage() { 37 | let input = 9 38 | 39 | Just(input).result { 40 | guard case .success(let received) = $0 else { return XCTFail() } 41 | XCTAssertEqual(received, input) 42 | }.store(in: &self._cancellables) 43 | 44 | [input].publisher.result { 45 | guard case .success(let received) = $0 else { return XCTFail() } 46 | XCTAssertEqual(received, input) 47 | }.store(in: &self._cancellables) 48 | 49 | let exp = self.expectation(description: "Deferred passthrough provides a result") 50 | DeferredPassthrough { (subject) in 51 | let queue = DispatchQueue.main 52 | queue.asyncAfter(deadline: .now() + .milliseconds(50)) { subject.send(input) } 53 | queue.asyncAfter(deadline: .now() + .milliseconds(100)) { subject.send(completion: .finished) } 54 | }.result { 55 | guard case .success(let received) = $0 else { return XCTFail() } 56 | XCTAssertEqual(received, input) 57 | exp.fulfill() 58 | }.store(in: &self._cancellables) 59 | 60 | self.wait(for: [exp], timeout: 0.2) 61 | } 62 | 63 | /// Tests the `result` operator in failure situations. 64 | func testFailure() { 65 | Fail(error: .init()).result { 66 | guard case .failure(_) = $0 else { return XCTFail() } 67 | } 68 | 69 | let expA = self.expectation(description: "A failure result is provided") 70 | DeferredPassthrough { (subject) in 71 | let queue = DispatchQueue.main 72 | queue.asyncAfter(deadline: .now() + .milliseconds(50)) { subject.send(completion: .failure(.init())) } 73 | }.result { 74 | guard case .failure(_) = $0 else { return XCTFail() } 75 | expA.fulfill() 76 | }.store(in: &self._cancellables) 77 | 78 | self.wait(for: [expA], timeout: 0.2) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /sources/runtime/publishers/DeferredValue.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | /// A publisher emitting the value generated by a given closure followed by a successful completion. 4 | /// 5 | /// This publisher is used at the origin of a publisher chain and it ony executes the passed closure when it receives a request with a demand greater than zero. 6 | public struct DeferredValue: Publisher where Failure:Swift.Error { 7 | /// The closure type being store for delayed execution. 8 | public typealias Closure = () -> Output 9 | /// Deferred closure. 10 | /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). 11 | public let closure: Closure 12 | 13 | /// Creates a publisher which will a value and completes successfully, or just fail depending on the result of the given closure. 14 | /// - parameter closure: Closure in charge of generating the value to be emitted. 15 | /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). 16 | @inlinable public init(failure: Failure.Type = Failure.self, closure: @escaping Closure) { 17 | self.closure = closure 18 | } 19 | 20 | public func receive(subscriber: S) where S:Subscriber, S.Input==Output, S.Failure==Failure { 21 | let subscription = _Conduit(downstream: subscriber, closure: self.closure) 22 | subscriber.receive(subscription: subscription) 23 | } 24 | } 25 | 26 | private extension DeferredValue { 27 | /// The shadow subscription chain's origin. 28 | final class _Conduit: Subscription where Downstream:Subscriber, Downstream.Input==Output, Downstream.Failure==Failure { 29 | /// Enum listing all possible conduit states. 30 | @ConduitLock private var state: ConduitState<(),_Configuration> 31 | 32 | /// Sets up the guarded state. 33 | /// - parameter downstream: Downstream subscriber receiving the data from this instance. 34 | /// - parameter closure: Closure in charge of generating the emitted value. 35 | init(downstream: Downstream, closure: @escaping Closure) { 36 | self.state = .active(_Configuration(downstream: downstream, closure: closure)) 37 | } 38 | 39 | deinit { 40 | self._state.invalidate() 41 | } 42 | 43 | func request(_ demand: Subscribers.Demand) { 44 | guard demand > 0, 45 | case .active(let config) = self._state.terminate() else { return } 46 | 47 | _ = config.downstream.receive(config.closure()) 48 | config.downstream.receive(completion: .finished) 49 | } 50 | 51 | func cancel() { 52 | self._state.terminate() 53 | } 54 | } 55 | } 56 | 57 | private extension DeferredValue._Conduit { 58 | /// Values needed for the subscription's active state. 59 | struct _Configuration { 60 | /// The downstream subscriber awaiting any value and/or completion events. 61 | let downstream: Downstream 62 | /// The closure generating the optional value and/or the successful/failure completion. 63 | let closure: DeferredValue.Closure 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /sources/runtime/publishers/DeferredTryComplete.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | /// A publisher that never emits any values and just completes successfully or with a failure (depending on whether an error was thrown in the closure). 4 | /// 5 | /// This publisher is used at the origin of a publisher chain and it only provides the value when it receives a request with a demand greater than zero. 6 | public struct DeferredTryComplete: Publisher { 7 | public typealias Failure = Swift.Error 8 | /// The closure type being store for delayed execution. 9 | public typealias Closure = () throws -> Void 10 | 11 | /// Deferred closure. 12 | /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). 13 | public let closure: Closure 14 | 15 | /// Creates a publisher that send a successful completion once it receives a positive request (i.e. a request greater than zero) 16 | public init() { 17 | self.closure = { return } 18 | } 19 | 20 | /// Creates a publisher that send a value and completes successfully or just fails depending on the result of the given closure. 21 | /// - parameter closure: The closure which produces an empty successful completion or a failure (if it throws). 22 | /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). 23 | @inlinable public init(closure: @escaping Closure) { 24 | self.closure = closure 25 | } 26 | 27 | public func receive(subscriber: S) where S:Subscriber, S.Input==Output, S.Failure==Failure { 28 | let subscription = _Conduit(downstream: subscriber, closure: self.closure) 29 | subscriber.receive(subscription: subscription) 30 | } 31 | } 32 | 33 | private extension DeferredTryComplete { 34 | /// The shadow subscription chain's origin. 35 | final class _Conduit: Subscription where Downstream:Subscriber, Downstream.Failure==Failure { 36 | /// Enum listing all possible conduit states. 37 | @ConduitLock private var state: ConduitState 38 | 39 | init(downstream: Downstream, closure: @escaping Closure) { 40 | self.state = .active(_Configuration(downstream: downstream, closure: closure)) 41 | } 42 | 43 | deinit { 44 | self._state.invalidate() 45 | } 46 | 47 | func request(_ demand: Subscribers.Demand) { 48 | guard demand > 0, case .active(let config) = self._state.terminate() else { return } 49 | 50 | do { 51 | try config.closure() 52 | } catch let error { 53 | return config.downstream.receive(completion: .failure(error)) 54 | } 55 | 56 | config.downstream.receive(completion: .finished) 57 | } 58 | 59 | func cancel() { 60 | self._state.terminate() 61 | } 62 | } 63 | } 64 | 65 | private extension DeferredTryComplete._Conduit { 66 | /// Values needed for the subscription's active state. 67 | struct _Configuration { 68 | /// The downstream subscriber awaiting any value and/or completion events. 69 | let downstream: Downstream 70 | /// The closure generating the successful/failure completion. 71 | let closure: DeferredTryComplete.Closure 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/runtime/publishers/DeferredPassthroughTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Conbini 3 | import Combine 4 | 5 | /// Tests the correct behavior of the `Complete` publisher. 6 | final class DeferredPassthroughTests: XCTestCase { 7 | /// A convenience storage of cancellables. 8 | private var _cancellables = Set() 9 | 10 | override func setUp() { 11 | self.continueAfterFailure = false 12 | self._cancellables.removeAll() 13 | } 14 | 15 | override func tearDown() { 16 | self._cancellables.removeAll() 17 | } 18 | 19 | /// A custom error to send as a dummy. 20 | private struct _CustomError: Swift.Error {} 21 | } 22 | 23 | extension DeferredPassthroughTests { 24 | /// Tests a successful completion of the `DeferredCompletion` publisher. 25 | func testSuccessfulCompletion() { 26 | let exp = self.expectation(description: "Publisher completes successfully") 27 | 28 | let values: [Int] = [1, 2, 3, 4] 29 | var receivedValues: [Int] = [] 30 | 31 | DeferredPassthrough { (subject) in 32 | for i in values { subject.send(i) } 33 | subject.send(completion: .finished) 34 | }.sink(receiveCompletion: { 35 | guard case .finished = $0 else { return XCTFail("The subject failed unexpectedly") } 36 | exp.fulfill() 37 | }, receiveValue: { receivedValues.append($0) }) 38 | .store(in: &self._cancellables) 39 | 40 | self.wait(for: [exp], timeout: 0.5) 41 | XCTAssertEqual(values, receivedValues) 42 | } 43 | 44 | /// Tests a failure completion of the `DeferredCompletion` publisher. 45 | func testFailedCompletion() { 46 | let exp = self.expectation(description: "Publisher completes successfully") 47 | 48 | let values: [Int] = [1, 2, 3, 4] 49 | var receivedValues: [Int] = [] 50 | 51 | DeferredPassthrough { (subject) in 52 | for i in values { subject.send(i) } 53 | subject.send(completion: .failure(.init())) 54 | }.sink(receiveCompletion: { 55 | guard case .failure = $0 else { return XCTFail("The subject succeeeded unexpectedly") } 56 | exp.fulfill() 57 | }, receiveValue: { receivedValues.append($0) }) 58 | .store(in: &self._cancellables) 59 | 60 | self.wait(for: [exp], timeout: 0.5) 61 | XCTAssertEqual(values, receivedValues) 62 | } 63 | 64 | func testBackpressure() { 65 | let exp = self.expectation(description: "Publisher completes successfully") 66 | 67 | let values: [Int] = [1, 2, 3, 4] 68 | var receivedValues: [Int] = [] 69 | 70 | let oneByOneSubscriber = AnySubscriber(receiveSubscription: { 71 | $0.request(.max(2)) 72 | }, receiveValue: { 73 | receivedValues.append($0) 74 | return .none 75 | }, receiveCompletion: { 76 | guard case .finished = $0 else { return XCTFail("The subject failed unexpectedly") } 77 | exp.fulfill() 78 | }) 79 | 80 | DeferredPassthrough { (subject) in 81 | for i in values { subject.send(i) } 82 | subject.send(completion: .finished) 83 | }.subscribe(oneByOneSubscriber) 84 | 85 | self.wait(for: [exp], timeout: 100) 86 | XCTAssertEqual(.init(values.prefix(upTo: 2)), receivedValues) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /sources/runtime/publishers/DeferredTryValue.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | /// A publisher emitting the value generated by a given closure followed by a successful completion. If the closure throws an error, the publisher will complete with a failure. 4 | /// 5 | /// This publisher is used at the origin of a publisher chain and it only executes the passed closure when it receives a request with a demand greater than zero. 6 | public struct DeferredTryValue: Publisher { 7 | public typealias Failure = Swift.Error 8 | /// The closure type being store for delayed execution. 9 | public typealias Closure = () throws -> Output 10 | /// Deferred closure. 11 | /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). 12 | public let closure: Closure 13 | 14 | /// Creates a publisher which will a value and completes successfully, or just fail depending on the result of the given closure. 15 | /// - parameter closure: Closure in charge of generating the value to be emitted. 16 | /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). 17 | @inlinable public init(closure: @escaping Closure) { 18 | self.closure = closure 19 | } 20 | 21 | public func receive(subscriber: S) where S:Subscriber, S.Input==Output, S.Failure==Failure { 22 | let subscription = _Conduit(downstream: subscriber, closure: self.closure) 23 | subscriber.receive(subscription: subscription) 24 | } 25 | } 26 | 27 | private extension DeferredTryValue { 28 | /// The shadow subscription chain's origin. 29 | final class _Conduit: Subscription where Downstream:Subscriber, Downstream.Input==Output, Downstream.Failure==Failure { 30 | /// Enum listing all possible conduit states. 31 | @ConduitLock private var state: ConduitState 32 | 33 | /// Sets up the guarded state. 34 | /// - parameter downstream: Downstream subscriber receiving the data from this instance. 35 | /// - parameter closure: Closure in charge of generating the emitted value. 36 | init(downstream: Downstream, closure: @escaping Closure) { 37 | self.state = .active(_Configuration(downstream: downstream, closure: closure)) 38 | } 39 | 40 | deinit { 41 | self._state.invalidate() 42 | } 43 | 44 | func request(_ demand: Subscribers.Demand) { 45 | guard demand > 0, case .active(let config) = self._state.terminate() else { return } 46 | 47 | let input: Output 48 | do { 49 | input = try config.closure() 50 | } catch let error { 51 | return config.downstream.receive(completion: .failure(error)) 52 | } 53 | 54 | _ = config.downstream.receive(input) 55 | config.downstream.receive(completion: .finished) 56 | } 57 | 58 | func cancel() { 59 | self._state.terminate() 60 | } 61 | } 62 | } 63 | 64 | private extension DeferredTryValue._Conduit { 65 | /// Values needed for the subscription's active state. 66 | struct _Configuration { 67 | /// The downstream subscriber awaiting any value and/or completion events. 68 | let downstream: Downstream 69 | /// The closure generating the optional value and successful/failure completion. 70 | let closure: DeferredTryValue.Closure 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/runtime/subscribers/FixedSinkTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Conbini 3 | import Combine 4 | 5 | /// Tests the correct behavior of the `Then` operator. 6 | final class FixedSinkTests: XCTestCase { 7 | override func setUp() { 8 | self.continueAfterFailure = false 9 | } 10 | /// A custom error to send as a dummy. 11 | private struct _CustomError: Swift.Error {} 12 | } 13 | 14 | extension FixedSinkTests { 15 | /// Tests a subscription with a fixed sink yielding some values and a successful completion. 16 | func testSuccessfulCompletion() { 17 | let e = self.expectation(description: "Successful completion") 18 | 19 | let input = (0..<10) 20 | var received = [Int]() 21 | 22 | let subscriber = Subscribers.FixedSink(demand: input.count, receiveCompletion: { 23 | guard case .finished = $0 else { return XCTFail("A failure completion was received when a successful completion was expected.")} 24 | e.fulfill() 25 | }, receiveValue: { received.append($0) }) 26 | 27 | input.publisher.setFailureType(to: _CustomError.self) 28 | .map { $0 * 2} 29 | .subscribe(subscriber) 30 | 31 | self.wait(for: [e], timeout: 1) 32 | XCTAssertEqual(input.map { $0 * 2 }, received) 33 | subscriber.cancel() 34 | } 35 | 36 | /// Tests a subscription with a fixed sink yielding some values and a successful completion. 37 | func testCutCompletion() { 38 | let e = self.expectation(description: "Successful completion") 39 | 40 | let input = (0..<10) 41 | var received = [Int]() 42 | 43 | let subscriber = Subscribers.FixedSink(demand: 3, receiveCompletion: { 44 | guard case .finished = $0 else { return XCTFail("A failure completion was received when a successful completion was expected.")} 45 | e.fulfill() 46 | }, receiveValue: { received.append($0) }) 47 | 48 | input.publisher.setFailureType(to: _CustomError.self) 49 | .map { $0 * 2} 50 | .subscribe(subscriber) 51 | 52 | self.wait(for: [e], timeout: 1) 53 | XCTAssertEqual(input.prefix(upTo: 3).map { $0 * 2 }, received) 54 | subscriber.cancel() 55 | } 56 | 57 | /// Tests a subscription with a fixed sink yielding some values and a failure completion. 58 | func testFailedCompletion() { 59 | let e = self.expectation(description: "Failure completion") 60 | 61 | let input = (0..<5) 62 | var received = [Int]() 63 | 64 | let subscriber = Subscribers.FixedSink(demand: input.count + 1, receiveCompletion: { 65 | guard case .failure = $0 else { return XCTFail("A succesful completion was received when a failure completion was expected.")} 66 | e.fulfill() 67 | }, receiveValue: { received.append($0) }) 68 | 69 | let subject = PassthroughSubject() 70 | subject.map { $0 * 2} 71 | .subscribe(subscriber) 72 | 73 | let queue = DispatchQueue(label: "io.dehesa.conbini.tests.subscribers.fixedSink") 74 | for i in input { 75 | queue.asyncAfter(deadline: .now() + .milliseconds(i * 10)) { subject.send(i) } 76 | } 77 | 78 | queue.asyncAfter(deadline: .now() + .milliseconds((input.last! + 1) * 10)) { 79 | subject.send(completion: .failure(_CustomError())) 80 | } 81 | 82 | self.wait(for: [e], timeout: 1) 83 | XCTAssertEqual(input.map { $0 * 2 }, received) 84 | subscriber.cancel() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /tests/runtime/publishers/DeferredTryCompleteTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Conbini 3 | import Combine 4 | 5 | /// Tests the correct behavior of the `DeferredTryComplete` publisher. 6 | final class DeferredTryCompleteTests: XCTestCase { 7 | /// A convenience storage of cancellables. 8 | private var _cancellables = Set() 9 | 10 | override func setUp() { 11 | self.continueAfterFailure = false 12 | self._cancellables.removeAll() 13 | } 14 | 15 | override func tearDown() { 16 | self._cancellables.removeAll() 17 | } 18 | 19 | /// A custom error to send as a dummy. 20 | private struct _CustomError: Swift.Error {} 21 | } 22 | 23 | extension DeferredTryCompleteTests { 24 | /// Tests a successful completion of the publisher. 25 | func testSuccessfulEmptyCompletion() { 26 | let exp = self.expectation(description: "Publisher completes successfully") 27 | 28 | DeferredTryComplete() 29 | .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) 30 | .sink(receiveCompletion: { 31 | guard case .finished = $0 else { return XCTFail("The successful completion publisher has failed!") } 32 | exp.fulfill() 33 | }, receiveValue: { _ in XCTFail("The empty complete publisher has emitted a value!") }) 34 | .store(in: &self._cancellables) 35 | 36 | self.wait(for: [exp], timeout: 0.2) 37 | } 38 | 39 | /// Tests a successful closure completion. 40 | func testSuccessfulClosureCompletion() { 41 | let exp = self.expectation(description: "Publisher completes successfully") 42 | 43 | DeferredTryComplete { return } 44 | .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) 45 | .sink(receiveCompletion: { 46 | guard case .finished = $0 else { return XCTFail("The successful completion publisher has failed!") } 47 | exp.fulfill() 48 | }, receiveValue: { _ in XCTFail("The empty complete publisher has emitted a value!") }) 49 | .store(in: &self._cancellables) 50 | 51 | self.wait(for: [exp], timeout: 0.2) 52 | } 53 | 54 | /// Tests a failure completion from the closure 55 | func testFailedCompletion() { 56 | let exp = self.expectation(description: "Publisher completes with a failure") 57 | 58 | DeferredTryComplete { throw _CustomError() } 59 | .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) 60 | .sink(receiveCompletion: { 61 | guard case .failure = $0 else { return XCTFail("The failed completion publisher has completed successfully!") } 62 | exp.fulfill() 63 | }, receiveValue: { _ in XCTFail("The empty complete publisher has emitted a value!") }) 64 | .store(in: &self._cancellables) 65 | 66 | self.wait(for: [exp], timeout: 0.2) 67 | } 68 | 69 | /// Tests the correct resource dumping. 70 | func testPublisherPipeline() { 71 | let exp = self.expectation(description: "Publisher completes successfully") 72 | 73 | DeferredTryComplete() 74 | .map { $0 * 2 } 75 | .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) 76 | .sink(receiveCompletion: { 77 | guard case .finished = $0 else { return XCTFail("The successful completion publisher has failed!") } 78 | exp.fulfill() 79 | }, receiveValue: { _ in XCTFail("The empty complete publisher has emitted a value!") }) 80 | .store(in: &self._cancellables) 81 | 82 | self.wait(for: [exp], timeout: 0.2) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /tests/runtime/publishers/DeferredFutureTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Conbini 3 | import Combine 4 | 5 | /// Tests the correct behavior of the `DeferredComplete` publisher. 6 | final class DeferredFutureTests: XCTestCase { 7 | override func setUp() { 8 | self.continueAfterFailure = false 9 | } 10 | 11 | /// A custom error to send as a dummy. 12 | private struct _CustomError: Swift.Error {} 13 | } 14 | 15 | extension DeferredFutureTests { 16 | /// Tests a successful completion of the publisher. 17 | func testSuccessfulSyncCompletion() { 18 | let exp = self.expectation(description: "Publisher completes successfully") 19 | 20 | let value = 42 21 | let cancellable = DeferredFuture { (promise) in 22 | promise(.success(value)) 23 | }.handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) 24 | .sink(receiveCompletion: { 25 | guard case .finished = $0 else { return XCTFail("The successful completion publisher has failed!") } 26 | exp.fulfill() 27 | }, receiveValue: { XCTAssertEqual($0, value) }) 28 | 29 | self.wait(for: [exp], timeout: 0.2) 30 | cancellable.cancel() 31 | } 32 | 33 | /// Tests a successful completion of the publisher. 34 | func testSuccessfulAsyncCompletion() { 35 | let exp = self.expectation(description: "Publisher completes successfully") 36 | 37 | let value = 42 38 | let cancellable = DeferredFuture { (promise) in 39 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) { promise(.success(value)) } 40 | }.handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) 41 | .sink(receiveCompletion: { 42 | guard case .finished = $0 else { return XCTFail("The successful completion publisher has failed!") } 43 | exp.fulfill() 44 | }, receiveValue: { XCTAssertEqual($0, value) }) 45 | 46 | self.wait(for: [exp], timeout: 0.2) 47 | cancellable.cancel() 48 | } 49 | 50 | /// Tests a successful completion of the publisher. 51 | func testSuccessfulSyncFailure() { 52 | let exp = self.expectation(description: "Publisher completes successfully") 53 | 54 | let cancellable = DeferredFuture { (promise) in 55 | promise(.failure(_CustomError())) 56 | }.handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) 57 | .sink(receiveCompletion: { 58 | guard case .failure = $0 else { return XCTFail("The failure deferred future publisher has succeeded!") } 59 | exp.fulfill() 60 | }, receiveValue: { _ in XCTFail("No value was expected") }) 61 | 62 | self.wait(for: [exp], timeout: 0.2) 63 | cancellable.cancel() 64 | } 65 | 66 | /// Tests a successful completion of the publisher. 67 | func testSuccessfulAsyncFailure() { 68 | let exp = self.expectation(description: "Publisher completes successfully") 69 | 70 | let cancellable = DeferredFuture { (promise) in 71 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) { promise(.failure(_CustomError())) } 72 | }.handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) 73 | .sink(receiveCompletion: { 74 | guard case .failure = $0 else { return XCTFail("The failure deferred future publisher has failed!") } 75 | exp.fulfill() 76 | }, receiveValue: { _ in XCTFail("No value was expected") }) 77 | 78 | self.wait(for: [exp], timeout: 0.2) 79 | cancellable.cancel() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /docs/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at san.dehesa@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /sources/runtime/publishers/DeferredComplete.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | /// A publisher that never emits any values and just completes. The completion might be successful or with a failure (depending on whether an error was returned in the closure). 4 | /// 5 | /// This publisher only executes the stored closure when it receives a request with a demand greater than zero. Right after closure execution, the closure is removed and cleaned up. 6 | public struct DeferredComplete: Publisher where Failure:Swift.Error { 7 | /// The closure type being store for delayed execution. 8 | public typealias Closure = () -> Failure? 9 | 10 | /// Deferred closure. 11 | /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). 12 | public let closure: Closure 13 | 14 | /// Creates a publisher that forwards a successful completion once it receives a positive request (i.e. a request greater than zero) 15 | public init() { 16 | self.closure = { nil } 17 | } 18 | 19 | /// Creates a publisher that completes successfully or fails depending on the result of the given closure. 20 | /// - parameter output: The output type of this *empty* publisher. It is given here as convenience, since it may help compiler inferral. 21 | /// - parameter closure: The closure which produces an empty successful completion (if it returns `nil`) or a failure (if it returns an error). 22 | /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). 23 | @inlinable public init(output: Output.Type = Output.self, closure: @escaping Closure) { 24 | self.closure = closure 25 | } 26 | 27 | /// Creates a publisher that fails with the error provided. 28 | /// - parameter error: *Autoclosure* that will get executed on the first positive request (i.e. a request greater than zero). 29 | @inlinable public init(error: @autoclosure @escaping ()->Failure) { 30 | self.closure = { error() } 31 | } 32 | 33 | public func receive(subscriber: S) where S:Subscriber, S.Input==Output, S.Failure==Failure { 34 | let subscription = _Conduit(downstream: subscriber, closure: self.closure) 35 | subscriber.receive(subscription: subscription) 36 | } 37 | } 38 | 39 | private extension DeferredComplete { 40 | /// The shadow subscription chain's origin. 41 | final class _Conduit: Subscription where Downstream:Subscriber, Downstream.Failure==Failure { 42 | /// Enum listing all possible conduit states. 43 | @ConduitLock private var state: ConduitState 44 | 45 | init(downstream: Downstream, closure: @escaping Closure) { 46 | self.state = .active(_Configuration(downstream: downstream, closure: closure)) 47 | } 48 | 49 | deinit { 50 | self._state.invalidate() 51 | } 52 | 53 | func request(_ demand: Subscribers.Demand) { 54 | guard demand > 0, case .active(let config) = self._state.terminate() else { return } 55 | 56 | if let error = config.closure() { 57 | return config.downstream.receive(completion: .failure(error)) 58 | } else { 59 | return config.downstream.receive(completion: .finished) 60 | } 61 | } 62 | 63 | func cancel() { 64 | self._state.terminate() 65 | } 66 | } 67 | } 68 | 69 | private extension DeferredComplete._Conduit { 70 | /// Values needed for the subscription's active state. 71 | struct _Configuration { 72 | /// The downstream subscriber awaiting any value and/or completion events. 73 | let downstream: Downstream 74 | /// The closure generating the succesful/failure completion. 75 | let closure: DeferredComplete.Closure 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /sources/runtime/subscribers/FixedSink.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | extension Subscribers { 4 | /// A subscriber that requests the given number of values upon subscription and then don't request any further. 5 | /// 6 | /// For example, if the subscriber is initialized with a demand of 5, this subscriber will received 0 to 5 values, but no more. 7 | /// ```swift 8 | /// let subscriber = FixedSink(demand: .max(5), receiveValue: { print($0) }) 9 | /// ``` 10 | /// If five values are received, then a successful completion is send to `receiveCompletion` and the upstream gets cancelled. 11 | public final class FixedSink: Subscriber, Cancellable where Failure:Error { 12 | /// The total allowed value events. 13 | public let demand: Int 14 | /// The closure executed when a value is received. 15 | public private(set) var receiveValue: ((Input)->Void)? 16 | /// The closure executed when a completion event is received. 17 | public private(set) var receiveCompletion: ((Subscribers.Completion)->Void)? 18 | /// The subscriber's state. 19 | @ConduitLock private var state: ConduitState 20 | 21 | /// Designated initializer specifying the number of expected values. 22 | /// - precondition: `demand` must be greater than zero. 23 | /// - parameter demand: The maximum number of values to be received. 24 | /// - parameter receiveCompletion: The closure executed when the provided amount of values are received or a completion event is received. 25 | /// - parameter receiveValue: The closure executed when a value is received. 26 | public init(demand: Int, receiveCompletion: ((Subscribers.Completion)->Void)? = nil, receiveValue: ((Input)->Void)? = nil) { 27 | precondition(demand > 0) 28 | self.demand = demand 29 | self.receiveValue = receiveValue 30 | self.receiveCompletion = receiveCompletion 31 | self.state = .awaitingSubscription(()) 32 | } 33 | 34 | deinit { 35 | self.cancel() 36 | self._state.invalidate() 37 | } 38 | 39 | public func receive(subscription: Subscription) { 40 | guard case .some = self._state.activate(atomic: { _ in .init(upstream: subscription, receivedValues: 0) }) else { 41 | return subscription.cancel() 42 | } 43 | subscription.request(.max(self.demand)) 44 | } 45 | 46 | public func receive(_ input: Input) -> Subscribers.Demand { 47 | self._state.lock() 48 | guard var config = self._state.value.activeConfiguration else { 49 | self._state.unlock() 50 | return .none 51 | } 52 | config.receivedValues += 1 53 | 54 | if config.receivedValues < self.demand { 55 | self._state.value = .active(config) 56 | self._state.unlock() 57 | self.receiveValue?(input) 58 | } else { 59 | self._state.value = .terminated 60 | self._state.unlock() 61 | self.receiveValue?(input) 62 | self.receiveValue = nil 63 | self.receiveCompletion?(.finished) 64 | self.receiveCompletion = nil 65 | config.upstream.cancel() 66 | } 67 | 68 | return .none 69 | } 70 | 71 | public func receive(completion: Subscribers.Completion) { 72 | guard case .active = self._state.terminate() else { return } 73 | self.receiveValue = nil 74 | self.receiveCompletion?(completion) 75 | self.receiveCompletion = nil 76 | } 77 | 78 | public func cancel() { 79 | guard case .active = self._state.terminate() else { return } 80 | self.receiveValue = nil 81 | self.receiveCompletion = nil 82 | } 83 | } 84 | } 85 | 86 | private extension Subscribers.FixedSink { 87 | /// Variables required during the *active* stage. 88 | struct _Configuration { 89 | /// Upstream subscription. 90 | let upstream: Subscription 91 | /// The current amount of values received. 92 | var receivedValues: Int 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /sources/runtime/publishers/DeferredFuture.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | /// A publisher that may produce a value before completing (whether successfully or with a failure). 4 | /// 5 | /// This publisher only executes the stored closure when it receives a request with a demand greater than zero. Right after the closure execution, the closure is removed and clean up. 6 | public struct DeferredFuture: Publisher { 7 | /// The promise returning the value (or failure) of the whole publisher. 8 | public typealias Promise = (Result) -> Void 9 | /// The closure type being store for delayed execution. 10 | public typealias Closure = (_ promise: @escaping Promise) -> Void 11 | 12 | /// Deferred closure. 13 | /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). 14 | public let closure: Closure 15 | 16 | /// Creates a publisher that send a value and completes successfully or just fails depending on the result of the given closure. 17 | /// - parameter attempToFulfill: Closure in charge of generating the value to be emitted. 18 | /// - attention: The closure is kept till a greater-than-zero demand is received (at which point, it is executed and then deleted). 19 | @inlinable public init(_ attempToFulfill: @escaping Closure) { 20 | self.closure = attempToFulfill 21 | } 22 | 23 | public func receive(subscriber: S) where S: Subscriber, S.Input==Output, S.Failure==Failure { 24 | let subscription = _Conduit(downstream: subscriber, closure: self.closure) 25 | subscriber.receive(subscription: subscription) 26 | } 27 | } 28 | 29 | private extension DeferredFuture { 30 | /// The shadow subscription chain's origin. 31 | final class _Conduit: Subscription where Downstream:Subscriber, Downstream.Input==Output, Downstream.Failure==Failure { 32 | /// Enum listing all possible conduit states. 33 | @ConduitLock private var state: ConduitState 34 | 35 | init(downstream: Downstream, closure: @escaping Closure) { 36 | self.state = .active(_Configuration(downstream: downstream, step: .awaitingExecution(closure))) 37 | } 38 | 39 | deinit { 40 | self._state.invalidate() 41 | } 42 | 43 | func request(_ demand: Subscribers.Demand) { 44 | guard demand > 0 else { return } 45 | 46 | self._state.lock() 47 | guard var config = self._state.value.activeConfiguration, 48 | case .awaitingExecution(let closure) = config.step else { return self._state.unlock() } 49 | config.step = .awaitingPromise 50 | self._state.value = .active(config) 51 | self._state.unlock() 52 | 53 | closure { [weak self] (result) in 54 | guard let self = self else { return } 55 | 56 | guard case .active(let config) = self._state.terminate() else { return } 57 | guard case .awaitingPromise = config.step else { fatalError() } 58 | 59 | switch result { 60 | case .success(let value): 61 | _ = config.downstream.receive(value) 62 | config.downstream.receive(completion: .finished) 63 | case .failure(let error): 64 | config.downstream.receive(completion: .failure(error)) 65 | } 66 | } 67 | } 68 | 69 | func cancel() { 70 | self._state.terminate() 71 | } 72 | } 73 | } 74 | 75 | private extension DeferredFuture._Conduit { 76 | /// Values needed for the subscription's active state. 77 | struct _Configuration { 78 | /// The downstream subscriber awaiting any value and/or completion events. 79 | let downstream: Downstream 80 | /// The state on the promise execution 81 | var step: Step 82 | 83 | enum Step { 84 | /// The closure hasn't been executed. 85 | case awaitingExecution(_ closure: DeferredFuture.Closure) 86 | /// The closure has been executed, but no promise has been received yet. 87 | case awaitingPromise 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tests/testing/ExpectationsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Conbini 3 | import ConbiniForTesting 4 | import Combine 5 | 6 | /// Tests the correct behavior of the *expectation* conveniences. 7 | final class ExpectationsTests: XCTestCase { 8 | override func setUp() { 9 | self.continueAfterFailure = false 10 | } 11 | 12 | /// A custom error to send as a dummy. 13 | private struct _CustomError: Swift.Error {} 14 | } 15 | 16 | extension ExpectationsTests { 17 | /// Tests successful completion expectations. 18 | func testCompletionExpectations() { 19 | [0, 2, 4, 6, 8].publisher.expectsCompletion(timeout: 0.2, on: self) 20 | 21 | DeferredPassthrough { (subject) in 22 | subject.send(0) 23 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(20)) { subject.send(2) } 24 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(40)) { subject.send(4) } 25 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(60)) { subject.send(6) } 26 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(80)) { subject.send(completion: .finished) } 27 | }.expectsCompletion(timeout: 0.2, on: self) 28 | } 29 | 30 | /// Tests failure completion expectations. 31 | func testFailedExpectations() { 32 | Fail(error: .init()).expectsFailure(timeout: 0.2, on: self) 33 | 34 | DeferredPassthrough { (subject) in 35 | subject.send(0) 36 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(20)) { subject.send(2) } 37 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(40)) { subject.send(4) } 38 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(60)) { subject.send(6) } 39 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(80)) { subject.send(completion: .failure(_CustomError())) } 40 | }.expectsFailure(timeout: 0.2, on: self) 41 | } 42 | 43 | /// Tests one single value emission expectations. 44 | func testSingleValueEmissionExpectations() { 45 | Just(0).expectsOne(timeout: 0.2, on: self) 46 | 47 | DeferredPassthrough { (subject) in 48 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(20)) { subject.send(0) } 49 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(80)) { subject.send(completion: .failure(_CustomError())) } 50 | }.expectsFailure(timeout: 0.2, on: self) 51 | } 52 | 53 | /// Tests the reception of all emitted values. 54 | func testAllEmissionExpectations() { 55 | let values = [0, 2, 4, 6, 8] 56 | 57 | let sequenceEmitted = values.publisher.expectsAll(timeout: 0.2, on: self) 58 | XCTAssertEqual(values, sequenceEmitted) 59 | 60 | let subjectEmitted = DeferredPassthrough { (subject) in 61 | for (index, value) in values.enumerated() { 62 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(index*10)) { subject.send(value) } 63 | } 64 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(values.count*10)) { subject.send(completion: .finished) } 65 | }.expectsAll(timeout: 0.2, on: self) 66 | XCTAssertEqual(values, subjectEmitted) 67 | } 68 | 69 | /// Tests the reception of at least a given amount of values. 70 | func testAtLeastEmisionExpectations() { 71 | let values = [0, 2, 4, 6, 8] 72 | 73 | let sequenceEmitted = values.publisher.expectsAtLeast(values: 2, timeout: 0.2, on: self) 74 | XCTAssertEqual(.init(values[0..<2]), sequenceEmitted) 75 | 76 | let subjectEmitted = DeferredPassthrough { (subject) in 77 | for (index, value) in values.enumerated() { 78 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(index*10)) { subject.send(value) } 79 | } 80 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(values.count*10)) { subject.send(completion: .finished) } 81 | }.expectsAtLeast(values: 2, timeout: 0.2, on: self) 82 | XCTAssertEqual(.init(values[0..<2]), subjectEmitted) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /tests/runtime/publishers/DeferredCompleteTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Conbini 3 | import Combine 4 | 5 | /// Tests the correct behavior of the `DeferredComplete` publisher. 6 | final class DeferredCompleteTests: XCTestCase { 7 | override func setUp() { 8 | self.continueAfterFailure = false 9 | } 10 | 11 | /// A custom error to send as a dummy. 12 | private struct _CustomError: Swift.Error {} 13 | } 14 | 15 | extension DeferredCompleteTests { 16 | /// Tests a successful completion of the publisher. 17 | func testSuccessfulEmptyCompletion() { 18 | let exp = self.expectation(description: "Publisher completes successfully") 19 | 20 | let cancellable = DeferredComplete() 21 | .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) 22 | .sink(receiveCompletion: { 23 | guard case .finished = $0 else { return XCTFail("The successful completion publisher has failed!") } 24 | exp.fulfill() 25 | }, receiveValue: { _ in XCTFail("The empty complete publisher has emitted a value!") }) 26 | 27 | self.wait(for: [exp], timeout: 0.2) 28 | cancellable.cancel() 29 | } 30 | 31 | /// Tests a successful closure completion. 32 | func testSuccessfulClosureCompletion() { 33 | let exp = self.expectation(description: "Publisher completes successfully") 34 | 35 | let cancellable = DeferredComplete { return nil } 36 | .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) 37 | .sink(receiveCompletion: { 38 | guard case .finished = $0 else { return XCTFail("The successful completion publisher has failed!") } 39 | exp.fulfill() 40 | }, receiveValue: { _ in XCTFail("The empty complete publisher has emitted a value!") }) 41 | 42 | self.wait(for: [exp], timeout: 0.2) 43 | cancellable.cancel() 44 | } 45 | 46 | /// Tests a failure completion from an autoclosure. 47 | func testFailedCompletion() { 48 | let exp = self.expectation(description: "Publisher completes with a failure") 49 | 50 | let cancellable = DeferredComplete(error: .init()) 51 | .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) 52 | .sink(receiveCompletion: { 53 | guard case .failure = $0 else { return XCTFail("The failed completion publisher has completed successfully!") } 54 | exp.fulfill() 55 | }, receiveValue: { _ in XCTFail("The empty complete publisher has emitted a value!") }) 56 | 57 | self.wait(for: [exp], timeout: 0.2) 58 | cancellable.cancel() 59 | } 60 | 61 | /// Tests a failure completion from the full-fledge closure. 62 | func testFailedClosureCompletion() { 63 | let exp = self.expectation(description: "Publisher completes with a failure") 64 | 65 | let cancellable = DeferredComplete(output: Int.self) { return _CustomError() } 66 | .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) 67 | .sink(receiveCompletion: { 68 | guard case .failure = $0 else { return XCTFail("The failed completion publisher has completed successfully!") } 69 | exp.fulfill() 70 | }, receiveValue: { _ in XCTFail("The empty complete publisher has emitted a value!") }) 71 | 72 | self.wait(for: [exp], timeout: 0.2) 73 | cancellable.cancel() 74 | } 75 | 76 | /// Tests the correct resource dumping. 77 | func testPublisherPipeline() { 78 | let exp = self.expectation(description: "Publisher completes successfully") 79 | 80 | let cancellable = DeferredComplete() 81 | .map { $0 * 2 } 82 | .handleEvents(receiveCancel: { XCTFail("The publisher has cancelled before completion") }) 83 | .sink(receiveCompletion: { 84 | guard case .finished = $0 else { return XCTFail("The successful completion publisher has failed!") } 85 | exp.fulfill() 86 | }, receiveValue: { _ in XCTFail("The empty complete publisher has emitted a value!") }) 87 | 88 | self.wait(for: [exp], timeout: 0.2) 89 | cancellable.cancel() 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/runtime/operators/DelayedRetryOpTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Conbini 3 | import Combine 4 | 5 | /// Tests the correct behavior of the `Then` operator. 6 | final class DelayedRetryOpTests: XCTestCase { 7 | override func setUp() { 8 | self.continueAfterFailure = false 9 | } 10 | 11 | /// A custom error to send as a dummy. 12 | private struct _CustomError: Swift.Error {} 13 | } 14 | 15 | extension DelayedRetryOpTests { 16 | /// Test a normal "happy path" example for the custom "delayed retry" operator. 17 | func testSuccessfulEnding() { 18 | let e = self.expectation(description: "Successful completion") 19 | 20 | let input = 0..<10 21 | var output = [Int]() 22 | let queue = DispatchQueue(label: "io.dehesa.conbini.tests.operators.retry") 23 | 24 | let cancellable = input.publisher 25 | .map { $0 * 2 } 26 | .retry(on: queue, intervals: [0, 0.2, 0.5]) 27 | .map { $0 * 2 } 28 | .sink(receiveCompletion: { 29 | guard case .finished = $0 else { return XCTFail("A failure completion has been received, when a successful one was expected") } 30 | e.fulfill() 31 | }, receiveValue: { output.append($0) }) 32 | 33 | self.wait(for: [e], timeout: 0.2) 34 | XCTAssertEqual(Array(input).map { $0 * 4 }, output) 35 | cancellable.cancel() 36 | } 37 | 38 | /// Tests a failure reception and successful recovery. 39 | func testSingleFailureRecovery() { 40 | let e = self.expectation(description: "Single failure recovery") 41 | 42 | let input = [0, 1, 2] 43 | var output = [Int]() 44 | var passes = 0 45 | var marker: (start: CFAbsoluteTime?, end: CFAbsoluteTime?) = (nil, nil) 46 | let queue = DispatchQueue(label: "io.dehesa.conbini.tests.operators.retry") 47 | 48 | let cancellable = DeferredPassthrough { (subject) in 49 | passes += 1 50 | if passes == 2 { marker.end = CFAbsoluteTimeGetCurrent() } 51 | 52 | subject.send(input[0]) 53 | subject.send(input[1]) 54 | subject.send(input[2]) 55 | 56 | if passes == 1 { 57 | marker.start = CFAbsoluteTimeGetCurrent() 58 | subject.send(completion: .failure(_CustomError())) 59 | } else { 60 | subject.send(completion: .finished) 61 | } 62 | }.retry(on: queue, intervals: [0.2, 0.4, 0.6]) 63 | .sink(receiveCompletion: { 64 | guard case .finished = $0 else { return XCTFail("A failure completion has been received, when a successful one was expected") } 65 | e.fulfill() 66 | }, receiveValue: { output.append($0) }) 67 | 68 | self.wait(for: [e], timeout: 0.6) 69 | XCTAssertEqual(passes, 2) 70 | XCTAssertEqual(input + input, output) 71 | XCTAssertGreaterThan(marker.end! - marker.start!, 0.2) 72 | cancellable.cancel() 73 | } 74 | 75 | /// Test publishing a stream of failure from which is impossible to recover. 76 | func testFailuresStream() { 77 | let e = self.expectation(description: "Failure stream") 78 | 79 | let intervals: [TimeInterval] = [0.1, 0.3, 0.5] 80 | var markers = [CFAbsoluteTime]() 81 | let queue = DispatchQueue(label: "io.dehesa.conbini.tests.operators.retry") 82 | 83 | let cancellable = Deferred { Fail(outputType: Int.self, failure: _CustomError()) } 84 | .handleEvents(receiveCompletion: { 85 | markers.append(CFAbsoluteTimeGetCurrent()) 86 | guard case .failure = $0 else { return XCTFail("A success completion has been received, when a failure one was expected") } 87 | }) 88 | .retry(on: queue, intervals: intervals) 89 | .sink(receiveCompletion: { 90 | markers.append(CFAbsoluteTimeGetCurrent()) 91 | guard case .failure = $0 else { return XCTFail("A success completion has been received, when a failure one was expected") } 92 | e.fulfill() 93 | }, receiveValue: { _ in XCTFail("A value has been received when none were expected") }) 94 | 95 | self.wait(for: [e], timeout: 3) 96 | XCTAssertEqual(markers.count, 5) 97 | XCTAssertGreaterThan(markers[1] - intervals[0], intervals[0]) 98 | XCTAssertGreaterThan(markers[2] - intervals[1], intervals[1]) 99 | XCTAssertGreaterThan(markers[3] - intervals[2], intervals[2]) 100 | cancellable.cancel() 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /sources/runtime/publishers/HandleEnd.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | extension Publishers { 4 | /// A publisher that performs the specified closure when the publisher completes or get cancelled. 5 | public struct HandleEnd: Publisher where Upstream:Publisher { 6 | public typealias Output = Upstream.Output 7 | public typealias Failure = Upstream.Failure 8 | /// Closure getting executed once the publisher receives a completion event or when the publisher gets cancelled. 9 | /// - parameter completion: A completion event if the publisher completes (whether successfully or not), or `nil` in case the publisher is cancelled. 10 | public typealias Closure = (_ completion: Subscribers.Completion?) -> Void 11 | 12 | /// Publisher emitting the events being received here. 13 | public let upstream: Upstream 14 | /// Closure executing the *ending* event. 15 | public let closure: Closure 16 | 17 | /// Designated initializer providing the upstream publisher and the closure receiving the *ending* event. 18 | /// - parameter upstream: Upstream publisher chain. 19 | /// - parameter handle: A closure that executes when the publisher receives a completion event or when the publisher gets cancelled. 20 | @inlinable public init(upstream: Upstream, handle: @escaping Closure) { 21 | self.upstream = upstream 22 | self.closure = handle 23 | } 24 | 25 | public func receive(subscriber: S) where S:Subscriber, S.Input==Output, S.Failure==Failure { 26 | let conduit = _Conduit(downstream: subscriber, closure: self.closure) 27 | self.upstream.subscribe(conduit) 28 | } 29 | } 30 | } 31 | 32 | private extension Publishers.HandleEnd { 33 | /// Represents an active `HandleEnd` publisher taking both the role of `Subscriber` (for upstream publishers) and `Subscription` (for downstream subscribers). 34 | final class _Conduit: Subscription, Subscriber where Downstream:Subscriber, Downstream.Input==Output, Downstream.Failure==Failure { 35 | typealias Input = Upstream.Output 36 | typealias Failure = Upstream.Failure 37 | /// Enum listing all possible states. 38 | @ConduitLock private var state: ConduitState<_WaitConfiguration,_ActiveConfiguration> 39 | 40 | init(downstream: Downstream, closure: @escaping Closure) { 41 | self.state = .awaitingSubscription(.init(closure: closure, downstream: downstream)) 42 | } 43 | 44 | deinit { 45 | self.cancel() 46 | self._state.invalidate() 47 | } 48 | 49 | func receive(subscription: Subscription) { 50 | guard let config = self._state.activate(atomic: { .init(upstream: subscription, closure: $0.closure, downstream: $0.downstream) }) else { 51 | return subscription.cancel() 52 | } 53 | config.downstream.receive(subscription: self) 54 | } 55 | 56 | func request(_ demand: Subscribers.Demand) { 57 | self._state.lock() 58 | guard let config = self._state.value.activeConfiguration else { return self._state.unlock() } 59 | self._state.unlock() 60 | config.upstream.request(demand) 61 | } 62 | 63 | func receive(_ input: Upstream.Output) -> Subscribers.Demand { 64 | self._state.lock() 65 | guard let config = self._state.value.activeConfiguration else { self._state.unlock(); return .unlimited } 66 | self._state.unlock() 67 | 68 | return config.downstream.receive(input) 69 | } 70 | 71 | func receive(completion: Subscribers.Completion) { 72 | switch self._state.terminate() { 73 | case .active(let config): 74 | config.closure(completion) 75 | config.downstream.receive(completion: completion) 76 | case .terminated: return 77 | case .awaitingSubscription: fatalError() 78 | } 79 | } 80 | 81 | func cancel() { 82 | switch self._state.terminate() { 83 | case .awaitingSubscription(let config): 84 | config.closure(nil) 85 | case .active(let config): 86 | config.closure(nil) 87 | config.upstream.cancel() 88 | case .terminated: return 89 | } 90 | } 91 | } 92 | } 93 | 94 | private extension Publishers.HandleEnd._Conduit { 95 | /// The necessary variables during the *awaiting* stage. 96 | struct _WaitConfiguration { 97 | /// The closure being executed only once when the publisher completes or get cancelled. 98 | let closure: Publishers.HandleEnd.Closure 99 | /// The subscriber further down the chain. 100 | let downstream: Downstream 101 | } 102 | 103 | /// The necessary variables during the *active* stage. 104 | struct _ActiveConfiguration { 105 | /// The upstream subscription. 106 | let upstream: Subscription 107 | /// The closure being executed only once when the publisher completes or get cancelled. 108 | let closure: Publishers.HandleEnd.Closure 109 | /// The subscriber further down the chain. 110 | let downstream: Downstream 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /sources/runtime/publishers/DeferredPassthrough.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | /// Similar to a `Passthrough` subject with the difference that the given closure will only get activated once the first positive demand is received. 4 | /// 5 | /// There are some interesting quirks to this publisher: 6 | /// - Each subscription to the publisher will get its own `Passthrough` subject. 7 | /// - The `Passthrough` subject passed on the closure is already *chained* and can start forwarding values right away. 8 | /// - The given closure will receive the `Passthrough` at the origin of the chain so it can be used to send information downstream. 9 | /// - The closure will get *cleaned up* as soon as it returns. 10 | /// - remark: Please notice, the pipeline won't complete if the subject within the closure doesn't forwards `.send(completion:)`. 11 | public struct DeferredPassthrough: Publisher { 12 | /// The closure type being store for delayed execution. 13 | public typealias Closure = (PassthroughSubject) -> Void 14 | 15 | /// Publisher's closure storage. 16 | /// - note: The closure is kept in the publisher, thus if you keep the publisher around any reference in the closure will be kept too. 17 | public let closure: Closure 18 | /// Creates a publisher that sends 19 | /// - parameter setup: The closure for delayed execution. 20 | /// - remark: Please notice, the pipeline won't complete if the subject within the closure doesn't send `.send(completion:)`. 21 | @inlinable public init(_ setup: @escaping Closure) { 22 | self.closure = setup 23 | } 24 | 25 | public func receive(subscriber: S) where S:Subscriber, S.Input==Output, S.Failure==Failure { 26 | let upstream = PassthroughSubject() 27 | let conduit = _Conduit(upstream: upstream, downstream: subscriber, closure: self.closure) 28 | upstream.subscribe(conduit) 29 | } 30 | } 31 | 32 | private extension DeferredPassthrough { 33 | /// Internal Shadow subscription catching all messages from downstream and forwarding them upstream. 34 | final class _Conduit: Subscription, Subscriber where Downstream:Subscriber, Downstream.Input==Output, Downstream.Failure==Failure { 35 | /// Enum listing all possible conduit states. 36 | @ConduitLock private var state: ConduitState<_WaitConfiguration,_ActiveConfiguration> 37 | 38 | /// Designated initializer passing all the needed info (except the upstream subscription). 39 | init(upstream: PassthroughSubject, downstream: Downstream, closure: @escaping Closure) { 40 | self.state = .awaitingSubscription(_WaitConfiguration(upstream: upstream, downstream: downstream, closure: closure)) 41 | } 42 | 43 | deinit { 44 | self.cancel() 45 | self._state.invalidate() 46 | } 47 | 48 | func receive(subscription: Subscription) { 49 | guard let config = self._state.activate(atomic: { _ActiveConfiguration(upstream: subscription, downstream: $0.downstream, setup: ($0.upstream, $0.closure)) }) else { 50 | return subscription.cancel() 51 | } 52 | config.downstream.receive(subscription: self) 53 | } 54 | 55 | func request(_ demand: Subscribers.Demand) { 56 | guard demand > 0 else { return } 57 | 58 | self._state.lock() 59 | guard let config = self._state.value.activeConfiguration else { return self._state.unlock() } 60 | self._state.value = .active(.init(upstream: config.upstream, downstream: config.downstream, setup: nil)) 61 | self._state.unlock() 62 | 63 | config.upstream.request(demand) 64 | guard let (subject, closure) = config.setup else { return } 65 | closure(subject) 66 | } 67 | 68 | func receive(_ input: Output) -> Subscribers.Demand { 69 | self._state.lock() 70 | guard let config = self._state.value.activeConfiguration else { 71 | self._state.unlock() 72 | return .none 73 | } 74 | self._state.unlock() 75 | return config.downstream.receive(input) 76 | } 77 | 78 | func receive(completion: Subscribers.Completion) { 79 | guard case .active(let config) = self._state.terminate() else { return } 80 | config.downstream.receive(completion: completion) 81 | } 82 | 83 | func cancel() { 84 | guard case .active(let config) = self._state.terminate() else { return } 85 | config.upstream.cancel() 86 | } 87 | } 88 | } 89 | 90 | private extension DeferredPassthrough._Conduit { 91 | /// Values needed for the subscription's awaiting state. 92 | struct _WaitConfiguration { 93 | let upstream: PassthroughSubject 94 | let downstream: Downstream 95 | let closure: DeferredPassthrough.Closure 96 | } 97 | 98 | /// Values needed for the subscription's active state. 99 | struct _ActiveConfiguration { 100 | typealias Setup = (subject: PassthroughSubject, closure: DeferredPassthrough.Closure) 101 | 102 | let upstream: Subscription 103 | let downstream: Downstream 104 | var setup: Setup? 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tests/runtime/operators/HandleEndOpTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Conbini 3 | import Combine 4 | 5 | /// Tests the correct behavior of the `Then` operator. 6 | final class HandleEndOpTests: XCTestCase { 7 | override func setUp() { 8 | self.continueAfterFailure = false 9 | } 10 | 11 | /// A custom error to send as a dummy. 12 | private struct _CustomError: Swift.Error {} 13 | } 14 | 15 | extension HandleEndOpTests { 16 | /// Test a normal "happy path" example for the custom "then" combine operator. 17 | func testSuccessfulEnding() { 18 | let e = self.expectation(description: "Successful completion") 19 | 20 | let values = [0, 1, 2] 21 | var (received, isFinished) = ([Int](), false) 22 | 23 | let subject = PassthroughSubject() 24 | let cancellable = subject 25 | .handleEnd { 26 | guard !isFinished else { return XCTFail("The end closure has been executed more than once") } 27 | 28 | switch $0 { 29 | case .none: XCTFail("A cancel event has been received, when a successful completion was expected") 30 | case .finished: isFinished = true 31 | case .failure: XCTFail("A failure completion has been received, when a successful one was expected") 32 | } 33 | }.sink(receiveCompletion: { _ in e.fulfill() }) { received.append($0) } 34 | 35 | let queue = DispatchQueue.main 36 | queue.asyncAfter(deadline: .now() + .milliseconds(100)) { subject.send(values[0]) } 37 | queue.asyncAfter(deadline: .now() + .milliseconds(200)) { subject.send(values[1]) } 38 | queue.asyncAfter(deadline: .now() + .milliseconds(300)) { subject.send(completion: .finished) } 39 | queue.asyncAfter(deadline: .now() + .milliseconds(400)) { subject.send(values[2]) } 40 | 41 | self.wait(for: [e], timeout: 1) 42 | 43 | XCTAssertTrue(isFinished) 44 | XCTAssertEqual(received, values.dropLast()) 45 | cancellable.cancel() 46 | } 47 | 48 | /// Tests the behavior of the `then` operator reacting to a upstream error. 49 | func testFailureEnding() { 50 | let e = self.expectation(description: "Failure completion") 51 | 52 | let values = [0, 1, 2] 53 | var (received, isFinished) = ([Int](), false) 54 | 55 | let subject = PassthroughSubject() 56 | let cancellable = subject 57 | .handleEnd { 58 | guard !isFinished else { return XCTFail("The end closure has been executed more than once") } 59 | 60 | switch $0 { 61 | case .none: XCTFail("A cancel event has been received, when a failure completion was expected") 62 | case .finished: XCTFail("A successful completion has been received, when a failure one was expected") 63 | case .failure: isFinished = true 64 | } 65 | }.sink(receiveCompletion: { _ in e.fulfill() }) { received.append($0) } 66 | 67 | let queue = DispatchQueue.main 68 | queue.asyncAfter(deadline: .now() + .milliseconds(100)) { subject.send(values[0]) } 69 | queue.asyncAfter(deadline: .now() + .milliseconds(200)) { subject.send(values[1]) } 70 | queue.asyncAfter(deadline: .now() + .milliseconds(300)) { subject.send(completion: .failure(_CustomError())) } 71 | queue.asyncAfter(deadline: .now() + .milliseconds(400)) { subject.send(values[2]) } 72 | 73 | self.wait(for: [e], timeout: 1) 74 | 75 | XCTAssertTrue(isFinished) 76 | XCTAssertEqual(received, values.dropLast()) 77 | cancellable.cancel() 78 | } 79 | 80 | func testCancelEnding() { 81 | let eClosure = self.expectation(description: "Cancel completion on closure") 82 | let eTimeout = self.expectation(description: "Cancel completion on timeout") 83 | 84 | let values = [0, 1] 85 | var (received, isFinished) = ([Int](), false) 86 | 87 | let subject = PassthroughSubject() 88 | let cancellable = subject 89 | .handleEnd { 90 | guard !isFinished else { return XCTFail("The end closure has been executed more than once") } 91 | 92 | switch $0 { 93 | case .none: isFinished = true 94 | case .finished: XCTFail("A successful completion has been received, when a cancellation was expected") 95 | case .failure: XCTFail("A failure completion has been received, when a cancellation was expected") 96 | } 97 | 98 | eClosure.fulfill() 99 | }.sink(receiveCompletion: { _ in XCTFail() }) { received.append($0) } 100 | 101 | let queue = DispatchQueue.main 102 | queue.asyncAfter(deadline: .now() + .milliseconds(100)) { subject.send(values[0]) } 103 | queue.asyncAfter(deadline: .now() + .milliseconds(200)) { subject.send(values[1]) } 104 | queue.asyncAfter(deadline: .now() + .milliseconds(300)) { cancellable.cancel() } 105 | queue.asyncAfter(deadline: .now() + .milliseconds(400)) { eTimeout.fulfill() } 106 | 107 | self.wait(for: [eClosure, eTimeout], timeout: 1) 108 | 109 | XCTAssertTrue(isFinished) 110 | XCTAssertEqual(received, values) 111 | cancellable.cancel() 112 | } 113 | 114 | func testAbruptEndings() { 115 | let e = self.expectation(description: "Abrupt completion") 116 | 117 | var isFinished = false 118 | let cancellable = Empty(completeImmediately: true) 119 | .handleEnd { 120 | guard !isFinished else { return XCTFail("The end closure has been executed more than once") } 121 | 122 | switch $0 { 123 | case .none: XCTFail("A cancel event has been received, when a successful completion was expected") 124 | case .finished: isFinished = true 125 | case .failure: XCTFail("A failure completion has been received, when a successful one was expected") 126 | } 127 | }.sink(receiveCompletion: { (_) in e.fulfill() }, receiveValue: { _ in }) 128 | 129 | self.wait(for: [e], timeout: 0.5) 130 | XCTAssertTrue(isFinished) 131 | cancellable.cancel() 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /sources/runtime/operators/AssignOp.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | extension Publisher where Self.Failure==Never { 4 | /// Assigns a publisher's output to a property of an object. 5 | /// 6 | /// The difference between `assign(to:onWeak:)` and Combine's `assign(to:on:)` is two-fold: 7 | /// - `assign(to:onWeak:)` doesn't set a _strong bond_ to `object`. 8 | /// This breaks memory cycles when `object` also stores the returned cancellable (e.g. passing `self` is a common case). 9 | /// - `assign(to:onWeak:)` cancels the upstream publisher if it detects `object` is deinitialized. 10 | /// 11 | /// The difference between is that a _strong bond_ is not set to the`object`. This breaks memory cycles when `object` also stores the returned cancellable (e.g. passing `self` is a common case). 12 | /// - parameter keyPath: A key path that indicates the property to assign. 13 | /// - parameter object: The object that contains the property. The subscriber assigns the object's property every time it receives a new value. 14 | /// - returns: An `AnyCancellable` instance. Call `cancel()` on the instance when you no longer want the publisher to automatically assign the property. Deinitializing this instance will also cancel automatic assignment. 15 | @_transparent public func assign(to keyPath: ReferenceWritableKeyPath, onWeak object: Root) -> AnyCancellable where Root:AnyObject { 16 | weak var cancellable: AnyCancellable? = nil 17 | let cleanup: (Subscribers.Completion) -> Void = { _ in 18 | cancellable?.cancel() 19 | cancellable = nil 20 | } 21 | 22 | let subscriber = Subscribers.Sink(receiveCompletion: cleanup, receiveValue: { [weak object] (value) in 23 | guard let object = object else { return cleanup(.finished) } 24 | object[keyPath: keyPath] = value 25 | }) 26 | 27 | let result = AnyCancellable(subscriber) 28 | cancellable = result 29 | self.subscribe(subscriber) 30 | return result 31 | } 32 | 33 | /// Assigns a publisher's output to a property of an object. 34 | /// 35 | /// The difference between `assign(to:onUnowned:)` and Combine's `assign(to:on:)` is that a _strong bond_ is not set to the`object`. This breaks memory cycles when `object` also stores the returned cancellable (e.g. passing `self` is a common case). 36 | /// - parameter keyPath: A key path that indicates the property to assign. 37 | /// - parameter object: The object that contains the property. The subscriber assigns the object's property every time it receives a new value. 38 | /// - returns: An `AnyCancellable` instance. Call `cancel()` on the instance when you no longer want the publisher to automatically assign the property. Deinitializing this instance will also cancel automatic assignment. 39 | @_transparent public func assign(to keyPath: ReferenceWritableKeyPath, onUnowned object: Root) -> AnyCancellable where Root:AnyObject { 40 | self.sink(receiveValue: { [unowned object] (value) in 41 | object[keyPath: keyPath] = value 42 | }) 43 | } 44 | } 45 | 46 | extension Publisher where Self.Failure==Never { 47 | /// Invoke on the given instance the specified method. 48 | /// - remark: A strong bond is set to `instance`. If you store the cancellable in the same instance as `instance`, a memory cycle will be created. 49 | /// - parameter method: A method/function metatype. 50 | /// - parameter instance: The instance defining the specified method. 51 | /// - returns: An `AnyCancellable` instance. Call `cancel()` on the instance when you no longer want the publisher to automatically call the method. Deinitializing this instance will also cancel automatic invocation. 52 | @_transparent public func invoke(_ method: @escaping (Root)->(Output)->Void, on instance: Root) -> AnyCancellable { 53 | self.sink(receiveValue: { (value) in 54 | method(instance)(value) 55 | }) 56 | } 57 | 58 | /// Invoke on the given instance the specified method. 59 | /// 60 | /// The difference between `invoke(_:onWeak:)` and Combine's `invoke(_:on:)` is two-fold: 61 | /// - `invoke(_:onWeak:)` doesn't set a _strong bond_ to `object`. 62 | /// This breaks memory cycles when `object` also stores the returned cancellable (e.g. passing `self` is a common case). 63 | /// - `invoke(_:onWeak:)` cancels the upstream publisher if it detects `object` is deinitialized. 64 | /// 65 | /// - parameter method: A method/function metatype. 66 | /// - parameter instance: The instance defining the specified method. 67 | /// - returns: An `AnyCancellable` instance. Call `cancel()` on the instance when you no longer want the publisher to automatically call the method. Deinitializing this instance will also cancel automatic invocation. 68 | @_transparent public func invoke(_ method: @escaping (Root)->(Output)->Void, onWeak object: Root) -> AnyCancellable where Root:AnyObject { 69 | weak var cancellable: AnyCancellable? = nil 70 | let cleanup: (Subscribers.Completion) -> Void = { _ in 71 | cancellable?.cancel() 72 | cancellable = nil 73 | } 74 | 75 | let subscriber = Subscribers.Sink(receiveCompletion: cleanup, receiveValue: { [weak object] (value) in 76 | guard let object = object else { return cleanup(.finished) } 77 | method(object)(value) 78 | }) 79 | 80 | let result = AnyCancellable(subscriber) 81 | cancellable = result 82 | self.subscribe(subscriber) 83 | return result 84 | } 85 | 86 | /// Invoke on the given instance the specified method. 87 | /// 88 | /// The difference between `invoke(_:onUnowned:)` and Combine's `invoke(_:on:)` is that a _strong bond_ is not set to the`object`. This breaks memory cycles when `object` also stores the returned cancellable (e.g. passing `self` is a common case). 89 | /// - parameter method: A method/function metatype. 90 | /// - parameter instance: The instance defining the specified method. 91 | /// - returns: An `AnyCancellable` instance. Call `cancel()` on the instance when you no longer want the publisher to automatically call the method. Deinitializing this instance will also cancel automatic invocation. 92 | @_transparent public func invoke(_ method: @escaping (Root)->(Output)->Void, onUnowned object: Root) -> AnyCancellable where Root:AnyObject { 93 | self.sink(receiveValue: { [unowned object] (value) in 94 | method(object)(value) 95 | }) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /docs/assets/badges/Apple.svg: -------------------------------------------------------------------------------- 1 | +6watchOS+13tvOS+13iOS+10.15macOS -------------------------------------------------------------------------------- /sources/testing/Expectations.swift: -------------------------------------------------------------------------------- 1 | #if !os(watchOS) 2 | import XCTest 3 | import Combine 4 | 5 | extension Publisher { 6 | /// Expects the receiving publisher to complete (with or without values) within the provided timeout. 7 | /// 8 | /// This operator will subscribe to the publisher chain and "block" the running test till the expectation is completed or thet timeout ellapses. 9 | /// - precondition: `timeout` must be greater than or equal to zero. 10 | /// - parameter timeout: The maximum amount of seconds that the test will wait. It must be greater or equal to zero. 11 | /// - parameter test: The test were the expectation shall be fulfilled. 12 | /// - parameter description: The expectation description. 13 | /// - parameter file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called. 14 | /// - parameter line: The line number on which failure occurred. Defaults to the line number on which this function was called. 15 | public func expectsCompletion(timeout: TimeInterval, on test: XCTWaiterDelegate, _ description: String = "The publisher completes successfully", file: StaticString = #file, line: UInt = #line) { 16 | precondition(timeout >= 0) 17 | let exp = XCTestExpectation(description: description) 18 | 19 | let cancellable = self.sink(receiveCompletion: { 20 | switch $0 { 21 | case .finished: exp.fulfill() 22 | case .failure(let e): XCTFail("The publisher completed with failure when successfull completion was expected.\n\(e)\n", file: (file), line: line) 23 | } 24 | }, receiveValue: { _ in return }) 25 | 26 | let waiter = XCTWaiter(delegate: test) 27 | waiter.wait(for: [exp], timeout: timeout) 28 | cancellable.cancel() 29 | } 30 | 31 | /// Expects the receiving publisher to complete with a failure within the provided timeout. 32 | /// 33 | /// This operator will subscribe to the publisher chain and "block" the running test till the expectation is completed or thet timeout ellapses. 34 | /// - precondition: `timeout` must be greater than or equal to zero. 35 | /// - parameter timeout: The maximum amount of seconds that the test will wait. It must be greater or equal to zero. 36 | /// - parameter test: The test were the expectation shall be fulfilled. 37 | /// - parameter description: The expectation description. 38 | /// - parameter file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called. 39 | /// - parameter line: The line number on which failure occurred. Defaults to the line number on which this function was called. 40 | public func expectsFailure(timeout: TimeInterval, on test: XCTWaiterDelegate, _ description: String = "The publisher completes with failure", file: StaticString = #file, line: UInt = #line) { 41 | precondition(timeout >= 0) 42 | let exp = XCTestExpectation(description: description) 43 | 44 | let cancellable = self.sink(receiveCompletion: { 45 | switch $0 { 46 | case .finished: XCTFail("The publisher completed successfully when a failure was expected", file: (file), line: line) 47 | case .failure(_): exp.fulfill() 48 | } 49 | }, receiveValue: { (_) in return }) 50 | 51 | let waiter = XCTWaiter(delegate: test) 52 | waiter.wait(for: [exp], timeout: timeout) 53 | cancellable.cancel() 54 | } 55 | 56 | /// Expects the receiving publisher to produce a single value and then complete within the provided timeout. 57 | /// 58 | /// This operator will subscribe to the publisher chain and "block" the running test till the expectation is completed or thet timeout ellapses. 59 | /// - precondition: `timeout` must be greater than or equal to zero. 60 | /// - parameter timeout: The maximum amount of seconds that the test will wait. It must be greater or equal to zero. 61 | /// - parameter test: The test were the expectation shall be fulfilled. 62 | /// - parameter description: The expectation description. 63 | /// - parameter file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called. 64 | /// - parameter line: The line number on which failure occurred. Defaults to the line number on which this function was called. 65 | /// - returns: The value forwarded by the publisher. 66 | @discardableResult public func expectsOne(timeout: TimeInterval, on test: XCTWaiterDelegate, _ description: String = "The publisher emits a single value and then completes successfully", file: StaticString = #file, line: UInt = #line) -> Self.Output { 67 | precondition(timeout >= 0) 68 | let exp = XCTestExpectation(description: description) 69 | 70 | var value: Self.Output? = nil 71 | var cancellable: AnyCancellable? 72 | cancellable = self.sink(receiveCompletion: { 73 | cancellable = nil 74 | switch $0 { 75 | case .failure(let e): 76 | return XCTFail("The publisher completed with failure when successfull completion was expected\n\(e)\n", file: (file), line: line) 77 | case .finished: 78 | guard case .some = value else { 79 | return XCTFail("The publisher completed without outputting any value", file: (file), line: line) 80 | } 81 | exp.fulfill() 82 | } 83 | }, receiveValue: { 84 | guard case .none = value else { 85 | cancellable?.cancel() 86 | cancellable = nil 87 | return XCTFail("The publisher produced more than one value when only one was expected", file: (file), line: line) 88 | } 89 | value = $0 90 | }) 91 | 92 | let waiter = XCTWaiter(delegate: test) 93 | waiter.wait(for: [exp], timeout: timeout) 94 | cancellable?.cancel() 95 | 96 | guard let result = value else { 97 | XCTFail("The publisher didn't produce any value before the timeout ellapsed", file: (file), line: line) 98 | fatalError(file: file, line: line) 99 | } 100 | return result 101 | } 102 | 103 | /// Expects the receiving publisher to produce zero, one, or many values and then complete within the provided timeout. 104 | /// 105 | /// This operator will subscribe to the publisher chain and "block" the running test till the expectation is completed or thet timeout ellapses. 106 | /// - precondition: `timeout` must be greater than or equal to zero. 107 | /// - parameter timeout: The maximum amount of seconds that the test will wait. It must be greater or equal to zero. 108 | /// - parameter test: The test were the expectation shall be fulfilled. 109 | /// - parameter description: The expectation description. 110 | /// - parameter file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called. 111 | /// - parameter line: The line number on which failure occurred. Defaults to the line number on which this function was called. 112 | /// - returns: The forwarded values by the publisher (it can be empty). 113 | @discardableResult public func expectsAll(timeout: TimeInterval, on test: XCTWaiterDelegate, _ description: String = "The publisher emits zero or more value and then completes successfully", file: StaticString = #file, line: UInt = #line) -> [Self.Output] { 114 | precondition(timeout >= 0) 115 | let exp = XCTestExpectation(description: description) 116 | 117 | var result: [Self.Output] = [] 118 | var cancellable: AnyCancellable? 119 | cancellable = self.sink(receiveCompletion: { 120 | cancellable = nil 121 | switch $0 { 122 | case .finished: 123 | exp.fulfill() 124 | case .failure(let e): 125 | XCTFail("The publisher completed with failure when successfull completion was expected\n\(e)\n", file: (file), line: line) 126 | fatalError() 127 | } 128 | }, receiveValue: { result.append($0) }) 129 | 130 | let waiter = XCTWaiter(delegate: test) 131 | waiter.wait(for: [exp], timeout: timeout) 132 | cancellable?.cancel() 133 | return result 134 | } 135 | 136 | /// Expects the receiving publisher to produce at least a given number of values. Once the publisher has produced the given amount of values, it will get cancel by this function. 137 | /// 138 | /// This operator will subscribe to the publisher chain and "block" the running test till the expectation is completed or thet timeout ellapses. 139 | /// - precondition: `values` must be greater than zero. 140 | /// - precondition: `timeout` must be greater than or equal to zero. 141 | /// - parameter timeout: The maximum amount of seconds that the test will wait. It must be greater or equal to zero. 142 | /// - parameter test: The test were the expectation shall be fulfilled. 143 | /// - parameter description: The expectation description. 144 | /// - parameter file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called. 145 | /// - parameter line: The line number on which failure occurred. Defaults to the line number on which this function was called. 146 | /// - parameter check: The closure to be executed per value received. 147 | /// - returns: An array of all the values forwarded by the publisher. 148 | @discardableResult public func expectsAtLeast(values: Int, timeout: TimeInterval, on test: XCTWaiterDelegate, _ description: String = "The publisher emits at least the given amount of values and then completes successfully", file: StaticString = #file, line: UInt = #line, foreach check: ((Output)->Void)? = nil) -> [Self.Output] { 149 | precondition(values > 0 && timeout >= 0) 150 | 151 | let exp = XCTestExpectation(description: "Waiting for \(values) values") 152 | 153 | var result: [Self.Output] = [] 154 | var cancellable: AnyCancellable? 155 | cancellable = self.sink(receiveCompletion: { 156 | cancellable = nil 157 | switch $0 { 158 | case .finished: if result.count == values { return exp.fulfill() } 159 | case .failure(let e): XCTFail(String(describing: e), file: (file), line: line) 160 | } 161 | }, receiveValue: { (output) in 162 | guard result.count < values else { return } 163 | result.append(output) 164 | check?(output) 165 | guard result.count == values else { return } 166 | cancellable?.cancel() 167 | cancellable = nil 168 | exp.fulfill() 169 | }) 170 | 171 | let waiter = XCTWaiter(delegate: test) 172 | waiter.wait(for: [exp], timeout: timeout) 173 | cancellable?.cancel() 174 | return result 175 | } 176 | } 177 | 178 | #endif 179 | -------------------------------------------------------------------------------- /sources/runtime/publishers/Retry.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | extension Publishers { 5 | /// A publisher that attempts to recreate its subscription to a failed upstream publisher waiting a given time interval between retries. 6 | /// 7 | /// Please notice that any value sent before a failure is still received downstream. 8 | public struct DelayedRetry: Publisher where Upstream:Publisher, S:Scheduler { 9 | public typealias Output = Upstream.Output 10 | public typealias Failure = Upstream.Failure 11 | 12 | /// The upstream publisher. 13 | public let upstream: Upstream 14 | /// The scheduler used to wait for a specific time interval. 15 | public let scheduler: S 16 | /// The tolerance used when scheduling a new attempt after a failure. A default implies the minimum tolerance. 17 | public let tolerance: S.SchedulerTimeType.Stride? 18 | /// The options for the specified scheduler. 19 | public let options: S.SchedulerOptions? 20 | /// The amount of seconds being waited after a failure occurrence. Negative values are considered zero. 21 | public let intervals: [TimeInterval] 22 | /// Creates a publisher that transforms the incoming value into another value, but may respond at a time in the future. 23 | /// - parameter upstream: The event emitter to the publisher being created. 24 | /// - parameter scheduler: The scheduler used to wait for the specific intervals. 25 | /// - parameter options: The options for the given scheduler. 26 | /// - parameter intervals: The amount of seconds to wait after a failure occurrence. Negative values are considered zero. 27 | @inlinable public init(upstream: Upstream, scheduler: S, tolerance: S.SchedulerTimeType.Stride? = nil, options: S.SchedulerOptions? = nil, intervals: [TimeInterval]) { 28 | self.upstream = upstream 29 | self.scheduler = scheduler 30 | self.tolerance = tolerance 31 | self.options = options 32 | self.intervals = intervals 33 | } 34 | 35 | public func receive(subscriber: S) where S:Subscriber, S.Input==Output, S.Failure==Failure { 36 | let conduit = DelayedRetry._Conduit(upstream: self.upstream, downstream: subscriber, 37 | scheduler: self.scheduler, tolerance: self.tolerance, options: self.options, 38 | intervals: self.intervals) 39 | self.upstream.subscribe(conduit) 40 | } 41 | } 42 | } 43 | 44 | private extension Publishers.DelayedRetry { 45 | /// Represents an active `DelayedRetry` publisher taking both the role of `Subscriber` (for upstream publishers) and `Subscription` (for downstream subscribers). 46 | final class _Conduit: Subscription, Subscriber where Downstream:Subscriber, Downstream.Input==Output, Downstream.Failure==Failure { 47 | typealias Input = Upstream.Output 48 | typealias Failure = Upstream.Failure 49 | /// Enum listing all possible conduit states. 50 | @ConduitLock private var state: ConduitState<_WaitConfiguration,_ActiveConfiguration> 51 | 52 | init(upstream: Upstream, downstream: Downstream, scheduler: S, tolerance: S.SchedulerTimeType.Stride?, options: S.SchedulerOptions?, intervals: [TimeInterval]) { 53 | self.state = .awaitingSubscription( 54 | .init(publisher: upstream, downstream: downstream, 55 | scheduler: scheduler, tolerance: tolerance, options: options, 56 | intervals: intervals, next: 0, demand: .none) 57 | ) 58 | } 59 | 60 | deinit { 61 | self.cancel() 62 | self._state.invalidate() 63 | } 64 | 65 | func receive(subscription: Subscription) { 66 | guard let config = self._state.activate(atomic: { .init(upstream: subscription, config: $0) }) else { 67 | return subscription.cancel() 68 | } 69 | 70 | switch config.isPrime { 71 | case true: config.downstream.receive(subscription: self) 72 | case false: config.upstream.request(config.demand) 73 | } 74 | } 75 | 76 | func request(_ demand: Subscribers.Demand) { 77 | guard demand > 0 else { return } 78 | self._state.lock() 79 | 80 | switch self._state.value { 81 | case .awaitingSubscription(let config): 82 | guard !config.isPrime else { fatalError("A request cannot happen before a subscription has been performed") } 83 | config.demand += demand 84 | self._state.unlock() 85 | case .active(let config): 86 | config.demand += demand 87 | self._state.unlock() 88 | config.upstream.request(demand) 89 | case .terminated: 90 | self._state.unlock() 91 | } 92 | } 93 | 94 | func receive(_ input: Upstream.Output) -> Subscribers.Demand { 95 | self._state.lock() 96 | guard let config1 = self._state.value.activeConfiguration else { 97 | self._state.unlock() 98 | return .none 99 | } 100 | 101 | config1.demand -= 1 102 | let downstream = config1.downstream 103 | self._state.unlock() 104 | 105 | let demand = downstream.receive(input) 106 | self._state.lock() 107 | guard let config2 = self._state.value.activeConfiguration else { 108 | self._state.unlock() 109 | return .none 110 | } 111 | 112 | config2.demand += demand 113 | self._state.unlock() 114 | return demand 115 | } 116 | 117 | func receive(completion: Subscribers.Completion) { 118 | self._state.lock() 119 | guard let activeConfig = self._state.value.activeConfiguration else { 120 | return self._state.unlock() 121 | } 122 | 123 | guard case .failure = completion, let pause = activeConfig.nextPause else { 124 | let downstream = activeConfig.downstream 125 | self._state.value = .terminated 126 | self._state.unlock() 127 | return downstream.receive(completion: completion) 128 | } 129 | 130 | self._state.value = .awaitingSubscription(.init(config: activeConfig)) 131 | 132 | guard pause > 0 else { 133 | let publisher = activeConfig.publisher 134 | self._state.unlock() 135 | return publisher.subscribe(self) 136 | } 137 | 138 | let config = _WaitConfiguration(config: activeConfig) 139 | let (scheduler, tolerance, options) = (config.scheduler, config.tolerance ?? config.scheduler.minimumTolerance, config.options) 140 | self._state.unlock() 141 | 142 | let date = scheduler.now.advanced(by: .seconds(pause)) 143 | scheduler.schedule(after: date, tolerance: tolerance, options: options) { [weak self] in 144 | guard let self = self else { return } 145 | self._state.lock() 146 | guard let config = self._state.value.awaitingConfiguration else { return self._state.unlock() } 147 | let publisher = config.publisher 148 | self._state.unlock() 149 | publisher.subscribe(self) 150 | } 151 | } 152 | 153 | func cancel() { 154 | guard case .active(let config) = self._state.terminate() else { return } 155 | config.upstream.cancel() 156 | } 157 | } 158 | } 159 | 160 | private extension Publishers.DelayedRetry._Conduit { 161 | /// The necessary variables during the *awaiting* stage. 162 | final class _WaitConfiguration { 163 | /// The publisher to be initialized in case of problems. 164 | let publisher: Upstream 165 | /// The subscriber further down the chain. 166 | let downstream: Downstream 167 | /// The scheduler used to wait for the specific intervals. 168 | let scheduler: S 169 | /// The tolerance used when scheduling a new attempt after a failure. A default implies the minimum tolerance. 170 | let tolerance: S.SchedulerTimeType.Stride? 171 | /// The options for the given scheduler. 172 | let options: S.SchedulerOptions? 173 | /// The amount of seconds to wait after a failure occurrence. 174 | let intervals: [TimeInterval] 175 | /// The interval to use next if a failure is received. 176 | var next: Int 177 | /// The downstream requested demand. 178 | var demand: Subscribers.Demand 179 | 180 | /// Boolean indicating whether the conduit has ever been subscribed to or not. 181 | var isPrime: Bool { 182 | return self.next == 0 183 | } 184 | 185 | /// Designated initializer. 186 | init(publisher: Upstream, downstream: Downstream, scheduler: S, tolerance: S.SchedulerTimeType.Stride?, options: S.SchedulerOptions?, intervals: [TimeInterval], next: Int, demand: Subscribers.Demand) { 187 | precondition(next >= 0) 188 | self.publisher = publisher 189 | self.downstream = downstream 190 | self.scheduler = scheduler 191 | self.tolerance = tolerance 192 | self.options = options 193 | self.intervals = intervals 194 | self.next = next 195 | self.demand = demand 196 | } 197 | 198 | convenience init(config: _ActiveConfiguration) { 199 | self.init(publisher: config.publisher, downstream: config.downstream, 200 | scheduler: config.scheduler, tolerance: config.tolerance, options: config.options, 201 | intervals: config.intervals, next: config.next, demand: config.demand) 202 | } 203 | } 204 | 205 | /// The necessary variables during the *active* stage. 206 | final class _ActiveConfiguration { 207 | /// The publisher to be initialized in case of problems. 208 | let publisher: Upstream 209 | /// The upstream subscription. 210 | let upstream: Subscription 211 | /// The subscriber further down the chain. 212 | let downstream: Downstream 213 | /// The scheduler used to wait for the specific intervals. 214 | let scheduler: S 215 | /// The tolerance used when scheduling a new attempt after a failure. A default implies the minimum tolerance. 216 | let tolerance: S.SchedulerTimeType.Stride? 217 | /// The options for the given scheduler. 218 | let options: S.SchedulerOptions? 219 | /// The amount of seconds to wait after a failure occurrence. 220 | let intervals: [TimeInterval] 221 | /// The interval to use next if a failure is received. 222 | var next: Int 223 | /// The downstream requested demand. 224 | var demand: Subscribers.Demand 225 | 226 | /// Boolean indicating whether it is the first ever attemp (primal attempt). 227 | var isPrime: Bool { 228 | return self.next == 0 229 | } 230 | 231 | /// Produces the next pause and increments the index integer. 232 | var nextPause: TimeInterval? { 233 | guard self.next < self.intervals.endIndex else { return nil } 234 | let pause = self.intervals[self.next] 235 | self.next += 1 236 | return pause 237 | } 238 | 239 | /// Designated initializer. 240 | init(publisher: Upstream, upstream: Subscription, downstream: Downstream, scheduler: S, tolerance: S.SchedulerTimeType.Stride?, options: S.SchedulerOptions?, intervals: [TimeInterval], next: Int, demand: Subscribers.Demand) { 241 | precondition(next >= 0) 242 | self.publisher = publisher 243 | self.upstream = upstream 244 | self.downstream = downstream 245 | self.scheduler = scheduler 246 | self.tolerance = tolerance 247 | self.options = options 248 | self.intervals = intervals 249 | self.next = next 250 | self.demand = demand 251 | } 252 | 253 | convenience init(upstream: Subscription, config: _WaitConfiguration) { 254 | self.init(publisher: config.publisher, upstream: upstream, downstream: config.downstream, 255 | scheduler: config.scheduler, tolerance: config.tolerance, options: config.options, 256 | intervals: config.intervals, next: config.next, demand: config.demand) 257 | } 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /sources/runtime/publishers/Then.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | extension Publishers { 4 | /// Transform the upstream successful completion event into a new or existing publisher. 5 | public struct Then: Publisher where Upstream:Publisher, Child:Publisher, Upstream.Failure==Child.Failure { 6 | public typealias Output = Child.Output 7 | public typealias Failure = Child.Failure 8 | /// Closure generating the publisher to be pipelined after upstream completes. 9 | public typealias Closure = () -> Child 10 | 11 | /// Publisher emitting the events being received here. 12 | public let upstream: Upstream 13 | /// The maximum demand requested to the upstream at the same time. 14 | public let maxDemand: Subscribers.Demand 15 | /// Closure that will crete the publisher that will emit events downstream once a successful completion is received. 16 | public let transform: Closure 17 | 18 | /// Designated initializer providing the upstream publisher and the closure in charge of arranging the transformation. 19 | /// 20 | /// The `maxDemand` must be greater than zero (`precondition`). 21 | /// - parameter upstream: Upstream publisher chain which successful completion will trigger the `transform` closure. 22 | /// - parameter maxDemand: The maximum demand requested to the upstream at the same time. 23 | /// - parameter transfom: Closure providing the new (or existing) publisher. 24 | @inlinable public init(upstream: Upstream, maxDemand: Subscribers.Demand = .unlimited, transform: @escaping ()->Child) { 25 | precondition(maxDemand > .none) 26 | self.upstream = upstream 27 | self.maxDemand = maxDemand 28 | self.transform = transform 29 | } 30 | 31 | public func receive(subscriber: S) where S:Subscriber, S.Input==Output, S.Failure==Failure { 32 | let conduitDownstream = _DownstreamConduit(downstream: subscriber, transform: self.transform) 33 | let conduitUpstream = UpstreamConduit(subscriber: conduitDownstream, maxDemand: self.maxDemand) 34 | self.upstream.subscribe(conduitUpstream) 35 | } 36 | } 37 | } 38 | 39 | // MARK: - 40 | 41 | fileprivate extension Publishers.Then { 42 | /// Helper that acts as a `Subscriber` for the upstream, but just forward events to the given `Conduit` instance. 43 | final class UpstreamConduit: Subscription, Subscriber where Downstream:Subscriber, Downstream.Input==Child.Output, Downstream.Failure==Child.Failure { 44 | typealias Input = Upstream.Output 45 | typealias Failure = Upstream.Failure 46 | 47 | /// Enum listing all possible conduit states. 48 | @ConduitLock private var state: ConduitState<_WaitConfiguration,_ActiveConfiguration> 49 | /// The combine identifier shared with the `DownstreamConduit`. 50 | let combineIdentifier: CombineIdentifier 51 | /// The maximum demand requested to the upstream at the same time. 52 | private let _maxDemand: Subscribers.Demand 53 | 54 | /// Designated initializer for this helper establishing the strong bond between the `Conduit` and the created helper. 55 | init(subscriber: _DownstreamConduit, maxDemand: Subscribers.Demand) { 56 | precondition(maxDemand > .none) 57 | self.state = .awaitingSubscription(_WaitConfiguration(downstream: subscriber)) 58 | self.combineIdentifier = subscriber.combineIdentifier 59 | self._maxDemand = maxDemand 60 | } 61 | 62 | deinit { 63 | self.cancel() 64 | self._state.invalidate() 65 | } 66 | 67 | func receive(subscription: Subscription) { 68 | guard let config = self._state.activate(atomic: { _ActiveConfiguration(upstream: subscription, downstream: $0.downstream, didDownstreamRequestValues: false) }) else { 69 | return subscription.cancel() 70 | } 71 | config.downstream.receive(subscription: self) 72 | } 73 | 74 | func request(_ demand: Subscribers.Demand) { 75 | guard demand > 0 else { return } 76 | 77 | self._state.lock() 78 | guard var config = self._state.value.activeConfiguration, !config.didDownstreamRequestValues else { return self._state.unlock() } 79 | config.didDownstreamRequestValues = true 80 | self._state.value = .active(config) 81 | self._state.unlock() 82 | 83 | config.upstream.request(self._maxDemand) 84 | } 85 | 86 | func receive(_ input: Upstream.Output) -> Subscribers.Demand { 87 | .max(1) 88 | } 89 | 90 | func receive(completion: Subscribers.Completion) { 91 | guard case .active(let config) = self._state.terminate() else { return } 92 | config.downstream.receive(completion: completion) 93 | } 94 | 95 | func cancel() { 96 | guard case .active(let config) = self._state.terminate() else { return } 97 | config.upstream.cancel() 98 | } 99 | } 100 | } 101 | 102 | private extension Publishers.Then.UpstreamConduit { 103 | /// The necessary variables during the *awaiting* stage. 104 | /// 105 | /// The *Conduit* has been initialized, but it is not yet connected to the upstream. 106 | struct _WaitConfiguration { 107 | let downstream: Publishers.Then._DownstreamConduit 108 | } 109 | /// The necessary variables during the *active* stage. 110 | /// 111 | /// The *Conduit* is receiving values from upstream. 112 | struct _ActiveConfiguration { 113 | let upstream: Subscription 114 | let downstream: Publishers.Then._DownstreamConduit 115 | var didDownstreamRequestValues: Bool 116 | } 117 | } 118 | 119 | // MARK: - 120 | 121 | private extension Publishers.Then { 122 | /// Represents an active `Then` publisher taking both the role of `Subscriber` (for upstream publishers) and `Subscription` (for downstream subscribers). 123 | /// 124 | /// This subscriber takes as inputs any value provided from upstream, but ignores them. Only when a successful completion has been received, a `Child` publisher will get generated. 125 | /// The child events will get emitted as-is (i.e. without any modification). 126 | final class _DownstreamConduit: Subscription, Subscriber where Downstream: Subscriber, Downstream.Input==Child.Output, Downstream.Failure==Child.Failure { 127 | typealias Input = Downstream.Input 128 | typealias Failure = Downstream.Failure 129 | 130 | /// Enum listing all possible conduit states. 131 | @ConduitLock private var state: ConduitState<_WaitConfiguration,_ActiveConfiguration> 132 | 133 | /// Designated initializer holding the downstream subscribers. 134 | /// - parameter downstream: The subscriber receiving values downstream. 135 | /// - parameter transform: The closure that will eventually generate another publisher to switch to. 136 | init(downstream: Downstream, transform: @escaping Closure) { 137 | self.state = .awaitingSubscription(_WaitConfiguration(closure: transform, downstream: downstream)) 138 | } 139 | 140 | deinit { 141 | self.cancel() 142 | self._state.invalidate() 143 | } 144 | 145 | func receive(subscription: Subscription) { 146 | // A subscription can be received in the following two cases: 147 | // a) The pipeline has just started and an acknowledgment is being waited from the upstream. 148 | // b) The upstream has completed successfully and a child has been instantiated. The acknowledgement is being waited upon. 149 | self._state.lock() 150 | switch self._state.value { 151 | case .awaitingSubscription(let config): 152 | self._state.value = .active(_ActiveConfiguration(stage: .upstream(subscription: subscription, closure: config.closure, storedRequests: .none), downstream: config.downstream)) 153 | self._state.unlock() 154 | config.downstream.receive(subscription: self) 155 | case .active(var config): 156 | guard case .awaitingChild(let storedRequests) = config.stage else { fatalError() } 157 | config.stage = .child(subscription: subscription) 158 | self._state.value = .active(config) 159 | self._state.unlock() 160 | subscription.request(storedRequests) 161 | case .terminated: 162 | self._state.unlock() 163 | } 164 | } 165 | 166 | func request(_ demand: Subscribers.Demand) { 167 | guard demand > 0 else { return } 168 | 169 | self._state.lock() 170 | guard var config = self._state.value.activeConfiguration else { return self._state.unlock() } 171 | 172 | switch config.stage { 173 | case .upstream(let subscription, let closure, let requests): 174 | config.stage = .upstream(subscription: subscription, closure: closure, storedRequests: requests + demand) 175 | self._state.value = .active(config) 176 | self._state.unlock() 177 | if requests == .none { subscription.request(.max(1)) } 178 | case .awaitingChild(let requests): 179 | config.stage = .awaitingChild(storedRequests: requests + demand) 180 | self._state.value = .active(config) 181 | self._state.unlock() 182 | case .child(let subscription): 183 | self._state.unlock() 184 | return subscription.request(demand) 185 | } 186 | } 187 | 188 | func receive(_ input: Downstream.Input) -> Subscribers.Demand { 189 | self._state.lock() 190 | guard let config = self._state.value.activeConfiguration else { self._state.unlock(); return .unlimited } 191 | guard case .child = config.stage else { fatalError() } 192 | self._state.unlock() 193 | return config.downstream.receive(input) 194 | } 195 | 196 | func receive(completion: Subscribers.Completion) { 197 | self._state.lock() 198 | guard var config = self._state.value.activeConfiguration else { return self._state.unlock() } 199 | 200 | switch config.stage { 201 | case .upstream(_, let closure, let requests): 202 | switch completion { 203 | case .finished: 204 | config.stage = .awaitingChild(storedRequests: requests) 205 | self._state.value = .active(config) 206 | self._state.unlock() 207 | closure().subscribe(self) 208 | case .failure: 209 | self._state.value = .terminated 210 | self._state.unlock() 211 | config.downstream.receive(completion: completion) 212 | } 213 | case .awaitingChild: 214 | fatalError() 215 | case .child: 216 | self._state.value = .terminated 217 | self._state.unlock() 218 | config.downstream.receive(completion: completion) 219 | } 220 | } 221 | 222 | func cancel() { 223 | guard case .active(let config) = self._state.terminate() else { return } 224 | switch config.stage { 225 | case .upstream(let subscription, _, _): subscription.cancel() 226 | case .awaitingChild(_): break 227 | case .child(let subscription): subscription.cancel() 228 | } 229 | } 230 | } 231 | } 232 | 233 | private extension Publishers.Then._DownstreamConduit { 234 | /// The necessary variables during the *awaiting* stage. 235 | /// 236 | /// The `Conduit` has been initialized, but it is not yet connected to the upstream. 237 | struct _WaitConfiguration { 238 | /// Closure generating the publisher which will take over once the publisher has completed (successfully). 239 | let closure: Publishers.Then.Closure 240 | /// The subscriber further down the chain. 241 | let downstream: Downstream 242 | } 243 | /// The necessary variables during the *active* stage. 244 | /// 245 | /// The *Conduit* is receiving values from upstream or child publisher. 246 | struct _ActiveConfiguration { 247 | /// The active stage 248 | var stage: Stage 249 | /// The subscriber further down the chain. 250 | let downstream: Downstream 251 | 252 | /// Once the pipeline is activated, there are two main stages: upsatream connection, and child publishing. 253 | enum Stage { 254 | /// Values are being received from upstream, but the child publisher hasn't been activated (switched to) yet. 255 | case upstream(subscription: Subscription, closure: ()->Child, storedRequests: Subscribers.Demand) 256 | /// Upstream has completed successfully and the child publisher has been instantiated and it is being waited for subscription acknowledgement. 257 | case awaitingChild(storedRequests: Subscribers.Demand) 258 | /// Upstream has completed successfully and the child is sending values. 259 | case child(subscription: Subscription) 260 | } 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Conbini icon 3 |

4 | 5 |

6 | Swift 5.2 7 | macOS 10.15+ - iOS 13+ - tvOS 13+ - watchOS 6+ 8 | MIT License 9 |

10 | 11 | Conbini provides convenience `Publisher`s, operators, and `Subscriber`s to squeeze the most out of Apple's [Combine framework](https://developer.apple.com/documentation/combine). 12 | 13 | # Usage 14 | 15 | To use this library, you need to: 16 | 17 |
    18 |
    Add Conbini to your project through SPM.

    19 | 20 | ```swift 21 | // swift-tools-version:5.2 22 | import PackageDescription 23 | 24 | let package = Package( 25 | /* Your package name, supported platforms, and generated products go here */ 26 | dependencies: [ 27 | .package(url: "https://github.com/dehesa/package-conbini.git", from: "0.6.2") 28 | ], 29 | targets: [ 30 | .target(name: /* Your target name here */, dependencies: ["Conbini"]) 31 | ] 32 | ) 33 | ``` 34 | 35 | If you want to use Conbini's [testing](#testing) extension, you need to define the `CONBINI_FOR_TESTING` flag on your SPM targets or testing targets. Conbini testing extensions require `XCTest`, which is not available in runtime on some platforms (such as watchOS), or you may not want to link to such dynamic library (e.g. when building command-line tools). 36 | 37 | ```swift 38 | targets: [ 39 | .testTarget(name: /* Your target name here */, dependencies: ["Conbini"], swiftSettings: [.define("CONBINI_FOR_TESTING")]) 40 | ] 41 | ``` 42 | 43 |

    44 | 45 |
    Import Conbini in the file that needs it.

    46 | 47 | ```swift 48 | import Conbini 49 | ``` 50 | 51 | The testing conveniences depend on [XCTest](https://developer.apple.com/documentation/xctest), which is not available on regular execution. That is why Conbini is offered in two flavors: 52 | 53 | - `import Conbini` includes all code excepts the testing conveniences. 54 | - `import ConbiniForTesting` includes the testing functionality only. 55 | 56 |

    57 |
58 | 59 | ## Operators 60 | 61 | Publisher Operators: 62 | 63 |
    64 | 65 |
    handleEnd(_:)

    66 | 67 | Executes (only once) the provided closure when the publisher completes (whether successfully or with a failure) or when the publisher gets cancelled. 68 | 69 | It performs the same operation that the standard `handleEvents(receiveSubscription:receiveOutput:receiveCompletion:receiveCancel:receiveRequest:)` would perform if you add similar closures to `receiveCompletion` and `receiveCancel`. 70 | 71 | ```swift 72 | let publisher = upstream.handleEnd { (completion) in 73 | switch completion { 74 | case .none: // The publisher got cancelled. 75 | case .finished: // The publisher finished successfully. 76 | case .failure(let error): // The publisher generated an error. 77 | } 78 | } 79 | ``` 80 | 81 |

    82 | 83 |
    retry(on:intervals:)

    84 | 85 | Attempts to recreate a failed subscription with the upstream publisher a given amount of times waiting the specified number of seconds between failed attempts. 86 | 87 | ```swift 88 | let apiCallPublisher.retry(on: queue, intervals: [0.5, 2, 5]) 89 | // Same functionality to retry(3), but waiting between attemps 0.5, 2, and 5 seconds after each failed attempt. 90 | ``` 91 | 92 | This operator accept any scheduler conforming to `Scheduler` (e.g. `DispatchQueue`, `RunLoop`, etc). You can also optionally tweak the tolerance and scheduler operations. 93 | 94 |

    95 | 96 |
    then(maxDemand:_:)

    97 | 98 | Ignores all values and executes the provided publisher once a successful completion is received. If a failed completion is emitted, it is forwarded downstream. 99 | 100 | ```swift 101 | let publisher = setConfigurationOnServer.then { 102 | subscribeToWebsocket.publisher 103 | } 104 | ``` 105 | 106 | This operator optionally lets you control backpressure with its `maxDemand` parameter. The parameter behaves like `flatMap`'s `maxPublishers`, which specifies the maximum demand requested to the upstream at any given time. 107 | 108 |

    109 |
110 | 111 | Subscriber Operators: 112 | 113 |
    114 |
    assign(to:on:) variants.

    115 | 116 | Combine's `assign(to:on:)` operation creates memory cycles when the "on" object also holds the publisher's cancellable. A common situation happens when assigning a value to `self`. 117 | 118 | ```swift 119 | class CustomObject { 120 | var value: Int = 0 121 | var cancellable: AnyCancellable? = nil 122 | 123 | func performOperation() { 124 | cancellable = numberPublisher.assign(to: \.value, on: self) 125 | } 126 | } 127 | ``` 128 | 129 | Conbini's `assign(to:onWeak:)` operator points to the given object weakly with the added benefit of cancelling the pipeline when the object is deinitialized. 130 | 131 | Conbini also introduces the `assign(to:onUnowned:)` operator which also avoids memory cycles, but uses `unowned` instead. 132 | 133 |

    134 | 135 |
    await

    136 | 137 | Wait synchronously for the response of the receiving publisher. 138 | 139 | ```swift 140 | let publisher = Just("Hello") 141 | .delay(for: 2, scheduler: DispatchQueue.global()) 142 | 143 | let greeting = publisher.await 144 | ``` 145 | 146 | The synchronous wait is performed through `DispatchGroup`s. Please, consider where are you using `await`, since the executing queue stops and waits for an answer: 147 | - Never call this property from `DispatchQueue.main` or any other queue who is performing any background tasks. 148 | - Awaiting publishers should never process events in the same queue as the executing queue (or the queue will become stalled). 149 | 150 |

    151 | 152 |
    invoke(_:on:) variants.

    153 | 154 | This operator calls the specified function on the given value/reference passing the upstream value. 155 | 156 | ```swift 157 | struct Custom { 158 | func performOperation(_ value: Int) { /* do something */ } 159 | } 160 | 161 | let instance = Custom() 162 | let cancellable = [1, 2, 3].publisher.invoke(Custom.performOperation, on: instance) 163 | ``` 164 | 165 | Conbini also offers the variants `invoke(_:onWeak:)` and `invoke(_:onUnowned:)`, which avoid memory cycles on reference types. 166 | 167 |

    168 | 169 |
    result(onEmpty:_:)

    170 | 171 | It subscribes to the receiving publisher and executes the provided closure when a value is received. In case of failure, the handler is executed with such failure. 172 | 173 | ```swift 174 | let cancellable = serverRequest.result { (result) in 175 | switch result { 176 | case .success(let value): ... 177 | case .failure(let error): ... 178 | } 179 | } 180 | ``` 181 | 182 | The operator lets you optionally generate an error (which will be consumed by your `handler`) for cases where upstream completes without a value. 183 | 184 |

    185 | 186 |
    sink(fixedDemand:)

    187 | 188 | It subscribes upstream and request exactly `fixedDemand` values (after which the subscriber completes). The subscriber may receive zero to `fixedDemand` of values before completing, but never more than that. 189 | 190 | ```swift 191 | let cancellable = upstream.sink(fixedDemand: 5, receiveCompletion: { (completion) in ... }) { (value) in ... } 192 | ``` 193 | 194 |

    195 | 196 |
    sink(maxDemand:)

    197 | 198 | It subscribes upstream requesting `maxDemand` values and always keeping the same backpressure. 199 | 200 | ```swift 201 | let cancellable = upstream.sink(maxDemand: 3) { (value) in ... } 202 | ``` 203 | 204 |

    205 |
206 | 207 | ## Publishers 208 | 209 |
    210 |
    Deferred variants.

    211 | 212 | These publishers accept a closure that is executed once a _greater-than-zero_ demand is requested. There are several flavors: 213 | 214 |

      215 |
      DeferredValue emits a single value and then completes.

      216 | 217 | The value is not provided/cached, but instead a closure will generate it. The closure is executed once a positive subscription is received. 218 | 219 | ```swift 220 | let publisher = DeferredValue { 221 | return intenseProcessing() 222 | } 223 | ``` 224 | 225 | A `Try` variant is also offered, enabling you to `throw` from within the closure. It loses the concrete error type (i.e. it gets converted to `Swift.Error`). 226 | 227 |

      228 | 229 |
      DeferredResult forwards downstream a value or a failure depending on the generated Result.

      230 | 231 | ```swift 232 | let publisher = DeferredResult { 233 | guard someExpression else { return .failure(CustomError()) } 234 | return .success(someValue) 235 | } 236 | ``` 237 | 238 |

      239 | 240 |
      DeferredComplete forwards a completion event (whether success or failure).

      241 | 242 | ```swift 243 | let publisher = DeferredComplete { 244 | return errorOrNil 245 | } 246 | ``` 247 | 248 | A `Try` variant is also offered, enabling you to `throw` from within the closure; but it loses the concrete error type (i.e. gets converted to `Swift.Error`). 249 | 250 |

      251 | 252 |
      DeferredPassthrough provides a passthrough subject in a closure to be used to send values downstream.

      253 | 254 | It is similar to wrapping a `Passthrough` subject on a `Deferred` closure, with the diferrence that the `Passthrough` given on the closure is already _wired_ on the publisher chain and can start sending values right away. Also, the memory management is taken care of and every new subscriber receives a new subject (closure re-execution). 255 | 256 | ```swift 257 | let publisher = DeferredPassthrough { (subject) in 258 | subject.send(something) 259 | subject.send(somethingElse) 260 | subject.send(completion: .finished) 261 | } 262 | ``` 263 | 264 |

      265 |
    266 | 267 | There are several reason for these publishers to exist instead of using other `Combine`-provided closure such as `Just`, `Future`, or `Deferred`: 268 | 269 | - Combine's `Just` forwards a value immediately and each new subscriber always receive the same value. 270 | - Combine's `Future` executes its closure right away (upon initialization) and then cache the returned value. That value is then forwarded for any future subscription. 271 |
    `Deferred...` publishers await for subscriptions and a _greater-than-zero_ demand before executing. This also means, the closure will re-execute for any new subscriber. 272 | - Combine's `Deferred` has similar functionality to Conbini's, but it only accepts a publisher. 273 |
    This becomes annoying when compounding operators. 274 | 275 |

    276 | 277 |
    DelayedRetry

    278 | 279 | It provides the functionality of the `retry(on:intervals:)` operator. 280 | 281 |

    282 | 283 |
    Then

    284 | 285 | It provides the functionality of the `then` operator. 286 | 287 |

    288 | 289 |
    HandleEnd

    290 | 291 | It provides the functionality of the `handleEnd(_:)` operator. 292 | 293 |

    294 |
295 | 296 | Extra Functionality: 297 | 298 |
    299 |
    Publishers.PrefetchStrategy

    300 | 301 | It has been extended with a `.fatalError(message:file:line:)` option to stop execution if the buffer is filled. This is useful during development and debugging and for cases when you are sure the buffer will never be filled. 302 | 303 | ```swift 304 | publisher.buffer(size: 10, prefetch: .keepFull, whenFull: .fatalError()) 305 | ``` 306 | 307 |

    308 |
309 | 310 | ## Subscribers 311 | 312 |
    313 |
    FixedSink

    314 | 315 | It requests a fixed amount of values upon subscription and once if has received them all it completes/cancel the pipeline. 316 | The values are requested through backpressure, so no more than the allowed amount of values are generated upstream. 317 | 318 | ```swift 319 | let subscriber = FixedSink(demand: 5) { (value) in ... } 320 | upstream.subscribe(subscriber) 321 | ``` 322 | 323 |

    324 | 325 |
    GraduatedSink

    326 | 327 | It requests a fixed amount of values upon subscription and always keep the same demand by asking one more value upon input reception. The standard `Subscribers.Sink` requests an `.unlimited` amount of values upon subscription. This might not be what we want since some times a control of in-flight values might be desirable (e.g. allowing only _n_ in-flight\* API calls at the same time). 328 | 329 | ```swift 330 | let subscriber = GraduatedSink(maxDemand: 3) { (value) in ... } 331 | upstream.subscribe(subscriber) 332 | ``` 333 | 334 |

    335 |
336 | 337 | > The names for these subscribers are not very good/accurate. Any suggestion is appreciated. 338 | 339 | ## Testing 340 | 341 | Conbini provides convenience subscribers to ease code testing. These subscribers make the test wait till a specific expectation is fulfilled (or making the test fail in a negative case). Furthermore, if a timeout ellapses or a expectation is not fulfilled, the affected test line will be marked _in red_ correctly in Xcode. 342 | 343 |
    344 | 345 |
    expectsAll(timeout:on:)

    346 | 347 | It subscribes to a publisher making the running test wait for zero or more values and a successful completion. 348 | 349 | ```swift 350 | let emittedValues = publisherChain.expectsAll(timeout: 0.8, on: test) 351 | ``` 352 | 353 |

    354 | 355 |
    expectsAtLeast(timeout:on:)

    356 | 357 | It subscribes to a publisher making the running test wait for at least the provided amount of values. Once the provided amount of values is received, the publisher gets cancelled and the values are returned. 358 | 359 | ```swift 360 | let emittedValues = publisherChain.expectsAtLeast(values: 5, timeout: 0.8, on: test) 361 | ``` 362 | 363 | This operator/subscriber accepts an optional closure to check every value received. 364 | 365 | ```swift 366 | let emittedValues = publisherChain.expectsAtLeast(values: 5, timeout: 0.8, on: test) { (value) in 367 | XCTAssert... 368 | } 369 | ``` 370 | 371 |

    372 | 373 |
    expectsCompletion(timeout:on:)

    374 | 375 | It subscribes to a publisher making the running test wait for a successful completion while ignoring all emitted values. 376 | 377 | ```swift 378 | publisherChain.expectsCompletion(timeout: 0.8, on: test) 379 | ``` 380 | 381 |

    382 | 383 |
    expectsFailure(timeout:on:)

    384 | 385 | It subscribes to a publisher making the running test wait for a failed completion while ignoring all emitted values. 386 | 387 | ```swift 388 | publisherChain.expectsFailure(timeout: 0.8, on: test) 389 | ``` 390 | 391 |

    392 | 393 |
    expectsOne(timeout:on:)

    394 | 395 | It subscribes to a publisher making the running test wait for a single value and a successful completion. If more than one value are emitted or the publisher fails, the subscription gets cancelled and the test fails. 396 | 397 | ```swift 398 | let emittedValue = publisherChain.expectsOne(timeout: 0.8, on: test) 399 | ``` 400 | 401 |

    402 |
403 | 404 | `XCTestCase` has been _extended_ to support the following functionality. 405 | 406 |
    407 |
    wait(seconds:)

    408 | 409 | Locks the receiving test for `interval` amount of seconds. 410 | 411 | ```swift 412 | final class CustomTests: XCTestCase { 413 | func testSomething() { 414 | let subject = PassthroughSubject() 415 | let cancellable = subject.sink { print($0) } 416 | 417 | let queue = DispatchQueue.main 418 | queue.asyncAfter(.now() + 1) { subject.send(1) } 419 | queue.asyncAfter(.now() + 2) { subject.send(2) } 420 | 421 | self.wait(seconds: 3) 422 | cancellable.cancel() 423 | } 424 | } 425 | ``` 426 | 427 |

    428 |
429 | 430 | # References 431 | 432 | - Apple's [Combine documentation](https://developer.apple.com/documentation/combine). 433 | - [The Combine book](https://store.raywenderlich.com/products/combine-asynchronous-programming-with-swift) is an excellent Ray Wenderlich book about the Combine framework. 434 | - [Cocoa with love](https://www.cocoawithlove.com) has a great series of articles about the inner workings of Combine: [1](https://www.cocoawithlove.com/blog/twenty-two-short-tests-of-combine-part-1.html), [2](https://www.cocoawithlove.com/blog/twenty-two-short-tests-of-combine-part-2.html), [3](https://www.cocoawithlove.com/blog/twenty-two-short-tests-of-combine-part-3.html). 435 | - [OpenCombine](https://github.com/broadwaylamb/OpenCombine) is an open source implementation of Apple's Combine framework. 436 | - [CombineX](https://github.com/cx-org/CombineX) is an open source implementation of Apple's Combine framework. 437 | --------------------------------------------------------------------------------