├── .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 | 
5 | [](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