[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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 90 additions and 31 deletions

View File

@ -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) {

View File

@ -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 = {

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');
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);

View File

@ -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.<Promise.<module:openmct.DomainObject>>}
* 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

View File

@ -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);
});
}

View File

@ -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;