├── .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.
--------------------------------------------------------------------------------