From 748957ef9fea673f5bce43b8213f3040c5853fb6 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Tue, 11 Jun 2019 13:58:54 +0200 Subject: [PATCH 1/2] Create a map between template id and associated faces and actions. --- .../Core/Data Pool/Regions/BLOCKvRegion.swift | 100 ++++++++++++++++-- 1 file changed, 90 insertions(+), 10 deletions(-) diff --git a/BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift b/BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift index 9a2e97dc..0a4ce315 100644 --- a/BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift +++ b/BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift @@ -165,6 +165,30 @@ class BLOCKvRegion: Region { self.update(objects: [changes]) } + + private var templateIndex: [String: TemplateMap] = [:] + + fileprivate struct TemplateMap: Equatable { + + fileprivate var faceIdentifiers: Set = [] + fileprivate var actionIdentifiers: Set = [] + + init() { + self.faceIdentifiers = [] + self.faceIdentifiers = [] + } + + } + + override func add(objects: [DataObject]) { + + // build up index + self.createIndex(objects: objects) + + print(templateIndex) + + super.add(objects: objects) + } // MARK: - Transformations @@ -206,16 +230,15 @@ class BLOCKvRegion: Region { // get vatom info guard let template = object.data![keyPath: "vAtom::vAtomType.template"] as? String else { return nil } - - // fetch all faces linked to this vatom - let faces = objects.values.filter { $0.type == "face" && $0.data?["template"] as? String == template } - objectData["faces"] = faces.map { $0.data } - - // fetch all actions linked to this vatom - let actionNamePrefix = template + "::Action::" - let actions = objects.values.filter { $0.type == "action" && ($0.data?["name"] as? String)? - .starts(with: actionNamePrefix) == true } - objectData["actions"] = actions.map { $0.data } + + let faceIds = self.templateIndex[template]?.faceIdentifiers ?? [] + let actionIds = self.templateIndex[template]?.actionIdentifiers ?? [] + + let faces = objects.filter { faceIds.contains($0.key) } + let actions = objects.filter {actionIds.contains($0.key) } + + objectData["faces"] = faces.map { $0.value.data } + objectData["actions"] = actions.map { $0.value.data } do { if JSONSerialization.isValidJSONObject(objectData) { @@ -393,3 +416,60 @@ class BLOCKvRegion: Region { } } + + +extension BLOCKvRegion { + + /// Creates a fast lookup index mapping template identifers to their faces and actions. + func createIndex(objects: [DataObject]) { + + // build up index + for object in objects { + + if object.type == "vatom" { + + // find template + guard let templateID = object.data![keyPath: "vAtom::vAtomType.template"] as? String else { + assertionFailure("Missing template id.") + return + } + self.templateIndex[templateID] = TemplateMap() + } + + else if object.type == "face" { + + // find template + guard let faceTemplateID = object.data![keyPath: "template"] as? String else { + assertionFailure("Missing template id.") + return + } + // insert mapped face id + self.templateIndex[faceTemplateID]?.faceIdentifiers.insert(object.id) + + } + + else if object.type == "action" { + + // find compound name + guard let compoundName = object.data![keyPath: "name"] as? String else { + assertionFailure("Missing compound name.") + return + } + + // find the marker + guard let markerRange = compoundName.range(of: "::action::", options: .caseInsensitive, range: nil, + locale: nil) + else { return } + + // extract template id + let actionTemplateID = String(compoundName[compoundName.startIndex.. Date: Thu, 13 Jun 2019 15:56:50 +0200 Subject: [PATCH 2/2] Remove orig file --- .../Data Pool/Regions/BLOCKvRegion.swift.orig | 490 ------------------ 1 file changed, 490 deletions(-) delete mode 100644 BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift.orig diff --git a/BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift.orig b/BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift.orig deleted file mode 100644 index e0f12f9b..00000000 --- a/BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift.orig +++ /dev/null @@ -1,490 +0,0 @@ -// -// BlockV AG. Copyright (c) 2018, all rights reserved. -// -// Licensed under the BlockV SDK License (the "License"); you may not use this file or -// the BlockV SDK except in compliance with the License accompanying it. Unless -// required by applicable law or agreed to in writing, the BlockV SDK distributed under -// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -// ANY KIND, either express or implied. See the License for the specific language -// governing permissions and limitations under the License. -// - -import Foundation -//import DictionaryCoding - -/// Abstract subclass of `Region`. This intermediate class handles updates from the BLOCKv Web socket. Regions should -/// subclass to automatically handle Web socket updates. -/// -/// The BLOCKv subclass is hardcoded to treat the 'objects' as vatoms. That is, the freeform object must be a vatom. -/// This is a product of the design, but it could be generalised in the future if say faces were to become a monitored -/// region. -/// -/// Roles: -/// - Handles some Web socket events (including queuing, pausing, and processing). -/// - Only 'state_update' events are intercepted. -/// > State-update messages are transformed into DataObjectUpdateRecord (Sparse Object) and use to update the region. -/// - Data transformations (map) to VatomModel -/// - Notifications -/// > Add, update, remove notification are broadcast for the changed vatom. An update is always emitted for the parent -/// of the changed vatom. This is useful since the parent can then update its state, e.g. on child removal. -/// - Parsing unpackaged vatom payload into data-objects. -class BLOCKvRegion: Region { - - /// Constructor - required init(descriptor: Any) throws { - try super.init(descriptor: descriptor) - - // subscribe to socket connections - BLOCKv.socket.onConnected.subscribe(with: self) { _ in - self.onWebSocketConnect() - } - - // subscribe to raw socket messages - BLOCKv.socket.onMessageReceivedRaw.subscribe(with: self) { descriptor in - self.onWebSocketMessage(descriptor) - } - - // monitor for timed updates - DataObjectAnimator.shared.add(region: self) - - } - - deinit { - - // stop listening for animation updates - DataObjectAnimator.shared.remove(region: self) - - } - - /// Queue of pending messages. - private var queuedMessages: [[String: Any]] = [] - /// Boolean value that is `true` if message processing is paused. - private var socketPaused = false - /// Boolean valie that is `true` if a message is currently being processed. - private var socketProcessing = false - - /// Called when this region is going to be shut down. - override func close() { - super.close() - - // remove listeners - DataObjectAnimator.shared.remove(region: self) - - } - - /// Called to pause processing of socket messages. - func pauseMessages() { - self.socketPaused = true - } - - /// Called to resume processing of socket messages. - func resumeMessages() { - - // unpause - self.socketPaused = false - - // process next message if needed - if !self.socketProcessing { - self.processNextMessage() - } - - } - - /// Called when the Web socket re-connects. - @objc func onWebSocketConnect() { - - // mark as unstable - self.synchronized = false - - // re-sync the entire thing. Don't worry about synchronize() getting called while it's running already, - // it handles that case. - self.synchronize() - - } - - /// Called when there's a new event message via the Web socket. - @objc func onWebSocketMessage(_ descriptor: [String: Any]) { - - // add to queue - self.queuedMessages.append(descriptor) - - // process it if necessary - if !self.socketPaused && !self.socketProcessing { - self.processNextMessage() - } - - } - - /// Called to process the next WebSocket message. - func processNextMessage() { - - // stop if socket is paused - if socketPaused { - return - } - - // stop if already processing - if socketProcessing { return } - socketProcessing = true - - // get next msg to process - if queuedMessages.count == 0 { - - // no more messages - self.socketProcessing = false - return - - } - - // process message - let msg = queuedMessages.removeFirst() - self.processMessage(msg) - - // done, process next message - self.socketProcessing = false - self.processNextMessage() - - } - - /// Processes a raw Web socket message. - /// - /// Only 'state_update' events intercepted and used to perform parital updates on the region's objects. - /// Message processing is not paused for 'state_update' events. - func processMessage(_ msg: [String: Any]) { - - // get info - guard let msgType = msg["msg_type"] as? String else { return } - guard let payload = msg["payload"] as? [String: Any] else { return } - guard let newData = payload["new_object"] as? [String: Any] else { return } - guard let vatomID = payload["id"] as? String else { return } - if msgType != "state_update" { - return - } - - // update existing objects - let changes = DataObjectUpdateRecord(id: vatomID, changes: newData) - self.update(objects: [changes]) - - } - - private var templateIndex: [String: TemplateMap] = [:] - - fileprivate struct TemplateMap: Equatable { - - fileprivate var faceIdentifiers: Set = [] - fileprivate var actionIdentifiers: Set = [] - - init() { - self.faceIdentifiers = [] - self.faceIdentifiers = [] - } - - } - - override func add(objects: [DataObject]) { - - // build up index - self.createIndex(objects: objects) - - print(templateIndex) - - super.add(objects: objects) - } - - // MARK: - Transformations - - /// Map data objects to Vatom objects. - /// - /// This is the primary transformation function which converts freeform data pool objects into concrete types. - override func map(_ object: DataObject) -> Any? { - - //FIXME: This method is synchronous which may affect performance. - - /* - How to transfrom data objects into types? - - Data > Decoder > Type (decode from external representation) - Type > Encoder > Data (encode for extrernal representation) - - Facts: - - Data pool store heterogeneous object of type [String: Any] - it is type independent. - - `map(:DataObject)` needs to transformt this into a concrete type. - - The codable machinary is good for data <> native type transformations. - - - Options: - 1. Convert [String: Any] into Data, then Data into Type (very inefficient). - 2. Write an init(descriptor: [String: Any])` - this allows VatomModel to be initialized with a dictionary. - > This sucks because a) it's a lot of work, b) does not leverage the CodingKeys of Codable conformance. - 3. Write a Decoder with transforms [String: Any] into Type AND leverages the CodingKeys - */ - - // only handle vatoms - guard object.type == "vatom" else { - return nil - } - - // stop if no data available - guard var objectData = object.data else { - return nil - } - - // get vatom info - guard let template = object.data![keyPath: "vAtom::vAtomType.template"] as? String else { return nil } -<<<<<<< HEAD - - let faceIds = self.templateIndex[template]?.faceIdentifiers ?? [] - let actionIds = self.templateIndex[template]?.actionIdentifiers ?? [] - - let faces = objects.filter { faceIds.contains($0.key) } - let actions = objects.filter {actionIds.contains($0.key) } - - objectData["faces"] = faces.map { $0.value.data } - objectData["actions"] = actions.map { $0.value.data } - -======= - - // fetch all faces linked to this vatom - let faces = objects.values.filter { $0.type == "face" && $0.data?["template"] as? String == template } - objectData["faces"] = faces.map { $0.data } - - // fetch all actions linked to this vatom - let actionNamePrefix = template + "::Action::" - let actions = objects.values.filter { $0.type == "action" && ($0.data?["name"] as? String)? - .starts(with: actionNamePrefix) == true } - objectData["actions"] = actions.map { $0.data } - - // create vatoms, face, anf actions using member-wise initilializer ->>>>>>> core/fix/vatom-descriptable - do { - let faces = faces.compactMap { $0.data }.compactMap { try? FaceModel(from: $0) } - let actions = actions.compactMap { $0.data }.compactMap { try? ActionModel(from: $0) } - var vatom = try VatomModel(from: objectData) - vatom.faceModels = faces - vatom.actionModels = actions - return vatom - } catch { - printBV(error: error.localizedDescription) - return nil - } - - } - - /// Parses the unpackaged vatom payload from the server and returns an array of `DataObject`. - /// - /// Returns `nil` if the payload cannot be parsed. - func parseDataObject(from payload: [String: Any]) -> [DataObject]? { - - // create list of items - var items: [DataObject] = [] - - // ensure - guard - let vatoms = payload["vatoms"] as? [[String: Any]] ?? payload["results"] as? [[String: Any]], - let faces = payload["faces"] as? [[String: Any]], - let actions = payload["actions"] as? [[String: Any]] - else { return nil } - - // add faces to the list - for face in faces { - - // add data object - let obj = DataObject() - obj.type = "face" - obj.id = face["id"] as? String ?? "" - obj.data = face - items.append(obj) - - } - - // add actions to the list - for action in actions { - - // add data object - let obj = DataObject() - obj.type = "action" - obj.id = action["name"] as? String ?? "" - obj.data = action - items.append(obj) - - } - - // add vatoms to the list - for vatom in vatoms { - - // add data object - let obj = DataObject() - obj.type = "vatom" - obj.id = vatom["id"] as? String ?? "" - obj.data = vatom - items.append(obj) - - } - - return items - - } - - // MARK: - Notifications - - // - Add - - /// Called when an object is about to be added. -// override func will(add object: DataObject) { -// -// // Notify parent as well -// guard let parentID = (object.data?["vAtom::vAtomType"] as? [String: Any])?["parent_id"] as? String else { -// return -// } -// DispatchQueue.main.async { -// // broadcast update the vatom's parent -// self.emit(.objectUpdated, userInfo: ["id": parentID]) //FIXME: Does this make sense? If the parent calls list children at this point it will not have updated yet -//// // broadbast the add -// self.emit(.objectAdded, userInfo: ["id": object.id]) -// } -// -// } - - override func did(add object: DataObject) { - // Notify parent as well - guard let parentID = (object.data?["vAtom::vAtomType"] as? [String: Any])?["parent_id"] as? String else { - return - } - DispatchQueue.main.async { - // broadcast update the vatom's parent - self.emit(.objectUpdated, userInfo: ["id": parentID]) - // broadbast the add - self.emit(.objectAdded, userInfo: ["id": object.id]) - } - } - - // - Update - - /// Called when an object is about to be updated. - override func will(update object: DataObject, withFields: [String: Any]) { - - // notify parent as well - guard let oldParentID = (object.data?["vAtom::vAtomType"] as? [String: Any])?["parent_id"] as? String else { - return - } - guard let newParentID = (withFields["vAtom::vAtomType"] as? [String: Any])?["parent_id"] as? String else { - return - } - DispatchQueue.main.async { - self.emit(.objectUpdated, userInfo: ["id": oldParentID]) - self.emit(.objectUpdated, userInfo: ["id": newParentID]) - } - - } - - /// Called when an object is about to be updated. - override func did(update object: DataObject, withFields: [String: Any]) { - - // notify parent as well - guard let oldParentID = (object.data?["vAtom::vAtomType"] as? [String: Any])?["parent_id"] as? String else { - return - } - guard let newParentID = (withFields["vAtom::vAtomType"] as? [String: Any])?["parent_id"] as? String else { - return - } - DispatchQueue.main.async { - self.emit(.objectUpdated, userInfo: ["id": oldParentID]) - self.emit(.objectUpdated, userInfo: ["id": newParentID]) - } - - } - - /// Called when an object is about to be updated. - override func will(update: DataObject, keyPath: String, oldValue: Any?, newValue: Any?) { - - // check if parent ID is changing - if keyPath != "vAtom::vAtomType.parent_id" { - return - } - - // notify parent as well - guard let oldParentID = oldValue as? String else { return } - guard let newParentID = newValue as? String else { return } - DispatchQueue.main.async { - self.emit(.objectUpdated, userInfo: ["id": oldParentID]) - self.emit(.objectUpdated, userInfo: ["id": newParentID]) - } - - } - - // - Remove - - /// Called when an object is about to be removed. - override func will(remove object: DataObject) { - - // notify parent as well - guard let parentID = (object.data?["vAtom::vAtomType"] as? [String: Any])?["parent_id"] as? String else { - return - } - DispatchQueue.main.async { - if parentID != "." { - self.emit(.objectUpdated, userInfo: ["id": parentID]) - } - self.emit(.objectRemoved, userInfo: ["id": object.id]) - } - - } - -} - - -extension BLOCKvRegion { - - /// Creates a fast lookup index mapping template identifers to their faces and actions. - func createIndex(objects: [DataObject]) { - - // build up index - for object in objects { - - if object.type == "vatom" { - - // find template - guard let templateID = object.data![keyPath: "vAtom::vAtomType.template"] as? String else { - assertionFailure("Missing template id.") - return - } - self.templateIndex[templateID] = TemplateMap() - } - - else if object.type == "face" { - - // find template - guard let faceTemplateID = object.data![keyPath: "template"] as? String else { - assertionFailure("Missing template id.") - return - } - // insert mapped face id - self.templateIndex[faceTemplateID]?.faceIdentifiers.insert(object.id) - - } - - else if object.type == "action" { - - // find compound name - guard let compoundName = object.data![keyPath: "name"] as? String else { - assertionFailure("Missing compound name.") - return - } - - // find the marker - guard let markerRange = compoundName.range(of: "::action::", options: .caseInsensitive, range: nil, - locale: nil) - else { return } - - // extract template id - let actionTemplateID = String(compoundName[compoundName.startIndex..