baja/ord/Ord.js

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

/*global niagara:false*/

/**
 * Defines {@link baja.Ord}.
 * @module baja/ord/Ord
 */
define([
  "bajaPromises",
  "bajaScript/sys",
  "bajaScript/baja/obj/Simple",
  "bajaScript/baja/ord/OrdTarget",
  "bajaScript/baja/comm/Callback" ], function (
  Promise,
  baja,
  Simple,
  OrdTarget,
  Callback) {
  
  "use strict";
  
  var subclass = baja.subclass,
      callSuper = baja.callSuper,
      objectify = baja.objectify,
      strictArg = baja.strictArg,
      bajaDef = baja.def;

  var VARIABLE_REGEX = "\\$\\(([^)]*)\\)?";

  /**
   * NCCB-27229
   *
   * When resolving a relativized Ord the default base should be the current
   * session. Most of the time this is localhost, but in Workbench it could be
   * any session (e.g. platform:).
   *
   * getSessionOrd() is injected by BWebWidget.
   */
  function getDefaultBaseOrd() {
    var sessionOrd;
    if (baja.isOffline() &&
      typeof niagara !== 'undefined' &&
      niagara.env &&
      typeof niagara.env.getSessionOrd === 'function') {
      sessionOrd = niagara.env.getSessionOrd();
    }
    if (!sessionOrd) {
      sessionOrd = 'local:';
    }
    return baja.Ord.make(sessionOrd);
  }
  
  /**
   * Object Resolution Descriptor.
   * 
   * An ORD is how we can access Objects in the Server from BajaScript. It's 
   * similar to a URI but is much more powerful and extensible. For more
   * information, please see the Niagara developer documentation on ORDs and how
   * they're used.
   * 
   * If more than one ORD needs to be resolved then use a {@link baja.BatchResolve}.
   * 
   * This Constructor shouldn't be invoked directly. Please use the `make()` 
   * methods to create an instance of an ORD.
   *
   * @see baja.Ord.make
   * @see baja.BatchResolve
   *
   * @class
   * @alias baja.Ord
   * @extends baja.Simple
   * 
   * @example
   *   <caption>Resolve an ORD</caption>
   *   baja.Ord.make("station:|slot:/Folder/NumericWritable").get({ lease: true })
   *     .then(function (numericWritable) {
   *       baja.outln(numericWritable.getOutDisplay());
   *     });
   */
  var Ord = function Ord(ord) {
    callSuper(Ord, this, arguments);
    this.$ord = strictArg(ord, String);    
  };
  
  subclass(Ord, Simple);
  
  /**
   * Default ORD instance.
   * @type {baja.Ord}
   */
  Ord.DEFAULT = new Ord("null");
    
  /**
   * Make an ORD.
   * 
   * The argument can be a `String`, `Ord` or an `Object`.
   * 
   * If an `Object` is passed in then if there's a `base` and `child` property, 
   * this will be used to construct the ORD. (If only `base` is present, and
   * `child` is omitted or null, then just the `base` will be used.)
   * Otherwise `toString` will be called on the `Object` for the ORD.
   *
   * @param {String|baja.Ord|Object} ord
   * @returns {baja.Ord}
   * 
   * @example
   *   <caption>Resolve an ORD</caption>
   *   baja.Ord.make("station:|slot:/Folder/NumericWritable").get({ lease: true })
   *     .then(function (numericWritable) {
   *       baja.outln(numericWritable.getOutDisplay());
   *     });
   */
  Ord.make = function (ord) {
    if (arguments.length === 0) {
      return Ord.DEFAULT;
    }
    
    var ordString;
    
    // Handle child and base
    if (typeof ord === "object" && ord.base) {
      var base = ord.base;
      var child = ord.child;
      ordString = String(base);
      if (child && !baja.Ord.make(child).isNull()) {
        ordString += '|' + child;
      }
    } else {
      ordString = ord.toString(); 
    }  
        
    // Handle URL decoding
    if (ordString.match(/^\/ord/)) {
      // Remove '/ord?' or '/ord/'
      ordString = ordString.substring(5, ordString.length);
      
      // Replace this with the pipe character
      ordString = decodeURIComponent(ordString);
    }
    
    if (ordString === "" || ordString === "null") {
      return Ord.DEFAULT;
    }
    
    return new Ord(ordString);
  };
  
  /**
   * Make an ORD.
   *
   * @see baja.Ord.make
   *
   * @param {String|baja.Ord|Object} ord
   * @returns {baja.Ord}
   */
  Ord.prototype.make = function (ord) {
    return Ord.make(ord);
  };
  
  /**
   * Decode an ORD from a `String`.
   *
   * @param {String} str  the ORD String.
   * @returns {baja.Ord} the decoded ORD.
   */
  Ord.prototype.decodeFromString = function (str) {
    return Ord.make(str);
  };
  
  /**
   * Encode an ORD to a `String`.
   *
   * @returns {String} the ORD encoded to a String.
   */
  Ord.prototype.encodeToString = function () {
    return this.$ord;
  };

  /**
   * @returns {boolean} if this represents the null ORD.
   * @since Niagara 4.10
   */
  Ord.prototype.isNull = function () {
    return this === Ord.DEFAULT;
  };
      
  /**
   * Return an `String` representation of the object.
   *
   * @returns {String} a String representation of an ORD.
   */
  Ord.prototype.toString = function () {
    return this.$ord;
  };
  
  /**
   * Return the inner value of this `Object`.
   *
   * @returns {String} a String representation of an ORD.
   */
  Ord.prototype.valueOf = function () {
    return this.toString();
  };
      
  /**
   * Parse an ORD to a number of ORD Query objects.
   *
   * @returns {baja.OrdQueryList} a list of ORDs to resolve.
   */
  Ord.prototype.parse = function () {
    // TODO: Validate all characters are valid
    var os = this.$ord.split("|"), // ORDs
        list = new baja.OrdQueryList(),
        i,
        ind,
        schemeName,
        scheme,
        body;
    
    if (this.$ord === "null") {
      return list; 
    }

    for (i = 0; i < os.length; ++i) {      
      ind = os[i].indexOf(":");
      if (ind === -1) {
        throw new Error("Unable to parse ORD: " + os[i]);
      }
        
      schemeName = os[i].substring(0, ind);
      body = os[i].substring(ind + 1, os[i].length);      
      scheme = baja.OrdScheme.lookup(schemeName);
            
      // Create the ORD scheme      
      list.add(scheme.parse(schemeName, body));
    }
    return list;
  };
     
  /**
   * Resolve an ORD.
   * 
   * Resolving an ORD consists of parsing and processing it to get a result. 
   * The result is an ORD Target.
   * 
   * Any network calls that result from processing an ORD are always 
   * asynchronous.
   * 
   * The `resolve` method requires an `ok` function callback or an object 
   * literal that contains the method's arguments.
   * 
   * Please note that unlike other methods that require network calls, no 
   * batch object can be specified!   
   *
   * @see module:baja/ord/OrdTarget
   * @see baja.Ord#get
   * @see baja.RelTime
   *
   * @param {Object} [obj] the object literal that contains the method's 
   * arguments.
   * @param {Function} [obj.ok] (Deprecated: use Promise) the ok function called
   * once the ORD has been successfully resolved. The ORD Target is passed to
   * this function when invoked.
   * @param {Function} [obj.fail] (Deprecated: use Promise) the fail function
   * called if the ORD fails to resolve. An error cause is passed to this
   * function when invoked.
   * @param [obj.base] the base Object to resolve the ORD against.
   * @param {Boolean} [obj.lease] if defined and true, any Components are 
   * temporarily subscribed.
   * @param {Number|baja.RelTime} [obj.leaseTime] the amount of time in 
   * milliseconds to lease for (`lease` argument must be true). As well as a 
   * Number, this can also be a {@link baja.RelTime}. If undefined, BajaScript's
   * default lease time will be used.
   * @param {baja.Subscriber} [obj.subscriber] if defined the `Component` is 
   * subscribed using this `Subscriber`.
   * @param {Object} [obj.cursor] if defined, this specifies parameters for 
   * iterating through a Cursor (providing the ORD resolves to a Collection 
   * or Table). For more information, please see 
   * {@link baja.coll.tableMixIn.cursor}.
   * @returns {Promise.<Object>} a promise that will be resolved with an
   * OrdTarget when the ORD has been resolved.
   * 
   * @example
   *   <caption>Resolve an ORD</caption>
   *   baja.Ord.make("station:|slot:/").resolve({
   *     lease: true // ensure any resolved Components are leased
   *   })
   *     .then(function (target) {
   *       // process the ORD Target
   *     })
   *     .catch(function (err) {
   *       // ORD failed to resolve
   *     });
   */
  Ord.prototype.resolve = function (obj) {
    var inpObj = obj;
    obj = objectify(obj, 'ok');

    const that = this,
        inpBase = obj.base,
        baseIsNavNode = baja.hasType(inpBase, 'baja:INavNode'),
        baseOrd = baseIsNavNode ? inpBase.getNavOrd() : getDefaultBaseOrd(),
        base = bajaDef(inpBase, baja.nav.localhost),
        cb = obj.cb === undefined ? new Callback(obj.ok, obj.fail) : obj.cb,
        subscriber = bajaDef(obj.subscriber, null),
        lease = bajaDef(obj.lease, false), 
        leaseTime = obj.leaseTime,
        full = bajaDef(obj.full, false),
        cursor = obj.cursor;

    if (inpBase && !baseIsNavNode) {
      cb.fail(new Error('Base must be a NavNode'));
      return cb.promise();
    }

    if (typeof inpObj === "function" || arguments.length === 0) {
      obj.lease = true;
    }   

    // Ensure 'this' in callback is the target's Component. If it's not a Component then 
    // fallback to the resolved Object.
    cb.addOk(function (ok, fail, target) {
      const resolvedObj = target.getComponent() || target.getObject();
      ok.call(resolvedObj, target);
    });
     
    if (subscriber !== null) {
       // If we need to subscribe using a Subscriber once the Component is resolved...
      cb.addOk((ok, fail, target) => subscribeTarget(target, subscriber).then(() => ok(target), fail));
    }

    if (lease) {
      // If we need to lease once the Component is resolved...
      cb.addOk((ok, fail, target) => leaseTarget(target, leaseTime).then(() => ok(target), fail));
    }

    try {            
      // Check the user isn't trying to batch an ORD as this isn't supported
      if (obj.batch) {
        return failCallback(cb, "Cannot batch ORD resolution");
      }
    
      const ordQueries = that.parse();
      if (ordQueries.isEmpty()) {
        return failCallback(cb, "Cannot resolve null ORD: " + that.toString());
      }

      const canResolveLocally = ordQueries.isClientResolvable() && !obj.forceServerResolve;

      const target = new OrdTarget();
      target.object = base;
      target.ord = that;
      
      const options = {
        full: full,
        callback: cb,
        queries: ordQueries,
        ord: that,
        cursor: cursor,
        sp: obj.$sp
      };
      
      // Normalize using the Context parameter that indicates it's resolution time
      ordQueries.normalize(baja.OrdQuery.RESOLVING_ORD_CX);

      if (canResolveLocally) {
        // Resolve the ORD locally. Each ORD scheme must call 'resolveNext' on the cursor to process the next
        // part of the ORD. This design has been chosen because some ORD schemes may need to make network calls.
        // If the network call is asynchronous in nature then there will be a delay before the ORD can process further
        ordQueries.getCursor().resolveNext(target, options);
      } else {
        // If there are ORD Schemes that aren't implemented in BajaScript then we
        // simply make a network call and resolve the ORD Server side
        const newTarget = new OrdTarget(target);

        cb.addOk(function (ok, fail, responseFromOrdChannel) {
          const {
            c: cursorResultEncodings,
            f: facetsEncoding,
            o: resolutionResultEncoding,
            sp: ordChannelRedirectedToLocalOrd,
            p: permissions
          } = responseFromOrdChannel;

          const { 
            v: encodedPermissions,
            cr: canRead,
            cw: canWrite,
            ci: canInvoke
           } = permissions || {};
          newTarget.$serverPermissionsStr = encodedPermissions;
          newTarget.$canRead = !!canRead;
          newTarget.$canWrite = !!canWrite;
          newTarget.$canInvoke = !!canInvoke;

          if (!resolutionResultEncoding) {
            //NCCB-55298
            const isClientUnableToResolve = obj.$sp === false;
            if (isClientUnableToResolve) {
              return failCallback(cb, new Error("Server instructed us to resolve this ORD in the client, but we can't"));
            }
          }

          // Decode the result
          Promise.all([
            resolutionResultEncoding && baja.bson.decodeAsync(resolutionResultEncoding, baja.$serverDecodeContext)
              .then((value) => {
                return Promise.resolve(cursorResultEncodings && canCursor(value) && doCursor(value, cursor, cursorResultEncodings))
                  .then(() => value);
              }),
            facetsEncoding && baja.Facets.DEFAULT.decodeAsync(facetsEncoding)
          ])
            .then(([ valueResolvedFromStation, serverFacets ]) => {
              newTarget.object = valueResolvedFromStation;
              newTarget.$serverFacets = serverFacets;

              // since Niagara 4.10
              // All requests to the OrdChannel will get a SlotPath and the space as
              // response if the target is already mounted. Resolving the SlotPath
              // will get us the expected mounted component thus reducing any additional
              // network calls to get the target objects with all slots loaded.
              // If the resolved object and the component are not the same we ensure both
              // getComponent and getObject resolve to the expected values.
              if (ordChannelRedirectedToLocalOrd) {
                return determineLocalRedirectOrd(responseFromOrdChannel, that)
                  .then((localRedirectOrd) => {
                    function localTargetResolved(localTarget) {
                      /*
                      at first glance this looks wasteful because we're resolving a value *plus* doing
                      a redirect to a local component. but ORDs also resolve to non-components like
                      Strings. so if we do a server resolve to, say, a String slot of a component we
                      look up via neql, the server can respond with "here's the string you asked for,
                      but it lives on a mounted component you can actually find over *here* in your
                      component space."
                       */
                      if (valueResolvedFromStation !== undefined) {
                        localTarget = new OrdTarget(localTarget);
                        localTarget.object = valueResolvedFromStation;
                      }

                      if (serverFacets) {
                        localTarget.$serverFacets = serverFacets;
                      }
                      ok(localTarget);
                    }

                    localRedirectOrd.resolve({
                      ok: localTargetResolved, fail, lease, leaseTime, subscriber, full, $sp: false
                    });
                  });
              } else {
                // Finished iterating so just make the callback
                ok(newTarget);
              }
            }, fail);
        });
        
        // If Cursor information is defined, ensure we set some defaults
        if (cursor) {
          cursor.limit = cursor.limit || 10;
          cursor.offset = cursor.offset || 0;
        }
        
        // Make the network call to resolve the complete ORD Server side       
        baja.comm.resolve(that, baseOrd, cb, options);
      }
    } catch (err) {
      return failCallback(cb, err);
    }
    return cb.promise();
  };
     
  /**
   * Resolve the ORD and get the resolved Object from the ORD Target.
   * 
   * This method calls {@link baja.Ord#resolve} and calls `get` on the ORD 
   * Target to pass the object onto the `ok` function callback.
   * 
   * For more information on how to use this method please see 
   * {@link baja.Ord#resolve}.
   * 
   * @see module:baja/ord/OrdTarget#resolve
   *
   * @param {Object} [obj] if no obj provided, this will call baja.Ord#resolve
   * with lease true.
   * @returns {Promise} a promise that will be resolved with the value specified
   * by the ORD.
   * @example
   *   <caption>Resolve/get an ORD</caption>
   *   baja.Ord.make("service:baja:UserService|slot:jack").get({ lease: true })
   *     .then(function (user) {
   *       baja.outln(user.get('fullName'));
   *     })
   *     .catch(function (err) {
   *       baja.error('ORD failed to resolve: ' + err);
   *     });
   */
  Ord.prototype.get = function (obj) {
    var oldObj = obj;
    obj = objectify(obj, "ok"); 
    if (typeof oldObj === "function" || arguments.length === 0) {
      obj.lease = true;
    }      
       
    obj.cb = new Callback(obj.ok, obj.fail);
    obj.cb.addOk(function (ok, fail, target) {
      ok.call(this, target.getObject());
    }); 
  
    this.resolve(obj);
    
    return obj.cb.promise();
  };
  
  /**
   * Return a normalized version of the ORD.
   *
   * @returns {baja.Ord}
   */   
  Ord.prototype.normalize = function () {
    return Ord.make(this.parse().normalize());
  };
    
  /**
   * Relativize is used to extract the relative portion
   * of this ord within an session:
   * 
   * 1. First the ord is normalized.
   * 2. Starting from the left to right, if any queries are
   *    found which return true for `isSession()`, then remove
   *    everything from that query to the left.
   * 
   * @see baja.OrdQuery#isSession
   * @returns {baja.Ord}
   */
  Ord.prototype.relativizeToSession = function () {
    var list = this.parse().normalize(),
        newList = new baja.OrdQueryList();
        
    for (var i = 0, len = list.size(); i < len; ++i) {
      var q = list.get(i);
      
      if (!q.isSession() && !q.isHost()) {
        newList.add(q);
      }
    } 
    return Ord.make(newList);
  };

  /**
   * Slot and file path ord queries may contain "../" to do relative traversal up the tree.  If
   * there is more than one backup, the ord will contain "/../", which will be replaced by the
   * browser within a URL by removing other sections.  For example, https://127.0.0.1/a/b/c/d/../e/f
   * is converted to https://127.0.0.1/a/b/c/e/f and https://127.0.0.1/a/b/c/d/../../e/f is
   * converted to https://127.0.0.1/a/b/c/f.  This will result in unintended behavior in subsequent
   * ord resolution with that URL.  Therefore, all but the last "../" is replaced with {@code
   * "<schema>:..|"}.  This function replicates the behavior of BOrdUtil#replaceBackups.
   *
   * @param {baja.Ord} ord ord that is searched for "../" backups
   * @returns {baja.Ord} the original ord if no changes are necessary or an updated ord with the
   * necessary replacements
   * @since Niagara 4.3U1
   */
  Ord.replaceBackups = function (ord) {
    var queries = ord.parse();
    var newQueries = new baja.OrdQueryList();
    var remakeOrd = false;
    for (var i = 0; i < queries.size(); ++i) {
      var query = queries.get(i);

      // In BajaScript, only the slot and virtual schemes have a backup depth.  The backup depth is
      // only a problem if greater than one because then the ord contains one or more "/../".
      if (query.getScheme() instanceof baja.SlotScheme && query.getBackupDepth() > 1) {
        remakeOrd = true;

        // Replace all but one backup with a new slot path with ".." as the body
        for (var j = 0; j < query.getBackupDepth() - 1; ++j) {
          newQueries.add(query.makeSlotPath('..'));
        }

        // Remove all the "/.." from the body of the original OrdQuery.  For example,
        // slot:../../../abc/def becomes slot:../abc/def
        var newBody = query.getBody().replace(/\/\.\./g, '');
        newQueries.add(query.makeSlotPath(newBody));
      } else {
        newQueries.add(query);
      }
    }

    if (remakeOrd) {
      ord = baja.Ord.make(newQueries);
    }

    return ord;
  };
  
  /**
   * Return the ORD as a URI that can be used in a browser.
   *
   * @returns {String}
   */   
  Ord.prototype.toUri = function () {
    // Handle whether there is a hyperlink to another Station by guessing 
    // what's available from fox. For example...
    // ip:{ipAddress}|:fox{s}|station:|slot:/ -> http{s}://{ipAddress}/ord/station:%7Cslot:/
    var ord = this.normalize(),
        uri = String(ord),
        res = /^ip:([^|]+)\|fox(s|wss)?:.*/.exec(uri),
        prefix = res ? ("http" + (res[2] ? "s" : "") + "://" + res[1]) : "";

    // If the ORD isn't already an HTTP(S) ORD then process it.
    if (!uri.match(/^http/i)) {
      ord = this.relativizeToSession();
      ord = Ord.replaceBackups(ord);
      uri = encodeURI(String(ord)).replaceAll(/[#;]/g, (match) => encodeURIComponent(match));
      uri = "/ord/" + uri;
    }

    return prefix + uri;
  };

  /**
   * Substitute all variables in the ORD from the given variable map.
   * @param {baja.Facets|Object} variables a Facets or object literal containing
   * variable names and their values
   * @returns {baja.Ord} an ORD with the variables substituted in
   * @throws {Error} if a variable name is invalid or empty, or if a variable
   * declaration is malformed
   * @since Niagara 4.10
   */
  Ord.prototype.substitute = function (variables) {
    if (!baja.hasType(variables, 'baja:Facets')) {
      variables = baja.Facets.make(variables || {});
    }
    return baja.Ord.make(String(this).replace(new RegExp(VARIABLE_REGEX, 'g'), function (match, key) {
      validateVariableMatch(match, key);
      return variables.get(key, match);
    }));
  };

  /**
   * @returns {boolean} true if this ORD has any variables present
   * @throws {Error} if a variable name is invalid or empty, or if a variable
   * declaration is malformed
   * @since Niagara 4.10
   */
  Ord.prototype.hasVariables = function () {
    var match = String(this).match(VARIABLE_REGEX);
    if (match) {
      validateVariableMatch(match[0], match[1]);
      return true;
    } else {
      return false;
    }
  };

  /**
   * @returns {string[]} an array of all variable names present in this ORD
   * @throws {Error} if a variable name is invalid or empty, or if a variable
   * declaration is malformed
   * @since Niagara 4.10
   */
  Ord.prototype.getVariables = function () {
    var str = String(this);
    var variables = [];

    var match;
    var regex = new RegExp(VARIABLE_REGEX, 'g');
    while ((match = regex.exec(str))) {
      validateVariableMatch(match[0], match[1]);
      variables.push(match[1]);
    }
    return variables;
  };

  /**
   * @param {string} match the whole regex match, like `$(foo)`
   * @param {string} name the variable name, like `foo`
   */
  function validateVariableMatch(match, name) {
    if (match[match.length - 1] !== ')') {
      throw new Error('Missing closing paren');
    }

    if (!name) {
      throw new Error('Empty variable name');
    }

    var illegalChar = name.match(/[^A-Za-z0-9]/);
    if (illegalChar) {
      throw new Error('Illegal character in variable name: \'' + illegalChar[0] + '\'');
    }
  }

  /**
   * Return the data type symbol.
   *
   * @returns {String} the Symbol used for encoding this data type (primarily 
   * used for facets).
   */
  Ord.prototype.getDataTypeSymbol = function () {
    return "o";
  };

  function failCallback(cb, err) {
    if (typeof err === 'string') { err = new Error(err); }
    cb.fail(err);
    return cb.promise();
  }

  /**
   * If we request an ORD resolution from the OrdChannel, it can sometimes determine that the result
   * is a component that should be resolved in a local component space, and send a response that
   * just redirects us to a local ORD. Figure out that redirect ORD and verify that we may, in fact,
   * resolve it locally.
   *
   * @param {object} responseFromOrdChannel
   * @param {baja.Ord} originalOrd if we can't resolve it locally, something went wrong - but we'll
   * send this same original ORD back up to the OrdChannel for re-resolution, with a flag indicating
   * that redirecting to a local ComponentSpace is *not* allowed.
   * @returns {Promise.<baja.Ord>}
   */
  function determineLocalRedirectOrd(responseFromOrdChannel, originalOrd) {
    const {
      cs: redirectComponentSpaceOrd,
      pp: redirectPropertyPath,
      sp: redirectSlotPath
    } = responseFromOrdChannel;

    return verifyComponentSpaceIsUsable(redirectComponentSpaceOrd, String(originalOrd))
      .then(() => {
        let ordStr = redirectComponentSpaceOrd + "|slot:" + redirectSlotPath;
        if (redirectPropertyPath) {
          ordStr += '|slot:' + redirectPropertyPath.join('/');
        }
        return Ord.make(ordStr);
      })
      .catch(() => originalOrd);
  }

  /**
   * Determines if the component space the server returned is one that the client can resolve.
   * @private
   * @param {String} componentSpaceOrd the component space ORD returned from the server.
   * @param {String} currOrd the current ORD value that THIS is using
   * @returns {Promise} to be resolved if the component space ORD from the server is usable,
   * otherwise rejected
   */
  function verifyComponentSpaceIsUsable(componentSpaceOrd, currOrd) {
    //If the componentSpaceOrd === the currOrd (that) then we are stuck in a loop and need to exit
    // by sending back the ord we got from the server instead of trying to resolve it again.
    if (componentSpaceOrd === currOrd) {
      return Promise.resolve();
    }

    return baja.Ord.make(componentSpaceOrd).get()
      .then((componentSpace) => {
        const csType = String(componentSpace.getType());
        if (csType !== 'baja:ComponentSpace' && csType !== 'baja:VirtualComponentSpace') {
          throw new Error('Your resp.cs was not a usable component space');
        }
      });
  }

  function subscribeTarget(target, subscriber) {
    const comp = target.getComponent();
    if (comp !== null && comp.isMounted()) {
      return subscriber.subscribe({ comps: [ comp ] });
    } else {
      return Promise.resolve();
    }
  }

  function leaseTarget(target, leaseTime) {
    const comp = target.getComponent();
    if (comp !== null && comp.isMounted()) {
      return comp.lease({ time: leaseTime, importAsync: true });
    } else {
      return Promise.resolve();
    }
  }

  function canCursor(value) {
    return baja.hasType(value) && typeof value.cursor === "function";
  }

  function doCursor(cursorableValue, cursorArgs, cursorResultEncodings) {
    cursorArgs.$cursorBsonArray = cursorResultEncodings;
    return cursorableValue.cursor(cursorArgs);
  }

  return Ord;
});