├── .gitignore ├── .travis.yml ├── LICENSE.md ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── StreamKit │ ├── Bag.swift │ ├── Disposable.swift │ ├── Error.swift │ ├── Event.swift │ ├── Observer.swift │ ├── Promise.swift │ ├── Signal.swift │ └── Source.swift └── Tests └── StreamKitTests └── Tests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/linux,osx,xcode,swift 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | ## Other 17 | *.xcodeproj 18 | #*.xcodeproj/*.plist 19 | *.moved-aside 20 | *.xcuserstate 21 | 22 | ### OSX ### 23 | .DS_Store 24 | .AppleDouble 25 | .LSOverride 26 | 27 | # Icon must end with two \r 28 | Icon 29 | 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | 49 | 50 | ### Xcode ### 51 | # Xcode 52 | # 53 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 54 | 55 | ## Build generated 56 | build/ 57 | DerivedData/ 58 | 59 | ## Various settings 60 | *.pbxuser 61 | !default.pbxuser 62 | *.mode1v3 63 | !default.mode1v3 64 | *.mode2v3 65 | !default.mode2v3 66 | *.perspectivev3 67 | !default.perspectivev3 68 | xcuserdata/ 69 | 70 | ## Other 71 | *.moved-aside 72 | *.xccheckout 73 | *.xcscmblueprint 74 | #*.xcodeproj/*.plist 75 | #*.xcodeproj 76 | 77 | 78 | ### Swift ### 79 | # Xcode 80 | # 81 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 82 | 83 | ## Build generated 84 | build/ 85 | DerivedData/ 86 | 87 | ## Various settings 88 | *.pbxuser 89 | !default.pbxuser 90 | *.mode1v3 91 | !default.mode1v3 92 | *.mode2v3 93 | !default.mode2v3 94 | *.perspectivev3 95 | !default.perspectivev3 96 | xcuserdata/ 97 | 98 | ## Other 99 | *.moved-aside 100 | *.xcuserstate 101 | 102 | ## Obj-C/Swift specific 103 | *.hmap 104 | *.ipa 105 | 106 | ## Playgrounds 107 | timeline.xctimeline 108 | playground.xcworkspace 109 | 110 | # Swift Package Manager 111 | # 112 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 113 | Packages/ 114 | .build/ 115 | 116 | # CocoaPods 117 | # 118 | # We recommend against adding the Pods directory to your .gitignore. However 119 | # you should judge for yourself, the pros and cons are mentioned at: 120 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 121 | # 122 | # Pods/ 123 | 124 | # Carthage 125 | # 126 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 127 | # Carthage/Checkouts 128 | 129 | Carthage/Build 130 | 131 | # fastlane 132 | # 133 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 134 | # screenshots whenever they are needed. 135 | # For more information about the recommended setup visit: 136 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 137 | 138 | fastlane/report.xml 139 | fastlane/Preview.html 140 | fastlane/screenshots 141 | fastlane/test_output 142 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode8 3 | 4 | xcode_project: Reflex.xcodeproj 5 | xcode_scheme: Reflex 6 | xcode_sdk: iphonesimulator10.0 7 | 8 | script: 9 | - set -o pipefail && xcodebuild -project Reflex.xcodeproj -scheme Reflex -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO CODE_SIGNING_REQUIRED=NO -destination "platform=iOS Simulator,name=iPhone 6s" build | xcpretty 10 | - pod lib lint 11 | 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Edge 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "PromiseKit", 6 | "repositoryURL": "https://github.com/mxcl/PromiseKit.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "6bab5e0c7f93947d9c0a7df0937add7454657f2c", 10 | "version": "4.5.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 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: "StreamKit", 8 | products: [ 9 | .library( 10 | name: "StreamKit", 11 | targets: ["StreamKit"] 12 | ), 13 | ], 14 | dependencies: [ 15 | .package(url: "https://github.com/mxcl/PromiseKit.git", from: "4.5.0"), 16 | ], 17 | targets: [ 18 | .target( 19 | name: "StreamKit", 20 | dependencies: ["PromiseKit"] 21 | ), 22 | .testTarget( 23 | name: "StreamKitTests", 24 | dependencies: ["StreamKit"] 25 | ), 26 | ] 27 | ) 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StreamKit 2 | 3 | StreamKit is a very small and compact Functional Reactive Programming library 4 | which is used to implement the Edge event system. 5 | 6 | It is inspired by ReactiveCocoa, but the API has been greatly reduced in scope 7 | and simplified. The StreamKit API is designed so that it can also be used as a 8 | simple callback system, much like Node.js Events. You don't need to be an FRP 9 | wizard to use it. 10 | -------------------------------------------------------------------------------- /Sources/StreamKit/Bag.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bag.swift 3 | // ReactiveSwift 4 | // 5 | // Created by Justin Spahr-Summers on 2014-07-10. 6 | // Copyright (c) 2014 GitHub. All rights reserved. 7 | // 8 | /// A uniquely identifying token for removing a value that was inserted into a 9 | /// Bag. 10 | 11 | public final class RemovalToken { 12 | fileprivate var identifier: UInt? 13 | 14 | fileprivate init(identifier: UInt) { 15 | self.identifier = identifier 16 | } 17 | } 18 | 19 | /// An unordered, non-unique collection of values of type `Element`. 20 | public struct Bag { 21 | fileprivate var elements: [BagElement] = [] 22 | private var currentIdentifier: UInt = 0 23 | 24 | public init() { 25 | } 26 | 27 | /// Insert the given value into `self`, and return a token that can 28 | /// later be passed to `removeValueForToken()`. 29 | /// 30 | /// - parameters: 31 | /// - value: A value that will be inserted. 32 | @discardableResult 33 | public mutating func insert(_ value: Element) -> RemovalToken { 34 | let (nextIdentifier, overflow) = currentIdentifier.addingReportingOverflow(1) 35 | if overflow { 36 | reindex() 37 | } 38 | 39 | let token = RemovalToken(identifier: currentIdentifier) 40 | let element = BagElement(value: value, identifier: currentIdentifier, token: token) 41 | 42 | elements.append(element) 43 | currentIdentifier = nextIdentifier 44 | 45 | return token 46 | } 47 | 48 | /// Remove a value, given the token returned from `insert()`. 49 | /// 50 | /// - note: If the value has already been removed, nothing happens. 51 | /// 52 | /// - parameters: 53 | /// - token: A token returned from a call to `insert()`. 54 | public mutating func remove(using token: RemovalToken) { 55 | if let identifier = token.identifier { 56 | // Removal is more likely for recent objects than old ones. 57 | for i in elements.indices.reversed() { 58 | if elements[i].identifier == identifier { 59 | elements.remove(at: i) 60 | token.identifier = nil 61 | break 62 | } 63 | } 64 | } 65 | } 66 | 67 | /// In the event of an identifier overflow (highly, highly unlikely), reset 68 | /// all current identifiers to reclaim a contiguous set of available 69 | /// identifiers for the future. 70 | private mutating func reindex() { 71 | for i in elements.indices { 72 | currentIdentifier = UInt(i) 73 | 74 | elements[i].identifier = currentIdentifier 75 | elements[i].token.identifier = currentIdentifier 76 | } 77 | } 78 | } 79 | 80 | extension Bag: Collection { 81 | public typealias Index = Array.Index 82 | 83 | public var startIndex: Index { 84 | return elements.startIndex 85 | } 86 | 87 | public var endIndex: Index { 88 | return elements.endIndex 89 | } 90 | 91 | public subscript(index: Index) -> Element { 92 | return elements[index].value 93 | } 94 | 95 | public func index(after i: Index) -> Index { 96 | return i + 1 97 | } 98 | } 99 | 100 | private struct BagElement { 101 | let value: Value 102 | var identifier: UInt 103 | let token: RemovalToken 104 | } 105 | 106 | extension BagElement: CustomStringConvertible { 107 | var description: String { 108 | return "BagElement(\(value))" 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Sources/StreamKit/Disposable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Disposable.swift 3 | // Edge 4 | // 5 | // Created by Tyler Fleming Cloutier on 5/29/16. 6 | // 7 | // 8 | 9 | /// Represents something that can be “disposed,” usually associated with freeing 10 | /// resources or canceling work. 11 | public protocol Disposable { 12 | /// Whether this disposable has been disposed already. 13 | var disposed: Bool { get } 14 | 15 | func dispose() 16 | } 17 | 18 | /// A disposable that will run an action upon disposal. 19 | public final class ActionDisposable: Disposable { 20 | var action: (() -> Void)? 21 | 22 | public var disposed: Bool { 23 | return action == nil 24 | } 25 | 26 | /// Initializes the disposable to run the given action upon disposal. 27 | public init(action: (() -> Void)?) { 28 | self.action = action 29 | } 30 | 31 | public func dispose() { 32 | let oldAction = action 33 | action = nil 34 | oldAction?() 35 | } 36 | } 37 | 38 | /// A disposable that, upon deinitialization, will automatically dispose of 39 | /// another disposable. 40 | public final class ScopedDisposable: Disposable { 41 | 42 | /// The disposable which will be disposed when the ScopedDisposable 43 | /// deinitializes. 44 | public let innerDisposable: Disposable 45 | 46 | public var disposed: Bool { 47 | return innerDisposable.disposed 48 | } 49 | 50 | /// Initializes the receiver to dispose of the argument upon 51 | /// deinitialization. 52 | public init(_ disposable: Disposable) { 53 | innerDisposable = disposable 54 | } 55 | 56 | deinit { 57 | dispose() 58 | } 59 | 60 | public func dispose() { 61 | innerDisposable.dispose() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/StreamKit/Error.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Error.swift 3 | // Reflex 4 | // 5 | // Created by Tyler Fleming Cloutier on 11/15/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public enum NoError: Swift.Error { } 12 | -------------------------------------------------------------------------------- /Sources/StreamKit/Event.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Event.swift 3 | // Edge 4 | // 5 | // Created by Tyler Fleming Cloutier on 5/29/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | /// Represents a signal event. 12 | /// 13 | /// Signals must conform to the grammar: 14 | /// `next* (failed | completed | interrupted)?` 15 | public enum Event { 16 | 17 | /// A value provided by the signal. 18 | case next(Value) 19 | 20 | /// The signal terminated because of an error. No further events will be 21 | /// received. 22 | case failed(Error) 23 | 24 | /// The signal successfully terminated. No further events will be received. 25 | case completed 26 | 27 | /// Event production on the signal has been interrupted. No further events 28 | /// will be received. 29 | case interrupted 30 | 31 | 32 | /// Whether this event indicates signal termination (i.e., that no further 33 | /// events will be received). 34 | public var isTerminating: Bool { 35 | switch self { 36 | case .next: 37 | return false 38 | 39 | case .failed, .completed, .interrupted: 40 | return true 41 | } 42 | } 43 | 44 | /// Lifts the given function over the event's value. 45 | public func map(_ f: (Value) -> U) -> Event { 46 | switch self { 47 | case let .next(value): 48 | return .next(f(value)) 49 | 50 | case let .failed(error): 51 | return .failed(error) 52 | 53 | case .completed: 54 | return .completed 55 | 56 | case .interrupted: 57 | return .interrupted 58 | } 59 | } 60 | 61 | /// Lifts the given function over the event's value. 62 | public func flatMap(_ f: (Value) -> U?) -> Event? { 63 | switch self { 64 | case let .next(value): 65 | if let nextValue = f(value) { 66 | return .next(nextValue) 67 | } 68 | return nil 69 | 70 | case let .failed(error): 71 | return .failed(error) 72 | 73 | case .completed: 74 | return .completed 75 | 76 | case .interrupted: 77 | return .interrupted 78 | } 79 | } 80 | 81 | /// Lifts the given function over the event's error. 82 | public func mapError(_ f: (Error) -> F) -> Event { 83 | switch self { 84 | case let .next(value): 85 | return .next(value) 86 | 87 | case let .failed(error): 88 | return .failed(f(error)) 89 | 90 | case .completed: 91 | return .completed 92 | 93 | case .interrupted: 94 | return .interrupted 95 | } 96 | } 97 | 98 | /// Unwraps the contained `Next` value. 99 | public var value: Value? { 100 | if case let .next(value) = self { 101 | return value 102 | } else { 103 | return nil 104 | } 105 | } 106 | 107 | /// Unwraps the contained `Error` value. 108 | public var error: Error? { 109 | if case let .failed(error) = self { 110 | return error 111 | } else { 112 | return nil 113 | } 114 | } 115 | } 116 | 117 | public func == (lhs: Event, rhs: Event) -> Bool { 118 | switch (lhs, rhs) { 119 | case let (.next(left), .next(right)): 120 | return left == right 121 | 122 | case let (.failed(left), .failed(right)): 123 | return left.localizedDescription == right.localizedDescription 124 | 125 | case (.completed, .completed): 126 | return true 127 | 128 | case (.interrupted, .interrupted): 129 | return true 130 | 131 | default: 132 | return false 133 | } 134 | } 135 | 136 | -------------------------------------------------------------------------------- /Sources/StreamKit/Observer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Observer.swift 3 | // Edge 4 | // 5 | // Created by Tyler Fleming Cloutier on 5/29/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | /// A CiruitBreaker optionally holds a strong reference to either a 13 | /// `Signal` or a `Source` until a terminating event is 14 | /// received. At such time, it delivers the event and then 15 | /// removes its reference. In so doing, it "breaks the circuit" 16 | /// between the signal, the handler, and the input observer. 17 | /// This allows the downstream signal to be released. 18 | class CircuitBreaker { 19 | 20 | private var signal: Signal? = nil 21 | private var source: Source? = nil 22 | fileprivate var action: Observer.Action! = nil 23 | 24 | // This variable is used to maintain memory access exclusivity when 25 | // the signal or source is released causing that signal to send an 26 | // interrupt to its observer. 27 | private var hasTerminated = false 28 | 29 | /// Holds a strong reference to a `Signal` until a 30 | /// terminating event is received. 31 | init(holding signal: Signal?) { 32 | self.signal = signal 33 | self.action = { [weak self] event in 34 | guard let weakSelf = self else { return } 35 | guard !weakSelf.hasTerminated else { return } 36 | weakSelf.signal?.observers.forEach { observer in 37 | observer.send(event) 38 | } 39 | 40 | if event.isTerminating { 41 | // Do not have to dispose of cancel disposable 42 | // since it will be disposed when the signal is deallocated. 43 | weakSelf.hasTerminated = true 44 | weakSelf.signal = nil 45 | } 46 | } 47 | } 48 | 49 | /// Holds a strong reference to a `Source` until a 50 | /// terminating event is received. 51 | init(holding source: Source?) { 52 | self.source = source 53 | self.action = { [weak self] event in 54 | guard let weakSelf = self else { return } 55 | guard !weakSelf.hasTerminated else { return } 56 | self?.source?.observers.forEach { observer in 57 | observer.send(event) 58 | } 59 | 60 | if event.isTerminating { 61 | // Do not have to dispose of cancel disposable 62 | // since it will be disposed when the signal is deallocated. 63 | weakSelf.hasTerminated = true 64 | self?.source = nil 65 | } 66 | } 67 | } 68 | 69 | fileprivate init(with action: @escaping (Event) -> Void) { 70 | self.action = action 71 | } 72 | 73 | } 74 | 75 | 76 | public struct Observer { 77 | 78 | public typealias Action = (Event) -> Void 79 | let breaker: CircuitBreaker 80 | 81 | init(with breaker: CircuitBreaker) { 82 | self.breaker = breaker 83 | } 84 | 85 | public init(_ action: @escaping Action) { 86 | self.breaker = CircuitBreaker(with: action) 87 | } 88 | 89 | /// Creates an Observer with an action which calls each of the provided 90 | /// callbacks 91 | public init( 92 | failed: ((Error) -> Void)? = nil, 93 | completed: (() -> Void)? = nil, 94 | interrupted: (() -> Void)? = nil, 95 | next: ((Value) -> Void)? = nil) 96 | { 97 | self.init { event in 98 | switch event { 99 | case let .next(value): 100 | next?(value) 101 | 102 | case let .failed(error): 103 | failed?(error) 104 | 105 | case .completed: 106 | completed?() 107 | 108 | case .interrupted: 109 | interrupted?() 110 | } 111 | } 112 | } 113 | 114 | /// Puts any `Event` into the the given observer. 115 | public func send(_ event: Event) { 116 | breaker.action(event) 117 | } 118 | 119 | /// Puts a `Next` event into the given observer. 120 | public func sendNext(_ value: Value) { 121 | send(.next(value)) 122 | } 123 | 124 | /// Puts an `Failed` event into the given observer. 125 | public func sendFailed(_ error: Error) { 126 | send(.failed(error)) 127 | } 128 | 129 | /// Puts a `Completed` event into the given observer. 130 | public func sendCompleted() { 131 | send(.completed) 132 | } 133 | 134 | /// Puts a `Interrupted` event into the given observer. 135 | public func sendInterrupted() { 136 | send(.interrupted) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Sources/StreamKit/Promise.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Promise.swift 3 | // StreamKit 4 | // 5 | // Created by Tyler Fleming Cloutier on 11/20/17. 6 | // 7 | 8 | import Foundation 9 | import PromiseKit 10 | 11 | extension Promise { 12 | 13 | public func asSignal() -> Signal { 14 | // TODO: If the promise is already resolved the value will 15 | // be sent to the signals before there are any observers added. 16 | return Signal { observer in 17 | self.then { value -> () in 18 | observer.sendNext(value) 19 | observer.sendCompleted() 20 | }.catch { error in 21 | observer.sendFailed(error) 22 | } 23 | return nil 24 | } 25 | } 26 | 27 | } 28 | 29 | extension SignalType { 30 | 31 | public func asPromise() -> Promise<[Value]> { 32 | var values: [Value] = [] 33 | return Promise { resolve, reject in 34 | self.onNext { 35 | values.append($0) 36 | } 37 | self.onCompleted { 38 | resolve(values) 39 | } 40 | self.onFailed { 41 | reject($0) 42 | } 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /Sources/StreamKit/Signal.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Signal.swift 3 | // Edge 4 | // 5 | // Created by Tyler Fleming Cloutier on 5/29/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public final class Signal: SignalType, InternalSignalType, SpecialSignalGenerator { 12 | 13 | internal var observers = Bag>() 14 | 15 | private var handlerDisposable: Disposable? 16 | 17 | public var cancelDisposable: Disposable? 18 | 19 | public var signal: Signal { 20 | return self 21 | } 22 | 23 | /// Initializes a Signal that will immediately invoke the given generator, 24 | /// then forward events sent to the given observer. 25 | /// 26 | /// The disposable returned from the closure will be automatically disposed 27 | /// if a terminating event is sent to the observer. The Signal itself will 28 | /// remain alive until the observer is released. This is because the observer 29 | /// captures a self reference. 30 | public init(_ startHandler: @escaping (Observer) -> Disposable?) { 31 | 32 | let observer = Observer(with: CircuitBreaker(holding: self)) 33 | let handlerDisposable = startHandler(observer) 34 | 35 | // The cancel disposable should send interrupted and then dispose of the 36 | // disposable produced by the startHandler. 37 | cancelDisposable = ActionDisposable { 38 | observer.sendInterrupted() 39 | handlerDisposable?.dispose() 40 | } 41 | } 42 | 43 | deinit { 44 | cancelDisposable?.dispose() 45 | } 46 | 47 | /// Creates a Signal that will be controlled by sending events to the returned 48 | /// observer. 49 | /// 50 | /// The Signal will remain alive until a terminating event is sent to the 51 | /// observer. 52 | public static func pipe() -> (Signal, Observer) { 53 | var observer: Observer! 54 | let signal = self.init { innerObserver in 55 | observer = innerObserver 56 | return nil 57 | } 58 | return (signal, observer) 59 | } 60 | } 61 | 62 | extension Signal: CustomDebugStringConvertible { 63 | 64 | public var debugDescription: String { 65 | let obs = Array(self.observers.map { String(describing: $0) }) 66 | return "Signal[\(obs.joined(separator: ", "))]" 67 | } 68 | 69 | } 70 | 71 | public protocol SpecialSignalGenerator { 72 | 73 | /// The type of values being sent on the signal. 74 | associatedtype Value 75 | 76 | init(_ generator: @escaping (Observer) -> Disposable?) 77 | 78 | } 79 | 80 | public extension SpecialSignalGenerator { 81 | 82 | /// Creates a Signal that will immediately send one value 83 | /// then complete. 84 | public init(value: Value) { 85 | self.init { observer in 86 | observer.sendNext(value) 87 | observer.sendCompleted() 88 | return nil 89 | } 90 | } 91 | 92 | /// Creates a Signal that will immediately fail with the 93 | /// given error. 94 | public init(error: Error) { 95 | self.init { observer in 96 | observer.sendFailed(error) 97 | return nil 98 | } 99 | } 100 | 101 | /// Creates a Signal that will immediately send the values 102 | /// from the given sequence, then complete. 103 | public init(values: S) where S.Iterator.Element == Value { 104 | self.init { observer in 105 | var disposed = false 106 | for value in values { 107 | observer.sendNext(value) 108 | 109 | if disposed { 110 | break 111 | } 112 | } 113 | observer.sendCompleted() 114 | 115 | return ActionDisposable { 116 | disposed = true 117 | } 118 | } 119 | } 120 | 121 | /// Creates a Signal that will immediately send the values 122 | /// from the given sequence, then complete. 123 | public init(values: Value...) { 124 | self.init(values: values) 125 | } 126 | 127 | /// A Signal that will immediately complete without sending 128 | /// any values. 129 | public static var empty: Self { 130 | return self.init { observer in 131 | observer.sendCompleted() 132 | return nil 133 | } 134 | } 135 | 136 | /// A Signal that never sends any events to its observers. 137 | public static var never: Self { 138 | return self.init { _ in return nil } 139 | } 140 | 141 | } 142 | 143 | /// An internal protocol for adding methods that require access to the observers 144 | /// of the signal. 145 | internal protocol InternalSignalType: SignalType { 146 | 147 | var observers: Bag> { get } 148 | 149 | } 150 | 151 | /// Note that this type is not parameterized by an Error type which is in line 152 | /// with the Swift error handling model. 153 | /// A good reference for a discussion of the pros and cons is here: 154 | /// https://github.com/ReactiveX/RxSwift/issues/650 155 | public protocol SignalType { 156 | 157 | /// The type of values being sent on the signal. 158 | associatedtype Value 159 | 160 | /// The exposed raw signal that underlies the `SignalType`. 161 | var signal: Signal { get } 162 | 163 | var cancelDisposable: Disposable? { get } 164 | 165 | } 166 | 167 | public extension SignalType { 168 | 169 | /// Adds an observer to the `Signal` which observes any future events from the `Signal`. 170 | /// If the `Signal` has already terminated, the observer will immediately receive an 171 | /// `Interrupted` event. 172 | /// 173 | /// Returns a Disposable which can be used to disconnect the observer. Disposing 174 | /// of the Disposable will have no effect on the `Signal` itself. 175 | @discardableResult 176 | public func add(observer: Observer) -> Disposable? { 177 | let token = signal.observers.insert(observer) 178 | return ActionDisposable { 179 | self.signal.observers.remove(using: token) 180 | } 181 | 182 | } 183 | 184 | /// Convenience override for add(observer:) to allow trailing-closure style 185 | /// invocations. 186 | @discardableResult 187 | public func on(action: @escaping Observer.Action) -> Disposable? { 188 | return self.add(observer: Observer(action)) 189 | } 190 | 191 | /// Observes the Signal by invoking the given callback when `next` events are 192 | /// received. 193 | /// 194 | /// Returns a Disposable which can be used to stop the invocation of the 195 | /// callbacks. Disposing of the Disposable will have no effect on the Signal 196 | /// itself. 197 | @discardableResult 198 | public func onNext(next: @escaping (Value) -> Void) -> Disposable? { 199 | return self.add(observer: Observer(next: next)) 200 | } 201 | 202 | /// Observes the Signal by invoking the given callback when a `completed` event is 203 | /// received. 204 | /// 205 | /// Returns a Disposable which can be used to stop the invocation of the 206 | /// callback. Disposing of the Disposable will have no effect on the Signal 207 | /// itself. 208 | @discardableResult 209 | public func onCompleted(completed: @escaping () -> Void) -> Disposable? { 210 | return self.add(observer: Observer(completed: completed)) 211 | } 212 | 213 | /// Observes the Signal by invoking the given callback when a `failed` event is 214 | /// received. 215 | /// 216 | /// Returns a Disposable which can be used to stop the invocation of the 217 | /// callback. Disposing of the Disposable will have no effect on the Signal 218 | /// itself. 219 | @discardableResult 220 | public func onFailed(error: @escaping (Error) -> Void) -> Disposable? { 221 | return self.add(observer: Observer(failed: error)) 222 | } 223 | 224 | /// Observes the Signal by invoking the given callback when an `interrupted` event is 225 | /// received. If the Signal has already terminated, the callback will be invoked 226 | /// immediately. 227 | /// 228 | /// Returns a Disposable which can be used to stop the invocation of the 229 | /// callback. Disposing of the Disposable will have no effect on the Signal 230 | /// itself. 231 | @discardableResult 232 | public func onInterrupted(interrupted: @escaping () -> Void) -> Disposable? { 233 | return self.add(observer: Observer(interrupted: interrupted)) 234 | } 235 | 236 | } 237 | 238 | public extension SignalType { 239 | 240 | public var identity: Signal { 241 | return self.map { $0 } 242 | } 243 | 244 | /// Maps each value in the signal to a new value. 245 | public func map(_ transform: @escaping (Value) -> U) -> Signal { 246 | return Signal { observer in 247 | return self.on { event -> Void in 248 | observer.send(event.map(transform)) 249 | } 250 | } 251 | } 252 | 253 | /// Maps errors in the signal to a new error. 254 | public func mapError(_ transform: @escaping (Error) -> F) -> Signal { 255 | return Signal { observer in 256 | return self.on { event -> Void in 257 | observer.send(event.mapError(transform)) 258 | } 259 | } 260 | } 261 | 262 | /// Preserves only the values of the signal that pass the given predicate. 263 | public func filter(_ predicate: @escaping (Value) -> Bool) -> Signal { 264 | return Signal { observer in 265 | return self.on { (event: Event) -> Void in 266 | guard let value = event.value else { 267 | observer.send(event) 268 | return 269 | } 270 | 271 | if predicate(value) { 272 | observer.sendNext(value) 273 | } 274 | } 275 | } 276 | } 277 | 278 | /// Splits the signal into two signals. The first signal in the tuple matches the 279 | /// predicate, the second signal does not match the predicate 280 | public func partition(_ predicate: @escaping (Value) -> Bool) -> (Signal, Signal) { 281 | let (left, leftObserver) = Signal.pipe() 282 | let (right, rightObserver) = Signal.pipe() 283 | self.on { (event: Event) -> Void in 284 | guard let value = event.value else { 285 | leftObserver.send(event) 286 | rightObserver.send(event) 287 | return 288 | } 289 | 290 | if predicate(value) { 291 | leftObserver.sendNext(value) 292 | } else { 293 | rightObserver.sendNext(value) 294 | } 295 | } 296 | return (left, right) 297 | } 298 | 299 | /// Aggregate values into a single combined value. Mirrors the Swift Collection 300 | public func reduce(initial: T, _ combine: @escaping (T, Value) -> T) -> Signal { 301 | return Signal { observer in 302 | var accumulator = initial 303 | return self.on { event in 304 | observer.send(event.map { value in 305 | accumulator = combine(accumulator, value) 306 | return accumulator 307 | }) 308 | } 309 | } 310 | } 311 | 312 | public func flatMap(_ transform: @escaping (Value) -> U?) -> Signal { 313 | return Signal { observer in 314 | return self.on { event -> Void in 315 | if let e = event.flatMap(transform) { 316 | observer.send(e) 317 | } 318 | } 319 | } 320 | } 321 | 322 | public func flatMap(_ transform: @escaping (Value) -> Signal) -> Signal { 323 | return map(transform).joined() 324 | } 325 | 326 | } 327 | 328 | extension SignalType where Value: SignalType { 329 | 330 | /// Listens to every `Source` produced from the current `Signal` 331 | /// Starts each `Source` and forwards on all values and errors onto 332 | /// the `Signal` which is returned. In this way it joins each of the 333 | /// `Source`s into a single `Signal`. 334 | /// 335 | /// The joined `Signal` completes when the current `Signal` and all of 336 | /// its produced `Source`s complete. 337 | /// 338 | /// Note: This means that each `Source` will be started as it is received. 339 | public func joined() -> Signal { 340 | // Start the number in flight at 1 for `self` 341 | 342 | return Signal { observer in 343 | 344 | var numberInFlight = 1 345 | var disposables = [Disposable]() 346 | func decrementInFlight() { 347 | numberInFlight -= 1 348 | if numberInFlight == 0 { 349 | observer.sendCompleted() 350 | } 351 | } 352 | 353 | func incrementInFlight() { 354 | numberInFlight += 1 355 | } 356 | 357 | self.on { event in 358 | 359 | switch event { 360 | case .next(let source): 361 | incrementInFlight() 362 | source.on { event in 363 | switch event { 364 | case .completed, .interrupted: 365 | decrementInFlight() 366 | 367 | case .next, .failed: 368 | observer.send(event) 369 | } 370 | } 371 | source.cancelDisposable.map { disposables.append($0) } 372 | (source as? Source)?.start() 373 | 374 | case .failed(let error): 375 | observer.sendFailed(error) 376 | 377 | case .completed: 378 | decrementInFlight() 379 | 380 | case .interrupted: 381 | observer.sendInterrupted() 382 | } 383 | 384 | } 385 | 386 | return ActionDisposable { 387 | for disposable in disposables { 388 | disposable.dispose() 389 | } 390 | } 391 | 392 | } 393 | } 394 | } 395 | 396 | -------------------------------------------------------------------------------- /Sources/StreamKit/Source.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignalProducer.swift 3 | // Edge 4 | // 5 | // Created by Tyler Fleming Cloutier on 5/29/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public final class Source: SourceType, InternalSignalType, SpecialSignalGenerator { 12 | 13 | public typealias Value = V 14 | 15 | internal var observers = Bag>() 16 | 17 | public var source: Source { 18 | return self 19 | } 20 | 21 | internal let startHandler: (Observer) -> Disposable? 22 | 23 | public var cancelDisposable: Disposable? 24 | 25 | private var started: Bool { 26 | if let disposable = cancelDisposable { 27 | return !disposable.disposed 28 | } 29 | return false 30 | } 31 | 32 | /// Initializes a Source that will invoke the given closure at the 33 | /// invocation of `start()`. 34 | /// 35 | /// The events that the closure puts into the given observer will become 36 | /// the events sent to this `Source`. 37 | /// 38 | /// In order to stop or dispose of the signal, invoke `stop()`. Calling this method 39 | /// will dispose of the disposable returned by the given closure. 40 | /// 41 | /// Invoking `start()` will have no effect until the signal is stopped. After 42 | /// `stop()` is called this process may be repeated. 43 | public init(_ startHandler: @escaping (Observer) -> Disposable?) { 44 | self.startHandler = startHandler 45 | } 46 | 47 | /// Creates a Signal from the producer, then attaches the given observer to 48 | /// the Signal as an observer. 49 | /// 50 | /// Returns a Disposable which can be used to interrupt the work associated 51 | /// with the signal and immediately send an `Interrupted` event. 52 | public func start() { 53 | if !started { 54 | let observer = Observer(with: CircuitBreaker(holding: self)) 55 | let handlerDisposable = startHandler(observer) 56 | 57 | // The cancel disposable should send interrupted and then dispose of the 58 | // disposable produced by the startHandler. 59 | cancelDisposable = ActionDisposable { 60 | observer.sendInterrupted() 61 | handlerDisposable?.dispose() 62 | } 63 | } 64 | } 65 | 66 | public func stop() { 67 | cancelDisposable?.dispose() 68 | } 69 | 70 | deinit { 71 | self.stop() 72 | } 73 | 74 | } 75 | 76 | extension Source: CustomDebugStringConvertible { 77 | 78 | public var debugDescription: String { 79 | let obs = Array(self.observers.map { String(describing: $0) }) 80 | return "Source[\(obs.joined(separator: ", "))]" 81 | } 82 | 83 | } 84 | 85 | public protocol SourceType: SignalType { 86 | 87 | /// The exposed raw signal that underlies the SourceType 88 | var source: Source { get } 89 | 90 | /// Invokes the closure provided upon initialization, and passes in a newly 91 | /// created observer to which events can be sent. 92 | func start() 93 | 94 | /// Stops the `Source` by sending an interrupt to all of it's 95 | /// observers and then invoking the disposable returned by the closure 96 | /// that was provided upon initialization. 97 | func stop() 98 | 99 | } 100 | 101 | public extension SourceType { 102 | 103 | public var signal: Signal { 104 | return Signal { observer in 105 | self.source.add(observer: observer) 106 | } 107 | } 108 | 109 | /// Invokes the closure provided upon initialization, and passes in a newly 110 | /// created observer to which events can be sent. 111 | func start() { 112 | source.start() 113 | } 114 | 115 | /// Stops the `Source` by sending an interrupt to all of it's 116 | /// observers and then invoking the disposable returned by the closure 117 | /// that was provided upon initialization. 118 | func stop() { 119 | source.stop() 120 | } 121 | 122 | } 123 | 124 | public extension SourceType { 125 | 126 | /// Adds an observer to the `Source` which observes any future events from the 127 | /// `Source`. If the `Signal` has already terminated, the observer will immediately 128 | /// receive an `Interrupted` event. 129 | /// 130 | /// Returns a Disposable which can be used to disconnect the observer. Disposing 131 | /// of the Disposable will have no effect on the Signal itself. 132 | @discardableResult 133 | public func add(observer: Observer) -> Disposable? { 134 | let token = source.observers.insert(observer) 135 | return ActionDisposable { [weak source = source] in 136 | source?.observers.remove(using: token) 137 | } 138 | } 139 | 140 | /// Creates a `Source`, adds exactly one observer, and then immediately 141 | /// invokes start on the `Source`. 142 | /// 143 | /// Returns a Disposable which can be used to dispose of the added observer. 144 | @discardableResult 145 | public func start(with observer: Observer) -> Disposable? { 146 | let disposable = source.add(observer: observer) 147 | source.start() 148 | return disposable 149 | } 150 | 151 | /// Creates a `Source`, adds exactly one observer, and then immediately 152 | /// invokes start on the `Source`. 153 | /// 154 | /// Returns a Disposable which can be used to dispose of the added observer. 155 | @discardableResult 156 | public func start(_ observerAction: @escaping Observer.Action) -> Disposable? { 157 | return start(with: Observer(observerAction)) 158 | } 159 | 160 | /// Creates a `Source`, adds exactly one observer for next, and then immediately 161 | /// invokes start on the `Source`. 162 | /// 163 | /// Returns a Disposable which can be used to dispose of the added observer. 164 | @discardableResult 165 | public func startWithNext(next: @escaping (Value) -> Void) -> Disposable? { 166 | return start(with: Observer(next: next)) 167 | } 168 | 169 | /// Creates a `Source`, adds exactly one observer for completed events, and then 170 | /// immediately invokes start on the `Source`. 171 | /// 172 | /// Returns a Disposable which can be used to dispose of the added observer. 173 | @discardableResult 174 | public func startWithCompleted(completed: @escaping () -> Void) -> Disposable? { 175 | return start(with: Observer(completed: completed)) 176 | } 177 | 178 | /// Creates a `Source`, adds exactly one observer for errors, and then 179 | /// immediately invokes start on the `Source`. 180 | /// 181 | /// Returns a Disposable which can be used to dispose of the added observer. 182 | @discardableResult 183 | public func startWithFailed(failed: @escaping (Error) -> Void) -> Disposable? { 184 | return start(with: Observer(failed: failed)) 185 | } 186 | 187 | /// Creates a `Source`, adds exactly one observer for interrupts, and then 188 | /// immediately invokes start on the `Source`. 189 | /// 190 | /// Returns a Disposable which can be used to dispose of the added observer. 191 | @discardableResult 192 | public func startWithInterrupted(interrupted: @escaping () -> Void) -> Disposable? { 193 | return start(with: Observer(interrupted: interrupted)) 194 | } 195 | 196 | } 197 | 198 | public extension SourceType { 199 | 200 | /// Creates a new `Source` which will apply a unary operator directly to events 201 | /// produced by the `startHandler`. 202 | /// 203 | /// The new `Source` is in no way related to the source `Source` except 204 | /// that they share a reference to the same `startHandler`. 205 | public func lift(_ transform: @escaping (Signal) -> Signal) -> Source { 206 | return Source { observer in 207 | let (pipeSignal, pipeObserver) = Signal.pipe() 208 | transform(pipeSignal).add(observer: observer) 209 | return self.source.startHandler(pipeObserver) 210 | } 211 | } 212 | 213 | public func lift(_ transform: @escaping (Signal) -> (Signal, Signal)) 214 | -> (Source, Source) 215 | { 216 | let (pipeSignal, pipeObserver) = Signal.pipe() 217 | let (left, right) = transform(pipeSignal) 218 | let sourceLeft = Source { observer in 219 | left.add(observer: observer) 220 | return self.source.startHandler(pipeObserver) 221 | } 222 | let sourceRight = Source { observer in 223 | right.add(observer: observer) 224 | return self.source.startHandler(pipeObserver) 225 | } 226 | return (sourceLeft, sourceRight) 227 | } 228 | 229 | public var identity: Source { 230 | return lift { $0.identity } 231 | } 232 | 233 | /// Maps each value in the signal to a new value. 234 | public func map(_ transform: @escaping (Value) -> U) -> Source { 235 | return lift { $0.map(transform) } 236 | } 237 | 238 | /// Maps errors in the signal to a new error. 239 | public func mapError(_ transform: @escaping (Error) -> F) -> Source { 240 | return lift { $0.mapError(transform) } 241 | } 242 | 243 | /// Preserves only the values of the signal that pass the given predicate. 244 | public func filter(_ predicate: @escaping (Value) -> Bool) -> Source { 245 | return lift { $0.filter(predicate) } 246 | } 247 | 248 | /// Splits the signal into two signals. The first signal in the tuple matches the 249 | /// predicate, the second signal does not match the predicate 250 | public func partition(_ predicate: @escaping (Value) -> Bool) 251 | -> (Source, Source) { 252 | return lift { $0.partition(predicate) } 253 | } 254 | 255 | /// Aggregate values into a single combined value. Mirrors the Swift Collection 256 | public func reduce(initial: T, _ combine: @escaping (T, Value) -> T) -> Source { 257 | return lift { $0.reduce(initial: initial, combine) } 258 | } 259 | 260 | public func flatMap(_ transform: @escaping (Value) -> U?) -> Source { 261 | return lift { $0.flatMap(transform) } 262 | } 263 | 264 | public func flatMap(_ transform: @escaping (Value) -> Source) -> Source { 265 | return lift { $0.map(transform).joined() } 266 | } 267 | 268 | 269 | } 270 | 271 | -------------------------------------------------------------------------------- /Tests/StreamKitTests/Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import StreamKit 2 | 3 | import XCTest 4 | 5 | class TestBasic: XCTestCase { 6 | 7 | func testSignalPipe() { 8 | 9 | let (signal, observer) = Signal.pipe() 10 | var nextIndex = 0 11 | let nextVals = [0, 3, 5, 2, -3] 12 | var didComplete = false 13 | 14 | signal.onNext { next in 15 | XCTAssert(next == nextVals[nextIndex], "Value \(next) is incorrect.") 16 | nextIndex += 1 17 | } 18 | 19 | signal.onCompleted { 20 | XCTAssert(nextIndex == nextVals.count, "Completed incorrectly.") 21 | didComplete = true 22 | } 23 | 24 | signal.onFailed { error in 25 | XCTFail(error.localizedDescription) 26 | } 27 | 28 | for val in nextVals { 29 | observer.sendNext(val) 30 | } 31 | observer.sendCompleted() 32 | 33 | XCTAssert(didComplete, "Signal never completed.") 34 | 35 | } 36 | 37 | } 38 | --------------------------------------------------------------------------------