/**
* @copyright 2015 Tridium, Inc. All Rights Reserved.
* @author Gareth Johnson
*/
/**
* Defines {@link baja.Subscriber}.
* @module baja/comp/Subscriber
*/
define(["bajaScript/nav",
"bajaScript/baja/comp/compUtil",
"bajaScript/baja/comm/Callback",
"bajaPromises"], function (
baja,
compUtil,
Callback,
Promise) {
"use strict";
var subclass = baja.subclass,
strictArg = baja.strictArg,
objectify = baja.objectify,
bajaDef = baja.def,
bajaError = baja.error,
BaseBajaObj = baja.BaseBajaObj,
setContextInOkCallback = compUtil.setContextInOkCallback,
setContextInFailCallback = compUtil.setContextInFailCallback,
canUnsubscribe = compUtil.canUnsubscribe;
/**
* Component Subscriber used to subscribe to multiple `Component`s for events.
*
* @class
* @alias baja.Subscriber
* @extends baja.BaseBajaObj
*/
var Subscriber = function Subscriber() {
this.$comps = [];
};
subclass(Subscriber, BaseBajaObj);
// Mix-in the event handlers for baja.Subscriber
baja.event.mixin(Subscriber.prototype);
// The following comments have been added for the benefit of JsDoc Toolkit...
/**
* Attach an Event Handler to this Subscriber.
*
* A Subscriber can be used to subscribe to multiple Components
* in the Station and can be used to listen for Component Events.
*
* An event handler consists of a name and a function. When the
* function is called, 'this' will map to the target Component.
*
* @function baja.Subscriber#attach
*
* @see baja.Component#attach
* @see baja.Subscriber#detach
* @see baja.Subscriber#getHandlers
* @see baja.Subscriber#hasHandlers
* @see baja.Slot
*
* @param {String} event handler name.
* @param {Function} func the event handler function.
*
* @example
* <caption>
* A common use case would be a Property changed event.
* </caption>
*
* // sub is a Subscriber and is subscribed to a Component
* sub.attach("changed", function (prop, cx) {
* if (prop.getName() === "out") {
* baja.outln("The output of the point is: " + this.getOutDisplay());
* }
* });
*
* @example
* <caption>
* An object literal can be used to specify multiple handlers.
* </caption>
*
* var sub = new baja.Subscriber();
* sub.attach({
* changed: function (prop, cx) {
* },
* subscribed: function (cx) {
* }
* });
*
* @example
* <caption>
* Spaces can be used in a name to specify a function for multiple events.
* </caption>
*
* var sub = new baja.Subscriber();
* sub.attach("subscribed changed", function () {
* updateGui(this);
* });
*
* @example
* <caption>
* Here are some examples of the different event handlers that can be
* attached to a Component.
* </caption>
*
* // Property Changed
* sub.attach("changed", function (prop, cx) {
* // prop: the Property that has changed
* // cx: the Context (used internally)
* });
*
* // Property Added
* sub.attach("added", function (prop, cx) {
* // prop: the Property that has been added
* // cx: the Context (used internally)
* });
*
* // Property Removed
* sub.attach("removed", function (prop, val, cx) {
* // prop: the Property that has been removed
* // val: the old value of the Property
* // cx: the Context (used internally)
* });
*
* // Property Renamed
* sub.attach("renamed", function (prop, oldName, cx) {
* // prop: the Property that has been renamed
* // oldName: the old slot name
* // cx: the Context (used internally)
* });
*
* // Dynamic Slots Reordered
* sub.attach("reordered", function (cx) {
* // cx: the Context (used internally)
* });
*
* // Topic Fired
* sub.attach("topicFired", function (topic, event, cx) {
* // topic: the Topic that has been fired
* // event: the Topic event data (can be null)
* // cx: the Context (used internally)
* });
*
* // Slot Flags Changed
* sub.attach("flagsChanged", function (slot, cx) {
* // slot: the slot whose flags have changed
* // cx: the Context (used internally)
* });
*
* // Slot Facets Changed
* sub.attach("facetsChanged", function (slot, cx) {
* // slot: the slot whose facets have changed
* // cx: the Context (used internally)
* });
*
* // Component subscribed
* sub.attach("subscribed", function (cx) {
* // cx: the Context (used internally)
* });
*
* // Component unsubscribed
* sub.attach("unsubscribed", function (cx) {
* // cx: the Context (used internally)
* });
*
* // Component unmounted (called just before Component is removed from parent)
* sub.attach("unmount", function (cx) {
* // cx: the Context (used internally)
* });
*
* // Component renamed in parent
* sub.attach("componentRenamed", function (oldName, cx) {
* // cx: the Context (used internally)
* });
*
* // Component's flags changed in parent
* sub.attach("componentFlagsChanged", function (cx) {
* // cx: the Context (used internally)
* });
*
* // Component's facets changed in parent
* sub.attach("componentFacetsChanged", function (cx) {
* // cx: the Context (used internally)
* });
*
* // Component reordered in parent
* sub.attach("componentReordered", function (cx) {
* // cx: the Context (used internally)
* });
*/
/**
* Detach an Event Handler from the Subscriber.
*
* If no arguments are used with this method then all events are removed.
*
* For a list of all the event handlers, please see {@link baja.Subscriber#attach}.
*
* @function baja.Subscriber#detach
*
* @see baja.Component#attach
* @see baja.Subscriber#attach
* @see baja.Subscriber#getHandlers
* @see baja.Subscriber#hasHandlers
*
* @param {String} [hName] the name of the handler to detach from the Subscriber.
* @param {Function} [func] the function to remove from the Subscriber. It's recommended to supply this just in case
* other scripts have added event handlers.
* @example
* sub.detach("renamed"); // Remove a single handler
* sub.detach("subscribed changed"); // Remove multiple handlers at once
*/
/**
* Return an array of event handlers.
*
* For a list of all the event handlers, please see {@link baja.Subscriber#attach}.
*
* To access multiple handlers, insert a space between the handler names.
*
* @function baja.Subscriber#getHandlers
*
* @see baja.Component#attach
* @see baja.Subscriber#detach
* @see baja.Subscriber#attach
* @see baja.Subscriber#hasHandlers
*
* @param {String} hName the name of the handler
* @returns {Array.<Function>}
*/
/**
* Return true if there any handlers registered for the given handler name.
*
* If no handler name is specified then test to see if there are any handlers
* registered at all.
*
* For a list of all the event handlers, please see {@link baja.Subscriber#attach}.
*
* Multiple handlers can be tested for by using a space character between the names.
*
* @function baja.Subscriber#hasHandlers
*
* @see baja.Component#attach
* @see baja.Subscriber#detach
* @see baja.Subscriber#attach
* @see baja.Subscriber#getHandlers
*
* @param {String} [hName] the name of the handler. If undefined, then see if there are any
* handlers registered at all.
* @returns {Boolean}
*/
/**
* Return an array of the `Component`s currently being
* subscribed to by this `Subscriber`.
*
* @returns {Array.<baja.Component>} a copy of the array of components
* subscribed to by this `Subscriber`.
*/
Subscriber.prototype.getComponents = function () {
return this.$comps.slice();
};
/**
* Return true if the `Subscriber` is empty of subscriptions.
*
* @returns {Boolean} true if the `Subscriber` is empty.
*/
Subscriber.prototype.isEmpty = function () {
return this.$comps.length === 0;
};
/**
* Subscribe a `Component` or a number of `Component`s.
*
* This will put the `Component`s into subscription if they are not already
* subscribed and are mounted. The Subscription will last until the page is
* refreshed or `unsubscribe()` is called.
*
* If the `Component`s are mounted and able to be subscribed, this will result in
* an **asynchronous** network call.
*
* If the `Component`s are mounted in multiple component spaces, there is a slight
* chance for the promise to fail because a single component space subscription
* failed, but `Component`s from other successful component spaces
* could still be subscribed. Therefore, in a failure condition, it is not safe to
* assume that none of the `Component`s were successfully subscribed. You can call
* {@link baja.Subscriber#isSubscribed} in such a scenario to determine which
* `Component`s were successfully subscribed and which were not. Furthermore, even
* when a failure condition occurs, you may still need to remember to call
* {@link baja.Subscriber#unsubscribe} due to the scenario just described.
*
* For callbacks, the `this` keyword is set to the `comps` property.
*
* @name baja.Subscriber#subscribe
* @function
*
* @param {Object} [obj] the object literal used for the method's arguments.
* @param {baja.Component|Array} obj.comps The `Component` or array of
* `Component`s to be subscribed.
* @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback.
* Called once the `Component`s have been subscribed.
* @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
* Called if the `Component`s fail to subscribe. Any errors will be passed to
* this function.
* @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
* batched into this object.
* @returns {Promise} a promise that will be resolved once component(s) have
* been subscribed.
*
* @example
* <caption>
* A <code>Component</code> instance, array of <code>Component</code>s
* or an optional object literal can be used.
* </caption>
*
* // Subscribe a single Component
* sub.subscribe(aComp);
*
* // ...or subscribe an array of Components...
* sub.subscribe([aComp1, aComp2]);
*
* // ...or use an object literal for more arguments...
* sub.subscribe({
* comps: [aComp1, aComp2], // Can also just be an singular Component instance
* batch // if defined, any network calls will be batched into this object (optional)
* })
* .then(function () {
* baja.outln('components have been subscribed');
* })
* .catch(function (err) {
* baja.error('some components failed to subscribe: ' + err);
*
* // If the components were mounted in different component spaces,
* // remember to check sub.isSubscribed(comp) to determine which
* // components successfully subscribed and which failed
* });
*/
Subscriber.prototype.subscribe = function (obj) {
obj = objectify(obj, "comps");
var comps = obj.comps,
cb = new Callback(obj.ok, obj.fail, obj.batch),
c,
i,
space,
spaceOrd,
spaces = [],
ordsBySpace = {},
remoteCallRequired = false,
compsToSub = [],
promises = [];
// Ensure 'this' is Component in callbacks...
setContextInOkCallback(comps, cb);
setContextInFailCallback(comps, cb);
try {
// Ensure we have an array of valid Components to subscribe too
if (!(comps instanceof Array)) {
comps = [comps];
}
// Remove references and see if we need a network call
for (i = 0; i < comps.length; ++i) {
c = comps[i];
// Make sure we have a Component
strictArg(c, baja.Component);
if (!c.isMounted()) {
throw new Error("Cannot subscribe unmounted Component!");
}
space = c.getComponentSpace();
spaceOrd = space.getAbsoluteOrd().toString();
if (!ordsBySpace.hasOwnProperty(spaceOrd)) {
spaces.push(space);
ordsBySpace[spaceOrd] = [];
}
// Add to this Subscribers Component array
if (!this.$comps.contains(c)) {
this.$comps.push(c);
}
// Make sure this Subscriber is listed in the Subscribed Component
if (!c.$subs.contains(this)) {
c.$subs.push(this);
}
// See if we need to make any network calls.
if (!c.$subDf || Promise.isRejected(c.$subDf.promise())) {
remoteCallRequired = true;
ordsBySpace[spaceOrd].push("h:" + c.getHandle());
compsToSub.push({
spaceOrd: spaceOrd,
subscribedRemote: false,
comp: c,
df: (c.$subDf = Promise.deferred())
});
}
promises.push(c.$subDf.promise());
}
// When all of these promises are resolved then we're done.
cb.addOk(function (ok, fail) {
Promise.all(promises).then(ok, fail);
});
// If there is nothing to subscribe to at this point then just bail
if (remoteCallRequired && bajaDef(obj.netCall, true)) {
// Signal that each Component has been subscribed
cb.addOk(function (ok, fail, handle, timestamp) {
var i;
for (i = 0; i < compsToSub.length; ++i) {
try {
compsToSub[i].comp.$fw("fwSubscribed");
}
catch (err0) {
bajaError(err0);
}
finally {
try {
compsToSub[i].comp.$lastSubTimestamp = timestamp;
compsToSub[i].df.resolve();
}
catch(err1) {
bajaError(err1);
}
}
}
ok();
});
cb.addFail(function (ok, fail, err) {
var i;
for (i = 0; i < compsToSub.length; ++i) {
if (compsToSub[i].subscribedRemote)
{
try {
compsToSub[i].comp.$fw("fwSubscribed");
}
catch (err0) {
bajaError(err0);
}
finally {
try {
compsToSub[i].df.resolve();
}
catch(err1) {
bajaError(err1);
}
}
} else {
try {
compsToSub[i].df.reject(err);
}
catch (err0) {
bajaError(err0);
}
}
}
fail(err);
});
// Make the network call through the Spaces
var batch = obj.batch || new baja.comm.Batch(),
spacePromises = [];
baja.iterate(spaces, function (sp) {
var spaceAbsoluteOrd = sp.getAbsoluteOrd().toString(),
ordsInSpace = ordsBySpace[spaceAbsoluteOrd];
if (ordsInSpace.length > 0) {
var spaceCb = new Callback(function() {
for (i = 0; i < compsToSub.length; ++i) {
if (compsToSub[i].spaceOrd === spaceAbsoluteOrd) {
compsToSub[i].subscribedRemote = true;
}
}
}, baja.fail, batch);
sp.getCallbacks().subscribe(ordsInSpace, spaceCb);
spacePromises.push(spaceCb.promise());
}
});
Promise.all(spacePromises)
.then(function () { cb.ok(); })
.catch(function (err) { cb.fail(err); });
if (!obj.batch) {
batch.commit();
}
}
else {
cb.ok();
}
}
catch (err) {
cb.fail(err);
}
return cb.promise();
};
/**
* An internal private method for subscribing `Component`s via their ORDs.
* Please note, this method is strictly intended for Tridium developers only!
* Also note that all Component ORDs subscribed using this method must live
* in the same component space.
*
* @private
* @internal
*
* @param {Object} [obj] the object literal used for the method's arguments.
* @param {Array.<String>} obj.ords an Array of `String` ORDs that should resolve to
* `Component`s for subscription.
* @param {baja.ComponentSpace} obj.space the Component Space used for ORD
* resolution. All component ORDs must live in this same space.
* @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback.
* Called once the `Component`s have been subscribed.
* @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
* Called if the `Component`s fail to subscribe. Any errors will be passed to
* this function.
* @param {baja.comm.Batch} [obj.batch] if defined, any network calls will
* be batched into this object.
* @returns {Promise} a promise that will be resolved once the callbacks have
* been invoked.
*/
Subscriber.prototype.$ordSubscribe = function (obj) {
var ords = obj.ords,
space = obj.space,
cb = new Callback(obj.ok, obj.fail, obj.batch),
that = this;
try {
// Ensure these Components are all subscribed
cb.addOk(function(ok, fail, handles, timestamp) {
// Remove references and see if we need a network call
var i,
c,
prevSub;
for (i = 0; i < handles.length; ++i) {
// Attempt to find the Component locally
c = space.findByHandle(handles[i]);
if (c) {
// Mark the Component as subscribed
prevSub = c.isSubscribed();
// Add to this Subscribers Component array
if (!that.$comps.contains(c)) {
that.$comps.push(c);
}
// Make sure this Subscriber is listed in the Subscribed Component
if (!c.$subs.contains(that)) {
c.$subs.push(that);
}
// If this is now subscribed then fire the relevant callback
if (!prevSub) {
try {
c.$fw("fwSubscribed");
c.$lastSubTimestamp = timestamp;
c.$subDf = c.$subDf || Promise.deferred();
c.$subDf.resolve();
}
catch (err) {
bajaError(err);
}
}
}
else {
bajaError("Could not Batch Resolve Subscribe: " + handles[i]);
}
ok();
}
});
// Make the network call through the Space
space.getCallbacks().subscribe(ords, cb);
}
catch (err) {
cb.fail(err);
}
return cb.promise();
};
/**
* Unsubscribe a `Component` or a number of `Component`s.
*
* This will unsubscribe the mounted `Component`s if they are not already
* unsubscribed.
*
* If the `Component`s are able to be unsubscribed, this will result in
* an **asynchronous** network call.
*
* If the `Component`s are mounted in multiple component spaces, there is a slight
* chance for the promise to fail because a single component space
* unsubscription failed, but `Component`s from other component spaces could
* have been successfully unsubscribed.
*
* For callbacks, the 'this' keyword is set to the comps property.
*
* @name baja.Subscriber#unsubscribe
* @function
*
* @param {Object} [obj] the object literal used for the method's arguments.
* @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback.
* Called once the Components have been unsubscribed.
* @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
* Called if the Components fail to subscribe. Any errors will be passed to
* this function.
* @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
* batched into this object.
* @returns {Promise} a promise that will be resolved once the components have
* been unsubscribed.
*
* @example
* <caption>
* A `Component` instance, array of `Component`s or an optional object
* literal can be used to specify the method's arguments.
* </caption>
*
* // Unsubscribe a single Component
* sub.unsubscribe(aComp);
*
* // ...or unsubscribe an array of Components...
* sub.unsubscribe([aComp1, aComp2]);
*
* // ...or use an object literal for more arguments...
* sub.unsubscribe({
* comps: [aComp1, aComp2], // Can also just be an singular Component instance
* batch // if defined, any network calls will be batched into this object (optional)
* })
* .then(function () {
* baja.outln('components have been unsubscribed');
* })
* .catch(function (err) {
* baja.error('components failed to unsubscribe: ' + err);
* });
*/
Subscriber.prototype.unsubscribe = function (obj) {
obj = objectify(obj, "comps");
var that = this,
comps = obj.comps,
cb = new Callback(obj.ok, obj.fail, obj.batch),
c,
i,
j,
k,
space,
spaceOrd,
spaces = [],
ordsBySpace = {},
remoteCallRequired = false;
// Ensure 'this' is Component in callbacks...
setContextInOkCallback(that, cb);
setContextInFailCallback(that, cb);
try {
// Ensure we have an array of valid Components to subscribe too
if (!(comps instanceof Array)) {
comps = [comps];
}
// Add references and see if we need a network call
for (i = 0; i < comps.length; ++i) {
c = comps[i];
// Make sure we have a Component
strictArg(c, baja.Component);
// Attempt to remove Component from this Subscribers Component list
for (j = 0; j < that.$comps.length; ++j) {
if (that.$comps[j] === c) {
that.$comps.splice(j, 1);
break;
}
}
// Remove this Subscriber from the Component
for (k = 0; k < c.$subs.length; ++k) {
if (c.$subs[k] === that) {
c.$subs.splice(k, 1);
break;
}
}
// If the Component is not subscribed but was previously subscribed then make a network call
if (canUnsubscribe(c)) {
space = c.getComponentSpace();
spaceOrd = space.getAbsoluteOrd().toString();
if (!ordsBySpace.hasOwnProperty(spaceOrd)) {
spaces.push(space);
ordsBySpace[spaceOrd] = [];
}
delete c.$subDf;
remoteCallRequired = true;
ordsBySpace[spaceOrd].push("h:" + c.getHandle());
}
}
// If there is nothing to unsubscribe at this point then just bail
if (remoteCallRequired) {
// Make the network call through the Spaces
var batch = obj.batch || new baja.comm.Batch(),
promises = [];
baja.iterate(spaces, function (sp) {
if (sp.hasCallbacks()) {
var ordsInSpace = ordsBySpace[sp.getAbsoluteOrd()];
if (ordsInSpace.length > 0) {
var spaceCb = new Callback(baja.ok, baja.fail, batch);
sp.getCallbacks().unsubscribe(ordsInSpace, spaceCb);
promises.push(spaceCb.promise());
}
}
});
Promise.all(promises)
.then(function () { cb.ok(); })
.catch(function (err) { cb.fail(err); });
if (!obj.batch) {
batch.commit();
}
}
else {
cb.ok();
}
}
catch (err) {
cb.fail(err);
}
return cb.promise();
};
/**
* Unsubscribe and unregister all `Component`s from a `Subscriber`.
*
* If the `Component`s are able to be unsubscribed, this will result in
* an **asynchronous** network call.
*
* For callbacks, the `this` keyword is set to the internal component array.
*
* @param {Object} [obj] the Object Literal used for the method's arguments.
* @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback.
* Called once the Components have been unsubscribed.
* @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
* Called if the Components fail to unsubscribe. Any errors will be passed to
* this function.
* @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
* batched into this object.
* @returns {Promise} a promise that will be resolved once all components are
* unsubscribed.
*
* @example
* sub.unsubscribeAll({
* batch // if defined, any network calls will be batched into this object (optional)
* })
* .then(function () {
* baja.outln('all components unsubscribed');
* })
* .catch(function (err) {
* baja.error('failed to unsubscribe components: ' + err);
* });
*/
Subscriber.prototype.unsubscribeAll = function (obj) {
obj = objectify(obj);
obj.comps = this.$comps.slice();
return this.unsubscribe(obj);
};
/**
* Return true if the `Component` is subscribed in this `Subscriber`.
*
* @param {baja.Component} comp the `Component` to be tested for subscription.
* @returns {Boolean}
*/
Subscriber.prototype.isSubscribed = function (comp) {
strictArg(comp, baja.Component);
return this.$comps.contains(comp);
};
return Subscriber;
});