diff --git a/src/api/annotation/AnnotationAPI.js b/src/api/annotation/AnnotationAPI.js index 3c75f00009..1211a7d9bc 100644 --- a/src/api/annotation/AnnotationAPI.js +++ b/src/api/annotation/AnnotationAPI.js @@ -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); }); diff --git a/src/api/objects/ObjectAPI.js b/src/api/objects/ObjectAPI.js index 6013a88361..e7a5bd3e55 100644 --- a/src/api/objects/ObjectAPI.js +++ b/src/api/objects/ObjectAPI.js @@ -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} path an array of path objects + * @param {AbortSignal} abortSignal (optional) signal to abort fetch requests * @returns {Promise>} 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; } diff --git a/src/ui/components/ObjectPath.vue b/src/ui/components/ObjectPath.vue index b6622b1e5c..d0fe7d4ff9 100644 --- a/src/ui/components/ObjectPath.vue +++ b/src/ui/components/ObjectPath.vue @@ -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(); }); diff --git a/src/ui/layout/search/GrandSearch.vue b/src/ui/layout/search/GrandSearch.vue index a773de4323..8f1497aabb 100644 --- a/src/ui/layout/search/GrandSearch.vue +++ b/src/ui/layout/search/GrandSearch.vue @@ -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, diff --git a/src/ui/layout/search/GrandSearchSpec.js b/src/ui/layout/search/GrandSearchSpec.js index 2e76cbc95a..7d90e85ace 100644 --- a/src/ui/layout/search/GrandSearchSpec.js +++ b/src/ui/layout/search/GrandSearchSpec.js @@ -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 []; }