├── .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 | MIT License
--------------------------------------------------------------------------------
/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.2 Swift
--------------------------------------------------------------------------------
/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 | + 6 watchOS + 13 tvOS + 13 iOS + 10.15 macOS
--------------------------------------------------------------------------------
/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 |
3 |
4 |
5 |
6 |
7 |
8 |
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 |
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 |
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 |
--------------------------------------------------------------------------------