diff --git a/platform/telemetry/src/TelemetryAggregator.js b/platform/telemetry/src/TelemetryAggregator.js index 0c3d98baab..797fc55c72 100644 --- a/platform/telemetry/src/TelemetryAggregator.js +++ b/platform/telemetry/src/TelemetryAggregator.js @@ -9,17 +9,20 @@ define( "use strict"; /** + * A telemetry aggregator makes many telemetry providers + * appear as one. * * @constructor */ function TelemetryAggregator($q, telemetryProviders) { + // Merge the results from many providers into one + // result object. function mergeResults(results) { var merged = {}; results.forEach(function (result) { Object.keys(result).forEach(function (k) { - // Otherwise, just take the result merged[k] = result[k]; }); }); @@ -27,6 +30,8 @@ define( return merged; } + // Request telemetry from all providers; once they've + // responded, merge the results into one result object. function requestTelemetry(requests) { return $q.all(telemetryProviders.map(function (provider) { return provider.requestTelemetry(requests); @@ -34,6 +39,14 @@ define( } return { + /** + * Request telemetry data. + * @param {TelemetryRequest[]} requests and array of + * requests to be handled + * @returns {Promise} a promise for telemetry data + * which may (or may not, depending on + * availability) satisfy the requests + */ requestTelemetry: requestTelemetry }; } diff --git a/platform/telemetry/src/TelemetryCapability.js b/platform/telemetry/src/TelemetryCapability.js index 8a781880b9..2cdcd23f02 100644 --- a/platform/telemetry/src/TelemetryCapability.js +++ b/platform/telemetry/src/TelemetryCapability.js @@ -9,6 +9,9 @@ define( "use strict"; /** + * A telemetry capability provides a means of requesting telemetry + * for a specific object, and for unwrapping the response (to get + * at the specific data which is appropriate to the domain object.) * * @constructor */ @@ -23,6 +26,8 @@ define( telemetryService = $q.when($injector.get("telemetryService")); } catch (e) { + // $injector should throw is telemetryService + // is unavailable or unsatisfiable. $log.warn("Telemetry service unavailable"); telemetryService = $q.reject(e); } @@ -30,21 +35,30 @@ define( return telemetryService; } + // Build a request object. This takes the request that was + // passed to the capability, and adds source, id, and key + // fields associated with the object (from its type definition + // and/or its model) function buildRequest(request) { + // Start with any "telemetry" field in type; use that as a + // basis for the request. var type = domainObject.getCapability("type"), typeRequest = (type && type.getDefinition().telemetry) || {}, modelTelemetry = domainObject.getModel().telemetry, fullRequest = Object.create(typeRequest); + // Add properties from the telemetry field of this + // specific domain object. Object.keys(modelTelemetry).forEach(function (k) { fullRequest[k] = modelTelemetry[k]; }); + // Add properties from this specific requestData call. Object.keys(request).forEach(function (k) { fullRequest[k] = request[k]; }); - // Include domain object ID, at minimum + // Ensure an ID and key are present if (!fullRequest.id) { fullRequest.id = domainObject.getId(); } @@ -55,37 +69,63 @@ define( return fullRequest; } + // Issue a request for telemetry data function requestTelemetry(request) { // Bring in any defaults from the object model var fullRequest = buildRequest(request || {}), source = fullRequest.source, key = fullRequest.key; + // Pull out the relevant field from the larger, + // structured response. function getRelevantResponse(response) { return ((response || {})[source] || {})[key] || {}; } + // Issue a request to the service function requestTelemetryFromService(telemetryService) { return telemetryService.requestTelemetry([fullRequest]); } + // If a telemetryService is not available, + // getTelemetryService() should reject, and this should + // bubble through subsequent then calls. return getTelemetryService() .then(requestTelemetryFromService) .then(getRelevantResponse); } return { + /** + * Request telemetry data for this specific domain object. + * @param {TelemetryRequest} [request] parameters for this + * specific request + * @returns {Promise} a promise for the resulting telemetry + * object + */ requestData: requestTelemetry, + + /** + * Get metadata about this domain object's associated + * telemetry. + */ getMetadata: function () { + // metadata just looks like a request, + // so use buildRequest to bring in both + // type-level and object-level telemetry + // properties return buildRequest({}); } }; } + /** + * The telemetry capability is applicable when a + * domain object model has a "telemetry" field. + */ TelemetryCapability.appliesTo = function (model) { return (model && - model.telemetry && - model.telemetry.source) ? true : false; + model.telemetry) ? true : false; }; return TelemetryCapability; diff --git a/platform/telemetry/src/TelemetryController.js b/platform/telemetry/src/TelemetryController.js index 67f8ad7186..77318a5eb9 100644 --- a/platform/telemetry/src/TelemetryController.js +++ b/platform/telemetry/src/TelemetryController.js @@ -17,19 +17,44 @@ define( */ function TelemetryController($scope, $q, $timeout, $log) { + // Private to maintain in this scope var self = { + // IDs of domain objects with telemetry ids: [], + + // Containers for latest responses (id->response) + // Used internally; see buildResponseContainer + // for format response: {}, + + // Request fields (id->requests) request: {}, + + // Number of outstanding requests pending: 0, + + // Array of object metadatas, for easy retrieval metadatas: [], + + // Interval at which to poll for new data interval: 1000, + + // Flag tracking whether or not a request + // is in progress refreshing: false, + + // Used to track whether a new telemetryUpdate + // is being issued. broadcasting: false }; - + // Broadcast that a telemetryUpdate has occurred. function doBroadcast() { + // This may get called multiple times from + // multiple objects, so set a flag to suppress + // multiple simultaneous events from being + // broadcast, then issue the actual broadcast + // later (via $timeout) if (!self.broadcasting) { self.broadcasting = true; $timeout(function () { @@ -39,11 +64,14 @@ define( } } + // Issue a request for new telemetry for one of the + // objects being tracked by this controller function requestTelemetryForId(id, trackPending) { var responseObject = self.response[id], domainObject = responseObject.domainObject, telemetry = domainObject.getCapability('telemetry'); + // Callback for when data comes back function storeData(data) { self.pending -= trackPending ? 1 : 0; responseObject.data = data; @@ -52,30 +80,50 @@ define( self.pending += trackPending ? 1 : 0; + // Shouldn't happen, but isn't fatal, + // so warn. if (!telemetry) { $log.warn([ "Expected telemetry capability for ", id, " but found none. Cannot request data." ].join("")); + + // Request won't happen, so don't + // mark it as pending. + self.pending -= trackPending ? 1 : 0; return; } + // Issue the request using the object's telemetry capability return $q.when(telemetry.requestData(self.request)) .then(storeData); } + // Request telemetry for all objects tracked by this + // controller. A flag is passed to indicate whether the + // pending counter should be incremented (this will + // cause isRequestPending() to change, which we only + // want to happen for requests which have originated + // outside of this controller's polling action.) function requestTelemetry(trackPending) { return $q.all(self.ids.map(function (id) { return requestTelemetryForId(id, trackPending); })); } + // Look up domain objects which have telemetry capabilities. + // This will either be the object in view, or object that + // this object delegates its telemetry capability to. function promiseRelevantDomainObjects(domainObject) { + // If object has been cleared, there are no relevant + // telemetry-providing domain objects. if (!domainObject) { return $q.when([]); } + // Otherwise, try delegation first, and attach the + // object itself if it has a telemetry capability. return $q.when(domainObject.useCapability( "delegation", "telemetry" @@ -87,6 +135,9 @@ define( }); } + // Build the response containers that are used internally + // by this controller to track latest responses, etc, for + // a given domain object. function buildResponseContainer(domainObject) { var telemetry = domainObject && domainObject.getCapability("telemetry"), @@ -103,12 +154,17 @@ define( data: {} }; } else { + // Shouldn't happen, as we've checked for + // telemetry capabilities previously, but + // be defensive. $log.warn([ "Expected telemetry capability for ", domainObject.getId(), " but none was found." ].join("")); + // Create an empty container so subsequent + // behavior won't hit an exception. self.response[domainObject.getId()] = { name: domainObject.getModel().name, domainObject: domainObject, @@ -119,11 +175,21 @@ define( } } + // Build response containers (as above) for all + // domain objects, and update some controller-internal + // state to support subsequent calls. function buildResponseContainers(domainObjects) { + // Build the containers domainObjects.forEach(buildResponseContainer); + + // Maintain a list of relevant ids, to convert + // back from dictionary-like container objects to arrays. self.ids = domainObjects.map(function (obj) { return obj.getId(); }); + + // Keep a reference to all applicable metadata + // to return from getMetadata self.metadatas = self.ids.map(function (id) { return self.response[id].metadata; }); @@ -135,11 +201,16 @@ define( } } + // Get relevant telemetry-providing domain objects + // for the domain object which is represented in this + // scope. This will be the domain object itself, or + // its telemetry delegates, or both. function getTelemetryObjects(domainObject) { promiseRelevantDomainObjects(domainObject) .then(buildResponseContainers); } + // Handle a polling refresh interval function startTimeout() { if (!self.refreshing && self.interval !== undefined) { self.refreshing = true; @@ -154,19 +225,58 @@ define( } } - + // Watch for a represented domain object $scope.$watch("domainObject", getTelemetryObjects); - startTimeout(); // Begin refreshing + + // Begin polling for data changes + startTimeout(); return { + /** + * Get all telemetry metadata associated with + * telemetry-providing domain objects managed by + * this controller. + * + * This will ordered in the + * same manner as `getTelemetryObjects()` or + * `getResponse()`; that is, the metadata at a + * given index will correspond to the telemetry-providing + * domain object at the same index. + * @returns {Array} an array of metadata objects + */ getMetadata: function () { return self.metadatas; }, + /** + * Get all telemetry-providing domain objects managed by + * this controller. + * + * This will ordered in the + * same manner as `getMetadata()` or + * `getResponse()`; that is, the metadata at a + * given index will correspond to the telemetry-providing + * domain object at the same index. + * @returns {DomainObject[]} an array of metadata objects + */ getTelemetryObjects: function () { return self.ids.map(function (id) { return self.response[id].domainObject; }); }, + /** + * Get the latest telemetry response for a specific + * domain object (if an argument is given) or for all + * objects managed by this controller (if no argument + * is supplied.) + * + * In the first form, this returns a single object; in + * the second form, it returns an array ordered in + * same manner as `getMetadata()` or + * `getTelemetryObjects()`; that is, the telemetry + * response at agiven index will correspond to the + * telemetry-providing domain object at the same index. + * @returns {Array} an array of responses + */ getResponse: function getResponse(arg) { var id = arg && (typeof arg === 'string' ? arg : arg.getId()); @@ -177,13 +287,33 @@ define( return (self.ids || []).map(getResponse); }, + /** + * Check if the latest request (not counting + * requests from TelemtryController's own polling) + * is still outstanding. Users of the TelemetryController + * may use this method as a condition upon which to + * show user feedback, such as a wait spinner. + * + * @returns {boolean} true if the request is still outstanding + */ isRequestPending: function () { return self.pending > 0; }, + /** + * Issue a new data request. This will change the + * request parameters that are passed along to all + * telemetry capabilities managed by this controller. + */ requestData: function (request) { self.request = request || {}; return requestTelemetry(true); }, + /** + * Change the interval at which this controller will + * perform its polling activity. + * @param {number} durationMillis the interval at + * which to poll, in milliseconds + */ setRefreshInterval: function (durationMillis) { self.interval = durationMillis; startTimeout(); diff --git a/platform/telemetry/test/TelemetryCapabilitySpec.js b/platform/telemetry/test/TelemetryCapabilitySpec.js index 045da297df..1f8ef9ecbf 100644 --- a/platform/telemetry/test/TelemetryCapabilitySpec.js +++ b/platform/telemetry/test/TelemetryCapabilitySpec.js @@ -65,9 +65,6 @@ define( expect(TelemetryCapability.appliesTo({ telemetry: { source: "testSource" } })).toBeTruthy(); - expect(TelemetryCapability.appliesTo({ - telemetry: { xsource: "testSource" } - })).toBeFalsy(); expect(TelemetryCapability.appliesTo({ xtelemetry: { source: "testSource" } })).toBeFalsy();