Skip to content

Commit 72e0621

Browse files
authored
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
1 parent e7b9481 commit 72e0621

File tree

5 files changed

+74
-38
lines changed

5 files changed

+74
-38
lines changed

src/api/annotation/AnnotationAPI.js

+13-6
Original file line numberDiff line numberDiff line change
@@ -366,15 +366,19 @@ export default class AnnotationAPI extends EventEmitter {
366366
return tagsAddedToResults;
367367
}
368368

369-
async #addTargetModelsToResults(results) {
369+
async #addTargetModelsToResults(results, abortSignal) {
370370
const modelAddedToResults = await Promise.all(
371371
results.map(async (result) => {
372372
const targetModels = await Promise.all(
373373
result.targets.map(async (target) => {
374374
const targetID = target.keyString;
375-
const targetModel = await this.openmct.objects.get(targetID);
375+
const targetModel = await this.openmct.objects.get(targetID, abortSignal);
376376
const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);
377-
const originalPathObjects = await this.openmct.objects.getOriginalPath(targetKeyString);
377+
const originalPathObjects = await this.openmct.objects.getOriginalPath(
378+
targetKeyString,
379+
[],
380+
abortSignal
381+
);
378382

379383
return {
380384
originalPath: originalPathObjects,
@@ -442,7 +446,7 @@ export default class AnnotationAPI extends EventEmitter {
442446
* @param {Object} [abortController] An optional abort method to stop the query
443447
* @returns {Promise} returns a model of matching tags with their target domain objects attached
444448
*/
445-
async searchForTags(query, abortController) {
449+
async searchForTags(query, abortSignal) {
446450
const matchingTagKeys = this.#getMatchingTags(query);
447451
if (!matchingTagKeys.length) {
448452
return [];
@@ -452,7 +456,7 @@ export default class AnnotationAPI extends EventEmitter {
452456
await Promise.all(
453457
this.openmct.objects.search(
454458
matchingTagKeys,
455-
abortController,
459+
abortSignal,
456460
this.openmct.objects.SEARCH_TYPES.TAGS
457461
)
458462
)
@@ -465,7 +469,10 @@ export default class AnnotationAPI extends EventEmitter {
465469
combinedSameTargets,
466470
matchingTagKeys
467471
);
468-
const appliedTargetsModels = await this.#addTargetModelsToResults(appliedTagSearchResults);
472+
const appliedTargetsModels = await this.#addTargetModelsToResults(
473+
appliedTagSearchResults,
474+
abortSignal
475+
);
469476
const resultsWithValidPath = appliedTargetsModels.filter((result) => {
470477
return this.openmct.objects.isReachable(result.targetModels?.[0]?.originalPath);
471478
});

src/api/objects/ObjectAPI.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -786,16 +786,17 @@ export default class ObjectAPI {
786786
* Given an identifier, constructs the original path by walking up its parents
787787
* @param {module:openmct.ObjectAPI~Identifier} identifier
788788
* @param {Array<module:openmct.DomainObject>} path an array of path objects
789+
* @param {AbortSignal} abortSignal (optional) signal to abort fetch requests
789790
* @returns {Promise<Array<module:openmct.DomainObject>>} a promise containing an array of domain objects
790791
*/
791-
async getOriginalPath(identifier, path = []) {
792-
const domainObject = await this.get(identifier);
792+
async getOriginalPath(identifier, path = [], abortSignal = null) {
793+
const domainObject = await this.get(identifier, abortSignal);
793794
path.push(domainObject);
794795
const { location } = domainObject;
795796
if (location && !this.#pathContainsDomainObject(location, path)) {
796797
// if we have a location, and we don't already have this in our constructed path,
797798
// then keep walking up the path
798-
return this.getOriginalPath(utils.parseKeyString(location), path);
799+
return this.getOriginalPath(utils.parseKeyString(location), path, abortSignal);
799800
} else {
800801
return path;
801802
}

src/ui/components/ObjectPath.vue

+16-1
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export default {
7878
};
7979
},
8080
async mounted() {
81+
this.abortController = new AbortController();
8182
this.nameChangeListeners = {};
8283
const keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
8384

@@ -87,7 +88,18 @@ export default {
8788

8889
let rawPath = null;
8990
if (this.objectPath === null) {
90-
rawPath = await this.openmct.objects.getOriginalPath(keyString);
91+
try {
92+
rawPath = await this.openmct.objects.getOriginalPath(
93+
keyString,
94+
[],
95+
this.abortController.signal
96+
);
97+
} catch (error) {
98+
// aborting the search is ok, everything else should be thrown
99+
if (error.name !== 'AbortError') {
100+
throw error;
101+
}
102+
}
91103
} else {
92104
rawPath = this.objectPath;
93105
}
@@ -115,6 +127,9 @@ export default {
115127
}
116128
},
117129
unmounted() {
130+
if (this.abortController) {
131+
this.abortController.abort();
132+
}
118133
Object.values(this.nameChangeListeners).forEach((unlisten) => {
119134
unlisten();
120135
});

src/ui/layout/search/GrandSearch.vue

+40-27
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export default {
104104
});
105105
};
106106
},
107-
getPathsForObjects(objectsNeedingPaths) {
107+
getPathsForObjects(objectsNeedingPaths, abortSignal) {
108108
return Promise.all(
109109
objectsNeedingPaths.map(async (domainObject) => {
110110
if (!domainObject) {
@@ -114,7 +114,9 @@ export default {
114114

115115
const keyStringForObject = this.openmct.objects.makeKeyString(domainObject.identifier);
116116
const originalPathObjects = await this.openmct.objects.getOriginalPath(
117-
keyStringForObject
117+
keyStringForObject,
118+
[],
119+
abortSignal
118120
);
119121

120122
return {
@@ -130,45 +132,56 @@ export default {
130132
this.searchLoading = true;
131133
this.$refs.searchResultsDropDown.showSearchStarted();
132134
this.abortSearchController = new AbortController();
133-
const abortSignal = this.abortSearchController.signal;
135+
134136
try {
135-
this.annotationSearchResults = await this.openmct.annotation.searchForTags(
136-
this.searchValue,
137-
abortSignal
138-
);
139-
const fullObjectSearchResults = await Promise.all(
140-
this.openmct.objects.search(this.searchValue, abortSignal)
141-
);
142-
const aggregatedObjectSearchResults = fullObjectSearchResults.flat();
143-
const aggregatedObjectSearchResultsWithPaths = await this.getPathsForObjects(
144-
aggregatedObjectSearchResults
145-
);
146-
const filterAnnotationsAndValidPaths = aggregatedObjectSearchResultsWithPaths.filter(
147-
(result) => {
148-
if (this.openmct.annotation.isAnnotation(result)) {
149-
return false;
150-
}
137+
const searchObjectsPromise = this.searchObjects(this.abortSearchController.signal);
138+
const searchAnnotationsPromise = this.searchAnnotations(this.abortSearchController.signal);
139+
140+
// Wait for all promises, but they process their results as they complete
141+
await Promise.allSettled([searchObjectsPromise, searchAnnotationsPromise]);
151142

152-
return this.openmct.objects.isReachable(result?.objectPath);
153-
}
154-
);
155-
this.objectSearchResults = filterAnnotationsAndValidPaths;
156143
this.searchLoading = false;
157144
this.showSearchResults();
158145
} catch (error) {
159146
this.searchLoading = false;
160147

161-
if (this.abortSearchController) {
162-
delete this.abortSearchController;
163-
}
164-
165148
// Is this coming from the AbortController?
166149
// If so, we can swallow the error. If not, 🤮 it to console
167150
if (error.name !== 'AbortError') {
168151
console.error(`😞 Error searching`, error);
169152
}
153+
} finally {
154+
if (this.abortSearchController) {
155+
delete this.abortSearchController;
156+
}
157+
}
158+
},
159+
async searchObjects(abortSignal) {
160+
const objectSearchPromises = this.openmct.objects.search(this.searchValue, abortSignal);
161+
for await (const objectSearchResult of objectSearchPromises) {
162+
const objectsWithPaths = await this.getPathsForObjects(objectSearchResult, abortSignal);
163+
this.objectSearchResults.push(
164+
...objectsWithPaths.filter((result) => {
165+
// Check if the result is NOT an annotation and has a reachable path
166+
return (
167+
!this.openmct.annotation.isAnnotation(result) &&
168+
this.openmct.objects.isReachable(result?.objectPath)
169+
);
170+
})
171+
);
172+
// Display the available results so far for objects
173+
this.showSearchResults();
170174
}
171175
},
176+
async searchAnnotations(abortSignal) {
177+
const annotationSearchResults = await this.openmct.annotation.searchForTags(
178+
this.searchValue,
179+
abortSignal
180+
);
181+
this.annotationSearchResults = annotationSearchResults;
182+
// Display the available results so far for annotations
183+
this.showSearchResults();
184+
},
172185
showSearchResults() {
173186
const dropdownOptions = {
174187
searchLoading: this.searchLoading,

src/ui/layout/search/GrandSearchSpec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ describe('GrandSearch', () => {
247247
// eslint-disable-next-line require-await
248248
mockObjectProvider.search = async (query, abortSignal, searchType) => {
249249
if (searchType === openmct.objects.SEARCH_TYPES.OBJECTS) {
250-
return mockNewObject;
250+
return [mockNewObject];
251251
} else {
252252
return [];
253253
}

0 commit comments

Comments
 (0)