/**
* @copyright 2015 Tridium, Inc. All Rights Reserved.
* @author Gareth Johnson
*/
/**
* Core System Architecture for BajaScript including Type and Registry System.
*
* @author Gareth Johnson
* @version 2.0.0.0
*/
/* eslint-env browser */
/*global bajaJsPrint */
////////////////////////////////////////////////////////////////
// Very common/generic methods
////////////////////////////////////////////////////////////////
define([ "bajaPromises",
"bajaScript/baja/comm/BoxError",
"nmodule/js/rc/asyncUtils/promiseMux",
"bajaScript/baja/sys/BaseBajaObj",
"bajaScript/baja/sys/bajaUtils",
"bajaScript/baja/sys/inherit",
"bajaScript/baja/sys/structures/AsyncCursor",
"bajaScript/baja/sys/structures/Cursor",
"bajaScript/baja/sys/structures/FilterCursor",
"bajaScript/baja/sys/structures/OrderedMap",
"bajaScript/baja/sys/structures/SyncCursor",
"lex!" ], function bajaDefine(
bajaPromises,
BoxError,
promiseMux,
BaseBajaObj,
bajaUtils,
inherit,
AsyncCursor,
Cursor,
FilterCursor,
OrderedMap,
SyncCursor,
lexjs) {
// Use ECMAScript 5 Strict Mode
"use strict";
/**
* The core BajaScript namespace.
* @namespace
* @alias baja
*/
const baja = new BaseBajaObj();
const callSuper = baja.callSuper = inherit.callSuper;
const subclass = baja.subclass = inherit.subclass;
const bajaDef = baja.def = bajaUtils.def;
const objectify = baja.objectify = bajaUtils.objectify;
const strictArg = baja.strictArg = bajaUtils.strictArg;
const strictAllArgs = baja.strictAllArgs = bajaUtils.strictAllArgs;
const { doRequire, resolveDependencies } = bajaUtils;
baja.unescapeXml = bajaUtils.unescapeXml;
baja.BaseBajaObj = BaseBajaObj;
baja.Cursor = Cursor;
baja.AsyncCursor = AsyncCursor;
baja.SyncCursor = SyncCursor;
baja.FilterCursor = FilterCursor;
baja.OrderedMap = OrderedMap;
baja.mixin = inherit.mixin;
baja.iterate = bajaUtils.iterate;
baja.strictNumber = bajaUtils.strictNumber;
var typeCtor;
////////////////////////////////////////////////////////////////
// Extra Array Methods
////////////////////////////////////////////////////////////////
/**
* The native JavaScript Array constructor. BajaScript extends it to add
* extra functionality.
*
* @external Array
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array Array}
*/
if (typeof Array.prototype.contains !== "function") {
if (typeof Array.prototype.indexOf === "function") {
/**
* Returns true if the given value is contained in this array, checking
* with strict equality (`===`).
*
* @function external:Array#contains
* @param o the value to check for
* @returns {Boolean}
* @deprecated This will be removed in a future version of BajaScript. Use the native browser
* function Array#includes.
*/
// eslint-disable-next-line no-extend-native
Array.prototype.contains = function (o) {
return this.indexOf(o) > -1;
};
} else {
// eslint-disable-next-line no-extend-native
Array.prototype.contains = function (o) {
var i;
for (i = 0; i < this.length; i++) {
if (this[i] === o) {
return true;
}
}
return false;
};
}
}
function isUpperCase(c) {
return c === c.toUpperCase();
}
function toFriendly(str) {
if (!str) {
return '';
}
var firstCharacter = str.substring(0, 1).toUpperCase();
return str.substring(1).split('').reduce(function (memo, c) {
return memo + (isUpperCase(c) ? ' ' + c : c);
}, firstCharacter);
}
////////////////////////////////////////////////////////////////
// BajaScript
////////////////////////////////////////////////////////////////
(function bajaNamespace() {
////////////////////////////////////////////////////////////////
// Baja
////////////////////////////////////////////////////////////////
/**
* BajaScript's version number (maj.min.build.patch).
*
* @constant
* @type {String}
* @default
*/
baja.version = "2.0.0.0";
const invalidTypeSpecs = {};
var bsRegStorage = null, // BajaScript Registry Storage.
// must be JSON only as it will be serialized
// to localStorage.
bsClockTimeouts = {}, // BajaScript ticket timeouts
bsClockIntervals = {}; // BajaScript ticket intervals
////////////////////////////////////////////////////////////////
// Debug
////////////////////////////////////////////////////////////////
(function debug() {
/**
* Print a message to BajaScript's debug output followed by a newline.
* By default just looks for a `bajaJsPrint` global function and
* passes the message to that.
*
* @param {String} msg the message to output.
*
* @returns baja
*/
baja.outln = function (msg) {
// If BajaScript has stopped then don't output anything else...
if (baja.isStopping()) {
return this;
}
if (typeof bajaJsPrint === "function") {
bajaJsPrint(msg);
}
return this;
};
/**
* Attempt to clear BajaScript's debug output.
*
* @returns baja
*/
baja.clearOut = function () {
return this;
};
/**
* Print out the error to the specified line printer function. This gets called
* by the default error handler.
*
* @method baja.printError
* @private
* @inner
*
* @see baja.error
*
* @param {Error} e the error to be printed.
* @param {Function} print the print function to be called each time a line of
* the error is to be printed out.
*/
function printError(e, print) {
var errorMsg = e.name + ": ";
//if it is a LocalizableError and has the toStack function the stack will already be
//correctly formatted
if (e.name === 'LocalizableError' && typeof e.toStack === 'function') {
return e.toStack()
.then((stack) => {
return print(stack);
});
}
// Some JavaScript engines give errors a nice full stack trace...
if (e.stack) {
if (e.message &&
typeof e.stack === "string" &&
e.stack.indexOf(errorMsg + e.message) !== 0) {
print(errorMsg + e.message);
}
print(e.stack);
return;
}
if (e.message) {
print(errorMsg + e.message);
} else {
print(errorMsg + e);
}
// Add a try/catch just in case because ECMAScript 5 disallows access to the callee and caller
try {
// Cater for IE and Safari and try to print out a pseudo stack trace...
var func,
fn,
args = arguments,
stackSize = 0,
maxStack = baja.stackTraceLimit || 20,
maxFuncLength = 200;
if (args.callee && args.callee.caller) {
func = args.callee.caller;
while (typeof func === "function" && stackSize < maxStack) {
fn = func.name;
if (!fn) {
// Attempt to format the function into some sort of stack trace...
fn = func.toString().replace(/[\r\n]/g, " ");
// Don't allow the string to get too big...
if (fn.length > maxFuncLength) {
fn = fn.substring(0, maxFuncLength) + "...";
}
}
print(" at " + fn);
func = func.caller;
stackSize++;
}
if (stackSize >= maxStack) {
print("Stack trace limit exceeded (" + maxStack + ")");
}
}
} catch (ignore) {
}
}
/**
* Print an error message in BajaScript's debug output. In IE/Safari the
* length of the stack trace will be limited to 20 records - you can
* change this by setting {@link baja.stackTraceLimit}.
*
* By default, this method calls {@link baja.printError} using
* {@link baja.outln} as the printer function.
*
* @param e the error to output.
*
* @returns baja
*/
baja.error = function (e) {
if (baja.isStopping()) {
return this;
}
// Make sure we have an Error object
e = e instanceof Error ? e : new Error(e);
// Print the error out using baja.outln.
printError(e, baja.outln);
// If this isn't a Server error then attempt to also log it in the Server (if specified)
if (baja.isStarted() &&
baja.isLogClientErrorsInServer() &&
e && !(e instanceof baja.comm.ServerError)) {
if (e.name === 'LocalizableError' && typeof e.toStack === 'function') {
return e.toStack()
.then((stack) => {
return baja.comm.error(stack);
});
}
var error = "";
printError(e, function (line) {
error += line + "\n";
});
// Make the network call to log the error
baja.comm.error(error);
}
return this;
};
/**
* Limits the length of stack traces printed by {@link baja.error}.
* @type {Number}
* @default
*/
baja.stackTraceLimit = 20;
}());
////////////////////////////////////////////////////////////////
// Baja Util
////////////////////////////////////////////////////////////////
/**
* The global default fail callback function.
*
* Throughout BajaScript there are places where network calls may be
* made. In one of these cases, a developer has the option of specifying
* a 'fail' callback. If the fail callback isn't specified by the
* developer, it will default back to this function.
*
* @see baja.ok
* @param {Error} [err=new Error()] an error to print
*/
baja.fail = function (err) {
// Output error to the BajaScript Console...
err = err || new Error();
baja.error(err instanceof Error ? err : new Error(err));
};
/**
* The global default ok callback function.
*
* Throughout BajaScript there are places where network calls may be
* made. In one of these cases, a developer has the option of specifying
* an 'ok' callback. If the ok callback isn't specifed by the developer,
* it will default back to this function.
*
* @see baja.fail
*/
baja.ok = function () {
};
/**
* A function that does nothing (noop = no-operation).
*
* @function
*/
baja.noop = baja.ok;
(function util() {
/**
* Ensure that a callback object literal exists and has ok and fail callbacks
* - default them to <code>baja.ok</code> and <code>baja.fail</code>,
* respectively, if they do not exist.
*
* @param {Object} [callbacks] an object containing ok/fail callbacks
* (the object may be missing one or both callbacks or may itself be
* undefined)
* @param {Function} [defaultOk=baja.ok] the ok handler to use if none
* provided
* @param {Function} [defaultFail=baja.fail] the fail handler to use if
* none provided
*/
baja.callbackify = function callbackify(callbacks, defaultOk, defaultFail) {
callbacks = baja.objectify(callbacks, 'ok');
callbacks.ok = callbacks.ok || defaultOk || baja.ok;
callbacks.fail = callbacks.fail || defaultFail || baja.fail;
return callbacks;
};
/**
* A general utility method that tries to prevents a function from running
* more often than the specified interval. Due to the vagaries of Javascript
* time management you may see the occasional drop of a few milliseconds but
* on the whole execution of the given function should not be permitted more
* than once in the specified interval.
*
* If the function is called before the interval is up, it will still be
* executed once the remainder of the interval elapses - it will not be
* cancelled.
*
* However, if the function is called *multiple times* before the
* interval has elapsed, only the *last* function call will execute -
* calls prior to that one will be cancelled.
*
* The function passed in will be run *asynchronously* via
* `setTimeout()`, so if you wish to process the results of the
* function call, you must do so via callback.
*
* @param {Function} func a function to be executed - must take no parameters
* and may optionally return a value.
* @param {Object} obj an object literal with additional parameters -
* also, passing a Number in as the second parameter will use that as
* `obj.interval` and the other params will be ignored/false.
* @param {Number} [obj.interval] the interval in milliseconds - the
* function will be prevented from executing more often than this (if
* omitted, 100ms).
* @param {Function} [obj.ok] an optional callback function that will
* handle the return value from the throttled function.
* @param {Function} [obj.fail] an optional fail callback function that
* will be called if the function throws any exceptions.
* @param {Boolean} [obj.drop] if drop is set to true, then additional
* function invocations that occur before the interval is up will be simply
* ignored rather than queued to execute at the end of the interval period.
* @returns {Function} a throttled function that can be executed the same
* way as the original function.
*/
baja.throttle = function (func, obj) {
obj = baja.objectify(obj, 'interval');
var interval = obj.interval || 100,
ok = obj.ok || baja.ok,
fail = obj.fail || baja.fail,
drop = obj.drop;
var mustWaitUntil = 0,
waitingForTicket = baja.clock.expiredTicket;
function getTimeToWait() {
var now = baja.clock.ticks(),
timeToWait = mustWaitUntil - now;
if (timeToWait <= 0) {
mustWaitUntil = now + interval;
}
return timeToWait;
}
return function () {
var that = this,
args = Array.prototype.slice.call(arguments),
funcToRun,
timeToWait = getTimeToWait();
if ((timeToWait > 0) && drop) {
return;
}
funcToRun = function () {
try {
ok(func.apply(that, args));
} catch (err) {
fail(err);
} finally {
// once the function is done executing, always reset the next time it
// will be allowed to execute again
getTimeToWait();
}
};
// don't allow function executions to pile up - only have one waiting at a time
waitingForTicket.cancel();
if (timeToWait <= 0) {
baja.runAsync(funcToRun);
} else if (!drop) {
waitingForTicket = baja.clock.schedule(funcToRun, timeToWait);
}
};
};
/**
* Test an Object to see if it's a Baja Object and has the 'getType' function.
*
* Please note: this test excludes objects that may extend
* {@link baja.Slot}.
*
* @param {Object} obj the Object to be tested.
* @param {Type|String} [type] the type or (String type specification -
* `module:typeName`) to test object against. When passing a string,
* it's not necessary to import the Type before calling `hasType`. Please note,
* {@link Type#is} is used and not `equals`.
*
* @returns {Boolean} true if the given Object is a proper BajaScript Object
*/
baja.hasType = function (obj, type) {
if (obj === null || obj === undefined) {
return false;
}
if (obj && typeof obj === "object" && obj instanceof baja.Slot) {
return false;
}
if (typeof obj.getType === "function") {
var objType = obj.getType();
if (!(objType instanceof typeCtor)) { return false; }
return type ? objType.is(type) : true;
}
return false;
};
/**
* Returns a Lexicon Object for a given module. The locale will be
* whatever the current user is set to.
*
* Please note, if BajaScript has Web Storage enabled, the Lexicon
* will be permanently cached.
*
* If the argument is just a `String`, no network call will be made.
* Instead, an attempt will be made to see if the `Lexicon` is cached
* locally. If available, it's returned. If not then an `Error` will be
* thrown. If you are not certain whether the lexicon is already loaded,
* it's recommended to use a callback.
*
* Please note, this method is now deprecated. Please use the `lex` module
* instead.
*
* @param {String|Object} obj a String for the module name or an object
* literal for the method's arguments. If a String is specified, NO
* network call will be made: the Lexicon will be returned if locally
* available, or an error will be thrown if not.
* @param {String} [obj.module] the module name to asynchronously look
* up.
* @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback.
* Invoked once the Lexicon has been acquired. The Lexicon will be passed
* as an argument to this callback.
* @param {Function} [obj.fail] (Deprecated: use Promise) the fail
* callback. Invoked if Lexicon fails to import.
* @param {baja.comm.Batch} [obj.batch] If specified, any network calls
* will be batched into this object.
*
* @see {module:nmodule/js/rc/lex/lex}
* @see baja.request
*
* @deprecated Please use Lexicon JS instead.
*
* @returns {module:nmodule/js/rc/lex/lex|Promise.<module:nmodule/js/rc/lex/lex>}
* If a String argument was specified, the Lexicon will be returned
* directly; otherwise if an object literal was given, a Promise to be
* resolved with the specified Lexicon.
* @throws {Error} if looking up a Lexicon by name, but the Lexicon has
* not yet been loaded
*
* @example
* // Get a value from a Lexicon. An asynchronous network call will be
* // made if the Lexicon isn't available locally. Note that this form
* // is safe to use whether the lexicon is already loaded or not.
* baja.lex({
* module: "bajaui"
* })
* .then(function (lex) {
* lex.get("dialog.ok");
*
* //now that the lexicon is loaded, baja.lex("bajaui") will
* //succeed from here on out.
* });
*
* // Get a value from a Lexicon. This will throw an error if the lexicon
* // was not already loaded via a network call as above.
* baja.lex("bajaui").get("dialog.ok");
*/
baja.lex = function (obj) {
var lex,
moduleName,
cb;
// Attempt to find cached Lexicon if string argument is specified.
if (typeof obj === "string") {
lex = lexjs.getLexiconFromCache(obj);
if (!lex) {
throw new Error("Could not find Lexicon for: " + obj);
}
return lex;
}
// If Object Literal then attempt to make asynchronous network call.
obj = baja.objectify(obj, "module");
moduleName = obj.module;
cb = new baja.comm.Callback(obj.ok, obj.fail, obj.batch);
lexjs.module(moduleName)
.then(function ok(lex) {
cb.ok(lex);
}, function fail(err) {
cb.fail(err);
});
return cb.promise();
};
/**
* Run the specified Function asynchronously.
*
* @param {Function} fn the Function to run asynchronously.
*/
baja.runAsync = function (fn) {
baja.clock.schedule(fn, 0);
};
/**
* Make an RPC call to a method on the Server that implements the
* Java 'NiagaraRpc' annotation.
*
* Any extra arguments passed in will be encoded in raw JSON and passed up
* to the Server. If one of those arguments is a 'baja.comm.Batch', the
* call will be batched accordingly.
*
* @param {baja.Ord|String|Object} ord The ORD used to resolve the RPC call on the Server.
* This can also be an Object Literal that has methodName (or method), args, ord
* (or typeSpec), batch, ok and fail callback properties.
* @param {String} [methodName] The method name of the RPC call to invoke on
* the Server.
* @returns {Promise} A promise that is resolved once the RPC call
* has completed. If the Station RPC call returns a value then it will
* be encoded and returned as a value in the promise.
*/
baja.rpc = function (ord, methodName) {
var args = Array.prototype.slice.call(arguments),
i,
batch,
cb,
ok,
fail;
if (args.length === 1 && ord && typeof ord === "object") {
methodName = ord.methodName || ord.method;
args = ord.args || [];
batch = ord.batch;
ok = ord.ok;
fail = ord.fail;
ord = ord.ord || "type:" + ord.typeSpec;
} else {
// Find a batch. If it's being used then remove it from the arguments
// and use it in the callback.
args.splice(0, 2);
for (i = 0; i < args.length; ++i) {
if (args[i] instanceof baja.comm.Batch) {
batch = args[i];
args.splice(i, 1);
break;
}
}
}
cb = new baja.comm.Callback(ok || baja.ok, fail || baja.fail, batch);
baja.comm.rpc(String(ord), methodName, args, cb);
return cb.promise();
};
}());
////////////////////////////////////////////////////////////////
// Clock
////////////////////////////////////////////////////////////////
baja.clock = (function bajaClockNamespace() {
/**
* Baja Clock methods used for scheduling.
* @namespace
* @alias baja.clock
*/
var clock = new BaseBajaObj();
/**
* Clock Ticket used when scheduling function calls in BajaScript.
*
* This Constructor shouldn't be invoked directly.
*
* @see baja.clock.schedule
*
* @class
* @alias Ticket
* @extends baja.BaseBajaObj
* @inner
* @public
*/
var Ticket = function () {
this.$id = -1;
};
subclass(Ticket, BaseBajaObj);
/**
* Cancel the currently scheduled Ticket.
*/
Ticket.prototype.cancel = function () {
clearTimeout(this.$id);
delete bsClockTimeouts[this.$id.toString()];
this.$id = -1;
};
/**
* Test for ticket expiration.
*
* @returns {Boolean} if the scheduled Ticket is currently expired.
*/
Ticket.prototype.isExpired = function () {
return this.$id === -1;
};
/**
* Clock Ticket used when scheduling periodic events in Niagara.
*
* This Constructor shouldn't be invoked directly.
*
* @class
* @alias IntervalTicket
* @extends Ticket
* @inner
* @public
*/
var IntervalTicket = function () {
callSuper(IntervalTicket, this, arguments);
};
subclass(IntervalTicket, Ticket);
/**
* Cancel the currently scheduled Ticket.
*/
IntervalTicket.prototype.cancel = function () {
clearInterval(this.$id);
delete bsClockIntervals[this.$id.toString()];
this.$id = -1;
};
/**
* Returns the number of Clock ticks on the system.
*
* This method is typically used for profiling.
*
* @returns {Number} the number of Clock ticks on the system.
*/
clock.ticks = function () {
return new Date().valueOf();
};
/**
* An expired Ticket.
* @constant
* @type {Ticket}
*/
clock.expiredTicket = new Ticket();
/**
* Schedule a one-off timer.
*
* When the callback is invoked, `this` will refer to the `Ticket` instance.
*
* If any variables need to be passed into this function then it's best to do this via
* JavaScript closures.
*
* @param {Function} func the function to be invoked once the specified time has elapsed.
* @param {Number} time the number of milliseconds before the event is run.
* @returns {Ticket} a new Ticket for the scheduled event.
*/
clock.schedule = function (func, time) {
strictAllArgs([ func, time ], [ Function, Number ]);
// If BajaScript has fully stopped then don't schedule anything else...
if (baja.isStopped()) {
return this.expiredTicket;
}
// Create ticket before timer so we get closure
var t = new Ticket();
t.$id = setTimeout(function () {
delete bsClockTimeouts[t.$id.toString()];
t.$id = -1;
func.call(t);
}, time);
// Register the ticket so we can keep track of it
bsClockTimeouts[t.$id.toString()] = t;
return t;
};
/**
* Schedule a periodic timer.
*
* When the callback is invoked, `this` will refer to the Ticket instance.
*
* If any variables need to be passed into this function then it's best to do this via
* JavaScript closures.
*
* @param {Function} func the function to be invoked each time the specified time has elapsed.
* @param {Number} time the number of milliseconds before the event is run.
*
* @returns {IntervalTicket} a new Ticket for the scheduled event.
*/
clock.schedulePeriodically = function (func, time) {
strictAllArgs([ func, time ], [ Function, Number ]);
// If BajaScript has fully stopped then don't schedule anything else...
if (baja.isStopped()) {
return this.expiredTicket;
}
// Create ticket before timer so we get closure
var t = new IntervalTicket();
t.$id = setInterval(function () {
func.call(t);
}, time);
// Keep track of the ticket internally
bsClockIntervals[t.$id.toString()] = t;
return t;
};
return clock;
}());
////////////////////////////////////////////////////////////////
// BajaScript Start and Stop
////////////////////////////////////////////////////////////////
(function startAndStop() {
var bsStarted = false, // BajaScript started flag
bsStopped = false, // BajaScript stopped flag
bsStopping = false, // BajaScript stopping flag
bsStartedCallbacks = null, // Started callbacks
bsPreStopCallbacks = null, // Stopped callbacks
bsUserName = "", // BajaScript user name for current session
bsLang = "", // BajaScript language for user
bsUserHome = "station:|slot:/", // BajaScript user home
bsTimeFormat = "", // BajaScript user time format pattern
bsUnitConversion = 0, // BajaScript user unit conversion (BUnitConversion ordinal)
bsLogClientErrorsInServer = false, // BajaScript log client errors in the Server
bsPreLoadRegStorage = null, // The pre-loaded BajaScript Registry storage
muxConfig = { enabled: false }; // Envelope mux configuration settings
/**
* Return true if BajaScript has started.
*
* @returns {Boolean} started
*/
baja.isStarted = function () {
return bsStarted;
};
/**
* Return true if BajaScript has stopped.
*
* @returns {Boolean} stopped
*/
baja.isStopped = function () {
return bsStopped;
};
/**
* Return true if BajaScript has stopped or is in the process of stopping.
*
* @returns {Boolean} stopping
*/
baja.isStopping = function () {
return bsStopping;
};
/**
* Return true if BajaScript is running in offline mode.
*
* @return {Boolean} offline.
*/
baja.isOffline = function () {
return !!baja.$offline;
};
/**
* Add a function to be invoked once BajaScript has started.
*
* If BajaScript has already started, the function will be invoked
* immediately.
*
* @param {Function} func invoked once BajaScript has started.
*/
baja.started = function (func) {
strictArg(func, Function);
// If we're already started then return immediately
if (bsStarted) {
try {
func.call(baja);
} catch (err) {
baja.error(err);
}
return;
}
// If not started yet then add the function to a callback list
if (!bsStartedCallbacks) {
bsStartedCallbacks = [];
}
bsStartedCallbacks.push(func);
};
/**
* Add a function to be invoked just before BajaScript is stopped.
*
* If BajaScript has already stopped, the function will be invoked
* immediately.
*
* @param {Function} func
*/
baja.preStop = function (func) {
strictArg(func, Function);
// If we're already started then return immediately
if (bsStopped) {
try {
func.call(baja);
} catch (err) {
baja.error(err);
}
return;
}
// If not started yet then add the function to a callback list
if (!bsPreStopCallbacks) {
bsPreStopCallbacks = [];
}
if (bsPreStopCallbacks.indexOf(func) < 0) {
bsPreStopCallbacks.push(func);
}
};
/**
* Start BajaScript.
*
* This must be called to start BajaScript. This will make a network
* call to create a Session that starts BajaScript. It's recommended to
* call this as soon as possible.
*
* @see baja.started
* @see baja.save
* @see baja.preStop
* @see baja.stop
*
* @param {Object|Function} [obj] the Object Literal for the method's arguments or the function invoke after the comms have started.
* @param {Function} [obj.started] function to invoke after the comms have started and (if running in a browser) the DOM is ready.
* @param {Function} [obj.commFail] function to invoke if the comms fail.
* @param {Array} [obj.typeSpecs] an array of type specs (moduleName:typeName) to import from the Server.
* Please note, this may not be needed if the Type and Contract information
* has been cached by web storage.
* @param {Boolean} [obj.navFile] if true, this will load the nav file for the user on start up. By default, this is false.
*
* @example
* <caption>
* This method takes a <code>started</code> function or an object
* literal.
* </caption>
*
* baja.start(function () {
* // Called once BajaScript has started.
* });
*
* //...or this can be invoked via an Object Literal...
*
* baja.start({
* started: function () {
* // Called when BajaScript has started. We're ready to rock and roll at this point!
* },
* commFail: function () {
* // Called when the BajaScript communications engine completely fails
* },
* typeSpecs: ["control:BooleanWritable", "control:NumericWritable"] // Types and Contracts we want imported
* // upfront before our Web App loads
* });
*/
baja.start = function (obj) {
// Initialize Comms Engine
obj = objectify(obj, "started");
var started = obj.started;
obj.started = function () {
// Signal that we have started
bsStarted = true;
// Call started callbacks
if (bsStartedCallbacks) {
var i;
for (i = 0; i < bsStartedCallbacks.length; ++i) {
try {
bsStartedCallbacks[i].call(baja);
} catch (err) {
baja.error(err);
}
}
bsStartedCallbacks = null;
}
// Invoke original started function
if (typeof started === "function") {
try {
started();
} catch (err2) {
baja.error(err2);
}
}
};
// Registry the common type library
baja.$ctypes = JSON.parse(baja.$ctypes);
baja.registry.register(baja.$ctypes);
//TODO: uncomment after NCCB-19190
//delete baja.$ctypes;
baja.comm.start(obj);
// Load Web Storage while the BajaScript comms engine starts.
// baja.$bsRegStorage could have already been set to the parent
// session's registry, if reusing a connection.
var reusedStorage = baja.$bsRegStorage;
if (reusedStorage) {
bsPreLoadRegStorage = reusedStorage;
} else {
bsPreLoadRegStorage = baja.registry.loadFromStorage();
}
// If a pre-start function was passed in then invoke that here.
if (typeof obj.preStart === "function") {
obj.preStart();
}
};
/**
* Save BajaScript's Registry Storage. This is automatically called when
* BajaScript stops.
*/
baja.save = function () {
// If available, save registry information to storage
if (bsRegStorage) {
baja.registry.saveToStorage(bsRegStorage);
}
};
/**
* Stop BajaScript.
*
* This method should be called when the page running BajaScript is
* unloaded. This will make a network call to stop BajaScript's session.
*
* @see baja.start
* @see baja.started
* @see baja.preStop
*
* @param {Object|Function} [obj] the Object Literal for the method's arguments or a function
* to be called once BajaScript has stopped.
* @param {Function} [obj.stopped] called once BajaScript has stopped.
* @param {Function} [obj.preStop] called just before BajaScript has stopped.
*
* @example
* <caption>
* This method takes a <code>stopped</code> function or an object
* literal.
* </caption>
*
* baja.stop(function () {
* // Called once stop has completed
* });
*
* //...or this can be invoked via an Object Literal...
*
* baja.stop({
* stopped: function () {
* // Called once stop has completed
* }
* });
*/
baja.stop = function (obj) {
try {
// Don't allow stop to happen twice
if (bsStopped) {
return;
}
// Flag up that we're in the middle of stopping BajaScript...
bsStopping = true;
baja.save();
obj = objectify(obj, "stopped");
if (typeof obj.preStop === "function") {
baja.preStop(obj.preStop);
}
// Call preStop callbacks
if (bsPreStopCallbacks) {
var i;
for (i = 0; i < bsPreStopCallbacks.length; ++i) {
try {
bsPreStopCallbacks[i].call(baja);
} catch (err) {
baja.error(err);
}
}
bsPreStopCallbacks = null;
}
// Stop all registered timers
var id;
for (id in bsClockTimeouts) {
if (bsClockTimeouts.hasOwnProperty(id)) {
bsClockTimeouts[id].cancel();
}
}
for (id in bsClockIntervals) {
if (bsClockIntervals.hasOwnProperty(id)) {
bsClockIntervals[id].cancel();
}
}
// These should be empty but we'll recreate them anyway
bsClockTimeouts = {};
bsClockIntervals = {};
// Stop Comms Engine
baja.comm.stop(obj);
} finally {
// Signal that BajaScript has fully stopped
bsStopped = true;
}
};
/**
* Returns the user name the user is currently logged in with. Note that
* it is not SlotPath-escaped; remember to escape it if retrieving the
* user from the UserService by slot name.
*
* @returns {String} the user name.
*/
baja.getUserName = function () {
return bsUserName;
};
/**
* Returns language code for the user.
*
* @returns {String} the language character code.
*/
baja.getLanguage = function () {
return bsLang;
};
/**
* Return the user home.
*
* @returns {baja.Ord}
*/
baja.getUserHome = function () {
return baja.Ord.make(bsUserHome);
};
/**
* Return the user's default time format pattern.
*
* @returns {String}
*/
baja.getTimeFormatPattern = function () {
return bsTimeFormat;
};
/**
* Return the user's configured unit conversion.
* @returns {number} an ordinal corresponding to a `BUnitConversion` enum
*/
baja.getUnitConversion = function () {
return bsUnitConversion;
};
/**
* Return true if any client Errors are also being logged in the Server.
*
* Please note, since BOX and HTTP Errors are technically from the Server,
* these do not constitute as client Errors.
*
* @returns {Boolean}
*/
baja.isLogClientErrorsInServer = function () {
return bsLogClientErrorsInServer;
};
/**
* Initialize BajaScript from System Properties.
*
* @private
*
* @param {Object} props System Properties loaded from Server.
*/
baja.initFromSysProps = function (props) {
const { requireHttps } = props;
// Record system properties...
bsUserName = props.userName;
bsLang = props.lang;
bsUserHome = props.userHome;
bsTimeFormat = props.timeFormat;
bsUnitConversion = props.unitConversion;
bsLogClientErrorsInServer = props.logClientErrors || false;
baja.$requireHttps = typeof requireHttps === 'boolean' ? requireHttps : true;
// Bail if web storage isn't enabled
if (!props.enableWebStorage) {
// Free up the reference to original loaded data
bsPreLoadRegStorage = null;
// If Storage isn't enabled then try clearing it anyway
baja.registry.clearStorage();
return;
}
// logan's words from NCCB-56730: "no reason for strictArg when you're not developing"
// ...unless it's to check incorrect args that were not caught during development (not using grunt watch)
// bajaUtils.$setValidateArgs(props.webdev);
if (bsPreLoadRegStorage && bsPreLoadRegStorage !== baja.$bsRegStorage) {
// If any of this information has changed then we need to wipe the registry storage and start again...
if (bsPreLoadRegStorage.lang !== props.lang ||
bsPreLoadRegStorage.version !== baja.version ||
bsPreLoadRegStorage.regLastBuildTime !== props.regLastBuildTime ||
bsPreLoadRegStorage.profile !== props.profile ||
bsPreLoadRegStorage.wdLastModified !== props.wdLastModified) {
bsPreLoadRegStorage = null;
baja.registry.clearStorage();
}
}
if (!bsPreLoadRegStorage) {
bsPreLoadRegStorage = {
lang: props.lang,
version: baja.version,
regLastBuildTime: props.regLastBuildTime,
wdLastModified: props.wdLastModified,
profile: props.profile,
types: {}
};
}
// Now we know we want to use this as the registry storage, assign it accordingly.
bsRegStorage = baja.$bsRegStorage = bsPreLoadRegStorage;
// Free up the reference to original loaded data
bsPreLoadRegStorage = null;
// Submit all Type information into the registry
baja.registry.register(bsRegStorage.types);
muxConfig = props.mux || { enabled: false };
};
var envelopeCounter = 0;
baja.$mux = {
isEnabled: function () { return muxConfig.enabled; },
getMinDelay: function () { return muxConfig.minDelay; },
getMaxDelay: function () { return muxConfig.maxDelay; },
getMaxMessageSize: function () { return muxConfig.maxMessageSize; },
getMaxEnvelopeSize: function () { return muxConfig.maxEnvelopeSize; },
nextEnvelopeId: function () { return envelopeCounter++; }
};
}()); // startAndStop
////////////////////////////////////////////////////////////////
// Types and Registry
////////////////////////////////////////////////////////////////
(function registry() {
// Used to keep track of Types that have already been scanned for type extensions.
// We only need to scan and load a type extension once.
var loadTypeExtPromises = {};
/**
* Return an instance from the Type.
*
* When creating an instance of a Type, this method should always be
* used by preference.
*
* At first this 'dollar' function looks a bit strange. However, much
* like other popular JavaScript libraries, this function has been
* reserved for the most commonly used part of BajaScript: creating
* instances of BajaScript Objects.
*
* @see Type#getInstance
*
* @param {String} typeSpec the Type Specification.
* @returns {baja.Value} an instance from the Type
* @throws {Error} if the given type spec could not be found
* @see baja.Simple#make
*
* @example
* // Create an instance of a NumericWritable Control Component...
* var v = baja.$("control:NumericWritable");
*
* @example
* <caption>
* Please note, if the Type information is invalid or hasn't been
* imported into BajaScript, this will throw an Error. Please use
* <code>baja.importTypes</code> to import Type information into
* BajaScript.
* </caption>
*
* baja.importTypes({
* typeSpecs: ["control:NumericWritable"]
* })
* .then(function () {
* var v = baja.$("control:NumericWritable");
* });
*
* //or use the RequireJS plugin:
*
* require(['baja!control:NumericWritable'], function () {
* var v = baja.$("control:NumericWritable");
* });
*
* @example
* <caption>Create a Complex instance, using an object literal to specify
* its slot values.</caption>
*
* var user = baja.$('baja:User', {
* fullName: 'Santa Claus',
* email: 'kringlek@north.pohl'
* });
*/
baja.$ = function (typeSpec) {
// Get a fully loaded Type
var type = baja.lt(typeSpec);
if (!type) {
throw new Error("Type '" + typeSpec + "' not found");
}
// If only the TypeSpec was specified then just create an instance of that Type
if (arguments.length === 1) {
return type.getInstance();
}
// If more arguments were specified then pass them onto to 'getInstance'
var args = Array.prototype.slice.call(arguments);
args.shift(); // remove the first 'typeSpec argument
return type.getInstance.apply(type, args);
};
/**
* Ensures any Type Extensions are loaded. This requires type information
* to already be loaded into the registry for all requested type specs.
*
* @param {Array} typeSpecs
* @returns {Promise}
*/
function loadTypeExtensions(typeSpecs) {
return bajaPromises.all(typeSpecs.map(doLoadTypeExtension));
}
function doLoadTypeExtension(typeSpec) {
//don't bother trying to load the same type extension twice.
var prom = loadTypeExtPromises[typeSpec];
if (prom) { return prom; }
return (loadTypeExtPromises[typeSpec] = loadTypeExtension(baja.lt(typeSpec)));
}
function loadTypeExtension(type) {
var dependentTypes = {},
dependentsToLoad = [],
t = type;
while (t) {
scanContract(t, dependentTypes);
t = t.getSuperType();
}
delete dependentTypes[type];
// For each scanned type, try to find any type extension dependencies.
Object.keys(dependentTypes).forEach(function (typeSpec) {
var teType = baja.lt(typeSpec).findTypeExtType();
if (teType) {
dependentsToLoad.push(teType);
}
});
return bajaPromises.all([
resolveDependencies(getDependencyGraph(type)),
bajaPromises.all(dependentsToLoad.map(doLoadTypeExtension))
])
.then(function () {
var typeExt = type.$typeExt,
js = typeExt && typeExt.js;
if (!js) { return; }
return doRequire([ js ])
.then(function (c) {
var constructor = c[0];
// Bit of a hack but using a function as a flag means it won't get saved to local storage
type.$typeExtLoaded = baja.ok;
// Register the required Constructor.
baja.registerType(type.getTypeSpec(), function () {
return constructor;
});
});
});
}
function getDependencyGraph(type) {
var teType = type.findTypeExtType(),
typeExt = teType && teType.$typeExt,
dependencyGraph = typeExt && typeExt.dg;
return dependencyGraph;
}
/*
when building up the list of type extensions we need to load, we
actually have to scan through the entire contract of each type to get
everything. this is because the contract of a child slot can declare
a type extension not present in the contract for the parent complex.
for instance: TimeTrigger contract contains DailyTriggerMode, while
AlarmService.escalationTimeTrigger only gives us IntervalTriggerMode.
see NCCB-22020.
*/
function scanContract(type, dependentTypes) {
//check for "true" instead of truthy because bson.scan adds to
//dependentTypes too, but we still need to scan again
if (type && type.hasContract() && dependentTypes[type] !== true) {
dependentTypes[type] = true;
var contract = type.getContract();
baja.bson.scan(contract, dependentTypes);
for (var i = 0; i < contract.length; i++) {
var obj = contract[i];
if (obj.st === 'p') {
scanContract(baja.lt(obj.v.t), dependentTypes);
} else if (obj.st === 'a' && obj.art) {
scanContract(baja.lt(obj.art), dependentTypes);
} else if (obj.st === 't') {
scanContract(baja.lt(obj.tet), dependentTypes);
}
}
scanContract(type.getSuperType(), dependentTypes);
}
}
function retrieveAndRegisterTypes(typeSpecs, batch) {
//TODO: NCCB-49565
let invalidError;
function handleResults(importedTypeData) {
var imported = Object.keys(importedTypeData),
toRegister = null,
errs = null;
imported.forEach(function (typeSpec) {
var obj = importedTypeData[typeSpec];
if (obj.isErr) {
(errs || (errs = {}))[typeSpec] = BoxError.decodeFromServer(obj.c || 'BoxError', obj);
} else {
(toRegister || (toRegister = {}))[typeSpec] = obj;
}
});
if (toRegister) {
baja.registry.register(toRegister, /*updateRegStorageTypes*/true);
}
if (errs) {
invalidError = errs;
typeSpecs.forEach((typeSpec) => {
invalidTypeSpecs[typeSpec] = true;
});
return bajaPromises.reject(errs);
}
return typeSpecs.map(baja.lt);
}
let err = false;
typeSpecs.forEach((typeSpec) => {
if (invalidTypeSpecs[typeSpec]) {
err = true;
}
});
if (err) {
return bajaPromises.reject(invalidError);
}
if (typeSpecs.length) {
var cb = new baja.comm.Callback(null, null, batch);
baja.comm.loadTypes(typeSpecs.map(String), cb);
return cb.promise().then(handleResults);
} else {
return bajaPromises.resolve([]);
}
}
/*
We'll get tons of calls to importTypes from tons of baja! RequireJS
imports in minified files. Allow these calls to batch together to cut
down drastically on network calls.
*/
var muxLoadType = promiseMux({
exec: retrieveAndRegisterTypes,
cache: false,
delay: 30,
deferred: bajaPromises.deferred
});
/**
* Load the Type and Contract information for the given TypeSpecs.
*
* A TypeSpec is a String in the format of `moduleName:typeName` in
* Niagara.
*
* This method may perform a network call if one of the TypeSpecs can't
* be found locally in the Registry or if a Contract needs to be
* retrieved.
*
* If a number of network calls is expected to be made then a
* {@link baja.comm.Batch Batch} object can be passed into this method
* along with a callback. The network call will then be 'batched'
* accordingly.
*
* If the types for a given Web Application are known upfront then
* please specify them in {@link baja.start} instead.
*
* @see baja.start
*
* @param {Object|Array} obj the object literal for the method's
* arguments (or the array of type specs directly)
* @param {Array} [obj.typeSpecs] an array of TypeSpecs to import.
* @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback.
* An array of the newly added Types will be passed into this handler.
* @param {Function} [obj.fail] (Deprecated: use Promise) the fail
* callback.
* @param {baja.comm.Batch} [obj.batch] If defined, any network calls will
* be batched into this object.
* @returns {Promise.<Array.<Type>>} A promise that will be resolved once
* the types have been imported.
*
* @example
* <caption>
* The method can be invoked with an array of TypeSpecs or an object
* literal.
* </caption>
*
* baja.importTypes(["control:NumericWritable", "control:BooleanWritable"]);
*
* // ...or via an Object Literal to specify further options...
*
* baja.importTypes({
* typeSpecs: ["control:NumericWritable", "control:BooleanWritable"],
* batch: batch // If specified, network calls are batched into this object
* })
* .then(function (types) {
* baja.outln('imported all types: ' + types.join());
* })
* .catch(function (errObj) {
* baja.error('some types failed to import');
* Object.keys(errObj).forEach(function (typeSpec) {
* baja.error(typeSpec + ' failed because: ' + errObj[typeSpec]);
* });
* });
*/
baja.importTypes = function (obj) {
obj = objectify(obj, "typeSpecs");
var batch = obj.batch,
cb = new baja.comm.Callback(obj.ok, obj.fail),
typeSpecs = obj.typeSpecs,
loadTypes;
try {
// Only request Types that aren't already present in the registry
var toImport = typeSpecs.filter(function (typeSpec) {
return !baja.registry.$types[typeSpec];
});
//the muxed loadTypes calls cannot use a batch by design, so if the
//caller desires a batch, skip the muxed function and call directly.
//baja.registry.register() will protect against double-registration.
if (batch) {
loadTypes = bajaPromises.resolve(
toImport.length && retrieveAndRegisterTypes(toImport, batch));
} else {
loadTypes = bajaPromises.all(toImport.map(muxLoadType));
}
loadTypes
.then(function () { return loadTypeExtensions(typeSpecs); })
.then(function () { cb.ok(typeSpecs.map(baja.lt)); })
.catch(function (err) { cb.fail(err); });
} catch (err) {
cb.fail(err);
}
return cb.promise();
};
/**
* Resolve a RequireJS module that is known to reside in a JsBuild. The JsBuild dependencies
* will be correctly resolved before resolving the module.
*
* @private
* @param {string} id the RequireJS ID to require
* @param {string|Type} jsBuildType the Type of the `web:JsBuild` that contains this module
* @returns {Promise<*>}
* @since Niagara 4.13
*/
baja.$requireFromJsBuild = function (id, jsBuildType) {
if (!jsBuildType) {
return doRequire([ id ]).then(([ result ]) => result);
}
return baja.importTypes([ String(jsBuildType) ])
.then(([ jsBuildType ]) => {
if (!jsBuildType.is('web:JsBuild')) {
throw new Error('JsBuild required');
}
return jsBuildType.resolveJsResource(id);
});
};
/**
* Returns a Type. The Type **must** have already been imported first, via a
* call to `baja.importTypes` or using a `baja!` RequireJS import. If the
* type has not yet been loaded, `null` will be returned.
*
* @see baja.importTypes
*
* @param {String} typeSpec the type spec of the type we're interested
* in (`moduleName:typeName`).
* @returns {Type} the Type for the given type spec or null if the Type
* can't be found locally.
*
* @example
* // Ensure the Type information is locally available.
* baja.importTypes({
* typeSpecs: ["foo:Boo"]
* })
* .then(function () {
* var type = baja.lt("foo:Boo");
* });
*/
baja.lt = function (typeSpec) {
var types = baja.registry.$types;
return types.hasOwnProperty(typeSpec) ? types[typeSpec] : null;
};
/**
* Find a Type's Constructor and return it. If the Type hasn't been
* Properly loaded then return null.
*
* @param {String} typeSpec the type spec of the type we're interested
* in (`moduleName:typeName`).
* @returns {Function} the Constructor for the Type or null if nothing
* is found.
*/
baja.findCtor = function (typeSpec) {
var t = baja.lt(typeSpec);
return t ? t.findCtor() : null;
};
(function bajaRegistryNamespace() {
// BajaScript Registry
var bsRegistryOrdTypes = {},
bsRegistrySimples = {},
bsRegistryCtors = {};
/**
* A BajaScript Type.
*
* This Constructor shouldn't be invoked directly.
*
* Type is an inner class. To access a Type please use {@link baja.lt}.
*
* @class
* @alias Type
* @extends baja.BaseBajaObj
* @inner
* @param {String} typeSpec
* @param {String} superType
* @param {Array.<String>} interfaces
* @param {Object} data
*/
var Type = function (typeSpec,
superType,
interfaces,
data) {
// TODO: Never store transient Types in WebStorage
this.$typeSpec = typeSpec;
this.$superType = superType;
this.$interfaces = interfaces || [];
this.$isAbstract = !!data.a;
this.$isInterface = !!data.i;
this.$contract = bajaDef(data.c, null);
this.$isTransient = !!data.t;
this.$iconStr = bajaDef(data.ic, null);
this.$isValue = !!data.isv;
this.$isSimple = !!data.iss;
this.$isSingleton = !!data.isg;
this.$isNumber = !!data.isn;
this.$isComplex = !!data.isx;
this.$isComponent = !!data.isc;
this.$isLink = !!data.isl;
this.$isAction = !!data.isa;
this.$isTopic = !!data.ist;
this.$isFrozenEnum = !!data.ise;
this.$isOrdScheme = !!data.os;
this.$typeExt = bajaDef(data.te, null);
this.$js = bajaDef(data.js, null);
this.$enc = data.enc;
this.$isBlackListed = data.bl;
this.$isSensitive = data.sns;
this.$getSelf = () => this;
};
typeCtor = Type;
subclass(Type, BaseBajaObj);
/**
* Test for equality.
*
* @param obj value to test for equality.
* @returns {Boolean}
*/
Type.prototype.equals = function (obj) {
if (obj === undefined || obj === null) {
return false;
}
if (obj.constructor !== Type) {
return false;
}
return obj.$typeSpec === this.$typeSpec;
};
/**
* Return the Module Name for the Type.
*
* @returns {String} module name.
*/
Type.prototype.getModuleName = function () {
return this.$typeSpec.split(":")[0];
};
/**
* Return the Type Name for the Type.
*
* @returns {String} type name.
*/
Type.prototype.getTypeName = function () {
return this.$typeSpec.split(":")[1];
};
/**
* Return the full Type Specification for the Type (`moduleName:typeName`).
*
* @returns {String} type spec.
*/
Type.prototype.getTypeSpec = function () {
return this.$typeSpec;
};
/**
* If this Type (or a super Type) has a Type Extension then return the
* Type that has the registered Type Extension information.
*
* @private
*
* @returns {Type} a Type that has a Type Extension (otherwise null if nothing can be found).
*/
Type.prototype.findTypeExtType = function () {
var that = this,
t = that,
cachedTeType = null;
// If not undefined then we should have a cached search result
if (that.$cachedTeType !== undefined) {
return that.$cachedTeType;
}
// Create a list of all the Super Types
while (t) {
if (t.$typeExt) {
cachedTeType = t;
break;
}
t = t.$superType;
}
// Cache the result for optimization
that.$cachedTeType = cachedTeType;
// Return the Type that has the Type Extension that needs to be loaded
return cachedTeType;
};
/**
* Return the nearest JavaScript Constructor for the given Type.
*
* @private
*
* @returns {Function} the nearest JavaScript Constructor (or null if none can be found).
*/
Type.prototype.findCtor = function () {
var t = this, // Type used in iteration
Ct = null;
// Go up Super types until we find a valid constructor
while (t) {
// Create the new Type if there's a Constructor associated with it
if (bsRegistryCtors.hasOwnProperty(t.getTypeSpec())) {
Ct = bsRegistryCtors[t.getTypeSpec()];
break;
} else {
t = t.$superType;
}
}
return Ct;
};
/**
* Return an instance of the Type.
*
* A Type may have an Function Constructor associated with it. If a Constructor
* is found with this Type, it is used to return an instance.
*
* If a Constructor can't be found on this Type, then the Super Types
* are inspected and the first Constructor found is used instead.
* This provides an elegant 'dynamic typing' mechanism whereby a
* Constructor is not needed for every single Type.
*
* If the Type is a concrete `Simple` or `Singleton`, then the
* `DEFAULT` property on the constructor is returned.
*
* @throws {Error} if an instance of the Type can't be created
* (e.g. if the Type is an interface, is abstract, or if no
* constructor can be found).
* @returns instance of Type.
*/
Type.prototype.getInstance = function (arg) {
var that = this,
typeSpec = that.getTypeSpec(),
def,
Ct,
teType,
o;
if (that.$isInterface) {
throw new Error("Cannot call 'getInstance' on an Interface Type: " + typeSpec);
}
if (that.$isAbstract) {
throw new Error("Cannot call 'getInstance' on an Abstract Type: " + typeSpec);
}
// If this is a Simple then check to see if this Type as a default instance on its Constructor, if so then return it
// If there isn't a Constructor then default to baja.DefaultSimple
if ((that.isSimple() || that.isSingleton()) && bsRegistryCtors.hasOwnProperty(typeSpec)) {
def = bsRegistryCtors[typeSpec].DEFAULT;
// If there were any arguments then attempt to use then in the Simple's make method
if (arguments.length) {
if (arguments.length === 1 && typeof arg === 'string') {
return def.decodeFromString(arg);
} else {
return def.make.apply(def, arguments);
}
} else {
return def;
}
}
// If we have a cached version of a Simple then return it. This is used since there may be
// DefaultSimples and FrozenEnums out there that don't have their own Constructor but are still immutable.
// Hence we still have an internal caching mechanism for them as a back up.
if (bsRegistrySimples.hasOwnProperty(typeSpec)) {
def = bsRegistrySimples[typeSpec];
// If there are arguments specified then use them in the Simple's make method
return arguments.length === 0 ? def : def.make.apply(def, arguments);
}
// Find any Type Extension and make sure it's loaded
teType = that.findTypeExtType();
if (teType && !teType.$typeExtLoaded) {
throw new Error("Type Extension is not loaded: " + typeSpec);
}
// Find the Constructor
Ct = that.findCtor();
// Throw error if we can't decode this properly. Usually this means we need to rebuild the common type library!
if (!Ct) {
//this intermittently fails for StatusValue and subtypes and i have
//no idea why.
//TODO: remove this after solving the problem.
//debugger;
throw new Error("Could not find JS Constructor for Type: " + typeSpec);
}
// Otherwise just default to creating a new Type
if (that.isSimple() && !arguments.length) {
//for Simple no-arg (baja.$(typeSpec)), use the DEFAULT string encoding.
arg = that.$enc;
}
o = new Ct(arg);
// Update the Type on the new instance if necessary
if (typeSpec !== o.getType().getTypeSpec()) {
// always have getType be instance-equal for the same type. this helps
// deepEquals implementations etc. work on DefaultSimples.
o.getType = that.$getSelf;
}
// Apply the Contract to the new instance if there's one available.
// This will load the default frozen Slots for the given Type
if (that.isComplex() || that.isFrozenEnum()) {
if (that.isComplex()) {
// Decode Complex BSON for all of the Slots
baja.bson.decodeComplexContract(that, o);
} else if (that.isFrozenEnum()) {
// Get default ordinal for Enum and assign it to the FrozenEnum instance
var ordinals = that.getOrdinals();
if (ordinals.length > 0) {
o.$ordinal = that.getDefaultOrdinal();
}
}
// Also pass any start up arguments into Contract Committed
o.contractCommitted(arg);
}
if (that.isSimple()) {
if (!arguments.length) {
// If this is a simple then cache the default instance
bsRegistrySimples[typeSpec] = o;
} else {
// If there are arguments specified then use them with the Simple's make method
o = o.make.apply(o, arguments);
}
}
return o;
};
/**
* Test if one Type is another.
*
* @param {String|Type} type this can be an instance of a Type object
* or a String type specification (`module:typeName`). When passing a string,
* it's not necessary to import the Type before calling this function.
* @returns {Boolean} true if this Type polymorphically matches the other.
*/
Type.prototype.is = function (type) {
if (type.constructor === String) {
type = baja.lt(type);
if (!type) {
return false;
}
}
// Test Type against this
if (this.$typeSpec === type.$typeSpec) {
return true;
}
// Can only perform this test if the type passed in is an interface
var i;
if (type.$isInterface) {
// Test Type against any interfaces
for (i = 0; i < this.$interfaces.length; ++i) {
if (this.$interfaces[i].is(type)) {
return true;
}
}
} else if (type.$isSimple && !this.$isSimple) {
return false;
}
if (type.$isComponent && !this.$isComponent) {
return false;
}
if (type.$isComplex && !this.$isComplex) {
return false;
}
// Test Type against any super Types
if (this.$superType) {
return this.$superType.is(type);
}
return false;
};
/**
* Return true if the Type is a Value.
*
* @returns {Boolean}
*/
Type.prototype.isValue = function () {
return this.$isValue;
};
/**
* Return true if the Type is a Simple.
*
* @returns {Boolean}
*/
Type.prototype.isSimple = function () {
return this.$isSimple;
};
/**
* Return true if the Type is a Singleton.
*
* @returns {Boolean}
*/
Type.prototype.isSingleton = function () {
return this.$isSingleton;
};
/**
* Return true if the Type is a Number.
*
* @returns {Boolean}
*/
Type.prototype.isNumber = function () {
return this.$isNumber;
};
/**
* Return true if the Type is a Complex.
*
* @returns {Boolean}
*/
Type.prototype.isComplex = function () {
return this.$isComplex;
};
/**
* Return true if the Type is a Component.
*
* @returns {Boolean}
*/
Type.prototype.isComponent = function () {
return this.$isComponent;
};
/**
* Return true if the Type is a Struct.
*
* @returns {Boolean}
*/
Type.prototype.isStruct = function () {
return this.isComplex() && !this.isComponent();
};
/**
* Return true if the Type is a Link.
*
* @returns {Boolean}
*/
Type.prototype.isLink = function () {
return this.$isLink;
};
/**
* Return true if the Type is a baja:Action.
*
* @returns {Boolean}
*/
Type.prototype.isAction = function () {
return this.$isAction;
};
/**
* Return true if the Type is a baja:Topic.
*
* @returns {Boolean}
*/
Type.prototype.isTopic = function () {
return this.$isTopic;
};
/**
* Return true if the Type is a baja:FrozenEnum.
*
* @returns {Boolean}
*/
Type.prototype.isFrozenEnum = function () {
return this.$isFrozenEnum;
};
/**
* Return true if the Type is a baja:OrdScheme.
*
* @returns {Boolean}
*/
Type.prototype.isOrdScheme = function () {
return this.$isOrdScheme;
};
/**
* Return the Super Type.
*
* @returns {Type} Super Type or null if not available
*/
Type.prototype.getSuperType = function () {
return this.$superType;
};
/**
* Return an array of interfaces Types implemented by this Type.
*
* @returns {Array} an array of interface types (all Type)
*/
Type.prototype.getInterfaces = function () {
return this.$interfaces.slice(0); // Return copy of array
};
/**
* Return true if Type is Abstract.
*
* @returns {Boolean}
*/
Type.prototype.isAbstract = function () {
return this.$isAbstract;
};
/**
* Return true if Type is an Interface.
*
* @returns {Boolean}
*/
Type.prototype.isInterface = function () {
return this.$isInterface;
};
/**
* Return true if Type is transient.
*
* @returns {Boolean}
*/
Type.prototype.isTransient = function () {
return this.$isTransient;
};
/**
* Return type spec as toString (`moduleName:typeName`).
*
* @returns {String} type spec
*/
Type.prototype.toString = function () {
return this.getTypeSpec();
};
/**
* Return the Contract for the Type.
*
* @private
*
* @returns Contract
*/
Type.prototype.getContract = function () {
return this.$contract;
};
/**
* Return true if the Type has a Contract.
*
* @private
*
* @returns {Boolean} true if the Type has a Contract.
*/
Type.prototype.hasContract = function () {
return this.$contract !== null;
};
/**
* Return the Type's Icon.
*
* @returns {baja.Icon}
*/
Type.prototype.getIcon = function () {
if (this.$icon) {
return this.$icon;
}
this.$icon = this.$iconStr ? baja.Icon.DEFAULT.decodeFromString(this.$iconStr) : baja.Icon.getStdObjectIcon();
return this.$icon;
};
function enumTypeCheck(type) {
if (!type.isFrozenEnum()) {
throw new Error("Type must be a FrozenEnum");
}
}
/**
* Returns the ordinals for a Type that maps to a FrozenEnum.
*
* @private
*
* @throws error if Type is not a FrozenEnum or if Contract can't be loaded.
* @returns {Array} array of ordinals for frozen enum.
*/
Type.prototype.getOrdinals = function () {
enumTypeCheck(this);
var ordinals = [], i;
for (i = 0; i < this.$contract.length; ++i) {
ordinals.push(this.$contract[i].o);
}
return ordinals;
};
/**
* Returns whether the specified ordinal exists for this FrozenEnum Type.
*
* @private
*
* @param {Number} ordinal
* @throws {Error} if Type is not a FrozenEnum or if Contract can't be loaded.
* @returns {Boolean} true if the ordinal number exists in this FrozenEnum Type.
*/
Type.prototype.isOrdinal = function (ordinal) {
enumTypeCheck(this);
var contract = this.$contract, i;
// Lazily generate the byOrdinal map
if (this.$byOrdinal === undefined) {
this.$byOrdinal = {};
for (i = 0; i < contract.length; ++i) {
this.$byOrdinal[contract[i].o] = {
o: contract[i].o,
t: contract[i].t,
dt: contract[i].dt,
d: contract[i].d
};
}
}
return this.$byOrdinal.hasOwnProperty(ordinal);
};
/**
* Returns the tag for the ordinal (this Type has to map to a FrozenEnum).
*
* @private
*
* @param {Number} ordinal
* @throws {Error} if Type is not a FrozenEnum, if Contract can't be loaded or if the ordinal doesn't exist.
* @returns {String} the tag for the ordinal.
*/
Type.prototype.getTag = function (ordinal) {
enumTypeCheck(this);
if (!this.isOrdinal(ordinal)) {
throw new Error("No tag for ordinal: " + ordinal + " in: " +
this.getTypeSpec());
}
return this.$byOrdinal[ordinal].t;
};
/**
* Returns the display tag for the ordinal (this Type has to map to a FrozenEnum).
*
* @private
*
* @param {Number} ordinal
* @throws {Error} if Type is not a FrozenEnum, if Contract can't be loaded or if the ordinal doesn't exist.
* @returns {String} the display tag for the ordinal.
*/
Type.prototype.getDisplayTag = function (ordinal) {
enumTypeCheck(this);
if (!this.isOrdinal(ordinal)) {
throw new Error("No display tag for ordinal: " + ordinal + " in: " +
this.getTypeSpec());
}
return this.$byOrdinal[ordinal].dt;
};
/**
* Returns the default Ordinal for the enul (this Type has to map to a FrozenEnum).
*
* @private
*
* @returns {String} the default ordinal if specified or 0
*/
Type.prototype.getDefaultOrdinal = function () {
enumTypeCheck(this);
let contract = this.$contract;
var ordinal = 0;
for (var i = 0; i < contract.length; ++i) {
if (contract[i].d) {
ordinal = contract[i].o;
break;
}
}
return ordinal;
};
/**
* Resolves to the type display name for this type.
*
* Attempts to look up the lexicon value for the given type if context is
* provided. If not found, the display name is given as the type name with
* spaces added between words based upon capital letters.
*
* If no context provided, it will return the friendly version of the type
* name.
*
* @param {Object} [cx]
* @returns {Promise.<string>|string}
*/
Type.prototype.getDisplayName = function (cx) {
var module = this.getModuleName(),
name = this.getTypeName();
if (cx) {
return lexjs.module(module)
.then(function (lex) {
return lex.get(name + '.displayName') || toFriendly(name);
}).catch(function () { return toFriendly(name); });
} else {
return toFriendly(name);
}
};
/**
* Returns whether the specified tag exists for this FrozenEnum Type.
*
* @private
*
* @param {String} tag
* @throws {Error} if Type is not a FrozenEnum or if Contract can't be loaded.
* @returns {Boolean} true if the tag exists.
*/
Type.prototype.isTag = function (tag) {
enumTypeCheck(this);
var contract = this.$contract, i;
// Lazily generate the byTag map
if (this.$byTag === undefined) {
this.$byTag = {};
for (i = 0; i < contract.length; ++i) {
this.$byTag[contract[i].t] = {
o: contract[i].o,
t: contract[i].t,
dt: contract[i].dt,
d: contract[i].d
};
}
}
return this.$byTag.hasOwnProperty(tag);
};
/**
* Returns the ordinal for the tag (providing the Type maps to a FrozenEnum).
*
* @private
*
* @param {String} tag
* @throws {Error} if Type is not a FrozenEnum, if Contract can't be loaded or the tag doesn't exist.
* @returns {Number} ordinal for the tag.
*/
Type.prototype.tagToOrdinal = function (tag) {
enumTypeCheck(this);
if (!this.isTag(tag)) {
throw new Error("No ordinal tag for tag: " + tag + " in: " +
this.getTypeSpec());
}
return this.$byTag[tag].o;
};
/**
* Returns the EnumRange for the Type (providing the Type maps to a FrozenEnum).
*
* @private
*
* @see baja.EnumRange
*
* @throws {Error} if Type is not a FrozenEnum, if Contract can't be loaded or the tag doesn't exist.
* @returns {baja.EnumRange} the enum range.
*/
Type.prototype.getRange = function () {
enumTypeCheck(this);
if (this.$range === undefined) {
this.$range = baja.EnumRange.make(this);
}
return this.$range;
};
/**
* Returns the FrozenEnum for the Type (providing the Type maps to a FrozenEnum).
*
* @private
*
* @see baja.EnumRange
*
* @param {String|Number} arg the tag or ordinal for the frozen enum to return.
* @throws {Error} if Type is not a FrozenEnum, if Contract can't be loaded or the tag or ordinal doesn't exist.
* @returns {FrozenEnum} the frozen enumeration for the tag or ordinal.
*/
Type.prototype.getFrozenEnum = function (arg) {
enumTypeCheck(this);
var ordinal = 0, fe;
// Set the ordinal depending on the argument...
if (typeof arg === "string") {
// Look up the ordinal if the tag was passed in
ordinal = this.tagToOrdinal(arg);
} else if (typeof arg === "number" && this.isOrdinal(arg)) {
// Validate the ordinal that was passed in
ordinal = arg;
} else {
throw new Error("Invalid argument for FrozenEnum creation");
}
// Lazily create cache of frozen enum instances
if (this.$enums === undefined) {
this.$enums = {};
}
// Look up enum with ordinal in simples Cache
if (this.$enums.hasOwnProperty(ordinal)) {
fe = this.$enums[ordinal];
} else {
// Create instance and set up type access
fe = this.getInstance();
// If the ordinal differs then make a clone of the object
if (fe.getOrdinal() !== ordinal) {
var newFe = new fe.constructor();
newFe.getType = fe.getType;
fe = newFe;
fe.$ordinal = ordinal;
}
// Cache the frozen enum in the Type
this.$enums[ordinal] = fe;
}
return fe;
};
/**
* Return true if this Type has a constructor directly associated with it.
*
* @private
*
* @returns {Boolean}
*/
Type.prototype.hasConstructor = function () {
return bsRegistryCtors.hasOwnProperty(this.$typeSpec);
};
/**
* Return true if this type implements the BIJavaScript interface.
*
* @private
*
* @returns {Boolean} Returns true if this is a JavaScript object.
*/
Type.prototype.isJs = function () {
return !!this.$js;
};
/**
* Return the RequireJS module Id for the JavaScript Type. If no id
* can be found then an empty string is returned.
*
* @private
*
* @return {String} Returns the Id.
*/
Type.prototype.getJsId = function () {
return this.isJs() ? this.$js.id : "";
};
/**
* Return the JavaScript dependencies for this Type. If no dependencies
* can be found then an empty array is returned. Each dependency is
* the corresponding 'id' that needs to be loaded before this Type's id
* can be successfully loaded.
*
* @private
*
* @return {Array.<Array.<String>>} An array of arrays of JavaScript dependencies. The
* dependencies in each array (starting at index 0) can be resolved concurrently without
* having any unsatisfied dependencies.
*/
Type.prototype.getJsDependencies = function () {
return (this.isJs() && this.$js.dg) || [];
};
/**
* Resolves all JS dependencies this Type knows about, and then resolves the requested
* RequireJS resource.
*
* @private
* @param {string} [id] if given, resolve this RequireJS module. If not given, require this
* Type's own RequireJS module.
* @returns {Promise.<*>}
* @since Niagara 4.13
*/
Type.prototype.resolveJsResource = function (id) {
return (this.$rjsPromise || (this.$rjsPromise = resolveDependencies(this.getJsDependencies())))
.then(() => doRequire([ id || this.getJsId() ]))
.then(([ result ]) => result);
};
/**
* Type Registration.
*
* Registers the constructor with the given TypeSpec. This method is
* used to extend BajaScript and associate JavaScript constructors with
* Niagara Types.
*
* This method also performs checks on the given constructor to ensure it's
* suitable for the given Type.
*
* @private
*
* @param {String} typeSpec the TypeSpec we want to register with.
* @param {Function} func the function that will return the Constructor to be associated with the TypeSpec.
* @returns {Function} Constructor.
*/
baja.registerType = function (typeSpec, func) {
strictAllArgs([ typeSpec, func ], [ String, Function ]);
var Ctor = func.call(baja);
// Register the TypeSpec with this Constructor function
bsRegistryCtors[typeSpec] = Ctor;
// Please note, the Constructor is now lazily validated when the Type information
// is registered with BajaScript.
// Set up default type access
var type;
Ctor.prototype.getType = function () {
// Lazily load Type information
type = type || baja.lt(typeSpec);
if (type === null && baja.isStarted()) {
throw new Error(`You have attempted to instantiate ${ typeSpec } without importing it first - please require baja!${ typeSpec } or use baja.importTypes.`);
}
return type;
};
return Ctor;
};
/**
* Storage utility functions.
*
* The baja registry, and, occasionally, other BajaScript-related data,
* needs to get saved into local storage in order to persist across
* BajaScript sessions. In a browser, this will use the `localStorage`
* object.
*
* This namespace should be augmented/overridden by different
* environment implementations to perform the proper operations.
* By default, all functions in this namespace are no-ops.
*
* @namespace
* @private
* @alias baja.storage
* @type {BaseBajaObj}
*/
var storage = baja.storage = new BaseBajaObj();
/**
* Remove one item from local storage. By default, does nothing -
* override to delete the data under the given key.
*
* @abstract
* @private
* @param {String} key the key name of the item to delete.
*/
storage.removeItem = function (key) {
};
/**
* Add an item to local storage. By default, does nothing - override
* to persist the string data under the given key.
*
* @abstract
* @param {String} key
* @param {String} data
*/
storage.setItem = function (key, data) {
};
/**
* Retrieve an item from local storage. By default, returns null -
* override to return a string value retrieved by key.
*
* @abstract
* @param {String} key
* @returns {String}
*/
storage.getItem = function (key) {
return null;
};
/**
* Baja Type Registry.
*
* At the core of BajaScript is a lazily loading Type and Contract
* system. A Contract represents a frozen slot definition for a Complex
* or a set of FrozenEnum ordinals, tags and display tags.
*
* The most commonly used Types are held in a Common Type Library that
* is built into BajaScript to avoid unnecessary network calls.
*
* @namespace
* @alias baja.registry
*/
var registry = baja.registry = new BaseBajaObj();
registry.$types = {
// Add very core Baja Types as these will NEVER be included into any JSON
"baja:Object": new Type("baja:Object", null, [], { a: true }),
"baja:Interface": new Type("baja:Interface", null, [], { a: true, i: true })
};
registry.$modules = {};
/**
* Inspect the object structure and create Type information to add to
* the Registry.
*
* This should only be invoked by Tridium developers and is normally
* used in a network callback.
*
* @private
*
* @param {Object} obj the Object structure holding the Type information.
* @param {Boolean} [updateRegStorageTypes] if true, this will update the Registry
* storage type database. (Contracts held in the
* registry storage database will be updated regardless
* of whether this flag is truthy or not).
*/
registry.register = function (obj, updateRegStorageTypes) {
if (obj === undefined) {
return;
}
var intType = baja.lt("baja:Interface"),
objType = baja.lt("baja:Object"),
typeSpec, // Add Types to Registry database without super or interface information...
data,
newTypeObj = {};
for (typeSpec in obj) {
if (obj.hasOwnProperty(typeSpec)) {
data = obj[typeSpec];
// If the type doesn't exist in the registry then create it...
if (!this.$types.hasOwnProperty(typeSpec)) {
this.$types[typeSpec] = new Type(typeSpec, null, [], data);
// Create object with new information we're processing...
newTypeObj[typeSpec] = data;
// Update local storage if available...
if (updateRegStorageTypes && bsRegStorage) {
// Update web storage (providing Type isn't transient)...
if (!data.t) {
bsRegStorage.types[typeSpec] = data;
}
}
}
}
}
// Now add all super and interface information...
var i,
type;
for (typeSpec in newTypeObj) {
if (newTypeObj.hasOwnProperty(typeSpec)) {
data = newTypeObj[typeSpec];
type = this.$types[typeSpec];
// Skip baja:Interface and baja:Object...
if (type === intType || type === objType) {
continue;
}
// Set up interfaces...
if (data.it && data.it.length > 0) {
for (i = 0; i < data.it.length; ++i) {
type.$interfaces.push(baja.lt(data.it[i]));
}
} else if (data.i) {
// If this is an interface then this must at least extend baja:Interface
type.$interfaces.push(intType);
}
// Set up Super Type for non-interfaces...
if (!data.i) {
if (data.p) {
type.$superType = baja.lt(data.p);
} else {
type.$superType = objType;
}
if (!type.$superType) {
//debugger; //TODO: remove this when i figure out why StatusValue subtypes sometimes have a null supertype
}
}
}
}
// Iterate through newly added Type information
var ctor;
for (typeSpec in newTypeObj) {
if (newTypeObj.hasOwnProperty(typeSpec)) {
data = newTypeObj[typeSpec];
type = baja.lt(typeSpec);
// Lazily do checks on Ctor information associated with Type
if (!type.isAbstract()) {
if (bsRegistryCtors.hasOwnProperty(typeSpec)) {
ctor = bsRegistryCtors[typeSpec];
// Check concrete Simple Types conform
if (type.isSimple()) {
if (ctor.DEFAULT === undefined) {
throw new Error("Concrete Simple implementations must define a DEFAULT instance argument on the Constructor: " + typeSpec);
}
if (typeof ctor.DEFAULT.make !== "function") {
throw new Error("Concrete Simple implementations must define a make method: " + typeSpec);
}
if (typeof ctor.DEFAULT.decodeFromString !== "function") {
throw new Error("Concrete Simple implementations must define a decodeFromString method: " + typeSpec);
}
if (typeof ctor.DEFAULT.encodeToString !== "function") {
throw new Error("Concrete Simple implementations must define a encodeToString method: " + typeSpec);
}
}
// Check concrete Singletons Types conform
if (type.isSingleton()) {
if (ctor.DEFAULT === undefined) {
throw new Error("Concrete Singletons must define a DEFAULT instance argument on the Constructor: " + typeSpec);
}
}
}
// Register ORD schemes...
if (data.os && type.isOrdScheme()) {
bsRegistryOrdTypes[data.os] = type;
}
}
}
}
};
/**
* Return the Type for the given ORD scheme name.
*
* @private
*
* @param {String} schemeId the ORD scheme name.
* @returns the Type for the ORD Scheme (null is returned if a Type can't be found).
*/
registry.getOrdScheme = function (schemeId) {
return bajaDef(bsRegistryOrdTypes[schemeId], null);
};
/**
* Does the Type exist in the BajaScript registry?
*
* This method will not result in any network calls.
*
* @param {String} typeSpec the type specification to query the registry for.
* @returns {Boolean} true if the Type exists in the BajaScript registry.
*/
registry.hasType = function (typeSpec) {
return registry.$types.hasOwnProperty(typeSpec);
};
/**
* Same as {@link baja.lt}.
*
* @function
*
* @see baja.lt
*/
registry.loadType = baja.lt;
/**
* Same as {@link baja.importTypes}.
*
* @function
*
* @see baja.importTypes
*/
registry.importTypes = baja.importTypes;
/**
* Get an array of concrete Types from the given typeInfo. This method
* will make a network call. The type information returned in the
* ok handler may not necessarily have Contract information loaded.
* To then ensure the Contract information for Complex and FrozenEnums
* is loaded, please use {@link baja.importTypes}.
*
* @see baja.importTypes
* @see Type
*
* @param {Object} obj the Object literal for the method's arguments.
* @param {String|Type} obj.type the type or (String type specification
* - module:typeName) to query the registry for.
* @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback.
* When invoked, an array of Types will be passed in as an argument.
* @param {Function} [obj.fail] (Deprecated: use Promise) the fail
* callback.
* @param {baja.comm.Batch} [obj.batch] If defined, the network call
* will be batched into this object.
* @returns {Promise.<Array.<Type>>} a promise that will be resolved
* once the concrete Types have been retrieved.
*
* @example
* <caption>
* This method takes an object literal as an argument.
* </caption>
*
* baja.registry.getConcreteTypes({
* type: "control:ControlPoint",
* batch: batch // If specified, network calls are batched into this object
* })
* .then(function (concreteTypes) {
* baja.outln('got concrete types: ' + concreteTypes.join());
* })
* .catch(function (err) {
* baja.error('failed to retrieve concrete types: ' + err);
* });
*/
registry.getConcreteTypes = function (obj) {
obj = objectify(obj);
var typeSpec;
if (obj.type) {
typeSpec = obj.type.toString();
}
// Ensure we have these arguments
baja.strictAllArgs([ typeSpec ], [ String ]);
var cb = new baja.comm.Callback(obj.ok, obj.fail, obj.batch);
cb.addOk(function (ok, fail, resp) {
const { s: specs, t: types } = resp;
// Register all of the Type information we get back
registry.register(types, /*updateRegStorageTypes*/true);
loadTypeExtensions(specs)
.then(() => ok(specs.map((spec) => baja.lt(spec))))
.catch(fail);
});
// Make the network call
baja.comm.getConcreteTypes(typeSpec, cb);
return cb.promise();
};
/**
* Make a query for an agent list. This can be made against
* a type (i.e. 'type:moduleName:TypeName') or a BObject resolved
* from an ORD (i.e. 'station:|slot:/MyComponent').
*
* If an array of ORDs is passed in the response will resolve with an Object.
* Every key in that object will be an ORD String. Each value in the Object will
* contain an array of Agent Info. An Agent Info object contains a 'typeSpec',
* 'id', 'displayName' and 'icon' String property. If a single
* ORD was passed in then the response will be an array of Agent Infos.
*
* @example
* <caption>
* Get a Type's agent list.
* </caption>
* baja.registry.getAgents("type:moduleName:TypeName")
* .then(function (agentInfos) {
* var i;
* for (i = 0; i < agentInfos.length; ++i) {
* console.log("typeSpec: " + agentInfos[i].typeSpec);
* console.log("id: " + agentInfos[i].id);
* console.log("displayName: " + agentInfos[i].displayName);
* console.log("icon: " + agentInfos[i].icon);
* }
* });
*
* @example
* <caption>
* Get the Agent List from a mounted Component.
* </caption>
* baja.registry.getAgents("station:|slot:/Folder/MyComponent")
* .then(function (agentInfos) {
* var i;
* for (i = 0; i < agentInfos.length; ++i) {
* console.log("typeSpec: " + agentInfos[i].typeSpec);
* console.log("id: " + agentInfos[i].id);
* console.log("displayName: " + agentInfos[i].displayName);
* console.log("icon: " + agentInfos[i].icon);
* }
* });
*
* @param {String|baja.Ord|Array<String|baja.Ord>} ords An ORD or a number of ORDs
* in an array to make an agent list query for.
* @param {Array<String>} [is] An optional list of type specs to make a query against.
* @param {baja.comm.Batch} [batch] If specified, the request is batched.
* @returns {Promise} A promise that will be resolved once completed.
*/
registry.getAgents = function (ords, is, batch) {
var cb = new baja.comm.Callback(baja.ok, baja.fail, batch),
isArray = Array.isArray(ords),
commOrdsArg = [],
ordStr,
respObj = {},
isStr = is ? "," + is.join(",") : "",
regAgents = bsRegStorage && bsRegStorage.agents,
typesToImport = [];
if (!isArray) {
ords = [ ords ];
}
if (!ords.length) {
cb.fail("No ORDs for getAgents");
return cb.promise();
}
for (var i = 0; i < ords.length; ++i) {
ordStr = ords[i] = String(ords[i]);
var agents = regAgents && regAgents[ordStr + isStr];
// Check cache to see if this query has already been made.
if (agents) {
respObj[ordStr] = agents.slice(); //safe copy
// if a child session in an iframe has called getAgents(), then
// the registry could be populated with agent info without
// having called registry.register() in this session.
for (var j = 0; j < agents.length; ++j) {
typesToImport.push(agents[j].typeSpec);
}
} else {
commOrdsArg.push(ordStr);
}
}
cb.addOk(function (ok, fail, resp) {
var prop;
if (resp) {
if (resp.t) {
// Register all of the Type information we get back.
registry.register(resp.t, /*updateRegStorageTypes*/true);
}
// Copy response over. Cache as necessary. Please note, we're caching
// the agent list with the 'is' query parameter as different 'is' arguments will
// yield different results.
for (prop in resp.rp) {
if (resp.rp.hasOwnProperty(prop)) {
respObj[prop] = resp.rp[prop];
// Try to cache any type based queries.
if (bsRegStorage && /^type:\w+:\w+$/.test(prop)) {
bsRegStorage.agents = bsRegStorage.agents || {};
bsRegStorage.agents[prop + isStr] = respObj[prop].slice(); //safe copy
}
}
}
}
// If the call originally passed in an array then pass back an object.
ok(isArray ? respObj : respObj[ords[0]]);
});
if (commOrdsArg.length) {
baja.comm.getAgents({
ords: commOrdsArg,
is: is
}, cb);
} else {
cb.ok();
}
return bajaPromises.all([
cb.promise(),
typesToImport.length && baja.importTypes({ typeSpecs: typesToImport, batch: batch })
])
.then(function (results) {
return results[0];
});
};
var webStorageKeyName = "bajaScriptReg";
/**
* Clear the registry from permanent storage.
*
* @private
*/
registry.clearStorage = function () {
try {
// Always access local storage in try/catch...
storage.removeItem(webStorageKeyName);
} catch (ignore) {
}
};
/**
* Saves the registry to permanent storage.
*
* @private
*
* @param {Object} regStorage the BajaScript registry information to store
*/
registry.saveToStorage = function (regStorage) {
// If available, save any cached registry information to web storage
if (regStorage) {
// Always access local storage in try/catch...
try {
storage.setItem(webStorageKeyName, JSON.stringify(regStorage));
} catch (ignore) {
registry.clearStorage();
}
}
};
/**
* Load Registry Storage information and return it.
*
* @private
* @returns {Object} the loaded storage information, or `null` if the
* storage could not be loaded
*/
registry.loadFromStorage = function () {
var regStorage = null;
// Always access local storage in try/catch...
try {
regStorage = storage.getItem(webStorageKeyName);
// If we have data then parse it
if (regStorage) {
regStorage = JSON.parse(regStorage);
}
} catch (ignore) {
registry.clearStorage();
}
return regStorage;
};
}());
}());
}());
return baja;
});