From 25ccae982f4037b32d39d6228cdd06c3fc026ea4 Mon Sep 17 00:00:00 2001 From: Rob Skillington Date: Tue, 11 Nov 2014 17:03:47 -0800 Subject: [PATCH 1/4] Updating for server compatibility with fixes for reply network messages --- Jetstream/ModelObject.swift | 2 +- Jetstream/NetworkMessage.swift | 4 +++ Jetstream/Scope.swift | 17 ++++++++--- Jetstream/ScopeFetchReplyMessage.swift | 20 +++++++++++-- Jetstream/Session.swift | 4 +-- Jetstream/SyncFragment.swift | 8 ++--- .../ShapesDemoViewController.swift | 29 +++++++++++-------- README.md | 4 ++- 8 files changed, 61 insertions(+), 27 deletions(-) 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..8d5deba 100644 --- a/Jetstream/ScopeFetchReplyMessage.swift +++ b/Jetstream/ScopeFetchReplyMessage.swift @@ -17,12 +17,16 @@ class ScopeFetchReplyMessage: ReplyMessage { return ScopeFetchReplyMessage.messageType } + let success: Bool let scopeIndex: UInt? let error: NSError? + let response: AnyObject? - init(index: UInt, replyTo: UInt, scopeIndex: UInt?, error: NSError? = nil) { + init(index: UInt, replyTo: UInt, success: Bool, scopeIndex: UInt?, error: NSError?, response: AnyObject?) { + self.success = success self.scopeIndex = scopeIndex self.error = error + self.response = response super.init(index: index, replyTo: replyTo) } @@ -33,16 +37,26 @@ class ScopeFetchReplyMessage: ReplyMessage { override class func unserialize(dictionary: [String: AnyObject]) -> NetworkMessage? { var index = dictionary["index"] as? UInt var replyTo = dictionary["replyTo"] as? UInt + var success = dictionary["success"] as? Bool var scopeIndex = dictionary["scopeIndex"] as? UInt + var error: NSError? if let serializedError = dictionary["error"] as? [String: AnyObject] { error = errorFromDictionary(.SyncFragmentApplyError, serializedError) } - if index == nil || replyTo == nil || (scopeIndex == nil && error == nil) { + var response: AnyObject? = dictionary["response"] + + if index == nil || replyTo == nil || success == nil || (scopeIndex == nil && error == nil) { return nil } else { - return ScopeFetchReplyMessage(index: index!, replyTo: replyTo!, scopeIndex: scopeIndex, error: error) + return ScopeFetchReplyMessage( + index: index!, + replyTo: replyTo!, + success: success!, + scopeIndex: scopeIndex, + error: error, + response: response) } } } diff --git a/Jetstream/Session.swift b/Jetstream/Session.swift index 98ff47e..6f57e69 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 let definiteRootModelObject = scope.root { 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 let definiteRootModelObject = scope.root { if scopeSyncMessage.syncFragments.count > 0 { scope.startApplyingRemote() scope.applySyncFragments(scopeSyncMessage.syncFragments) diff --git a/Jetstream/SyncFragment.swift b/Jetstream/SyncFragment.swift index c8e02a7..83d932c 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 definiteRootModelObject = scope.root { + applyPropertiesToModelObject(definiteRootModelObject, scope: scope, applyDefaults: applyDefaults) + scope.updateUUIDForModel(definiteRootModelObject, 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..d5e8e1f 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) @@ -47,15 +47,20 @@ class ShapesDemoViewController: UIViewController, NSURLConnectionDataDelegate { let transportAdapter = WebsocketTransportAdapter(options: WebsocketConnectionOptions(url: NSURL(string: "ws://" + host + ":3000")!)) 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) { [weak self] session in + if let definiteSelf = self { + definiteSelf.sessionDidStart(session) + } + } + } + + 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/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: From 8ad759e4c23fcabfca7b238a5d172cfd6e41305d Mon Sep 17 00:00:00 2001 From: Rob Skillington Date: Tue, 11 Nov 2014 17:40:18 -0800 Subject: [PATCH 2/4] Remove optional responses for some reply messages as per CR --- .../xcschemes/JetstreamTests.xcscheme | 96 +++++++++++++++++++ Jetstream/ScopeFetchReplyMessage.swift | 9 +- Jetstream/SessionCreateReplyMessage.swift | 8 +- JetstreamTests/StateMessageTests.swift | 2 +- JetstreamTests/TransactionTests.swift | 2 +- 5 files changed, 102 insertions(+), 15 deletions(-) create mode 100644 Jetstream.xcodeproj/xcshareddata/xcschemes/JetstreamTests.xcscheme 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/ScopeFetchReplyMessage.swift b/Jetstream/ScopeFetchReplyMessage.swift index 8d5deba..a1cd786 100644 --- a/Jetstream/ScopeFetchReplyMessage.swift +++ b/Jetstream/ScopeFetchReplyMessage.swift @@ -20,13 +20,11 @@ class ScopeFetchReplyMessage: ReplyMessage { let success: Bool let scopeIndex: UInt? let error: NSError? - let response: AnyObject? - init(index: UInt, replyTo: UInt, success: Bool, scopeIndex: UInt?, error: NSError?, response: AnyObject?) { + init(index: UInt, replyTo: UInt, success: Bool, scopeIndex: UInt?, error: NSError?) { self.success = success self.scopeIndex = scopeIndex self.error = error - self.response = response super.init(index: index, replyTo: replyTo) } @@ -45,8 +43,6 @@ class ScopeFetchReplyMessage: ReplyMessage { error = errorFromDictionary(.SyncFragmentApplyError, serializedError) } - var response: AnyObject? = dictionary["response"] - if index == nil || replyTo == nil || success == nil || (scopeIndex == nil && error == nil) { return nil } else { @@ -55,8 +51,7 @@ class ScopeFetchReplyMessage: ReplyMessage { replyTo: replyTo!, success: success!, scopeIndex: scopeIndex, - error: error, - response: response) + error: error) } } } diff --git a/Jetstream/SessionCreateReplyMessage.swift b/Jetstream/SessionCreateReplyMessage.swift index dcaad00..7d9a73e 100644 --- a/Jetstream/SessionCreateReplyMessage.swift +++ b/Jetstream/SessionCreateReplyMessage.swift @@ -19,12 +19,10 @@ class SessionCreateReplyMessage: NetworkMessage { let success: Bool let sessionToken: String - let response: AnyObject? - init(index: UInt, success: Bool, sessionToken: String, response: AnyObject?) { + init(index: UInt, success: Bool, sessionToken: String) { self.success = success self.sessionToken = sessionToken - self.response = response super.init(index: index) } @@ -32,7 +30,6 @@ class SessionCreateReplyMessage: 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 { return nil @@ -40,8 +37,7 @@ class SessionCreateReplyMessage: NetworkMessage { return SessionCreateReplyMessage( index: index!, success: success!, - sessionToken: sessionToken!, - response: response) + sessionToken: sessionToken!) } } } diff --git a/JetstreamTests/StateMessageTests.swift b/JetstreamTests/StateMessageTests.swift index eea6bbe..5e4dfd5 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, success: true, sessionToken: "jeah") client.receivedMessage(msg) client.session!.scopeAttach(scope, scopeIndex: 1) diff --git a/JetstreamTests/TransactionTests.swift b/JetstreamTests/TransactionTests.swift index 70749ce..c82780c 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, success: true, sessionToken: "jeah") client.receivedMessage(msg) client.session!.scopeAttach(scope, scopeIndex: 1) } From 00e8e7e6ce076348f2d34bd834d6e16ea354a5df Mon Sep 17 00:00:00 2001 From: Rob Skillington Date: Tue, 11 Nov 2014 18:00:02 -0800 Subject: [PATCH 3/4] Remove all `success` fields and use `error` with nil to indicate success --- Jetstream/Client.swift | 11 +++++------ Jetstream/ScopeFetchReplyMessage.swift | 8 ++------ Jetstream/SessionCreateReplyMessage.swift | 20 ++++++++++++-------- JetstreamTests/StateMessageTests.swift | 2 +- JetstreamTests/TransactionTests.swift | 2 +- 5 files changed, 21 insertions(+), 22 deletions(-) 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/ScopeFetchReplyMessage.swift b/Jetstream/ScopeFetchReplyMessage.swift index a1cd786..30bdc5f 100644 --- a/Jetstream/ScopeFetchReplyMessage.swift +++ b/Jetstream/ScopeFetchReplyMessage.swift @@ -17,12 +17,10 @@ class ScopeFetchReplyMessage: ReplyMessage { return ScopeFetchReplyMessage.messageType } - let success: Bool let scopeIndex: UInt? let error: NSError? - init(index: UInt, replyTo: UInt, success: Bool, scopeIndex: UInt?, error: NSError?) { - self.success = success + init(index: UInt, replyTo: UInt, scopeIndex: UInt?, error: NSError?) { self.scopeIndex = scopeIndex self.error = error super.init(index: index, replyTo: replyTo) @@ -35,7 +33,6 @@ class ScopeFetchReplyMessage: ReplyMessage { override class func unserialize(dictionary: [String: AnyObject]) -> NetworkMessage? { var index = dictionary["index"] as? UInt var replyTo = dictionary["replyTo"] as? UInt - var success = dictionary["success"] as? Bool var scopeIndex = dictionary["scopeIndex"] as? UInt var error: NSError? @@ -43,13 +40,12 @@ class ScopeFetchReplyMessage: ReplyMessage { error = errorFromDictionary(.SyncFragmentApplyError, serializedError) } - if index == nil || replyTo == nil || success == nil || (scopeIndex == nil && error == nil) { + if index == nil || replyTo == nil || (scopeIndex == nil && error == nil) { return nil } else { return ScopeFetchReplyMessage( index: index!, replyTo: replyTo!, - success: success!, scopeIndex: scopeIndex, error: error) } diff --git a/Jetstream/SessionCreateReplyMessage.swift b/Jetstream/SessionCreateReplyMessage.swift index 7d9a73e..0e1f822 100644 --- a/Jetstream/SessionCreateReplyMessage.swift +++ b/Jetstream/SessionCreateReplyMessage.swift @@ -17,27 +17,31 @@ class SessionCreateReplyMessage: NetworkMessage { return SessionCreateReplyMessage.messageType } - let success: Bool - let sessionToken: String + let sessionToken: String? + let error: NSError? - init(index: UInt, success: Bool, sessionToken: String) { - self.success = success + init(index: UInt, sessionToken: String?, error: NSError?) { self.sessionToken = sessionToken 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 - 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!) + sessionToken: sessionToken, + error: error) } } } diff --git a/JetstreamTests/StateMessageTests.swift b/JetstreamTests/StateMessageTests.swift index 5e4dfd5..4ec16cb 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") + 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 c82780c..5d283aa 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") + var msg = SessionCreateReplyMessage(index: 1, sessionToken: "jeah", error: nil) client.receivedMessage(msg) client.session!.scopeAttach(scope, scopeIndex: 1) } From 78d3ab77470d92fdc5bed6902aa93fafa2d9c7e1 Mon Sep 17 00:00:00 2001 From: Rob Skillington Date: Tue, 11 Nov 2014 18:03:50 -0800 Subject: [PATCH 4/4] Code review feedback --- Jetstream/Session.swift | 4 ++-- Jetstream/SyncFragment.swift | 6 +++--- .../JetstreamDemos/ShapesDemoViewController.swift | 11 ++++------- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/Jetstream/Session.swift b/Jetstream/Session.swift index 6f57e69..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 definiteRootModelObject = scope.root { + 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 definiteRootModelObject = scope.root { + if scope.root != nil { if scopeSyncMessage.syncFragments.count > 0 { scope.startApplyingRemote() scope.applySyncFragments(scopeSyncMessage.syncFragments) diff --git a/Jetstream/SyncFragment.swift b/Jetstream/SyncFragment.swift index 83d932c..c8a9335 100644 --- a/Jetstream/SyncFragment.swift +++ b/Jetstream/SyncFragment.swift @@ -241,9 +241,9 @@ public class SyncFragment: Equatable { switch type { case .Root: - if let definiteRootModelObject = scope.root { - applyPropertiesToModelObject(definiteRootModelObject, scope: scope, applyDefaults: applyDefaults) - scope.updateUUIDForModel(definiteRootModelObject, 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 d5e8e1f..4914da6 100644 --- a/JetstreamDemos/JetstreamDemos/ShapesDemoViewController.swift +++ b/JetstreamDemos/JetstreamDemos/ShapesDemoViewController.swift @@ -44,19 +44,16 @@ 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) { [weak self] session in - if let definiteSelf = self { - definiteSelf.sessionDidStart(session) - } - } + client?.onSession.listenOnce(self) { [unowned self] in self.sessionDidStart($0) } } func sessionDidStart(session: Session) { self.session = session - session.fetch(scope) { (error) in + session.fetch(scope) { error in if error != nil { self.alertError("Error fetching scope", message: "\(error)") } else {