diff --git a/Jetstream.xcodeproj/xcshareddata/xcschemes/JetstreamTests.xcscheme b/Jetstream.xcodeproj/xcshareddata/xcschemes/JetstreamTests.xcscheme new file mode 100644 index 0000000..3547ffb --- /dev/null +++ b/Jetstream.xcodeproj/xcshareddata/xcschemes/JetstreamTests.xcscheme @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Jetstream/Client.swift b/Jetstream/Client.swift index 4ac19d5..1f917aa 100644 --- a/Jetstream/Client.swift +++ b/Jetstream/Client.swift @@ -137,16 +137,15 @@ public enum ClientStatus { func receivedMessage(message: NetworkMessage) { switch message { - case let sessionCreateResponse as SessionCreateReplyMessage: + case let sessionCreateReply as SessionCreateReplyMessage: if session != nil { logger.error("Received session create response with existing session") - } else if sessionCreateResponse.success == false { - logger.info("Denied starting session") - onSessionDenied.fire() - } else { - let token = sessionCreateResponse.sessionToken + } else if let token = sessionCreateReply.sessionToken { logger.info("Starting session with token: \(token)") session = Session(client: self, token: token) + } else { + logger.info("Denied starting session, error: \(sessionCreateReply.error)") + onSessionDenied.fire() } default: session?.receivedMessage(message) diff --git a/Jetstream/ModelObject.swift b/Jetstream/ModelObject.swift index 35e043b..51d1c04 100644 --- a/Jetstream/ModelObject.swift +++ b/Jetstream/ModelObject.swift @@ -109,7 +109,7 @@ struct PropertyInfo { } private var internalScope: Scope? - var scope: Scope? { + public internal(set) var scope: Scope? { get { return internalScope } diff --git a/Jetstream/NetworkMessage.swift b/Jetstream/NetworkMessage.swift index 93d7696..26e6373 100644 --- a/Jetstream/NetworkMessage.swift +++ b/Jetstream/NetworkMessage.swift @@ -40,10 +40,14 @@ public class NetworkMessage { return SessionCreateReplyMessage.unserialize(dictionary) case SessionCreateReplyMessage.messageType: return SessionCreateReplyMessage.unserialize(dictionary) + case ScopeFetchReplyMessage.messageType: + return ScopeFetchReplyMessage.unserialize(dictionary) case ScopeStateMessage.messageType: return ScopeStateMessage.unserialize(dictionary) case ScopeSyncMessage.messageType: return ScopeSyncMessage.unserialize(dictionary) + case ScopeSyncReplyMessage.messageType: + return ScopeSyncReplyMessage.unserialize(dictionary) case PingMessage.messageType: return PingMessage.unserialize(dictionary) default: diff --git a/Jetstream/Scope.swift b/Jetstream/Scope.swift index e6e2bc9..c8159a6 100644 --- a/Jetstream/Scope.swift +++ b/Jetstream/Scope.swift @@ -20,11 +20,20 @@ import Signals public var name: String /// The root model associates with the scope - public var rootModel: ModelObject? { - if modelObjects.count > 0 { - return modelObjects[0] + public var root: ModelObject? { + get { + if modelObjects.count > 0 { + return modelObjects[0] + } + return nil + } + set { + if modelObjects.count == 0 { + if let definiteNewValue = newValue { + definiteNewValue.setScopeAndMakeRootModel(self) + } + } } - return nil } var syncFragmentLookup = [NSUUID: SyncFragment]() diff --git a/Jetstream/ScopeFetchReplyMessage.swift b/Jetstream/ScopeFetchReplyMessage.swift index 3f994be..30bdc5f 100644 --- a/Jetstream/ScopeFetchReplyMessage.swift +++ b/Jetstream/ScopeFetchReplyMessage.swift @@ -20,7 +20,7 @@ class ScopeFetchReplyMessage: ReplyMessage { let scopeIndex: UInt? let error: NSError? - init(index: UInt, replyTo: UInt, scopeIndex: UInt?, error: NSError? = nil) { + init(index: UInt, replyTo: UInt, scopeIndex: UInt?, error: NSError?) { self.scopeIndex = scopeIndex self.error = error super.init(index: index, replyTo: replyTo) @@ -34,6 +34,7 @@ class ScopeFetchReplyMessage: ReplyMessage { var index = dictionary["index"] as? UInt var replyTo = dictionary["replyTo"] as? UInt var scopeIndex = dictionary["scopeIndex"] as? UInt + var error: NSError? if let serializedError = dictionary["error"] as? [String: AnyObject] { error = errorFromDictionary(.SyncFragmentApplyError, serializedError) @@ -42,7 +43,11 @@ class ScopeFetchReplyMessage: ReplyMessage { if index == nil || replyTo == nil || (scopeIndex == nil && error == nil) { return nil } else { - return ScopeFetchReplyMessage(index: index!, replyTo: replyTo!, scopeIndex: scopeIndex, error: error) + return ScopeFetchReplyMessage( + index: index!, + replyTo: replyTo!, + scopeIndex: scopeIndex, + error: error) } } } diff --git a/Jetstream/Session.swift b/Jetstream/Session.swift index 98ff47e..75af071 100644 --- a/Jetstream/Session.swift +++ b/Jetstream/Session.swift @@ -92,7 +92,7 @@ public class Session { switch message { case let scopeStateMessage as ScopeStateMessage: if let scope = scopes[scopeStateMessage.scopeIndex] { - if let rootModel = scope.rootModel { + if scope.root != nil { scope.startApplyingRemote() scope.applyRootFragment(scopeStateMessage.rootFragment, additionalFragments: scopeStateMessage.syncFragments) scope.endApplyingRemote() @@ -104,7 +104,7 @@ public class Session { } case let scopeSyncMessage as ScopeSyncMessage: if let scope = scopes[scopeSyncMessage.scopeIndex] { - if let rootModel = scope.rootModel { + if scope.root != nil { if scopeSyncMessage.syncFragments.count > 0 { scope.startApplyingRemote() scope.applySyncFragments(scopeSyncMessage.syncFragments) diff --git a/Jetstream/SessionCreateReplyMessage.swift b/Jetstream/SessionCreateReplyMessage.swift index dcaad00..0e1f822 100644 --- a/Jetstream/SessionCreateReplyMessage.swift +++ b/Jetstream/SessionCreateReplyMessage.swift @@ -17,31 +17,31 @@ class SessionCreateReplyMessage: NetworkMessage { return SessionCreateReplyMessage.messageType } - let success: Bool - let sessionToken: String - let response: AnyObject? + let sessionToken: String? + let error: NSError? - init(index: UInt, success: Bool, sessionToken: String, response: AnyObject?) { - self.success = success + init(index: UInt, sessionToken: String?, error: NSError?) { self.sessionToken = sessionToken - self.response = response super.init(index: index) } override class func unserialize(dictionary: [String: AnyObject]) -> NetworkMessage? { var index = dictionary["index"] as? UInt - var success = dictionary["success"] as? Bool var sessionToken = dictionary["sessionToken"] as? String - var response: AnyObject? = dictionary["response"] - if index == nil || success == nil || sessionToken == nil { + + var error: NSError? + if let serializedError = dictionary["error"] as? [String: AnyObject] { + error = errorFromDictionary(.SyncFragmentApplyError, serializedError) + } + + if index == nil || (sessionToken == nil && error == nil) { return nil } else { return SessionCreateReplyMessage( index: index!, - success: success!, - sessionToken: sessionToken!, - response: response) + sessionToken: sessionToken, + error: error) } } } diff --git a/Jetstream/SyncFragment.swift b/Jetstream/SyncFragment.swift index c8e02a7..c8a9335 100644 --- a/Jetstream/SyncFragment.swift +++ b/Jetstream/SyncFragment.swift @@ -235,15 +235,15 @@ public class SyncFragment: Equatable { } func applyChangesToScope(scope: Scope, applyDefaults: Bool = false) { - if (scope.rootModel == nil) { + if scope.root == nil { return } switch type { case .Root: - if let definiteRootModel = scope.rootModel { - applyPropertiesToModelObject(definiteRootModel, scope: scope, applyDefaults: applyDefaults) - scope.updateUUIDForModel(definiteRootModel, uuid: self.objectUUID) + if let rootModel = scope.root { + applyPropertiesToModelObject(rootModel, scope: scope, applyDefaults: applyDefaults) + scope.updateUUIDForModel(rootModel, uuid: self.objectUUID) } case .Change: if let modelObject = scope.getObjectById(objectUUID) { diff --git a/JetstreamDemos/JetstreamDemos/ShapesDemoViewController.swift b/JetstreamDemos/JetstreamDemos/ShapesDemoViewController.swift index c22192f..4914da6 100644 --- a/JetstreamDemos/JetstreamDemos/ShapesDemoViewController.swift +++ b/JetstreamDemos/JetstreamDemos/ShapesDemoViewController.swift @@ -11,7 +11,7 @@ import UIKit import Jetstream class ShapesDemoViewController: UIViewController, NSURLConnectionDataDelegate { - var scope = Scope(name: "ShapesDemo") + var scope = Scope(name: "Canvas") var canvas = Canvas() var client: Client? @@ -23,8 +23,8 @@ class ShapesDemoViewController: UIViewController, NSURLConnectionDataDelegate { let tapRecognizer = UITapGestureRecognizer(target: self, action: Selector("handleTap:")) self.view.addGestureRecognizer(tapRecognizer) - - canvas.setScopeAndMakeRootModel(scope) + + scope.root = canvas canvas.observeCollectionAdd(self, key: "shapes") { (element: Shape) in let shapeView = ShapeView(shape: element) self.view.addSubview(shapeView) @@ -44,18 +44,20 @@ class ShapesDemoViewController: UIViewController, NSURLConnectionDataDelegate { super.viewWillAppear(animated) showLoader() - let transportAdapter = WebsocketTransportAdapter(options: WebsocketConnectionOptions(url: NSURL(string: "ws://" + host + ":3000")!)) + let options = WebsocketConnectionOptions(url: NSURL(string: "ws://" + host + ":3000")!) + let transportAdapter = WebsocketTransportAdapter(options: options) client = Client(transportAdapter: transportAdapter) client?.connect() - client?.onSession.listenOnce(self) { (session) in - self.session = session - let scope = self.scope - session.fetch(scope) { (error) in - if error != nil { - self.alertError("Error fetching scope", message: "\(error)") - } else { - self.hideLoader() - } + client?.onSession.listenOnce(self) { [unowned self] in self.sessionDidStart($0) } + } + + func sessionDidStart(session: Session) { + self.session = session + session.fetch(scope) { error in + if error != nil { + self.alertError("Error fetching scope", message: "\(error)") + } else { + self.hideLoader() } } } diff --git a/JetstreamTests/StateMessageTests.swift b/JetstreamTests/StateMessageTests.swift index f64f4f8..1f84ee2 100644 --- a/JetstreamTests/StateMessageTests.swift +++ b/JetstreamTests/StateMessageTests.swift @@ -24,7 +24,7 @@ class StateMessageTests: XCTestCase { XCTAssertEqual(scope.modelObjects.count, 1, "Correct number of objects in scope to start with") client = Client(transportAdapter: WebSocketTransportAdapter(options: WebSocketConnectionOptions(url: NSURL(string: "localhost")!))) - var msg = SessionCreateReplyMessage(index: 1, success: true, sessionToken: "jeah", response: nil) + var msg = SessionCreateReplyMessage(index: 1, sessionToken: "jeah", error: nil) client.receivedMessage(msg) client.session!.scopeAttach(scope, scopeIndex: 1) diff --git a/JetstreamTests/TransactionTests.swift b/JetstreamTests/TransactionTests.swift index 82fbe15..9395ad4 100644 --- a/JetstreamTests/TransactionTests.swift +++ b/JetstreamTests/TransactionTests.swift @@ -27,7 +27,7 @@ class TransactionTests: XCTestCase { scope.getAndClearSyncFragments() client = Client(transportAdapter: WebSocketTransportAdapter(options: WebSocketConnectionOptions(url: NSURL(string: "localhost")!))) - var msg = SessionCreateReplyMessage(index: 1, success: true, sessionToken: "jeah", response: nil) + var msg = SessionCreateReplyMessage(index: 1, sessionToken: "jeah", error: nil) client.receivedMessage(msg) client.session!.scopeAttach(scope, scopeIndex: 1) } diff --git a/README.md b/README.md index ae186b3..3af25d1 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,9 @@ Once you've defined your model classes, instante a canvas and mark it as a scope ```swift var canvas = Canvas() -canvas.isScopeRoot = true + +var scope = Scope(name: "Canvas") +scope.root = canvas ``` This will create a new scope and assign `canvas` as the root of the scope. The root object or any branches or leafs attached now belong to the scope. This lets you start observing changes happening to any models that have been attached to the tree: