├── .github └── workflows │ ├── ci.awk │ ├── ci.yml │ └── docs.yml ├── .gitignore ├── .swiftpm └── xcode │ ├── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDETemplateMacros.plist │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ └── xcschemes │ ├── Entwine-Package.xcscheme │ ├── Entwine.xcscheme │ └── EntwineTest.xcscheme ├── Assets ├── Entwine │ └── README.md └── EntwineTest │ └── README.md ├── LICENSE ├── Makefile ├── Package.swift ├── README.md ├── Sources ├── Common │ ├── DataStructures │ │ ├── LinkedListQueue.swift │ │ ├── LinkedListStack.swift │ │ └── PriorityQueue.swift │ └── Utilities │ │ └── SinkQueue.swift ├── Entwine │ ├── Common │ │ ├── DataStructures │ │ │ ├── LinkedListQueue.swift │ │ │ ├── LinkedListStack.swift │ │ │ └── PriorityQueue.swift │ │ └── Utilities │ │ │ └── SinkQueue.swift │ ├── Deprecated │ │ ├── CancellableBag.swift │ │ └── Deprecations.swift │ ├── Operators │ │ ├── Dematerialize.swift │ │ ├── Materialize.swift │ │ ├── ReferenceCounted.swift │ │ ├── ReplaySubject.swift │ │ ├── ShareReplay.swift │ │ ├── Signpost.swift │ │ └── WithLatestFrom.swift │ ├── Publishers │ │ └── Factory.swift │ ├── Schedulers │ │ └── TrampolineScheduler.swift │ ├── Signal.swift │ └── Utilities │ │ └── DeallocToken.swift └── EntwineTest │ ├── Common │ ├── DataStructures │ │ ├── LinkedListQueue.swift │ │ ├── LinkedListStack.swift │ │ └── PriorityQueue.swift │ └── Utilities │ │ └── SinkQueue.swift │ ├── Deprecations.swift │ ├── Signal+CustomDebugStringConvertible.swift │ ├── TestEvent.swift │ ├── TestScheduler │ ├── TestScheduler.swift │ ├── VirtualTime.swift │ └── VirtualTimeInterval.swift │ ├── TestSequence.swift │ ├── TestablePublisher │ └── TestablePublisher.swift │ └── TestableSubscriber │ ├── DemandLedger.swift │ └── TestableSubscriber.swift └── Tests ├── EntwineTestTests ├── TestSchedulerTests.swift ├── TestablePublisherTests.swift ├── TestableSubscriberTests.swift └── XCTestManifests.swift └── EntwineTests ├── DematerializeTests.swift ├── FactoryTests.swift ├── MaterializeTests.swift ├── ReferenceCountedTests.swift ├── ReplaySubjectTests.swift ├── ShareReplayTests.swift ├── TrampolineSchedulerTests.swift └── WithLatestFromTests.swift /.github/workflows/ci.awk: -------------------------------------------------------------------------------- 1 | #!/usr/bin/awk -f 2 | BEGIN { FS=":"; errCode=0; mask=fails_on_warning==1?"(error|warning)":"error"; } 3 | { 4 | print; 5 | if(match($0,"^.+:[0-9]+:[0-9]+:[ ]*"mask":[ ]*")) { 6 | message=substr($0,RSTART+RLENGTH); file=$1; gsub("^"prefix"/","",file); 7 | printf("::error file=%s,line=%s,col=%s::%s\n", file, $2, $3, message); 8 | errCode=1; 9 | } else if(match($0,"^.+:[0-9]+:[ ]*"mask":[ ]*")) { 10 | message=substr($0,RSTART+RLENGTH); file=$1; gsub("^"prefix"/","",file); 11 | printf("::error file=%s,line=%s::%s\n", file, $2, message); 12 | errCode=1; 13 | } else if(match($0,/^.+:[0-9]+:[ ]*\*\*\*[ ]*/)) { 14 | message=substr($0,RSTART+RLENGTH); file=$1; gsub("^"prefix"/","",file); 15 | printf("::error file=%s,line=%s::%s\n", file, $2, message); 16 | errCode=1; 17 | } else if(match($0,/^make:[ ]*\*\*\*[ ]*/)) { 18 | message=substr($0,RSTART+RLENGTH); file=$1; gsub("^"prefix"/","",file); 19 | printf("::error ::%s\n", message); 20 | errCode=1; 21 | } 22 | } 23 | END { exit errCode; } 24 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ '*' ] 8 | 9 | jobs: 10 | test: 11 | name: Test 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | 18 | - name: Build 19 | run: | 20 | make build -C ${{ github.workspace }} 2>&1 \ 21 | | ${{ github.workspace }}/.github/workflows/ci.awk \ 22 | -v prefix=${{ github.workspace }} -v fails_on_warning=1 23 | 24 | - name: Run Tests 25 | run: | 26 | make test -C ${{ github.workspace }} 2>&1 \ 27 | | ${{ github.workspace }}/.github/workflows/ci.awk \ 28 | -v prefix=${{ github.workspace }} -v fails_on_warning=1 29 | 30 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | publish-docs: 9 | name: Publish Docs 10 | runs-on: macos-latest 11 | 12 | steps: 13 | - name: Set up Environment 14 | run: | 15 | git config --global user.email "action@github.com" 16 | git config --global user.name "GitHub Action" 17 | gem install bundler 18 | gem install jazzy --no-document 19 | 20 | - name: Checkout 21 | uses: actions/checkout@v2 22 | with: 23 | ref: 'gh-pages' 24 | 25 | - name: Build Docs 26 | run: make update-docs -C ${{ github.workspace }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | # 51 | # Add this line if you want to avoid checking in source code from the Xcode workspace 52 | # *.xcworkspace 53 | 54 | # Carthage 55 | # 56 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 57 | # Carthage/Checkouts 58 | 59 | Carthage/Build 60 | 61 | # Accio dependency management 62 | Dependencies/ 63 | .accio/ 64 | 65 | # fastlane 66 | # 67 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 68 | # screenshots whenever they are needed. 69 | # For more information about the recommended setup visit: 70 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 71 | 72 | fastlane/report.xml 73 | fastlane/Preview.html 74 | fastlane/screenshots/**/*.png 75 | fastlane/test_output 76 | 77 | # Code Injection 78 | # 79 | # After new code Injection tools there's a generated folder /iOSInjectionProject 80 | # https://github.com/johnno1962/injectionforxcode 81 | 82 | iOSInjectionProject/ -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDETemplateMacros.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FILEHEADER 6 | 7 | // Entwine 8 | // https://github.com/tcldr/Entwine 9 | // 10 | // Copyright © 2020 Tristan Celder. All rights reserved. 11 | // 12 | // Permission is hereby granted, free of charge, to any person obtaining a copy 13 | // of this software and associated documentation files (the "Software"), to 14 | // deal in the Software without restriction, including without limitation the 15 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 16 | // sell copies of the Software, and to permit persons to whom the Software is 17 | // furnished to do so, subject to the following conditions: 18 | // 19 | // The above copyright notice and this permission notice shall be included in 20 | // all copies or substantial portions of the Software. 21 | // 22 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 27 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 28 | // IN THE SOFTWARE. 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/Entwine-Package.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 57 | 63 | 64 | 65 | 66 | 67 | 72 | 73 | 75 | 81 | 82 | 83 | 84 | 85 | 95 | 96 | 102 | 103 | 109 | 110 | 111 | 112 | 114 | 115 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/Entwine.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 67 | 68 | 74 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/EntwineTest.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 67 | 68 | 74 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Assets/Entwine/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Entwine Utilities 3 | 4 | Part of [Entwine](https://github.com/tcldr/Entwine) – A collection of accessories for [Apple's Combine Framework](https://developer.apple.com/documentation/combine). 5 | 6 | --- 7 | 8 | ## Contents 9 | - [About](#about) 10 | - [Getting Started](#getting-started) 11 | - [Installation](#installation) 12 | - [Documentation](#documentation) 13 | - [Copyright and License](#copyright-and-license) 14 | 15 | --- 16 | 17 | ## About 18 | 19 | _Entwine Utilities_ are a collection of operators, tools and extensions to make working with _Combine_ even more productive. 20 | 21 | - The `ReplaySubject` makes it simple for subscribers to receive the most recent values immediately upon subscription. 22 | - The `withLatest(from:)` operator enables state to be taken alongside UI events. 23 | - `Publishers.Factory` makes creating publishers fast and simple – they can even be created inline! 24 | 25 | be sure to checkout the [documentation](http://tcldr.github.io/Entwine/EntwineDocs) for the full list of operators and utilities. 26 | 27 | --- 28 | 29 | ## Getting started 30 | 31 | Ensure to `import Entwine` in each file you wish to the utilities with. 32 | 33 | The operators can then be used as part of your usual publisher chain declaration: 34 | 35 | ```swift 36 | 37 | import Combine 38 | import Entwine 39 | 40 | class MyClass { 41 | 42 | let myEntwineCancellationBag = CancellationBag() 43 | 44 | ... 45 | 46 | func printLatestColorOnClicks() { 47 | let clicks: AnyPublisher = SomeClickPublisher.shared 48 | let color: AnyPublisher = SomeColorSource.shared 49 | 50 | clicks.withLatest(from: color) 51 | .sink { 52 | print("clicked when the latest color was: \($0)") 53 | } 54 | .cancelled(by: myEntwineCancellationBag) 55 | } 56 | } 57 | 58 | ``` 59 | 60 | Each operator, subject and utility is documented with examples. check out the [full documentation.](https://tcldr.github.io/Enwtine/EntwineDocs) 61 | 62 | --- 63 | 64 | ## Installation 65 | ### As part of another Swift Package: 66 | 1. Include it in your `Package.swift` file as both a dependency and a dependency of your target. 67 | 68 | ```swift 69 | import PackageDescription 70 | 71 | let package = Package( 72 | ... 73 | dependencies: [ 74 | .package(url: "http://github.com/tcldr/Entwine.git", .upToNextMajor(from: "0.0.0")), 75 | ], 76 | ... 77 | targets: [ 78 | .target(name: "MyTarget", dependencies: ["Entwine"]), 79 | ] 80 | ) 81 | ``` 82 | 83 | 2. Then run `swift package update` from the root directory of your SPM project. If you're using Xcode 11 to edit your SPM project this should happen automatically. 84 | 85 | ### As part of an Xcode 11 or greater project: 86 | 1. Select the `File -> Swift Packages -> Add package dependency...` menu item. 87 | 2. Enter the repository url `https://github.com/tcldr/Entwine` and tap next. 88 | 3. Select 'version, 'up to next major', enter `0.0.0`, hit next. 89 | 4. Select the _Entwine_ library and specify the target you wish to use it with. 90 | 91 | *n.b. _Entwine_ is pre-release software and as such the API may change prior to reaching 1.0. For finer-grained control please use `.upToNextMinor(from:)` in your SPM dependency declaration* 92 | 93 | --- 94 | 95 | ## Documentation 96 | Full documentation for _Entwine_ can be found at [http://tcldr.github.io/Entwine/EntwineDocs](http://tcldr.github.io/Entwine/EntwineDocs). 97 | 98 | --- 99 | 100 | ## Copyright and license 101 | Copyright 2019 © Tristan Celder 102 | 103 | _Entwine_ is made available under the [MIT License](http://github.com/tcldr/Entwine/blob/master/LICENSE) 104 | 105 | --- 106 | -------------------------------------------------------------------------------- /Assets/EntwineTest/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Entwine Test 3 | 4 | Part of [Entwine](https://github.com/tcldr/Entwine) – A collection of accessories for [Apple's Combine Framework](https://developer.apple.com/documentation/combine). 5 | 6 | --- 7 | 8 | ## Contents 9 | - [About _EntwineTest_](#about-entwinetest) 10 | - [Getting Started](#getting-started) 11 | - [TestScheduler](#testscheduler) 12 | - [TestablePublisher](#testablepublisher) 13 | - [TestableSubscriber](#testablesubscriber) 14 | - [Putting it all together](#putting-it-all-together) 15 | - [Installation](#installation) 16 | - [Documentation](#documentation) 17 | - [Acknowledgements](#acknowledgements) 18 | - [Copyright and License](#copyright-and-license) 19 | 20 | --- 21 | 22 | ## About _EntwineTest_ 23 | 24 | _EntwineTest_ packages a concise set of tools that are designed to work together to help you test your _Combine_ sequences and operators. 25 | 26 | In addition, _EntwineTest_ includes tools to help you to determine whether your publishers are complying with subscriber demand requests (backpressure) so that you can ensure your publisher is behaving like a good _Combine_ citizen before releasing it out in the wild. 27 | 28 | --- 29 | 30 | ## Getting Started 31 | 32 | The _EntwineTest_ packages consists of three major components – together, they help you write better tests for your _Combine_ sequences. 33 | 34 | Let's go through them one by one before finally seeing how they all fit together: 35 | 36 | ### `TestScheduler`: 37 | 38 | At the heart of _Combine_ is the concept of schedulers. Without them, no work gets done. Essentially they are responsible for both _where_ an action is excuted (main thread, a dipatch queue, or the current thread), and _when_ it is is executed (right now, after the current task has finished, five minutes from now). 39 | 40 | Our `TestScheduler` is a special kind of scheduler that uses 'virtual time' to schedule its tasks on the current thread. Our `VirtualTime` is really just an `Int` and its only purpose is to prioritise the order in which tasks are done. However, for testing purposes, we pretend it is _actual time_, as it helps us to articulate the seqeunce in which we'd like our tests to run. 41 | 42 | The best thing about virtual time? It's instantaneous! So we keep our test suites lean and fast. 43 | 44 | Here's how you might use the `TestScheduler` in isolation: 45 | 46 | ```swift 47 | import EntwineTest 48 | 49 | let scheduler = TestScheduler() 50 | 51 | scheduler.schedule(after: 300) { print("bosh") } 52 | scheduler.schedule(after: 200) { print("bash") } 53 | scheduler.schedule(after: 100) { print("bish") } 54 | 55 | scheduler.resume() // the clock is paused until this is called 56 | 57 | // outputs: 58 | // "bish" 59 | // "bash" 60 | // "bosh" 61 | 62 | ``` 63 | Notice that as we've scheduled "bosh" to print at `300`, and "bish" to print at `100`, when we start the scheduler by calling `.resume()`, "bish" is printed first. 64 | ### `TestablePublisher`: 65 | 66 | Now that we have our scheduler, we can think about how we're going to simulate some _Combine_ sequences. If we want to simulate a sequence, we'll need a publisher that lets us define _what_ each element a sequence should be, and _when_ that element should be emitted. 67 | 68 | A `TestablePublisher` is exactly that. 69 | 70 | You can generate a `TestablePublisher` from two factory methods on the `TestScheduler`. (We do it this way instead of instantiating directly as they depend on the scheduler.) 71 | 72 | One, `createAbsoluteTestablePublisher(_:)`, schedules events at exactly the time specified – if the time of an event has passed at the point the publisher is subscribed to, that event won't be fired. 73 | 74 | The other, `createRelativeTestablePublisher(_:)`, schedules events at the time specified _plus_ the time the publisher was subscribed to. So an event scheduled at `100` with a subscription at `200` means the event will fire at `300`. 75 | 76 | ```swift 77 | import Combine 78 | import EntwineTest 79 | 80 | // we'll set the schedulers clock a little forward – at 200 81 | 82 | let scheduler = TestScheduler(initialClock: 200) 83 | 84 | let relativeTimePublisher: TestablePublisher = scheduler.createRelativeTestablePublisher([ 85 | (020, .input("Mi")), 86 | (030, .input("Fa")), 87 | ]) 88 | 89 | let absoluteTimePublisher: TestablePublisher = scheduler.createAbsoluteTestablePublisher([ 90 | (200, .input("Do")), 91 | (210, .input("Re")), 92 | ]) 93 | 94 | let relativeSubscription = relativeTimePublisher.sink { element in 95 | print("time: \(scheduler.now) - \(element)") 96 | } 97 | 98 | let absoluteSubscription = absoluteTimePublisher.sink { element in 99 | print("time: \(scheduler.now) - \(element)") 100 | } 101 | 102 | scheduler.resume() 103 | 104 | // Outputs: 105 | // time: 200 - Do 106 | // time: 210 - Re 107 | // time: 220 - Mi 108 | // time: 230 - Fa 109 | ``` 110 | Notice how the events scheduled by the relative publisher fired _after_ the events scheduled by the absolute publisher. As we had set the time of our scheduler to `200` in its initializer, when we subscribed to our relative publisher with the `sink(_:)` method, our publisher took the current time and added that value to each scheduled event. 111 | 112 | ### `TestableSubscriber`: 113 | 114 | The final piece in the jigsaw is the `TestableSubscriber`. Its role is to gather the output of a publisher so that it can be compared against some expected output. It also depends on the `TestScheduler`, so to get one we call `createTestableSubscriber(_:_:)` on our scheduler. 115 | 116 | Once we subscribe to a publisher, `TestableSubscriber` records all the events with their time of arrival and makes them available on its `.recordedOutput` property ready for us to compare against some expected output. It also records the time the subscription began, as well as its completion (should it end). 117 | 118 | ```swift 119 | import Combine 120 | import EntwineTest 121 | 122 | let scheduler = TestScheduler() 123 | let passthroughSubject = PassthroughSubject() 124 | 125 | scheduler.schedule(after: 100) { passthroughSubject.send("yippee") } 126 | scheduler.schedule(after: 200) { passthroughSubject.send("ki") } 127 | scheduler.schedule(after: 300) { passthroughSubject.send("yay") } 128 | 129 | let subscriber = scheduler.createTestableSubscriber(String.self, Never.self) 130 | 131 | passthroughSubject.subscribe(subscriber) 132 | 133 | scheduler.resume() 134 | 135 | let expected: TestSequence = [ 136 | (000, .subscription), 137 | (100, .input("yippee")), 138 | (200, .input("ki")), 139 | (300, .input("yay")), 140 | ] 141 | 142 | print("sequences match: \(expected == subscriber.recordedOutput)") 143 | 144 | // outputs: 145 | // sequences match: true 146 | ``` 147 | 148 | ### Putting it all together 149 | Now that we have our `TestScheduler`, `TestPublisher`, and `TestSubscriber` let's put them together to test our operators and sequences. 150 | 151 | But first, there's one additional method that you should be aware of. That's the `start(create:)` method on `TestScheduler`. 152 | 153 | The `start(create:)` method accepts a closure that produces any publisher and then: 154 | 1. Schedules the creation of the publisher (invocation of the passed closure) at `100` 155 | 2. Schedules the subscription of the publisher to a `TestableSubscriber` at `200` 156 | 3. Schedules the cancellation of the subscription at `900` 157 | 4. Resumes the scheduler clock 158 | 159 | _These are all configurable by using the `start(configuration:create:)` method. See the docs for more info._ 160 | 161 | With that knowledge in place, let's test _Combine_'s map operator. (I'm sure it's fine – but just in case.) 162 | 163 | ```swift 164 | 165 | import XCTest 166 | import EntwineTest 167 | 168 | func testMap() { 169 | 170 | let testScheduler = TestScheduler(initialClock: 0) 171 | 172 | // creates a publisher that will schedule it's elements relatively, at the point of subscription 173 | let testablePublisher: TestablePublisher = testScheduler.createRelativeTestablePublisher([ 174 | (100, .input("a")), 175 | (200, .input("b")), 176 | (300, .input("c")), 177 | ]) 178 | 179 | // a publisher that maps strings to uppercase 180 | let subjectUnderTest = testablePublisher.map { $0.uppercased() } 181 | 182 | // uses the method described above (schedules a subscription at 200, to be cancelled at 900) 183 | let results = testScheduler.start { subjectUnderTest } 184 | 185 | XCTAssertEqual(results.recordedOutput, [ 186 | (200, .subscription), // subscribed at 200 187 | (300, .input("A")), // received uppercased input @ 100 + subscription time 188 | (400, .input("B")), // received uppercased input @ 200 + subscription time 189 | (500, .input("C")), // received uppercased input @ 300 + subscription time 190 | ]) 191 | } 192 | ``` 193 | Hopefully this should be everything you need to get you started with testing your _Combine_ sequences. Don't forget that further information can be found [in the docs](http://tcldr.github.io/Entwine/EntwineTestDocs). 194 | 195 | --- 196 | 197 | ## Installation 198 | ### As part of another Swift Package: 199 | 1. Include it in your `Package.swift` file as both a dependency and a dependency of your target. 200 | 201 | ```swift 202 | import PackageDescription 203 | 204 | let package = Package( 205 | ... 206 | dependencies: [ 207 | .package(url: "http://github.com/tcldr/Entwine.git", .upToNextMajor(from: "0.0.0")), 208 | ], 209 | ... 210 | targets: [ 211 | .testTarget(name: "MyTestTarget", dependencies: [.product(name: "EntwineTest", package: "Entwine")]), 212 | ] 213 | ) 214 | ``` 215 | 216 | 2. Then run `swift package update` from the root directory of your SPM project. If you're using Xcode 11 to edit your SPM project this should happen automatically. 217 | 218 | ### As part of an Xcode 11 or greater project: 219 | 1. Select the `File -> Swift Packages -> Add package dependency...` menu item. 220 | 2. Enter the repository url `https://github.com/tcldr/Entwine` and tap next. 221 | 3. Select 'version, 'up to next major', enter `0.0.0`, hit next. 222 | 4. Select the _EntwineTest_ library and specify the target you wish to use it with. 223 | 224 | *n.b. _EntwineTest_ is pre-release software and as such the API may change prior to reaching 1.0. For finer-grained control please use `.upToNextMinor(from:)` in your SPM dependency declaration* 225 | 226 | --- 227 | 228 | ## Documentation 229 | Full documentation for _EntwineTest_ can be found at [http://tcldr.github.io/Entwine/EntwineTestDocs](http://tcldr.github.io/Entwine/EntwineTestDocs). 230 | 231 | --- 232 | 233 | ## Acknowledgments 234 | _EntwineTest_ is inspired by the great work done in the _RxTest_ library by the contributors to [_RxSwift_](https://github.com/ReactiveX/RxSwift). 235 | 236 | --- 237 | 238 | ## Copyright and license 239 | Copyright 2019 © Tristan Celder 240 | 241 | _EntwineTest_ is made available under the [MIT License](http://github.com/tcldr/Entwine/blob/master/LICENSE) 242 | 243 | --- 244 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2019 Tristan Celder. All rights reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | swift build -c release 3 | 4 | test: 5 | swift test \ 6 | --enable-test-discovery \ 7 | --parallel 8 | 9 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Entwine", 8 | platforms: [ 9 | .macOS(.v10_12), .iOS(.v10), .tvOS(.v10), .watchOS(.v3) 10 | ], 11 | products: [ 12 | .library( 13 | name: "Entwine", 14 | targets: ["Entwine"]), 15 | .library( 16 | name: "EntwineTest", 17 | targets: ["EntwineTest"]), 18 | ], 19 | targets: [ 20 | .target( 21 | name: "Entwine", 22 | dependencies: []), 23 | .target( 24 | name: "EntwineTest", 25 | dependencies: ["Entwine"]), 26 | .testTarget( 27 | name: "EntwineTests", 28 | dependencies: ["Entwine", "EntwineTest"]), 29 | .testTarget( 30 | name: "EntwineTestTests", 31 | dependencies: ["EntwineTest"]), 32 | ] 33 | ) 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # [Entwine](https://github.com/tcldr/Entwine) 4 | ![CI](https://github.com/tcldr/Entwine/workflows/CI/badge.svg) 5 | [![@tcldr1](https://img.shields.io/static/v1?label=&message=@tcldr1&color=1DA1F2&logo=twitter&labelColor=24292e)](https://twitter.com/tcldr1) 6 | 7 | Accessories for [Apple's Combine Framework](https://developer.apple.com/documentation/combine). 8 | 9 | --- 10 | 11 | ## About 12 | Entwine consists of three libraries (over two repositories) to be used in conjuction with Apple's Combine framework: 13 | - The [_Entwine Utilities library_](https://github.com/tcldr/Entwine/blob/master/Assets/Entwine/README.md) includes additional operators, subjects and utilities for working with Combine sequences. 14 | The package currently includes a `ReplaySubject`, a `withLatest(from:)` operator and a `Publishers.Factory` for rapidly defining publishers inline from any source. 15 | 16 | **[View the README for the Entwine Utilities Library](https://github.com/tcldr/Entwine/blob/master/Assets/Entwine/README.md)** 17 | 18 | - The [_EntwineTest library_](https://github.com/tcldr/Entwine/blob/master/Assets/EntwineTest/README.md) consists of tools for verifying expected behavior of Combine sequences. It houses 19 | a `TestScheduler` that uses virtual time, a `TestablePublisher` that schedules a user-defined sequence of 20 | elements in absolute or relative time, and a `TestableSubscriber` that record a time-stamped list of events that can be compared against expected behavior. 21 | 22 | **[View the README for the EntwineTest Library](https://github.com/tcldr/Entwine/blob/master/Assets/EntwineTest/README.md)** 23 | 24 | - The [_EntwineRx library_](https://github.com/tcldr/EntwineRx/blob/master/README.md) is a small library maintained under a [separate repository](https://github.com/tcldr/EntwineRx) that contains bridging operators from RxSwift to Combine and vice versa 25 | making _RxSwift_ and _Combine_ work together seamlessly. 26 | 27 | **[View the README for the EntwineRx Library](https://github.com/tcldr/EntwineRx)** 28 | 29 | 30 | 31 | _Note: EntwineRx is maintained as a separate Swift package to minimize the SPM dependency graph_. 32 | 33 | 34 | --- 35 | 36 | ## Quick start guide 37 | ### Create _Combine_ publishers from any source 38 | Use the [`Publishers.Factory`](https://tcldr.github.io/Entwine/EntwineDocs/Extensions/Publishers/Factory.html) publisher from the _Entwine_ package to effortlessly create a publisher that 39 | meets Combine's backpressure requirements from any source. [Find out more about the _Entwine Utilities_ library.](https://github.com/tcldr/Entwine/blob/master/Assets/Entwine/README.md) 40 | 41 | _Inline publisher creation for PhotoKit authorization status:_ 42 | ```swift 43 | 44 | import Entwine 45 | 46 | let photoKitAuthorizationStatus = Publishers.Factory { dispatcher in 47 | let status = PHPhotoLibrary.authorizationStatus() 48 | dispatcher.forward(status) 49 | switch status { 50 | case .notDetermined: 51 | PHPhotoLibrary.requestAuthorization { newStatus in 52 | dispatcher.forward(newStatus) 53 | dispatcher.forward(completion: .finished) 54 | } 55 | default: 56 | dispatcher.forward(completion: .finished) 57 | } 58 | return AnyCancellable {} 59 | } 60 | ``` 61 | ### Unit test _Combine_ publisher sequences 62 | Use the `TestScheduler`, `TestablePublisher` and `TestableSubscriber` to simulate _Combine_ sequences and test against expected output. [Find out more about the _EntwineTest_ library](https://github.com/tcldr/Entwine/blob/master/Assets/EntwineTest/README.md) 63 | 64 | _Testing Combine's `map(_:)` operator:_ 65 | 66 | ```swift 67 | 68 | import XCTest 69 | import EntwineTest 70 | 71 | func testMap() { 72 | 73 | let testScheduler = TestScheduler(initialClock: 0) 74 | 75 | // creates a publisher that will schedule it's elements relatively, at the point of subscription 76 | let testablePublisher: TestablePublisher = testScheduler.createRelativeTestablePublisher([ 77 | (100, .input("a")), 78 | (200, .input("b")), 79 | (300, .input("c")), 80 | ]) 81 | 82 | let subjectUnderTest = testablePublisher.map { $0.uppercased() } 83 | 84 | // schedules a subscription at 200, to be cancelled at 900 85 | let results = testScheduler.start { subjectUnderTest } 86 | 87 | XCTAssertEqual(results.recordedOutput, [ 88 | (200, .subscription), // subscribed at 200 89 | (300, .input("A")), // received uppercased input @ 100 + subscription time 90 | (400, .input("B")), // received uppercased input @ 200 + subscription time 91 | (500, .input("C")), // received uppercased input @ 300 + subscription time 92 | ]) 93 | } 94 | ``` 95 | 96 | ### Bridge your _RxSwift_ view models to _Combine_ and use with _SwiftUI_ 97 | First, make sure you add the [_EntwineRx Swift Package_](https://github.com/tcldr/EntwineRx) (located in an external repo) as a dependency to your project. 98 | 99 | _Example coming soon_ 100 | 101 | --- 102 | 103 | ## Requirements 104 | Entwine sits on top of Apple's Combine framework and therefore requires _Xcode 11_ and is has minimum deployment targets of _macOS 10.15_, _iOS 13_, _tvOS 13_ or _watchOS 6_. 105 | 106 | --- 107 | 108 | ## Installation 109 | Entwine is delivered via a Swift Package and can be installed either as a dependency to another Swift Package by adding it to the dependencies section of a `Package.swift` file 110 | or to an Xcode 11 project by via the `File -> Swift Packages -> Add package dependency...` menu in Xcode 11. 111 | 112 | --- 113 | 114 | ## Documentation 115 | Documentation for each package is available at: 116 | - [Entwine Documentation](https://tcldr.github.io/Entwine/EntwineDocs/) (Operators, Publishers and Accessories) 117 | - [EntwineTest Documentation](https://tcldr.github.io/Entwine/EntwineTestDocs/) (Tools for testing _Combine_ sequence behavior) 118 | - [EntwineRx Documentation](https://tcldr.github.io/Entwine/EntwineRxDocs/) (Bridging operators for _RxSwift_) 119 | 120 | --- 121 | 122 | ## Copyright and license 123 | 124 | This project is released under the [MIT license](https://github.com/tcldr/Entwine/blob/master/LICENSE) 125 | 126 | --- 127 | 128 | -------------------------------------------------------------------------------- /Sources/Common/DataStructures/LinkedListQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entwine 3 | // https://github.com/tcldr/Entwine 4 | // 5 | // Copyright © 2019 Tristan Celder. All rights reserved. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | // MARK: - LinkedListQueue definition 26 | 27 | /// FIFO Queue based on a dual linked-list data structure 28 | struct LinkedListQueue { 29 | 30 | static var empty: LinkedListQueue { LinkedListQueue() } 31 | 32 | typealias Subnode = LinkedList 33 | 34 | private var regular = Subnode.empty 35 | private var inverse = Subnode.empty 36 | private (set) var count = 0 37 | 38 | /// O(n) where n is the length of the sequence 39 | init(_ elements: S) where S.Element == Element { 40 | self.inverse = LinkedList(elements.reversed()) 41 | self.regular = .empty 42 | } 43 | 44 | /// This is an O(1) operation 45 | var isEmpty: Bool { 46 | switch (regular, inverse) { 47 | case (.empty, .empty): 48 | return true 49 | default: 50 | return false 51 | } 52 | } 53 | 54 | /// This is an O(1) operation 55 | mutating func enqueue(_ element: Element) { 56 | inverse = .value(element, tail: inverse) 57 | count += 1 58 | } 59 | 60 | /// This is an O(1) operation 61 | mutating func dequeue() -> Element? { 62 | // Assuming the entire queue is consumed this is actually an O(1) operation. 63 | // This is because each element only passes through the expensive O(n) reverse 64 | // operation a single time and remains there until ready to be dequeued. 65 | switch (regular, inverse) { 66 | case (.empty, .empty): 67 | return nil 68 | case (.value(let head, let tail), _): 69 | regular = tail 70 | count -= 1 71 | return head 72 | default: 73 | regular = inverse.reversed 74 | inverse = .empty 75 | return dequeue() 76 | } 77 | } 78 | } 79 | 80 | // MARK: - Sequence conformance 81 | 82 | extension LinkedListQueue: Sequence { 83 | 84 | typealias Iterator = Self 85 | 86 | __consuming func makeIterator() -> LinkedListQueue { 87 | return self 88 | } 89 | } 90 | 91 | // MARK: - IteratorProtocol conformance 92 | 93 | extension LinkedListQueue: IteratorProtocol { 94 | 95 | mutating func next() -> Element? { 96 | return dequeue() 97 | } 98 | } 99 | 100 | // MARK: - ExpressibleByArrayLiteral conformance 101 | 102 | extension LinkedListQueue: ExpressibleByArrayLiteral { 103 | 104 | typealias ArrayLiteralElement = Element 105 | 106 | init(arrayLiteral elements: Element...) { 107 | self.init(elements) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Sources/Common/DataStructures/LinkedListStack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entwine 3 | // https://github.com/tcldr/Entwine 4 | // 5 | // Copyright © 2019 Tristan Celder. All rights reserved. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | // MARK: - LinkedList definition 26 | 27 | /// Value based linked list data structure. Effective as a LIFO Queue. 28 | struct LinkedListStack { 29 | 30 | typealias Node = LinkedList 31 | 32 | private (set) var node = Node.empty 33 | 34 | init(_ elements: C) where C.Element == Element { 35 | node = LinkedList(elements) 36 | } 37 | 38 | func peek() -> Element? { 39 | node.value 40 | } 41 | 42 | mutating func push(_ element: Element) { 43 | node.prepend(element) 44 | } 45 | } 46 | 47 | // MARK: - IteratorProtocol conformance 48 | 49 | extension LinkedListStack: IteratorProtocol { 50 | 51 | mutating func next() -> Element? { 52 | guard let value = node.poll() else { return nil } 53 | return value 54 | } 55 | } 56 | 57 | // MARK: - Sequence conformance 58 | 59 | extension LinkedListStack: Sequence { 60 | 61 | typealias Iterator = Self 62 | 63 | __consuming func makeIterator() -> Self { 64 | return self 65 | } 66 | } 67 | 68 | // MARK: - ExpressibleByArrayLiteral conformance 69 | 70 | extension LinkedListStack: ExpressibleByArrayLiteral { 71 | 72 | typealias ArrayLiteralElement = Element 73 | 74 | init(arrayLiteral elements: Element...) { 75 | self.init(elements) 76 | } 77 | } 78 | 79 | // MARK: - LinkedListNode definition 80 | 81 | indirect enum LinkedList { 82 | case value(Element, tail: LinkedList) 83 | case empty 84 | } 85 | 86 | extension LinkedList { 87 | 88 | typealias Index = Int 89 | 90 | init(_ elements: S) where S.Element == Element { 91 | self = elements.reduce(Self.empty) { acc, element in .value(element, tail: acc) } 92 | } 93 | 94 | var isEmpty: Bool { 95 | guard case .empty = self else { return false } 96 | return true 97 | } 98 | 99 | var reversed: Self { 100 | Self.reverse(self) 101 | } 102 | 103 | var value: Element? { 104 | guard case .value(let head, _) = self else { return nil } 105 | return head 106 | } 107 | 108 | var tail: LinkedList? { 109 | guard case .value(_, let tail) = self else { return nil } 110 | return tail 111 | } 112 | 113 | mutating func prepend(_ element: Element) { 114 | self = .value(element, tail: self) 115 | } 116 | 117 | mutating func poll() -> Element? { 118 | guard case .value(let head, let tail) = self else { return nil } 119 | self = tail 120 | return head 121 | } 122 | 123 | private static func reverse(_ node: Self, accumulator: Self = .empty) -> Self { 124 | guard case .value(let head, let tail) = node else { return accumulator } 125 | return reverse(tail, accumulator: .value(head, tail: accumulator)) 126 | } 127 | } 128 | 129 | -------------------------------------------------------------------------------- /Sources/Common/DataStructures/PriorityQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftPriorityQueue.swift 3 | // SwiftPriorityQueue 4 | // 5 | // Copyright (c) 2015-2019 David Kopec 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | // This code was inspired by Section 2.4 of Algorithms by Sedgewick & Wayne, 4th Edition 25 | // 26 | // Lifted from: https://github.com/davecom/SwiftPriorityQueue/blob/master/Sources/SwiftPriorityQueue/SwiftPriorityQueue.swift. 27 | 28 | 29 | /// A PriorityQueue takes objects to be pushed of any type that implements Comparable. 30 | /// It will pop the objects in the order that they would be sorted. A pop() or a push() 31 | /// can be accomplished in O(lg n) time. It can be specified whether the objects should 32 | /// be popped in ascending or descending order (Max Priority Queue or Min Priority Queue) 33 | /// at the time of initialization. 34 | /// 35 | struct PriorityQueue { 36 | 37 | fileprivate var heap = [T]() 38 | private let ordered: (T, T) -> Bool 39 | 40 | init(ascending: Bool = false, startingValues: [T] = []) { 41 | self.init(order: ascending ? { $0 > $1 } : { $0 < $1 }, startingValues: startingValues) 42 | } 43 | 44 | /// Creates a new PriorityQueue with the given ordering. 45 | /// 46 | /// - parameter order: A function that specifies whether its first argument should 47 | /// come after the second argument in the PriorityQueue. 48 | /// - parameter startingValues: An array of elements to initialize the PriorityQueue with. 49 | init(order: @escaping (T, T) -> Bool, startingValues: [T] = []) { 50 | ordered = order 51 | 52 | // Based on "Heap construction" from Sedgewick p 323 53 | heap = startingValues 54 | var i = heap.count/2 - 1 55 | while i >= 0 { 56 | sink(i) 57 | i -= 1 58 | } 59 | } 60 | 61 | /// How many elements the Priority Queue stores 62 | var count: Int { return heap.count } 63 | 64 | /// true if and only if the Priority Queue is empty 65 | var isEmpty: Bool { return heap.isEmpty } 66 | 67 | /// Add a new element onto the Priority Queue. O(lg n) 68 | /// 69 | /// - parameter element: The element to be inserted into the Priority Queue. 70 | mutating func push(_ element: T) { 71 | heap.append(element) 72 | swim(heap.count - 1) 73 | } 74 | 75 | /// Remove and return the element with the highest priority (or lowest if ascending). O(lg n) 76 | /// 77 | /// - returns: The element with the highest priority in the Priority Queue, or nil if the PriorityQueue is empty. 78 | mutating func pop() -> T? { 79 | 80 | if heap.isEmpty { return nil } 81 | if heap.count == 1 { return heap.removeFirst() } // added for Swift 2 compatibility 82 | // so as not to call swap() with two instances of the same location 83 | heap.swapAt(0, heap.count - 1) 84 | let temp = heap.removeLast() 85 | sink(0) 86 | 87 | return temp 88 | } 89 | 90 | 91 | /// Removes the first occurence of a particular item. Finds it by value comparison using ==. O(n) 92 | /// Silently exits if no occurrence found. 93 | /// 94 | /// - parameter item: The item to remove the first occurrence of. 95 | mutating func remove(_ item: T) { 96 | if let index = heap.firstIndex(of: item) { 97 | heap.swapAt(index, heap.count - 1) 98 | heap.removeLast() 99 | if index < heap.count { // if we removed the last item, nothing to swim 100 | swim(index) 101 | sink(index) 102 | } 103 | } 104 | } 105 | 106 | /// Removes all occurences of a particular item. Finds it by value comparison using ==. O(n) 107 | /// Silently exits if no occurrence found. 108 | /// 109 | /// - parameter item: The item to remove. 110 | mutating func removeAll(_ item: T) { 111 | var lastCount = heap.count 112 | remove(item) 113 | while (heap.count < lastCount) { 114 | lastCount = heap.count 115 | remove(item) 116 | } 117 | } 118 | 119 | /// Get a look at the current highest priority item, without removing it. O(1) 120 | /// 121 | /// - returns: The element with the highest priority in the PriorityQueue, or nil if the PriorityQueue is empty. 122 | func peek() -> T? { 123 | return heap.first 124 | } 125 | 126 | /// Eliminate all of the elements from the Priority Queue. 127 | mutating func clear() { 128 | heap.removeAll(keepingCapacity: false) 129 | } 130 | 131 | // Based on example from Sedgewick p 316 132 | mutating func sink(_ index: Int) { 133 | var index = index 134 | while 2 * index + 1 < heap.count { 135 | 136 | var j = 2 * index + 1 137 | 138 | if j < (heap.count - 1) && ordered(heap[j], heap[j + 1]) { j += 1 } 139 | if !ordered(heap[index], heap[j]) { break } 140 | 141 | heap.swapAt(index, j) 142 | index = j 143 | } 144 | } 145 | 146 | // Based on example from Sedgewick p 316 147 | mutating func swim(_ index: Int) { 148 | var index = index 149 | while index > 0 && ordered(heap[(index - 1) / 2], heap[index]) { 150 | heap.swapAt((index - 1) / 2, index) 151 | index = (index - 1) / 2 152 | } 153 | } 154 | } 155 | 156 | // MARK: - GeneratorType 157 | extension PriorityQueue: IteratorProtocol { 158 | 159 | typealias Element = T 160 | mutating func next() -> Element? { return pop() } 161 | } 162 | 163 | // MARK: - SequenceType 164 | extension PriorityQueue: Sequence { 165 | 166 | typealias Iterator = PriorityQueue 167 | func makeIterator() -> Iterator { return self } 168 | } 169 | 170 | // MARK: - CollectionType 171 | extension PriorityQueue: Collection { 172 | 173 | typealias Index = Int 174 | 175 | var startIndex: Int { return heap.startIndex } 176 | var endIndex: Int { return heap.endIndex } 177 | 178 | subscript(i: Int) -> T { return heap[i] } 179 | 180 | func index(after i: PriorityQueue.Index) -> PriorityQueue.Index { 181 | return heap.index(after: i) 182 | } 183 | } 184 | 185 | // MARK: - CustomStringConvertible, CustomDebugStringConvertible 186 | extension PriorityQueue: CustomStringConvertible, CustomDebugStringConvertible { 187 | 188 | var description: String { return heap.description } 189 | var debugDescription: String { return heap.debugDescription } 190 | } 191 | -------------------------------------------------------------------------------- /Sources/Common/Utilities/SinkQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entwine 3 | // https://github.com/tcldr/Entwine 4 | // 5 | // Copyright © 2019 Tristan Celder. All rights reserved. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | #if canImport(Combine) 26 | 27 | import Combine 28 | 29 | // MARK: - SinkQueue definition 30 | 31 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 32 | class SinkQueue { 33 | 34 | private var sink: Sink? 35 | private var buffer = LinkedListQueue() 36 | 37 | private var demandRequested = Subscribers.Demand.none 38 | private var demandProcessed = Subscribers.Demand.none 39 | private var demandForwarded = Subscribers.Demand.none 40 | 41 | private var completion: Subscribers.Completion? 42 | private var isActive: Bool { sink != nil && completion == nil } 43 | private var shouldBuffer: Bool { demandRequested < .unlimited } 44 | 45 | init(sink: Sink) { 46 | self.sink = sink 47 | } 48 | 49 | func requestDemand(_ demand: Subscribers.Demand) -> Subscribers.Demand { 50 | demandRequested += demand 51 | return processDemand() 52 | } 53 | 54 | func enqueue(_ input: Sink.Input) -> Subscribers.Demand { 55 | guard completion == nil, let sink = sink else { 56 | assertionFailure("Out of sequence. A completion signal is queued or has already been sent.") 57 | return .none 58 | } 59 | guard shouldBuffer else { 60 | return sink.receive(input) 61 | } 62 | buffer.enqueue(input) 63 | return processDemand() 64 | } 65 | 66 | func enqueue(completion: Subscribers.Completion) -> Subscribers.Demand { 67 | guard self.completion == nil, sink != nil else { 68 | assertionFailure("Out of sequence. A completion signal is queued or has already been sent.") 69 | return .none 70 | } 71 | guard shouldBuffer else { 72 | expediteCompletion(completion) 73 | return .none 74 | } 75 | self.completion = completion 76 | return processDemand() 77 | } 78 | 79 | func expediteCompletion(_ completion: Subscribers.Completion) { 80 | guard let sink = sink else { 81 | assertionFailure("Out of sequence. A completion signal has already been sent.") 82 | return 83 | } 84 | self.sink = nil 85 | self.buffer = .empty 86 | sink.receive(completion: completion) 87 | } 88 | 89 | // Processes as much demand as requested, returns spare capacity that 90 | // can be forwarded to upstream subscriber/s 91 | func processDemand() -> Subscribers.Demand { 92 | guard let sink = sink else { return .none } 93 | while demandProcessed < demandRequested, let next = buffer.next() { 94 | demandProcessed += 1 95 | demandRequested += sink.receive(next) 96 | } 97 | if let completion = completion, buffer.count < 1 { 98 | expediteCompletion(completion) 99 | return .none 100 | } 101 | let forwardableDemand = (demandRequested - demandForwarded) 102 | demandForwarded += forwardableDemand 103 | return forwardableDemand 104 | } 105 | } 106 | 107 | #endif 108 | -------------------------------------------------------------------------------- /Sources/Entwine/Common/DataStructures/LinkedListQueue.swift: -------------------------------------------------------------------------------- 1 | ../../../Common/DataStructures/LinkedListQueue.swift -------------------------------------------------------------------------------- /Sources/Entwine/Common/DataStructures/LinkedListStack.swift: -------------------------------------------------------------------------------- 1 | ../../../Common/DataStructures/LinkedListStack.swift -------------------------------------------------------------------------------- /Sources/Entwine/Common/DataStructures/PriorityQueue.swift: -------------------------------------------------------------------------------- 1 | ../../../Common/DataStructures/PriorityQueue.swift -------------------------------------------------------------------------------- /Sources/Entwine/Common/Utilities/SinkQueue.swift: -------------------------------------------------------------------------------- 1 | ../../../Common/Utilities/SinkQueue.swift -------------------------------------------------------------------------------- /Sources/Entwine/Deprecated/CancellableBag.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entwine 3 | // https://github.com/tcldr/Entwine 4 | // 5 | // Copyright © 2019 Tristan Celder. All rights reserved. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | #if canImport(Combine) 26 | 27 | import Combine 28 | 29 | /// A container for cancellables that will be cancelled when the bag is deallocated or cancelled itself 30 | @available(*, deprecated, message: "Replace with mutable Set") 31 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 32 | public final class CancellableBag: Cancellable { 33 | 34 | public init() {} 35 | 36 | private var cancellables = [AnyCancellable]() 37 | 38 | /// Adds a cancellable to the bag which will have its `.cancel()` method invoked 39 | /// when the bag is deallocated or cancelled itself 40 | public func add(_ cancellable: C) { 41 | cancellables.append(AnyCancellable { cancellable.cancel() }) 42 | } 43 | 44 | /// Empties the bag and cancels each contained item 45 | public func cancel() { 46 | cancellables.removeAll() 47 | } 48 | } 49 | 50 | // MARK: - Cancellable extension 51 | 52 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 53 | public extension Cancellable { 54 | @available(*, deprecated, message: "Replace CancellableBag with Set and use `store(in:)`") 55 | func cancelled(by cancellableBag: CancellableBag) { 56 | cancellableBag.add(self) 57 | } 58 | } 59 | 60 | #endif 61 | -------------------------------------------------------------------------------- /Sources/Entwine/Deprecated/Deprecations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entwine 3 | // https://github.com/tcldr/Entwine 4 | // 5 | // Copyright © 2019 Tristan Celder. All rights reserved. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | #if canImport(Combine) 26 | 27 | import Combine 28 | 29 | @available(*, deprecated, message: "Replace with mutable Set") 30 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 31 | public typealias CancellationBag = Set 32 | 33 | #endif 34 | -------------------------------------------------------------------------------- /Sources/Entwine/Operators/Materialize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entwine 3 | // https://github.com/tcldr/Entwine 4 | // 5 | // Copyright © 2019 Tristan Celder. All rights reserved. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | #if canImport(Combine) 26 | 27 | import Combine 28 | 29 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 30 | extension Publishers { 31 | 32 | // MARK: - Publisher 33 | 34 | /// Wraps all the elements as well as the subscription and completion events of an upstream publisher 35 | /// into a stream of `Signal` elements 36 | public struct Materialize: Publisher { 37 | 38 | public typealias Output = Signal 39 | public typealias Failure = Never 40 | 41 | private let upstream: Upstream 42 | 43 | init(upstream: Upstream) { 44 | self.upstream = upstream 45 | } 46 | 47 | public func receive(subscriber: S) where Failure == S.Failure, Output == S.Input { 48 | subscriber.receive(subscription: MaterializeSubscription(upstream: upstream, downstream: subscriber)) 49 | } 50 | } 51 | 52 | // MARK: - Subscription 53 | 54 | // Owned by the downstream subscriber 55 | fileprivate class MaterializeSubscription: Subscription 56 | where Never == Downstream.Failure, Signal == Downstream.Input 57 | { 58 | var sink: MaterializeSink? 59 | 60 | init(upstream: Upstream, downstream: Downstream) { 61 | self.sink = MaterializeSink(upstream: upstream, downstream: downstream) 62 | } 63 | 64 | // Subscription Methods 65 | 66 | // Called by the downstream subscriber to signal 67 | // additional elements can be sent 68 | func request(_ demand: Subscribers.Demand) { 69 | sink?.signalDemand(demand) 70 | } 71 | 72 | // Called by the downstream subscriber to end the 73 | // subscription and clean up any resources. 74 | // Shouldn't be a blocking call, but it is legal 75 | // for a few more elements to arrive after this is 76 | // called. 77 | func cancel() { 78 | self.sink = nil 79 | } 80 | } 81 | 82 | // MARK: - Sink 83 | 84 | fileprivate class MaterializeSink: Subscriber 85 | where Never == Downstream.Failure, Signal == Downstream.Input 86 | { 87 | 88 | typealias Input = Upstream.Output 89 | typealias Failure = Upstream.Failure 90 | 91 | var queue: SinkQueue 92 | var upstreamSubscription: Subscription? 93 | 94 | init(upstream: Upstream, downstream: Downstream) { 95 | self.queue = SinkQueue(sink: downstream) 96 | upstream.subscribe(self) 97 | } 98 | 99 | deinit { 100 | cancelUpstreamSubscription() 101 | } 102 | 103 | // Called by the upstream publisher (or its agent) to signal 104 | // that the subscription has begun 105 | func receive(subscription: Subscription) { 106 | self.upstreamSubscription = subscription 107 | let demand = queue.enqueue(.subscription) 108 | guard demand > .none else { return } 109 | subscription.request(demand) 110 | } 111 | 112 | // Called by the upstream publisher (or its agent) to signal 113 | // an element has arrived, signals to the upstream publisher 114 | // how many more elements can be sent 115 | func receive(_ input: Input) -> Subscribers.Demand { 116 | return queue.enqueue(.input(input)) 117 | } 118 | 119 | // Called by the upstream publisher (or its agent) to signal 120 | // that the sequence has terminated 121 | func receive(completion: Subscribers.Completion) { 122 | _ = queue.enqueue(.completion(completion)) 123 | _ = queue.enqueue(completion: .finished) 124 | cancelUpstreamSubscription() 125 | } 126 | 127 | // Indirectly called by the downstream subscriber via its subscription 128 | // to signal that more items can be sent downstream 129 | func signalDemand(_ demand: Subscribers.Demand) { 130 | let spareDemand = queue.requestDemand(demand) 131 | guard spareDemand > .none else { return } 132 | upstreamSubscription?.request(spareDemand) 133 | } 134 | 135 | func cancelUpstreamSubscription() { 136 | upstreamSubscription?.cancel() 137 | upstreamSubscription = nil 138 | } 139 | } 140 | } 141 | 142 | // MARK: - Operator 143 | 144 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 145 | public extension Publisher { 146 | 147 | /// Wraps each element from the upstream publisher, as well as its subscription and completion events, 148 | /// into `Signal` values. 149 | /// 150 | /// - Returns: A publisher that wraps each element from the upstream publisher, as well as its 151 | /// subscription and completion events, into `Signal` values. 152 | func materialize() -> Publishers.Materialize { 153 | Publishers.Materialize(upstream: self) 154 | } 155 | } 156 | 157 | #endif 158 | -------------------------------------------------------------------------------- /Sources/Entwine/Operators/ReferenceCounted.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entwine 3 | // https://github.com/tcldr/Entwine 4 | // 5 | // Copyright © 2019 Tristan Celder. All rights reserved. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | #if canImport(Combine) 26 | 27 | import Combine 28 | 29 | // MARK: - Publisher 30 | 31 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 32 | extension Publishers { 33 | 34 | /// Automates the process of connecting to a multicast publisher. Connects when the first 35 | /// subscriber connects then cancels and discards when the subscriber count falls to zero. 36 | public final class ReferenceCounted: Publisher 37 | where Upstream.Output == SubjectType.Output, Upstream.Failure == SubjectType.Failure 38 | { 39 | public typealias Output = Upstream.Output 40 | public typealias Failure = Upstream.Failure 41 | 42 | private let upstream: Upstream 43 | private let createSubject: () -> SubjectType 44 | private weak var sharedUpstreamReference: Publishers.Autoconnect>? 45 | 46 | init(upstream: Upstream, createSubject: @escaping () -> SubjectType) { 47 | self.upstream = upstream 48 | self.createSubject = createSubject 49 | } 50 | 51 | public func receive(subscriber: S) where Failure == S.Failure, Output == S.Input { 52 | let sharedUpstream = sharedUpstreamPublisher() 53 | sharedUpstream.subscribe(ReferenceCountedSink(upstream: sharedUpstream, downstream: subscriber)) 54 | } 55 | 56 | func sharedUpstreamPublisher() -> Publishers.Autoconnect> { 57 | guard let shared = sharedUpstreamReference else { 58 | let shared = upstream.multicast(createSubject).autoconnect() 59 | self.sharedUpstreamReference = shared 60 | return shared 61 | } 62 | return shared 63 | } 64 | } 65 | 66 | // MARK: - Sink 67 | 68 | fileprivate final class ReferenceCountedSink: Subscriber 69 | where Upstream.Output == Downstream.Input, Upstream.Failure == Downstream.Failure 70 | { 71 | typealias Input = Downstream.Input 72 | typealias Failure = Downstream.Failure 73 | 74 | private let upstream: Upstream 75 | private let downstream: Downstream 76 | 77 | init(upstream: Upstream, downstream: Downstream) { 78 | self.upstream = upstream 79 | self.downstream = downstream 80 | } 81 | 82 | func receive(subscription: Subscription) { 83 | downstream.receive(subscription: ReferenceCountedSubscription(wrappedSubscription: subscription, sink: self)) 84 | } 85 | 86 | func receive(_ input: Input) -> Subscribers.Demand { 87 | downstream.receive(input) 88 | } 89 | 90 | func receive(completion: Subscribers.Completion) { 91 | downstream.receive(completion: completion) 92 | } 93 | } 94 | 95 | fileprivate final class ReferenceCountedSubscription: Subscription { 96 | 97 | let wrappedSubscription: Subscription 98 | var sink: Sink? 99 | 100 | init(wrappedSubscription: Subscription, sink: Sink) { 101 | self.wrappedSubscription = wrappedSubscription 102 | self.sink = sink 103 | } 104 | 105 | func request(_ demand: Subscribers.Demand) { 106 | wrappedSubscription.request(demand) 107 | } 108 | 109 | func cancel() { 110 | wrappedSubscription.cancel() 111 | sink = nil 112 | } 113 | } 114 | } 115 | 116 | // MARK: - Operator 117 | 118 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 119 | extension Publishers.Multicast { 120 | 121 | /// Automates the process of connecting to a multicast publisher. Connects when the first 122 | /// subscriber connects then cancels and discards when the subscriber count falls to zero. 123 | /// 124 | /// - Returns: A publisher which automatically connects to its upstream multicast publisher. 125 | public func referenceCounted() -> Publishers.ReferenceCounted { 126 | .init(upstream: upstream, createSubject: createSubject) 127 | } 128 | } 129 | 130 | #endif 131 | -------------------------------------------------------------------------------- /Sources/Entwine/Operators/ReplaySubject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entwine 3 | // https://github.com/tcldr/Entwine 4 | // 5 | // Copyright © 2019 Tristan Celder. All rights reserved. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | #if canImport(Combine) 26 | 27 | import Combine 28 | 29 | /// A subject that maintains a buffer of its latest values for replay to new subscribers and passes 30 | /// through subsequent elements and completion 31 | /// 32 | /// The subject passes through elements and completion states unchanged and in addition 33 | /// replays the latest elements to any new subscribers. Use this subject when you want subscribers 34 | /// to receive the most recent previous elements in addition to all future elements. 35 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 36 | public final class ReplaySubject { 37 | 38 | typealias Sink = AnySubscriber 39 | 40 | private var subscriptions = [ReplaySubjectSubscription]() 41 | private var replayValues: ReplaySubjectValueBuffer 42 | private var completion: Subscribers.Completion? 43 | 44 | private var isActive: Bool { completion == nil } 45 | var subscriptionCount: Int { subscriptions.count } 46 | 47 | /// - Parameter maxBufferSize: The number of elements that should be buffered for 48 | /// replay to new subscribers 49 | /// - Returns: A subject that maintains a buffer of its recent values for replay to new subscribers 50 | /// and passes through subsequent values and completion 51 | public init(maxBufferSize: Int) { 52 | self.replayValues = .init(maxBufferSize: maxBufferSize) 53 | } 54 | } 55 | 56 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 57 | extension ReplaySubject: Publisher { 58 | 59 | public func receive(subscriber: S) where Failure == S.Failure, Output == S.Input { 60 | let subscriberIdentifier = subscriber.combineIdentifier 61 | let subscription = ReplaySubjectSubscription(sink: AnySubscriber(subscriber)) 62 | subscriptions.append(subscription) 63 | subscription.cleanupHandler = { [weak self] in 64 | let firstIndex = self?.subscriptions.firstIndex { subscriberIdentifier == $0.subscriberIdentifier } 65 | guard let index = firstIndex else { return } 66 | self?.subscriptions.remove(at: index) 67 | } 68 | subscriber.receive(subscription: subscription) 69 | subscription.replayInputs(replayValues.buffer, completion: completion) 70 | } 71 | } 72 | 73 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 74 | extension ReplaySubject: Subject { 75 | 76 | public func send(subscription: Subscription) { 77 | subscription.request(.unlimited) 78 | } 79 | 80 | public func send(_ value: Output) { 81 | guard isActive else { return } 82 | replayValues.addValueToBuffer(value) 83 | subscriptions.forEach { subscription in 84 | subscription.forwardValueToSink(value) 85 | } 86 | } 87 | 88 | public func send(completion: Subscribers.Completion) { 89 | guard isActive else { return } 90 | self.completion = completion 91 | subscriptions.forEach { subscription in 92 | subscription.forwardCompletionToSink(completion) 93 | } 94 | } 95 | } 96 | 97 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 98 | fileprivate final class ReplaySubjectSubscription: Subscription { 99 | 100 | private let queue: SinkQueue 101 | 102 | var cleanupHandler: (() -> Void)? 103 | let subscriberIdentifier: CombineIdentifier 104 | 105 | init(sink: Sink) { 106 | self.queue = SinkQueue(sink: sink) 107 | self.subscriberIdentifier = sink.combineIdentifier 108 | } 109 | 110 | func replayInputs(_ replayedInputs: ReplayedInputs, completion: Subscribers.Completion?) where ReplayedInputs.Element == Sink.Input { 111 | replayedInputs.forEach(forwardValueToSink) 112 | if let completion = completion { 113 | forwardCompletionToSink(completion) 114 | } 115 | } 116 | 117 | func forwardValueToSink(_ value: Sink.Input) { 118 | _ = queue.enqueue(value) 119 | } 120 | 121 | func forwardCompletionToSink(_ completion: Subscribers.Completion) { 122 | _ = queue.enqueue(completion: completion) 123 | } 124 | 125 | func request(_ demand: Subscribers.Demand) { 126 | _ = queue.requestDemand(demand) 127 | } 128 | 129 | func cancel() { 130 | cleanupHandler?() 131 | cleanupHandler = nil 132 | } 133 | } 134 | 135 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 136 | extension ReplaySubject { 137 | 138 | public static func createUnbounded() -> ReplaySubject { 139 | return .init(maxBufferSize: .max) 140 | } 141 | 142 | public static func create(bufferSize: Int) -> ReplaySubject { 143 | return .init(maxBufferSize: bufferSize) 144 | } 145 | } 146 | 147 | fileprivate struct ReplaySubjectValueBuffer { 148 | 149 | let maxBufferSize: Int 150 | private (set) var buffer = LinkedListQueue() 151 | 152 | init(maxBufferSize: Int) { 153 | self.maxBufferSize = maxBufferSize 154 | } 155 | 156 | mutating func addValueToBuffer(_ value: Value) { 157 | buffer.enqueue(value) 158 | if buffer.count > maxBufferSize { 159 | _ = buffer.dequeue() 160 | } 161 | } 162 | } 163 | 164 | #endif 165 | -------------------------------------------------------------------------------- /Sources/Entwine/Operators/ShareReplay.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entwine 3 | // https://github.com/tcldr/Entwine 4 | // 5 | // Copyright © 2019 Tristan Celder. All rights reserved. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | #if canImport(Combine) 26 | 27 | import Combine 28 | 29 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 30 | extension Publisher { 31 | /// Returns a publisher as a class instance that replays previous values to new subscribers 32 | /// 33 | /// The downstream subscriber receives elements and completion states unchanged from the 34 | /// previous subscriber, and in addition replays the latest elements received from the upstream 35 | /// subscriber to any new subscribers. Use this operator when you want new subscribers to 36 | /// receive the most recently produced values immediately upon subscription. 37 | /// 38 | /// - Parameter maxBufferSize: The number of elements that should be buffered for 39 | /// replay to new subscribers 40 | /// - Returns: A class instance that republishes its upstream publisher and maintains a 41 | /// buffer of its latest values for replay to new subscribers 42 | public func share(replay maxBufferSize: Int) -> Publishers.ReferenceCounted> { 43 | multicast { ReplaySubject(maxBufferSize: maxBufferSize) }.referenceCounted() 44 | } 45 | } 46 | 47 | #endif 48 | -------------------------------------------------------------------------------- /Sources/Entwine/Operators/Signpost.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entwine 3 | // https://github.com/tcldr/Entwine 4 | // 5 | // Copyright © 2019 Tristan Celder. All rights reserved. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | #if canImport(Combine) 26 | 27 | import Combine 28 | import os.log 29 | 30 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 31 | extension Publishers { 32 | 33 | // MARK: - Publisher configuration 34 | 35 | /// Configuration values for the `Publishers.Signpost` operator. 36 | /// 37 | /// Pass `Publishers.SignpostConfiguration.Marker` values to the initializer to define how signposts for 38 | /// each event should be labelled: 39 | /// - A nil marker value will disable signposts for that event 40 | /// - A `.default` marker value will use the event name as the event label 41 | /// - A `.named(_:)` marker value will use the passed string as the event label 42 | public struct SignpostConfiguration { 43 | 44 | public struct Marker { 45 | 46 | /// A marker that specifies a signpost should use the default label for an event 47 | public static let `default` = Marker() 48 | 49 | /// A marker that specifies a signpost should use the passed name as a label for an event 50 | /// - Parameter name: The name the signpost should be labelled with 51 | public static func named(_ name: StaticString) -> Marker { 52 | Marker(name) 53 | } 54 | 55 | let name: StaticString? 56 | 57 | init (_ name: StaticString? = nil) { 58 | self.name = name 59 | } 60 | } 61 | 62 | /// Configuration values for the `Publishers.Signpost` operator. 63 | /// 64 | /// The default value specifies signposts should be grouped into the `com.github.tcldr.Entwine.Signpost` 65 | /// subsystem using the `.pointsOfInterest` category (displayed in most Xcode Intruments templates 66 | /// by default under the 'Points of Interest' instrument). 67 | /// 68 | /// Use a `Publishers.SignpostConfiguration.Marker` value to define how signposts for each event should 69 | /// be labelled: 70 | /// - A nil marker value will disable signposts for that event 71 | /// - A `.default` marker value will use the event name as the event label 72 | /// - A `.named(_:)` marker value will use the passed string as the event label 73 | /// 74 | /// - Parameters: 75 | /// - log: The OSLog parameters to be used to mark signposts 76 | /// - receiveSubscriptionMarker: A marker value to identify subscription events. A `default` value yields an event labelled `subscription` 77 | /// - receiveMarker: A marker value to identify sequence output events. A `default` value yields an event labelled `receive` 78 | /// - receiveCompletionMarker: A marker value to identify sequence completion events. A `default` value yields an event labelled `receiveCompletion` 79 | /// - requestMarker: A marker value to identify demand request events. A `default` value yields an event labelled `request` 80 | /// - cancelMarker: A marker value to identify cancellation events. A `default` value yields an event labelled `cancel` 81 | public init( 82 | log: OSLog = OSLog(subsystem: "com.github.tcldr.Entwine.Signpost", category: .pointsOfInterest), 83 | receiveSubscriptionMarker: Marker? = nil, 84 | receiveMarker: Marker? = nil, 85 | receiveCompletionMarker: Marker? = nil, 86 | requestMarker: Marker? = nil, 87 | cancelMarker: Marker? = nil 88 | ) { 89 | self.log = log 90 | self.receiveSubscriptionMarker = receiveSubscriptionMarker 91 | self.receiveMarker = receiveMarker 92 | self.receiveCompletionMarker = receiveCompletionMarker 93 | self.requestMarker = requestMarker 94 | self.cancelMarker = cancelMarker 95 | } 96 | 97 | public var log: OSLog 98 | public var receiveSubscriptionMarker: Marker? 99 | public var receiveMarker: Marker? 100 | public var receiveCompletionMarker: Marker? 101 | public var requestMarker: Marker? 102 | public var cancelMarker: Marker? 103 | } 104 | 105 | // MARK: - Publisher 106 | 107 | public struct Signpost: Publisher { 108 | 109 | public typealias Output = Upstream.Output 110 | public typealias Failure = Upstream.Failure 111 | 112 | private let upstream: Upstream 113 | private let configuration: SignpostConfiguration 114 | 115 | init(upstream: Upstream, configuration: SignpostConfiguration) { 116 | self.upstream = upstream 117 | self.configuration = configuration 118 | } 119 | 120 | public func receive(subscriber: S) where Failure == S.Failure, Output == S.Input { 121 | upstream.subscribe(SignpostSink(downstream: subscriber, configuration: configuration)) 122 | } 123 | } 124 | 125 | // MARK: - Sink 126 | 127 | fileprivate class SignpostSink: Subscriber { 128 | 129 | typealias Input = Downstream.Input 130 | typealias Failure = Downstream.Failure 131 | 132 | private var downstream: Downstream 133 | private let configuration: SignpostConfiguration 134 | 135 | init(downstream: Downstream, configuration: SignpostConfiguration) { 136 | self.downstream = downstream 137 | self.configuration = configuration 138 | } 139 | 140 | func receive(subscription: Subscription) { 141 | downstream.receive(subscription: SignpostSubscription(wrappedSubscription: subscription, configuration: configuration)) 142 | } 143 | 144 | func receive(_ input: Input) -> Subscribers.Demand { 145 | guard let marker = configuration.receiveMarker else { 146 | return downstream.receive(input) 147 | } 148 | let signpostID = OSSignpostID(log: configuration.log) 149 | os_signpost(.begin, log: configuration.log, name: marker.name ?? "receive", signpostID: signpostID) 150 | defer { os_signpost(.end, log: configuration.log, name: marker.name ?? "receive", signpostID: signpostID) } 151 | return downstream.receive(input) 152 | } 153 | 154 | func receive(completion: Subscribers.Completion) { 155 | guard let marker = configuration.receiveCompletionMarker else { 156 | downstream.receive(completion: completion) 157 | return 158 | } 159 | let signpostID = OSSignpostID(log: configuration.log) 160 | os_signpost(.begin, log: configuration.log, name: marker.name ?? "receiveCompletion", signpostID: signpostID) 161 | downstream.receive(completion: completion) 162 | os_signpost(.end, log: configuration.log, name: marker.name ?? "receiveCompletion", signpostID: signpostID) 163 | } 164 | } 165 | 166 | // MARK: - Subscription 167 | 168 | fileprivate class SignpostSubscription: Subscription { 169 | 170 | private let wrappedSubscription: Subscription 171 | private let configuration: SignpostConfiguration 172 | 173 | init(wrappedSubscription: Subscription, configuration: SignpostConfiguration) { 174 | self.wrappedSubscription = wrappedSubscription 175 | self.configuration = configuration 176 | } 177 | 178 | func request(_ demand: Subscribers.Demand) { 179 | guard let marker = configuration.requestMarker else { 180 | wrappedSubscription.request(demand) 181 | return 182 | } 183 | let signpostID = OSSignpostID(log: configuration.log) 184 | os_signpost(.begin, log: configuration.log, name: marker.name ?? "request", signpostID: signpostID, "%{public}@", String(describing: demand)) 185 | wrappedSubscription.request(demand) 186 | os_signpost(.end, log: configuration.log, name: marker.name ?? "request", signpostID: signpostID) 187 | } 188 | 189 | func cancel() { 190 | guard let marker = configuration.cancelMarker else { 191 | wrappedSubscription.cancel() 192 | return 193 | } 194 | let signpostID = OSSignpostID(log: configuration.log) 195 | os_signpost(.begin, log: configuration.log, name: marker.name ?? "cancel", signpostID: signpostID) 196 | wrappedSubscription.cancel() 197 | os_signpost(.end, log: configuration.log, name: marker.name ?? "cancel", signpostID: signpostID) 198 | } 199 | } 200 | } 201 | 202 | // MARK: - Operator 203 | 204 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 205 | public extension Publisher { 206 | 207 | /// Marks points of interest for your publisher events as time intervals for debugging performance in Instruments. 208 | /// 209 | /// - Parameter configuration: A configuration value specifying which events to mark as points of interest. 210 | /// The default value specifies signposts should be grouped into the `com.github.tcldr.Entwine.Signpost` 211 | /// subsystem using the `.pointsOfInterest` category (displayed in most Xcode Intruments templates 212 | /// by default under the 'Points of Interest' instrument) . See `Publishers.SignpostConfiguration` 213 | /// initializer for detailed options. 214 | /// - Returns: A publisher that marks points of interest when specified publisher events occur 215 | func signpost(configuration: Publishers.SignpostConfiguration = .init(receiveMarker: .default)) -> Publishers.Signpost { 216 | Publishers.Signpost(upstream: self, configuration: configuration) 217 | } 218 | } 219 | 220 | #endif 221 | -------------------------------------------------------------------------------- /Sources/Entwine/Operators/WithLatestFrom.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entwine 3 | // https://github.com/tcldr/Entwine 4 | // 5 | // Copyright © 2019 Tristan Celder. All rights reserved. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | #if canImport(Combine) 26 | 27 | import Combine 28 | 29 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 30 | extension Publishers { 31 | 32 | /// A publisher that combines the latest value from another publisher with each value from an upstream publisher 33 | public struct WithLatestFrom: Publisher where Upstream.Failure == Other.Failure { 34 | 35 | public typealias Failure = Upstream.Failure 36 | 37 | private let upstream: Upstream 38 | private let other: Other 39 | private let transform: (Upstream.Output, Other.Output) -> Output 40 | 41 | public init(upstream: Upstream, other: Other, transform: @escaping (Upstream.Output, Other.Output) -> Output) { 42 | self.upstream = upstream 43 | self.other = other 44 | self.transform = transform 45 | } 46 | 47 | public func receive(subscriber: S) where Failure == S.Failure, Output == S.Input { 48 | let otherSink = WithLatestFromOtherSink(publisher: other) 49 | let upstreamSink = WithLatestFromSink(upstream: upstream, downstream: subscriber, otherSink: otherSink, transform: transform) 50 | subscriber.receive(subscription: WithLatestFromSubscription(sink: upstreamSink)) 51 | } 52 | } 53 | 54 | // MARK: - Subscription 55 | 56 | fileprivate class WithLatestFromSubscription: Subscription 57 | where Upstream.Failure == Other.Failure, Upstream.Failure == Downstream.Failure 58 | { 59 | 60 | var sink: WithLatestFromSink? 61 | 62 | init(sink: WithLatestFromSink) { 63 | self.sink = sink 64 | } 65 | 66 | func request(_ demand: Subscribers.Demand) { 67 | sink?.signalDemand(demand) 68 | } 69 | 70 | func cancel() { 71 | self.sink?.terminateSubscription() 72 | self.sink = nil 73 | } 74 | } 75 | 76 | // MARK: - Main Sink 77 | 78 | fileprivate class WithLatestFromSink: Subscriber 79 | where Upstream.Failure == Other.Failure, Upstream.Failure == Downstream.Failure 80 | { 81 | 82 | typealias Input = Upstream.Output 83 | typealias Failure = Upstream.Failure 84 | 85 | var queue: SinkQueue 86 | var upstreamSubscription: Subscription? 87 | 88 | let otherSink: WithLatestFromOtherSink 89 | let transform: (Upstream.Output, Other.Output) -> Downstream.Input 90 | 91 | init(upstream: Upstream, downstream: Downstream, otherSink: WithLatestFromOtherSink, transform: @escaping (Input, Other.Output) -> Downstream.Input) { 92 | self.queue = SinkQueue(sink: downstream) 93 | self.otherSink = otherSink 94 | self.transform = transform 95 | 96 | upstream.subscribe(self) 97 | } 98 | 99 | func receive(subscription: Subscription) { 100 | self.upstreamSubscription = subscription 101 | otherSink.subscribe() 102 | } 103 | 104 | func receive(_ input: Input) -> Subscribers.Demand { 105 | guard let otherInput = otherSink.lastInput else { 106 | // we ignore results from the `Upstream` publisher until 107 | // we have received an item from the `Other` publisher. 108 | // 109 | // As we are ignoring this item, we need to signal to the 110 | // upstream publisher that they may reclaim the budget for 111 | // the dropped item by returning a Subscribers.Demand of 1. 112 | return .max(1) 113 | } 114 | return queue.enqueue(transform(input, otherInput)) 115 | } 116 | 117 | func receive(completion: Subscribers.Completion) { 118 | terminateSubscription() 119 | _ = queue.enqueue(completion: completion) 120 | } 121 | 122 | func signalDemand(_ demand: Subscribers.Demand) { 123 | let spareDemand = queue.requestDemand(demand) 124 | guard spareDemand > .none else { return } 125 | upstreamSubscription?.request(spareDemand) 126 | } 127 | 128 | func terminateSubscription() { 129 | otherSink.terminateSubscription() 130 | upstreamSubscription?.cancel() 131 | upstreamSubscription = nil 132 | } 133 | } 134 | 135 | // MARK: - Other Sink 136 | 137 | fileprivate class WithLatestFromOtherSink: Subscriber { 138 | 139 | typealias Input = P.Output 140 | typealias Failure = P.Failure 141 | 142 | private let publisher: AnyPublisher 143 | private (set) var lastInput: Input? 144 | private var subscription: Subscription? 145 | 146 | init(publisher: P) { 147 | self.publisher = publisher.eraseToAnyPublisher() 148 | } 149 | 150 | func subscribe() { 151 | publisher.subscribe(self) 152 | } 153 | 154 | func receive(subscription: Subscription) { 155 | self.subscription = subscription 156 | subscription.request(.unlimited) 157 | } 158 | 159 | func receive(_ input: Input) -> Subscribers.Demand { 160 | lastInput = input 161 | return .none 162 | } 163 | 164 | func receive(completion: Subscribers.Completion) { } 165 | 166 | func terminateSubscription() { 167 | subscription?.cancel() 168 | subscription = nil 169 | } 170 | } 171 | } 172 | 173 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 174 | public extension Publisher { 175 | 176 | /// Subscribes to an additional publisher and invokes a closure upon receiving output from this 177 | /// publisher. 178 | /// 179 | /// - Parameter other: Another publisher to combibe with this one 180 | /// - Parameter transform: A closure that receives each value produced by this 181 | /// publisher and the latest value from another publisher and returns a new value to publish 182 | /// - Returns: A publisher that combines the latest value from another publisher with each 183 | /// value from this publisher 184 | func withLatest(from other: P, transform: @escaping (Output, P.Output) -> T) -> Publishers.WithLatestFrom where P.Failure == Failure { 185 | Publishers.WithLatestFrom(upstream: self, other: other, transform: transform) 186 | } 187 | 188 | /// Subscribes to an additional publisher and produces its latest value each time this publisher 189 | /// produces a value. 190 | /// 191 | /// - Parameter other: Another publisher to gather latest values from 192 | /// - Returns: A publisher that produces the latest value from another publisher each time 193 | /// this publisher produces an element 194 | func withLatest(from other: P) -> Publishers.WithLatestFrom where P.Failure == Failure { 195 | withLatest(from: other, transform: { _, b in b }) 196 | } 197 | } 198 | 199 | #endif 200 | -------------------------------------------------------------------------------- /Sources/Entwine/Publishers/Factory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entwine 3 | // https://github.com/tcldr/Entwine 4 | // 5 | // Copyright © 2019 Tristan Celder. All rights reserved. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | #if canImport(Combine) 26 | 27 | import Combine 28 | 29 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 30 | extension Publishers { 31 | 32 | /// Creates a simple publisher inline from a provided closure 33 | /// 34 | /// This publisher can be used to turn any arbitrary source of values (such as a timer or a user authorization 35 | /// request) into a new publisher sequence. 36 | /// 37 | /// From within the scope of the closure passed into the initializer, it is possible to call the methods of the 38 | /// `Dispatcher` object – which is passed in as a parameter – to send values down stream. 39 | /// 40 | /// 41 | /// ## Example 42 | /// 43 | /// ```swift 44 | /// 45 | /// import Entwine 46 | /// 47 | /// let photoKitAuthorizationStatus = Publishers.Factory { dispatcher in 48 | /// let status = PHPhotoLibrary.authorizationStatus() 49 | /// dispatcher.forward(status) 50 | /// switch status { 51 | /// case .notDetermined: 52 | /// PHPhotoLibrary.requestAuthorization { newStatus in 53 | /// dispatcher.forward(newStatus) 54 | /// dispatcher.forward(completion: .finished) 55 | /// } 56 | /// default: 57 | /// dispatcher.forward(completion: .finished) 58 | /// } 59 | /// return AnyCancellable {} 60 | /// } 61 | /// ``` 62 | /// 63 | /// - Warning: Developers should be aware that a `Dispatcher` has an unbounded buffer that stores values 64 | /// yet to be requested by the downstream subscriber. 65 | /// 66 | /// When creating a publisher from a source with an unbounded rate of production that cannot be influenced, 67 | /// developers should consider following this operator with a `Publishers.Buffer` operator to prevent a 68 | /// strain on resources 69 | public struct Factory: Publisher { 70 | 71 | let subscription: (Dispatcher) -> AnyCancellable 72 | 73 | public init(_ subscription: @escaping (Dispatcher) -> AnyCancellable) { 74 | self.subscription = subscription 75 | } 76 | 77 | public func receive(subscriber: S) where Failure == S.Failure, Output == S.Input { 78 | subscriber.receive(subscription: FactorySubscription(sink: subscriber, subscription: subscription)) 79 | } 80 | } 81 | } 82 | 83 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 84 | fileprivate class FactorySubscription: Subscription { 85 | 86 | var subscription: ((Dispatcher) -> AnyCancellable)? 87 | var dispatcher: FactoryDispatcher? 88 | var cancellable: AnyCancellable? 89 | 90 | init(sink: Sink, subscription: @escaping (Dispatcher) -> AnyCancellable) { 91 | self.subscription = subscription 92 | self.dispatcher = FactoryDispatcher(sink: sink) 93 | } 94 | 95 | func request(_ demand: Subscribers.Demand) { 96 | _ = dispatcher?.queue.requestDemand(demand) 97 | startUpstreamSubscriptionIfNeeded() 98 | } 99 | 100 | func startUpstreamSubscriptionIfNeeded() { 101 | guard let subscription = subscription, let dispatcher = dispatcher else { return } 102 | self.subscription = nil 103 | cancellable = subscription(dispatcher) 104 | } 105 | 106 | func cancel() { 107 | cancellable = nil 108 | dispatcher = nil 109 | } 110 | } 111 | 112 | // MARK: - Public facing Dispatcher defintion 113 | 114 | /// Manages a queue of publisher sequence elements to be delivered to a subscriber 115 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 116 | public class Dispatcher { 117 | 118 | /// Queues an element to be delivered to the subscriber 119 | /// 120 | /// - Warning: If either the `forward(completion:)` or the 121 | /// `forwardImmediately(completion:)`method of the dispatcher has already 122 | /// been called this will raise an assertion failure. 123 | /// 124 | /// - Parameter input: a value to be delivered to a downstream subscriber 125 | public func forward(_ input: Input) { 126 | fatalError("Abstract class. Override in subclass.") 127 | } 128 | 129 | /// Completes the sequence once any queued elements are delivered to the subscriber 130 | /// 131 | /// - Warning: If either the `forward(completion:)` or the 132 | /// `forwardImmediately(completion:)`method of the dispatcher has already 133 | /// been called this will raise an assertion failure. 134 | /// 135 | /// - Parameter completion: a completion value to be delivered to the subscriber once 136 | /// the remaining items in the queue have been delivered 137 | public func forward(completion: Subscribers.Completion) { 138 | fatalError("Abstract class. Override in subclass.") 139 | } 140 | 141 | /// Completes the sequence immediately regardless of any elements that are waiting to be delivered 142 | /// - Warning: subsequent calls to the dispatcher will raise an assertion failure 143 | /// - Parameter completion: a completion value to be delivered immediately to the subscriber 144 | public func forwardImmediately(completion: Subscribers.Completion) { 145 | fatalError("Abstract class. Override in subclass.") 146 | } 147 | } 148 | 149 | // MARK: - Internal Dispatcher defintion 150 | 151 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 152 | class FactoryDispatcher: Dispatcher 153 | where Input == Sink.Input, Failure == Sink.Failure 154 | { 155 | 156 | let queue: SinkQueue 157 | 158 | init(sink: Sink) { 159 | self.queue = SinkQueue(sink: sink) 160 | } 161 | 162 | public override func forward(_ input: Input) { 163 | _ = queue.enqueue(input) 164 | } 165 | 166 | public override func forward(completion: Subscribers.Completion) { 167 | _ = queue.enqueue(completion: completion) 168 | } 169 | 170 | public override func forwardImmediately(completion: Subscribers.Completion) { 171 | queue.expediteCompletion(completion) 172 | } 173 | } 174 | 175 | #endif 176 | -------------------------------------------------------------------------------- /Sources/Entwine/Schedulers/TrampolineScheduler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entwine 3 | // https://github.com/tcldr/Entwine 4 | // 5 | // Copyright © 2019 Tristan Celder. All rights reserved. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | #if canImport(Combine) 26 | 27 | import Combine 28 | import Foundation 29 | 30 | // MARK: - Class definition 31 | 32 | /// A scheduler for performing trampolined actions. 33 | /// 34 | /// This scheduler will queue scheduled actions immediately on the current thread performing them in a first in, first out order. 35 | /// 36 | /// You can only use this scheduler for immediate actions. If you attempt to schedule 37 | /// actions after a specific date, the scheduler produces a fatal error. 38 | public final class TrampolineScheduler { 39 | 40 | public static let shared = TrampolineScheduler() 41 | 42 | private static let localThreadActionQueueKey = "com.github.tcldr.Entwine.TrampolineScheduler.localThreadActionQueueKey" 43 | 44 | private static var localThreadActionQueue: TrampolineSchedulerQueue { 45 | guard let queue = Thread.current.threadDictionary.value(forKey: Self.localThreadActionQueueKey) as? TrampolineSchedulerQueue else { 46 | let newQueue = TrampolineSchedulerQueue() 47 | Thread.current.threadDictionary.setValue(newQueue, forKey: Self.localThreadActionQueueKey) 48 | return newQueue 49 | } 50 | return queue 51 | } 52 | } 53 | 54 | // MARK: - Scheduler conformance 55 | 56 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 57 | extension TrampolineScheduler: Scheduler { 58 | 59 | public typealias SchedulerTimeType = ImmediateScheduler.SchedulerTimeType 60 | public typealias SchedulerOptions = ImmediateScheduler.SchedulerOptions 61 | 62 | public var now: TrampolineScheduler.SchedulerTimeType { 63 | ImmediateScheduler.shared.now 64 | } 65 | 66 | public var minimumTolerance: TrampolineScheduler.SchedulerTimeType.Stride { 67 | ImmediateScheduler.shared.minimumTolerance 68 | } 69 | 70 | public func schedule(options: SchedulerOptions?, _ action: @escaping () -> Void) { 71 | Self.localThreadActionQueue.push(action) 72 | } 73 | 74 | public func schedule(after date: SchedulerTimeType, tolerance: SchedulerTimeType.Stride, options: SchedulerOptions?, _ action: @escaping () -> Void) { 75 | fatalError("You can only use this scheduler for immediate actions") 76 | } 77 | 78 | public func schedule(after date: SchedulerTimeType, interval: SchedulerTimeType.Stride, tolerance: SchedulerTimeType.Stride, options: SchedulerOptions?, _ action: @escaping () -> Void) -> Cancellable { 79 | fatalError("You can only use this scheduler for immediate actions") 80 | } 81 | } 82 | 83 | 84 | // MARK: - Scheduler Queue 85 | 86 | fileprivate final class TrampolineSchedulerQueue { 87 | 88 | typealias Action = () -> Void 89 | enum Status { case idle, active } 90 | 91 | private var queuedActions = LinkedListQueue() 92 | private var status = Status.idle 93 | 94 | func push(_ action: @escaping Action) { 95 | queuedActions.enqueue(action) 96 | dispatchQueuedActions() 97 | } 98 | 99 | func dispatchQueuedActions() { 100 | guard status == .idle else { return } 101 | status = .active 102 | while let action = queuedActions.next() { 103 | action() 104 | } 105 | status = .idle 106 | } 107 | } 108 | 109 | #endif 110 | -------------------------------------------------------------------------------- /Sources/Entwine/Signal.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entwine 3 | // https://github.com/tcldr/Entwine 4 | // 5 | // Copyright © 2019 Tristan Celder. All rights reserved. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | #if canImport(Combine) 26 | 27 | import Combine 28 | 29 | // MARK: - Signal value definition 30 | 31 | /// A materialized representation of a `Publisher`s output. 32 | /// 33 | /// Upon a call to subscribe, a legal `Publisher` produces signals in strictly the following order: 34 | /// - Exactly one 'subscription' signal. 35 | /// - Followed by zero or more 'input' signals. 36 | /// - Terminated finally by a single 'completion' signal. 37 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 38 | public enum Signal { 39 | /// Sent by a `Publisher` to a `Subscriber` in acknowledgment of the `Subscriber`'s 40 | /// subscription request. 41 | case subscription 42 | /// The payload of a subscription. Zero to many `.input(_)` signals may be produced 43 | /// during the lifetime of a `Subscriber`'s subscription to a `Publisher`. 44 | case input(Input) 45 | /// The final signal sent to a `Subscriber` during a subscription to a `Publisher`. 46 | /// Indicates termination of the stream as well as the reason. 47 | case completion(Subscribers.Completion) 48 | } 49 | 50 | // MARK: - Signal extensions 51 | 52 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 53 | public extension Signal { 54 | /// Whether the signal indicates sequence completion 55 | var isCompletion: Bool { 56 | guard case .completion(_) = self else { return false } 57 | return true 58 | } 59 | 60 | /// Returns a signal with a transformed input type and input element 61 | /// - Parameter transform: A mapping closure. `transform` accepts an element of this signal's input type 62 | /// as its parameter and returns a transformed value of the same or of a different type. 63 | /// - Returns: A signal with a transformed input type and input element 64 | func mapInput(_ transform: (Input) -> T) -> Signal { 65 | switch self { 66 | case .input(let value): return .input(transform(value)) 67 | case .completion(let completion): return .completion(completion) 68 | case .subscription: return .subscription 69 | } 70 | } 71 | 72 | /// Returns a signal with a transformed failure type and completion element 73 | /// - Parameter transform: A mapping closure. `transform` accepts an element of this signal's failure type 74 | /// as its parameter and returns a transformed error of the same or of a different type. 75 | /// - Returns: A signal with a transformed failure type and completion element 76 | func mapFailure(_ transform: (Failure) -> T) -> Signal { 77 | switch self { 78 | case .completion(let completion): 79 | guard case .failure(let error) = completion else { 80 | return .completion(.finished) 81 | } 82 | return .completion(.failure(transform(error))) 83 | case .input(let value): return .input(value) 84 | case .subscription: return .subscription 85 | } 86 | } 87 | } 88 | 89 | // MARK: - Equatable conformance 90 | 91 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 92 | extension Signal: Equatable where Input: Equatable, Failure: Equatable { 93 | 94 | public static func ==(lhs: Signal, rhs: Signal) -> Bool { 95 | switch (lhs, rhs) { 96 | case (.subscription, .subscription): 97 | return true 98 | case (.input(let lhsInput), .input(let rhsInput)): 99 | return (lhsInput == rhsInput) 100 | case (.completion(let lhsCompletion), .completion(let rhsCompletion)): 101 | return completionsMatch(lhs: lhsCompletion, rhs: rhsCompletion) 102 | default: 103 | return false 104 | } 105 | } 106 | 107 | fileprivate static func completionsMatch(lhs: Subscribers.Completion, rhs: Subscribers.Completion) -> Bool { 108 | switch (lhs, rhs) { 109 | case (.finished, .finished): 110 | return true 111 | case (.failure(let lhsError), .failure(let rhsError)): 112 | return (lhsError == rhsError) 113 | default: 114 | return false 115 | } 116 | } 117 | } 118 | 119 | /// A type that can be converted into a `Signal` 120 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 121 | public protocol SignalConvertible { 122 | 123 | /// The `Input` type of the produced `Signal` 124 | associatedtype Input 125 | /// The `Failure` type of the produced `Signal` 126 | associatedtype Failure: Error 127 | 128 | init(_ signal: Signal) 129 | /// The converted `Signal` 130 | var signal: Signal { get } 131 | } 132 | 133 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 134 | extension Signal: SignalConvertible { 135 | 136 | public init(_ signal: Signal) { 137 | self = signal 138 | } 139 | 140 | public var signal: Signal { 141 | return self 142 | } 143 | } 144 | 145 | #endif 146 | -------------------------------------------------------------------------------- /Sources/Entwine/Utilities/DeallocToken.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entwine 3 | // https://github.com/tcldr/Entwine 4 | // 5 | // Copyright © 2019 Tristan Celder. All rights reserved. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | #if canImport(Combine) 26 | 27 | import Combine 28 | 29 | /// An object that notifies of its deallocation via a publisher sequence 30 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 31 | public final class DeallocToken { 32 | 33 | private let subject = PassthroughSubject<(), Never>() 34 | 35 | /// A publisher that, upon deallocation of the token, publishes a single element and then immediately completes. 36 | public var publisher: AnyPublisher<(), Never> { subject.eraseToAnyPublisher() } 37 | 38 | public init() {} 39 | 40 | deinit { 41 | subject.send() 42 | subject.send(completion: .finished) 43 | } 44 | } 45 | 46 | #endif 47 | -------------------------------------------------------------------------------- /Sources/EntwineTest/Common/DataStructures/LinkedListQueue.swift: -------------------------------------------------------------------------------- 1 | ../../../Common/DataStructures/LinkedListQueue.swift -------------------------------------------------------------------------------- /Sources/EntwineTest/Common/DataStructures/LinkedListStack.swift: -------------------------------------------------------------------------------- 1 | ../../../Common/DataStructures/LinkedListStack.swift -------------------------------------------------------------------------------- /Sources/EntwineTest/Common/DataStructures/PriorityQueue.swift: -------------------------------------------------------------------------------- 1 | ../../../Common/DataStructures/PriorityQueue.swift -------------------------------------------------------------------------------- /Sources/EntwineTest/Common/Utilities/SinkQueue.swift: -------------------------------------------------------------------------------- 1 | ../../../Common/Utilities/SinkQueue.swift -------------------------------------------------------------------------------- /Sources/EntwineTest/Deprecations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entwine 3 | // https://github.com/tcldr/Entwine 4 | // 5 | // Copyright © 2019 Tristan Celder. All rights reserved. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | #if canImport(Combine) 26 | 27 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 28 | public extension TestableSubscriber { 29 | 30 | @available(*, deprecated, renamed: "recordedOutput") 31 | var sequence: TestSequence{ recordedOutput } 32 | 33 | @available(*, deprecated, renamed: "recordedDemandLog") 34 | var demands: DemandLedger{ recordedDemandLog } 35 | } 36 | 37 | #endif 38 | -------------------------------------------------------------------------------- /Sources/EntwineTest/Signal+CustomDebugStringConvertible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entwine 3 | // https://github.com/tcldr/Entwine 4 | // 5 | // Copyright © 2019 Tristan Celder. All rights reserved. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | #if canImport(Combine) 26 | 27 | import Entwine 28 | 29 | // MARK: - CustomDebugStringConvertible conformance 30 | 31 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 32 | extension Signal: CustomDebugStringConvertible { 33 | public var debugDescription: String { 34 | switch self { 35 | case .subscription: 36 | return ".subscribe" 37 | case .input(let input): 38 | return ".input(\(input))" 39 | case .completion(let completion): 40 | return ".completion(\(completion))" 41 | } 42 | } 43 | } 44 | 45 | #endif 46 | -------------------------------------------------------------------------------- /Sources/EntwineTest/TestEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entwine 3 | // https://github.com/tcldr/Entwine 4 | // 5 | // Copyright © 2019 Tristan Celder. All rights reserved. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | #if canImport(Combine) 26 | 27 | import Combine 28 | import Entwine 29 | 30 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 31 | struct TestEvent { 32 | 33 | let time: VirtualTime 34 | let signal: Signal 35 | 36 | init(_ time: VirtualTime, _ signal: Signal) { 37 | self.time = time 38 | self.signal = signal 39 | } 40 | } 41 | 42 | // MARK: - Equatable conformance 43 | 44 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 45 | extension TestEvent: Equatable where Signal: Equatable {} 46 | 47 | // MARK: - CustomDebugStringConvertible conformance 48 | 49 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 50 | extension TestEvent: CustomDebugStringConvertible { 51 | public var debugDescription: String { 52 | return "SignalEvent(\(time), \(signal))" 53 | } 54 | } 55 | 56 | #endif 57 | -------------------------------------------------------------------------------- /Sources/EntwineTest/TestScheduler/TestScheduler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entwine 3 | // https://github.com/tcldr/Entwine 4 | // 5 | // Copyright © 2019 Tristan Celder. All rights reserved. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | // Based on RxSwift's `TestScheduler` 26 | // Copyright © 2015 Krunoslav Zaher All rights reserved. 27 | // https://github.com/ReactiveX/RxSwift 28 | 29 | #if canImport(Combine) 30 | 31 | import Entwine 32 | import Combine 33 | 34 | // MARK: - TestScheduler definition 35 | 36 | 37 | /// A `Scheduler` thats uses `VirtualTime` to schedule its tasks. 38 | /// 39 | /// A special, non thread-safe scheduler for testing operators that require a scheduler without introducing 40 | /// real concurrency. Faciliates a recreatable sequence of tasks executed within 'virtual time'. 41 | /// 42 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 43 | public class TestScheduler { 44 | 45 | /// Configuration values for a`TestScheduler` test run. 46 | public struct Configuration { 47 | /// Determines if the scheduler starts the test immediately 48 | public var pausedOnStart = false 49 | /// Absolute `VirtualTime`at which `Publisher` factory is invoked. 50 | public var created: VirtualTime = 100 51 | /// Absolute `VirtualTime` at which initialised `Publisher` is subscribed to. 52 | public var subscribed: VirtualTime = 200 53 | /// Absolute `VirtualTime` at which subscription to `Publisher` is cancelled. 54 | public var cancelled: VirtualTime = 900 55 | /// Options for the generated `TestableSubscriber` 56 | public var subscriberOptions = TestableSubscriberOptions.default 57 | 58 | public static let `default` = Configuration() 59 | } 60 | 61 | private var currentTime = VirtualTime(0) 62 | private let maxTime: VirtualTime 63 | private var lastTaskId = -1 64 | private var schedulerQueue: PriorityQueue 65 | 66 | /// Initialises the scheduler with the given commencement time 67 | /// 68 | /// - Parameters: 69 | /// - initialClock: The VirtualTime at which the scheduler will start 70 | /// - maxClock: The VirtualTime ceiling after which the scheduler will cease to process tasks 71 | public init(initialClock: VirtualTime = 0, maxClock: VirtualTime = 100_000) { 72 | self.schedulerQueue = PriorityQueue(ascending: true, startingValues: []) 73 | self.currentTime = initialClock 74 | self.maxTime = maxClock 75 | } 76 | 77 | /// Schedules the creation and subscription of an arbitrary `Publisher` to a `TestableSubscriber`, and 78 | /// finally the subscription's subsequent cancellation. 79 | /// 80 | /// The default `Configuration`: 81 | /// - Creates the publisher (executes the supplied publisher factory) at `100` 82 | /// - Subscribes to the publisher at `200` 83 | /// - Cancels the subscription at `900` 84 | /// - Starts the scheduler immediately. 85 | /// - Uses `TestableSubscriberOptions.default` for the subscriber configuration. 86 | /// 87 | /// - Parameters: 88 | /// - configuration: The parameters of the test subscription including scheduling details 89 | /// - create: A factory function that returns the publisher to be subscribed to 90 | /// - Returns: A `TestableSubscriber` that contains, or is scheduled to contain, the output of the publisher subscription. 91 | public func start(configuration: Configuration = .default, create: @escaping () -> P) -> TestableSubscriber { 92 | 93 | let subscriber = createTestableSubscriber(P.Output.self, P.Failure.self, options: configuration.subscriberOptions) 94 | var source: AnyPublisher! 95 | 96 | schedule(after: configuration.created, tolerance: minimumTolerance, options: nil) { 97 | source = create().eraseToAnyPublisher() 98 | } 99 | schedule(after: configuration.subscribed, tolerance: minimumTolerance, options: nil) { 100 | source.subscribe(subscriber) 101 | } 102 | schedule(after: configuration.cancelled, tolerance: minimumTolerance, options: nil) { 103 | subscriber.cancel() 104 | } 105 | 106 | guard !configuration.pausedOnStart else { 107 | return subscriber 108 | } 109 | 110 | defer { resume() } 111 | 112 | return subscriber 113 | } 114 | 115 | /// Initialises a `TestablePublisher` with events scheduled in absolute time. 116 | /// 117 | /// Creates a `TestablePublisher` and schedules the supplied `TestSequence` to occur in 118 | /// absolute time. Sequence elements with virtual times in the 'past' will be ignored. 119 | /// 120 | /// - Warning: This method will produce an assertion failure if the supplied `TestSequence` includes 121 | /// a `Signal.subscription` element. Subscription time is dictated by the subscriber and can not be 122 | /// predetermined by the publisher. 123 | /// 124 | /// - Parameter sequence: The sequence of values the publisher should produce 125 | /// - Returns: A `TestablePublisher` loaded with the supplied `TestSequence`. 126 | public func createAbsoluteTestablePublisher(_ sequence: TestSequence) -> TestablePublisher { 127 | return TestablePublisher(testScheduler: self, behavior: .absolute, testSequence: sequence) 128 | } 129 | 130 | /// Initialises a `TestablePublisher` with events scheduled in relative time. 131 | /// 132 | /// Creates a `TestablePublisher` and schedules the supplied `TestSequence` to occur in 133 | /// absolute time. 134 | /// 135 | /// - Warning: This method will produce an assertion failure if the supplied `TestSequence` includes 136 | /// a `Signal.subscription` element. Subscription time is dictated by the subscriber and can not be 137 | /// predetermined by the publisher. 138 | /// 139 | /// - Parameter sequence: The sequence of values the publisher should produce 140 | /// - Returns: A `TestablePublisher` loaded with the supplied `TestSequence`. 141 | public func createRelativeTestablePublisher(_ sequence: TestSequence) -> TestablePublisher { 142 | return TestablePublisher(testScheduler: self, behavior: .relative, testSequence: sequence) 143 | } 144 | 145 | 146 | /// Produces a `TestableSubscriber` pre-populated with this scheduler. 147 | /// 148 | /// - Parameters: 149 | /// - inputType: The `Input` associated type for the produced `Subscriber` 150 | /// - failureType: The `Failure` associated type for the produced `Subscriber` 151 | /// - options: Behavior options for the produced `Subscriber` 152 | /// - Returns: A configured `TestableSubscriber`. 153 | public func createTestableSubscriber(_ inputType: Input.Type, _ failureType: Failure.Type, options: TestableSubscriberOptions = .default) -> TestableSubscriber { 154 | return TestableSubscriber(scheduler: self, options: options) 155 | } 156 | 157 | /// Performs all the actions in the scheduler's queue, in time order followed by submission order, until no 158 | /// more actions remain. 159 | public func resume() { 160 | while let next = findNext() { 161 | guard next.time <= maxTime else { 162 | print(""" 163 | ⚠️ TestScheduler maxClock (\(maxTime)) reached. Scheduler aborted with \(schedulerQueue.count) remaining tasks. 164 | """) 165 | break 166 | } 167 | if next.time > currentTime { 168 | currentTime = next.time 169 | } 170 | schedulerQueue.remove(next) 171 | if next.interval > 0 { 172 | schedulerQueue.push( 173 | .init( 174 | id: next.id, 175 | time: now + max(minimumTolerance, next.interval), 176 | interval: next.interval, 177 | action: next.action 178 | ) 179 | ) 180 | } 181 | next.action() 182 | } 183 | } 184 | 185 | func reset(initialClock: VirtualTime = 0) { 186 | self.schedulerQueue = PriorityQueue(ascending: true, startingValues: []) 187 | self.currentTime = initialClock 188 | self.lastTaskId = -1 189 | } 190 | 191 | private func findNext() -> TestSchedulerTask? { 192 | schedulerQueue.peek() 193 | } 194 | 195 | private func nextTaskId() -> Int { 196 | lastTaskId += 1 197 | return lastTaskId 198 | } 199 | } 200 | 201 | // MARK: - TestScheduler Scheduler conformance 202 | 203 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 204 | extension TestScheduler: Scheduler { 205 | 206 | public typealias SchedulerTimeType = VirtualTime 207 | public typealias SchedulerOptions = Never 208 | 209 | public var now: VirtualTime { currentTime } 210 | 211 | public var minimumTolerance: VirtualTimeInterval { 1 } 212 | 213 | public func schedule(options: Never?, _ action: @escaping () -> Void) { 214 | schedulerQueue.push( 215 | TestSchedulerTask(id: nextTaskId(), time: currentTime, interval: 0, action: action)) 216 | } 217 | 218 | public func schedule(after date: VirtualTime, tolerance: VirtualTimeInterval, options: Never?, _ action: @escaping () -> Void) { 219 | schedulerQueue.push( 220 | TestSchedulerTask(id: nextTaskId(), time: date, interval: 0, action: action)) 221 | } 222 | 223 | public func schedule(after date: VirtualTime, interval: VirtualTimeInterval, tolerance: VirtualTimeInterval, options: Never?, _ action: @escaping () -> Void) -> Cancellable { 224 | let task = TestSchedulerTask( 225 | id: nextTaskId(), time: date, interval: interval, action: action) 226 | schedulerQueue.push(task) 227 | return AnyCancellable { 228 | self.schedulerQueue.remove(task) 229 | } 230 | } 231 | } 232 | 233 | // MARK: - TestSchedulerTask definition 234 | 235 | struct TestSchedulerTask { 236 | 237 | typealias Action = () -> Void 238 | 239 | let id: Int 240 | let time: VirtualTime 241 | let interval: VirtualTimeInterval 242 | let action: Action 243 | 244 | init(id: Int, time: VirtualTime, interval: VirtualTimeInterval, action: @escaping Action) { 245 | self.id = id 246 | self.time = time 247 | self.interval = interval 248 | self.action = action 249 | } 250 | } 251 | 252 | // MARK: - TestSchedulerTask Comparable conformance 253 | 254 | extension TestSchedulerTask: Comparable { 255 | 256 | static func < (lhs: TestSchedulerTask, rhs: TestSchedulerTask) -> Bool { 257 | (lhs.time, lhs.id) < (rhs.time, rhs.id) 258 | } 259 | 260 | static func == (lhs: TestSchedulerTask, rhs: TestSchedulerTask) -> Bool { 261 | lhs.id == rhs.id 262 | } 263 | } 264 | 265 | #endif 266 | -------------------------------------------------------------------------------- /Sources/EntwineTest/TestScheduler/VirtualTime.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entwine 3 | // https://github.com/tcldr/Entwine 4 | // 5 | // Copyright © 2019 Tristan Celder. All rights reserved. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | 26 | // MARK: - VirtualTime value definition 27 | 28 | /// Unit of virtual time consumed by the `TestScheduler` 29 | public struct VirtualTime: Hashable { 30 | 31 | internal var _time: Int 32 | 33 | public init(_ time: Int) { 34 | _time = time 35 | } 36 | } 37 | 38 | // MARK: - SignedNumeric conformance 39 | 40 | extension VirtualTime: SignedNumeric { 41 | 42 | public typealias Magnitude = Int 43 | 44 | public var magnitude: Int { Int(_time.magnitude) } 45 | 46 | public init?(exactly source: T) where T : BinaryInteger { 47 | guard let value = Int(exactly: source) else { return nil } 48 | self.init(value) 49 | } 50 | 51 | public static func * (lhs: VirtualTime, rhs: VirtualTime) -> VirtualTime { 52 | .init(lhs._time * rhs._time) 53 | } 54 | 55 | public static func *= (lhs: inout VirtualTime, rhs: VirtualTime) { 56 | lhs._time *= rhs._time 57 | } 58 | 59 | public static func + (lhs: VirtualTime, rhs: VirtualTime) -> VirtualTime { 60 | .init(lhs._time + rhs._time) 61 | } 62 | 63 | public static func - (lhs: VirtualTime, rhs: VirtualTime) -> VirtualTime { 64 | .init(lhs._time - rhs._time) 65 | } 66 | 67 | public static func += (lhs: inout VirtualTime, rhs: VirtualTime) { 68 | lhs._time += rhs._time 69 | } 70 | 71 | public static func -= (lhs: inout VirtualTime, rhs: VirtualTime) { 72 | lhs._time -= rhs._time 73 | } 74 | } 75 | 76 | // MARK: - Strideable conformance 77 | 78 | extension VirtualTime: Strideable { 79 | 80 | public typealias Stride = VirtualTimeInterval 81 | 82 | public func distance(to other: VirtualTime) -> VirtualTimeInterval { 83 | .init(other._time - _time) 84 | } 85 | 86 | public func advanced(by n: VirtualTimeInterval) -> VirtualTime { 87 | .init(_time + n._duration) 88 | } 89 | } 90 | 91 | // MARK: - ExpressibleByIntegerLiteral conformance 92 | 93 | extension VirtualTime: ExpressibleByIntegerLiteral { 94 | 95 | public typealias IntegerLiteralType = Int 96 | 97 | public init(integerLiteral value: Int) { 98 | self.init(value) 99 | } 100 | } 101 | 102 | // MARK: - CustomDebugStringConvertible conformance 103 | 104 | extension VirtualTime: CustomDebugStringConvertible { 105 | public var debugDescription: String { 106 | return "\(_time)" 107 | } 108 | } 109 | 110 | // MARK: - Int initializer 111 | 112 | extension Int { 113 | init(_ value: VirtualTime) { 114 | self.init(value._time) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Sources/EntwineTest/TestScheduler/VirtualTimeInterval.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entwine 3 | // https://github.com/tcldr/Entwine 4 | // 5 | // Copyright © 2019 Tristan Celder. All rights reserved. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | #if canImport(Combine) 26 | 27 | import Combine 28 | 29 | // MARK: - VirtualTimeInterval value definition 30 | 31 | /// Unit of relative virtual time consumed by the `TestScheduler` 32 | public struct VirtualTimeInterval { 33 | 34 | internal var _duration: Int 35 | 36 | public init(_ duration: Int) { 37 | _duration = duration 38 | } 39 | } 40 | 41 | // MARK: - SignedNumeric conformance 42 | 43 | extension VirtualTimeInterval: SignedNumeric { 44 | 45 | public typealias Magnitude = Int 46 | 47 | public var magnitude: Int { Int(_duration.magnitude) } 48 | 49 | public init?(exactly source: T) where T : BinaryInteger { 50 | guard let value = Int(exactly: source) else { return nil } 51 | self.init(value) 52 | } 53 | 54 | public static func * (lhs: VirtualTimeInterval, rhs: VirtualTimeInterval) -> VirtualTimeInterval { 55 | .init(lhs._duration * rhs._duration) 56 | } 57 | 58 | public static func *= (lhs: inout VirtualTimeInterval, rhs: VirtualTimeInterval) { 59 | lhs._duration *= rhs._duration 60 | } 61 | 62 | public static func + (lhs: VirtualTimeInterval, rhs: VirtualTimeInterval) -> VirtualTimeInterval { 63 | .init(lhs._duration + rhs._duration) 64 | } 65 | 66 | public static func - (lhs: VirtualTimeInterval, rhs: VirtualTimeInterval) -> VirtualTimeInterval { 67 | .init(lhs._duration - rhs._duration) 68 | } 69 | 70 | public static func += (lhs: inout VirtualTimeInterval, rhs: VirtualTimeInterval) { 71 | lhs._duration += rhs._duration 72 | } 73 | 74 | public static func -= (lhs: inout VirtualTimeInterval, rhs: VirtualTimeInterval) { 75 | lhs._duration -= rhs._duration 76 | } 77 | } 78 | 79 | // MARK: - Comparable conformance 80 | 81 | extension VirtualTimeInterval: Comparable { 82 | 83 | public static func < (lhs: VirtualTimeInterval, rhs: VirtualTimeInterval) -> Bool { 84 | lhs._duration < rhs._duration 85 | } 86 | } 87 | 88 | // MARK: - ExpressibleByIntegerLiteral conformance 89 | 90 | extension VirtualTimeInterval: ExpressibleByIntegerLiteral { 91 | 92 | public typealias IntegerLiteralType = Int 93 | 94 | public init(integerLiteral value: Int) { 95 | self.init(value) 96 | } 97 | } 98 | 99 | // MARK: - SchedulerTimeIntervalConvertible conformance 100 | 101 | extension VirtualTimeInterval: SchedulerTimeIntervalConvertible { 102 | 103 | public static func seconds(_ s: Int) -> VirtualTimeInterval { 104 | return .init(s) 105 | } 106 | 107 | public static func seconds(_ s: Double) -> VirtualTimeInterval { 108 | return .init(Int(s + 0.5)) 109 | } 110 | 111 | public static func milliseconds(_ ms: Int) -> VirtualTimeInterval { 112 | .seconds(Double(ms) / 1e+3) 113 | } 114 | 115 | public static func microseconds(_ us: Int) -> VirtualTimeInterval { 116 | .seconds(Double(us) / 1e+6) 117 | } 118 | 119 | public static func nanoseconds(_ ns: Int) -> VirtualTimeInterval { 120 | .seconds(Double(ns) / 1e+9) 121 | } 122 | } 123 | 124 | // MARK: - VirtualTime artithmetic 125 | 126 | extension VirtualTimeInterval { 127 | 128 | public static func * (lhs: VirtualTime, rhs: VirtualTimeInterval) -> VirtualTime { 129 | .init(lhs._time * rhs._duration) 130 | } 131 | 132 | public static func *= (lhs: inout VirtualTime, rhs: VirtualTimeInterval) { 133 | lhs._time *= rhs._duration 134 | } 135 | 136 | public static func + (lhs: VirtualTime, rhs: VirtualTimeInterval) -> VirtualTime { 137 | .init(lhs._time + rhs._duration) 138 | } 139 | 140 | public static func - (lhs: VirtualTime, rhs: VirtualTimeInterval) -> VirtualTime { 141 | .init(lhs._time - rhs._duration) 142 | } 143 | 144 | public static func += (lhs: inout VirtualTime, rhs: VirtualTimeInterval) { 145 | lhs._time += rhs._duration 146 | } 147 | 148 | public static func -= (lhs: inout VirtualTime, rhs: VirtualTimeInterval) { 149 | lhs._time -= rhs._duration 150 | } 151 | } 152 | 153 | // MARK: - Int initializer 154 | 155 | extension Int { 156 | init(_ value: VirtualTimeInterval) { 157 | self.init(value._duration) 158 | } 159 | } 160 | 161 | #endif 162 | -------------------------------------------------------------------------------- /Sources/EntwineTest/TestSequence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entwine 3 | // https://github.com/tcldr/Entwine 4 | // 5 | // Copyright © 2019 Tristan Celder. All rights reserved. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | #if canImport(Combine) 26 | 27 | import Entwine 28 | 29 | // MARK: - TestSequence definition 30 | 31 | /// A collection of time-stamped `Signal`s 32 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 33 | public struct TestSequence { 34 | 35 | private var contents: [Element] 36 | 37 | /// Initializes the `TestSequence` with a series of tuples of the format `(VirtualTime, Signal)` 38 | public init(_ elements: S) where S.Element == Element { 39 | self.contents = Array(elements) 40 | } 41 | 42 | /// Initializes an empty `TestSequence` 43 | public init() { 44 | self.contents = [Element]() 45 | } 46 | 47 | 48 | /// Returns a TestSequence containing the results of mapping the given closure over the sequence’s input elements. 49 | /// - Parameter transform: A mapping closure. `transform` accepts an element of this sequence's Input type 50 | /// as its parameter and returns a transformed value of the same or of a different type. 51 | /// - Returns: A TestSequence containing the transformed input elements 52 | public func mapInput(_ transform: (Input) -> T) -> TestSequence { 53 | TestSequence(map { ($0.0, $0.1.mapInput(transform)) }) 54 | } 55 | 56 | /// Returns a TestSequence containing the results of mapping the given closure over the sequence’s completion elements. 57 | /// - Parameter transform: A mapping closure. `transform` accepts an element of this sequence's failure type 58 | /// as its parameter and returns a transformed error of the same or a different type 59 | /// - Returns: A TestSequence containing the transformed completion elements 60 | public func mapFailure(_ transform: (Failure) -> T) -> TestSequence { 61 | TestSequence(map { ($0.0, $0.1.mapFailure(transform)) }) 62 | } 63 | } 64 | 65 | // MARK: - Sequence conformance 66 | 67 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 68 | extension TestSequence: Sequence { 69 | 70 | public typealias Iterator = IndexingIterator<[Element]> 71 | public typealias Element = (VirtualTime, Signal) 72 | 73 | public __consuming func makeIterator() -> IndexingIterator<[(VirtualTime, Signal)]> { 74 | contents.makeIterator() 75 | } 76 | } 77 | 78 | // MARK: - RangeReplaceableCollection conformance 79 | 80 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 81 | extension TestSequence: RangeReplaceableCollection { 82 | 83 | public typealias Index = Int 84 | 85 | public subscript(position: Index) -> Element { 86 | get { contents[position] } 87 | set { contents[position] = newValue } 88 | } 89 | 90 | public var startIndex: Index { 91 | contents.startIndex 92 | } 93 | 94 | public var endIndex: Index { 95 | contents.endIndex 96 | } 97 | 98 | public func index(after i: Index) -> Index { 99 | contents.index(after: i) 100 | } 101 | 102 | public mutating func replaceSubrange(_ subrange: R, with newElements: C) where Element == C.Element, Index == R.Bound { 103 | contents.replaceSubrange(subrange, with: newElements) 104 | } 105 | } 106 | 107 | // MARK: - ExpressibleByArrayLiteral conformance 108 | 109 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 110 | extension TestSequence: ExpressibleByArrayLiteral { 111 | 112 | public typealias ArrayLiteralElement = Element 113 | 114 | public init(arrayLiteral elements: Element...) { 115 | self.init(elements) 116 | } 117 | } 118 | 119 | // MARK: - Equatable conformance 120 | 121 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 122 | extension TestSequence: Equatable where Input: Equatable, Failure: Equatable { 123 | 124 | private var events: [TestEvent>] { 125 | map { TestEvent($0.0, $0.1) } 126 | } 127 | 128 | public static func == (lhs: TestSequence, rhs: TestSequence) -> Bool { 129 | lhs.events == rhs.events 130 | } 131 | } 132 | 133 | #endif 134 | -------------------------------------------------------------------------------- /Sources/EntwineTest/TestablePublisher/TestablePublisher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entwine 3 | // https://github.com/tcldr/Entwine 4 | // 5 | // Copyright © 2019 Tristan Celder. All rights reserved. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | #if canImport(Combine) 26 | 27 | import Combine 28 | import Entwine 29 | import Foundation 30 | 31 | // MARK: - Behavior value definition 32 | 33 | enum TestablePublisherBehavior { case absolute, relative } 34 | 35 | // MARK: - Publisher definition 36 | 37 | /// A `Publisher` that produces the elements provided in a `TestSequence`. 38 | /// 39 | /// Initializable using the factory methods on `TestScheduler` 40 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 41 | public struct TestablePublisher: Publisher { 42 | 43 | private let testScheduler: TestScheduler 44 | private let testSequence: TestSequence 45 | private let behavior: TestablePublisherBehavior 46 | 47 | init(testScheduler: TestScheduler, behavior: TestablePublisherBehavior, testSequence: TestSequence) { 48 | self.testScheduler = testScheduler 49 | self.testSequence = testSequence 50 | self.behavior = behavior 51 | } 52 | 53 | public func receive(subscriber: S) where S.Failure == Failure, S.Input == Output { 54 | subscriber.receive(subscription: 55 | TestablePublisherSubscription( 56 | sink: subscriber, testScheduler: testScheduler, behavior: behavior, testSequence: testSequence)) 57 | } 58 | } 59 | 60 | // MARK: - Subscription definition 61 | 62 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 63 | fileprivate final class TestablePublisherSubscription: Subscription { 64 | 65 | private let linkedList = LinkedList.empty 66 | private var queue: SinkQueue? 67 | private var cancellables = [AnyCancellable]() 68 | 69 | init(sink: Sink, testScheduler: TestScheduler, behavior: TestablePublisherBehavior, testSequence: TestSequence) { 70 | 71 | let queue = SinkQueue(sink: sink) 72 | 73 | testSequence.forEach { (time, signal) in 74 | 75 | guard behavior == .relative || testScheduler.now <= time else { return } 76 | let due = behavior == .relative ? testScheduler.now + time : time 77 | 78 | switch signal { 79 | case .subscription: 80 | assertionFailure("Illegal input. A `.subscription` event scheduled at \(time) will be ignored. Only a Subscriber can initiate a Subscription.") 81 | break 82 | case .input(let value): 83 | let cancellable = testScheduler.schedule(after: due, interval: 0) { 84 | _ = queue.enqueue(value) 85 | } 86 | cancellables.append(AnyCancellable { cancellable.cancel() }) 87 | case .completion(let completion): 88 | let cancellable = testScheduler.schedule(after: due, interval: 0) { 89 | queue.expediteCompletion(completion) 90 | } 91 | cancellables.append(AnyCancellable { cancellable.cancel() }) 92 | } 93 | } 94 | 95 | self.queue = queue 96 | } 97 | 98 | deinit { 99 | cancellables.forEach { $0.cancel() } 100 | } 101 | 102 | func request(_ demand: Subscribers.Demand) { 103 | _ = queue?.requestDemand(demand) 104 | } 105 | 106 | func cancel() { 107 | queue = nil 108 | } 109 | } 110 | 111 | #endif -------------------------------------------------------------------------------- /Sources/EntwineTest/TestableSubscriber/DemandLedger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entwine 3 | // https://github.com/tcldr/Entwine 4 | // 5 | // Copyright © 2019 Tristan Celder. All rights reserved. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | #if canImport(Combine) 26 | 27 | import Combine 28 | 29 | // MARK: - TestSequence definition 30 | 31 | /// A sequence of `Subscribers.Demand` transactions. 32 | /// 33 | /// `DemandLedger`'s can be compared to see if they match expectations. 34 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 35 | public struct DemandLedger where Time.Stride : SchedulerTimeIntervalConvertible { 36 | 37 | /// The kind of transcation for a `DemandLedger` 38 | /// 39 | /// - `.credit(amount:)`: The raise in authorized demand issued by a `Subscriber`. 40 | /// - `.debit(authorized:)`: The consumption of credit by an upstream `Publisher`. The debit is only considered authorised if the overall 41 | /// credit is greater or equal to the total debit over the lifetime of a subscription. A `debit` always has an implicit amount of `1`. 42 | public enum Transaction: Equatable where Time.Stride : SchedulerTimeIntervalConvertible { 43 | case credit(amount: Subscribers.Demand) 44 | case debit(authorized: Bool) 45 | } 46 | 47 | private var contents: [Element] 48 | 49 | 50 | /// Initializes a pre-populated `DemandLedger` 51 | /// - Parameter elements: A sequence of elements of the format `(VirtualTime, Subscribers.Demand, Transaction