/**
* @copyright 2015 Tridium, Inc. All Rights Reserved.
* @author Gareth Johnson
*/
/**
* Defines {@link baja.BatchResolve}.
* @module baja/ord/BatchResolve
*/
define([
"bajaScript/comm",
"bajaScript/baja/ord/SlotPath",
"bajaScript/baja/comp/compUtil",
"bajaScript/baja/comm/Callback",
"bajaScript/baja/ord/VirtualPath",
"bajaPromises" ], function (
baja,
SlotPath,
compUtil,
Callback,
VirtualPath,
Promise) {
"use strict";
var subclass = baja.subclass,
callSuper = baja.callSuper,
strictArg = baja.strictArg,
objectify = baja.objectify,
bajaDef = baja.def,
setContextInOkCallback = compUtil.setContextInOkCallback,
setContextInFailCallback = compUtil.setContextInFailCallback,
BaseBajaObj = baja.BaseBajaObj;
/**
* `BatchResolve` is used to resolve a list of ORDs together.
*
* This method should always be used if multiple ORDs need to be resolved at
* the same time.
*
* @class
* @alias baja.BatchResolve
* @extends baja.BaseBajaObj
* @param {Array.<baja.Ord>} ords an array of ORDs to resolve.
*/
var BatchResolve = function BatchResolve(ords) {
callSuper(BatchResolve, this, arguments);
strictArg(ords, Array);
// Ensure ORDs are normalized
var items = [], i;
for (i = 0; i < ords.length; ++i) {
items.push({
ord: baja.Ord.make(ords[i].toString()).normalize(),
target: null
});
}
this.$items = items;
this.$groups = [];
this.$ok = baja.ok;
this.$fail = baja.fail;
this.$resolved = false;
};
subclass(BatchResolve, BaseBajaObj);
/**
* Resolve an array of ORDs.
*
* @param {Object} params parameters object. This may also be the array of
* ORDs directly if no other parameters are required.
* @param {Array.<String|baja.Ord>} [params.ords] the ORDs to resolve
* @param {baja.Object} [params.base] the base Object to resolve the ORDs
* against.
* @param {baja.Subscriber} [params.subscriber] if defined, any mounted
* `Component`s are subscribed using this `Subscriber`.
* @param {Boolean} [params.lease] if defined, any resolved and mounted
* components are leased.
* @param {Number|baja.RelTime} [params.leaseTime] the lease time used for
* leasing `Component`s.
* @returns {Promise} promise to be resolved with a `BatchResolve` instance
*
* @example
* BatchResolve.resolve([ 'station:|slot:/foo', 'station:|slot:/bar' ])
* .then(function (br) {
* var foo = br.get(0), bar = br.get(1);
* });
*/
BatchResolve.resolve = function (params) {
params = objectify(params, 'ords');
return new BatchResolve(params.ords).resolve(params);
};
/**
* Return the number of items in the Batch.
*
* @returns {Number}
*/
BatchResolve.prototype.size = function () {
return this.$items.length;
};
/**
* Return the ORD at the specified index or null if the index is invalid.
*
* @param {Number} index
* @returns {baja.Ord}
*/
BatchResolve.prototype.getOrd = function (index) {
strictArg(index, Number);
var t = this.$items[index];
return t ? t.ord : null;
};
/**
* Return true if the ORD at the specified index has successfully resolved.
*
* @param {Number} index
* @returns {Boolean}
*/
BatchResolve.prototype.isResolved = function (index) {
strictArg(index, Number);
var t = this.$items[index];
return !!(t && t.target);
};
/**
* Return the error for a particular ORD that failed to resolve or null for no error.
*
* @param {Number} index
* @returns {Error} the error or null if no error.
*/
BatchResolve.prototype.getFail = function (index) {
strictArg(index, Number);
var t = this.$items[index];
return t && t.fail ? t.fail : null;
};
/**
* Return the ORD Target at the specified index.
*
* If the ORD failed to resolve, an error will be thrown.
*
* @param {Number} index
* @returns {module:baja/ord/OrdTarget} the ORD Target
* @throws {Error} thrown if ORD failed to resolve
*/
BatchResolve.prototype.getTarget = function (index) {
strictArg(index, Number);
var t = this.$items[index];
if (!t || !t.target) {
throw t && t.fail ? t.fail : new Error("Unresolved ORD");
}
return t.target;
};
/**
* Return an array of resolved ORD Targets.
*
* If any of the ORDs failed to resolve, an error will be thrown.
*
* @returns {Array<module:baja/ord/OrdTarget>} an array of ORD Targets
* @throws {Error} thrown if any ORDs failed to resolve
*/
BatchResolve.prototype.getTargets = function () {
var targets = [], i;
for (i = 0; i < this.$items.length; ++i) {
targets.push(this.getTarget(i));
}
return targets;
};
/**
* Return an array of resolved objects.
*
* If any of the ORDs failed to resolve, an error will be thrown.
*
* @returns {Array.<baja.Object>} an array of objects
* @throws {Error} thrown if any ORDs failed to resolve
*/
BatchResolve.prototype.getTargetObjects = function () {
var objects = [], i;
for (i = 0; i < this.$items.length; ++i) {
objects.push(this.get(i));
}
return objects;
};
/**
* Return the resolved object at the specified index.
*
* If the ORD failed to resolve, an error will be thrown.
*
* @param {Number} index
* @returns {baja.Object} the resolved object
* @throws {Error} thrown if the ORD failed to resolve
*/
BatchResolve.prototype.get = function (index) {
return this.getTarget(index).getObject();
};
/**
* For each resolved target, call the specified function.
*
* If any ORDs failed to resolve, an error will be thrown.
*
* When the function is called, the `this` will be the resolved
* Component's target. The target's object will be passed as a parameter
* into the function.
*
* @param {Function} func
* @throws {Error} thrown if any of the ORDs failed to resolve
*/
BatchResolve.prototype.each = function (func) {
strictArg(func, Function);
var target,
result,
i,
obj;
for (i = 0; i < this.$items.length; ++i) {
target = this.getTarget(i);
try {
obj = target.getComponent() || target.getObject();
result = func.call(obj, target.getObject(), i);
if (result) {
return result;
}
} catch (err) {
baja.error(err);
}
}
};
function endResolve(batchResolve) {
// Called at the very end once everything has resolved
// If there are any unresolved ORDs then fail the callback
for (var i = 0; i < batchResolve.$items.length; ++i) {
if (!batchResolve.$items[i].target) {
batchResolve.$fail(batchResolve.$items[i].fail || new Error("Unresolved ORD"));
break;
}
}
batchResolve.$ok();
}
function isSubscribable(comp) {
return comp && comp.isMounted() && comp.getComponentSpace().hasCallbacks();
}
function batched(func, batch) {
batch = batch || new baja.comm.Batch();
var result = func(batch);
batch.commit();
return result;
}
function getSubscribableComponents(batchResolve) {
var comps = [],
groups = batchResolve.$groups;
for (var i = 0; i < groups.length; i++) {
var items = groups[i].items;
for (var j = 0; j < items.length; j++) {
var target = items[j].target;
if (target) {
var c = target.getComponent();
if (isSubscribable(c)) {
comps.push(c);
}
}
}
}
return comps;
}
function subscribeTargets(batchResolve, resolveParams) {
return batched(function (batch) {
var subscriber = resolveParams.subscriber,
lease = resolveParams.lease,
// If there's no lease and no subscriber then nothing to subscribe.
comps = (subscriber || lease) && getSubscribableComponents(batchResolve);
return Promise.resolve(comps && comps.length && Promise.all([
// If we can subscribe then batch up a subscription network call
subscriber && subscriber.subscribe({
comps: comps,
batch: batch
}),
lease && baja.Component.lease({
comps: comps,
time: resolveParams.leaseTime,
batch: batch
})
]));
});
}
function resolveOrds(batchResolve, resolveParams) {
var resolutions = batchResolve.$items.map(function (item) {
// Resolve each ORD to its target (unless it's an unknown ORD whereby we've
// already tried to resolve it earlier)
if (!item.unknown) {
return item.ord.resolve({ base: resolveParams.base })
.then(function (target) { item.target = target; })
.catch(function (err) { item.fail = err; });
}
});
return Promise.all(resolutions);
}
/*
* How deep into the component is the given slot path already loaded?
* Gets the index at which the slot path is exhausted and we need to make
* a network call to finish it. -1 if no network calls are needed.
*/
function getDepthRequestIndex(comp, path) {
var isVirtual = path instanceof baja.VirtualPath,
nameAtDepth,
slot,
x;
// Find out what exists and doesn't exist
for (x = 0; x < path.depth(); ++x) {
nameAtDepth = path.nameAt(x);
if (isVirtual) {
nameAtDepth = VirtualPath.toSlotPathName(nameAtDepth);
}
slot = comp.getSlot(nameAtDepth);
// If there's no slot present then we need to try and make a network call for it.
if (slot === null) {
return x;
}
// If the Slot isn't a Property then bail
if (!slot.isProperty()) {
return -1;
}
// If the Property isn't a Component then bail since we're only interested
// in really loading up to a Component
if (!slot.getType().isComponent()) {
return -1;
}
comp = comp.get(slot);
// If the slot is a component but doesn't have a handle then we need to request it.
if (!comp || !comp.getHandle()) {
return x;
}
}
return -1;
}
function addToRequestMap(map, path, depthRequestIndex) {
// Load ops on Slots that don't exist
var slotOrd = "slot:/",
isVirtual = path instanceof baja.VirtualPath,
fullPath,
o,
nameAt,
sn,
i;
for (i = 0; i < path.depth(); i++) {
// If we've gone past the depth we need to request then build up the network
// calls we need to make
nameAt = path.nameAt(i);
o = slotOrd;
sn = isVirtual ? VirtualPath.toSlotPathName(nameAt) : nameAt;
if (i >= depthRequestIndex) {
fullPath = o + "/" + sn;
// Only request the Slot Path if it already isn't going to be requested
if (!map.hasOwnProperty(fullPath)) {
map[fullPath] = { o: o, sn: sn };
}
}
if (i > 0) {
slotOrd += "/";
}
slotOrd += sn;
}
}
function resolveSlotPaths(batchResolve, batch, resolveParams) {
var groups = batchResolve.$groups;
var subscriptions = groups.map(function (group) {
var space = group.space,
items = group.items,
comp = space.getRootComponent(),
slotPathInfo = [],
slotPathInfoMap = {},
subscribeOrds = resolveParams.subscriber ? [] : null,
path, depthRequestIndex;
for (var i = 0; i < items.length; i++) {
path = items[i].slot;
// Skip if no valid SlotPath is available
if (path) {
// Record ORD for possible subscription
if (subscribeOrds) {
subscribeOrds.push(path.toString());
}
depthRequestIndex = getDepthRequestIndex(comp, path);
// If we've got Slots to request then do so
if (depthRequestIndex > -1) {
addToRequestMap(slotPathInfoMap, path, depthRequestIndex);
}
}
}
//assemble network requests into an array for loadSlotPath
baja.iterate(slotPathInfoMap, function (arg) { slotPathInfo.push(arg); });
// Make network request if there are slot paths to load for this Space
if (slotPathInfo.length) {
return Promise.all([
loadSlotPath(space, slotPathInfo, batch),
// Attempt to roll the network subscription call into
// the Slot Path resolution to avoid another network call...
// TODO: subscribing in this way is not ideal. Here we're subscribing Components before they're
// fully loaded into the Proxy Component Space to avoid another network call. This assumes that
// each of these Components will fully load into the Component Space without any problems. This will
// do for now since it's critical to customer's perceptions that BajaScript loads values quickly.
// If any errors do occur (i.e. the values haven't loaded properly), they are flagged up using baja.error.
subscribeOrds && resolveParams.subscriber.$ordSubscribe({
ords: subscribeOrds,
space: space,
batch: batch
})
]);
}
});
return Promise.all(subscriptions);
}
function loadSlotPath(space, slotPathInfo, batch) {
var cb = new Callback(null, null, batch);
space.getCallbacks().loadSlotPath(slotPathInfo, space, cb, /*importAsync*/false);
return cb.promise();
}
function groupComponentSpaceItems(batchResolve) {
// Group Items together by Component Space
var added,
item,
group,
i,
x;
for (i = 0; i < batchResolve.$items.length; ++i) {
item = batchResolve.$items[i];
added = false;
// Skip grouping for Spaces that don't have callbacks
if (!item.space) {
continue;
}
if (!item.space.hasCallbacks()) {
continue;
}
for (x = 0; x < batchResolve.$groups.length; ++x) {
group = batchResolve.$groups[x];
if (String(group.space.getNavOrd()) === String(item.spaceOrd)) {
group.items.push(item);
added = true;
break;
}
}
// If the item isn't added then create a new group for this item
if (!added) {
batchResolve.$groups.push({
space: item.space,
items: [ item ]
});
}
}
}
function resolveComponentSpaces(batchResolve, resolveParams) {
var resolutions = batchResolve.$items.map(function (item) {
var spaceOrd = item.spaceOrd;
if (!spaceOrd) { return; }
var spaceOrdStr = spaceOrd.toString();
// Optimization for local Station Space
if (spaceOrdStr === 'station:' || spaceOrdStr === 'local:|station:') {
item.space = baja.station;
item.spaceOrd = baja.station.getNavOrd();
} else {
return spaceOrd.get({ base: resolveParams.base })
.then(function (value) {
var space;
if (value.getType().is("baja:ComponentSpace")) {
space = item.space = value;
item.spaceOrd = space.getNavOrd();
} else if (value.getType().is("baja:VirtualGateway")) {
// Note: this may result in some network calls to mount the Virtual Component Space
space = item.space = value.getVirtualSpace();
if (!space) {
return value.loadSlots()
.then(function () {
space = item.space = value.getVirtualSpace();
item.spaceOrd = space.getNavOrd();
});
} else {
item.spaceOrd = space.getNavOrd();
}
} else if (value.getType().is("baja:Component")) {
space = item.space = value.getComponentSpace();
item.spaceOrd = space.getNavOrd();
}
})
.catch(ignore);
}
});
return Promise.all(resolutions);
}
function makeAbsSlotPath(query, resolveParams) {
// Create an absolute SlotPath using the base if necessary
var isVirtual = query.getSchemeName() === "virtual",
path = isVirtual ?
new baja.VirtualPath(query.getBody()) :
new SlotPath(query.getBody()),
basePath,
newBody;
// If the path is already absolute then use it
if (path.isAbsolute()) {
return path;
}
// Attempt to merge the ORD with the base to get our Absolute SlotPath
if (resolveParams.base.getType().isComponent() && !isVirtual) {
basePath = resolveParams.base.getSlotPath();
if (basePath !== null) {
newBody = basePath.merge(path);
return new SlotPath(newBody);
}
}
return null;
}
function resolveUnknown(item, resolveParams, batch) {
const { base, subscriber, lease, leaseTime } = resolveParams;
// Batch up the unknown ORD resolution...
var cb = new Callback(null, null, batch);
// Make the network call to resolve the complete ORD Server side
return item.ord.resolve({
cb: cb,
base,
fromBatchResolve: true,
subscriber,
lease,
leaseTime
})
.then(function (target) {
item.target = target;
})
.catch(function (err) {
item.fail = err;
});
}
function ignore(ignore) {}
function processOrds(batchResolve, resolveParams) {
var unknownBatch, unknownResolutions = [];
batchResolve.$items.forEach(function (item) {
var list = item.list = item.ord.parse();
if (!list.isClientResolvable()) {
// unknown ORDs get processed completely server side.
item.unknown = true;
if (!unknownBatch) { unknownBatch = new baja.comm.Batch(); }
return unknownResolutions.push(resolveUnknown(item, resolveParams, unknownBatch));
}
var cursor = list.getCursor(),
foundIndex = -1;
// Work out the ORD just before the virtual, slot or handle scheme
while (cursor.next()) {
var q = cursor.get();
var scheme = q.getSchemeName();
if (scheme === "virtual") {
foundIndex = cursor.getIndex();
item.slot = makeAbsSlotPath(q, resolveParams);
break;
} else if (scheme === "h" && foundIndex === -1) {
foundIndex = cursor.getIndex();
item.h = q.getBody();
} else if (scheme === "slot" && foundIndex === -1) {
foundIndex = cursor.getIndex();
item.slot = makeAbsSlotPath(q, resolveParams);
} else if (scheme === 'hierarchy') {
item.spaceOrd = baja.Ord.make('hierarchy:');
return;
} else if (scheme === 'http' || scheme === 'https') {
return; // no space ord
}
}
// Note down the ORD to the Space
if (foundIndex !== -1) {
item.spaceOrd = baja.Ord.make(list.toString(foundIndex));
// If there's no ORD then just try using the base to resolve the CS.
if (item.spaceOrd === baja.Ord.DEFAULT) {
item.spaceOrd = resolveParams.base.getNavOrd();
}
}
});
//perform the individual steps. we ignore all promise rejections and
//continue trying to resolve whatever we can. at the end, individual ORDs
//will be marked as resolved or not.
return resolveComponentSpaces(batchResolve, resolveParams)
.then(function () {
groupComponentSpaceItems(batchResolve);
// piggy back the slot path resolution off the same batch as the unknown ords
return Promise.all([
batched(function (batch) {
return resolveSlotPaths(batchResolve, batch, resolveParams);
}, unknownBatch),
Promise.all(unknownResolutions)
])
.catch(ignore);
})
.then(function () {
// If we've resolved the SlotPaths from each group then we can finally attempt
// to resolve each ORD. With a bit of luck, this will result in minimal network calls
return resolveOrds(batchResolve, resolveParams).catch(ignore);
})
.then(function () {
return subscribeTargets(batchResolve, resolveParams).catch(ignore);
})
.then(function () {
return endResolve(batchResolve);
});
}
/**
* Batch resolve an array of ORDs.
*
* A Batch Resolve should be used whenever more than one ORD needs to resolved.
*
* Any network calls that result from processing an ORD are always asynchronous.
*
* This method can only be called once per BatchResolve instance.
*
* When using promises, the static `BatchResolve.resolve` function will be
* a little cleaner.
*
* @see baja.Ord
* @see baja.BatchResolve.resolve
*
* @param {Object} [obj] the object literal that contains the method's
* arguments.
* @param {Function} [obj.ok] (Deprecated: use Promise) the ok function called
* once all of the ORDs have been successfully resolved. When the function is
* called, `this` is set to the `BatchResolve` object.
* @param {Function} [obj.fail] (Deprecated: use Promise) the fail function
* called if any of the ORDs fail to resolve. The first error found is pass
* as an argument to this function.
* @param {baja.Object} [obj.base] the base Object to resolve the ORDs against.
* @param {baja.Subscriber} [obj.subscriber] if defined, any mounted
* `Component`s are subscribed using this `Subscriber`.
* @param {Boolean} [obj.lease] if defined, any resolved and mounted
* components are leased.
* @param {Number|baja.RelTime} [obj.leaseTime] the lease time used for
* leasing `Component`s.
* @returns {Promise} a promise that will be resolved once the ORD resolution
* is complete. The argument to the `then` callback will be the `BatchResolve`
* instance.
*
* @example
* var r = new baja.BatchResolve(["station:|slot:/Ramp", "station:|slot:/SineWave"]);
* var sub = new baja.Subscriber(); // Also batch subscribe all resolved Components
*
* r.resolve({ subscriber: sub })
* .then(function () {
* // Get resolved objects
* var obj = r.getTargetObjects();
* })
* .catch(function (err) {
* // Called if any of the ORDs fail to resolve
* });
*
* // Or use the each method (will only be called if all ORDs resolve). Each will
* // be called for each target.
* r.resolve({
* each: function () {
* baja.outln("Resolved: " + this.toPathString());
* },
* subscriber: sub
* });
*/
BatchResolve.prototype.resolve = function (obj) {
obj = objectify(obj);
var cb = new Callback(obj.ok, obj.fail),
that = this;
// Ensure 'this' is Component in callbacks...
setContextInOkCallback(that, cb);
setContextInFailCallback(that, cb);
// If an each function was passed in then call if everything resolves ok.
that.$ok = function () {
if (typeof obj.each === "function") {
try {
that.each(obj.each);
} catch (err) {
baja.error(err);
}
}
cb.ok(that);
};
that.$fail = function (err) {
cb.fail(err);
};
// Can only resolve once
if (that.$resolved) {
that.$fail("Cannot call resolve more than once");
return cb.promise();
}
that.$resolved = true;
// Initialize
obj.base = bajaDef(obj.base, baja.nav.localhost);
// Check the user isn't trying to batch an ORD as this isn't supported
if (obj.batch) {
that.$fail("Cannot batch ORD resolution");
return cb.promise();
}
// Start resolution
if (that.$items.length > 0) {
processOrds(that, {
subscriber: obj.subscriber,
lease: obj.lease,
leaseTime: obj.leaseTime,
base: obj.base
});
} else {
that.$ok();
}
return cb.promise();
};
return BatchResolve;
});