Merge remote-tracking branch 'github-open/open141' into open141-integration

This commit is contained in:
Pete Richards 2015-10-09 09:52:35 -07:00
commit fc0bfa77db
7 changed files with 348 additions and 165 deletions

View File

@ -4,7 +4,11 @@
{ {
"implementation": "WatchIndicator.js", "implementation": "WatchIndicator.js",
"depends": ["$interval", "$rootScope"] "depends": ["$interval", "$rootScope"]
},
{
"implementation": "DigestIndicator.js",
"depends": ["$interval", "$rootScope"]
} }
] ]
} }
} }

View File

@ -0,0 +1,77 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define*/
define(
[],
function () {
"use strict";
/**
* Displays the number of digests that have occurred since the
* indicator was first instantiated.
* @constructor
* @param $interval Angular's $interval
* @implements {Indicator}
*/
function DigestIndicator($interval, $rootScope) {
var digests = 0,
displayed = 0,
start = Date.now();
function update() {
var secs = (Date.now() - start) / 1000;
displayed = Math.round(digests / secs);
}
function increment() {
digests += 1;
}
$rootScope.$watch(increment);
// Update state every second
$interval(update, 1000);
// Provide initial state, too
update();
return {
getGlyph: function () {
return ".";
},
getGlyphClass: function () {
return undefined;
},
getText: function () {
return displayed + " digests/sec";
},
getDescription: function () {
return "";
}
};
}
return DigestIndicator;
}
);

View File

