├── .gitignore ├── .gitmodules ├── .travis.yml ├── Jetstream.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── Jetstream.xcscmblueprint └── xcshareddata │ └── xcschemes │ └── Jetstream.xcscheme ├── Jetstream ├── ChangeSet.swift ├── ChangeSetQueue.swift ├── Client.swift ├── Constants.swift ├── Constraint.swift ├── Functions.swift ├── Info.plist ├── Jetstream.h ├── Logging.swift ├── ModelObject.swift ├── ModelValue.swift ├── NetworkMessage.swift ├── PingMessage.swift ├── ReplyMessage.swift ├── Scope.swift ├── ScopeFetchMessage.swift ├── ScopeFetchReplyMessage.swift ├── ScopeStateMessage.swift ├── ScopeSyncMessage.swift ├── ScopeSyncReplyMessage.swift ├── Session.swift ├── SessionCreateMessage.swift ├── SessionCreateReplyMessage.swift ├── SyncFragment.swift ├── Transport.swift └── WebsocketTransportAdapter.swift ├── JetstreamDemos ├── JetstreamDemos.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── JetstreamDemos.xcscheme ├── JetstreamDemos │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── LaunchScreen.xib │ │ └── Main.storyboard │ ├── Canvas.swift │ ├── DemosListViewController.swift │ ├── Extensions.swift │ ├── Images.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Info.plist │ ├── Shape.swift │ ├── ShapeView.swift │ └── ShapesDemoViewController.swift └── JetstreamDemosTests │ ├── Info.plist │ └── JetstreamDemosTests.swift ├── JetstreamTests ├── ChangeSetQueueTests.swift ├── ChangeSetTests.swift ├── ClientTests.swift ├── ConstraintTests.swift ├── DependencyTests.swift ├── ImmediatePropertyListeners.swift ├── Info.plist ├── ModelObjectTests.swift ├── ModelValueTests.swift ├── PropertyListenerTests.swift ├── Resources │ └── test.jpg ├── ScopePauseTests.swift ├── ScopeTests.swift ├── StateMessageTests.swift ├── SyncFragmentTests.swift ├── TestHelpers.swift ├── TestModels.swift ├── TestTransportAdapter.swift ├── TransactionTests.swift └── TreeChangeTests.swift ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | *.xccheckout 14 | *.moved-aside 15 | DerivedData 16 | *.hmap 17 | *.ipa 18 | *.xcuserstate 19 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Jetstream/Signals"] 2 | path = Jetstream/Signals 3 | url=https://github.com/artman/Signals.git 4 | [submodule "Jetstream/starscream"] 5 | path = Jetstream/starscream 6 | url=https://github.com/daltoniam/starscream.git 7 | [submodule "Jetstream/Starscream"] 8 | path = Jetstream/Starscream 9 | url = https://github.com/daltoniam/Starscream.git 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode7 3 | env: 4 | global: 5 | - LC_CTYPE=en_US.UTF-8 6 | - LANG=en_US.UTF-8 7 | matrix: 8 | - DESTINATION="OS=8.1,name=iPhone 4S" SCHEME="Jetstream" SDK=iphonesimulator9.0 9 | - DESTINATION="OS=8.2,name=iPhone 5" SCHEME="Jetstream" SDK=iphonesimulator9.0 10 | - DESTINATION="OS=8.3,name=iPhone 5S" SCHEME="Jetstream" SDK=iphonesimulator9.0 11 | - DESTINATION="OS=8.4,name=iPhone 6" SCHEME="Jetstream" SDK=iphonesimulator9.0 12 | - DESTINATION="OS=9.0,name=iPhone 6 Plus" SCHEME="Jetstream" SDK=iphonesimulator9.0 13 | script: 14 | - set -o pipefail 15 | - xcodebuild -version 16 | - xcodebuild -project Jetstream.xcodeproj -scheme "$SCHEME" -sdk "$SDK" -destination "$DESTINATION" 17 | -configuration Debug ONLY_ACTIVE_ARCH=NO test | xcpretty -c 18 | notifications: 19 | email: false 20 | -------------------------------------------------------------------------------- /Jetstream.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Jetstream.xcodeproj/project.xcworkspace/xcshareddata/Jetstream.xcscmblueprint: -------------------------------------------------------------------------------- 1 | { 2 | "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "499A46737AADA8ADEDD865B9136486BDAC17B88E", 3 | "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : { 4 | 5 | }, 6 | "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : { 7 | "B0A9AE164019F593FB4F8C022EFEE89AEB1CB357" : 0, 8 | "499A46737AADA8ADEDD865B9136486BDAC17B88E" : 0, 9 | "C86D95FCAEB1FEA0694B5D4AC7241D7E5F42F31D" : 0 10 | }, 11 | "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "2D7C7C02-3538-4805-885F-10F89FAD08C6", 12 | "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : { 13 | "B0A9AE164019F593FB4F8C022EFEE89AEB1CB357" : "jetstream-iosJetstream\/Signals", 14 | "499A46737AADA8ADEDD865B9136486BDAC17B88E" : "jetstream-ios", 15 | "C86D95FCAEB1FEA0694B5D4AC7241D7E5F42F31D" : "jetstream-iosJetstream\/Starscream" 16 | }, 17 | "DVTSourceControlWorkspaceBlueprintNameKey" : "Jetstream", 18 | "DVTSourceControlWorkspaceBlueprintVersion" : 204, 19 | "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "Jetstream.xcodeproj", 20 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [ 21 | { 22 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:uber\/jetstream-ios.git", 23 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 24 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "499A46737AADA8ADEDD865B9136486BDAC17B88E" 25 | }, 26 | { 27 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/artman\/Signals.git", 28 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 29 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "B0A9AE164019F593FB4F8C022EFEE89AEB1CB357" 30 | }, 31 | { 32 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/daltoniam\/Starscream.git", 33 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 34 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "C86D95FCAEB1FEA0694B5D4AC7241D7E5F42F31D" 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /Jetstream.xcodeproj/xcshareddata/xcschemes/Jetstream.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | 66 | 67 | 68 | 78 | 79 | 85 | 86 | 87 | 88 | 89 | 90 | 96 | 97 | 103 | 104 | 105 | 106 | 108 | 109 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /Jetstream/ChangeSetQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChangeSetQueue.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import Foundation 26 | 27 | public class ChangeSetQueue { 28 | // A signal that fires whenever a change set is added to the queue 29 | public let onChangeSetAdded = Signal() 30 | 31 | // A signal that fires whenever the state of a change set in the queue changes 32 | public let onChangeSetStateChanged = Signal<(ChangeSet, ChangeSetState)>() 33 | 34 | // A signal that fires whenever a change set has been removed from the queue 35 | public let onChangeSetRemoved = Signal() 36 | 37 | // The number of change sets in the queue 38 | public var count: Int { 39 | return changeSets.count 40 | } 41 | 42 | var changeSets = [ChangeSet]() 43 | 44 | // MARK: - Private interface 45 | func addChangeSet(changeSet: ChangeSet) { 46 | assert(changeSets.indexOf(changeSet) == nil, "ChangeSet already in queue") 47 | 48 | changeSet.changeSetQueue = self 49 | changeSets.append(changeSet) 50 | onChangeSetAdded.fire(changeSet) 51 | 52 | changeSet.onStateChange.listen(self) { [weak self] state in 53 | if let definiteSelf = self { 54 | definiteSelf.onChangeSetStateChanged.fire(changeSet, state) 55 | if state == .Completed { 56 | definiteSelf.removeChangeSet(changeSet) 57 | } else if state == .Reverted { 58 | if let index = definiteSelf.changeSets.indexOf(changeSet) { 59 | if index < definiteSelf.changeSets.count - 1 { 60 | definiteSelf.changeSets[index+1].rebaseOnChangeSet(definiteSelf.changeSets[0]) 61 | } 62 | } 63 | definiteSelf.removeChangeSet(changeSet) 64 | } 65 | } 66 | } 67 | } 68 | 69 | func removeChangeSet(changeSet: ChangeSet) { 70 | if let index = changeSets.indexOf(changeSet) { 71 | changeSets.removeAtIndex(index) 72 | onChangeSetRemoved.fire(changeSet) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Jetstream/Client.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Client.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import Foundation 26 | 27 | let clientVersion = "0.2.0" 28 | let defaultErrorDomain = "com.uber.jetstream" 29 | 30 | /// Connectivity status of the client. 31 | public enum ClientStatus { 32 | /// Client is offline. 33 | case Offline 34 | /// Client is online. 35 | case Online 36 | } 37 | 38 | /// A function that when invoked creates a TransportAdapter for use. 39 | public typealias TransportAdapterFactory = () -> TransportAdapter 40 | 41 | /// A Client is used to initiate a connection between the application model and the remote Jetstream 42 | /// server. A client uses a TransportAdapter to establish a connection to the server. 43 | @objc public class Client: NSObject { 44 | // MARK: - Events 45 | /// Signal that fires whenever the status of the client changes. The fired data contains the 46 | /// new status for the client. 47 | public let onStatusChanged = Signal<(ClientStatus)>() 48 | 49 | /// Signal that fires whenever the clients gets a new session. The fired data contains the 50 | /// new session. 51 | public let onSession = Signal<(Session)>() 52 | 53 | /// Signal that fires whenever a session was denied. 54 | public let onSessionDenied = Signal<()>() 55 | 56 | /// Signal that fires whenever a message is sent that is waiting an acknowledgment from the 57 | /// server. This can be observed to tell when server has a lot of messages it has not replied to. 58 | public let onWaitingRepliesCountChanged: Signal<(UInt)> 59 | 60 | // MARK: - Properties 61 | 62 | /// The status of the client. 63 | public private(set) var status: ClientStatus = .Offline { 64 | didSet { 65 | if oldValue != status { 66 | onStatusChanged.fire(status) 67 | } 68 | } 69 | } 70 | 71 | /// The session of the client. 72 | public private(set) var session: Session? { 73 | didSet { 74 | if session != nil { 75 | transport.adapter.sessionEstablished(session!) 76 | onSession.fire(session!) 77 | } 78 | } 79 | } 80 | 81 | let logger = Logging.loggerFor("Client") 82 | let transportAdapterFactory: TransportAdapterFactory 83 | let restartSessionOnFatalError: Bool 84 | var transport: Transport 85 | var sessionCreateParams = [String: AnyObject]() 86 | 87 | // MARK: - Public interface 88 | 89 | /// Constructs a client. 90 | /// 91 | /// - parameter transportAdapterFactory: The factory used to create a transport adapter to connect to a Jetstream server. 92 | /// - parameter restartSessionOnFatalError: Whether to restart a session on a fatal session or transport error. 93 | public init(transportAdapterFactory: TransportAdapterFactory, restartSessionOnFatalError: Bool = true) { 94 | self.transportAdapterFactory = transportAdapterFactory 95 | self.transport = Transport(adapter: transportAdapterFactory()) 96 | self.onWaitingRepliesCountChanged = transport.onWaitingRepliesCountChanged 97 | self.restartSessionOnFatalError = restartSessionOnFatalError 98 | super.init() 99 | bindListeners() 100 | } 101 | 102 | /// Starts connecting the client to the server. 103 | public func connect() { 104 | transport.connect() 105 | } 106 | 107 | /// Starts connecting the client to the server with params to supply to server when creating a session. 108 | /// 109 | /// - parameter sessionCreateParams: The params that should be sent along with the session create request. 110 | public func connectWithSessionCreateParams(sessionCreateParams: [String: AnyObject]) { 111 | self.sessionCreateParams = sessionCreateParams 112 | connect() 113 | } 114 | 115 | /// Closes the connection to the server. 116 | public func close() { 117 | transport.disconnect() 118 | session?.close() 119 | session = nil 120 | } 121 | 122 | /// MARK: - Internal interface 123 | func bindListeners() { 124 | onStatusChanged.listen(self) { [weak self] (status) in 125 | if let this = self { 126 | this.statusChanged(status) 127 | } 128 | } 129 | bindTransportListeners() 130 | } 131 | 132 | func bindTransportListeners() { 133 | transport.onStatusChanged.listen(self) { [weak self] (status) in 134 | if let this = self { 135 | this.transportStatusChanged(status) 136 | } 137 | } 138 | transport.onMessage.listen(self) { [weak self] (message: NetworkMessage) in 139 | asyncMain { 140 | if let this = self { 141 | this.receivedMessage(message) 142 | } 143 | } 144 | } 145 | } 146 | 147 | func unbindTransportListeners() { 148 | transport.onStatusChanged.removeListener(self) 149 | transport.onMessage.removeListener(self) 150 | } 151 | 152 | func statusChanged(clientStatus: ClientStatus) { 153 | switch clientStatus { 154 | case .Online: 155 | logger.info("Online") 156 | if session == nil { 157 | transport.sendMessage(SessionCreateMessage(params: sessionCreateParams)) 158 | } 159 | case .Offline: 160 | logger.info("Offline") 161 | } 162 | } 163 | 164 | func transportStatusChanged(transportStatus: TransportStatus) { 165 | switch transportStatus { 166 | case .Closed: 167 | status = .Offline 168 | case .Connecting: 169 | status = .Offline 170 | case .Connected: 171 | status = .Online 172 | case .Fatal: 173 | status = .Offline 174 | if restartSessionOnFatalError && session != nil { 175 | reinitializeTransportAndRestartSession() 176 | } 177 | } 178 | } 179 | 180 | func receivedMessage(message: NetworkMessage) { 181 | switch message { 182 | case let sessionCreateReply as SessionCreateReplyMessage: 183 | if session != nil { 184 | logger.error("Received session create response with existing session") 185 | } else if let token = sessionCreateReply.sessionToken { 186 | logger.info("Starting session with token: \(token)") 187 | session = Session(client: self, token: token) 188 | } else { 189 | logger.info("Denied starting session, error: \(sessionCreateReply.error)") 190 | onSessionDenied.fire() 191 | } 192 | default: 193 | session?.receivedMessage(message) 194 | } 195 | } 196 | 197 | func reinitializeTransportAndRestartSession() { 198 | var scopesAndFetchParams = [ScopesWithFetchParams]() 199 | if let scopesByIndex = session?.scopes { 200 | scopesAndFetchParams = Array(scopesByIndex.values) 201 | } 202 | 203 | session?.close() 204 | session = nil 205 | 206 | onSession.listenOnce(self) { [weak self] session in 207 | if let this = self { 208 | for (scope, params) in scopesAndFetchParams { 209 | session.fetch(scope, params: params) { error in 210 | if let error = error { 211 | this.logger.error("Received error refetching scope '\(scope.name)': \(error)") 212 | } 213 | } 214 | } 215 | } 216 | } 217 | 218 | unbindTransportListeners() 219 | 220 | transport = Transport(adapter: transportAdapterFactory()) 221 | bindTransportListeners() 222 | 223 | connect() 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /Jetstream/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import Foundation 26 | 27 | public enum ErrorCode: Int { 28 | case SessionAlreadyClosed = 1 29 | case SessionBecameClosed 30 | case SessionDenied 31 | case SessionFetchFailed 32 | case SyncFragmentApplyError 33 | case ScopeFetchError 34 | } 35 | -------------------------------------------------------------------------------- /Jetstream/Constraint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constraint.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import Foundation 27 | 28 | /// The has new value property constraint is used to describe a property value has a new value. That is that this 29 | /// property has a new value described by the change. 30 | public class HasNewValuePropertyConstraint { 31 | public init() { 32 | // No-op 33 | } 34 | } 35 | 36 | /// An array property constraint type is used to describe a type of constraint on a new array property value. 37 | public enum ArrayPropertyConstraintType { 38 | case Insert 39 | case Remove 40 | } 41 | 42 | /// An array property constraint is used to describe a constraint on a new array property value. In future it should 43 | /// support actually specifying things like the insert or removal index, etc. 44 | public class ArrayPropertyConstraint { 45 | let type: ArrayPropertyConstraintType 46 | 47 | public init(type: ArrayPropertyConstraintType) { 48 | self.type = type 49 | } 50 | } 51 | 52 | /// A constraint is used to describe a change that must target a change or add of a certain model and can specify 53 | /// that the change sets properties to certain values or transforms them in specified manner. 54 | public class Constraint { 55 | /// The type of SyncFragment to target for constraint. 56 | public let type: SyncFragmentType 57 | 58 | /// The model class name to target for constraint. 59 | public let clsName: String 60 | 61 | /// The property values that must match for this constraint to pass. For simple checking of properties receiving 62 | /// new values you can use a HasNewValuePropertyConstraint as a value for the property name. For array properties 63 | /// you can use an ArrayPropertyConstraint as a value in place of the actual value to describe the transform the 64 | /// array should be applying as part of the constraint. 65 | public let properties: [String: AnyObject] 66 | 67 | /// Whether to allow additional properties than specified to pass the constraint. 68 | public let allowAdditionalProperties: Bool 69 | 70 | /// Validates that a set of constraints matches a set of SyncFragments. 71 | /// 72 | /// - parameter constraints: The constraints to apply. 73 | /// - parameter syncFragments: The sync fragments to apply the constraints on. 74 | public class func matchesAllConstraints(constraints: [Constraint], syncFragments: [SyncFragment]) -> Bool { 75 | var unmatchedFragments = syncFragments 76 | 77 | for constraint in constraints { 78 | // Remove fragments in batches so each fragment has an accounted for constraint 79 | unmatchedFragments = unmatchedFragments.filter { !constraint.matches($0) } 80 | } 81 | 82 | return unmatchedFragments.count == 0 83 | } 84 | 85 | /// Constructs the Constraint. 86 | /// 87 | /// - parameter type: The type of SyncFragment to target for constraint. 88 | /// - parameter clsName: The model class name to target for constraint. 89 | /// - parameter properties: The property values that must match for this constraint to pass. 90 | /// - parameter allowAdditionalProperties: Whether to allow additional properties than specified to pass the constraint. 91 | public init(type: SyncFragmentType, clsName: String, properties: [String: AnyObject] = [String: AnyObject](), allowAdditionalProperties: Bool = true) { 92 | self.type = type 93 | self.clsName = clsName 94 | self.properties = properties 95 | self.allowAdditionalProperties = allowAdditionalProperties 96 | } 97 | 98 | /// Validates that the constraint matches a SyncFragment. 99 | /// 100 | /// - parameter syncFragment: The sync fragment to validate the constraint matches. 101 | public func matches(syncFragment: SyncFragment) -> Bool { 102 | if type != syncFragment.type || syncFragment.clsName == nil || clsName != syncFragment.clsName! { 103 | // Does not match constraint type and class 104 | return false 105 | } 106 | 107 | if properties.count < 1 { 108 | if !allowAdditionalProperties && syncFragment.properties != nil && syncFragment.properties!.count > 0 { 109 | // Expecting no properties however there are some 110 | return false 111 | } else { 112 | // Matches as no constraint values to verify 113 | return true 114 | } 115 | } 116 | 117 | // Extract class property infos and fragment properties 118 | if let clsName = syncFragment.clsName { 119 | if let propertyInfos = ModelObject.Static.properties[clsName] { 120 | if let fragmentProperties = syncFragment.properties { 121 | // Ensure count matches if not allowing additional properties 122 | if !allowAdditionalProperties && self.properties.count != fragmentProperties.count { 123 | // Not allowing additional properties, needs to match count. If other mismatch a property 124 | // will be missing from fragment properties and it will be caught by the checking below. 125 | return false 126 | } 127 | 128 | // Iterate over constraints 129 | for (constraintKey, constraintValue) in self.properties { 130 | 131 | // Extract fragment value and property info for this constraint key 132 | if let value: AnyObject = fragmentProperties[constraintKey] { 133 | if let propertyInfo = propertyInfos[constraintKey] { 134 | // Check value matches constraint 135 | if let _ = constraintValue as? HasNewValuePropertyConstraint { 136 | // Allow all cases where constraintValue is HasNewValuePropertyConstraint 137 | } else if let arrayConstraintValue = constraintValue as? ArrayPropertyConstraint { 138 | // Apply an array constraint value 139 | if let array = value as? [AnyObject] { 140 | switch type { 141 | case .Add: 142 | if arrayConstraintValue.type != .Insert || array.count < 1 { 143 | // Allow an insert constraint on an add with actual values in the 144 | // array but not a remove or anything else as they do not make sense 145 | return false 146 | } 147 | case .Change: 148 | if let originalArray = syncFragment.originalProperties?[constraintKey] as? [AnyObject] { 149 | if arrayConstraintValue.type == .Insert && !(array.count > originalArray.count) { 150 | return false 151 | } else if arrayConstraintValue.type == .Remove && !(array.count < originalArray.count) { 152 | return false 153 | } 154 | } else { 155 | return false 156 | } 157 | } 158 | } else { 159 | return false 160 | } 161 | } else { 162 | // Apply a simple value constraint 163 | if constraintValue === NSNull() && value === NSNull() { 164 | // Allow case where constraint is nil and explicit nil matches 165 | } else { 166 | let constraintModelValue = convertAnyObjectToModelValue(constraintValue, type: propertyInfo.valueType) 167 | let fragmentModelValue = convertAnyObjectToModelValue(value, type: propertyInfo.valueType) 168 | if constraintModelValue == nil || fragmentModelValue == nil { 169 | return false 170 | } else if !constraintModelValue!.equalTo(fragmentModelValue!) { 171 | return false 172 | } 173 | } 174 | } 175 | } else { 176 | // Cannot check values as no propertyInfo for key for this class 177 | return false 178 | } 179 | } else { 180 | // Specified a constraint at a key which fragment does not include 181 | return false 182 | } 183 | } 184 | 185 | // All constraint values passed 186 | return true 187 | } else { 188 | // Had constraints we couldn't compare because fragment has no properties 189 | return false 190 | } 191 | } else { 192 | // Could not lookup property infos 193 | return false 194 | } 195 | } else { 196 | // Could not lookup class name for property infos 197 | return false 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /Jetstream/Functions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Functions.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import Foundation 26 | 27 | public func delay(delay: Double, callback: () -> ()) { 28 | dispatch_after( 29 | dispatch_time(DISPATCH_TIME_NOW, Int64(delay * Double(NSEC_PER_SEC))), 30 | dispatch_get_main_queue(), 31 | callback) 32 | } 33 | 34 | public func asyncMain(callback: () -> ()) { 35 | dispatch_async(dispatch_get_main_queue(), callback) 36 | } 37 | 38 | func error(code: ErrorCode, localizedDescription: String? = nil) -> NSError { 39 | var userInfo: [NSObject: AnyObject]? 40 | if let definiteLocalizedDescription = localizedDescription { 41 | userInfo = [NSLocalizedDescriptionKey: definiteLocalizedDescription] 42 | } 43 | return NSError( 44 | domain: defaultErrorDomain, 45 | code: code.rawValue, 46 | userInfo: userInfo) 47 | } 48 | 49 | func errorWithUserInfo(code: ErrorCode, userInfo: [NSObject: AnyObject]) -> NSError { 50 | return NSError( 51 | domain: defaultErrorDomain, 52 | code: code.rawValue, 53 | userInfo: userInfo) 54 | } 55 | 56 | func errorFromDictionary(code: ErrorCode, error: [NSString: AnyObject]) -> NSError { 57 | var userInfo = [NSLocalizedDescriptionKey: "Unknown error"] 58 | if let errorMessage = error["message"] as? String { 59 | userInfo[NSLocalizedDescriptionKey] = errorMessage 60 | } 61 | if let errorType = error["type"] as? String { 62 | userInfo[NSLocalizedFailureReasonErrorKey] = errorType 63 | } 64 | return errorWithUserInfo(code, userInfo: userInfo) 65 | } 66 | -------------------------------------------------------------------------------- /Jetstream/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 0.1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Jetstream/Jetstream.h: -------------------------------------------------------------------------------- 1 | // 2 | // Jetstream.h 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | #import 26 | 27 | //! Project version number for Jetstream. 28 | FOUNDATION_EXPORT double JetstreamVersionNumber; 29 | 30 | //! Project version string for Jetstream. 31 | FOUNDATION_EXPORT const unsigned char JetstreamVersionString[]; 32 | 33 | // In this header, you should import all the public headers of your framework using statements like #import 34 | -------------------------------------------------------------------------------- /Jetstream/Logging.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logging.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import Foundation 26 | 27 | let disabledLoggers = [String: Bool]() 28 | 29 | public enum LogLevel: String { 30 | case Trace = "TRACE" 31 | case Debug = "DEBUG" 32 | case Info = "INFO" 33 | case Warning = "WARN" 34 | case Error = "ERROR" 35 | } 36 | 37 | /// Jetstreams logging class. To subscribe to logging events from Jetstream, subscribe to the 38 | /// Logging.onMessage - signal. 39 | public class Logging { 40 | struct Static { 41 | static let baseLoggerName = "Jetstream" 42 | static var enabled = false 43 | static var consoleEnabled = true 44 | static var loggers = [String: Logger]() 45 | static let onMessage = Signal<(level: LogLevel, message: String)>() 46 | } 47 | 48 | class var logger: Logger { 49 | get { 50 | if let logger = Static.loggers[Static.baseLoggerName] { 51 | return logger 52 | } 53 | let logger = Logger(name: Static.baseLoggerName) 54 | Static.loggers[Static.baseLoggerName] = logger 55 | return logger 56 | } 57 | } 58 | 59 | public class func loggerFor(str: String) -> Logger { 60 | let loggerName = "\(Static.baseLoggerName).\(str)" 61 | 62 | var logger = Static.loggers[loggerName] 63 | if logger == nil { 64 | logger = Logger(name: loggerName) 65 | Static.loggers[loggerName] = logger 66 | } 67 | if disabledLoggers[str] == true { 68 | logger!.enabled = false 69 | } 70 | return logger! 71 | } 72 | 73 | /// A signal that is fired whenever Jetstream logs. The signal fires with the parameters LogLevel 74 | /// and Message. 75 | public class var onMessage: Signal<(level: LogLevel, message: String)> { 76 | get { 77 | return Static.onMessage 78 | } 79 | } 80 | 81 | /// Enables all logging. 82 | public class func enableAll() { 83 | Static.enabled = true 84 | } 85 | 86 | /// Disables all logging. 87 | public class func disableAll() { 88 | Static.enabled = false 89 | } 90 | 91 | /// Enables logging to the console. 92 | public class func enableConsole() { 93 | Static.enabled = true 94 | Static.consoleEnabled = true 95 | } 96 | 97 | /// Disables logging to the console 98 | public class func disableConsole() { 99 | Static.consoleEnabled = false 100 | } 101 | } 102 | 103 | public class Logger { 104 | let name: String 105 | var enabled: Bool 106 | 107 | init(name: String, enabled: Bool) { 108 | self.name = name 109 | self.enabled = enabled 110 | } 111 | 112 | convenience init(name: String) { 113 | self.init(name: name, enabled: true) 114 | } 115 | 116 | public func trace(message: T) { 117 | if !Logging.Static.enabled || !enabled { 118 | return 119 | } 120 | log(.Trace, message: message) 121 | } 122 | 123 | public func debug(message: T) { 124 | if !Logging.Static.enabled || !enabled { 125 | return 126 | } 127 | log(.Debug, message: message) 128 | } 129 | 130 | public func info(message: T) { 131 | if !Logging.Static.enabled || !enabled { 132 | return 133 | } 134 | log(.Info, message: message) 135 | } 136 | 137 | public func warn(message: T) { 138 | if !Logging.Static.enabled || !enabled { 139 | return 140 | } 141 | log(.Warning, message: message) 142 | } 143 | 144 | public func error(message: T) { 145 | if !Logging.Static.enabled || !enabled { 146 | return 147 | } 148 | log(.Error, message: message) 149 | } 150 | 151 | func log(level: LogLevel, message: T) { 152 | let str = "\(name): \(message)" 153 | if Logging.Static.consoleEnabled { 154 | print("\(level) \(str)") 155 | } 156 | 157 | Logging.Static.onMessage.fire((level: level, message: str)) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Jetstream/NetworkMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkMessage.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import Foundation 26 | 27 | /// A message wrapper used by Jetstream to communicate between the client and server. The message does not have 28 | /// a public interface as netwotk messages are internal to Jetstream. 29 | public class NetworkMessage { 30 | // Override to provide message type 31 | var type: String { 32 | return "Message" 33 | } 34 | 35 | public let index: UInt 36 | 37 | init(index: UInt) { 38 | self.index = index 39 | } 40 | 41 | convenience init(session: Session) { 42 | self.init(index: session.getNextMessageIndex()) 43 | } 44 | 45 | public func serialize() -> [String: AnyObject] { 46 | return ["type": type, "index": index] 47 | } 48 | 49 | public class func unserializeDictionary(dictionary: [String: AnyObject]) -> NetworkMessage? { 50 | let type: AnyObject? = dictionary["type"] 51 | if let definiteType = type as? String { 52 | switch definiteType { 53 | case SessionCreateMessage.messageType: 54 | return SessionCreateMessage.unserialize(dictionary) 55 | case SessionCreateReplyMessage.messageType: 56 | return SessionCreateReplyMessage.unserialize(dictionary) 57 | case SessionCreateReplyMessage.messageType: 58 | return SessionCreateReplyMessage.unserialize(dictionary) 59 | case ScopeFetchReplyMessage.messageType: 60 | return ScopeFetchReplyMessage.unserialize(dictionary) 61 | case ScopeStateMessage.messageType: 62 | return ScopeStateMessage.unserialize(dictionary) 63 | case ScopeSyncMessage.messageType: 64 | return ScopeSyncMessage.unserialize(dictionary) 65 | case ScopeSyncReplyMessage.messageType: 66 | return ScopeSyncReplyMessage.unserialize(dictionary) 67 | case PingMessage.messageType: 68 | return PingMessage.unserialize(dictionary) 69 | default: 70 | return nil 71 | } 72 | } 73 | return nil 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Jetstream/PingMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PingMessage.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import Foundation 26 | 27 | public class PingMessage: NetworkMessage { 28 | class var messageType: String { 29 | return "Ping" 30 | } 31 | 32 | override var type: String { 33 | return PingMessage.messageType 34 | } 35 | 36 | public let ack: UInt 37 | public let resendMissing: Bool 38 | 39 | init(index: UInt, ack: UInt, resendMissing: Bool) { 40 | self.ack = ack 41 | self.resendMissing = resendMissing 42 | super.init(index: index) 43 | } 44 | 45 | public convenience init(session: Session) { 46 | self.init(index: 0, ack: session.serverIndex, resendMissing: false) 47 | } 48 | 49 | public convenience init(session: Session, resendMissing: Bool) { 50 | self.init(index: 0, ack: session.serverIndex, resendMissing: resendMissing) 51 | } 52 | 53 | public override func serialize() -> [String: AnyObject] { 54 | var dictionary = super.serialize() 55 | dictionary["ack"] = ack 56 | dictionary["resendMissing"] = resendMissing 57 | return dictionary 58 | } 59 | 60 | public class func unserialize(dictionary: [String: AnyObject]) -> NetworkMessage? { 61 | let index = dictionary["index"] as? UInt 62 | let ack = dictionary["ack"] as? UInt 63 | let resendMissing = dictionary["resendMissing"] as? Bool 64 | 65 | if index == nil || ack == nil || resendMissing == nil { 66 | return nil 67 | } else { 68 | return PingMessage(index: index!, ack: ack!, resendMissing: resendMissing!) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Jetstream/ReplyMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReplyMessage.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import Foundation 26 | 27 | class ReplyMessage: NetworkMessage { 28 | 29 | let replyTo: UInt 30 | 31 | init(index: UInt, replyTo: UInt) { 32 | self.replyTo = replyTo 33 | super.init(index: index) 34 | } 35 | 36 | convenience init(session: Session, replyTo: UInt) { 37 | self.init(index: session.getNextMessageIndex(), replyTo: replyTo) 38 | } 39 | 40 | override func serialize() -> [String: AnyObject] { 41 | var dictionary = super.serialize() 42 | dictionary["replyTo"] = replyTo 43 | return dictionary 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Jetstream/ScopeFetchMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScopeFetchMessage.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import Foundation 26 | 27 | class ScopeFetchMessage: NetworkMessage { 28 | class var messageType: String { 29 | return "ScopeFetch" 30 | } 31 | 32 | override var type: String { 33 | return ScopeFetchMessage.messageType 34 | } 35 | 36 | let name: String 37 | let params: [String: AnyObject] 38 | 39 | init(index: UInt, name: String, params: [String: AnyObject]) { 40 | self.name = name 41 | self.params = params 42 | super.init(index: index) 43 | } 44 | 45 | convenience init(session: Session, name: String) { 46 | self.init(index: session.getNextMessageIndex(), name: name, params: [String: AnyObject]()) 47 | } 48 | 49 | convenience init(session: Session, name: String, params: [String: AnyObject]) { 50 | self.init(index: session.getNextMessageIndex(), name: name, params: params) 51 | } 52 | 53 | override func serialize() -> [String: AnyObject] { 54 | var dictionary = super.serialize() 55 | dictionary["name"] = name 56 | dictionary["params"] = params 57 | return dictionary 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Jetstream/ScopeFetchReplyMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScopeFetchReplyMessage.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import Foundation 26 | 27 | class ScopeFetchReplyMessage: ReplyMessage { 28 | class var messageType: String { 29 | return "ScopeFetchReply" 30 | } 31 | 32 | override var type: String { 33 | return ScopeFetchReplyMessage.messageType 34 | } 35 | 36 | let scopeIndex: UInt? 37 | let error: NSError? 38 | 39 | init(index: UInt, replyTo: UInt, scopeIndex: UInt?, error: NSError?) { 40 | self.scopeIndex = scopeIndex 41 | self.error = error 42 | super.init(index: index, replyTo: replyTo) 43 | } 44 | 45 | override func serialize() -> [String: AnyObject] { 46 | assertionFailure("ScopeSyncReplyMessage cannot serialize itself") 47 | return [String: AnyObject]() 48 | } 49 | 50 | class func unserialize(dictionary: [String: AnyObject]) -> NetworkMessage? { 51 | let index = dictionary["index"] as? UInt 52 | let replyTo = dictionary["replyTo"] as? UInt 53 | let scopeIndex = dictionary["scopeIndex"] as? UInt 54 | 55 | var error: NSError? 56 | if let serializedError = dictionary["error"] as? [String: AnyObject] { 57 | error = errorFromDictionary(.ScopeFetchError, error: serializedError) 58 | } 59 | 60 | if index == nil || replyTo == nil || (scopeIndex == nil && error == nil) { 61 | return nil 62 | } else { 63 | return ScopeFetchReplyMessage( 64 | index: index!, 65 | replyTo: replyTo!, 66 | scopeIndex: scopeIndex, 67 | error: error) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Jetstream/ScopeStateMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScopeStateMessage.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import Foundation 26 | 27 | class ScopeStateMessage: NetworkMessage { 28 | class var messageType: String { 29 | return "ScopeState" 30 | } 31 | 32 | let scopeIndex: UInt 33 | let rootUUID: NSUUID 34 | let syncFragments: [SyncFragment] 35 | 36 | init(index: UInt, scopeIndex: UInt, rootUUID: NSUUID, syncFragments: [SyncFragment]) { 37 | self.scopeIndex = scopeIndex 38 | self.rootUUID = rootUUID 39 | self.syncFragments = syncFragments 40 | super.init(index: index) 41 | } 42 | 43 | convenience init(session: Session, scopeIndex: UInt, rootUUID: NSUUID, syncFragments: [SyncFragment]) { 44 | self.init(index: session.getNextMessageIndex(), scopeIndex: scopeIndex, rootUUID: rootUUID, syncFragments: syncFragments) 45 | } 46 | 47 | override func serialize() -> [String: AnyObject] { 48 | var dictionary = super.serialize() 49 | let fragments = syncFragments.map { 50 | (syncFragment) -> [String: AnyObject] in 51 | 52 | return syncFragment.serialize() 53 | } 54 | 55 | dictionary["scopeIndex"] = scopeIndex 56 | dictionary["rootUUID"] = rootUUID.UUIDString.lowercaseString 57 | dictionary["fragments"] = fragments 58 | 59 | return dictionary 60 | } 61 | 62 | class func unserialize(dictionary: [String: AnyObject]) -> NetworkMessage? { 63 | var index: UInt? 64 | var scopeIndex: UInt? 65 | var rootUUID: NSUUID? 66 | var syncFragments = [SyncFragment]() 67 | 68 | for (key, value) in dictionary { 69 | switch key { 70 | case "index": 71 | if let definiteIndex = value as? UInt { 72 | index = definiteIndex 73 | } 74 | case "scopeIndex": 75 | if let definiteScopeIndex = value as? UInt { 76 | scopeIndex = definiteScopeIndex 77 | } 78 | case "fragments": 79 | if let fragments = value as? [[String: AnyObject]] { 80 | syncFragments = [SyncFragment]() 81 | for fragment in fragments { 82 | if let syncFragment = SyncFragment.unserialize(fragment) { 83 | syncFragments.append(syncFragment) 84 | } 85 | } 86 | } 87 | case "rootUUID": 88 | if let UUIDString = value as? String { 89 | if let UUID = NSUUID(UUIDString: UUIDString) { 90 | rootUUID = UUID 91 | } 92 | } 93 | default: 94 | break 95 | } 96 | } 97 | 98 | if index == nil || scopeIndex == nil || rootUUID == nil || syncFragments.count < 1 { 99 | return nil 100 | } else { 101 | return ScopeStateMessage( 102 | index: index!, 103 | scopeIndex: scopeIndex!, 104 | rootUUID: rootUUID!, 105 | syncFragments: syncFragments) 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Jetstream/ScopeSyncMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScopeSyncMessage.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import Foundation 26 | 27 | class ScopeSyncMessage: NetworkMessage { 28 | class var messageType: String { 29 | return "ScopeSync" 30 | } 31 | 32 | override var type: String { 33 | return ScopeSyncMessage.messageType 34 | } 35 | 36 | let scopeIndex: UInt 37 | let procedure: String? 38 | let atomic: Bool 39 | let syncFragments: [SyncFragment] 40 | 41 | init(index: UInt, scopeIndex: UInt, procedure: String?, atomic: Bool, syncFragments: [SyncFragment]) { 42 | self.scopeIndex = scopeIndex 43 | self.procedure = procedure 44 | self.atomic = atomic 45 | self.syncFragments = syncFragments 46 | super.init(index: index) 47 | } 48 | 49 | convenience init(session: Session, scopeIndex: UInt, procedure: String?, atomic: Bool, syncFragments: [SyncFragment]) { 50 | self.init(index: session.getNextMessageIndex(), scopeIndex: scopeIndex, procedure: procedure, atomic: atomic, syncFragments: syncFragments) 51 | } 52 | 53 | override func serialize() -> [String: AnyObject] { 54 | var dictionary = super.serialize() 55 | let fragments = syncFragments.map { 56 | (syncFragment) -> [String: AnyObject] in 57 | 58 | return syncFragment.serialize() 59 | } 60 | 61 | dictionary["scopeIndex"] = scopeIndex 62 | if let definiteProcedure = procedure { 63 | dictionary["procedure"] = definiteProcedure 64 | } 65 | if atomic { 66 | dictionary["atomic"] = atomic 67 | } 68 | dictionary["fragments"] = fragments 69 | 70 | return dictionary 71 | } 72 | 73 | class func unserialize(dictionary: [String: AnyObject]) -> NetworkMessage? { 74 | var index: UInt? 75 | var scopeIndex: UInt? 76 | var syncFragments: [SyncFragment]? 77 | var procedure: String? 78 | var atomic = false 79 | 80 | for (key, value) in dictionary { 81 | switch key { 82 | case "index": 83 | if let definiteIndex = value as? UInt { 84 | index = definiteIndex 85 | } 86 | case "scopeIndex": 87 | if let definiteScopeIndex = value as? UInt { 88 | scopeIndex = definiteScopeIndex 89 | } 90 | case "procedure": 91 | if let definiteProcedure = value as? String { 92 | procedure = definiteProcedure 93 | } 94 | case "atomic": 95 | if let definiteAtomic = value as? Bool { 96 | atomic = definiteAtomic 97 | } 98 | case "fragments": 99 | if let fragments = value as? [[String: AnyObject]] { 100 | syncFragments = [SyncFragment]() 101 | for fragment in fragments { 102 | if let syncFragment = SyncFragment.unserialize(fragment) { 103 | syncFragments!.append(syncFragment) 104 | } 105 | } 106 | } 107 | default: 108 | break 109 | } 110 | } 111 | 112 | if index == nil || scopeIndex == nil || syncFragments == nil { 113 | return nil 114 | } else { 115 | return ScopeSyncMessage( 116 | index: index!, 117 | scopeIndex: scopeIndex!, 118 | procedure: procedure, 119 | atomic: atomic, 120 | syncFragments: syncFragments!) 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Jetstream/ScopeSyncReplyMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScopeSyncReplyMessage.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import Foundation 26 | 27 | 28 | struct SyncFragmentReply { 29 | var accepted: Bool = true 30 | var error: NSError? 31 | var modifications: [NSString: AnyObject]? 32 | } 33 | 34 | class ScopeSyncReplyMessage: ReplyMessage { 35 | class var messageType: String { 36 | return "ScopeSyncReply" 37 | } 38 | 39 | override var type: String { 40 | return ScopeSyncReplyMessage.messageType 41 | } 42 | 43 | let fragmentReplies: [SyncFragmentReply] 44 | 45 | init(index: UInt, replyTo: UInt, fragmentReplies: [SyncFragmentReply]) { 46 | self.fragmentReplies = fragmentReplies 47 | super.init(index: index, replyTo: replyTo) 48 | } 49 | 50 | override func serialize() -> [String: AnyObject] { 51 | assertionFailure("ScopeSyncReplyMessage cannot serialize itself") 52 | return [String: AnyObject]() 53 | } 54 | 55 | class func unserialize(dictionary: [String: AnyObject]) -> NetworkMessage? { 56 | let index = dictionary["index"] as? UInt 57 | let replyTo = dictionary["replyTo"] as? UInt 58 | let serializedFragmentReplies = dictionary["fragmentReplies"] as? [[String: AnyObject]] 59 | 60 | if index == nil || replyTo == nil || serializedFragmentReplies == nil { 61 | return nil 62 | } else { 63 | var fragmentReplies = [SyncFragmentReply]() 64 | for serializedFragmentReply in serializedFragmentReplies! { 65 | var accepted = true 66 | var error: NSError? 67 | var modifications = [NSString: AnyObject]() 68 | 69 | if let serializedError = serializedFragmentReply["error"] as? [String: AnyObject] { 70 | accepted = false 71 | error = errorFromDictionary(.SyncFragmentApplyError, error: serializedError) 72 | } 73 | 74 | if let serializedModifications = serializedFragmentReply["modifications"] as? [String: AnyObject] { 75 | modifications = serializedModifications 76 | } 77 | 78 | let fragmentReply = SyncFragmentReply(accepted: accepted, error: error, modifications: modifications) 79 | fragmentReplies.append(fragmentReply) 80 | } 81 | 82 | return ScopeSyncReplyMessage(index: index!, replyTo: replyTo!, fragmentReplies: fragmentReplies) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Jetstream/Session.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Session.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import Foundation 26 | import UIKit 27 | 28 | typealias ScopesWithFetchParams = (scope: Scope, fetchParams: [String: AnyObject]) 29 | 30 | public class Session { 31 | /// The token of the session 32 | public let token: String 33 | 34 | let logger = Logging.loggerFor("Session") 35 | let client: Client 36 | var nextMessageIndex: UInt = 1 37 | var serverIndex: UInt = 0 38 | var scopes = [UInt: ScopesWithFetchParams]() 39 | var closed = false 40 | let changeSetQueue = ChangeSetQueue() 41 | 42 | init(client: Client, token: String) { 43 | self.client = client 44 | self.token = token 45 | } 46 | 47 | // MARK: - Public interface 48 | public func fetch(scope: Scope, callback: (NSError?) -> ()) { 49 | fetch(scope, params: [String: AnyObject](), callback: callback) 50 | } 51 | 52 | public func fetch(scope: Scope, params: [String: AnyObject], callback: (NSError?) -> ()) { 53 | if closed { 54 | return callback(errorWithUserInfo( 55 | .SessionAlreadyClosed, 56 | userInfo: [NSLocalizedDescriptionKey: "Session already closed"])) 57 | } 58 | 59 | let scopeFetchMessage = ScopeFetchMessage(session: self, name: scope.name, params: params) 60 | client.transport.sendMessage(scopeFetchMessage) { 61 | [weak self] (response) in 62 | if let definiteSelf = self { 63 | if let scopeFetchReply = response as? ScopeFetchReplyMessage { 64 | if scopeFetchReply.error != nil { 65 | return callback(scopeFetchReply.error) 66 | } 67 | if scopeFetchReply.scopeIndex == nil { 68 | return callback(error(.ScopeFetchError, localizedDescription: "Undefined scope index")) 69 | } 70 | 71 | if definiteSelf.closed { 72 | return callback(error(.SessionBecameClosed, localizedDescription: "Session became closed")) 73 | } 74 | definiteSelf.scopeAttach(scope, scopeIndex: scopeFetchReply.scopeIndex!, fetchParams: params) 75 | callback(nil) 76 | } else { 77 | callback(error(.ScopeFetchError, localizedDescription: "Invalid reply message")) 78 | } 79 | } 80 | } 81 | } 82 | 83 | // MARK: - Internal interface 84 | func getNextMessageIndex() -> UInt { 85 | return nextMessageIndex++ 86 | } 87 | 88 | func receivedMessage(message: NetworkMessage) { 89 | if closed { 90 | return 91 | } 92 | 93 | let isFirstOrEphermalMessage = message.index == 0 94 | if message.index <= serverIndex && !isFirstOrEphermalMessage { 95 | // Likely due to an outgoing Ping we sent up with a low ack number 96 | // that since increased by received messages from the server 97 | logger.info("Server resent seen message") 98 | return 99 | } 100 | if message.index != serverIndex + 1 && !isFirstOrEphermalMessage { 101 | logger.error("Received out of order message index") 102 | client.transport.reconnect() 103 | return 104 | } 105 | 106 | if !isFirstOrEphermalMessage { 107 | serverIndex = message.index 108 | } 109 | 110 | switch message { 111 | case let scopeStateMessage as ScopeStateMessage: 112 | if let scopeAndFetchParams = scopes[scopeStateMessage.scopeIndex] { 113 | let scope = scopeAndFetchParams.scope 114 | if scope.root != nil { 115 | scope.startApplyingRemote { 116 | scope.applyFullStateFromFragments(scopeStateMessage.syncFragments, rootUUID: scopeStateMessage.rootUUID) 117 | } 118 | } else { 119 | logger.error("Received state message without having a root model") 120 | } 121 | } else { 122 | logger.error("Received state message without having local scope") 123 | } 124 | case let scopeSyncMessage as ScopeSyncMessage: 125 | if let scopeAndFetchParams = scopes[scopeSyncMessage.scopeIndex] { 126 | let scope = scopeAndFetchParams.scope 127 | if scope.root != nil { 128 | if scopeSyncMessage.syncFragments.count > 0 { 129 | scope.startApplyingRemote { 130 | scope.applySyncFragments(scopeSyncMessage.syncFragments) 131 | } 132 | } else { 133 | logger.error("Received sync message without fragments") 134 | } 135 | } 136 | } 137 | default: 138 | break 139 | } 140 | } 141 | 142 | func scopeAttach(scope: Scope, scopeIndex: UInt, fetchParams: [String: AnyObject] = [String: AnyObject]()) { 143 | scopes[scopeIndex] = (scope: scope, fetchParams: fetchParams) 144 | scope.onChanges.listen(self) { 145 | [weak self] changeSet in 146 | if let definiteSelf = self { 147 | definiteSelf.scopeChanges(scope, atIndex: scopeIndex, changeSet: changeSet) 148 | } 149 | } 150 | } 151 | 152 | func scopeChanges(scope: Scope, atIndex: UInt, changeSet: ChangeSet) { 153 | if closed { 154 | return 155 | } 156 | changeSetQueue.addChangeSet(changeSet) 157 | client.transport.sendMessage(ScopeSyncMessage(session: self, scopeIndex: atIndex, procedure: changeSet.procedure, atomic: changeSet.atomic, syncFragments: changeSet.syncFragments)) { [weak self] reply in 158 | if let _ = self { 159 | if let syncReply = reply as? ScopeSyncReplyMessage { 160 | changeSet.processFragmentReplies(syncReply.fragmentReplies, scope: scope) 161 | } else { 162 | changeSet.revertOnScope(scope) 163 | } 164 | } 165 | } 166 | } 167 | 168 | func close() { 169 | for (_, entry) in scopes { 170 | entry.scope.onChanges.removeListener(self) 171 | } 172 | scopes.removeAll(keepCapacity: false) 173 | closed = true 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /Jetstream/SessionCreateMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionCreateMessage.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import Foundation 26 | 27 | class SessionCreateMessage: NetworkMessage { 28 | class var messageType: String { 29 | return "SessionCreate" 30 | } 31 | 32 | override var type: String { 33 | return SessionCreateMessage.messageType 34 | } 35 | 36 | let params: [String: AnyObject] 37 | let version: String 38 | 39 | init(params: [String: AnyObject], version: String) { 40 | self.params = params 41 | self.version = version 42 | super.init(index: 0) 43 | } 44 | 45 | convenience init() { 46 | self.init(params: [String: AnyObject]()) 47 | } 48 | 49 | convenience init(params: [String: AnyObject]) { 50 | self.init(params: params, version: clientVersion) 51 | } 52 | 53 | override func serialize() -> [String: AnyObject] { 54 | var dictionary = super.serialize() 55 | dictionary["params"] = params 56 | dictionary["version"] = version 57 | return dictionary 58 | } 59 | 60 | class func unserialize(dictionary: [String: AnyObject]) -> NetworkMessage? { 61 | let params = dictionary["params"] as? [String: AnyObject] 62 | let version = dictionary["version"] as? String 63 | 64 | if params != nil && version != nil { 65 | return SessionCreateMessage(params: params!, version: version!) 66 | } else if params != nil { 67 | return SessionCreateMessage(params: params!) 68 | } else { 69 | return SessionCreateMessage() 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Jetstream/SessionCreateReplyMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionCreateReplyMessage.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import Foundation 26 | 27 | class SessionCreateReplyMessage: NetworkMessage { 28 | class var messageType: String { 29 | return "SessionCreateReply" 30 | } 31 | 32 | override var type: String { 33 | return SessionCreateReplyMessage.messageType 34 | } 35 | 36 | var sessionToken: String? 37 | var error: NSError? 38 | 39 | init(index: UInt, sessionToken: String?, error: NSError?) { 40 | self.sessionToken = sessionToken 41 | super.init(index: index) 42 | } 43 | 44 | class func unserialize(dictionary: [String: AnyObject]) -> NetworkMessage? { 45 | let index = dictionary["index"] as? UInt 46 | let sessionToken = dictionary["sessionToken"] as? String 47 | 48 | 49 | var error: NSError? 50 | if let serializedError = dictionary["error"] as? [String: AnyObject] { 51 | error = errorFromDictionary(.SyncFragmentApplyError, error: serializedError) 52 | } 53 | 54 | if index == nil || (sessionToken == nil && error == nil) { 55 | return nil 56 | } else { 57 | return SessionCreateReplyMessage( 58 | index: index!, 59 | sessionToken: sessionToken, 60 | error: error) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Jetstream/Transport.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Transport.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import Foundation 26 | 27 | /// Transport adapters initialize themselves with options that conform to ConnectionOptions. 28 | public protocol ConnectionOptions { 29 | /// The url to connect to. 30 | var url: NSURL { get } 31 | } 32 | 33 | /// Status of the transport state. 34 | public enum TransportStatus { 35 | /// Not open. 36 | case Closed 37 | /// Connecting. 38 | case Connecting 39 | /// Connected. 40 | case Connected 41 | /// Fatally closed and cannot be reconnected. 42 | case Fatal 43 | } 44 | 45 | public protocol TransportAdapter { 46 | var onStatusChanged: Signal<(TransportStatus)> { get } 47 | var onMessage: Signal<(NetworkMessage)> { get } 48 | 49 | var adapterName: String { get } 50 | var status: TransportStatus { get } 51 | var options: ConnectionOptions { get } 52 | 53 | func connect() 54 | func disconnect() 55 | func reconnect() 56 | func sendMessage(message: NetworkMessage) 57 | func sessionEstablished(session: Session) 58 | } 59 | 60 | typealias ReplyCallback = (ReplyMessage) -> Void 61 | 62 | class Transport { 63 | class func defaultTransportAdapter(options: ConnectionOptions) -> TransportAdapter { 64 | return WebSocketTransportAdapter(options: WebSocketConnectionOptions(url: options.url)) 65 | } 66 | 67 | let logger = Logging.loggerFor("Transport") 68 | let onStatusChanged: Signal<(TransportStatus)> 69 | let onMessage: Signal 70 | let onWaitingRepliesCountChanged = Signal<(UInt)>() 71 | let adapter: TransportAdapter 72 | var waitingReply = [UInt: ReplyCallback]() 73 | var fatallyClosed = false 74 | 75 | var status: TransportStatus { 76 | return fatallyClosed ? .Fatal : adapter.status 77 | } 78 | 79 | init(adapter: TransportAdapter) { 80 | self.adapter = adapter 81 | onStatusChanged = adapter.onStatusChanged 82 | onMessage = adapter.onMessage 83 | bindListeners() 84 | } 85 | 86 | func bindListeners() { 87 | onStatusChanged.listen(self) { [weak self] (status) in 88 | if let definiteSelf = self { 89 | definiteSelf.statusChanged(status) 90 | } 91 | } 92 | onMessage.listen(self) { [weak self] (message) in 93 | if let definiteSelf = self { 94 | definiteSelf.messageReceived(message) 95 | } 96 | } 97 | } 98 | 99 | func unbindListeners() { 100 | onStatusChanged.removeListener(self) 101 | onMessage.removeListener(self) 102 | } 103 | 104 | func fatallyClose() { 105 | fatallyClosed = true 106 | unbindListeners() 107 | disconnect() 108 | } 109 | 110 | func statusChanged(status: TransportStatus) { 111 | switch status { 112 | case .Closed: 113 | logger.info("Closed") 114 | case .Connecting: 115 | logger.info("Connecting using \(self.adapter.adapterName) to \(self.adapter.options.url)") 116 | case .Connected: 117 | logger.info("Connected") 118 | case .Fatal: 119 | logger.info("Fatally closed") 120 | fatallyClose() 121 | } 122 | } 123 | 124 | func messageReceived(message: NetworkMessage) { 125 | switch message { 126 | case let replyMessage as ReplyMessage: 127 | didReceiveMessageResponseWaitingReply(replyMessage) 128 | default: 129 | break 130 | } 131 | } 132 | 133 | func connect() { 134 | adapter.connect() 135 | } 136 | 137 | func disconnect() { 138 | adapter.disconnect() 139 | } 140 | 141 | func reconnect() { 142 | adapter.reconnect() 143 | } 144 | 145 | func sendMessage(message: NetworkMessage) { 146 | adapter.sendMessage(message) 147 | } 148 | 149 | func sendMessage(message: NetworkMessage, withCallback: ReplyCallback) { 150 | adapter.sendMessage(message) 151 | didSendMessageWaitingReply(message.index, withCallback: withCallback) 152 | } 153 | 154 | func didSendMessageWaitingReply(index: UInt, withCallback: ReplyCallback) { 155 | waitingReply[index] = withCallback 156 | onWaitingRepliesCountChanged.fire(UInt(waitingReply.count)) 157 | } 158 | 159 | func didReceiveMessageResponseWaitingReply(replyMessage: ReplyMessage) { 160 | if let callback = waitingReply[replyMessage.replyTo] { 161 | callback(replyMessage) 162 | waitingReply.removeValueForKey(replyMessage.replyTo) 163 | onWaitingRepliesCountChanged.fire(UInt(waitingReply.count)) 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /JetstreamDemos/JetstreamDemos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /JetstreamDemos/JetstreamDemos.xcodeproj/xcshareddata/xcschemes/JetstreamDemos.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | 66 | 75 | 76 | 82 | 83 | 84 | 85 | 86 | 87 | 93 | 94 | 100 | 101 | 102 | 103 | 105 | 106 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /JetstreamDemos/JetstreamDemos/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import UIKit 26 | import Jetstream 27 | 28 | @UIApplicationMain 29 | class AppDelegate: UIResponder, UIApplicationDelegate { 30 | var window: UIWindow? 31 | 32 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 33 | Jetstream.Logging.enableAll() 34 | return true 35 | } 36 | 37 | func applicationWillResignActive(application: UIApplication) { 38 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 39 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 40 | } 41 | 42 | func applicationDidEnterBackground(application: UIApplication) { 43 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 44 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 45 | } 46 | 47 | func applicationWillEnterForeground(application: UIApplication) { 48 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 49 | } 50 | 51 | func applicationDidBecomeActive(application: UIApplication) { 52 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 53 | } 54 | 55 | func applicationWillTerminate(application: UIApplication) { 56 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /JetstreamDemos/JetstreamDemos/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /JetstreamDemos/JetstreamDemos/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /JetstreamDemos/JetstreamDemos/Canvas.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Canvas.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import Foundation 26 | import Jetstream 27 | 28 | class Canvas: ModelObject { 29 | dynamic var shapes = [Shape]() 30 | } 31 | -------------------------------------------------------------------------------- /JetstreamDemos/JetstreamDemos/DemosListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemosListViewController.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import UIKit 26 | 27 | class DemosListCollectionViewCell : UICollectionViewCell { 28 | @IBOutlet weak var titleLabel: UILabel! 29 | } 30 | 31 | class DemosListViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate { 32 | enum Demo { 33 | case ShapesDemo 34 | } 35 | 36 | private var demos: [(Demo, String)] = [(.ShapesDemo, "Drag shapes")] 37 | 38 | func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 39 | return self.demos.count 40 | } 41 | 42 | func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { 43 | let reuseIdentifier = "DemosListCollectionViewCell" 44 | var demo = self.demos[indexPath.row] 45 | var cell: DemosListCollectionViewCell 46 | cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as DemosListCollectionViewCell 47 | cell.titleLabel.text = demo.1; 48 | return cell 49 | } 50 | 51 | func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) { 52 | var demo = self.demos[indexPath.row] 53 | switch demo.0 { 54 | case .ShapesDemo: 55 | self.performSegueWithIdentifier("ShapesDemoSegue", sender: self) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /JetstreamDemos/JetstreamDemos/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extensions.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import Foundation 26 | import UIKit 27 | import ObjectiveC 28 | 29 | let loaderKey = UnsafePointer() 30 | 31 | extension UIViewController { 32 | var host: String { 33 | get { 34 | var result: AnyObject? = NSBundle.mainBundle().infoDictionary!["JetstreamServer"] 35 | if let host = result as? String { 36 | return host 37 | } else { 38 | return "localhost" 39 | } 40 | } 41 | } 42 | 43 | func showLoader() { 44 | if loader != nil { 45 | hideLoader() 46 | } 47 | var size = UIScreen.mainScreen().bounds.size 48 | loader = UIView(frame: CGRectMake(0, 0, size.width, size.height)) 49 | loader?.backgroundColor = UIColor.blackColor().colorWithAlphaComponent(0.6) 50 | navigationController?.view.addSubview(loader!) 51 | 52 | let activityIndicatorView = UIActivityIndicatorView() 53 | loader?.addSubview(activityIndicatorView) 54 | activityIndicatorView.center = loader!.center 55 | activityIndicatorView.startAnimating() 56 | } 57 | 58 | func hideLoader() { 59 | if let view: UIView = loader { 60 | view.hidden = true 61 | view.removeFromSuperview() 62 | loader = nil 63 | } 64 | } 65 | 66 | func alertError(title: String, message: String) { 67 | hideLoader() 68 | 69 | let alert = UIAlertView( 70 | title: "Error", 71 | message: message, 72 | delegate: nil, 73 | cancelButtonTitle: "Ok") 74 | alert.show() 75 | } 76 | 77 | var loader: UIView? { 78 | get { 79 | if let definiteSelf: AnyObject! = self as AnyObject! { 80 | if let value = objc_getAssociatedObject(definiteSelf, loaderKey) as? UIView { 81 | return value 82 | } 83 | } 84 | return nil 85 | } 86 | set(newValue) { 87 | if let definiteSelf: AnyObject! = self as AnyObject! { 88 | objc_setAssociatedObject(definiteSelf, loaderKey, newValue, UInt(OBJC_ASSOCIATION_RETAIN_NONATOMIC)) 89 | } 90 | } 91 | } 92 | } 93 | 94 | extension UIColor { 95 | class func colorWithHexString(hex: String) -> UIColor { 96 | let whitespace = NSCharacterSet.whitespaceAndNewlineCharacterSet() 97 | var colorString: NSString = hex.stringByTrimmingCharactersInSet(whitespace).uppercaseString 98 | 99 | if colorString.hasPrefix("#") { 100 | colorString = colorString.substringFromIndex(1) 101 | } 102 | 103 | if colorString.length != 6 { 104 | return UIColor.grayColor() 105 | } 106 | 107 | var rString: NSString = colorString.substringToIndex(2) 108 | var gString: NSString = colorString.substringFromIndex(2) 109 | gString = gString.substringToIndex(2) 110 | var bString: NSString = colorString.substringFromIndex(4) 111 | bString = bString.substringToIndex(2) 112 | 113 | var r: CUnsignedInt = 0, g: CUnsignedInt = 0, b: CUnsignedInt = 0 114 | NSScanner(string: rString).scanHexInt(&r) 115 | NSScanner(string: gString).scanHexInt(&g) 116 | NSScanner(string: bString).scanHexInt(&b) 117 | 118 | var red = CGFloat(r) / 255.0 119 | var green = CGFloat(g) / 255.0 120 | var blue = CGFloat(b) / 255.0 121 | return UIColor(red: red, green: green, blue: blue, alpha: 1.0) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /JetstreamDemos/JetstreamDemos/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /JetstreamDemos/JetstreamDemos/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JetstreamServer 6 | http://localhost:3000 7 | CFBundleDevelopmentRegion 8 | en 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | com.uber.$(PRODUCT_NAME:rfc1034identifier) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /JetstreamDemos/JetstreamDemos/Shape.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Shape.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import Foundation 26 | import Jetstream 27 | 28 | class Shape: ModelObject { 29 | dynamic var x: CGFloat = 100 30 | dynamic var y: CGFloat = 100 31 | dynamic var width: CGFloat = 100 32 | dynamic var height: CGFloat = 100 33 | dynamic var color: UIColor = shapeColors[0] 34 | } 35 | 36 | let shapeColors = [ 37 | "#1dd2af", "#19b698", "#40d47e", "#2cc36b", "#4aa3df", "#2e8ece", 38 | "#a66bbe", "#9b50ba", "#3d566e", "#354b60", "#f2ca27", "#f4a62a", 39 | "#e98b39", "#ec5e00", "#ea6153", "#d14233", "#8c9899" 40 | ].map { UIColor.colorWithHexString($0) } 41 | -------------------------------------------------------------------------------- /JetstreamDemos/JetstreamDemos/ShapeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShapeView.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import Foundation 26 | import UIKit 27 | 28 | public class ShapeView: UIView, UIGestureRecognizerDelegate { 29 | var shape: Shape = Shape() { 30 | willSet { 31 | shape.removeObservers(self) 32 | } 33 | didSet { 34 | shape.observeChange(self, keys: ["x", "y", "width", "height"]) { [weak self] in 35 | if let this = self { 36 | this.updateView() 37 | } 38 | } 39 | shape.observeDetach(self) { [weak self] (scope) in 40 | if let this = self { 41 | this.removeFromSuperview() 42 | } 43 | } 44 | updateView() 45 | } 46 | } 47 | 48 | public required init(coder: NSCoder) { 49 | super.init(coder: coder) 50 | } 51 | 52 | init(shape: Shape) { 53 | super.init(frame: CGRectZero) 54 | backgroundColor = shape.color 55 | layer.cornerRadius = 5 56 | let panRecognizer = UIPanGestureRecognizer(target: self, action: Selector("handlePan:")) 57 | self.addGestureRecognizer(panRecognizer) 58 | 59 | let tapRecognizer = UITapGestureRecognizer(target: self, action: Selector("handleTap:")) 60 | self.addGestureRecognizer(tapRecognizer) 61 | setShape(shape) 62 | } 63 | 64 | private func setShape(shape: Shape) { 65 | self.shape = shape 66 | } 67 | 68 | dynamic public func handlePan(recognizer:UIPanGestureRecognizer) { 69 | let translation = recognizer.translationInView(self) 70 | recognizer.setTranslation(CGPointZero, inView: self) 71 | 72 | shape.x += translation.x 73 | shape.y += translation.y 74 | } 75 | 76 | dynamic public func handleTap(recognizer:UITapGestureRecognizer) { 77 | shape.detach() 78 | } 79 | 80 | func updateView() { 81 | self.frame = CGRect( 82 | x: CGFloat(shape.x), 83 | y: CGFloat(shape.y), 84 | width: CGFloat(shape.width), 85 | height: CGFloat(shape.height)) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /JetstreamDemos/JetstreamDemos/ShapesDemoViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShapesDemoViewController.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import Foundation 26 | import UIKit 27 | import Jetstream 28 | 29 | class ShapesDemoViewController: UIViewController, NSURLConnectionDataDelegate { 30 | var scope = Scope(name: "Canvas") 31 | var canvas = Canvas() 32 | 33 | var client: Client? 34 | var session: Session? 35 | 36 | override func viewDidLoad() { 37 | super.viewDidLoad() 38 | title = "Shapes Demo" 39 | 40 | let tapRecognizer = UITapGestureRecognizer(target: self, action: Selector("handleTap:")) 41 | self.view.addGestureRecognizer(tapRecognizer) 42 | 43 | scope.root = canvas 44 | canvas.observeCollectionAdd(self, key: "shapes") { (element: Shape) in 45 | let shapeView = ShapeView(shape: element) 46 | self.view.addSubview(shapeView) 47 | } 48 | } 49 | 50 | func handleTap(recognizer: UITapGestureRecognizer) { 51 | let shape = Shape() 52 | shape.color = shapeColors[Int(arc4random_uniform(UInt32(shapeColors.count)))] 53 | var point = recognizer.locationInView(self.view) 54 | shape.x = point.x - shape.width / 2 55 | shape.y = point.y - shape.height / 2 56 | canvas.shapes.append(shape) 57 | } 58 | 59 | override func viewWillAppear(animated: Bool) { 60 | super.viewWillAppear(animated) 61 | showLoader() 62 | 63 | let options = WebSocketConnectionOptions(url: NSURL(string: "ws://" + host + ":3000")!) 64 | client = Client(transportAdapterFactory: { WebSocketTransportAdapter(options: options) }) 65 | client?.connect() 66 | client?.onSession.listenOnce(self) { [unowned self] in self.sessionDidStart($0) } 67 | } 68 | 69 | func sessionDidStart(session: Session) { 70 | self.session = session 71 | session.fetch(scope) { error in 72 | if error != nil { 73 | self.alertError("Error fetching scope", message: "\(error)") 74 | } else { 75 | self.hideLoader() 76 | } 77 | } 78 | } 79 | 80 | override func viewWillDisappear(animated: Bool) { 81 | super.viewWillDisappear(animated) 82 | hideLoader() 83 | } 84 | 85 | override func viewDidDisappear(animated: Bool) { 86 | super.viewDidDisappear(animated) 87 | client?.onSession.removeListener(self) 88 | client?.close() 89 | client = nil 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /JetstreamDemos/JetstreamDemosTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | com.uber.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /JetstreamDemos/JetstreamDemosTests/JetstreamDemosTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JetstreamDemosTests.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import UIKit 26 | import XCTest 27 | 28 | class JetstreamDemosTests: XCTestCase { 29 | 30 | override func setUp() { 31 | super.setUp() 32 | // Put setup code here. This method is called before the invocation of each test method in the class. 33 | } 34 | 35 | override func tearDown() { 36 | // Put teardown code here. This method is called after the invocation of each test method in the class. 37 | super.tearDown() 38 | } 39 | 40 | func testExample() { 41 | // This is an example of a functional test case. 42 | XCTAssert(true, "Pass") 43 | } 44 | 45 | func testPerformanceExample() { 46 | // This is an example of a performance test case. 47 | self.measureBlock() { 48 | // Put the code you want to measure the time of here. 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /JetstreamTests/ChangeSetQueueTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChangeSetQueueTests.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import XCTest 26 | @testable import Jetstream 27 | 28 | class ChangeSetQueueTests: XCTestCase { 29 | var root = TestModel() 30 | var child = TestModel() 31 | var scope = Scope(name: "Testing") 32 | var queue = ChangeSetQueue() 33 | 34 | override func setUp() { 35 | root = TestModel() 36 | child = TestModel() 37 | scope = Scope(name: "Testing") 38 | root.setScopeAndMakeRootModel(scope) 39 | } 40 | 41 | override func tearDown() { 42 | super.tearDown() 43 | } 44 | 45 | func testCompleting() { 46 | root.integer = 1 47 | root.float32 = 1.0 48 | root.string = "test 1" 49 | scope.getAndClearSyncFragments() 50 | 51 | root.integer = 2 52 | root.float32 = 2.0 53 | root.string = "test 2" 54 | let changeSet = ChangeSet(syncFragments: scope.getAndClearSyncFragments(), scope: scope) 55 | queue.addChangeSet(changeSet) 56 | changeSet.completed() 57 | 58 | XCTAssertEqual(root.integer, 2, "Change set not reverted") 59 | XCTAssertEqual(root.float32, Float(2.0), "Change set not reverted") 60 | XCTAssertEqual(root.string!, "test 2", "Change set not reverted") 61 | XCTAssertEqual(queue.count, 0, "Queue empty") 62 | } 63 | 64 | func testReverting() { 65 | root.integer = 1 66 | root.float32 = 1.0 67 | root.string = "test 1" 68 | scope.getAndClearSyncFragments() 69 | 70 | root.integer = 2 71 | root.float32 = 2.0 72 | root.string = "test 2" 73 | let changeSet = ChangeSet(syncFragments: scope.getAndClearSyncFragments(), scope: scope) 74 | queue.addChangeSet(changeSet) 75 | changeSet.revertOnScope(scope) 76 | 77 | XCTAssertEqual(root.integer, 1, "Change set reverted") 78 | XCTAssertEqual(root.float32, Float(1.0), "Change set reverted") 79 | XCTAssertEqual(root.string!, "test 1", "Change set reverted") 80 | XCTAssertEqual(queue.count, 0, "Queue empty") 81 | } 82 | 83 | func testRebasing() { 84 | root.integer = 1 85 | root.float32 = 1.0 86 | root.string = "test 1" 87 | scope.getAndClearSyncFragments() 88 | 89 | root.integer = 2 90 | root.float32 = 2.0 91 | root.string = "test 2" 92 | let changeSet = ChangeSet(syncFragments: scope.getAndClearSyncFragments(), scope: scope) 93 | queue.addChangeSet(changeSet) 94 | 95 | root.integer = 3 96 | root.float32 = 3.0 97 | root.string = "test 3" 98 | let changeSet2 = ChangeSet(syncFragments: scope.getAndClearSyncFragments(), scope: scope) 99 | queue.addChangeSet(changeSet2) 100 | 101 | changeSet.revertOnScope(scope) 102 | XCTAssertEqual(queue.count, 1, "Queue contains one change set") 103 | XCTAssertEqual(root.integer, 3, "Change set reverted") 104 | XCTAssertEqual(root.float32, Float(3.0), "Change set reverted") 105 | XCTAssertEqual(root.string!, "test 3", "Change set reverted") 106 | 107 | changeSet2.revertOnScope(scope) 108 | 109 | XCTAssertEqual(root.integer, 1, "Change set reverted") 110 | XCTAssertEqual(root.float32, Float(1.0), "Change set reverted") 111 | XCTAssertEqual(root.string!, "test 1", "Change set reverted") 112 | XCTAssertEqual(queue.count, 0, "Queue empty") 113 | } 114 | 115 | func testSubsetRebasing() { 116 | root.integer = 1 117 | root.float32 = 1.0 118 | root.string = "test 1" 119 | scope.getAndClearSyncFragments() 120 | 121 | root.integer = 2 122 | let changeSet = ChangeSet(syncFragments: scope.getAndClearSyncFragments(), scope: scope) 123 | queue.addChangeSet(changeSet) 124 | 125 | root.integer = 3 126 | root.float32 = 3.0 127 | let changeSet2 = ChangeSet(syncFragments: scope.getAndClearSyncFragments(), scope: scope) 128 | queue.addChangeSet(changeSet2) 129 | 130 | root.integer = 4 131 | root.float32 = 4.0 132 | root.string = "test 4" 133 | let changeSet3 = ChangeSet(syncFragments: scope.getAndClearSyncFragments(), scope: scope) 134 | queue.addChangeSet(changeSet3) 135 | 136 | changeSet.revertOnScope(scope) 137 | changeSet2.revertOnScope(scope) 138 | changeSet3.revertOnScope(scope) 139 | 140 | XCTAssertEqual(root.integer, 1, "Change set reverted") 141 | XCTAssertEqual(root.float32, Float(1.0), "Change set reverted") 142 | XCTAssertEqual(root.string!, "test 1", "Change set reverted") 143 | XCTAssertEqual(queue.count, 0, "Queue empty") 144 | } 145 | 146 | func testSupersetRebasing() { 147 | root.integer = 1 148 | root.float32 = 1.0 149 | root.string = "test 1" 150 | scope.getAndClearSyncFragments() 151 | 152 | root.integer = 2 153 | root.float32 = 2.0 154 | root.string = "test 2" 155 | let changeSet = ChangeSet(syncFragments: scope.getAndClearSyncFragments(), scope: scope) 156 | queue.addChangeSet(changeSet) 157 | 158 | root.integer = 3 159 | root.float32 = 3.0 160 | let changeSet2 = ChangeSet(syncFragments: scope.getAndClearSyncFragments(), scope: scope) 161 | queue.addChangeSet(changeSet2) 162 | 163 | root.integer = 4 164 | let changeSet3 = ChangeSet(syncFragments: scope.getAndClearSyncFragments(), scope: scope) 165 | queue.addChangeSet(changeSet3) 166 | 167 | changeSet.revertOnScope(scope) 168 | changeSet2.revertOnScope(scope) 169 | changeSet3.revertOnScope(scope) 170 | 171 | XCTAssertEqual(root.integer, 1, "Change set reverted") 172 | XCTAssertEqual(root.float32, Float(1.0), "Change set reverted") 173 | XCTAssertEqual(root.string!, "test 1", "Change set reverted") 174 | XCTAssertEqual(queue.count, 0, "Queue empty") 175 | } 176 | 177 | func testReverseInBetweenChangeSet() { 178 | root.integer = 1 179 | root.float32 = 1.0 180 | root.string = "test 1" 181 | scope.getAndClearSyncFragments() 182 | 183 | root.integer = 2 184 | root.float32 = 2.0 185 | root.string = "test 2" 186 | let changeSet = ChangeSet(syncFragments: scope.getAndClearSyncFragments(), scope: scope) 187 | queue.addChangeSet(changeSet) 188 | 189 | root.integer = 3 190 | root.float32 = 3.0 191 | root.string = "test 3" 192 | let changeSet2 = ChangeSet(syncFragments: scope.getAndClearSyncFragments(), scope: scope) 193 | queue.addChangeSet(changeSet2) 194 | 195 | changeSet.revertOnScope(scope) 196 | changeSet2.completed() 197 | 198 | XCTAssertEqual(root.integer, 3, "Change set reverted") 199 | XCTAssertEqual(root.float32, Float(3.0), "Change set reverted") 200 | XCTAssertEqual(root.string!, "test 3", "Change set reverted") 201 | XCTAssertEqual(queue.count, 0, "Queue empty") 202 | } 203 | 204 | func testRebasingOverChangeSets() { 205 | root.integer = 1 206 | root.float32 = 1.0 207 | root.string = "test 1" 208 | scope.getAndClearSyncFragments() 209 | 210 | root.integer = 2 211 | root.float32 = 2.0 212 | root.string = "test 2" 213 | let changeSet = ChangeSet(syncFragments: scope.getAndClearSyncFragments(), scope: scope) 214 | queue.addChangeSet(changeSet) 215 | 216 | root.integer = 3 217 | root.string = "test 3" 218 | let changeSet2 = ChangeSet(syncFragments: scope.getAndClearSyncFragments(), scope: scope) 219 | queue.addChangeSet(changeSet2) 220 | 221 | root.integer = 4 222 | root.float32 = 4.0 223 | root.string = "test 4" 224 | let changeSet3 = ChangeSet(syncFragments: scope.getAndClearSyncFragments(), scope: scope) 225 | queue.addChangeSet(changeSet3) 226 | 227 | changeSet.revertOnScope(scope) 228 | changeSet2.revertOnScope(scope) 229 | changeSet3.revertOnScope(scope) 230 | 231 | XCTAssertEqual(root.integer, 1, "Change set reverted") 232 | XCTAssertEqual(root.float32, Float(1.0), "Change set reverted") 233 | XCTAssertEqual(root.string!, "test 1", "Change set reverted") 234 | XCTAssertEqual(queue.count, 0, "Queue empty") 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /JetstreamTests/ChangeSetTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChangeSetTests.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import XCTest 26 | @testable import Jetstream 27 | 28 | class ChangeSetTests: XCTestCase { 29 | var root = TestModel() 30 | var child = TestModel() 31 | var scope = Scope(name: "Testing") 32 | 33 | override func setUp() { 34 | root = TestModel() 35 | child = TestModel() 36 | scope = Scope(name: "Testing") 37 | root.setScopeAndMakeRootModel(scope) 38 | } 39 | 40 | override func tearDown() { 41 | super.tearDown() 42 | } 43 | 44 | func testBasicReversal() { 45 | root.integer = 10 46 | root.float32 = 10.0 47 | root.string = "test" 48 | scope.getAndClearSyncFragments() 49 | 50 | root.integer = 20 51 | root.float32 = 20.0 52 | root.string = "test 2" 53 | let changeSet = ChangeSet(syncFragments: scope.getAndClearSyncFragments(), scope: scope) 54 | changeSet.revertOnScope(scope) 55 | 56 | XCTAssertEqual(scope.getAndClearSyncFragments().count, 0, "Should have reverted without generating Sync fragments") 57 | XCTAssertEqual(root.integer, 10, "Change set reverted") 58 | XCTAssertEqual(root.float32, Float(10.0), "Change set reverted") 59 | XCTAssertEqual(root.string!, "test", "Change set reverted") 60 | } 61 | 62 | func testModelReversal() { 63 | root.childModel = child 64 | let changeSet = ChangeSet(syncFragments: scope.getAndClearSyncFragments(), scope: scope) 65 | 66 | changeSet.revertOnScope(scope) 67 | 68 | XCTAssertEqual(scope.getAndClearSyncFragments().count, 0, "Should have reverted without generating Sync fragments") 69 | XCTAssert(root.childModel == nil, "Change set reverted") 70 | XCTAssertEqual(scope.modelObjects.count, 1 , "Scope knows correct models") 71 | } 72 | 73 | func testModelReapplying() { 74 | root.childModel = child 75 | scope.getAndClearSyncFragments() 76 | 77 | root.childModel = nil 78 | let changeSet = ChangeSet(syncFragments: scope.getAndClearSyncFragments(), scope: scope) 79 | 80 | changeSet.revertOnScope(scope) 81 | 82 | XCTAssertEqual(scope.getAndClearSyncFragments().count, 0, "Should have reverted without generating Sync fragments") 83 | XCTAssert(root.childModel == child, "Change set reverted") 84 | XCTAssertEqual(scope.modelObjects.count, 2 , "Scope knows correct models") 85 | } 86 | 87 | func testArrayReversal() { 88 | root.array.append(child) 89 | let changeSet = ChangeSet(syncFragments: scope.getAndClearSyncFragments(), scope: scope) 90 | XCTAssertEqual(changeSet.syncFragments.count, 2, "Correct number of sync fragments") 91 | 92 | changeSet.revertOnScope(scope) 93 | 94 | XCTAssertEqual(scope.getAndClearSyncFragments().count, 0, "Should have reverted without generating Sync fragments") 95 | XCTAssertEqual(root.array.count, 0, "Change set reverted") 96 | XCTAssertEqual(scope.modelObjects.count, 1 , "Scope knows correct models") 97 | } 98 | 99 | func testMovingChildModel() { 100 | root.childModel = child 101 | var changeSet = ChangeSet(syncFragments: scope.getAndClearSyncFragments(), scope: scope) 102 | XCTAssertEqual(changeSet.syncFragments.count, 2, "Correct number of sync fragments") 103 | 104 | root.childModel = nil 105 | root.childModel2 = child 106 | 107 | changeSet = ChangeSet(syncFragments: scope.getAndClearSyncFragments(), scope: scope) 108 | XCTAssertEqual(changeSet.syncFragments.count, 1, "No add fragment created") 109 | 110 | root.childModel2 = nil 111 | 112 | changeSet = ChangeSet(syncFragments: scope.getAndClearSyncFragments(), scope: scope) 113 | XCTAssertEqual(changeSet.syncFragments.count, 1, "No add fragment created") 114 | 115 | root.childModel = child 116 | 117 | changeSet = ChangeSet(syncFragments: scope.getAndClearSyncFragments(), scope: scope) 118 | XCTAssertEqual(changeSet.syncFragments.count, 2, "Add fragment created") 119 | } 120 | 121 | func testRemovalOfAddFragmentsWhenNotAttachedToAnyProperties() { 122 | root.localModel = TestModel() 123 | var changeSet = ChangeSet(syncFragments: scope.getAndClearSyncFragments(), scope: scope) 124 | XCTAssertEqual(changeSet.syncFragments.count, 0, "Add fragment should have been removed") 125 | 126 | root.localModelArray = [TestModel(), TestModel()] 127 | changeSet = ChangeSet(syncFragments: scope.getAndClearSyncFragments(), scope: scope) 128 | XCTAssertEqual(changeSet.syncFragments.count, 0, "Add fragment should have been removed") 129 | 130 | root.localModel = TestModel() 131 | root.localModelArray = [TestModel(), TestModel()] 132 | changeSet = ChangeSet(syncFragments: scope.getAndClearSyncFragments(), scope: scope) 133 | XCTAssertEqual(changeSet.syncFragments.count, 0, "Add fragment should have been removed") 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /JetstreamTests/ClientTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClientTests.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import XCTest 26 | @testable import Jetstream 27 | 28 | class ClientTests: XCTestCase { 29 | func testOnWaitingRepliesCountChanged() { 30 | let client = Client(transportAdapterFactory: { TestTransportAdapter() }) 31 | var waitingReplyCount = 0 32 | client.onWaitingRepliesCountChanged.listen(self) { count in 33 | waitingReplyCount = Int(count) 34 | } 35 | 36 | client.transport.sendMessage(PingMessage(index: 0, ack: 0, resendMissing: false)) { response in } 37 | XCTAssertEqual(waitingReplyCount, 1, "Did fire waiting reply with count 1") 38 | 39 | client.transport.sendMessage(PingMessage(index: 1, ack: 0, resendMissing: false)) { response in } 40 | XCTAssertEqual(waitingReplyCount, 2, "Did fire waiting reply with count 2") 41 | 42 | client.transport.messageReceived(ReplyMessage(index: 0, replyTo: 0)) 43 | XCTAssertEqual(waitingReplyCount, 1, "Did fire waiting reply with count 1") 44 | 45 | client.transport.messageReceived(ReplyMessage(index: 1, replyTo: 1)) 46 | XCTAssertEqual(waitingReplyCount, 0, "Did fire waiting reply with count 0") 47 | } 48 | 49 | func testRestartSessionOnFatalError() { 50 | var adapter = TestTransportAdapter() 51 | let onSendMessage = Signal() 52 | 53 | var factoryCallCount: Int = 0 54 | var transportAdapterDisconnectCallCount: Int = 0 55 | var sendMessageCallCount = 0 56 | 57 | let client = Client(transportAdapterFactory: { 58 | factoryCallCount++ 59 | adapter = TestTransportAdapter() 60 | adapter.onDisconnectCalled.listen(self) { 61 | transportAdapterDisconnectCallCount++ 62 | return 63 | } 64 | adapter.onSendMessageCalled.listen(self) { networkMessage in 65 | sendMessageCallCount++ 66 | onSendMessage.fire(networkMessage) 67 | } 68 | return adapter 69 | }) 70 | 71 | let scope1 = Scope(name: "Testing1") 72 | let scope1FetchParams: [String: AnyObject] = ["param0": 0, "param1": "param"] 73 | 74 | let scope2 = Scope(name: "Testing2") 75 | let scope2FetchParams: [String: AnyObject] = ["param0": 0, "param1": "param"] 76 | 77 | let scopesAndFetchParams: [(Scope, [String: AnyObject])] = [ 78 | (scope1, scope1FetchParams), 79 | (scope2, scope2FetchParams) 80 | ] 81 | 82 | let sessionToken1 = "sessionToken1" 83 | let sessionToken2 = "sessionToken2" 84 | 85 | var msg = SessionCreateReplyMessage(index: 1, sessionToken: sessionToken1, error: nil) 86 | client.receivedMessage(msg) 87 | client.session!.scopeAttach(scope1, scopeIndex: 0, fetchParams: scope1FetchParams) 88 | client.session!.scopeAttach(scope2, scopeIndex: 1, fetchParams: scope2FetchParams) 89 | 90 | XCTAssertEqual(client.session!.token, sessionToken1, "Did set correct session token") 91 | XCTAssertEqual(client.session!.scopes.count, 2, "Did load scopes") 92 | 93 | // Capture state before fatal close 94 | let transportBeforeFatalClose = client.transport 95 | let sendMessageCountBeforeFatalClose = sendMessageCallCount 96 | var sessionCreateMessage: NetworkMessage? = nil 97 | onSendMessage.listenOnce(self) { networkMessage in 98 | sessionCreateMessage = networkMessage 99 | } 100 | 101 | // Simulate fatal close 102 | client.transport.onStatusChanged.fire(.Fatal) 103 | 104 | XCTAssertEqual(transportAdapterDisconnectCallCount, 1, "Transport was disconnected") 105 | XCTAssertEqual(factoryCallCount, 2, "Did create a new transport") 106 | XCTAssertFalse(transportBeforeFatalClose === client.transport, "Did create a new transport") 107 | 108 | // Simulate come back online with new transport adapter 109 | adapter.status = .Connected 110 | 111 | XCTAssertNotNil(sessionCreateMessage, "Did send a session create message on the new transport") 112 | XCTAssertEqual(sendMessageCountBeforeFatalClose + 1, sendMessageCallCount, "Did send a session create message on the new transport") 113 | 114 | var sentMessages = [NetworkMessage]() 115 | onSendMessage.listen(self) { networkMessage in 116 | sentMessages.append(networkMessage) 117 | } 118 | 119 | msg = SessionCreateReplyMessage(index: 1, sessionToken: sessionToken2, error: nil) 120 | client.receivedMessage(msg) 121 | onSendMessage.removeListener(self) 122 | 123 | XCTAssertEqual(client.session!.token, sessionToken2, "Did set correct new session token") 124 | XCTAssertEqual(client.session!.scopes.count, 0, "Still loading scopes") 125 | 126 | XCTAssertEqual(sentMessages.count, 2, "Did send messages to fetch scopes") 127 | var index = 0 128 | for message in sentMessages { 129 | if let fetchMessage = message as? ScopeFetchMessage { 130 | var matched = scopesAndFetchParams.filter { $0.0.name == fetchMessage.name } 131 | 132 | if matched.count == 1 { 133 | let scope = matched[0].0 134 | let params = matched[0].1 135 | XCTAssertEqual(fetchMessage.name, scope.name, "Did send correct fetch name") 136 | XCTAssertEqual(fetchMessage.params.count, params.count, "Did send correct fetch params count") 137 | if fetchMessage.params.count != params.count { 138 | continue 139 | } 140 | for (key, value) in params { 141 | let fetchMessageValue: AnyObject = fetchMessage.params[key]! 142 | XCTAssertTrue(fetchMessageValue === value, "Did send correct fetch params") 143 | } 144 | } else { 145 | XCTAssert(false, "Sent message was a scope fetch message for non-existing scope") 146 | } 147 | } else { 148 | XCTAssert(false, "Sent message was not a scope fetch message") 149 | } 150 | index++ 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /JetstreamTests/ConstraintTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConstraintTests.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import XCTest 26 | @testable import Jetstream 27 | 28 | class ConstraintTests: XCTestCase { 29 | func testMultipleMatching() { 30 | let json: [[String: AnyObject]] = [ 31 | [ 32 | "type": "add", 33 | "uuid": NSUUID().UUIDString, 34 | "clsName": "TestModel", 35 | "properties": ["string": "set correctly"] 36 | ], 37 | [ 38 | "type": "add", 39 | "uuid": NSUUID().UUIDString, 40 | "clsName": "AnotherTestModel", 41 | "properties": ["anotherString": "set correctly"] 42 | ], 43 | [ 44 | "type": "change", 45 | "uuid": NSUUID().UUIDString, 46 | "clsName": "TestModel", 47 | "properties": ["integer": 3] 48 | ] 49 | ] 50 | let fragments = json.map { SyncFragment.unserialize($0)! } 51 | 52 | 53 | let constraints1: [String: AnyObject] = [ 54 | "string": "set correctly" 55 | ] 56 | let constraints2: [String: AnyObject] = [ 57 | "anotherString": "set correctly" 58 | ] 59 | let constraints3: [String: AnyObject] = [ 60 | "integer": 3 61 | ] 62 | let constraints = [ 63 | Constraint(type: .Add, clsName: "TestModel", properties: constraints1, allowAdditionalProperties: false), 64 | Constraint(type: .Add, clsName: "AnotherTestModel", properties: constraints2, allowAdditionalProperties: false), 65 | Constraint(type: .Change, clsName: "TestModel", properties: constraints3, allowAdditionalProperties: false), 66 | ] 67 | 68 | XCTAssertTrue(Constraint.matchesAllConstraints(constraints, syncFragments: fragments), "Constraint should match fragment") 69 | } 70 | 71 | func testSimpleValueExistsChangeMatching() { 72 | let json: [String: AnyObject] = [ 73 | "type": "change", 74 | "uuid": NSUUID().UUIDString, 75 | "clsName": "TestModel", 76 | "properties": ["string": "set new value", "integer": NSNull(), "childModel":"11111-11111-11111-11111-1111"] 77 | ] 78 | let fragment = SyncFragment.unserialize(json) 79 | 80 | let constraint = Constraint(type: .Change, clsName: "TestModel", properties: [ 81 | "string": HasNewValuePropertyConstraint(), 82 | "integer": HasNewValuePropertyConstraint(), 83 | "childModel": HasNewValuePropertyConstraint() 84 | ]) 85 | XCTAssertTrue(constraint.matches(fragment!), "Constraint should match fragment") 86 | } 87 | 88 | 89 | 90 | func testSimpleAddMatching() { 91 | let json: [String: AnyObject] = [ 92 | "type": "add", 93 | "uuid": NSUUID().UUIDString, 94 | "clsName": "TestModel", 95 | "properties": ["string": "set correctly"] 96 | ] 97 | let fragment = SyncFragment.unserialize(json) 98 | 99 | let constraint = Constraint(type: .Add, clsName: "TestModel") 100 | XCTAssertTrue(constraint.matches(fragment!), "Constraint should match fragment") 101 | } 102 | 103 | func testSimpleAddWithPropertiesMatching() { 104 | let json: [String: AnyObject] = [ 105 | "type": "add", 106 | "uuid": NSUUID().UUIDString, 107 | "clsName": "TestModel", 108 | "properties": ["string": "set correctly"] 109 | ] 110 | let fragment = SyncFragment.unserialize(json) 111 | 112 | let constraint = Constraint(type: .Add, clsName: "TestModel", properties: ["string": "set correctly"], allowAdditionalProperties: false) 113 | XCTAssertTrue(constraint.matches(fragment!), "Constraint should match fragment") 114 | } 115 | 116 | func testSimpleAddWithPropertiesMatchingWithBadAdditionalProperties() { 117 | let json: [String: AnyObject] = [ 118 | "type": "add", 119 | "uuid": NSUUID().UUIDString, 120 | "clsName": "TestModel", 121 | "properties": ["string": "set correctly", "integer": 3] 122 | ] 123 | let fragment = SyncFragment.unserialize(json) 124 | 125 | let constraint = Constraint(type: .Add, clsName: "TestModel", properties: ["string": "set correctly"], allowAdditionalProperties: false) 126 | XCTAssertFalse(constraint.matches(fragment!), "Constraint should match fragment") 127 | } 128 | 129 | func testSimpleAddWithPropertiesMatchingWithAllowedAdditionalProperties() { 130 | let json: [String: AnyObject] = [ 131 | "type": "add", 132 | "uuid": NSUUID().UUIDString, 133 | "clsName": "TestModel", 134 | "properties": ["string": "set correctly", "integer": 3] 135 | ] 136 | let fragment = SyncFragment.unserialize(json) 137 | 138 | let constraint = Constraint(type: .Add, clsName: "TestModel", properties: ["string": "set correctly"], allowAdditionalProperties: true) 139 | XCTAssertTrue(constraint.matches(fragment!), "Constraint should match fragment") 140 | } 141 | 142 | func testSimpleAddWithArrayInsertPropertyMatching() { 143 | let json: [String: AnyObject] = [ 144 | "type": "add", 145 | "uuid": NSUUID().UUIDString, 146 | "clsName": "TestModel", 147 | "properties": ["string": "set correctly", "array": [NSUUID().UUIDString]] 148 | ] 149 | let fragment = SyncFragment.unserialize(json) 150 | 151 | let constraint = Constraint(type: .Add, clsName: "TestModel", properties: [ 152 | "string": "set correctly", 153 | "array": ArrayPropertyConstraint(type: .Insert) 154 | ], allowAdditionalProperties: false) 155 | XCTAssertTrue(constraint.matches(fragment!), "Constraint should match fragment") 156 | } 157 | 158 | func testSimpleChangeWithArrayInsertPropertyMatching() { 159 | let json: [String: AnyObject] = [ 160 | "type": "change", 161 | "uuid": NSUUID().UUIDString, 162 | "clsName": "TestModel", 163 | "properties": ["string": "set correctly", "array": [NSUUID().UUIDString]] 164 | ] 165 | let fragment = SyncFragment.unserialize(json) 166 | fragment!.originalProperties = [ 167 | "array": [] 168 | ] 169 | 170 | let constraint = Constraint(type: .Change, clsName: "TestModel", properties: [ 171 | "string": "set correctly", 172 | "array": ArrayPropertyConstraint(type: .Insert) 173 | ], allowAdditionalProperties: false) 174 | XCTAssertTrue(constraint.matches(fragment!), "Constraint should match fragment") 175 | } 176 | 177 | func testSimpleChangeWithArrayInsertPropertyNotMatching() { 178 | let json: [String: AnyObject] = [ 179 | "type": "change", 180 | "uuid": NSUUID().UUIDString, 181 | "clsName": "TestModel", 182 | "properties": ["string": "set correctly", "array": [NSUUID().UUIDString]] 183 | ] 184 | let fragment = SyncFragment.unserialize(json) 185 | fragment!.originalProperties = [ 186 | "array": [NSUUID().UUIDString] 187 | ] 188 | 189 | let constraint = Constraint(type: .Change, clsName: "TestModel", properties: [ 190 | "string": "set correctly", 191 | "array": ArrayPropertyConstraint(type: .Insert) 192 | ], allowAdditionalProperties: false) 193 | XCTAssertFalse(constraint.matches(fragment!), "Constraint should match fragment") 194 | } 195 | 196 | func testSimpleChangeWithArrayRemovePropertyMatching() { 197 | let json: [String: AnyObject] = [ 198 | "type": "change", 199 | "uuid": NSUUID().UUIDString, 200 | "clsName": "TestModel", 201 | "properties": ["string": "set correctly", "array": []] 202 | ] 203 | let fragment = SyncFragment.unserialize(json) 204 | fragment!.originalProperties = [ 205 | "array": [NSUUID().UUIDString] 206 | ] 207 | 208 | let constraint = Constraint(type: .Change, clsName: "TestModel", properties: [ 209 | "string": "set correctly", 210 | "array": ArrayPropertyConstraint(type: .Remove) 211 | ], allowAdditionalProperties: false) 212 | XCTAssertTrue(constraint.matches(fragment!), "Constraint should match fragment") 213 | } 214 | 215 | func testSimpleChangeWithArrayRemovePropertyNotMatching() { 216 | let json: [String: AnyObject] = [ 217 | "type": "change", 218 | "uuid": NSUUID().UUIDString, 219 | "clsName": "TestModel", 220 | "properties": ["string": "set correctly", "array": [NSUUID().UUIDString]] 221 | ] 222 | let fragment = SyncFragment.unserialize(json) 223 | fragment!.originalProperties = [ 224 | "array": [NSUUID().UUIDString] 225 | ] 226 | 227 | let constraint = Constraint(type: .Change, clsName: "TestModel", properties: [ 228 | "string": "set correctly", 229 | "array": ArrayPropertyConstraint(type: .Remove) 230 | ], allowAdditionalProperties: false) 231 | XCTAssertFalse(constraint.matches(fragment!), "Constraint should match fragment") 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /JetstreamTests/DependencyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DependencyTests.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import XCTest 26 | @testable import Jetstream 27 | 28 | class DependencyTests: XCTestCase { 29 | var testModel = TestModel() 30 | var anotherTestModel = AnotherTestModel() 31 | 32 | override func setUp() { 33 | testModel = TestModel() 34 | anotherTestModel = AnotherTestModel() 35 | } 36 | 37 | override func tearDown() { 38 | super.tearDown() 39 | } 40 | 41 | func testDependentListeners() { 42 | var fireCount1 = 0 43 | var fireCount2 = 0 44 | 45 | testModel.observeChangeImmediately(self, key: "compositeProperty") { () -> Void in 46 | fireCount1 += 1 47 | } 48 | 49 | anotherTestModel.observeChangeImmediately(self, key: "anotherCompositeProperty") { () -> Void in 50 | fireCount2 += 1 51 | } 52 | 53 | testModel.float32 = 2.0 54 | testModel.float32 = 3.0 55 | testModel.anotherArray = [anotherTestModel] 56 | 57 | XCTAssertEqual(fireCount1, 3, "Dispatched three times") 58 | XCTAssertEqual(fireCount2, 0, "Not dispatched") 59 | 60 | anotherTestModel.anotherString = "kiva" 61 | anotherTestModel.anotherInteger = 1 62 | 63 | XCTAssertEqual(fireCount1, 3, "Dispatched three times") 64 | XCTAssertEqual(fireCount2, 2, "Dispatched twice") 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /JetstreamTests/ImmediatePropertyListeners.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImmediatePropertyListeners.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import XCTest 26 | @testable import Jetstream 27 | 28 | class ImmediatePropertyListenerTests: XCTestCase { 29 | func testSpecificPropertyListeners() { 30 | let model = TestModel() 31 | var dispatchCount = 0 32 | 33 | model.observeChange(self, key: "string") { 34 | dispatchCount += 1 35 | } 36 | 37 | model.string = "test" 38 | model.string = "test 2" 39 | model.integer = 1 40 | model.float32 = 2.5 41 | 42 | delayTest(self, delay: 0.01) { 43 | XCTAssertEqual(dispatchCount, 1 , "Dispatched once") 44 | } 45 | } 46 | 47 | func testMultiPropertyListeners() { 48 | let model = TestModel() 49 | var dispatchCount = 0 50 | 51 | model.observeChange(self, keys: ["string", "integer"]) { 52 | dispatchCount += 1 53 | } 54 | 55 | model.string = "test" 56 | model.integer = 1 57 | model.string = "test" 58 | model.integer = 1 59 | model.float32 = 2.5 60 | 61 | delayTest(self, delay: 0.01) { 62 | XCTAssertEqual(dispatchCount, 1 , "Dispatched once") 63 | } 64 | } 65 | 66 | func testNoDispatchForNoChange() { 67 | let model = TestModel() 68 | var dispatchCount = 0 69 | 70 | model.observeChange(self) { 71 | dispatchCount += 1 72 | } 73 | 74 | model.integer = 10 75 | model.integer = 10 76 | 77 | model.uint = 10 78 | model.uint = 10 79 | 80 | model.uint8 = 10 81 | model.uint8 = 10 82 | 83 | model.int8 = 10 84 | model.int8 = 10 85 | 86 | model.uint16 = 10 87 | model.uint16 = 10 88 | 89 | model.int16 = 10 90 | model.int16 = 10 91 | 92 | model.uint32 = 10 93 | model.uint32 = 10 94 | 95 | model.int32 = 10 96 | model.int32 = 10 97 | 98 | model.uint64 = 10 99 | model.uint64 = 10 100 | 101 | model.int64 = 10 102 | model.int64 = 10 103 | 104 | model.boolean = true 105 | model.boolean = true 106 | 107 | model.string = "test" 108 | model.string = "test" 109 | 110 | model.string = "test 2" 111 | model.string = "test 2" 112 | 113 | model.float32 = 10.0 114 | model.float32 = 10.0 115 | 116 | model.float32 = 10.1 117 | model.float32 = 10.2 118 | 119 | model.double64 = 10.0 120 | model.double64 = 10.0 121 | 122 | model.double64 = 10.1 123 | model.double64 = 10.2 124 | 125 | model.testType = .Active 126 | model.testType = .Active 127 | 128 | delayTest(self, delay: 0.01) { 129 | XCTAssertEqual(dispatchCount, 1 , "Dispatched once") 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /JetstreamTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /JetstreamTests/ModelObjectTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModelObjectTests.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import XCTest 26 | @testable import Jetstream 27 | 28 | class ModelObjectTests: XCTestCase { 29 | func testModelProperties() { 30 | let model = TestModel() 31 | 32 | var prop = model.properties["integer"] 33 | XCTAssertEqual(prop!.key, "integer" , "Property recognized") 34 | 35 | prop = model.properties["nonDynamicInt"] 36 | XCTAssert(prop == nil, "Non-dynamic property not recognized") 37 | 38 | prop = model.properties["nonDynamicString"] 39 | XCTAssert(prop == nil, "Non-dynamic property not recognized") 40 | } 41 | 42 | func testChildModelObjectsAccessor() { 43 | let model = TestModel() 44 | let model2 = TestModel() 45 | let model3 = TestModel() 46 | let model4 = TestModel() 47 | let model5 = TestModel() 48 | 49 | model.childModel = model2 50 | model.childModel2 = model3 51 | model.array = [model4, model5] 52 | 53 | XCTAssertEqual(model.childModelObjects.count, 4 , "All child models should be returned") 54 | XCTAssertNotNil(model.childModelObjects.indexOf(model2), "Model should be found") 55 | XCTAssertNotNil(model.childModelObjects.indexOf(model3), "Model should be found") 56 | XCTAssertNotNil(model.childModelObjects.indexOf(model4), "Model should be found") 57 | XCTAssertNotNil(model.childModelObjects.indexOf(model5), "Model should be found") 58 | } 59 | 60 | func testModelObjectPropertyRemoveParentUsingDifferingParentAndChildTypes() { 61 | let model = TestModel() 62 | let model2 = AnotherTestModel() 63 | 64 | model.anotherArray = [model2] 65 | model.anotherChildModel = model2 66 | XCTAssertEqual(model2.parents.count, 2, "Should add parent when set as child model") 67 | 68 | model.anotherArray = [] 69 | XCTAssertEqual(model2.parents.count, 1, "Should remove parent when unset as child model") 70 | 71 | model.anotherChildModel = nil 72 | XCTAssertEqual(model2.parents.count, 0, "Should remove parent when unset as child model") 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /JetstreamTests/ModelValueTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModelValueTests.swift 3 | // Jetstream 4 | // 5 | // Created by Tuomas Artman on 4/13/15. 6 | // Copyright (c) 2015 Uber Technologies Inc. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Jetstream 11 | 12 | class ModelValueTests: XCTestCase { 13 | 14 | func testEqaulity() { 15 | let string = "test" 16 | XCTAssertTrue(string.equalTo(string), "Should have passed") 17 | XCTAssertFalse(string.equalTo(1), "Should not have passed") 18 | 19 | let int: Int = 1 20 | XCTAssertTrue(int.equalTo(int), "Should have passed") 21 | XCTAssertFalse(int.equalTo(string), "Should not have passed") 22 | 23 | let uint: Int = 1 24 | XCTAssertTrue(uint.equalTo(uint), "Should have passed") 25 | XCTAssertFalse(uint.equalTo(string), "Should not have passed") 26 | 27 | let int8: Int8 = 1 28 | XCTAssertTrue(int8.equalTo(int8), "Should have passed") 29 | XCTAssertFalse(int8.equalTo(string), "Should not have passed") 30 | 31 | let uint8: UInt8 = 1 32 | XCTAssertTrue(uint8.equalTo(uint8), "Should have passed") 33 | XCTAssertFalse(uint8.equalTo(string), "Should not have passed") 34 | 35 | let int16: Int16 = 1 36 | XCTAssertTrue(int16.equalTo(int16), "Should have passed") 37 | XCTAssertFalse(int16.equalTo(string), "Should not have passed") 38 | 39 | let uint16: UInt16 = 1 40 | XCTAssertTrue(uint16.equalTo(uint16), "Should have passed") 41 | XCTAssertFalse(uint16.equalTo(string), "Should not have passed") 42 | 43 | let int32: Int32 = 1 44 | XCTAssertTrue(int32.equalTo(int32), "Should have passed") 45 | XCTAssertFalse(int32.equalTo(string), "Should not have passed") 46 | 47 | let uint32: UInt32 = 1 48 | XCTAssertTrue(uint32.equalTo(uint32), "Should have passed") 49 | XCTAssertFalse(uint32.equalTo(string), "Should not have passed") 50 | 51 | let float: Float = 1.0 52 | XCTAssertTrue(float.equalTo(float), "Should have passed") 53 | XCTAssertFalse(float.equalTo(string), "Should not have passed") 54 | 55 | let double: Double = 1.0 56 | XCTAssertTrue(double.equalTo(double), "Should have passed") 57 | XCTAssertFalse(double.equalTo(string), "Should not have passed") 58 | 59 | let bool: Bool = true 60 | XCTAssertTrue(bool.equalTo(bool), "Should have passed") 61 | XCTAssertFalse(bool.equalTo(string), "Should not have passed") 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /JetstreamTests/PropertyListenerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PropertyListenerTests.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import XCTest 26 | @testable import Jetstream 27 | 28 | class PropertyListenerTests: XCTestCase { 29 | func testGenericPropertyListeners() { 30 | let model = TestModel() 31 | var lastValue = "" 32 | 33 | model.onPropertyChange.listen(self, callback: {(key, oldValue, value) in 34 | if key == "string" { 35 | lastValue = value as! String 36 | } 37 | }) 38 | 39 | model.string = "test" 40 | 41 | XCTAssertEqual(lastValue, "test" , "Value change captured") 42 | } 43 | 44 | func testSpecificPropertyListeners() { 45 | let model = TestModel() 46 | var dispatchCount = 0 47 | 48 | model.observeChangeImmediately(self, key: "string") { 49 | dispatchCount += 1 50 | } 51 | 52 | model.string = "test" 53 | model.integer = 1 54 | model.float32 = 2.5 55 | 56 | XCTAssertEqual(dispatchCount, 1 , "Dispatched once") 57 | 58 | model.string = nil 59 | 60 | XCTAssertEqual(dispatchCount, 2 , "Dispatched twice") 61 | } 62 | 63 | func testCancelPropertyListeners() { 64 | let model = TestModel() 65 | var dispatchCount = 0 66 | 67 | let cancel = model.observeChangeImmediately(self, key: "string") { 68 | dispatchCount += 1 69 | } 70 | 71 | cancel() 72 | 73 | model.string = "test" 74 | model.integer = 1 75 | model.float32 = 2.5 76 | model.string = nil 77 | 78 | XCTAssertEqual(dispatchCount, 0 , "Never dispatehced") 79 | } 80 | 81 | func testMultiPropertyListeners() { 82 | let model = TestModel() 83 | var dispatchCount = 0 84 | 85 | model.observeChangeImmediately(self, keys: ["string", "integer"]) { 86 | dispatchCount += 1 87 | } 88 | 89 | model.string = "test" 90 | model.integer = 1 91 | model.float32 = 2.5 92 | 93 | XCTAssertEqual(dispatchCount, 2 , "Dispatched twice") 94 | } 95 | 96 | func testNoDispatchForNoChange() { 97 | let model = TestModel() 98 | var dispatchCount = 0 99 | 100 | model.observeChangeImmediately(self) { 101 | dispatchCount += 1 102 | } 103 | XCTAssertEqual(dispatchCount, 0 , "Dispatched once") 104 | 105 | model.integer = 10 106 | model.integer = 10 107 | XCTAssertEqual(dispatchCount, 1 , "Dispatched once") 108 | 109 | model.uint = 10 110 | model.uint = 10 111 | XCTAssertEqual(dispatchCount, 2 , "Dispatched once") 112 | 113 | model.uint8 = 10 114 | model.uint8 = 10 115 | XCTAssertEqual(dispatchCount, 3 , "Dispatched once") 116 | 117 | model.int8 = 10 118 | model.int8 = 10 119 | XCTAssertEqual(dispatchCount, 4 , "Dispatched once") 120 | 121 | model.uint16 = 10 122 | model.uint16 = 10 123 | XCTAssertEqual(dispatchCount, 5 , "Dispatched once") 124 | 125 | model.int16 = 10 126 | model.int16 = 10 127 | XCTAssertEqual(dispatchCount, 6 , "Dispatched once") 128 | 129 | model.uint32 = 10 130 | model.uint32 = 10 131 | XCTAssertEqual(dispatchCount, 7 , "Dispatched once") 132 | 133 | model.int32 = 10 134 | model.int32 = 10 135 | XCTAssertEqual(dispatchCount, 8 , "Dispatched once") 136 | 137 | model.uint64 = 10 138 | model.uint64 = 10 139 | XCTAssertEqual(dispatchCount, 9 , "Dispatched once") 140 | 141 | model.int64 = 10 142 | model.int64 = 10 143 | XCTAssertEqual(dispatchCount, 10 , "Dispatched once") 144 | 145 | model.boolean = true 146 | model.boolean = true 147 | XCTAssertEqual(dispatchCount, 11 , "Dispatched once") 148 | 149 | model.string = "test" 150 | model.string = "test" 151 | XCTAssertEqual(dispatchCount, 12 , "Dispatched once") 152 | 153 | model.string = "test 2" 154 | model.string = "test 2" 155 | XCTAssertEqual(dispatchCount, 13 , "Dispatched once") 156 | 157 | model.float32 = 10.0 158 | model.float32 = 10.0 159 | XCTAssertEqual(dispatchCount, 15 , "Dispatched twice") // float is part of composite property 160 | 161 | model.float32 = 10.1 162 | model.float32 = 10.2 163 | XCTAssertEqual(dispatchCount, 19 , "Dispatched four times") // float is part of composite property 164 | 165 | model.double64 = 10.0 166 | model.double64 = 10.0 167 | XCTAssertEqual(dispatchCount, 20 , "Dispatched once") 168 | 169 | model.double64 = 10.1 170 | model.double64 = 10.2 171 | XCTAssertEqual(dispatchCount, 22 , "Dispatched twice") 172 | 173 | model.testType = .Active 174 | model.testType = .Active 175 | XCTAssertEqual(dispatchCount, 23 , "Dispatched once") 176 | 177 | model.localString = "new value" 178 | model.localString = "new value" 179 | XCTAssertEqual(dispatchCount, 24 , "Dispatched once") 180 | } 181 | 182 | func testArrayListeners() { 183 | let model = TestModel() 184 | var changedCount = 0 185 | var addedCount = 0 186 | var removedCount = 0 187 | 188 | model.observeChangeImmediately(self, key: "array") { 189 | changedCount += 1 190 | } 191 | model.observeCollectionAdd(self, key: "array") { (element: TestModel) in 192 | addedCount += 1 193 | } 194 | 195 | model.observeCollectionRemove(self, key: "array") { (element: TestModel) in 196 | removedCount += 1 197 | } 198 | 199 | model.array.append(TestModel()) 200 | model.array[0] = TestModel() 201 | model.array.removeLast() 202 | model.array = [TestModel()] 203 | 204 | XCTAssertEqual(changedCount, 4 , "Dispatched four times") 205 | XCTAssertEqual(addedCount, 3 , "Dispatched three times") 206 | XCTAssertEqual(removedCount, 2 , "Dispatched two times") 207 | 208 | model.array[0].detach() 209 | XCTAssertEqual(removedCount, 3 , "Dispatched three times") 210 | } 211 | 212 | func testTreeListeners() { 213 | let expectation = expectationWithDescription("onChange") 214 | 215 | let parent = TestModel() 216 | let child = TestModel() 217 | let child2 = TestModel() 218 | 219 | var changedCount1 = 0 220 | var changedCount2 = 0 221 | var changedCount3 = 0 222 | 223 | parent.observeTreeChange(self) { 224 | changedCount1 += 1 225 | } 226 | child.observeTreeChange(self) { 227 | changedCount2 += 1 228 | } 229 | child2.observeTreeChange(self) { 230 | changedCount3 += 1 231 | } 232 | 233 | parent.array.append(child) 234 | 235 | delay(0.001) { 236 | XCTAssertEqual(changedCount1, 1 , "Correct dispatch count") 237 | XCTAssertEqual(changedCount2, 0 , "Correct dispatch count") 238 | XCTAssertEqual(changedCount3, 0 , "Correct dispatch count") 239 | 240 | child.childModel = child2 241 | delay(0.001) { 242 | XCTAssertEqual(changedCount1, 2 , "Correct dispatch count") 243 | XCTAssertEqual(changedCount2, 1 , "Correct dispatch count") 244 | XCTAssertEqual(changedCount3, 0 , "Correct dispatch count") 245 | child.string = "changed this" 246 | child.boolean = true 247 | 248 | delay(0.001) { 249 | XCTAssertEqual(changedCount1, 3 , "Correct dispatch count") 250 | XCTAssertEqual(changedCount2, 2 , "Correct dispatch count") 251 | XCTAssertEqual(changedCount3, 0 , "Correct dispatch count") 252 | 253 | child2.string = "changed this" 254 | child2.boolean = true 255 | 256 | delay(0.001) { 257 | XCTAssertEqual(changedCount1, 4 , "Correct dispatch count") 258 | XCTAssertEqual(changedCount2, 3 , "Correct dispatch count") 259 | XCTAssertEqual(changedCount3, 1 , "Correct dispatch count") 260 | expectation.fulfill() 261 | } 262 | } 263 | } 264 | } 265 | waitForExpectationsWithTimeout(2.0, handler: nil) 266 | } 267 | 268 | func testRemoveParent() { 269 | let parent = TestModel() 270 | let child = TestModel() 271 | var changedCount = 0 272 | 273 | child.observeRemovedFromParent(self, callback: { (parent, key) -> Void in 274 | changedCount += 1 275 | }) 276 | 277 | parent.childModel = child 278 | parent.childModel = nil 279 | 280 | XCTAssertEqual(changedCount, 1, "correct change count") 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /JetstreamTests/Resources/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uber-archive/jetstream-ios/a214183dd2de9bc2b5f6a1d2160b70f995502637/JetstreamTests/Resources/test.jpg -------------------------------------------------------------------------------- /JetstreamTests/ScopePauseTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScopePauseTests.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import XCTest 26 | @testable import Jetstream 27 | 28 | class ScopePauseTest: XCTestCase { 29 | var root = TestModel() 30 | var scope = Scope(name: "Testing") 31 | var client = Client(transportAdapterFactory: { TestTransportAdapter() }) 32 | var firstMessage: ScopeStateMessage! 33 | let uuid = NSUUID() 34 | 35 | override func setUp() { 36 | root = TestModel() 37 | scope = Scope(name: "Testing") 38 | root.setScopeAndMakeRootModel(scope) 39 | XCTAssertEqual(scope.modelObjects.count, 1, "Correct number of objects in scope to start with") 40 | 41 | client = Client(transportAdapterFactory: { TestTransportAdapter() }) 42 | let msg = SessionCreateReplyMessage(index: 1, sessionToken: "jeah", error: nil) 43 | client.receivedMessage(msg) 44 | client.session!.scopeAttach(scope, scopeIndex: 0) 45 | 46 | let childUUID = NSUUID() 47 | 48 | let json: [String: AnyObject] = [ 49 | "type": "ScopeState", 50 | "index": 1, 51 | "scopeIndex": 0, 52 | "rootUUID": uuid.UUIDString, 53 | "fragments": [ 54 | [ 55 | "type": "change", 56 | "uuid": uuid.UUIDString, 57 | "clsName": "TestModel", 58 | "properties": [ 59 | "string": "set correctly", 60 | "integer": 10, 61 | "childModel": childUUID.UUIDString 62 | ] 63 | ], 64 | [ 65 | "type": "add", 66 | "uuid": childUUID.UUIDString, 67 | "properties": ["string": "ok"], 68 | "clsName": "TestModel" 69 | ] 70 | ] 71 | ] 72 | 73 | firstMessage = NetworkMessage.unserializeDictionary(json) as! ScopeStateMessage 74 | client.receivedMessage(firstMessage) 75 | } 76 | 77 | override func tearDown() { 78 | super.tearDown() 79 | } 80 | 81 | func testPause() { 82 | let json: [String: AnyObject] = [ 83 | "type": "ScopeSync", 84 | "index": 2, 85 | "scopeIndex": 0, 86 | "fragments": [ 87 | [ 88 | "type": "change", 89 | "uuid": uuid.UUIDString, 90 | "clsName": "TestModel", 91 | "properties": ["string": "changed"], 92 | ] 93 | ] 94 | ] 95 | 96 | root.scope?.pauseIncomingMessages() 97 | client.receivedMessage(NetworkMessage.unserializeDictionary(json)!) 98 | XCTAssertEqual(root.string!, "set correctly", "Property not yet changed") 99 | 100 | root.scope?.resumeIncomingMessages() 101 | XCTAssertEqual(root.string!, "changed", "Property changed") 102 | } 103 | 104 | func testMultiMessagePause() { 105 | root.scope?.pauseIncomingMessages() 106 | 107 | var json: [String: AnyObject] = [ 108 | "type": "ScopeSync", 109 | "index": 2, 110 | "scopeIndex": 0, 111 | "fragments": [ 112 | [ 113 | "type": "change", 114 | "uuid": uuid.UUIDString, 115 | "clsName": "TestModel", 116 | "properties": ["string": "changed"], 117 | ] 118 | ] 119 | ] 120 | client.receivedMessage(NetworkMessage.unserializeDictionary(json)!) 121 | 122 | json = [ 123 | "type": "ScopeSync", 124 | "index": 3, 125 | "scopeIndex": 0, 126 | "fragments": [ 127 | [ 128 | "type": "change", 129 | "uuid": uuid.UUIDString, 130 | "clsName": "TestModel", 131 | "properties": ["integer": 20], 132 | ] 133 | ] 134 | ] 135 | client.receivedMessage(NetworkMessage.unserializeDictionary(json)!) 136 | 137 | XCTAssertEqual(root.string!, "set correctly", "Property not yet changed") 138 | XCTAssertEqual(root.integer, 10, "Property not yet changed") 139 | 140 | root.scope?.resumeIncomingMessages() 141 | XCTAssertEqual(root.string!, "changed", "Property changed") 142 | XCTAssertEqual(root.integer, 20, "Property changed") 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /JetstreamTests/SyncFragmentTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SyncFragmentTests.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import XCTest 26 | @testable import Jetstream 27 | 28 | class SyncFragmentTests: XCTestCase { 29 | var parent = TestModel() 30 | var child = TestModel() 31 | var scope = Scope(name: "Testing") 32 | 33 | override func setUp() { 34 | parent = TestModel() 35 | child = TestModel() 36 | parent.childModel = child 37 | scope = Scope(name: "Testing") 38 | parent.setScopeAndMakeRootModel(scope) 39 | } 40 | 41 | override func tearDown() { 42 | super.tearDown() 43 | } 44 | 45 | func testSerializationFailure() { 46 | let uuid = NSUUID() 47 | 48 | var json: [String: AnyObject] = [ 49 | "uuid": child.uuid.UUIDString 50 | ] 51 | var fragment = SyncFragment.unserialize(json) 52 | XCTAssertNil(fragment , "Fragment with missing type shouldn't be created") 53 | 54 | json = [ 55 | "type": "remove", 56 | ] 57 | fragment = SyncFragment.unserialize(json) 58 | XCTAssertNil(fragment , "Fragment with missing uuid shouldn't be created") 59 | 60 | json = [ 61 | "type": "add", 62 | "uuid": uuid.UUIDString, 63 | "properties": ["string": "set correctly"], 64 | ] 65 | fragment = SyncFragment.unserialize(json) 66 | XCTAssertNil(fragment , "Add fragment with missing cls property shouldn't be created") 67 | } 68 | 69 | func testChange() { 70 | let json: [String: AnyObject] = [ 71 | "type": "change", 72 | "uuid": child.uuid.UUIDString, 73 | "clsName": "TestModel", 74 | "properties": ["string": "testing", "integer": 20] 75 | 76 | ] 77 | let fragment = SyncFragment.unserialize(json) 78 | XCTAssertEqual(fragment!.objectUUID, child.uuid , "UUID unserialized") 79 | XCTAssertEqual(fragment!.objectUUID, child.uuid , "UUID unserialized") 80 | XCTAssertEqual(fragment!.properties!.count, 2 , "Properties unserialized") 81 | 82 | fragment?.applyChangesToScope(scope) 83 | XCTAssertEqual(parent.childModel!.string!, "testing" , "Properties applied") 84 | XCTAssertEqual(parent.childModel!.integer, 20 , "Properties applied") 85 | } 86 | 87 | func testAdd() { 88 | let uuid = NSUUID() 89 | 90 | let json: [String: AnyObject] = [ 91 | "type": "add", 92 | "uuid": uuid.UUIDString, 93 | "clsName": "TestModel", 94 | "properties": ["string": "set correctly"] 95 | ] 96 | let fragment = SyncFragment.unserialize(json) 97 | let json2: [String: AnyObject] = [ 98 | "type": "change", 99 | "uuid": child.uuid.UUIDString, 100 | "clsName": "TestModel", 101 | "properties": ["childModel": uuid.UUIDString], 102 | ] 103 | let fragment2 = SyncFragment.unserialize(json2) 104 | 105 | XCTAssertEqual(fragment!.objectUUID, uuid , "UUID unserialized") 106 | XCTAssertEqual(fragment!.clsName!, "TestModel" , "Class name unserialized") 107 | 108 | scope.applySyncFragments([fragment!, fragment2!]) 109 | let testModel = child.childModel! 110 | 111 | XCTAssertEqual(child.childModel!, testModel, "Child added") 112 | XCTAssertEqual(testModel.parents[0].parent, child , "Child has correct parent") 113 | XCTAssert(testModel.scope === scope , "Scope set correctly") 114 | XCTAssertEqual(testModel.string!, "set correctly" , "Properties set correctly") 115 | XCTAssertEqual(scope.modelObjects.count, 3 , "Scope knows of added model") 116 | } 117 | 118 | func testAddToArray() { 119 | let uuid = NSUUID() 120 | 121 | let json: [String: AnyObject] = [ 122 | "type": "add", 123 | "uuid": uuid.UUIDString, 124 | "clsName": "TestModel", 125 | "properties": ["string": "set correctly"] 126 | ] 127 | let fragment = SyncFragment.unserialize(json) 128 | let json2: [String: AnyObject] = [ 129 | "type": "change", 130 | "uuid": child.uuid.UUIDString, 131 | "clsName": "TestModel", 132 | "properties": ["array": [uuid.UUIDString]], 133 | ] 134 | let fragment2 = SyncFragment.unserialize(json2) 135 | 136 | scope.applySyncFragments([fragment!, fragment2!]) 137 | let testModel = child.array[0] 138 | 139 | XCTAssertEqual(child.array[0], testModel, "Child added") 140 | XCTAssertEqual(testModel.parents[0].parent, child , "Child has correct parent") 141 | XCTAssert(testModel.scope === scope , "Scope set correctly") 142 | XCTAssertEqual(testModel.string!, "set correctly" , "Properties set correctly") 143 | XCTAssertEqual(scope.modelObjects.count, 3 , "Scope knows of added model") 144 | } 145 | 146 | func testNullValues() { 147 | let json: [String: AnyObject] = [ 148 | "type": "change", 149 | "uuid": child.uuid.UUIDString, 150 | "clsName": "TestModel", 151 | "properties": [ 152 | "array": NSNull(), 153 | "string": NSNull(), 154 | "integer": NSNull(), 155 | "float32": NSNull(), 156 | "int32": NSNull() 157 | ], 158 | ] 159 | let fragment = SyncFragment.unserialize(json) 160 | 161 | child.string = "test" 162 | child.integer = 10 163 | child.float32 = 10.0 164 | child.int32 = 10 165 | 166 | scope.applySyncFragments([fragment!]) 167 | 168 | XCTAssertNil(child.string, "String was nilled out") 169 | XCTAssertEqual(child.integer, 10 , "Nill not applied") 170 | XCTAssertEqual(child.float32, Float(10.0) , "Nil not applied") 171 | XCTAssertEqual(child.int32, Int32(10) , "Nil not applied") 172 | } 173 | 174 | func testInvalidValues() { 175 | let json: [String: AnyObject] = [ 176 | "type": "change", 177 | "uuid": child.uuid.UUIDString, 178 | "clsName": "TestModel", 179 | "properties": [ 180 | "string": 10, 181 | "integer": "5", 182 | "float32": "whatever", 183 | "int32": 5.5 184 | ], 185 | ] 186 | let fragment = SyncFragment.unserialize(json) 187 | 188 | child.string = "test" 189 | child.integer = 10 190 | child.float32 = 10.0 191 | child.int32 = 10 192 | 193 | scope.applySyncFragments([fragment!]) 194 | 195 | XCTAssertNil(child.string, "String nilled out") 196 | XCTAssertEqual(child.integer, 10 , "Invalid property not applied") 197 | XCTAssertEqual(child.float32, Float(10.0) , "Invalid property not applied") 198 | XCTAssertEqual(child.int32, Int32(5) , "Int32 converted") 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /JetstreamTests/TestHelpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestHelpers.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import XCTest 26 | 27 | public func delayTest(test: XCTestCase, delay: Double, callback: () -> ()) { 28 | let expectation = test.expectationWithDescription("testDelay") 29 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(delay * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) { 30 | callback() 31 | expectation.fulfill() 32 | } 33 | test.waitForExpectationsWithTimeout(delay + 2.0, handler: nil) 34 | } -------------------------------------------------------------------------------- /JetstreamTests/TestModels.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestModel.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | @testable import Jetstream 26 | 27 | @objc enum TestType: Int { 28 | case Normal 29 | case Active 30 | }; 31 | 32 | @objc public class TestModel: ModelObject { 33 | dynamic var string: String? 34 | dynamic var integer: Int = 0 35 | dynamic var testType: TestType = .Normal 36 | dynamic var uint: UInt = 0 37 | dynamic var float32: Float = 0.0 38 | dynamic var uint8: UInt8 = 0 39 | dynamic var int8: Int8 = 0 40 | dynamic var uint16: UInt16 = 0 41 | dynamic var int16: Int16 = 0 42 | dynamic var uint32: UInt32 = 0 43 | dynamic var int32: Int32 = 0 44 | dynamic var uint64: UInt64 = 0 45 | dynamic var int64: UInt64 = 0 46 | dynamic var double64: Double = 0 47 | dynamic var boolean: Bool = false 48 | dynamic var date: NSDate? 49 | dynamic var color: UIColor? 50 | dynamic var image: UIImage? 51 | dynamic var localString: String? 52 | dynamic var localModel: ModelObject? 53 | dynamic var localModelArray: [ModelObject] = [] 54 | 55 | dynamic var array: [TestModel] = [] 56 | dynamic var array2: [TestModel] = [] 57 | dynamic var anotherArray: [AnotherTestModel] = [] 58 | dynamic var childModel: TestModel? 59 | dynamic var childModel2: TestModel? 60 | dynamic var anotherChildModel: AnotherTestModel? 61 | 62 | dynamic var throttledProperty: Int = 0 63 | 64 | private var nonDynamicInt: Int = 0 65 | private var nonDynamicString = "" 66 | 67 | var compositeProperty: String { 68 | return "\(float32) \(anotherArray.count)" 69 | } 70 | 71 | override public class func getPropertyAttributes() -> [String: [PropertyAttribute]] { 72 | return [ 73 | "localString": [.Local], 74 | "localModel": [.Local], 75 | "localModelArray": [.Local], 76 | "compositeProperty": [.Composite(["float32", "anotherArray"])], 77 | "throttledProperty": [.MinSyncInterval(0.05)] 78 | ] 79 | } 80 | } 81 | 82 | @objc public class AnotherTestModel: ModelObject { 83 | dynamic var anotherString: String? = "" 84 | dynamic var anotherInteger: Int = 0 85 | 86 | dynamic var anotherCompositeProperty: String { 87 | get { 88 | return "\(anotherString) \(anotherInteger)" 89 | } 90 | } 91 | 92 | override public class func getPropertyAttributes() -> [String: [PropertyAttribute]] { 93 | return [ 94 | "anotherCompositeProperty": [.Composite(["anotherString", "anotherInteger"])] 95 | ] 96 | } 97 | } -------------------------------------------------------------------------------- /JetstreamTests/TestTransportAdapter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestTransportAdapter.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | @testable import Jetstream 26 | 27 | class TestConnectionOptions: ConnectionOptions { 28 | var url: NSURL { return NSURL(string: "http://localhost")! } 29 | } 30 | 31 | public class TestTransportAdapter: TransportAdapter { 32 | public let onStatusChanged = Signal<(TransportStatus)>() 33 | public let onMessage = Signal<(NetworkMessage)>() 34 | 35 | public var adapterName: String { return "TestTransportAdapter" } 36 | public var status: TransportStatus = .Closed { 37 | didSet { 38 | onStatusChanged.fire(status) 39 | } 40 | } 41 | public let options: ConnectionOptions 42 | 43 | let onConnectCalled = Signal<()>() 44 | let onDisconnectCalled = Signal<()>() 45 | let onReconnectCalled = Signal<()>() 46 | let onSendMessageCalled = Signal<(NetworkMessage)>() 47 | let onSessionEstablishedCalled = Signal<(Session)>() 48 | 49 | init(options: ConnectionOptions) { 50 | self.options = options 51 | } 52 | 53 | convenience init() { 54 | self.init(options: TestConnectionOptions()) 55 | } 56 | 57 | public func connect() { 58 | onConnectCalled.fire() 59 | } 60 | 61 | public func disconnect() { 62 | onDisconnectCalled.fire() 63 | } 64 | 65 | public func reconnect() { 66 | onReconnectCalled.fire() 67 | } 68 | 69 | public func sendMessage(message: NetworkMessage) { 70 | onSendMessageCalled.fire(message) 71 | } 72 | 73 | public func sessionEstablished(session: Session) { 74 | onSessionEstablishedCalled.fire(session) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /JetstreamTests/TransactionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransactionTests.swift 3 | // Jetstream 4 | // 5 | // Copyright (c) 2014 Uber Technologies, Inc. 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 | import XCTest 26 | @testable import Jetstream 27 | 28 | class TransactionTests: XCTestCase { 29 | var root = TestModel() 30 | var child = TestModel() 31 | var scope = Scope(name: "Testing") 32 | var client = Client(transportAdapterFactory: { TestTransportAdapter() }) 33 | var firstMessage: ScopeStateMessage! 34 | 35 | override func setUp() { 36 | root = TestModel() 37 | child = TestModel() 38 | root.childModel = child 39 | scope = Scope(name: "Testing") 40 | root.setScopeAndMakeRootModel(scope) 41 | scope.getAndClearSyncFragments() 42 | 43 | client = Client(transportAdapterFactory: { TestTransportAdapter() }) 44 | let msg = SessionCreateReplyMessage(index: 1, sessionToken: "jeah", error: nil) 45 | client.receivedMessage(msg) 46 | client.session!.scopeAttach(scope, scopeIndex: 0) 47 | } 48 | 49 | override func tearDown() { 50 | super.tearDown() 51 | } 52 | 53 | func testChangeSetSuccessfulCompletionWithoutModifications() { 54 | var didCall = false 55 | 56 | root.scope!.modify { 57 | self.root.integer = 10 58 | self.root.float32 = 10.0 59 | self.root.string = "test" 60 | }.observeCompletion(self) { error in 61 | XCTAssertNil(error, "No error") 62 | didCall = true 63 | } 64 | 65 | let json: [String: AnyObject] = [ 66 | "type": "ScopeSyncReply", 67 | "index": 2, 68 | "replyTo": 1, 69 | "fragmentReplies": [ 70 | [String: AnyObject](), 71 | ] 72 | ] 73 | 74 | let reply = ScopeSyncReplyMessage.unserialize(json) 75 | client.transport.messageReceived(reply!) 76 | 77 | XCTAssertEqual(didCall, true, "Did invoke completion block") 78 | XCTAssertEqual(self.root.integer, 10, "No rollback") 79 | XCTAssert(self.root.float32 == 10.0, "No rollback") 80 | XCTAssertEqual(self.root.string!, "test", "No rollback") 81 | } 82 | 83 | func testChangeSetSuccessfulCompletionWithModifications() { 84 | var didCall = false 85 | 86 | root.scope!.modify { 87 | self.root.integer = 10 88 | self.root.float32 = 10.0 89 | self.root.string = "test" 90 | }.observeCompletion(self) { error in 91 | XCTAssertNil(error, "No error") 92 | didCall = true 93 | } 94 | 95 | let json: [String: AnyObject] = [ 96 | "type": "ScopeSyncReply", 97 | "index": 2, 98 | "replyTo": 1, 99 | "fragmentReplies": [ 100 | ["modifications": [ 101 | "integer": 20, 102 | "double64": 20.0 103 | ]] 104 | ] 105 | ] 106 | 107 | let reply = ScopeSyncReplyMessage.unserialize(json) 108 | client.transport.messageReceived(reply!) 109 | 110 | XCTAssertEqual(didCall, true, "Did invoke completion block") 111 | XCTAssertEqual(self.root.integer, 20, "Applied modification") 112 | XCTAssert(self.root.float32 == 10.0, "No rollback") 113 | XCTAssertEqual(self.root.string!, "test", "No rollback") 114 | XCTAssert(self.root.double64 == 20.0, "Applied modification") 115 | } 116 | 117 | func testChangeSetFragmentReplyMismatch() { 118 | var didCall = false 119 | 120 | root.scope!.modify { 121 | self.root.integer = 10 122 | self.root.float32 = 10.0 123 | self.root.string = "test" 124 | }.observeCompletion(self) { error in 125 | XCTAssertEqual(error!.localizedDescription, "Failed to apply change set", "Errored out") 126 | didCall = true 127 | } 128 | 129 | let json: [String: AnyObject] = [ 130 | "type": "ScopeSyncReply", 131 | "index": 2, 132 | "replyTo": 1, 133 | "fragmentReplies": [[String: AnyObject]]() 134 | ] 135 | 136 | let reply = ScopeSyncReplyMessage.unserialize(json) 137 | client.transport.messageReceived(reply!) 138 | 139 | XCTAssertEqual(didCall, true, "Did invoke completion block") 140 | XCTAssertEqual(self.root.integer, 0, "Did rollback") 141 | XCTAssert(self.root.float32 == 0.0, "Did rollback") 142 | XCTAssertNil(self.root.string, "Did rollback") 143 | } 144 | 145 | func testChangeInvalidMessageTypeError() { 146 | var didCall = false 147 | 148 | root.scope!.modify { 149 | self.root.integer = 10 150 | self.root.float32 = 10.0 151 | self.root.string = "test" 152 | }.observeCompletion(self) { error in 153 | XCTAssertEqual(error!.localizedDescription, "Failed to apply change set", "Errored out") 154 | didCall = true 155 | } 156 | 157 | client.transport.messageReceived(ReplyMessage(index: 2, replyTo: 1)) 158 | 159 | XCTAssertEqual(didCall, true, "Did invoke completion block") 160 | XCTAssertEqual(self.root.integer, 0, "Did rollback") 161 | XCTAssert(self.root.float32 == 0.0, "Did rollback") 162 | XCTAssertNil(self.root.string, "Did rollback") 163 | } 164 | 165 | func testSpecificFragmentReversal() { 166 | var didCall = false 167 | 168 | root.scope!.modify { 169 | self.root.integer = 20 170 | self.child.integer = 20 171 | }.observeCompletion(self) { error in 172 | XCTAssertEqual(self.root.integer, 0, "Kept change") 173 | XCTAssertEqual(self.child.integer, 20, "Reverted change") 174 | didCall = true 175 | } 176 | 177 | let json: [String: AnyObject] = [ 178 | "type": "ScopeSyncReply", 179 | "index": 2, 180 | "replyTo": 1, 181 | "fragmentReplies": [ 182 | [String: AnyObject](), 183 | ["error": [ 184 | "type": "invalid-type", 185 | "message": "Invalid type for property 'name'" 186 | ] 187 | ] 188 | ] 189 | ] 190 | 191 | let reply = ScopeSyncReplyMessage.unserialize(json) 192 | client.transport.messageReceived(reply!) 193 | XCTAssertEqual(didCall, true, "Did invoke completion block") 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Uber Technologies, Inc. 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. --------------------------------------------------------------------------------