├── Sources ├── Entwine │ ├── Common │ │ ├── Utilities │ │ │ └── SinkQueue.swift │ │ └── DataStructures │ │ │ ├── PriorityQueue.swift │ │ │ ├── LinkedListQueue.swift │ │ │ └── LinkedListStack.swift │ ├── Deprecated │ │ ├── Deprecations.swift │ │ └── CancellableBag.swift │ ├── Utilities │ │ └── DeallocToken.swift │ ├── Operators │ │ ├── ShareReplay.swift │ │ ├── ReferenceCounted.swift │ │ ├── Materialize.swift │ │ ├── ReplaySubject.swift │ │ ├── WithLatestFrom.swift │ │ └── Signpost.swift │ ├── Schedulers │ │ └── TrampolineScheduler.swift │ ├── Signal.swift │ └── Publishers │ │ └── Factory.swift ├── EntwineTest │ ├── Common │ │ ├── Utilities │ │ │ └── SinkQueue.swift │ │ └── DataStructures │ │ │ ├── PriorityQueue.swift │ │ │ ├── LinkedListQueue.swift │ │ │ └── LinkedListStack.swift │ ├── Deprecations.swift │ ├── Signal+CustomDebugStringConvertible.swift │ ├── TestEvent.swift │ ├── TestScheduler │ │ ├── VirtualTime.swift │ │ ├── VirtualTimeInterval.swift │ │ └── TestScheduler.swift │ ├── TestablePublisher │ │ └── TestablePublisher.swift │ ├── TestSequence.swift │ └── TestableSubscriber │ │ ├── DemandLedger.swift │ │ └── TestableSubscriber.swift └── Common │ ├── DataStructures │ ├── LinkedListQueue.swift │ ├── LinkedListStack.swift │ └── PriorityQueue.swift │ └── Utilities │ └── SinkQueue.swift ├── Makefile ├── .swiftpm └── xcode │ ├── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── IDETemplateMacros.plist │ └── xcshareddata │ └── xcschemes │ ├── Entwine.xcscheme │ ├── EntwineTest.xcscheme │ └── Entwine-Package.xcscheme ├── Tests ├── EntwineTestTests │ ├── XCTestManifests.swift │ └── TestablePublisherTests.swift └── EntwineTests │ ├── MaterializeTests.swift │ ├── DematerializeTests.swift │ ├── FactoryTests.swift │ ├── TrampolineSchedulerTests.swift │ ├── ReferenceCountedTests.swift │ ├── WithLatestFromTests.swift │ └── ShareReplayTests.swift ├── .github └── workflows │ ├── docs.yml │ ├── ci.yml │ └── ci.awk ├── Package.swift ├── LICENSE ├── .gitignore ├── Assets ├── Entwine │ └── README.md └── EntwineTest │ └── README.md └── README.md /Sources/Entwine/Common/Utilities/SinkQueue.swift: -------------------------------------------------------------------------------- 1 | ../../../Common/Utilities/SinkQueue.swift -------------------------------------------------------------------------------- /Sources/EntwineTest/Common/Utilities/SinkQueue.swift: -------------------------------------------------------------------------------- 1 | ../../../Common/Utilities/SinkQueue.swift -------------------------------------------------------------------------------- /Sources/Entwine/Common/DataStructures/PriorityQueue.swift: -------------------------------------------------------------------------------- 1 | ../../../Common/DataStructures/PriorityQueue.swift -------------------------------------------------------------------------------- /Sources/Entwine/Common/DataStructures/LinkedListQueue.swift: -------------------------------------------------------------------------------- 1 | ../../../Common/DataStructures/LinkedListQueue.swift -------------------------------------------------------------------------------- /Sources/Entwine/Common/DataStructures/LinkedListStack.swift: -------------------------------------------------------------------------------- 1 | ../../../Common/DataStructures/LinkedListStack.swift -------------------------------------------------------------------------------- /Sources/EntwineTest/Common/DataStructures/PriorityQueue.swift: -------------------------------------------------------------------------------- 1 | ../../../Common/DataStructures/PriorityQueue.swift -------------------------------------------------------------------------------- /Sources/EntwineTest/Common/DataStructures/LinkedListQueue.swift: -------------------------------------------------------------------------------- 1 | ../../../Common/DataStructures/LinkedListQueue.swift -------------------------------------------------------------------------------- /Sources/EntwineTest/Common/DataStructures/LinkedListStack.swift: -------------------------------------------------------------------------------- 1 | ../../../Common/DataStructures/LinkedListStack.swift -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | swift build -c release 3 | 4 | test: 5 | swift test \ 6 | --enable-test-discovery \ 7 | --parallel 8 | 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Tests/EntwineTestTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) && canImport(Combine) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(TestSchedulerTests.allTests), 7 | testCase(TestablePublisherTests.allTests), 8 | testCase(TestableSubscriberTests.allTests), 9 | ] 10 | } 11 | #endif 12 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /.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/ -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /Tests/EntwineTestTests/TestablePublisherTests.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 XCTest 28 | import Combine 29 | 30 | @testable import Entwine 31 | @testable import EntwineTest 32 | 33 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 34 | final class TestablePublisherTests: XCTestCase { 35 | 36 | struct Token: Equatable {} 37 | 38 | func testColdObservableProducesExpectedValues() { 39 | 40 | let testScheduler = TestScheduler(initialClock: 0) 41 | 42 | let testablePublisher: TestablePublisher = testScheduler.createRelativeTestablePublisher([ 43 | ( 0, .input(.init())), 44 | (200, .input(.init())), 45 | (400, .input(.init())), 46 | ]) 47 | 48 | let testableSubscriber = testScheduler.start { testablePublisher } 49 | 50 | XCTAssertEqual(testableSubscriber.recordedOutput, [ 51 | (200, .subscription), 52 | (200, .input(.init())), 53 | (400, .input(.init())), 54 | (600, .input(.init())), 55 | ]) 56 | } 57 | 58 | func testHotObservableProducesExpectedValues() { 59 | 60 | let testScheduler = TestScheduler(initialClock: 0) 61 | 62 | let testablePublisher: TestablePublisher = testScheduler.createAbsoluteTestablePublisher([ 63 | ( 0, .input(.init())), 64 | (200, .input(.init())), 65 | (400, .input(.init())), 66 | ]) 67 | 68 | let testableSubscriber = testScheduler.start { testablePublisher } 69 | 70 | XCTAssertEqual(testableSubscriber.recordedOutput, [ 71 | (200, .subscription), 72 | (200, .input(.init())), 73 | (400, .input(.init())), 74 | ]) 75 | } 76 | 77 | static var allTests = [ 78 | ("testColdObservableProducesExpectedValues", testColdObservableProducesExpectedValues), 79 | ("testHotObservableProducesExpectedValues", testHotObservableProducesExpectedValues), 80 | ] 81 | } 82 | 83 | #endif 84 | -------------------------------------------------------------------------------- /Tests/EntwineTests/MaterializeTests.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 XCTest 28 | import Combine 29 | 30 | @testable import Entwine 31 | @testable import EntwineTest 32 | 33 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 34 | final class MaterializeTests: XCTestCase { 35 | 36 | // MARK: - Properties 37 | 38 | private var scheduler: TestScheduler! 39 | 40 | // MARK: - Per test set-up and tear-down 41 | 42 | override func setUp() { 43 | scheduler = TestScheduler(initialClock: 0) 44 | } 45 | 46 | // MARK: - Tests 47 | 48 | func testMaterializesEmpty() { 49 | 50 | let results1 = scheduler.start { Empty().materialize() } 51 | 52 | let expected1: TestSequence, Never> = [ 53 | (200, .subscription), 54 | (200, .input(.subscription)), 55 | (200, .input(.completion(.finished))), 56 | (200, .completion(.finished)), 57 | ] 58 | 59 | XCTAssertEqual(expected1, results1.recordedOutput) 60 | } 61 | 62 | func testMaterializesError() { 63 | 64 | enum MaterializedError: Error { case error } 65 | 66 | let results1 = scheduler.start { Fail(error: .error).materialize() } 67 | 68 | let expected1: TestSequence, Never> = [ 69 | (200, .subscription), 70 | (200, .input(.subscription)), 71 | (200, .input(.completion(.failure(.error)))), 72 | (200, .completion(.finished)), 73 | ] 74 | 75 | XCTAssertEqual(expected1, results1.recordedOutput) 76 | } 77 | 78 | func testMaterializesJust1() { 79 | 80 | let results1 = scheduler.start { Just(1).materialize() } 81 | 82 | let expected1: TestSequence, Never> = [ 83 | (200, .subscription), 84 | (200, .input(.subscription)), 85 | (200, .input(.input(1))), 86 | (200, .input(.completion(.finished))), 87 | (200, .completion(.finished)), 88 | ] 89 | 90 | XCTAssertEqual(expected1, results1.recordedOutput) 91 | } 92 | } 93 | 94 | #endif 95 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /Tests/EntwineTests/DematerializeTests.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 XCTest 28 | import Combine 29 | 30 | @testable import Entwine 31 | @testable import EntwineTest 32 | 33 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 34 | final class DematerializeTests: XCTestCase { 35 | 36 | // MARK: - Properties 37 | 38 | private var scheduler: TestScheduler! 39 | 40 | // MARK: - Per test set-up and tear-down 41 | 42 | override func setUp() { 43 | scheduler = TestScheduler(initialClock: 0) 44 | } 45 | 46 | // MARK: - Tests 47 | 48 | func testDematerializesEmpty() { 49 | 50 | let materializedElements: [Signal] = [ 51 | .subscription, 52 | .completion(.finished) 53 | ] 54 | 55 | let results1 = scheduler.start { 56 | Publishers.Sequence(sequence: materializedElements) 57 | .dematerialize() 58 | .assertNoDematerializationFailure() 59 | } 60 | 61 | XCTAssertEqual(results1.recordedOutput, [ 62 | (200, .subscription), 63 | (200, .completion(.finished)), 64 | ]) 65 | } 66 | 67 | func testDematerializesError() { 68 | 69 | enum MaterializedError: Error { case error } 70 | 71 | let materializedElements: [Signal] = [ 72 | .subscription, 73 | .completion(.failure(.error)) 74 | ] 75 | 76 | let results1 = scheduler.start { 77 | Publishers.Sequence(sequence: materializedElements) 78 | .dematerialize() 79 | .assertNoDematerializationFailure() 80 | } 81 | 82 | XCTAssertEqual(results1.recordedOutput, [ 83 | (200, .subscription), 84 | (200, .completion(.failure(.error))), 85 | ]) 86 | } 87 | 88 | func testDematerializesJust1() { 89 | 90 | let materializedElements: [Signal] = [ 91 | .subscription, 92 | .input(1), 93 | .completion(.finished) 94 | ] 95 | 96 | let results1 = scheduler.start { 97 | Publishers.Sequence(sequence: materializedElements) 98 | .dematerialize() 99 | .assertNoDematerializationFailure() 100 | } 101 | 102 | XCTAssertEqual(results1.recordedOutput, [ 103 | (200, .subscription), 104 | (200, .input(1)), 105 | (200, .completion(.finished)), 106 | ]) 107 | } 108 | } 109 | 110 | #endif 111 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /Tests/EntwineTests/FactoryTests.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 XCTest 28 | import Combine 29 | 30 | @testable import Entwine 31 | @testable import EntwineTest 32 | 33 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 34 | final class FactoryTests: XCTestCase { 35 | 36 | func testCreatesNever() { 37 | 38 | let testScheduler = TestScheduler(initialClock: 0) 39 | 40 | let sut = Publishers.Factory { _ in AnyCancellable { } } 41 | 42 | let testableSubscriber = testScheduler.start { sut } 43 | 44 | XCTAssertEqual(testableSubscriber.recordedOutput, [ 45 | (200, .subscription), 46 | ]) 47 | } 48 | 49 | func testCreatesEmpty() { 50 | 51 | let testScheduler = TestScheduler(initialClock: 0) 52 | 53 | let sut = Publishers.Factory { dispatcher in 54 | dispatcher.forward(completion: .finished) 55 | return AnyCancellable { } 56 | } 57 | 58 | let testableSubscriber = testScheduler.start { sut } 59 | 60 | XCTAssertEqual(testableSubscriber.recordedOutput, [ 61 | (200, .subscription), 62 | (200, .completion(.finished)), 63 | ]) 64 | } 65 | 66 | func testCreatesJust() { 67 | 68 | let testScheduler = TestScheduler(initialClock: 0) 69 | 70 | let sut = Publishers.Factory { dispatcher in 71 | dispatcher.forward(0) 72 | dispatcher.forward(completion: .finished) 73 | return AnyCancellable { } 74 | } 75 | 76 | let testableSubscriber = testScheduler.start { sut } 77 | 78 | XCTAssertEqual(testableSubscriber.recordedOutput, [ 79 | (200, .subscription), 80 | (200, .input(0)), 81 | (200, .completion(.finished)), 82 | ]) 83 | } 84 | 85 | func testFiresAsynchronousEvents() { 86 | 87 | let testScheduler = TestScheduler(initialClock: 0) 88 | 89 | let sut = Publishers.Factory { dispatcher in 90 | 91 | testScheduler.schedule(after: testScheduler.now + 10) { dispatcher.forward(0) } 92 | testScheduler.schedule(after: testScheduler.now + 20) { dispatcher.forward(1) } 93 | testScheduler.schedule(after: testScheduler.now + 30) { dispatcher.forward(2) } 94 | testScheduler.schedule(after: testScheduler.now + 100) { dispatcher.forward(completion: .finished) } 95 | 96 | return AnyCancellable { } 97 | } 98 | 99 | let testableSubscriber = testScheduler.start { sut } 100 | 101 | XCTAssertEqual(testableSubscriber.recordedOutput, [ 102 | (200, .subscription), 103 | (210, .input(0)), 104 | (220, .input(1)), 105 | (230, .input(2)), 106 | (300, .completion(.finished)) 107 | ]) 108 | } 109 | } 110 | 111 | #endif 112 | -------------------------------------------------------------------------------- /Tests/EntwineTests/TrampolineSchedulerTests.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 XCTest 28 | import Combine 29 | 30 | @testable import Entwine 31 | @testable import EntwineTest 32 | 33 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 34 | final class TrampolineSchedulerTests: XCTestCase { 35 | 36 | func testSchedulerTrampolinesActions() { 37 | 38 | let testScheduler = TestScheduler(initialClock: 0) 39 | let subject = TrampolineScheduler.shared 40 | 41 | let publisher1 = PassthroughSubject() 42 | 43 | let results = testScheduler.createTestableSubscriber(String.self, Never.self) 44 | 45 | let action3 = { 46 | publisher1.send("3a") 47 | publisher1.send("3b") 48 | } 49 | 50 | let action2 = { 51 | publisher1.send("2a") 52 | subject.schedule(action3) 53 | publisher1.send("2b") 54 | } 55 | 56 | let action1 = { 57 | publisher1.send("1a") 58 | subject.schedule(action2) 59 | publisher1.send("1b") 60 | } 61 | 62 | testScheduler.schedule(after: 100) { publisher1.subscribe(results) } 63 | testScheduler.schedule(after: 200) { subject.schedule(action1) } 64 | 65 | testScheduler.resume() 66 | 67 | let expected: TestSequence = [ 68 | (100, .subscription), 69 | (200, .input("1a")), 70 | (200, .input("1b")), 71 | (200, .input("2a")), 72 | (200, .input("2b")), 73 | (200, .input("3a")), 74 | (200, .input("3b")), 75 | ] 76 | 77 | XCTAssertEqual(expected, results.recordedOutput) 78 | } 79 | 80 | func testSchedulerPerformsAsFIFOQueue() { 81 | 82 | let testScheduler = TestScheduler(initialClock: 0) 83 | let subject = TrampolineScheduler.shared 84 | 85 | let publisher1 = PassthroughSubject() 86 | 87 | let results = testScheduler.createTestableSubscriber(String.self, Never.self) 88 | 89 | let action = { 90 | publisher1.send("outerAction: A") 91 | subject.schedule { publisher1.send("innerAction1") } 92 | subject.schedule { publisher1.send("innerAction2") } 93 | publisher1.send("outerAction: B") 94 | } 95 | 96 | testScheduler.schedule(after: 100) { publisher1.subscribe(results) } 97 | testScheduler.schedule(after: 200) { subject.schedule(action) } 98 | 99 | testScheduler.resume() 100 | 101 | let expected: TestSequence = [ 102 | (100, .subscription), 103 | (200, .input("outerAction: A")), 104 | (200, .input("outerAction: B")), 105 | (200, .input("innerAction1")), 106 | (200, .input("innerAction2")), 107 | ] 108 | 109 | XCTAssertEqual(expected, results.recordedOutput) 110 | } 111 | } 112 | 113 | #endif 114 | -------------------------------------------------------------------------------- /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/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/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 -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /Tests/EntwineTests/ReferenceCountedTests.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 XCTest 28 | import Combine 29 | 30 | @testable import Entwine 31 | @testable import EntwineTest 32 | 33 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 34 | final class ReferenceCountedTests: XCTestCase { 35 | 36 | // MARK: - Properties 37 | 38 | private var scheduler: TestScheduler! 39 | 40 | // MARK: - Per test set-up and tear-down 41 | 42 | override func setUp() { 43 | scheduler = TestScheduler(initialClock: 0) 44 | } 45 | 46 | // MARK: - Tests 47 | 48 | func testAutoConnectsAndPassesThroughInitialValue() { 49 | 50 | let passthrough = PassthroughSubject() 51 | let subject = passthrough.prepend(-1).share() 52 | 53 | let results1 = scheduler.createTestableSubscriber(Int.self, Never.self) 54 | 55 | scheduler.schedule(after: 100) { subject.subscribe(results1) } 56 | scheduler.schedule(after: 200) { passthrough.send(0) } 57 | scheduler.schedule(after: 210) { passthrough.send(1) } 58 | 59 | scheduler.resume() 60 | 61 | let expected: TestSequence = [ 62 | (100, .subscription), 63 | (100, .input(-1)), 64 | (200, .input( 0)), 65 | (210, .input( 1)), 66 | ] 67 | 68 | XCTAssertEqual(expected, results1.recordedOutput) 69 | } 70 | 71 | func testPassesThroughInitialValueToFirstSubscriberOnly() { 72 | 73 | let passthrough = PassthroughSubject() 74 | let subject = passthrough.prepend(-1).share() 75 | 76 | let results1 = scheduler.createTestableSubscriber(Int.self, Never.self) 77 | let results2 = scheduler.createTestableSubscriber(Int.self, Never.self) 78 | 79 | scheduler.schedule(after: 100) { subject.subscribe(results1) } 80 | scheduler.schedule(after: 110) { subject.subscribe(results2) } 81 | scheduler.schedule(after: 200) { passthrough.send(0) } 82 | scheduler.schedule(after: 210) { passthrough.send(1) } 83 | 84 | scheduler.resume() 85 | 86 | let expected2: TestSequence = [ 87 | (110, .subscription), 88 | (200, .input( 0)), 89 | (210, .input( 1)), 90 | ] 91 | 92 | XCTAssertEqual(expected2, results2.recordedOutput) 93 | } 94 | 95 | func testResetsWhenReferenceCountReachesZero() { 96 | 97 | let passthrough = PassthroughSubject() 98 | let subject = passthrough.prepend(-1).share() 99 | 100 | let results1 = scheduler.createTestableSubscriber(Int.self, Never.self) 101 | let results2 = scheduler.createTestableSubscriber(Int.self, Never.self) 102 | let results3 = scheduler.createTestableSubscriber(Int.self, Never.self) 103 | 104 | scheduler.schedule(after: 100) { subject.subscribe(results1) } 105 | scheduler.schedule(after: 110) { subject.subscribe(results2) } 106 | scheduler.schedule(after: 200) { passthrough.send(0) } 107 | scheduler.schedule(after: 210) { passthrough.send(1) } 108 | scheduler.schedule(after: 300) { results1.cancel() } 109 | scheduler.schedule(after: 310) { results2.cancel() } 110 | scheduler.schedule(after: 400) { subject.subscribe(results3) } 111 | scheduler.schedule(after: 500) { passthrough.send(0) } 112 | scheduler.schedule(after: 510) { passthrough.send(1) } 113 | 114 | scheduler.resume() 115 | 116 | let expected3: TestSequence = [ 117 | (400, .subscription), 118 | (400, .input(-1)), 119 | (500, .input( 0)), 120 | (510, .input( 1)), 121 | ] 122 | 123 | XCTAssertEqual(expected3, results3.recordedOutput) 124 | } 125 | 126 | func testMulticastCreateSubjectCalledWhenSubscriberCountGoesFromZeroToOne() { 127 | 128 | var cancellables = Set() 129 | let factory: () -> PassthroughSubject = { Swift.print("createSubject()"); return PassthroughSubject() } 130 | let sut = Just(1) 131 | sut.multicast(factory).autoconnect().sink { print("A:\($0)") }.store(in: &cancellables) 132 | cancellables = Set() 133 | sut.multicast(factory).autoconnect().sink { print("B:\($0)") }.store(in: &cancellables) 134 | } 135 | } 136 | 137 | #endif 138 | -------------------------------------------------------------------------------- /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/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/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