diff --git a/platform/entanglement/src/services/LocatingObjectDecorator.js b/platform/entanglement/src/services/LocatingObjectDecorator.js index eb402f05eb..6ff30407b6 100644 --- a/platform/entanglement/src/services/LocatingObjectDecorator.js +++ b/platform/entanglement/src/services/LocatingObjectDecorator.js @@ -37,7 +37,7 @@ define( this.$q = $q; } - LocatingObjectDecorator.prototype.getObjects = function (ids) { + LocatingObjectDecorator.prototype.getObjects = function (ids, abortSignal) { var $q = this.$q, $log = this.$log, objectService = this.objectService, @@ -79,7 +79,7 @@ define( }); } - return objectService.getObjects([id]).then(attachContext); + return objectService.getObjects([id], abortSignal).then(attachContext); } ids.forEach(function (id) { diff --git a/platform/search/src/services/SearchAggregator.js b/platform/search/src/services/SearchAggregator.js index b186fead9d..9dff418adf 100644 --- a/platform/search/src/services/SearchAggregator.js +++ b/platform/search/src/services/SearchAggregator.js @@ -80,12 +80,15 @@ define([ * @param {Function} [filter] if provided, will be called for every * potential modelResult. If it returns false, the model result will be * excluded from the search results. + * @param {AbortController.signal} abortSignal (optional) can pass in an abortSignal to cancel any + * downstream fetch requests. * @returns {Promise} A Promise for a search result object. */ SearchAggregator.prototype.query = function ( inputText, maxResults, - filter + filter, + abortSignal ) { var aggregator = this, @@ -120,7 +123,7 @@ define([ modelResults = aggregator.applyFilter(modelResults, filter); modelResults = aggregator.removeDuplicates(modelResults); - return aggregator.asObjectResults(modelResults); + return aggregator.asObjectResults(modelResults, abortSignal); }); }; @@ -193,16 +196,19 @@ define([ * Convert modelResults to objectResults by fetching them from the object * service. * + * @param {Object} modelResults an object containing the results from the search + * @param {AbortController.signal} abortSignal (optional) abort signal to cancel any + * downstream fetch requests * @returns {Promise} for an objectResults object. */ - SearchAggregator.prototype.asObjectResults = function (modelResults) { + SearchAggregator.prototype.asObjectResults = function (modelResults, abortSignal) { var objectIds = modelResults.hits.map(function (modelResult) { return modelResult.id; }); return this .objectService - .getObjects(objectIds) + .getObjects(objectIds, abortSignal) .then(function (objects) { var objectResults = { diff --git a/src/adapter/services/LegacyObjectAPIInterceptor.js b/src/adapter/services/LegacyObjectAPIInterceptor.js index 76271942da..e39cdadfd5 100644 --- a/src/adapter/services/LegacyObjectAPIInterceptor.js +++ b/src/adapter/services/LegacyObjectAPIInterceptor.js @@ -139,10 +139,12 @@ define([ }); }; - ObjectServiceProvider.prototype.superSecretFallbackSearch = function (query, options) { + ObjectServiceProvider.prototype.superSecretFallbackSearch = function (query, abortSignal) { const searchService = this.$injector.get('searchService'); - return searchService.query(query); + // need to pass the abortSignal down, so need to + // pass in undefined for maxResults and filter on query + return searchService.query(query, undefined, undefined, abortSignal); }; // Injects new object API as a decorator so that it hijacks all requests. @@ -150,13 +152,13 @@ define([ function LegacyObjectAPIInterceptor(openmct, ROOTS, instantiate, topic, objectService) { const eventEmitter = openmct.objects.eventEmitter; - this.getObjects = function (keys) { + this.getObjects = function (keys, abortSignal) { const results = {}; const promises = keys.map(function (keyString) { const key = utils.parseKeyString(keyString); - return openmct.objects.get(key) + return openmct.objects.get(key, abortSignal) .then(function (object) { object = utils.toOldFormat(object); results[keyString] = instantiate(object, keyString); diff --git a/src/api/objects/ObjectAPI.js b/src/api/objects/ObjectAPI.js index f4152af118..96654b90cb 100644 --- a/src/api/objects/ObjectAPI.js +++ b/src/api/objects/ObjectAPI.js @@ -154,11 +154,12 @@ ObjectAPI.prototype.addProvider = function (namespace, provider) { * @method get * @memberof module:openmct.ObjectProvider# * @param {string} key the key for the domain object to load + * @param {AbortController.signal} abortSignal (optional) signal to abort fetch requests * @returns {Promise} a promise which will resolve when the domain object * has been saved, or be rejected if it cannot be saved */ -ObjectAPI.prototype.get = function (identifier) { +ObjectAPI.prototype.get = function (identifier, abortSignal) { let keystring = this.makeKeyString(identifier); if (this.cache[keystring] !== undefined) { return this.cache[keystring]; @@ -175,15 +176,12 @@ ObjectAPI.prototype.get = function (identifier) { throw new Error('Provider does not support get!'); } - let objectPromise = provider.get(identifier); + let objectPromise = provider.get(identifier, abortSignal); this.cache[keystring] = objectPromise; return objectPromise.then(result => { delete this.cache[keystring]; - const interceptors = this.listGetInterceptors(identifier, result); - interceptors.forEach(interceptor => { - result = interceptor.invoke(identifier, result); - }); + result = this.applyGetInterceptors(identifier, result); return result; }); @@ -200,19 +198,24 @@ ObjectAPI.prototype.get = function (identifier) { * @method search * @memberof module:openmct.ObjectAPI# * @param {string} query the term to search for - * @param {Object} options search options + * @param {AbortController.signal} abortSignal (optional) signal to cancel downstream fetch requests * @returns {Array.>} * an array of promises returned from each object provider's search function * each resolving to domain objects matching provided search query and options. */ -ObjectAPI.prototype.search = function (query, options) { +ObjectAPI.prototype.search = function (query, abortSignal) { const searchPromises = Object.values(this.providers) .filter(provider => provider.search !== undefined) - .map(provider => provider.search(query, options)); + .map(provider => provider.search(query, abortSignal)); - searchPromises.push(this.fallbackProvider.superSecretFallbackSearch(query, options) + searchPromises.push(this.fallbackProvider.superSecretFallbackSearch(query, abortSignal) .then(results => results.hits - .map(hit => utils.toNewFormat(hit.object.getModel(), hit.object.getId())))); + .map(hit => { + let domainObject = utils.toNewFormat(hit.object.getModel(), hit.object.getId()); + domainObject = this.applyGetInterceptors(domainObject.identifier, domainObject); + + return domainObject; + }))); return searchPromises; }; @@ -338,6 +341,19 @@ ObjectAPI.prototype.listGetInterceptors = function (identifier, object) { return this.interceptorRegistry.getInterceptors(identifier, object); }; +/** + * Inovke interceptors if applicable for a given domain object. + * @private + */ +ObjectAPI.prototype.applyGetInterceptors = function (identifier, domainObject) { + const interceptors = this.listGetInterceptors(identifier, domainObject); + interceptors.forEach(interceptor => { + domainObject = interceptor.invoke(identifier, domainObject); + }); + + return domainObject; +}; + /** * Modify a domain object. * @param {module:openmct.DomainObject} object the object to mutate diff --git a/src/plugins/persistence/couch/CouchObjectProvider.js b/src/plugins/persistence/couch/CouchObjectProvider.js index 0b14cd6bc7..ecdfa05bc8 100644 --- a/src/plugins/persistence/couch/CouchObjectProvider.js +++ b/src/plugins/persistence/couch/CouchObjectProvider.js @@ -57,11 +57,20 @@ export default class CouchObjectProvider { return options; } - request(subPath, method, value) { - return fetch(this.url + '/' + subPath, { - method: method, - body: JSON.stringify(value) - }).then(response => response.json()) + request(subPath, method, body, signal) { + let fetchOptions = { + method, + body, + signal + }; + + // stringify body if needed + if (fetchOptions.body) { + fetchOptions.body = JSON.stringify(fetchOptions.body); + } + + return fetch(this.url + '/' + subPath, fetchOptions) + .then(response => response.json()) .then(function (response) { return response; }, function () { @@ -121,8 +130,8 @@ export default class CouchObjectProvider { } } - get(identifier) { - return this.request(identifier.key, "GET").then(this.getModel.bind(this)); + get(identifier, abortSignal) { + return this.request(identifier.key, "GET", undefined, abortSignal).then(this.getModel.bind(this)); } async getObjectsByFilter(filter) { @@ -313,7 +322,8 @@ export default class CouchObjectProvider { this.enqueueObject(key, model, intermediateResponse); this.objectQueue[key].pending = true; const queued = this.objectQueue[key].dequeue(); - this.request(key, "PUT", new CouchDocument(key, queued.model)).then((response) => { + let document = new CouchDocument(key, queued.model); + this.request(key, "PUT", document).then((response) => { this.checkResponse(response, queued.intermediateResponse); }); @@ -324,7 +334,8 @@ export default class CouchObjectProvider { if (!this.objectQueue[key].pending) { this.objectQueue[key].pending = true; const queued = this.objectQueue[key].dequeue(); - this.request(key, "PUT", new CouchDocument(key, queued.model, this.objectQueue[key].rev)).then((response) => { + let document = new CouchDocument(key, queued.model, this.objectQueue[key].rev); + this.request(key, "PUT", document).then((response) => { this.checkResponse(response, queued.intermediateResponse); }); } diff --git a/src/ui/layout/mct-tree.vue b/src/ui/layout/mct-tree.vue index b90fe4131c..e55dea25b2 100644 --- a/src/ui/layout/mct-tree.vue +++ b/src/ui/layout/mct-tree.vue @@ -235,6 +235,12 @@ export default { }, watch: { syncTreeNavigation() { + // if there is an abort controller, then a search is in progress and will need to be canceled + if (this.abortController) { + this.abortController.abort(); + delete this.abortController; + } + this.searchValue = ''; if (!this.openmct.router.path) { @@ -685,12 +691,23 @@ export default { // clear any previous search results this.searchResultItems = []; - const promises = this.openmct.objects.search(this.searchValue) + // an abort controller will be passed in that will be used + // to cancel an active searches if necessary + this.abortController = new AbortController(); + const abortSignal = this.abortController.signal; + + const promises = this.openmct.objects.search(this.searchValue, abortSignal) .map(promise => promise .then(results => this.aggregateSearchResults(results))); Promise.all(promises).then(() => { this.searchLoading = false; + }).catch(reason => { + // search aborted + }).finally(() => { + if (this.abortController) { + delete this.abortController; + } }); }, async aggregateSearchResults(results) { @@ -714,6 +731,13 @@ export default { } }, searchTree(value) { + // if an abort controller exists, regardless of the value passed in, + // there is an active search that should be cancled + if (this.abortController) { + this.abortController.abort(); + delete this.abortController; + } + this.searchValue = value; this.searchLoading = true;