comm.js

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

/**
 * Network Communications for BajaScript.
 *
 * @author Gareth Johnson
 * @version 2.0.0.0
 */

define([
  "bajaPromises",
  "bajaScript/sys",
  "bajaScript/baja/sys/BaseBajaObj",
  "bajaScript/baja/comm/Batch",
  "bajaScript/baja/comm/BoxError",
  "bajaScript/baja/comm/BoxFrame",
  "bajaScript/baja/comm/Callback",
  "bajaScript/baja/comm/ServerError",
  "bajaScript/baja/comm/ServerSession" ], function (
    bajaPromises,
    baja,
    BaseBajaObj,
    Batch,
    BoxError,
    BoxFrame,
    Callback,
    ServerError,
    ServerSession) {

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

  // Create local for improved minification
  var strictArg = baja.strictArg;
  
  //need Contract for station root, and Month, Weekday needed for BDate
  var TYPES_TO_ALWAYS_IMPORT = [ 'baja:Station', 'baja:StatusValue',
    'baja:Month', 'baja:Weekday', 'baja:Link' ];

  /**
   * Baja Communications
   * @namespace baja.comm
   */
  baja.comm = new BaseBajaObj();

  baja.comm.ServerError = ServerError;
  baja.comm.BoxError = BoxError;
  baja.comm.BoxFrame = BoxFrame;
  baja.comm.Batch = Batch;
  baja.comm.Callback = Callback;

  ////////////////////////////////////////////////////////////////
  // Comms
  //////////////////////////////////////////////////////////////// 

  var defaultPollRate = 2500,
      eventModePollRate = 30000,
      pollRate = defaultPollRate, // Rate at which the Server Session is polled for events
      serverSession = null, // The main server session
      pollTicket = baja.clock.expiredTicket, // The ticket used to poll for events in the comms layer
      commFail = function (err) {   // Comm fail handler
        baja.outln("Comms failed: " + err.name);
        if (!err.noReconnect) {
          baja.outln("Attempting reconnect...");
        }
      },
      requestIdCounter = 0, // Number used for generating unique request ids for each BOX message.
      frameIdCounter = 0;   // Number used for generating unique frame ids for each BOX frame.

  /**
   * Increment the request id counter and returns its incremented value.
   * This is used on each outgoing BOX message in a BOX frame.
   *
   * @private
   * 
   * @return {Number} The newly incremented request id value.
   */
  baja.comm.incrementRequestId = function () {
    return ++requestIdCounter; 
  };  

  /**
   * Increment the frame id counter and returns its incremented value.
   * This is used on each outgoing BOX frame.
   *
   * @private
   * 
   * @return {Number} The newly incremented frame id value.
   */
  baja.comm.incrementFrameId = function () {
    return ++frameIdCounter; 
  };   

  /**
   * Set the comm fail function that gets called when the
   * communications layer of BajaScript fails.
   *
   * @private
   * @param func the comm fail error
   */
  baja.comm.setCommFailCallback = function (func) {
    strictArg(func, Function);
    commFail = func;
  };

  /**
   * Attempts a reconnection
   *
   * @abstract
   * @private
   */
  baja.comm.reconnect = function () {
    throw new Error("baja.comm.reconnect not implemented");
  };

  /**
   * @private
   * @returns {boolean} true if BajaScript is operating over a secure channel
   * @since Niagara 4.14
   */
  baja.comm.$isSecure = function () {
    return false;
  };

  function serverCommFail(err) {
    // If BajaScript has stopped then don't try to reconnect...
    if (baja.isStopping()) {
      return;
    }

    // Nullify the server session as this is no longer valid...
    serverSession = null;

    try {
      // Signal that comms have failed
      commFail(err);
    } catch (ignore) {
    }

    // Stop any further polling...
    pollTicket.cancel();

    function detectServer() {
      var cb = new Callback(function ok() {
          // If we can get a connection then reconnect
          baja.comm.reconnect();
        },
        function fail(err) {
          if (!err.noReconnect) {
            // If the BOX Service is unavailable or we can't connect at all then schedule another
            // server test to detect when it comes back online...
            if (err.delayReconnect) {
              // Schedule another test to try and detect the Server since we've got nothing back...
              baja.clock.schedule(detectServer, 1000);
            } else {
              // If we've got some sort of status code back then the server could be there
              // so best attempt a refresh
              baja.comm.reconnect();
            }
          }
        });

      cb.addReq("sys", "hello", {});
      cb.commit();
    }

    // Unless specified otherwise, attempt a reconnect...
    if (!err.noReconnect) {
      detectServer();
    }
  }

  //TODO: catch unrecoverable error from batch commit. this isn't so great
  baja.comm.serverCommFail = serverCommFail;

  ////////////////////////////////////////////////////////////////
  // Server Session Comms Calls
  //////////////////////////////////////////////////////////////// 

  baja.comm.getServerSessionId = function () {
    return serverSession && serverSession.$id;
  };
  
  /**
   * Makes the ServerSession.
   *
   * @private
   *
   * @param {baja.comm.Callback} cb
   */
  baja.comm.makeServerSession = function (cb) {

    // Add intermediate callbacks
    cb.addOk(function (ok, fail, id) {
      try {
        serverSession = new ServerSession(id);
        ok(id);
      } finally {
        pollTicket = baja.clock.schedule(baja.comm.poll, pollRate);
      }
    });

    cb.addFail(function (ok, fail, err) {
      try {
        fail(err);
      } finally {
        serverCommFail(err);
      }
    });

    // Make the ServerSession
    ServerSession.make(cb);

    // commit if we can
    cb.autoCommit();
  };

  /**
   * Make a Server Session Handler on the Server.
   *
   * A Server Session represents the session between a BajaScript Client and
   * the Server. Components can be created and mounted under the Server's
   * Server Session. These are called Server Session Handler Components.
   * Server Session Handler Components provide an architecture for Session
   * based Components that can receive requests and responses. A Server
   * Session Handler can also dispatch events to a BajaScript client for
   * further processing. A good example of a Server Session Handler is the
   * local Component Space BajaScript is connected to. The Server Session
   * Handler API represents a useful abstract layer for other Space subsystems
   * to be plugged into (e.g. Virtual Component Spaces).
   *
   * Currently, the Server Session API is considered to be private and should
   * only be used by Tridium framework developers.
   *
   * @private
   *
   * @param {String} serverHandlerId  a unique String that will identify the Server Session Handler under the Server Session.
   * @param {String} serverHandlerTypeSpec  the type spec (moduleName:typeName) of the Server Session Handler that will be mounted
   *                                        under the Server Session.
   * @param serverHandlerArg an initial argument to be passed into the Server Session Handler when it's created. This argument will be
   *                         encoded to standard JSON.
   * @param {Function} eventHandler an event handler callback function that will be called when any events are dispatched from the
   *                                  Server Session Handler.
   * @param {baja.comm.Callback} cb the callback handler.
   * @param {Boolean} [makeInBatch] set to true if the batch being used has the make present (hence the server session creation
   *                                is part of this network call.
   */
  baja.comm.makeServerHandler = function (serverHandlerId, serverHandlerTypeSpec, serverHandlerArg, eventHandler, cb, makeInBatch) {
    var arg;

    // If ServerSession isn't available then throw the appropriate error
    if (!serverSession) {
      // If this flag is true then the Server Session creation is part of this network request
      // hence the server session id will be picked up in the Server via the BoxContext
      if (makeInBatch) {
        arg = ServerSession.addReq("makessc", cb, {});
      } else {
        throw new Error("ServerSession not currently available!");
      }
    } else {
      arg = serverSession.addReq("makessc", cb);
    }

    ServerSession.addEventHandler(serverHandlerId, eventHandler);

    // Fill out other arguments
    arg.scid = serverHandlerId;
    arg.scts = serverHandlerTypeSpec;
    arg.scarg = serverHandlerArg;

    // commit if we can
    cb.autoCommit();
  };

  /**
   * Return true if the Server Handler is already registered.
   *
   * @private
   * @param  {String} id The event handler id to test for.
   * @returns {Boolean} Return true if found.
   */
  baja.comm.hasServerHandler = function (id) {
    return !!ServerSession.findEventHandler(id);
  };

  /**
   * Remove a Server Session Handler from the Server.
   *
   * @private
   *
   * @see baja.comm.makeServerHandler
   *
   * @param {String} serverHandlerId the id of the Server Session Handler to remove from the Server.
   * @param {baja.comm.Callback} cb the callback handler.
   */
  baja.comm.removeServerHandler = function (serverHandlerId, cb) {
    // If ServerSession isn't available then throw the appropriate error
    if (!serverSession) {
      throw new Error("ServerSession not currently available!");
    }

    ServerSession.removeEventHandler(serverHandlerId);

    // Make Server Session Request
    var arg = serverSession.addReq("removessc", cb);

    // Fill out other arguments
    arg.scid = serverHandlerId;

    // commit if we can
    cb.autoCommit();
  };

  /**
   * Represents an object that has a `ServerSessionHandler` residing server-side
   * that accepts server handler calls. For instance, a `ComponentSpace` will
   * have a corresponding `BComponentSpaceSessionHandler` on the server.
   * @private
   * @interface ServerHandlerProxy
   * @memberOf baja.comm
   */

  /**
   * @function
   * @name baja.comm.ServerHandlerProxy#getServerHandlerId
   * @returns {string} the ID of the `ServerSessionHandler` on which server
   * handler calls should be made.
   */

  /**
   * Make an RPC call to the Server Session Handler on the Server.
   *
   * @private
   *
   * @see baja.comm.makeServerHandler
   *
   * @param {String|baja.comm.ServerHandlerProxy} serverHandlerId the ID of the
   * Server Session Handler, or an object from whom the server handler ID can be
   * requested.
   * @param {String} serverHandlerKey the key of the request handler to invoke on the Server Session Handler.
   * @param serverHandlerArg the argument to pass into the request handler invoked on the Server Session Handler.
   *                         This argument is encoded to JSON.
   * @param {baja.comm.Callback} cb the callback handler.
   * @param {Boolean} [makeInBatch] set to true if the batch being used has the make present (hence the server session creation
   *                                is part of this network call).
   */
  baja.comm.serverHandlerCall = function (serverHandlerId, serverHandlerKey, serverHandlerArg, cb, makeInBatch) {
    var arg;

    // If ServerSession isn't available then it's request make be in this batch so
    // allow it anyway
    if (!serverSession) {
      // If this flag is true then the Server Session creation is part of this network request
      // hence the server session id will be picked up in the Server via the BoxContext
      if (makeInBatch) {
        arg = ServerSession.addReq("callssc", cb, {});
      } else {
        throw new Error("ServerSession not currently available!");
      }
    } else {
      arg = serverSession.addReq("callssc", cb);
    }

    // Fill out other arguments
    if (typeof serverHandlerId === 'string') {
      arg.scid = serverHandlerId;
    } else if (typeof serverHandlerId.getServerHandlerId === 'function') {
      arg.scid = serverHandlerId.getServerHandlerId();
    }
    arg.sck = serverHandlerKey;
    arg.scarg = serverHandlerArg;

    // commit if we can
    cb.autoCommit();
  };

  ////////////////////////////////////////////////////////////////
  // Server Session Event Polling
  //////////////////////////////////////////////////////////////// 

  function schedulePoll() {
    pollTicket.cancel();
    if (serverSession && !baja.isStopping()) {
      pollTicket = baja.clock.schedule(baja.comm.poll, pollRate);
    }
  }

  function processEvents(ok, fail, resp) {
    var length = resp.length,
        i,
        handler;

    if (baja.isStopping()) {
      return;
    }

    function handlerCb() {
      if (--length <= 0) {
        ok();
      }
    }

    if (length > 0) {
      for (i = 0; i < resp.length; ++i) {
        // Look up the event handler using the Server Side Component Id
        handler = ServerSession.findEventHandler(resp[i].scid);
        if (typeof handler === "function") {
          // Handle the events
          handler(resp[i].evs, handlerCb);
        }
      }
    } else {
      handlerCb();
    }
  }

  /**
   * Polls the ServerSession for Changes.
   *
   * @private
   *
   * @param {Object} [cb]  callback
   */
  baja.comm.poll = function (cb) {

    // Cancel any existing poll timers
    pollTicket.cancel();

    // Bail if we're stopping
    if (baja.isStopping()) {
      return;
    }

    // Flag indicating whether this was called from the poll timer.
    var fromTimer = false;

    // Ensure we have a callback
    if (!cb) {
      cb = new Callback(baja.ok, baja.error);
      fromTimer = true;
    }

    // Bail if we haven't got a serverSession
    if (!serverSession) {
      throw new Error("No Server Session available");
    }

    // Process the events
    cb.addOk(processEvents);

    // Schedule the next poll
    cb.getBatch().addCallback(new Callback(schedulePoll, schedulePoll));

    cb.addFail(function (ok, fail, err) {
      // TODO: Could make this more robust by explicitly checking for server id not existing?
      if (fromTimer) {
        // Delay raising this error by a second (just in case we're trying to shutdown at the time)...
        baja.clock.schedule(function () {
          // If there's an error from a timer poll then something must have screwed up 
          // really badly...
          try {
            fail(err);
          } finally {
            serverCommFail(err);
          }
        }, 1000);
      } else {
        fail(err);
      }
    });

    serverSession.addReq("pollchgs", cb);

    // Commit if we're able to
    cb.autoCommit();
  };

  ////////////////////////////////////////////////////////////////
  // Comms Unsolicited Event Mode
  //////////////////////////////////////////////////////////////// 

  baja.comm.startEventMode = function () {
    // Switch to a lower poll rate
    pollRate = eventModePollRate;
    if (serverSession) {
      baja.comm.poll();
    }
  };

  baja.comm.stopEventMode = function () {
    // Switch back to the standard poll rate
    pollRate = defaultPollRate;
    schedulePoll();
  };

  baja.comm.handleEvents = function (events) {
    // Process the events
    processEvents(baja.ok, baja.fail, events);
  };

  ////////////////////////////////////////////////////////////////
  // Comms Start and Stop
  //////////////////////////////////////////////////////////////// 

  /**
   * Start the Comms Engine.
   *
   * This is called to start BajaScript.
   *
   * @private
   *
   * @param {Object} obj the Object Literal for the method's arguments.
   * @param {Function} [obj.started] function called once BajaScript has started.
   * @param {Array} [obj.typeSpecs] an array of type specs (moduleName:typeName) to import
   *                                on start up. This will import both Type and Contract information.
   * @param {Function} [obj.commFail] function called if the BOX communications fail.
   * @param {Boolean} [obj.navFile] if true, this will load the nav file for the user on start up.
   */
  baja.comm.start = function (obj) {
    commFail = obj.commFail || commFail;
    
    var started = obj.started || baja.ok,
        typeSpecs = obj.typeSpecs || [],
        batch = new baja.comm.Batch();
    
    // Get the System Properties...
    var propsCb = new Callback(baja.initFromSysProps, baja.fail, batch);
    propsCb.addReq("sys", "props", {});
    
    TYPES_TO_ALWAYS_IMPORT.forEach(function (type) {
      if (typeSpecs.indexOf(type) < 0) { typeSpecs.push(type); }
    });

    // If specified, load the Nav File on start up
    if (obj.navFile) {
      baja.nav.navfile.load({ batch: batch });
    }

    // Import all of the requested Type Specification
    baja.importTypes({ "typeSpecs": typeSpecs, "batch": batch });

    // Don't open a local Station connection if BajaScript is running in offline mode.
    if (!baja.$offline) {
      // Once we have a ServerSession, we can start off our connection to
      // to the local Component Space.
      baja.station.init(batch);
    }

    // Call this once everything has finished
    batch.addCallback(new Callback(started));

    // Commit all comms messages in one request
    batch.commit();
  };

  /**
   * Stop the Comms Engine.
   *
   * @private
   *
   * @param {Object} obj the Object Literal for the method's arguments.
   * @param {Function} [obj.stopped]  function to invoke after the comms have stopped.
   * @param {Function} [obj.preStop] function to invoke just before the comms have stopped.
   */
  baja.comm.stop = function (obj) {
    var postStopFunc = obj.stopped || baja.ok;

    // Cancel the Ticket
    pollTicket.cancel();

    // Delete the ServerSession if it exists
    if (serverSession) {
      var batch = new baja.comm.Batch(),
          cb = new Callback(postStopFunc, baja.ok, batch);

      serverSession.addReq("del", cb);

      // Set hidden synchronous network call flag to 'beacon' so it can be
      // safely sent from window unload event handlers.
      batch.$sync = 'beacon';

      batch.commit();

      serverSession = null;
    }

    // TODO: Need to unsubscribe Components
  };

  ////////////////////////////////////////////////////////////////
  // Registry Channel Comms
  //////////////////////////////////////////////////////////////// 

  /**
   * Makes a network call for loading Type information.
   *
   * @private
   *
   * @param {String|Array} types  the TypeSpecs needed to be resolved.
   * @param {baja.comm.Callback} cb callback handler. If the callback had a
   *                             batch object originally defined then the network call
   *                             will be made.
   */
  baja.comm.loadTypes = function (types, cb) {
    if (typeof types === "string") {
      types = [ types ];
    }

    // Add a request message to the Builder
    cb.addReq("reg", "loadTypes", { t: types, ec: /*encodeContracts*/true });

    // commit if we can
    cb.autoCommit();
  };

  /**
   * Makes a network call for getting concrete Type information.
   *
   * @private
   *
   * @param {String} typeSpec  the TypeSpec used for querying the concrete types.
   * @param {baja.comm.Callback} cb the callback handler. If the callback had a
   *                                batch object originally defined then the network call
   *                                will be made.
   */
  baja.comm.getConcreteTypes = function (typeSpec, cb) {

    // Add a request message to the Builder
    cb.addReq("reg", "getConcreteTypes", typeSpec);

    // Commit if we're able too...
    cb.autoCommit();
  };

  /**
   * Makes a network call for getting a list of agent information.
   *
   * @private
   * 
   * @param  {Object} arg An object to be used as the argument for the 
   * `getAgents` call.
   * @param  {baja.comm.Callback} cb The callback handler.
   */
  baja.comm.getAgents = function (arg, cb) {
    cb.addReq("reg", "getAgents", arg);
    
    cb.autoCommit();
  };

  ////////////////////////////////////////////////////////////////
  // ORD Channel Comms
  ////////////////////////////////////////////////////////////////

  /**
   * Resolve an ORD.
   *
   * @private
   *
   * @param {baja.Ord} ord the ORD to be resolved.
   * @param {baja.Ord} baseOrd the base Ord.
   * @param {Object} cb the callback handler.
   * @param {Object} options Object Literal options.
   */
  baja.comm.resolve = function (ord, baseOrd, cb, options) {
    var bd = { // ORD Resolve Message Body
      o: String(ord), // ORD
      bo: String(baseOrd), // Base ORD
      sp: options.sp !== false // Flag to indicate OrdChannel to respond only with SlotPath
    };

    var cursor = options.cursor;

    // If cursor options are defined then use them...
    if (cursor) {
      const { offset, limit } = cursor;
      if (limit > Number.MAX_SAFE_INTEGER) {
        throw new Error('cursor.limit ' + limit + ' > Number.MAX_SAFE_INTEGER');
      }
      bd.c = {
        of: offset,
        lm: limit
      };
    }

    // Add Request Message
    cb.addReq("ord", "resolve", bd);

    // Commit if we're able too
    cb.autoCommit();
  };

  /**
   * Resolve Cursor data for a Collection (or Table).
   *
   * @private
   *
   * @param {Object} bd  the body of the Comms message.
   * @param {baja.comm.Callback} cb  the callback handler.
   * @param {Object} options Object Literal options.
   */
  baja.comm.cursor = function (bd, cb, options) {
    bd.of = options.offset;
    bd.lm = options.limit;

    // Add Request Message
    cb.addReq("ord", "cursor", bd);

    // Commit if we're able too
    cb.autoCommit();
  };

  ////////////////////////////////////////////////////////////////
  // Sys Channel Comms
  ////////////////////////////////////////////////////////////////  

  /**
   * Makes a network call for the nav file.
   *
   * @private
   *
   * @param {baja.comm.Callback} cb callback handler. If the callback had a
   *                             batch object originally defined then the network call
   *                             will be made.
   */
  baja.comm.navFile = function (cb) {
    // Add a request message to the Builder
    cb.addReq("sys", "navFile", {});

    // commit if we can
    cb.autoCommit();
  };

  /**
   * Make a network call with the specified error. This will Log the error
   * in the Server.
   *
   * @private
   *
   * @param {String} error the error message to be logged in the Server.
   */
  baja.comm.error = function (error) {
    // Make a network call but don't report back any errors if this doesn't work
    var cb = new Callback(baja.ok, baja.ok);

    // Add a request message to the Builder
    cb.addReq("sys", "error", error);

    // commit if we can
    cb.commit();
  };

  /**
   * Make an RPC network call with the specified arguments to the Server.
   *
   * @private
   *
   * @param {baja.Ord|String} ord The ORD used to resolve the RPC call on the Server.
   * @param {String} methodName The name of the method to invoke.
   * @param {Array} args The arguments to send to encode to the Server.
   * @param {baja.comm.Callback} cb The callback comms object.
   */
  baja.comm.rpc = function (ord, methodName, args, cb) {
    cb.addReq("sys", "rpc", {
      o: ord.toString(),
      m: methodName,
      a: args
    });

    cb.autoCommit();
  };

  ////////////////////////////////////////////////////////////////
  // Transfer Channel Comms
  ////////////////////////////////////////////////////////////////  

  /**
   * Performs a move operation on the station using the Transfer API.
   * If there is a naming conflict, names will be auto-generated.
   *
   * @private
   * @param {Object} config
   * @param {Array.<String|baja.Ord>} config.sourceOrds ORDs of the source
   * nodes to be copied
   * @param {Array.<baja.NavNode>} [config.sourceBases] Base objects used to
   * resolve the source ORDs; any missing bases will use localhost
   * @param {baja.NavNode} config.target a mounted target nav node to receive
   * the copied nodes
   * @param {baja.comm.Callback} cb
   * @returns {Promise.<{ undoKey: string, insertNames: string[] }>} promise to
   * be resolved after the copy operation completes; if the target is a
   * Component, its component space will also be synced. Promise will be
   * resolved with an array of the node names of the newly inserted nodes and a
   * key for a future call to `undo`.
   *
   * @see com.tridium.box.BTransferChannel
   */
  baja.comm.move = function (config, cb) {
    var sourceOrds = config.sourceOrds,
      sourceBases = config.sourceBases || [],
      target = config.target,
      origin = config.origin;

    var obj = {
      sources: sourceOrds.map(function (sourceOrd, i) {
        return toOrdObject(sourceOrd, sourceBases[i]);
      }),
      target: toOrdObject(target.getNavOrd())
    };

    if (origin) { obj.origin = origin; }

    cb.addReq('transfer', 'move', obj);

    if (baja.hasType(target, 'baja:Component')) {
      cb.addOk(function (ok, fail, resp) {
        target.getComponentSpace().sync({
          ok: function () { ok(resp); }, fail: fail
        });
      });
    }

    cb.addOk(function (ok, fail, resp) {
      ok(resp);
    });

    cb.autoCommit();

    return cb.promise();
  };

  /**
   * Performs a copy operation on the station using the Transfer API.
   * 
   * @private
   * @param {Object} config
   * @param {Array.<String|baja.Ord>} config.sourceOrds ORDs of the source
   * nodes to be copied
   * @param {Array.<baja.NavNode>} [config.sourceBases] Base objects used to
   * resolve the source ORDs; any missing bases will use localhost
   * @param {baja.NavNode} config.target a mounted target nav node to receive
   * the copied nodes
   * @param {Array.<String>} [config.names] desired names for the copied nodes.
   * If omitted, auto-generated names will be used.
   * @param {boolean} [config.keepLinks=false] set to true to have links
   * copied over from the source nodes
   * @param {boolean} [config.keepRelations=false] set to true to have
   * relations copied over from the source nodes
   * @param {string} [config.origin] set to `compute` to compute the origin
   * based off the position of the source nodes (as Paste Special does), or
   * set it to a `WsAnnotation` encoding such as `2,2,8` to specify exactly
   * what point on the wiresheet the copied nodes should go. If omitted, no
   * annotation will be added to the copied nodes.
   * @param {number} [config.numCopies=1] set how many times the nodes should
   * be duplicated
   * @param {baja.comm.Callback} cb
   * @returns {Promise.<{ undoKey: string, insertNames: string[] }>} promise to
   * be resolved after the copy operation completes; if the target is a
   * Component, its component space will also be synced. Promise will be
   * resolved with an array of the node names of the newly inserted nodes and a
   * key for a future call to `undo`.
   * 
   * @see com.tridium.box.BTransferChannel
   */
  baja.comm.copy = function (config, cb) {
    var sourceOrds = config.sourceOrds,
        sourceBases = config.sourceBases || [],
        target = config.target,
        names = config.names,
        keepLinks = config.keepLinks,
        keepRelations = config.keepRelations,
        numCopies = config.numCopies,
        origin = config.origin;
    
    var obj = {
      sources: sourceOrds.map(function (sourceOrd, i) {
        return toOrdObject(sourceOrd, sourceBases[i]);
      }),
      target: toOrdObject(target.getNavOrd())
    };
    
    if (names) { obj.names = names; }
    if (typeof keepLinks === 'boolean') { obj.keepLinks = keepLinks; }
    if (typeof keepRelations === 'boolean') { obj.keepRelations = keepRelations; }
    if (typeof numCopies === 'number') { obj.numCopies = numCopies; }
    if (typeof origin === 'string') { obj.origin = origin; }
    
    cb.addReq('transfer', 'copy', obj);
    
    if (baja.hasType(target, 'baja:Component')) {
      cb.addOk(function (ok, fail, resp) {
        target.getComponentSpace().sync({
          ok: function () { ok(resp); }, fail: fail
        });
      });
    }
    
    cb.addOk(function (ok, fail, resp) {
      ok(resp);
    });
    
    cb.autoCommit();

    return cb.promise();
  };

  /**
   * Performs a delete operation on the station using the Transfer API.
   *
   * @private
   * @param {Object} config
   * @param {Array.<String|baja.Ord>} config.deleteOrds ORDs of the nodes to be
   * deleted
   * @param {Array.<baja.NavNode>} [config.deleteBases] Base objects used to
   * resolve the delete ORDs; any missing bases will use localhost
   * @param {baja.comm.Callback} cb
   * @returns {Promise.<string>} promise to be resolved after the delete operation
   * completes. Promise will be resolved with a string key that may be used for a
   * future undelete operation.
   *
   * @see com.tridium.box.BTransferChannel
   * @since Niagara 4.11
   */
  baja.comm.delete = function (config, cb) {
    var deleteOrds = config.deleteOrds;
    var deleteBases = config.deleteBases || [];

    var obj = {
      deletes: deleteOrds.map(function (deleteOrd, i) {
        return toOrdObject(deleteOrd, deleteBases[i]);
      })
    };

    cb.addReq('transfer', 'delete', obj);

    cb.addOk(function (ok, fail, resp) {
      var undoKey = resp.undoKey;
      var spaceOrds = resp.spaceOrds || [];
      bajaPromises.all(spaceOrds.map(function (spaceOrd) {
        return baja.Ord.make(spaceOrd).get()
          .then(function (space) {
            return space.sync();
          });
      }))
        .then(function () {
          ok(undoKey);
        })
        .catch(fail);
    });

    cb.autoCommit();

    return cb.promise();
  };

  /**
   * Performs an undo operation on the station using the Transfer API.
   *
   * @private
   * @param {Object} config
   * @param {string} config.undoKey an undo key from a previous transfer
   * @param {baja.comm.Callback} cb
   * @returns {Promise} promise to be resolved after the undo operation
   * completes.
   *
   * @see com.tridium.box.BTransferChannel
   * @since Niagara 4.11
   */
  baja.comm.undo = function (config, cb) {
    var undoKey = config.undoKey;

    if (!undoKey) {
      cb.fail(new Error('undo key required'));
      return cb.promise();
    }

    cb.addReq('transfer', 'undo', undoKey);

    cb.addOk(function (ok, fail, resp) {
      var spaceOrds = resp.spaceOrds || [];
      bajaPromises.all(spaceOrds.map(function (spaceOrd) {
        return baja.Ord.make(spaceOrd).get()
          .then(function (space) {
            return space.sync();
          });
      }))
        .then(ok, fail);
    });

    cb.autoCommit();

    return cb.promise();
  };

  function toOrdObject(ord, base) {
    return {
      o: String(ord),
      bo: String((base || baja.nav.localhost).getNavOrd())
    };
  }

  ////////////////////////////////////////////////////////////////
  // History Channel Comms
  ////////////////////////////////////////////////////////////////  

  /**
   * Makes a network call to clear all the history records.
   *
   * @private
   *
   * @param {Array<String>} ords An array of ORDs to histories to clear.
   * @param {baja.comm.Callback} cb callback handler. If the callback had a
   *                             batch object originally defined then the network call
   *                             will be made.
   */
  baja.comm.clearAllRecords = function (ords, cb) {
    cb.addReq("history", "clearAllRecords", ords);
    cb.autoCommit();
  };

  /**
   * Makes a network call to clear all the history records before
   * a certain point in time.
   *
   * @private
   *
   * @param {Object} obj An object literal containing the arguments.
   * @param {Array<String>} obj.ords An array of ORDs to histories to clear.
   * @param {String} obj.before The encoded before date absolute date time.
   * @param {baja.comm.Callback} cb callback handler. If the callback had a
   *                             batch object originally defined then the network call
   *                             will be made.
   */
  baja.comm.clearOldRecords = function (obj, cb) {
    cb.addReq("history", "clearOldRecords", obj);
    cb.autoCommit();
  };

  /**
   * Makes a network call to delete a number of histories.
   *
   * @private
   *
   * @param {Array<String>} ords An array of ORDs to histories to clear.
   * @param {baja.comm.Callback} cb callback handler. If the callback had a
   *                             batch object originally defined then the network call
   *                             will be made.
   */
  baja.comm.deleteHistories = function (ords, cb) {
    cb.addReq("history", "deleteHistories", ords);
    cb.autoCommit();
  };

  ////////////////////////////////////////////////////////////////
  // Alarm Channel Comms
  ////////////////////////////////////////////////////////////////  

  /**
   * Makes a network call to query the alarm database for alarms within the supplied
   * time range argument. A null argument returns all alarms in the database
   *
   * @private
   *
   * @param {Object} obj literal containing timeRange (obj.t), limit (obj.l), offset (obj.o)
   *                 and [obj.a] is passed to indicate whether the view is invoked
   *                 from the alarm archive space (true) or alarm space (false)
   * @param {baja.comm.Callback} cb callback handler. If the callback had a
   *                             batch object originally defined then the network call
   *                             will be made.
   */
  baja.comm.queryAlarmDatabase = function (obj, cb) {
    cb.addReq("alarm", "queryAlarmDatabase", obj);
    cb.autoCommit();
  };



  /**
   * Makes a network call to clear all the alarm records.
   *
   * @private
   *
   * @param obj
   * {boolean} [obj.a] is passed to indicate whether the view is invoked
   *           from the alarm archive space (true) or alarm space (false)
   * @param {baja.comm.Callback} cb callback handler. If the callback had a
   *                             batch object originally defined then the network call
   *                             will be made.
   */
  baja.comm.clearAllAlarmRecords = function (obj, cb) {
    cb.addReq("alarm", "clearAllRecords", obj);
    cb.autoCommit();
  };

  /**
   * Makes a network call to get the number of alarms in the alarm database
   * @private
   * @param timeRange
   * @param cb
   */
  baja.comm.getRecordCount = function (timeRange, cb) {
    cb.addReq("alarm", "getRecordCount", timeRange);
    cb.autoCommit();
  };

  /**
   * Makes a network call to clear alarm records before a given baja.AbsTime
   *
   * @private
   * @param {baja.AbsTime} [obj.time] beforeTime The earliest time to keep in the result.  Records
   *   before this time will be removed.
   * @param {boolean} [obj.a] is passed to indicate whether the view is invoked
   *           from the alarm archive space (true) or alarm space (false)
   * @param {baja.comm.Callback} cb callback handler. If the callback had a
   *                             batch object originally defined then the network call
   *                             will be made.
   */
  baja.comm.clearOldAlarmRecords = function (obj, cb) {
    cb.addReq("alarm", "clearOldRecords", obj);
    cb.autoCommit();
  };


  /**
   * Makes a network call to clear an alarm record for a given uuid
   *
   * @private
   *
   * @param {Array} [obj.uuids] the Uuids of the Alarm Records to remove from the database.
   * @param {boolean} [obj.a] is passed to indicate whether the view is invoked
   *           from the alarm archive space (true) or alarm space (false)
   *
   * @param {baja.comm.Callback} cb callback handler. If the callback had a
   *                             batch object originally defined then the network call
   *                             will be made.
   */
  baja.comm.clearAlarmRecords = function (obj, cb) {
    cb.addReq("alarm", "clearRecords", obj);
    cb.autoCommit();
  };

  /**
   * Makes a network call to add a note to the specified alarm record
   *
   * @private
   *
   * @param {String} alarm A BSON encoded representation of the alarm record
   * @param {String} notes The notes to add to the alarm
   *
   * @param {baja.comm.Callback} cb callback handler. If the callback had a
   *                             batch object originally defined then the network call
   *                             will be made.
   */
  baja.comm.addNote = function (alarm, notes, cb) {
    cb.addReq("alarm", "addNote", { alarm: alarm, notes: notes });
    cb.autoCommit();
  };

  /**
   * Makes a network call to retrieve a map of alarm class names to alarm class display names
   * @private
   * @param {baja.comm.Callback} cb callback handler. If the callback had a
   *                             batch object originally defined then the network call
   *                             will be made.
   */
  baja.comm.getDisplayNamesMap = function (cb) {
    cb.addReq("alarm", "getDisplayNamesMap", {});
    cb.autoCommit();
  };

  /**
   * Makes a network call to retrieve a map of alarm class names to alarm class permissions
   * @private
   *
   * @since Niagara 4.9
   *
   * @param {String} alarmClasses A BSON encoded representation of the alarm class name array
   * @param {baja.comm.Callback} cb callback handler. If the callback had a
   *                             batch object originally defined then the network call
   *                             will be made.
   */
  baja.comm.getPermissionsMap = function (alarmClasses, cb) {
    cb.addReq("alarm", "getPermissionsMap", alarmClasses);
    cb.autoCommit();
  };

  /**
   * Makes a network call to retrieve an array of declared fields in an alarm record
   * @private
   * @param {baja.comm.Callback} cb callback handler. If the callback had a
   *                             batch object originally defined then the network call
   *                             will be made.
   */
  baja.comm.getAlarmFields = function (cb) {
    cb.addReq("alarm", "getAlarmFields", {});
    cb.autoCommit();
  };

  /**
   * Makes a network call to acknowledge a set of alarms specified by uuid and/or source
   * @private
   * @param {Object} params the object literal that contains the method's arguments.
   * @param {baja.comm.Callback} cb callback handler. If the callback had a
   *                             batch object originally defined then the network call
   *                             will be made.
   */
  baja.comm.ackAlarms = function (params, cb) {
    cb.addReq("alarm", "ackAlarms", params);
    cb.autoCommit();
  };

  /**
   * Makes a network call to a notes to a set of alarms specified by uuid and/or source.
   * @private
   * @param {Object} params the object literal that contains the method's arguments.
   * @param {baja.comm.Callback} cb callback handler. If the callback had a
   *                             batch object originally defined then the network call
   *                             will be made.
   */
  baja.comm.addNoteToAlarms = function (params, cb) {
    cb.addReq("alarm", "addNoteToAlarms", params);
    cb.autoCommit();
  };

  /**
   * Makes a network call to force clear a set of alarms specified by uuid and/or source
   * @private
   * @param {Object} params the object literal that contains the method's arguments.
   * @param {baja.comm.Callback} cb callback handler. If the callback had a
   *                             batch object originally defined then the network call
   *                             will be made.
   */
  baja.comm.forceClear = function (params, cb) {
    cb.addReq("alarm", "forceClear", params);
    cb.autoCommit();
  };

  /**
   * Makes a network call to query alarms details specified by uuid and/or source.
   * @private
   * @param {Object} params the object literal that contains the method's arguments.
   * @param {baja.comm.Callback} cb callback handler. If the callback had a
   *                             batch object originally defined then the network call
   *                             will be made.
   */
  baja.comm.getSingleSourceSummary = function (params, cb) {
    cb.addReq("alarm", "getSingleSourceSummary", params);
    cb.autoCommit();
  };

  /**
   * Makes a network call to query an alarm's notes.
   * @private
   * @param {Object} params the object literal that contains the method's arguments.
   * @param {baja.comm.Callback} cb callback handler. If the callback had a
   *                             batch object originally defined then the network call
   *                             will be made.
   */
  baja.comm.getNotes = function (params, cb) {
    cb.addReq("alarm", "getNotes", params);
    cb.autoCommit();
  };

  return baja;
});