mirror of
https://github.com/nasa/openmct.git
synced 2025-06-23 01:18:57 +00:00
[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:
@ -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) {
|
||||||
|
@ -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 = {
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user