bson.js

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

/**
 * BOX System Object Notation.
 *
 * BSON is BOG notation in a JSON format. JSON is used instead of XML because
 * tests show that browsers can parse JSON significantly faster.
 */
define([
  "lex!",
  "bajaScript/comp",
  "bajaScript/baja/obj/objUtil",
  "bajaPromises" ], function defineBson(
  lexjs,
  baja,
  objUtil,
  Promise) {

  // Use ECMAScript 5 Strict Mode
  "use strict";

  // Create local for improved minification
  const { BaseBajaObj, callSuper, def: bajaDef, objectify, subclass } = baja;
  const { capitalizeFirstLetter } = objUtil;
  const serverDecodeContext = baja.$serverDecodeContext = { serverDecode: true };
  const defaultDecodeAsync = baja.Simple.prototype.decodeAsync;

  let bsonDecodeValue;

  /**
   * BOX System Object Notation.
   * @namespace baja.bson
   */
  baja.bson = new BaseBajaObj();

  ////////////////////////////////////////////////////////////////
  // Decode Actions/Topics
  //////////////////////////////////////////////////////////////// 

  /**
   * Decode the BSON for an Action Type.
   *
   * @private
   *
   * @param bson the BSON to decode.
   * @returns {Type|null} the parameter for the Action Type (or null if none).
   */
  function decodeActionParamType(bson) {
    const { apt: actionParamType } = bson;
    return typeof actionParamType === 'string' ? baja.lt(actionParamType) : null;
  }

  /**
   * Decode the BSON for an Action Default.
   *
   * @private
   *
   * @param bson the BSON to decode.
   * @returns {baja.Value|null} the parameter for the Action Default (or null if none).
   */
  function decodeActionParamDefault(bson) {
    const { apd: actionParamDefault } = bson;
    if (typeof actionParamDefault === 'object') {
      return bsonDecodeValue(actionParamDefault, serverDecodeContext);
    } else {
      return null;
    }
  }

  /**
   * Decode the BSON for an Action Return Type.
   *
   * @private
   *
   * @param bson the BSON to decode.
   * @returns {Type|null} the parameter for the Action Return Type (or null if none).
   */
  function decodeActionReturnType(bson) {
    const { art: actionReturnType } = bson;
    return typeof actionReturnType === 'string' ? baja.lt(actionReturnType) : null;
  }

  /**
   * Decode the BSON for the Topic and return the event type.
   *
   * @private
   *
   * @param bson the BSON to decode.
   * @returns {Type|null} the event type for a Topic or null if not present
   */
  function decodeTopicEventType(bson) {
    const { tet: topicEventType } = bson;
    return typeof topicEventType === 'string' ? baja.lt(topicEventType) : null;
  }

  ////////////////////////////////////////////////////////////////
  // BSON Frozen Slots
  //////////////////////////////////////////////////////////////// 

  /**
   * Frozen Property Slot.
   *
   * Property defines a Slot which is a storage location
   * for a variable in a Complex.
   *
   * A new object should never be directly created with this Constructor. All Slots are
   * created internally by BajaScript.
   *
   * @class
   * @alias FrozenProperty
   * @extends baja.Property
   */
  const FrozenProperty = function (bson, complex) {
    callSuper(FrozenProperty, this, [ bson.n, bson.dn ]);
    this.$bson = bson;
    this.$complex = complex;
  };

  subclass(FrozenProperty, baja.Property);

  FrozenProperty.prototype.isFrozen = function () {
    return true;
  };

  /**
   * Return the Property value.
   *
   * Please note, this method is intended for INTERNAL use by Tridium only. An
   * external developer should never call this method.
   *
   * @private
   *
   * @returns value
   */
  FrozenProperty.prototype.$getValue = function () {
    if (this.$val === undefined) {
      const val = this.$val = bsonDecodeValue(this.$bson.v, serverDecodeContext);
      // Set up any parenting if needed      
      if (val.getType().isComplex() && this.$complex) {
        val.$parent = this.$complex;
        val.$propInParent = this;
      }
    }
    return this.$val;
  };

  /**
   * Return true if the value has been lazily decoded.
   *
   * @private
   *
   * @returns {Boolean}
   */
  FrozenProperty.prototype.$isValueDecoded = function () {
    return this.$val !== undefined;
  };

  /**
   * Set the Property value.
   *
   * Please note, this method is intended for INTERNAL use by Tridium only. An
   * external developer should never call this method.
   *
   * @private
   *
   * @param val value to be set.
   */
  FrozenProperty.prototype.$setValue = function (val) {
    this.$val = val;
  };

  /**
   * Return the Flags for the Property.
   *
   * @see baja.Flags
   *
   * @returns {Number}
   */
  FrozenProperty.prototype.getFlags = function () {
    if (this.$flags === undefined) {
      this.$flags = decodeFlags(this.$bson);
    }
    return this.$flags;
  };

  /**
   * Set the Flags for the Property.
   *
   * Please note, this method is intended for INTERNAL use by Tridium only. An
   * external developer should never call this method.
   *
   * @private
   * @see baja.Flags
   *
   * @param {Number} flags
   */
  FrozenProperty.prototype.$setFlags = function (flags) {
    this.$flags = flags;
  };

  /**
   * Return the Facets for the Property.
   *
   * @see baja.Facets
   *
   * @returns {baja.Facets} the Slot Facets
   */
  FrozenProperty.prototype.getFacets = function () {
    if (this.$facets === undefined) {
      this.$facets = this.$bson.x === undefined ? baja.Facets.DEFAULT : baja.Facets.DEFAULT.decodeFromString(this.$bson.x, baja.Simple.$unsafeDecode);
    }
    return this.$facets;
  };

  /**
   * Set the Facets for the Property.
   *
   * Please note, this method is intended for INTERNAL use by Tridium only. An
   * external developer should never call this method.
   *
   * @private
   * @see baja.Facets
   *
   * @param {baja.Facets} facets
   */
  FrozenProperty.prototype.$setFacets = function (facets) {
    this.$facets = facets;
  };

  /**
   * Return the default flags for the Property.
   *
   * @returns {Number}
   */
  FrozenProperty.prototype.getDefaultFlags = function () {
    if (this.$defFlags === undefined) {
      this.$defFlags = decodeFlags(this.$bson);
    }
    return this.$defFlags;
  };

  /**
   * Return the default value for the Property.
   *
   * @returns the default value for the Property.
   */
  FrozenProperty.prototype.getDefaultValue = function () {
    if (this.$defVal === undefined) {
      this.$defVal = bsonDecodeValue(this.$bson.v, serverDecodeContext);
    }
    return this.$defVal;
  };

  /**
   * Return the Type for this Property.
   *
   * @returns {Type} the Type for the Property.
   */
  FrozenProperty.prototype.getType = function () {
    if (this.$initType === undefined) {
      this.$initType = baja.lt(this.$bson.ts || this.$bson.v.t);
    }
    return this.$initType || null;
  };

  /**
   * Return the display String for this Property.
   *
   * Please note, this method is intended for INTERNAL use by Tridium only. An
   * external developer should never call this method.
   *
   * @private
   *
   * @returns {String}
   */
  FrozenProperty.prototype.$getDisplay = function () {
    if (this.$display === undefined) {
      this.$display = this.$bson.v.d || "";
    }
    return this.$display;
  };

  /**
   * Set the display for this Property.
   *
   * Please note, this method is intended for INTERNAL use by Tridium only. An
   * external developer should never call this method.
   *
   * @private
   *
   * @param {String} display the display String
   */
  FrozenProperty.prototype.$setDisplay = function (display) {
    this.$display = display;
  };

  /**
   * Frozen Action Slot.
   *
   * Action is a Slot that defines a behavior which can
   * be invoked on a Component.
   *
   * A new object should never be directly created with this Constructor. All Slots are
   * created internally by BajaScript
   *
   * @class
   * @alias FrozenAction
   * @extends baja.Action
   */
  const FrozenAction = function (bson) {
    callSuper(FrozenAction, this, [ bson.n, bson.dn ]);
    this.$bson = bson;
  };

  subclass(FrozenAction, baja.Action);

  FrozenAction.prototype.isFrozen = FrozenProperty.prototype.isFrozen;

  /**
   * Return the Flags for the Action.
   *
   * @function
   * @see baja.Flags
   *
   * @returns {Number}
   */
  FrozenAction.prototype.getFlags = FrozenProperty.prototype.getFlags;

  /**
   * Set the Flags for the Action.
   *
   * Please note, this method is intended for INTERNAL use by Tridium only. An
   * external developer should never call this method.
   *
   * @function
   * @private
   * @see baja.Flags
   *
   * @param {Number} flags
   */
  FrozenAction.prototype.$setFlags = FrozenProperty.prototype.$setFlags;

  /**
   * Return the Facets for the Action.
   *
   * @function
   * @see baja.Facets
   *
   * @returns the Slot Facets
   */
  FrozenAction.prototype.getFacets = FrozenProperty.prototype.getFacets;

  /**
   * Set the Facets for the Action.
   *
   * Please note, this method is intended for INTERNAL use by Tridium only. An
   * external developer should never call this method.
   *
   * @function
   * @private
   * @see baja.Facets
   *
   * @param {baja.Facets} facets
   */
  FrozenAction.prototype.$setFacets = FrozenProperty.prototype.$setFacets;

  /**
   * Return the default flags for the Action.
   *
   * @returns {Number}
   */
  FrozenAction.prototype.getDefaultFlags = FrozenProperty.prototype.getDefaultFlags;

  /**
   * Return the Action's Parameter Type.
   *
   * @returns {Type|null} the Parameter's Type (or null if the Action has no argument).
   */
  FrozenAction.prototype.getParamType = function () {
    if (this.$paramType === undefined) {
      this.$paramType = decodeActionParamType(this.$bson);
    }
    return this.$paramType;
  };

  /**
   * Return the Action's Default Value.
   *
   * @returns {baja.Value|null} the parameter default value (or null if the Action has no argument).
   */
  FrozenAction.prototype.getParamDefault = function () {
    if (this.$paramDef === undefined) {
      this.$paramDef = decodeActionParamDefault(this.$bson);
    }
    return this.$paramDef;
  };

  /**
   * Return the return Type for the Action.
   *
   * @returns {Type|null} the return Type (or null if the Action has no return Type).
   */
  FrozenAction.prototype.getReturnType = function () {
    if (this.$returnType === undefined) {
      this.$returnType = decodeActionReturnType(this.$bson);
    }
    return this.$returnType;
  };

  /**
   * Frozen Topic Slot.
   *
   * Topic defines a Slot which indicates an event that
   * is fired on a Component.
   *
   * A new object should never be directly created with this Constructor. All Slots are
   * created internally by BajaScript.
   *
   * @class
   * @alias FrozenTopic
   * @extends baja.Topic
   */
  const FrozenTopic = function (bson) {
    callSuper(FrozenTopic, this, [ bson.n, bson.dn ]);
    this.$bson = bson;
  };

  subclass(FrozenTopic, baja.Topic);

  FrozenTopic.prototype.isFrozen = FrozenProperty.prototype.isFrozen;

  /**
   * Return the Flags for the Topic.
   *
   * @function
   * @see baja.Flags
   *
   * @returns {Number}
   */
  FrozenTopic.prototype.getFlags = FrozenProperty.prototype.getFlags;

  /**
   * Set the Flags for the Topic.
   *
   * Please note, this method is intended for INTERNAL use by Tridium only. An
   * external developer should never call this method.
   *
   * @function
   * @private
   * @see baja.Flags
   *
   * @param {Number} flags
   */
  FrozenTopic.prototype.$setFlags = FrozenProperty.prototype.$setFlags;

  /**
   * Return the Facets for the Topic.
   *
   * @function
   * @see baja.Facets
   *
   * @returns the Slot Facets
   */
  FrozenTopic.prototype.getFacets = FrozenProperty.prototype.getFacets;

  /**
   * Set the Facets for the Topic.
   *
   * Please note, this method is intended for INTERNAL use by Tridium only. An
   * external developer should never call this method.
   *
   * @function
   * @private
   * @see baja.Facets
   *
   * @param {baja.Facets} facets
   */
  FrozenTopic.prototype.$setFacets = FrozenProperty.prototype.$setFacets;

  /**
   * Return the default flags for the Topic.
   *
   * @returns {Number}
   */
  FrozenTopic.prototype.getDefaultFlags = FrozenProperty.prototype.getDefaultFlags;

  /**
   * Return the event type.
   *
   * @returns {Type|null} the event type (or null if the Topic has no event).
   */
  FrozenTopic.prototype.getEventType = function () {
    if (this.$eventType === undefined) {
      this.$eventType = decodeTopicEventType(this.$bson);
    }
    return this.$eventType;
  };

  ////////////////////////////////////////////////////////////////
  // Auto-generate Slot Methods
  //////////////////////////////////////////////////////////////// 

  function generateSlotMethods(complexType, complex, slot) {
    // Cache auto-generated methods onto Type
    const autoGen = complexType.$autoGen = complexType.$autoGen || {};
    const slotName = slot.getName();
    let methods = autoGen.hasOwnProperty(slotName) ? autoGen[slotName] : null;

    // If the methods already exist then simply copy them over and return
    if (methods) {
      for (let methodName in methods) {
        if (methods.hasOwnProperty(methodName)) {
          complex[methodName] = methods[methodName];
        }
      }
      return;
    }

    autoGen[slotName] = methods = {};

    // Please note: these auto-generated methods should always respect the fact that a Slot can be overridden by
    // sub-Types. Therefore, be aware of using too much closure in these auto-generated methods.

    // Form appropriate name for getters, setters and firers
    const capSlotName = capitalizeFirstLetter(slot.getName());

    if (slot.isProperty()) {

      const origGetterName = "get" + capSlotName;
      let getterName = origGetterName;
      const origGetterDisplayName = origGetterName + "Display";
      let getterDisplayName = origGetterDisplayName;
      const origSetterName = "set" + capSlotName;
      let setterName = origSetterName;

      // Find some unique names for the getter and setter (providing this isn't the icon Slot which we DO want to override)...
      if (capSlotName !== "Icon") {
        let i = 0;
        while (complex[getterName] !== undefined || complex[getterDisplayName] !== undefined || complex[setterName] !== undefined) {
          getterName = origGetterName + (++i);
          getterDisplayName = origGetterDisplayName + i;
          setterName = origSetterName + i;
        }
      }

      // Add Getter
      complex[getterName] = methods[getterName] = function () {
        const v = this.get(slotName);

        // If a number then return its inner boxed value
        return v.getType().isNumber() ? v.valueOf() : v;
      };

      // Add Display String Getter
      complex[getterDisplayName] = methods[getterDisplayName] = function (cx) {
        return this.getDisplay(slotName, cx);
      };

      // Add Setter
      complex[setterName] = methods[setterName] = function (obj) {
        obj = objectify(obj, "value");
        obj.slot = slotName;

        // TODO: Need to check incoming value to ensure it's the same Type!!!
        return this.set(obj);
      };
    }

    let invokeActionName = slotName;
    if (slot.isAction()) {

      // Find a unique name for the Action invocation method
      let i = 0;
      while (complex[invokeActionName] !== undefined) {
        invokeActionName = slotName + (++i);
      }

      complex[invokeActionName] = methods[invokeActionName] = function (obj) {
        obj = objectify(obj, "value");
        obj.slot = slotName;

        return this.invoke(obj);
      };
    }

    if (slot.isTopic()) {

      // Find a unique name for the topic invocation method     
      const origFireTopicName = "fire" + capSlotName;
      let fireTopicName = origFireTopicName;
      let i = 0;
      while (complex[fireTopicName] !== undefined) {
        fireTopicName = origFireTopicName + (++i);
      }

      complex[fireTopicName] = methods[fireTopicName] = function (obj) {
        obj = objectify(obj, "value");
        obj.slot = slotName;

        return this.fire(obj);
      };
    }
  }

  ////////////////////////////////////////////////////////////////
  // Contracts
  //////////////////////////////////////////////////////////////// 

  /**
   * Return an instance of a frozen Slot.
   *
   * @param {Object} bson
   * @param {baja.Complex} [complex]
   *
   * @returns {baja.Slot}
   */
  function createContractSlot(bson, complex) {
    const slotType = bson.st;

    // Create frozen Slot
    switch (slotType) {
      case 'p':
        return new FrozenProperty(bson, complex);
      case 'a':
        return new FrozenAction(bson);
      case 't':
        return new FrozenTopic(bson);
      default:
        throw new Error("Invalid BSON: Cannot decode: " + JSON.stringify(bson));
    }
  }

  /**
   * Return a decoded Contract Slot.
   *
   * @private
   *
   * @param {Type} complexType
   * @param {baja.Complex} complex the Complex to decode the Slots onto
   * @param {Object} bson the BSON to decode
   */
  function decodeContractSlot(complexType, complex, bson) {
    const slot = createContractSlot(bson, complex);
    const slotName = bson.n;

    // Only auto-generate the Slot methods if Slot doesn't already exist.
    // This caters for Slots that are overridden by sub-Types.
    if (!complex.$map.contains(slotName)) {
      // Auto-generate the methods and copy them over to the complex
      generateSlotMethods(complexType, complex, slot);
    }

    // Add to Slot Map
    complex.$map.put(slotName, slot);
  }

  /**
   * Return a decoded array of Slots from a BSON Contract Definition.
   *
   * @private
   *
   * @see baja.Slot
   *
   * @param type the Type.
   * @param {baja.Complex} complex the complex instance the Slots are being loaded for.
   */
  baja.bson.decodeComplexContract = function (type, complex) {
    const clxTypes = [];
    let t = type;

    // Get a list of all the Super types
    while (t && t.isComplex()) {
      clxTypes.push(t);
      t = t.getSuperType();
    }

    // Iterate down through the Super Types and build up the Contract list
    for (let i = clxTypes.length - 1; i >= 0; --i) {
      const bson = clxTypes[i].getContract();

      if (bson) {
        for (let j = 0; j < bson.length; ++j) {
          // Add newly created Slot to array
          decodeContractSlot(type, complex, bson[j]);
        }
      }
    }
  };

  ////////////////////////////////////////////////////////////////
  // BSON Type Scanning
  //////////////////////////////////////////////////////////////// 

  /**
   * Scan a BSON object for type information. Only types known to the core component model will be
   * scanned - this includes Property types, Action/Topic arguments and return values, slot facets,
   * and values encoded into other core baja Simples like Status/Facets/Enums. The discovered types
   * can then be imported before decoding the values from the BSON, so no type information will be
   * missing at runtime.
   *
   * This will *not* scan for non-core Simples that might reference other Types. Type Extensions
   * for Simples can still load their own types when they are instantiated, separate from this
   * process, by implementing `decodeAsync`.
   *
   * @private
   *
   * @param {Object} bson A chunk of BSON to scan.
   * @param {Object} typeSpecs type specs will be appended onto this object as keys.
   */
  baja.bson.scan = function (bson, typeSpecs) {
    if (!bson) {
      return;
    }

    const { nm } = bson;

    // Ensure we're dealing with a Slot, an AddOp or a SetFacetsOp.
    if (nm === "p" || nm === "a" || nm === "t" || nm === "a" || nm === "x") {
      // If we've found a Type then record it
      if (bson.t) {
        typeSpecs[bson.t] = bson.t;
      }

      // Scan any type dependencies for facets
      if (bson.xtd) {
        for (let i = 0; i < bson.xtd.length; ++i) {
          typeSpecs[bson.xtd] = bson.xtd;
        }
      }

      // If this Property specifies some other Type dependencies then pick them up here
      if (nm === "p") {
        if (bson.td) {
          for (let i = 0; i < bson.td.length; ++i) {
            typeSpecs[bson.td[i]] = bson.td[i];
          }
        }
        if (bson.s) {
          for (let i = 0; i < bson.s.length; ++i) {
            baja.bson.scan(bson.s[i], typeSpecs);
          }
        }
      }

      // If an Action then record any parameter or return Type
      if (nm === "a") {
        // Parameter Type
        if (bson.apt) {
          typeSpecs[bson.apt] = bson.apt;
        }

        // Return Type
        if (bson.art) {
          typeSpecs[bson.art] = bson.art;
        }
      }

      // If a Topic then record any event type
      if (nm === "t" && bson.tet) {
        typeSpecs[bson.tet] = bson.tet;
      }
    }

    // Scan for other Slots
    if (bson instanceof Array) {
      for (let i = 0; i < bson.length; ++i) {
        if (bson[i] && (bson[i] instanceof Array || bson[i] instanceof Object)) {
          baja.bson.scan(bson[i], typeSpecs);
        }
      }
    } else if (bson instanceof Object) {
      for (let prop in bson) {
        if (bson.hasOwnProperty(prop)) {
          if (bson[prop] && (bson[prop] instanceof Array || bson[prop] instanceof Object)) {
            baja.bson.scan(bson[prop], typeSpecs);
          }
        }
      }
    }
  };

  /**
   * Scan for Types and Contracts that aren't yet loaded into the BajaScript Registry.
   *
   * @private
   *
   * @param bson  the BSON to scan Types for.
   * @param {Function} ok the ok callback.
   * @param {Function} [fail] the fail callback.
   * @param {baja.comm.Batch} [batch] the optional batch.
   * @returns {Promise}
   */
  baja.bson.importUnknownTypes = function (bson, ok, fail, batch) {

    // Store results in an object as we only want type information added once
    const typeSpecs = {};

    // Scan the data structure for Slot Type information
    baja.bson.scan(bson, typeSpecs);

    return baja.importTypes({
      "typeSpecs": Object.keys(typeSpecs),
      "ok": ok,
      "fail": fail || baja.fail,
      "batch": batch
    });
  };

  const bsonImportUnknownTypes = baja.bson.importUnknownTypes;

  ////////////////////////////////////////////////////////////////
  // BOG BSON Decoding
  ////////////////////////////////////////////////////////////////

  /**
   * Decode and return a Knob.
   *
   * @private
   *
   * @param {Object} bson the BSON that contains knob information to decode
   * @returns {Object} a decoded value (null if unable to decode)
   */
  baja.bson.decodeKnob = function (bson) {
    const targetOrd = baja.Ord.make(bson.to);

    // TODO: Document these methods
    return {
      getId: function getId() {
        return bson.id;
      },

      getSourceComponent: function getSourceComponent() {
        return null;
      },

      getSourceSlotName: function getSourceSlotName() {
        return bson.ss;
      },

      getTargetOrd: function getTargetOrd() {
        return targetOrd;
      },

      getTargetSlotName: function getTargetSlotName() {
        return bson.ts;
      }
    };
  };

  /**
   * Decode and return a Relation Knob.
   *
   * @private
   *
   * @param {Object} bson the BSON that contains relation knob information to decode
   * @returns {Object} a decoded value (null if unable to decode).
   */
  baja.bson.decodeRelationKnob = function (bson) {

    // TODO: Document these methods
    return {
      getId: function getId() {
        return bson.id;
      },

      getRelationId: function getRelationId() {
        return bson.ri;
      },

      getRelationTags: function getRelationTags() {
        return baja.Facets.DEFAULT.decodeFromString(bson.rt, baja.Simple.$unsafeDecode);
      },

      getRelationOrd: function getRelationOrd() {
        return baja.Ord.make(bson.ro);
      }
    };
  };

  /**
   * Return a decoded value.
   *
   * @private
   *
   * @param bson  the BSON to decode.
   * @param {Object} [cx] the context used when decoding.
   * @param {baja.Complex} [parent] the parent Complex that will contain the decoded value. Only to
   * be used internally!
   * @returns {baja.Value|null} a decoded value (null if unable to decode).
   */
  baja.bson.decodeValue = function (bson, cx, parent) {
    const { nm, s: kidEncodings } = bson;
    // TODO: Skip this from LoadOp - needed for loading security permissions at some point!
    if (!isValidSlotElementName(nm)) {
      return null;
    }

    cx = cx || {};

    const slot = getDecodingSlot(bson, parent);

    let value = null;
    if (!isActionOrTopic(slot)) {
      value = decodeValueSync(bson, slot, cx);
    }

    saveToParent(bson, value, parent, slot, cx);

    if (kidEncodings) {
      for (let i = 0; i < kidEncodings.length; ++i) {
        bsonDecodeValue(kidEncodings[i], cx, value);
      }
    }

    return value;
  };

  bsonDecodeValue = baja.bson.decodeValue;

  /**
   * decodeAsync will resolve to the decoded value. Use this method if you are
   * not sure whether all types in the bson have already been imported; it will
   * detect and import all unknown types before it attempts to decode the value.
   *
   * @private
   *
   * @param bson  the BSON to decode.
   * @param {Object} [cx] the context used when decoding.
   * @param {baja.Complex} [parent]
   * @returns {Promise<baja.Value|null>} resolves to a decoded value (null if unable to decode).
   */
  baja.bson.decodeAsync = function (bson, cx, parent) {
    return bsonImportUnknownTypes(bson)
      .then(() => decodeComplexTreeAsync(bson, cx, parent))
      .then(([ value, slot ]) => {
        saveToParent(bson, value, parent, slot, cx);
        return value;
      });
  };

  ////////////////////////////////////////////////////////////////
  // BSON Encoding
  ////////////////////////////////////////////////////////////////

  /**
   * @param {object} bson
   * @param {baja.Property} [prop]
   * @returns {baja.Value} the default instance of the value represented in the BSON
   */
  function instantiateSync(bson, prop) {
    const { t: typeSpec } = bson;

    // TODO: Should be getDefaultValue()?
    return typeSpec === undefined ? prop.$getValue() : baja.$(typeSpec);
  }

  /**
   * If decoding either a standalone BSON blob, or the default value of a frozen Property, we have
   * to start with the default instance. Since frozen Properties can have async-decoded default
   * values (such as a Facets with an un-imported FrozenEnum range), we have to go down an async
   * path to get the default value in both cases.
   *
   * Please note that this does _not_ add support for async decoding of default values when calling
   * baja.$()!
   *
   * @param {object} bson a BSON blob from the browser
   * @param [slot] if decoding a frozen Slot
   * @returns {Promise<baja.Value>}
   */
  function instantiateAsync(bson, slot) {
    const { t: typeSpec } = bson;
    const isFrozenProperty = typeSpec === undefined;

    // we start with the default instance, no async decoding yet.
    const instance = isFrozenProperty ? slot.$getValue() : baja.$(typeSpec);

    // next, ensure that any frozen Simples have a chance to decode async.
    return decodeFrozenSimpleDefaults(instance)
      .then(() => {

        // this BSON may be for a Complex, and it may be either standalone or a frozen Complex Property.
        // each slot now needs a chance to decode.
        // if I'm a standalone value sent from the station, these frozen slot definitions are provided in the BSON itself.
        // if I'm a frozen slot, these frozen slot definitions come from my parent Complex.
        const { s: slotEncodings } = isFrozenProperty ? slot.$bson.v : bson;

        if (!slotEncodings) {
          return instance;
        } else {
          return Promise.all(slotEncodings.map((slotBson, i) => {
            const slot = getDecodingSlot(slotBson, instance);
            if (!isActionOrTopic(slot)) {
              return decodeValueAsync(slotBson, slot)
                .then((value) => [ value, slot ]);
            } else {
              return [ null, slot ];
            }
          }))
            .then((results) => {
              results.forEach(([ kidValue, kidSlot ], i) => {
                const slotBson = slotEncodings[i];
                saveToParent(slotBson, kidValue, instance, kidSlot, serverDecodeContext);
              });
              return instance;
            });
        }
      });
  }

  /**
   * @param {object} bson a BSON blob from the station
   * @param {baja.Property} [prop] if decoding a frozen Property
   * @returns {baja.Value} the instance of the value as encoded to string in the BSON. If a Complex,
   * there is no string encoding, this is just the default instance of that Complex type.
   */
  function decodeValueSync(bson, prop, cx) {
    const { v: valueEncoding } = bson;
    let obj = instantiateSync(bson, prop);

    if (obj.getType().isSimple() && valueEncoding !== undefined) {
      obj = obj.decodeFromString(valueEncoding, baja.Simple.$unsafeDecode);
    }

    applyMetadataFromBson(bson, obj, cx);

    return obj;
  }

  /**
   * @param {object} bson a BSON blob from the station.
   * @param {baja.Property} [prop] provided if decoding a Property on a Complex
   * @param {object} cx
   * @returns {Promise.<baja.Value>} the instance of the value as encoded to string in the BSON. If a Complex,
   * there is no string encoding, this is just the default instance of that Complex type.
   */
  function decodeValueAsync(bson, prop, cx) {
    const { v: valueEncoding } = bson;
    return instantiateAsync(bson, prop)
      .then((defaultInstance) => {
        /*
        valueEncoding will be undefined for a Complex
         */
        if (defaultInstance.getType().isSimple() && valueEncoding !== undefined) {
          return defaultInstance.decodeAsync(valueEncoding, baja.Simple.$unsafeDecode);
        } else {
          return defaultInstance;
        }
      })
      .then((value) => {
        applyMetadataFromBson(bson, value, cx);
        return value;
      });
  }

  /**
   * Decodes a complete tree of BSON, giving any async Simples a chance to decode asynchronously.
   * It is expected that unknown types have already been imported before this function is called.
   *
   * @param {object} bson BSON encoding of a value, and if a Complex, its child slots
   * @param {object} cx context
   * @param {baja.Complex} [parent] parent complex that will receive the decoded value - this
   * function itself will **not** save the decoded value onto this complex
   * @returns {Array|Promise.<Array>} resolves to the decoded value (or null if nothing to decode),
   * and if it is to be saved to the parent complex, the slot to which it should be saved
   */
  function decodeComplexTreeAsync(bson, cx, parent) {
    const { nm } = bson;
    // TODO: Skip this from LoadOp - needed for loading security permissions at some point!
    if (!isValidSlotElementName(nm)) {
      return [ null ];
    }

    cx = cx || {};

    const slot = getDecodingSlot(bson, parent);

    if (isActionOrTopic(slot)) {
      return [ null ];
    }

    return decodeValueAsync(bson, slot, cx)
      .then((value) => [ value, slot ]);
  }

  /**
   * If a frozen slot needs to decode asynchronously, it will cause problems later because frozen
   * properties otherwise get synchronously, lazily decoded on demand (see
   * FrozenProperty#$getValue). We will look for Simples that need to decode asynchronously and
   * decode them now, so `complex.get('frozenSlotThatNeedsToDecodeAsync')` will succeed later.
   * @param {baja.Value} value
   * @returns {Promise}
   */
  function decodeFrozenSimpleDefaults(value) {
    if (!value.getType().isComplex()) {
      return Promise.resolve();
    }

    const frozenSimpleProperties = value.getSlots()
      .filter((slot) => slot.isProperty() && slot.isFrozen() && slot.getType().isSimple())
      .toArray();

    return Promise.all(frozenSimpleProperties.map((prop) => {

      const ctor = prop.getType().findCtor();

      if (ctor === String || ctor === Boolean || ctor === Number) {
        return;
      }

      if (ctor.prototype.decodeAsync === defaultDecodeAsync) {
        return;
      }

      return baja.bson.decodeAsync(prop.$bson.v)
        .then((val) => prop.$setValue(val));
    }));
  }

  /**
   * @param {object} bson
   * @param {baja.Complex} [parent]
   * @returns {baja.Slot|null} the slot in the parent Complex this BSON will get decoded into, if
   * applicable
   */
  function getDecodingSlot(bson, parent) {
    const { dn: slotDisplayName, n: slotName, nm: slotType } = bson;
    const slot = parent && parent.getSlot(slotName);

    if (slot) {
      if (slotDisplayName !== undefined) {
        slot.$setDisplayName(slotDisplayName);
      }
    } else {
      if (slotType !== "p") {
        throw new Error("Error decoding Slot from BSON: Missing frozen Slot: " + slotType);
      }
    }

    return slot;
  }

  function isActionOrTopic(slot) {
    return slot && !slot.isProperty();
  }

  function isValidSlotElementName(nm) {
    return nm === 'p' || nm === 'a' || nm === 't';
  }

  /**
   * Finishes applying any additional data encoded in the BSON to the value instance we are decoding
   * in the browser.
   *
   * @param {object} bson
   * @param {baja.Value} value
   * @param {object} cx
   */
  function applyMetadataFromBson(bson, value, cx) {
    const {
      d: displayValue,
      h: handle,
      l: loadInfoEncoding,
      nc: isNavChild,
      nk: knobEncodings,
      nrk: relationKnobEncodings,
      stub
    } = bson;

    const type = value.getType();

    if (displayValue !== undefined && value instanceof baja.DefaultSimple) {
      value.$displayValue = displayValue;
    }

    // Decode BSON specifically for baja:Action and baja:Topic
    if (type.isAction()) {
      value.$paramType = decodeActionParamType(bson);
      value.$paramDef = decodeActionParamDefault(bson);
      value.$returnType = decodeActionReturnType(bson);
    } else if (type.isTopic()) {
      value.$eventType = decodeTopicEventType(bson);
    }

    // Decode Component
    if (type.isComponent()) {
      if (handle !== undefined) {
        value.$handle = handle;
      }

      if (isNavChild !== undefined) {
        value.$nc = isNavChild === "true";
      }

      if (knobEncodings) {
        for (let i = 0; i < knobEncodings.length; ++i) {
          value.$fw("installKnob", baja.bson.decodeKnob(knobEncodings[i]), cx);
        }
      }

      if (relationKnobEncodings) {
        for (let i = 0; i < relationKnobEncodings.length; ++i) {
          value.$fw("installRelationKnob", baja.bson.decodeRelationKnob(relationKnobEncodings[i]), cx);
        }
      }

      if (loadInfoEncoding) {
        const { p: permissionsEncoding } = loadInfoEncoding;
        if (typeof permissionsEncoding === "string") {
          value.$fw("setPermissions", permissionsEncoding);
        }
      }

      // TODO: Handle Component Stub decoding here
      if (!stub) {
        value.$bPropsLoaded = true;
      }
    }
  }

  /**
   * Saves the bson and decoded value (if provided) to the parent Complex. This runs synchronously and will not make
   * network calls.
   *
   * @param {object} bson
   * @param {baja.Value} [value]
   * @param {baja.Complex} [parent]
   * @param {baja.Slot} [slot]
   * @param {object} cx
   */
  function saveToParent(bson, value, parent, slot, cx) {
    if (!parent) {
      return;
    }

    const { d: displayString, dn: displayName, f: flagEncoding } = bson;
    const hasFlags = flagEncoding !== undefined;
    const flags = hasFlags && decodeFlags(bson);
    const valueProvided = value !== null && value !== undefined;

    try {
      if (displayName !== undefined) {
        cx.displayName = displayName;
      }

      if (displayString !== undefined) {
        cx.display = displayString;
      }

      if (slot) {
        if (hasFlags && flags !== slot.getFlags()) { 
          parent.setFlags({ slot, flags, cx });
        }
        
        if (valueProvided) {
          parent.set({ slot, value: value, cx });
        }
      } else if (parent.getType().isComponent()) {
        const { n: slotName, x: facetsEncoding } = bson;
        const facets = baja.Facets.DEFAULT.decodeFromString(bajaDef(facetsEncoding, ""), baja.Simple.$unsafeDecode);
        const flags = decodeFlags(bson);
        if (valueProvided) {
          parent.add({ slot: slotName, value: value, flags, facets, cx });
        }
      }
    } finally {
      cx.displayName = undefined;
      cx.display = undefined;
    }
  }

  /**
   * From a bson object, derive the slot flags as a number. If the flags were
   * incorrectly encoded as a number `bson.flags`, use that - otherwise use the
   * correct method of decoding `bson.f` from string. See NCCB-25981.
   * @param {object} bson
   * @returns {number} slot flags, or 0 if not present
   */
  function decodeFlags(bson) {
    const flags = bson.flags;
    if (typeof flags === 'number') { return flags; }
    return baja.Flags.decodeFromString(bajaDef(bson.f, "0"));
  }

  function encodeSlot(parObj, par, slot) {
    if (slot === null) {
      return;
    }

    // Encode Slot Flags (if they differ from the default    
    let value = null; // Property Value
    let skipv = false;    // Skip value

    if (slot.isProperty()) {
      value = par.get(slot);
      if (slot.isFrozen()) {
        if (value.equivalent(slot.getDefaultValue())) {
          skipv = true;
        }
      }
    } else {
      skipv = true;
    }

    const flags = par.getFlags(slot);

    // Skip frozen Slots that have default flags and value
    if (flags === slot.getDefaultFlags() && skipv) {
      return;
    }

    // Encode Slot Type
    const o = {};
    if (slot.isProperty()) {
      o.nm = "p";
    } else if (slot.isAction()) {
      o.nm = "a";
    } else if (slot.isTopic()) {
      o.nm = "t";
    }

    // Slot name
    o.n = slot.getName();

    // Slot Flags if necessary
    if (((!slot.isFrozen() && flags !== 0) || (flags !== slot.getDefaultFlags())) && par.getType().isComponent()) {
      o.f = baja.Flags.encodeToString(flags);
    }

    // Slot facets if necessary
    const fc = slot.getFacets();
    if (!slot.isFrozen() && fc.getKeys().length > 0) {
      o.x = fc.encodeToString();
    }

    if (value !== null && value.getType().isComponent()) {
      // Encode handle
      if (value.isMounted()) {
        o.h = value.getHandle();
      }

      // TODO: Categories and stub?
    }
    // TODO: Need to re-evalulate this method by going through BogEncoder.encodeSlot again

    if (!skipv && slot.isProperty()) {
      o.t = value.getType().getTypeSpec();
      encodeVal(o, value);
    }

    // Now we've encoded the Slot, add it to the Slots array
    if (!parObj.s) {
      parObj.s = [];
    }

    parObj.s.push(o);
  }

  function encodeVal(obj, val) {

    const valueType = val.getType();
    if (valueType.isSimple()) {
      const defaultInstance = valueType.getInstance();
      //Throw if blacklisted (see BlacklistedTypes) and trying to set a different value
      if (valueType.$isBlackListed && !val.equals(defaultInstance)) {
        throw new Error("Cannot add blacklisted types to a component");
      }

      if (valueType.$isSensitive && !baja.bson.$canEncodeSecurely()) {
        throw new Error(lexjs.$getSync({
          module: 'baja',
          key: 'password.encoder.secureEnvironmentRequired',
          def: 'Cannot encode sensitive values to string in an insecure environment.'
        }));
      }

      // only send string encoding if not the DEFAULT instance
      if (val !== defaultInstance) {
        // Encode Simple
        obj.v = val.encodeToString();
      }
    } else {
      // Encode Complex
      const cursor = val.getSlots();

      // Encode all the Slots on the Complex
      while (cursor.next()) {
        encodeSlot(obj, val, cursor.get());
      }
    }
  }

  function encode(name, val) {
    const o = { nm: "p" };

    // Encode name
    if (name !== null) {
      o.n = name;
    }

    if (val.getType().isComponent()) {

      // Encode handle
      if (val.isMounted()) {
        o.h = val.getHandle();
      }

      // TODO: Encode categories

      // TODO: Encode whether this Component is fully loaded or not???
    }

    o.t = val.getType().getTypeSpec();
    encodeVal(o, val);
    return o;
  }

  /**
   * Return an encoded BSON value.
   *
   * @private
   *
   * @param val  the value to encode to BSON.
   * @returns encoded BSON value.
   */
  baja.bson.encodeValue = function (val) {
    return encode(null, val);
  };

  /**
   * @private
   * @returns {boolean}
   */
  baja.bson.$canEncodeSecurely = function () {
    return baja.comm.$isSecure() || baja.isOffline() || !baja.$requireHttps;
  };

  return baja;
});