@ -29,7 +29,8 @@ define(
function () { function () {
"use strict"; "use strict";
var TOPIC_PREFIX = "mutation:"; var GENERAL_TOPIC = "mutation",
TOPIC_PREFIX = "mutation:";
// Utility function to overwrite a destination object // Utility function to overwrite a destination object
// with the contents of a source object. // with the contents of a source object.
@ -78,7 +79,11 @@ define(
* @implements {Capability} * @implements {Capability}
*/ */
function MutationCapability(topic, now, domainObject) { function MutationCapability(topic, now, domainObject) {
this.mutationTopic = topic(TOPIC_PREFIX + domainObject.getId()); this.generalMutationTopic =
topic(GENERAL_TOPIC);
this.specificMutationTopic =
topic(TOPIC_PREFIX + domainObject.getId());
this.now = now; this.now = now;
this.domainObject = domainObject; this.domainObject = domainObject;
} }
@ -115,11 +120,17 @@ define(
// mutator function has a temporary copy to work with. // mutator function has a temporary copy to work with.
var domainObject = this.domainObject, var domainObject = this.domainObject,
now = this.now, now = this.now,
t = this.mutationTopic, generalTopic = this.generalMutationTopic,
specificTopic = this.specificMutationTopic,
model = domainObject.getModel(), model = domainObject.getModel(),
clone = JSON.parse(JSON.stringify(model)), clone = JSON.parse(JSON.stringify(model)),
useTimestamp = arguments.length > 1; useTimestamp = arguments.length > 1;
function notifyListeners(model) {
generalTopic.notify(domainObject);
specificTopic.notify(model);
}
// Function to handle copying values to the actual // Function to handle copying values to the actual
function handleMutation(mutationResult) { function handleMutation(mutationResult) {
// If mutation result was undefined, just use // If mutation result was undefined, just use
@ -136,7 +147,7 @@ define(
copyValues(model, result); copyValues(model, result);
} }
model.modified = useTimestamp ? timestamp : now(); model.modified = useTimestamp ? timestamp : now();
t.notify(model); notifyListeners(model);
} }
// Report the result of the mutation // Report the result of the mutation
@ -158,7 +169,7 @@ define(
* @memberof platform/core.MutationCapability# * @memberof platform/core.MutationCapability#
*/ */
MutationCapability.prototype.listen = function (listener) { MutationCapability.prototype.listen = function (listener) {
return this.mutationTopic.listen(listener); return this.specificMutationTopic.listen(listener);
}; };
/** /**

View File

@ -61,7 +61,7 @@ define(
* @memberof platform/core.Throttle# * @memberof platform/core.Throttle#
*/ */
return function (fn, delay, apply) { return function (fn, delay, apply) {
var promise, // Promise for the result of throttled function var promise,
args = []; args = [];
function invoke() { function invoke() {

View File

@ -45,7 +45,15 @@
"provides": "searchService", "provides": "searchService",
"type": "provider", "type": "provider",
"implementation": "services/GenericSearchProvider.js", "implementation": "services/GenericSearchProvider.js",
"depends": [ "$q", "$timeout", "objectService", "workerService", "GENERIC_SEARCH_ROOTS" ] "depends": [
"$q",
"$log",
"throttle",
"objectService",
"workerService",
"topic",
"GENERIC_SEARCH_ROOTS"
]
}, },
{ {
"provides": "searchService", "provides": "searchService",
@ -61,4 +69,4 @@
} }
] ]
} }
} }

View File

@ -28,61 +28,78 @@ define(
[], [],
function () { function () {
"use strict"; "use strict";
var DEFAULT_MAX_RESULTS = 100, var DEFAULT_MAX_RESULTS = 100,
DEFAULT_TIMEOUT = 1000, DEFAULT_TIMEOUT = 1000,
MAX_CONCURRENT_REQUESTS = 100,
FLUSH_INTERVAL = 0,
stopTime; stopTime;
/** /**
* A search service which searches through domain objects in * A search service which searches through domain objects in
* the filetree without using external search implementations. * the filetree without using external search implementations.
* *
* @constructor * @constructor
* @param $q Angular's $q, for promise consolidation. * @param $q Angular's $q, for promise consolidation.
* @param $timeout Angular's $timeout, for delayed function execution. * @param $log Anglar's $log, for logging.
* @param {Function} throttle a function to throttle function invocations
* @param {ObjectService} objectService The service from which * @param {ObjectService} objectService The service from which
* domain objects can be gotten. * domain objects can be gotten.
* @param {WorkerService} workerService The service which allows * @param {WorkerService} workerService The service which allows
* more easy creation of web workers. * more easy creation of web workers.
* @param {GENERIC_SEARCH_ROOTS} ROOTS An array of the root * @param {GENERIC_SEARCH_ROOTS} ROOTS An array of the root
* domain objects' IDs. * domain objects' IDs.
*/ */
function GenericSearchProvider($q, $timeout, objectService, workerService, ROOTS) { function GenericSearchProvider($q, $log, throttle, objectService, workerService, topic, ROOTS) {
var indexed = {}, var indexed = {},
pendingIndex = {},
pendingQueries = {}, pendingQueries = {},
worker = workerService.run('genericSearchWorker'); toRequest = [],
worker = workerService.run('genericSearchWorker'),
mutationTopic = topic("mutation"),
indexingStarted = Date.now(),
pendingRequests = 0,
scheduleFlush;
this.worker = worker; this.worker = worker;
this.pendingQueries = pendingQueries; this.pendingQueries = pendingQueries;
this.$q = $q; this.$q = $q;
// pendingQueries is a dictionary with the key value pairs st // pendingQueries is a dictionary with the key value pairs st
// the key is the timestamp and the value is the promise // the key is the timestamp and the value is the promise
function scheduleIdsForIndexing(ids) {
ids.forEach(function (id) {
if (!indexed[id] && !pendingIndex[id]) {
indexed[id] = true;
pendingIndex[id] = true;
toRequest.push(id);
}
});
scheduleFlush();
}
// Tell the web worker to add a domain object's model to its list of items. // Tell the web worker to add a domain object's model to its list of items.
function indexItem(domainObject) { function indexItem(domainObject) {
var message; var model = domainObject.getModel();
// undefined check worker.postMessage({
if (domainObject && domainObject.getModel) { request: 'index',
// Using model instead of whole domain object because model: model,
// it's a JSON object. id: domainObject.getId()
message = { });
request: 'index',
model: domainObject.getModel(), if (Array.isArray(model.composition)) {
id: domainObject.getId() scheduleIdsForIndexing(model.composition);
};
worker.postMessage(message);
} }
} }
// Handles responses from the web worker. Namely, the results of // Handles responses from the web worker. Namely, the results of
// a search request. // a search request.
function handleResponse(event) { function handleResponse(event) {
var ids = [], var ids = [],
id; id;
// If we have the results from a search // If we have the results from a search
if (event.data.request === 'search') { if (event.data.request === 'search') {
// Convert the ids given from the web worker into domain objects // Convert the ids given from the web worker into domain objects
for (id in event.data.results) { for (id in event.data.results) {
@ -91,7 +108,7 @@ define(
objectService.getObjects(ids).then(function (objects) { objectService.getObjects(ids).then(function (objects) {
var searchResults = [], var searchResults = [],
id; id;
// Create searchResult objects // Create searchResult objects
for (id in objects) { for (id in objects) {
searchResults.push({ searchResults.push({
@ -100,8 +117,8 @@ define(
score: event.data.results[id] score: event.data.results[id]
}); });
} }
// Resove the promise corresponding to this // Resove the promise corresponding to this
pendingQueries[event.data.timestamp].resolve({ pendingQueries[event.data.timestamp].resolve({
hits: searchResults, hits: searchResults,
total: event.data.total, total: event.data.total,
@ -110,83 +127,49 @@ define(
}); });
} }
} }
// Helper function for getItems(). Indexes the tree.
function indexItems(nodes) {
nodes.forEach(function (node) {
var id = node && node.getId && node.getId();
// If we have already indexed this item, stop here
if (indexed[id]) {
return;
}
// Index each item with the web worker
indexItem(node);
indexed[id] = true;
// If this node has children, index those
if (node && node.hasCapability && node.hasCapability('composition')) {
// Make sure that this is async, so doesn't block up page
$timeout(function () {
// Get the children...
node.useCapability('composition').then(function (children) {
$timeout(function () {
// ... then index the children
if (children.constructor === Array) {
indexItems(children);
} else {
indexItems([children]);
}
}, 0);
});
}, 0);
}
// Watch for changes to this item, in case it gets new children
if (node && node.hasCapability && node.hasCapability('mutation')) {
node.getCapability('mutation').listen(function (listener) {
if (listener && listener.composition) {
// If the node was mutated to have children, get the child domain objects
objectService.getObjects(listener.composition).then(function (objectsById) {
var objects = [],
id;
// Get each of the domain objects in objectsById function requestAndIndex(id) {
for (id in objectsById) { pendingRequests += 1;
objects.push(objectsById[id]); objectService.getObjects([id]).then(function (objects) {
} delete pendingIndex[id];
if (objects[id]) {
indexItems(objects); indexItem(objects[id]);
});
}
});
} }
}, function () {
$log.warn("Failed to index domain object " + id);
}).then(function () {
pendingRequests -= 1;
scheduleFlush();
}); });
} }
// Converts the filetree into a list scheduleFlush = throttle(function flush() {
function getItems() { var batchSize =
// Aquire root objects Math.max(MAX_CONCURRENT_REQUESTS - pendingRequests, 0);
objectService.getObjects(ROOTS).then(function (objectsById) {
var objects = [], if (toRequest.length + pendingRequests < 1) {
id; $log.info([
'GenericSearch finished indexing after ',
// Get each of the domain objects in objectsById ((Date.now() - indexingStarted) / 1000).toFixed(2),
for (id in objectsById) { ' seconds.'
objects.push(objectsById[id]); ].join(''));
} } else {
toRequest.splice(-batchSize, batchSize)
// Index all of the roots' descendents .forEach(requestAndIndex);
indexItems(objects); }
}); }, FLUSH_INTERVAL);
}
worker.onmessage = handleResponse; worker.onmessage = handleResponse;
// Index the tree's contents once at the beginning // Index the tree's contents once at the beginning
getItems(); scheduleIdsForIndexing(ROOTS);
// Re-index items when they are mutated
mutationTopic.listen(function (domainObject) {
var id = domainObject.getId();
indexed[id] = false;
scheduleIdsForIndexing([id]);
});
} }
/** /**
@ -266,4 +249,4 @@ define(
return GenericSearchProvider; return GenericSearchProvider;
} }
); );

View File

@ -31,35 +31,67 @@ define(
describe("The generic search provider ", function () { describe("The generic search provider ", function () {
var mockQ, var mockQ,
mockTimeout, mockLog,
mockThrottle,
mockDeferred, mockDeferred,
mockObjectService, mockObjectService,
mockObjectPromise, mockObjectPromise,
mockChainedPromise,
mockDomainObjects, mockDomainObjects,
mockCapability, mockCapability,
mockCapabilityPromise, mockCapabilityPromise,
mockWorkerService, mockWorkerService,
mockWorker, mockWorker,
mockTopic,
mockMutationTopic,
mockRoots = ['root1', 'root2'], mockRoots = ['root1', 'root2'],
mockThrottledFn,
throttledCallCount,
provider, provider,
mockProviderResults; mockProviderResults;
beforeEach(function () { function resolveObjectPromises() {
var i; var i;
for (i = 0; i < mockObjectPromise.then.calls.length; i += 1) {
mockChainedPromise.then.calls[i].args[0](
mockObjectPromise.then.calls[i]
.args[0](mockDomainObjects)
);
}
}
function resolveThrottledFn() {
if (mockThrottledFn.calls.length > throttledCallCount) {
mockThrottle.mostRecentCall.args[0]();
throttledCallCount = mockThrottledFn.calls.length;
}
}
function resolveAsyncTasks() {
resolveThrottledFn();
resolveObjectPromises();
}
beforeEach(function () {
mockQ = jasmine.createSpyObj( mockQ = jasmine.createSpyObj(
"$q", "$q",
[ "defer" ] [ "defer" ]
); );
mockLog = jasmine.createSpyObj(
"$log",
[ "error", "warn", "info", "debug" ]
);
mockDeferred = jasmine.createSpyObj( mockDeferred = jasmine.createSpyObj(
"deferred", "deferred",
[ "resolve", "reject"] [ "resolve", "reject"]
); );
mockDeferred.promise = "mock promise"; mockDeferred.promise = "mock promise";
mockQ.defer.andReturn(mockDeferred); mockQ.defer.andReturn(mockDeferred);
mockTimeout = jasmine.createSpy("$timeout"); mockThrottle = jasmine.createSpy("throttle");
mockThrottledFn = jasmine.createSpy("throttledFn");
throttledCallCount = 0;
mockObjectService = jasmine.createSpyObj( mockObjectService = jasmine.createSpyObj(
"objectService", "objectService",
[ "getObjects" ] [ "getObjects" ]
@ -68,9 +100,14 @@ define(
"promise", "promise",
[ "then", "catch" ] [ "then", "catch" ]
); );
mockChainedPromise = jasmine.createSpyObj(
"chainedPromise",
[ "then" ]
);
mockObjectService.getObjects.andReturn(mockObjectPromise); mockObjectService.getObjects.andReturn(mockObjectPromise);
mockTopic = jasmine.createSpy('topic');
mockWorkerService = jasmine.createSpyObj( mockWorkerService = jasmine.createSpyObj(
"workerService", "workerService",
[ "run" ] [ "run" ]
@ -80,68 +117,109 @@ define(
[ "postMessage" ] [ "postMessage" ]
); );
mockWorkerService.run.andReturn(mockWorker); mockWorkerService.run.andReturn(mockWorker);
mockCapabilityPromise = jasmine.createSpyObj( mockCapabilityPromise = jasmine.createSpyObj(
"promise", "promise",
[ "then", "catch" ] [ "then", "catch" ]
); );
mockDomainObjects = {}; mockDomainObjects = {};
for (i = 0; i < 4; i += 1) { ['a', 'root1', 'root2'].forEach(function (id) {
mockDomainObjects[i] = ( mockDomainObjects[id] = (
jasmine.createSpyObj( jasmine.createSpyObj(
"domainObject", "domainObject",
[ "getId", "getModel", "hasCapability", "getCapability", "useCapability" ] [
"getId",
"getModel",
"hasCapability",
"getCapability",
"useCapability"
]
) )
); );
mockDomainObjects[i].getId.andReturn(i); mockDomainObjects[id].getId.andReturn(id);
mockDomainObjects[i].getCapability.andReturn(mockCapability); mockDomainObjects[id].getCapability.andReturn(mockCapability);
mockDomainObjects[i].useCapability.andReturn(mockCapabilityPromise); mockDomainObjects[id].useCapability.andReturn(mockCapabilityPromise);
} mockDomainObjects[id].getModel.andReturn({});
// Give the first object children });
mockDomainObjects[0].hasCapability.andReturn(true);
mockCapability = jasmine.createSpyObj( mockCapability = jasmine.createSpyObj(
"capability", "capability",
[ "invoke", "listen" ] [ "invoke", "listen" ]
); );
mockCapability.invoke.andReturn(mockCapabilityPromise); mockCapability.invoke.andReturn(mockCapabilityPromise);
mockDomainObjects[0].getCapability.andReturn(mockCapability); mockDomainObjects.a.getCapability.andReturn(mockCapability);
mockMutationTopic = jasmine.createSpyObj(
provider = new GenericSearchProvider(mockQ, mockTimeout, mockObjectService, mockWorkerService, mockRoots); 'mutationTopic',
[ 'listen' ]
);
mockTopic.andCallFake(function (key) {
return key === 'mutation' && mockMutationTopic;
});
mockThrottle.andReturn(mockThrottledFn);
mockObjectPromise.then.andReturn(mockChainedPromise);
provider = new GenericSearchProvider(
mockQ,
mockLog,
mockThrottle,
mockObjectService,
mockWorkerService,
mockTopic,
mockRoots
);
}); });
it("indexes tree on initialization", function () { it("indexes tree on initialization", function () {
var i;
resolveThrottledFn();
expect(mockObjectService.getObjects).toHaveBeenCalled(); expect(mockObjectService.getObjects).toHaveBeenCalled();
expect(mockObjectPromise.then).toHaveBeenCalled(); expect(mockObjectPromise.then).toHaveBeenCalled();
// Call through the root-getting part // Call through the root-getting part
mockObjectPromise.then.mostRecentCall.args[0](mockDomainObjects); resolveObjectPromises();
// Call through the children-getting part mockRoots.forEach(function (id) {
mockTimeout.mostRecentCall.args[0](); expect(mockWorker.postMessage).toHaveBeenCalledWith({
// Array argument indicates multiple children request: 'index',
mockCapabilityPromise.then.mostRecentCall.args[0]([]); model: mockDomainObjects[id].getModel(),
mockTimeout.mostRecentCall.args[0](); id: id
// Call again, but for single child });
mockCapabilityPromise.then.mostRecentCall.args[0]({}); });
mockTimeout.mostRecentCall.args[0]();
expect(mockWorker.postMessage).toHaveBeenCalled();
}); });
it("when indexing, listens for composition changes", function () { it("indexes members of composition", function () {
var mockListener = {composition: {}}; mockDomainObjects.root1.getModel.andReturn({
composition: ['a']
// Call indexItems });
mockObjectPromise.then.mostRecentCall.args[0](mockDomainObjects);
resolveAsyncTasks();
// Call through listening for changes resolveAsyncTasks();
expect(mockCapability.listen).toHaveBeenCalled();
mockCapability.listen.mostRecentCall.args[0](mockListener); expect(mockWorker.postMessage).toHaveBeenCalledWith({
expect(mockObjectService.getObjects).toHaveBeenCalled(); request: 'index',
mockObjectPromise.then.mostRecentCall.args[0](mockDomainObjects); model: mockDomainObjects.a.getModel(),
id: 'a'
});
}); });
it("listens for changes to mutation", function () {
expect(mockMutationTopic.listen)
.toHaveBeenCalledWith(jasmine.any(Function));
mockMutationTopic.listen.mostRecentCall
.args[0](mockDomainObjects.a);
resolveAsyncTasks();
expect(mockWorker.postMessage).toHaveBeenCalledWith({
request: 'index',
model: mockDomainObjects.a.getModel(),
id: mockDomainObjects.a.getId()
});
});
it("sends search queries to the worker", function () { it("sends search queries to the worker", function () {
var timestamp = Date.now(); var timestamp = Date.now();
provider.query(' test "query" ', timestamp, 1, 2); provider.query(' test "query" ', timestamp, 1, 2);
@ -153,20 +231,20 @@ define(
timeout: 2 timeout: 2
}); });
}); });
it("gives an empty result for an empty query", function () { it("gives an empty result for an empty query", function () {
var timestamp = Date.now(), var timestamp = Date.now(),
queryOutput; queryOutput;
queryOutput = provider.query('', timestamp, 1, 2); queryOutput = provider.query('', timestamp, 1, 2);
expect(queryOutput.hits).toEqual([]); expect(queryOutput.hits).toEqual([]);
expect(queryOutput.total).toEqual(0); expect(queryOutput.total).toEqual(0);
queryOutput = provider.query(); queryOutput = provider.query();
expect(queryOutput.hits).toEqual([]); expect(queryOutput.hits).toEqual([]);
expect(queryOutput.total).toEqual(0); expect(queryOutput.total).toEqual(0);
}); });
it("handles responses from the worker", function () { it("handles responses from the worker", function () {
var timestamp = Date.now(), var timestamp = Date.now(),
event = { event = {
@ -181,13 +259,35 @@ define(
timestamp: timestamp timestamp: timestamp
} }
}; };
provider.query(' test "query" ', timestamp); provider.query(' test "query" ', timestamp);
mockWorker.onmessage(event); mockWorker.onmessage(event);
mockObjectPromise.then.mostRecentCall.args[0](mockDomainObjects); mockObjectPromise.then.mostRecentCall.args[0](mockDomainObjects);
expect(mockDeferred.resolve).toHaveBeenCalled(); expect(mockDeferred.resolve).toHaveBeenCalled();
}); });
it("warns when objects are unavailable", function () {
resolveAsyncTasks();
expect(mockLog.warn).not.toHaveBeenCalled();
mockChainedPromise.then.mostRecentCall.args[0](
mockObjectPromise.then.mostRecentCall.args[1]()
);
expect(mockLog.warn).toHaveBeenCalled();
});
it("throttles the loading of objects to index", function () {
expect(mockObjectService.getObjects).not.toHaveBeenCalled();
resolveThrottledFn();
expect(mockObjectService.getObjects).toHaveBeenCalled();
});
it("logs when all objects have been processed", function () {
expect(mockLog.info).not.toHaveBeenCalled();
resolveAsyncTasks();
resolveThrottledFn();
expect(mockLog.info).toHaveBeenCalled();
});
}); });
} }
); );