When searching, build the path objects asynchronously while returning the results (#7265)

* build paths as fast as we can

* fix tests

* add abort controllers and async load tags
This commit is contained in:
Scott Bell 2023-12-04 22:40:28 +01:00 committed by GitHub
parent e7b9481aa9
commit 72e0621ecd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 75 additions and 39 deletions

View File

@ -366,15 +366,19 @@ export default class AnnotationAPI extends EventEmitter {
return tagsAddedToResults;
}
async #addTargetModelsToResults(results) {
async #addTargetModelsToResults(results, abortSignal) {
const modelAddedToResults = await Promise.all(
results.map(async (result) => {
const targetModels = await Promise.all(
result.targets.map(async (target) => {
const targetID = target.keyString;
const targetModel = await this.openmct.objects.get(targetID);
const targetModel = await this.openmct.objects.get(targetID, abortSignal);
const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);
const originalPathObjects = await this.openmct.objects.getOriginalPath(targetKeyString);
const originalPathObjects = await this.openmct.objects.getOriginalPath(
targetKeyString,
[],
abortSignal
);
return {
originalPath: originalPathObjects,
@ -442,7 +446,7 @@ export default class AnnotationAPI extends EventEmitter {
* @param {Object} [abortController] An optional abort method to stop the query
* @returns {Promise} returns a model of matching tags with their target domain objects attached
*/
async searchForTags(query, abortController) {
async searchForTags(query, abortSignal) {
const matchingTagKeys = this.#getMatchingTags(query);
if (!matchingTagKeys.length) {
return [];
@ -452,7 +456,7 @@ export default class AnnotationAPI extends EventEmitter {
await Promise.all(
this.openmct.objects.search(
matchingTagKeys,
abortController,
abortSignal,
this.openmct.objects.SEARCH_TYPES.TAGS
)
)
@ -465,7 +469,10 @@ export default class AnnotationAPI extends EventEmitter {
combinedSameTargets,
matchingTagKeys
);
const appliedTargetsModels = await this.#addTargetModelsToResults(appliedTagSearchResults);
const appliedTargetsModels = await this.#addTargetModelsToResults(
appliedTagSearchResults,
abortSignal
);
const resultsWithValidPath = appliedTargetsModels.filter((result) => {
return this.openmct.objects.isReachable(result.targetModels?.[0]?.originalPath);
});

View File

@ -786,16 +786,17 @@ export default class ObjectAPI {
* Given an identifier, constructs the original path by walking up its parents
* @param {module:openmct.ObjectAPI~Identifier} identifier
* @param {Array<module:openmct.DomainObject>} path an array of path objects
* @param {AbortSignal} abortSignal (optional) signal to abort fetch requests
* @returns {Promise<Array<module:openmct.DomainObject>>} a promise containing an array of domain objects
*/
async getOriginalPath(identifier, path = []) {
const domainObject = await this.get(identifier);
async getOriginalPath(identifier, path = [], abortSignal = null) {
const domainObject = await this.get(identifier, abortSignal);
path.push(domainObject);
const { location } = domainObject;
if (location && !this.#pathContainsDomainObject(location, path)) {
// if we have a location, and we don't already have this in our constructed path,
// then keep walking up the path
return this.getOriginalPath(utils.parseKeyString(location), path);
return this.getOriginalPath(utils.parseKeyString(location), path, abortSignal);
} else {
return path;
}

View File

@ -78,6 +78,7 @@ export default {
};
},
async mounted() {
this.abortController = new AbortController();
this.nameChangeListeners = {};
const keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
@ -87,7 +88,18 @@ export default {
let rawPath = null;
if (this.objectPath === null) {
rawPath = await this.openmct.objects.getOriginalPath(keyString);
try {
rawPath = await this.openmct.objects.getOriginalPath(
keyString,
[],
this.abortController.signal
);
} catch (error) {
// aborting the search is ok, everything else should be thrown
if (error.name !== 'AbortError') {
throw error;
}
}
} else {
rawPath = this.objectPath;
}
@ -115,6 +127,9 @@ export default {
}
},
unmounted() {
if (this.abortController) {
this.abortController.abort();
}
Object.values(this.nameChangeListeners).forEach((unlisten) => {
unlisten();
});

View File

@ -104,7 +104,7 @@ export default {
});
};
},
getPathsForObjects(objectsNeedingPaths) {
getPathsForObjects(objectsNeedingPaths, abortSignal) {
return Promise.all(
objectsNeedingPaths.map(async (domainObject) => {
if (!domainObject) {
@ -114,7 +114,9 @@ export default {
const keyStringForObject = this.openmct.objects.makeKeyString(domainObject.identifier);
const originalPathObjects = await this.openmct.objects.getOriginalPath(
keyStringForObject
keyStringForObject,
[],
abortSignal
);
return {
@ -130,45 +132,56 @@ export default {
this.searchLoading = true;
this.$refs.searchResultsDropDown.showSearchStarted();
this.abortSearchController = new AbortController();
const abortSignal = this.abortSearchController.signal;
try {
this.annotationSearchResults = await this.openmct.annotation.searchForTags(
this.searchValue,
abortSignal
);
const fullObjectSearchResults = await Promise.all(
this.openmct.objects.search(this.searchValue, abortSignal)
);
const aggregatedObjectSearchResults = fullObjectSearchResults.flat();
const aggregatedObjectSearchResultsWithPaths = await this.getPathsForObjects(
aggregatedObjectSearchResults
);
const filterAnnotationsAndValidPaths = aggregatedObjectSearchResultsWithPaths.filter(
(result) => {
if (this.openmct.annotation.isAnnotation(result)) {
return false;
}
return this.openmct.objects.isReachable(result?.objectPath);
}
);
this.objectSearchResults = filterAnnotationsAndValidPaths;
try {
const searchObjectsPromise = this.searchObjects(this.abortSearchController.signal);
const searchAnnotationsPromise = this.searchAnnotations(this.abortSearchController.signal);
// Wait for all promises, but they process their results as they complete
await Promise.allSettled([searchObjectsPromise, searchAnnotationsPromise]);
this.searchLoading = false;
this.showSearchResults();
} catch (error) {
this.searchLoading = false;
if (this.abortSearchController) {
delete this.abortSearchController;
}
// Is this coming from the AbortController?
// If so, we can swallow the error. If not, 🤮 it to console
if (error.name !== 'AbortError') {
console.error(`😞 Error searching`, error);
}
} finally {
if (this.abortSearchController) {
delete this.abortSearchController;
}
}
},
async searchObjects(abortSignal) {
const objectSearchPromises = this.openmct.objects.search(this.searchValue, abortSignal);
for await (const objectSearchResult of objectSearchPromises) {
const objectsWithPaths = await this.getPathsForObjects(objectSearchResult, abortSignal);
this.objectSearchResults.push(
...objectsWithPaths.filter((result) => {
// Check if the result is NOT an annotation and has a reachable path
return (
!this.openmct.annotation.isAnnotation(result) &&
this.openmct.objects.isReachable(result?.objectPath)
);
})
);
// Display the available results so far for objects
this.showSearchResults();
}
},
async searchAnnotations(abortSignal) {
const annotationSearchResults = await this.openmct.annotation.searchForTags(
this.searchValue,
abortSignal
);
this.annotationSearchResults = annotationSearchResults;
// Display the available results so far for annotations
this.showSearchResults();
},
showSearchResults() {
const dropdownOptions = {
searchLoading: this.searchLoading,

View File

@ -247,7 +247,7 @@ describe('GrandSearch', () => {
// eslint-disable-next-line require-await
mockObjectProvider.search = async (query, abortSignal, searchType) => {
if (searchType === openmct.objects.SEARCH_TYPES.OBJECTS) {
return mockNewObject;
return [mockNewObject];
} else {
return [];
}