/**
* @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";
const { BaseBajaObj, def: bajaDef, error: bajaError, objectify, strictArg, subclass } = baja;
const { canUnsubscribe, setContextInFailCallback, setContextInOkCallback } = compUtil;
/**
* Component Subscriber used to subscribe to multiple `Component`s for events.
*
* @class
* @alias baja.Subscriber
* @extends baja.BaseBajaObj
*/
const 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 are 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 a 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");
let comps = obj.comps;
const inpBatch = obj.batch;
const cb = new Callback(obj.ok, obj.fail, inpBatch);
const ordsBySpace = {};
const spaces = [];
const componentSubscriptions = [];
let remoteCallRequired;
// 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
const subscribePromises = comps.map((comp) => {
// Make sure we have a Component
strictArg(comp, baja.Component);
if (!comp.isMounted()) {
throw new Error("Cannot subscribe unmounted Component!");
}
const space = comp.getComponentSpace();
const spaceOrd = space.getAbsoluteOrd().toString();
if (!ordsBySpace[spaceOrd]) {
spaces.push(space);
ordsBySpace[spaceOrd] = [];
}
// Add to this Subscribers Component array
if (!this.$comps.includes(comp)) {
this.$comps.push(comp);
}
// Make sure this Subscriber is listed in the Subscribed Component
if (!comp.$subs.includes(this)) {
comp.$subs.push(this);
}
// See if we need to make any network calls.
if (!comp.$subDf || Promise.isRejected(comp.$subDf.promise())) {
remoteCallRequired = true;
ordsBySpace[spaceOrd].push("h:" + comp.getHandle());
componentSubscriptions.push({
spaceOrd,
subscribedRemote: false,
comp,
df: (comp.$subDf = Promise.deferred())
});
}
return comp.$subDf.promise();
});
// When all of these promises are resolved then we're done.
cb.addOk(function (ok, fail) {
Promise.all(subscribePromises).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((ok, fail, handle, timestamp) => {
componentSubscriptions.forEach(({ comp, df }) => {
try {
comp.$fw("fwSubscribed");
} catch (err0) {
bajaError(err0);
} finally {
try {
comp.$lastSubTimestamp = timestamp;
df.resolve();
} catch (err1) {
bajaError(err1);
}
}
});
ok();
});
cb.addFail((ok, fail, err) => {
componentSubscriptions.forEach(({ comp, df, subscribedRemote }) => {
if (subscribedRemote) {
try {
comp.$fw("fwSubscribed");
} catch (err0) {
bajaError(err0);
} finally {
try {
df.resolve();
} catch (err1) {
bajaError(err1);
}
}
} else {
try {
df.reject(err);
} catch (err0) {
bajaError(err0);
}
}
});
fail(err);
});
// Make the network call through the Spaces
const batch = obj.batch || new baja.comm.Batch();
const spacePromises = spaces.map((sp) => {
const spaceAbsoluteOrd = sp.getAbsoluteOrd().toString();
const ordsInSpace = ordsBySpace[spaceAbsoluteOrd];
if (!ordsInSpace.length) { return; }
const spaceCb = new Callback(function () {
for (let i = 0; i < componentSubscriptions.length; ++i) {
if (componentSubscriptions[i].spaceOrd === spaceAbsoluteOrd) {
componentSubscriptions[i].subscribedRemote = true;
}
}
}, baja.fail, batch);
sp.getCallbacks().subscribe(ordsInSpace, spaceCb);
return 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) {
const { ok, fail, batch, ords, space } = obj;
const cb = new Callback(ok, fail, batch);
const { $comps } = this;
try {
// Ensure these Components are all subscribed
cb.addOk((ok, fail, handles, timestamp) => {
// Remove references and see if we need a network call
handles.forEach((handle) => {
// Attempt to find the Component locally
const c = space.findByHandle(handle);
const { $subs } = c;
if (c) {
// Mark the Component as subscribed
const prevSub = c.isSubscribed();
// Add to this Subscribers Component array
if (!$comps.includes(c)) {
$comps.push(c);
}
// Make sure this Subscriber is listed in the Subscribed Component
if (!$subs.includes(this)) {
$subs.push(this);
}
// 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: " + handle);
}
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 can 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 a 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");
const { $comps } = this;
const { ok, fail, batch: inpBatch } = obj;
const cb = new Callback(ok, fail, inpBatch);
const spaces = [];
const ordsBySpace = {};
let { comps } = obj;
let remoteCallRequired;
// Ensure 'this' is Component in callbacks...
setContextInOkCallback(this, cb);
setContextInFailCallback(this, 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
comps.forEach((c) => {
// Make sure we have a Component
strictArg(c, baja.Component);
const { $subs } = c;
// Attempt to remove Component from this Subscribers Component list
for (let i = 0; i < $comps.length; ++i) {
if ($comps[i] === c) {
$comps.splice(i, 1);
break;
}
}
// Remove this Subscriber from the Component
for (let i = 0; i < $subs.length; ++i) {
if ($subs[i] === this) {
$subs.splice(i, 1);
break;
}
}
// If the Component is not subscribed but was previously subscribed then make a network call
if (canUnsubscribe(c)) {
const space = c.getComponentSpace();
const spaceOrd = space.getAbsoluteOrd().toString();
if (!ordsBySpace[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
const batch = inpBatch || new baja.comm.Batch();
const unsubscribePromises = spaces.map((sp) => {
if (sp.hasCallbacks()) {
const ordsInSpace = ordsBySpace[sp.getAbsoluteOrd()];
if (ordsInSpace.length > 0) {
const spaceCb = new Callback(baja.ok, baja.fail, batch);
sp.getCallbacks().unsubscribe(ordsInSpace, spaceCb);
return spaceCb.promise();
}
}
});
Promise.all(unsubscribePromises)
.then(function () { cb.ok(); })
.catch(function (err) { cb.fail(err); });
if (!inpBatch) {
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 can 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.indexOf(comp) >= 0;
};
return Subscriber;
});