[Abort Search] Ability to Cancel Search Requests (#3716)

* adding first triggers for aborting search

* adding abort capabilities to the path a search request takes through the code

* switching empty args from null to undefined

* adding abortSignal to couchdb provider request function

* minor syntax tweak

* fixing accidental change of code

* simplifying the assignment of fetch options

* add finally to search promises to delete abort controller just in case it is still there

* passing signal in to provider.get not getProvider

* moving the couchdb doc creation out of the argument for request

* removing console log for aborted search error

* lint fix

* adding interceptors to objects.search

* removing the options object and replacing with abort signal

* removing unused variable leftover

* had accidentally removed stringifying the body of the request if present... added back in

* created an applyGetInterceptors function for search and get to use

* created an applyGetInterceptors function for search and get to use

* fixed bug that our TESTS FOUND!!!!
This commit is contained in:
Jamie V
2021-03-02 15:21:34 -08:00
committed by GitHub
parent 201d622b85
commit 5d656f0963
6 changed files with 90 additions and 31 deletions

View File

@ -37,7 +37,7 @@ define(
this.$q = $q; this.$q = $q;
} }
LocatingObjectDecorator.prototype.getObjects = function (ids) { LocatingObjectDecorator.prototype.getObjects = function (ids, abortSignal) {
var $q = this.$q, var $q = this.$q,
$log = this.$log, $log = this.$log,
objectService = this.objectService, 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) { ids.forEach(function (id) {

View File

@ -80,12 +80,15 @@ define([
* @param {Function} [filter] if provided, will be called for every * @param {Function} [filter] if provided, will be called for every
* potential modelResult. If it returns false, the model result will be * potential modelResult. If it returns false, the model result will be
* excluded from the search results. * 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. * @returns {Promise} A Promise for a search result object.
*/ */
SearchAggregator.prototype.query = function ( SearchAggregator.prototype.query = function (
inputText, inputText,
maxResults, maxResults,
filter filter,
abortSignal
) { ) {
var aggregator = this, var aggregator = this,
@ -120,7 +123,7 @@ define([
modelResults = aggregator.applyFilter(modelResults, filter); modelResults = aggregator.applyFilter(modelResults, filter);
modelResults = aggregator.removeDuplicates(modelResults); 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 * Convert modelResults to objectResults by fetching them from the object
* service. * 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. * @returns {Promise} for an objectResults object.
*/ */
SearchAggregator.prototype.asObjectResults = function (modelResults) { SearchAggregator.prototype.asObjectResults = function (modelResults, abortSignal) {
var objectIds = modelResults.hits.map(function (modelResult) { var objectIds = modelResults.hits.map(function (modelResult) {
return modelResult.id; return modelResult.id;
}); });
return this return this
.objectService .objectService
.getObjects(objectIds) .getObjects(objectIds, abortSignal)
.then(function (objects) { .then(function (objects) {
var objectResults = { var objectResults = {

View File

@ -139,10 +139,12 @@ define([
}); });
}; };
ObjectServiceProvider.prototype.superSecretFallbackSearch = function (query, options) { ObjectServiceProvider.prototype.superSecretFallbackSearch = function (query, abortSignal) {
const searchService = this.$injector.get('searchService'); 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. // 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) { function LegacyObjectAPIInterceptor(openmct, ROOTS, instantiate, topic, objectService) {
const eventEmitter = openmct.objects.eventEmitter; const eventEmitter = openmct.objects.eventEmitter;
this.getObjects = function (keys) { this.getObjects = function (keys, abortSignal) {
const results = {}; const results = {};
const promises = keys.map(function (keyString) { const promises = keys.map(function (keyString) {
const key = utils.parseKeyString(keyString); const key = utils.parseKeyString(keyString);
return openmct.objects.get(key) return openmct.objects.get(key, abortSignal)
.then(function (object) { .then(function (object) {
object = utils.toOldFormat(object); object = utils.toOldFormat(object);
results[keyString] = instantiate(object, keyString); results[keyString] = instantiate(object, keyString);

View File

@ -154,11 +154,12 @@ ObjectAPI.prototype.addProvider = function (namespace, provider) {
* @method get * @method get
* @memberof module:openmct.ObjectProvider# * @memberof module:openmct.ObjectProvider#
* @param {string} key the key for the domain object to load * @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 * @returns {Promise} a promise which will resolve when the domain object
* has been saved, or be rejected if it cannot be saved * 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); let keystring = this.makeKeyString(identifier);
if (this.cache[keystring] !== undefined) { if (this.cache[keystring] !== undefined) {
return this.cache[keystring]; return this.cache[keystring];
@ -175,15 +176,12 @@ ObjectAPI.prototype.get = function (identifier) {
throw new Error('Provider does not support get!'); throw new Error('Provider does not support get!');
} }
let objectPromise = provider.get(identifier); let objectPromise = provider.get(identifier, abortSignal);
this.cache[keystring] = objectPromise; this.cache[keystring] = objectPromise;
return objectPromise.then(result => { return objectPromise.then(result => {
delete this.cache[keystring]; delete this.cache[keystring];
const interceptors = this.listGetInterceptors(identifier, result); result = this.applyGetInterceptors(identifier, result);
interceptors.forEach(interceptor => {
result = interceptor.invoke(identifier, result);
});
return result; return result;
}); });
@ -200,19 +198,24 @@ ObjectAPI.prototype.get = function (identifier) {
* @method search * @method search
* @memberof module:openmct.ObjectAPI# * @memberof module:openmct.ObjectAPI#
* @param {string} query the term to search for * @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.<Promise.<module:openmct.DomainObject>>} * @returns {Array.<Promise.<module:openmct.DomainObject>>}
* an array of promises returned from each object provider's search function * an array of promises returned from each object provider's search function
* each resolving to domain objects matching provided search query and options. * 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) const searchPromises = Object.values(this.providers)
.filter(provider => provider.search !== undefined) .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 .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; return searchPromises;
}; };
@ -338,6 +341,19 @@ ObjectAPI.prototype.listGetInterceptors = function (identifier, object) {
return this.interceptorRegistry.getInterceptors(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. * Modify a domain object.
* @param {module:openmct.DomainObject} object the object to mutate * @param {module:openmct.DomainObject} object the object to mutate

View File

@ -57,11 +57,20 @@ export default class CouchObjectProvider {
return options; return options;
} }
request(subPath, method, value) { request(subPath, method, body, signal) {
return fetch(this.url + '/' + subPath, { let fetchOptions = {
method: method, method,
body: JSON.stringify(value) body,
}).then(response => response.json()) 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) { .then(function (response) {
return response; return response;
}, function () { }, function () {
@ -121,8 +130,8 @@ export default class CouchObjectProvider {
} }
} }
get(identifier) { get(identifier, abortSignal) {
return this.request(identifier.key, "GET").then(this.getModel.bind(this)); return this.request(identifier.key, "GET", undefined, abortSignal).then(this.getModel.bind(this));
} }
async getObjectsByFilter(filter) { async getObjectsByFilter(filter) {
@ -313,7 +322,8 @@ export default class CouchObjectProvider {
this.enqueueObject(key, model, intermediateResponse); this.enqueueObject(key, model, intermediateResponse);
this.objectQueue[key].pending = true; this.objectQueue[key].pending = true;
const queued = this.objectQueue[key].dequeue(); 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); this.checkResponse(response, queued.intermediateResponse);
}); });
@ -324,7 +334,8 @@ export default class CouchObjectProvider {
if (!this.objectQueue[key].pending) { if (!this.objectQueue[key].pending) {
this.objectQueue[key].pending = true; this.objectQueue[key].pending = true;
const queued = this.objectQueue[key].dequeue(); 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); this.checkResponse(response, queued.intermediateResponse);
}); });
} }

View File

@ -235,6 +235,12 @@ export default {
}, },
watch: { watch: {
syncTreeNavigation() { 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 = ''; this.searchValue = '';
if (!this.openmct.router.path) { if (!this.openmct.router.path) {
@ -685,12 +691,23 @@ export default {
// clear any previous search results // clear any previous search results
this.searchResultItems = []; 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 .map(promise => promise
.then(results => this.aggregateSearchResults(results))); .then(results => this.aggregateSearchResults(results)));
Promise.all(promises).then(() => { Promise.all(promises).then(() => {
this.searchLoading = false; this.searchLoading = false;
}).catch(reason => {
// search aborted
}).finally(() => {
if (this.abortController) {
delete this.abortController;
}
}); });
}, },
async aggregateSearchResults(results) { async aggregateSearchResults(results) {
@ -714,6 +731,13 @@ export default {
} }
}, },
searchTree(value) { 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.searchValue = value;
this.searchLoading = true; this.searchLoading = true;