baja/boxcs/BoxComponentSpace.js

/**
 * @copyright 2015 Tridium, Inc. All Rights Reserved.
 * @author Gareth Johnson
 */

/* eslint-disable camelcase */
/* global niagara_wb_util_getSessionOrdInHost: false */

/**
 * @module baja/boxcs/BoxComponentSpace
 * @private
 */
define([
  "bajaScript/bson",
  "bajaScript/baja/boxcs/BoxCallbacks",
  "bajaScript/baja/boxcs/syncUtil",
  "bajaScript/baja/comp/ComponentSpace",
  "bajaScript/baja/ord/Ord",
  "bajaScript/baja/boxcs/AddKnobOp",
  "bajaScript/baja/boxcs/AddOp",
  "bajaScript/baja/boxcs/FireTopicOp",
  "bajaScript/baja/boxcs/LoadOp",
  "bajaScript/baja/boxcs/RemoveKnobOp",
  "bajaScript/baja/boxcs/RemoveOp",
  "bajaScript/baja/boxcs/RenameOp",
  "bajaScript/baja/boxcs/ReorderOp",
  "bajaScript/baja/boxcs/SetOp",
  "bajaScript/baja/boxcs/SetFacetsOp",
  "bajaScript/baja/boxcs/SetFlagsOp",
  "bajaScript/baja/boxcs/AddRelationKnobOp",
  "bajaScript/baja/boxcs/RemoveRelationKnobOp",
  "bajaScript/baja/comm/Callback",
  "bajaPromises" ], function (
    baja,
    BoxCallbacks,
    syncUtil,
    ComponentSpace,
    Ord,
    AddKnobOp,
    AddOp,
    FireTopicOp,
    LoadOp,
    RemoveKnobOp,
    RemoveOp,
    RenameOp,
    ReorderOp,
    SetOp,
    SetFacetsOp,
    SetFlagsOp,
    AddRelationKnobOp,
    RemoveRelationKnobOp,
    Callback,
    bajaPromises) {
  
  "use strict";
  
  var subclass = baja.subclass,
      callSuper = baja.callSuper,
      bajaDef = baja.def,
      
      serverDecodeContext = baja.$serverDecodeContext,
      importUnknownTypes = baja.bson.importUnknownTypes,
      bsonDecodeValue = baja.bson.decodeValue,
      
      syncVal = syncUtil.syncVal,
      
      eventsQueue = [],
      syncOps = {};
  
  syncOps[AddKnobOp.id] = AddKnobOp; 
  syncOps[AddOp.id] = AddOp;
  syncOps[FireTopicOp.id] = FireTopicOp;
  syncOps[LoadOp.id] = LoadOp; 
  syncOps[RemoveKnobOp.id] = RemoveKnobOp; 
  syncOps[RemoveOp.id] = RemoveOp; 
  syncOps[RenameOp.id] = RenameOp; 
  syncOps[ReorderOp.id] = ReorderOp; 
  syncOps[SetOp.id] = SetOp;
  syncOps[SetFacetsOp.id] = SetFacetsOp; 
  syncOps[SetFlagsOp.id] = SetFlagsOp; 
  syncOps[AddRelationKnobOp.id] = AddRelationKnobOp; 
  syncOps[RemoveRelationKnobOp.id] = RemoveRelationKnobOp; 
  
  
  /**
   * BOX Component Space.
   *
   * A BOX Component Space is a Proxy Component Space that's linked to another 
   * Component Space in another host elsewhere.
   *
   * @class
   * @name baja.BoxComponentSpace
   * @extends baja.ComponentSpace
   * @private
   *
   * @param {String} name
   * @param {String} ordInHost
   * @param host
   */  
  function BoxComponentSpace(name, ordInHost, host) {
    callSuper(BoxComponentSpace, this, arguments);
    this.$callbacks = new BoxCallbacks(this);
  }
  
  subclass(BoxComponentSpace, ComponentSpace); 
  
  /**
   * Call to initialize a Component Space.
   *
   * @name baja.BoxComponentSpace#init
   * @function
   *
   * @private
   * 
   * @param {baja.comm.Batch} batch
   */
  BoxComponentSpace.prototype.init = function (batch) {
          
    // Any events are sync ops so process then in the normal way
    var that = this,
        id = that.getServerHandlerId(),
        cb;
    
    function eventHandler(events, callback) {
      that.$fw("commitSyncOps", events, callback);
    }
    
    try {   
      // Make the server side Handler for this Component Space   
      baja.comm.makeServerHandler(id, // The id of the Server Session Handler to be created
        "box:ComponentSpaceSessionHandler", // Type Spec of the Server Session Handler
        id, // Initial argument for the Server Session Handler
        eventHandler,
        new Callback(function (resp) {
          that.$isReadonly = resp.isReadonly;
        }, baja.fail, batch),
        /*makeInBatch*/true);         
              
      // Load Root Component of Station
      cb = new Callback(function ok(resp) {
      
        // Create the root of the Station
        that.$root = baja.$(resp.t);
        
        // Set the core handle of the Station
        that.$root.$handle = resp.h;
                
        // Mount the local Station root
        that.$fw("mount", that.$root);
      }, 
      baja.fail, batch);
      
      // Ensure type is imported before we create the root component
      cb.addOk(function (ok, fail, resp) {
        baja.importTypes({
          typeSpecs: [ resp.t ],
          ok: function () {
            ok(resp);
          },
          fail: fail
        });
      });
          
      // Make a call on the Server Side Handler  
      baja.comm.serverHandlerCall(this, "loadRoot", null, cb, /*makeInBatch*/true);
    } catch (err) {
      baja.fail(err);
    }

    return cb ? cb.promise() : bajaPromises.resolve();
  };
  
  /**
   * Sync the Component Space.
   *
   * This method will result in a network call to sync the master Space with this one.
   *
   * An Object Literal is used for the method's arguments.
   *
   * @name baja.BoxComponentSpace#sync
   * @function
   *
   * @private
   *
   * @param {Object} [obj] the Object Literal for the method's arguments.
   * @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback.
   * Called once the Component Space has been successfully synchronized with the
   * Server.
   * @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
   * Called If the Component Space can't be synchronized.
   * @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
   * batched into this object
   * @returns {Promise} a promise that will be resolved once the component space
   * is synced.
   */
  BoxComponentSpace.prototype.sync = function (obj) {
    obj = baja.objectify(obj, "ok");
    var cb = new Callback(obj.ok, obj.fail, obj.batch);
    try {
      this.$callbacks.poll(cb);
    } catch (err) {
      cb.fail(err);
    }
    return cb.promise();
  };
  
  /**
   * Find the Component via its handle (null if not found).
   *
   * An Object Literal is used for the method's arguments.
   *
   * @name baja.BoxComponentSpace#resolveByHandle
   * @function
   *
   * @private
   *
   * @param {Object} [obj] the Object Literal for the method's arguments.
   * @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback.
   * Called if the Component is resolved. The Component instance will be passed
   * to this function.
   * @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
   * Called if there's an error or the Component can't be resolved.
   * @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
   * batched into this object.
   * @returns {Promise.<baja.Component|null>} a promise that will be resolved
   * once the component is resolved.
   */  
  BoxComponentSpace.prototype.resolveByHandle = function (obj) {
    obj = baja.objectify(obj);
    
    var handle = obj.handle,
        cb = new Callback(obj.ok, obj.fail, obj.batch),
        that = this,
        comp;
    
    try {    
      comp = this.findByHandle(handle);    
      if (comp !== null) {
        cb.ok(comp);
      } else {
      
        // Intermediate callback to resolve the SlotPath into the target Component
        cb.addOk(function (ok, fail, slotPath) {
          // Resolve the SlotPath ORD
          Ord.make(slotPath.toString()).get({
            "base": that,
            "ok": ok, 
            "fail": fail
          });
        });
      
        this.$callbacks.handleToPath(handle, cb);
      }
    } catch (err) {
      cb.fail(err);
    }
    
    return cb.promise();
  }; 

  /**
   * Resolve to a list of enabled mix-in Types for the Component Space.
   *
   * An Object Literal is used for the method's arguments.
   *
   * @name baja.BoxComponentSpace#toEnabledMixIns
   * @function
   *
   * @param {Object} [obj] the Object Literal for the method's arguments.
   * @param {Function} [obj.ok] (Deprecated: use Promise) Callback handler
   * invoked once the enabled mix-in Types have been resolved.
   * @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
   * @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
   * batched into this object.
   * @returns {Promise.<Array.<Object>>} a promise that will be resolved once
   * the mixin information has been retrieved.
   */  
  BoxComponentSpace.prototype.toEnabledMixIns = function (obj) {
    obj = baja.objectify(obj);
    
    var cb = new Callback(obj.ok, obj.fail, obj.batch);

    try {
      // Intermediate callback to resolve the types from enabled mix-in
      // call.
      cb.addOk(function (ok, fail, typeSpecs) {
        baja.importTypes({
          typeSpecs: typeSpecs,
          ok: ok,
          fail: fail
        });
      });
    
      this.$callbacks.toEnabledMixIns(cb);
    } catch (err) {
      cb.fail(err);
    }
    
    return cb.promise();
  }; 
              
  /**
   * Commit Slots to the Component Space.
   *
   * @param {BoxComponentSpace} boxSpace
   * @param {Array} slotInfo.
   *
   * @private
   */
  function commitSlotInfo(boxSpace, slotInfo) {
    var comp, cx, newVal, slot, bson, i;
    
    for (i = 0; i < slotInfo.length; ++i) {
  
      bson = slotInfo[i];
      
      // Attempt to find the Component
      comp = boxSpace.findByHandle(bson.h);
            
      // Only load a singular Slot if the Component isn't already loaded
      // TODO: Ensure we sync with master before loadSlot is processed in
      // ORD resolution
      if (comp !== null && !comp.$bPropsLoaded) {
      
        // What about mounting a Component???
      
        // Decode the Value
        newVal = bsonDecodeValue(bson.v, serverDecodeContext);
        
        // Force any Component to be stubbed
        if (newVal.getType().isComponent()) {
          newVal.$bPropsLoaded = false;
        }
      
        cx = {
          commit: true, 
          serverDecode: true, 
          fromLoad: true 
        };
        
        // Add the display name if we've got one
        if (bson.dn) {    
          cx.displayName = bson.dn; 
        }
        
        // Add the display string if we've got one
        if (bson.d) {    
          cx.display = bson.d; 
        }
              
        // TODO: What if the Component is already fully loaded?
      
        // Does the Slot currently exist?
        slot = comp.getSlot(bson.n);
        if (slot === null) {
        
          // Add the Slot if it doesn't currently exist
          comp.add({
            "slot": bson.n, 
            "value": newVal, 
            "flags": baja.Flags.decodeFromString(bajaDef(bson.f, "")), 
            "facets": baja.Facets.DEFAULT.decodeFromString(bajaDef(bson.x, ""), baja.Simple.$unsafeDecode),
            "cx": cx
          });
        } else {
          if (bson.dn) {
            slot.$setDisplayName(bson.dn);
          }
          
          // Synchronize the value
          syncVal(newVal, comp, slot, bson.d);
        }
      }
    }
  }

  /** @returns {Promise} */
  function processNext() {
    var events = eventsQueue[0],
      space = events.space,
      callback = events.cb,
      syncOpsArray = events.data.ops;

    /** @returns {Promise} */
    function doDecodeAndCommit(syncOpData) {
      var comp, parentProp;

      try {
        // Get Component from SyncOp
        if (syncOpData.h !== undefined) { // Was the handle encoded?
          comp = space.findByHandle(syncOpData.h);
          parentProp = comp && comp.getPropertyInParent();

          // Update the display name of the component
          if (parentProp) {
            parentProp.$setDisplayName(syncOpData.cdn);
            parentProp.$setDisplay(syncOpData.cd);
          }
        } else {
          comp = null;
        }

        var syncOp = syncOps[syncOpData.nm];

        // Look up SyncOp, decode and Commit
        return bajaPromises.resolve(syncOp && syncOp.decodeAndCommit(comp, syncOpData));
      } catch (e) {
        return bajaPromises.reject(e);
      }
    }

    function finish() {
      for (var i = 0; i < eventsQueue.length; ++i) {
        if (eventsQueue[i] === events) {
          eventsQueue.splice(i, 1);
          break;
        }
      }

      if (eventsQueue.length > 0) {
        return processNext();
      }
    }

    function commit() {
      return (function decodeAtIndex(i) {
        var syncOp = syncOpsArray[i];
        if (syncOp) {
          return doDecodeAndCommit(syncOp)
            .then(function () {
              return decodeAtIndex(++i);
            });
        } else {
          return bajaPromises.resolve();
        }
      }(0))
        .then(callback)
        .then(finish, function (err) {
          baja.error('SyncOp failed to decode:');
          baja.error(err);
          return finish(); // keep going even if one SyncOp fails to decode
        });
    }

    return doImportTypes(syncOpsArray)
      .then(commit, commit);
  }

  function doImportTypes(bson) {
    var df = bajaPromises.deferred();
    importUnknownTypes(bson, df.resolve);
    return df.promise();
  }
  
  function eventsQueueSort(a, b) {
    return parseInt(a.data.id, 10) - parseInt(b.data.id, 10);
  }
  
  /**
   * Commit the sync ops to the Component Space.
   *
   * @private
   *
   * @param {BoxComponentSpace} boxSpace
   * @param {Object} eventData  the event data that contains the Sync Ops to commit.
   * @param {Function} callback the function callback for when everything has been processed.
   */
  function commitSyncOps(boxSpace, eventData, callback) {
    // Add to queue
    eventsQueue.push({
      space: boxSpace,
      data: eventData,
      cb: callback
    });

    // If this is the only thing in the queue to process then process it.
    if (eventsQueue.length === 1) {
      processNext().catch(baja.error);
    } else if (eventsQueue.length > 1) {
      eventsQueue.sort(eventsQueueSort);
    }
  }
  
  /**
   * Private framework handler for a Component Space.
   *
   * This is a private internal method for framework developers.
   *
   * @name baja.BoxComponentSpace#$fw
   * @function
   *
   * @private
   */
  BoxComponentSpace.prototype.$fw = function (x, a, b, c) {    
    if (x === "commitSyncOps") {
      // Process sync ops
      commitSyncOps(this, /*SyncOps*/a, /*callback*/b);
    } else if (x === "commitSlotInfo") {
      // Commit a singular Slot
      commitSlotInfo(this, /*Slot Information to Commit*/a);
    } else {
      // Else call super framework handler
      callSuper("$fw", BoxComponentSpace, this, arguments);
    }
  };

  /**
   * Get the default ORD to a component space, relative to the host. If running
   * in the browser, this will simply be `station:`. If Workbench interop is
   * present, then Java-JS interop may indicate that BOX is using a remote
   * connection to a station via FOX; in that case, this ORD will include the
   * appropriate session query.
   * @private
   * @returns {baja.Ord}
   */
  BoxComponentSpace.$getDefaultOrdInHost = function () {
    var sessionOrdInHost;
    // eslint-disable-next-line camelcase
    if (typeof niagara_wb_util_getSessionOrdInHost === 'function') {
      sessionOrdInHost = niagara_wb_util_getSessionOrdInHost();
    }

    if (sessionOrdInHost) {
      return Ord.make({ base: sessionOrdInHost, child: 'station:' });
    } else {
      return Ord.make('station:');
    }
  };
  
  return BoxComponentSpace;
});