Skip to content

Commit 4f6c656

Browse files
skearnesclaude
andcommitted
app: fix /search hang when the route changes via $router.push
Symptom: clicking "Search" from the filters panel left the spinner running forever, but refreshing the page made the same query complete quickly. Root cause: the polling loop that handles `fetch_query_result` returning 202 (task pending) was only set up inside `mounted()`. When the user submits the filter form, `updateSearchOptions` calls `this.$router.push({name: 'search', query: this.searchParams})`, which navigates within the same component. Vue Router reuses the mounted component, so `mounted()` does NOT re-fire — only the watch on `$route.query` does. The watch was calling `getSearchResults()` directly, missing the polling-setup branch. The first `fetch_query_result` poll returned 202, the response handler had no branch for that case, and the spinner stuck. Refresh worked because the component remounts on a hard page load. Fix: pull the "fetch then poll if 202" logic out of `mounted` into `runSearch()`. Both `mounted` and the watch call `runSearch`, so the polling-setup runs in both cases. Also: - `stopPolling()` helper that clears the interval AND nulls `searchPollingInterval`. The previous code only called `clearInterval(...)`; the handle was never reset, so subsequent searches saw a stale truthy value and skipped polling setup. - `runSearch` calls `stopPolling()` and resets `searchTaskId` at the top, so a new query doesn't poll for the previous query's stale task ID. - Drop the dead `await … .then(…)` pattern in `mounted`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c9534dd commit 4f6c656

1 file changed

Lines changed: 37 additions & 17 deletions

File tree

app/src/views/search/MainSearch.vue

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,12 @@ export default {
3030
watch: {
3131
'$route.query': {
3232
handler() {
33-
this.getSearchResults();
33+
// Submitting a search via the filter panel calls $router.push with new
34+
// query params. Vue Router reuses the component, so `mounted` does NOT
35+
// re-fire — only this watcher does. We have to kick off `runSearch`
36+
// (not `getSearchResults` directly) so polling is wired up for 202
37+
// responses; otherwise the spinner hangs until the user refreshes.
38+
this.runSearch();
3439
},
3540
deep: true,
3641
},
@@ -75,17 +80,17 @@ export default {
7580
// If one of these codes, search is finished. Return the results or lack of results.
7681
if (res?.status == 200) {
7782
this.searchTaskId = null;
78-
clearInterval(this.searchPollingInterval);
83+
this.stopPolling();
7984
return res.json();
8085
} else if (res?.status == 404) {
8186
let taskId = this.searchTaskId;
8287
this.searchTaskId = null;
83-
clearInterval(this.searchPollingInterval);
88+
this.stopPolling();
8489
throw new Error('Error - Search task ID ' + taskId + ' does not exist');
8590
} else if (res?.status >= 500) {
8691
let taskId = this.searchTaskId;
8792
this.searchTaskId = null;
88-
clearInterval(this.searchPollingInterval);
93+
this.stopPolling();
8994
throw new Error('Error - Search task ID ' + taskId + ' failed due to server error');
9095
}
9196
})
@@ -110,6 +115,33 @@ export default {
110115
this.loading = false;
111116
}
112117
},
118+
stopPolling() {
119+
// Idempotent: safe to call even when no interval is active. Keeps
120+
// `searchPollingInterval` truthful so the next runSearch() correctly
121+
// sees "no polling in progress".
122+
if (this.searchPollingInterval !== null) {
123+
clearInterval(this.searchPollingInterval);
124+
this.searchPollingInterval = null;
125+
}
126+
},
127+
async runSearch() {
128+
// Cancel any in-flight polling from a previous query, and discard any
129+
// stale task ID so the next getSearchResults() submits a fresh task
130+
// against the current URL params (not the previous query's task).
131+
this.stopPolling();
132+
this.searchTaskId = null;
133+
await this.getSearchResults();
134+
if (this.searchLoadStatus?.status == 202 && this.searchPollingInterval == null) {
135+
this.searchPollingInterval = setInterval(() => {
136+
this.getSearchResults();
137+
}, 1000);
138+
setTimeout(() => {
139+
this.stopPolling();
140+
this.searchTaskId = null;
141+
this.loading = false;
142+
}, 120000);
143+
}
144+
},
113145
updateSearchOptions(options) {
114146
// reagent options
115147
if (options.reagent.reagents.length) {
@@ -161,19 +193,7 @@ export default {
161193
},
162194
},
163195
async mounted() {
164-
// Fetch results. If server returns a 202, set up a poll to keep checking back until we have results.
165-
await this.getSearchResults().then(() => {
166-
if (this.searchLoadStatus?.status == 202 && this.searchPollingInterval == null) {
167-
this.searchPollingInterval = setInterval(() => {
168-
this.getSearchResults();
169-
}, 1000);
170-
setTimeout(() => {
171-
clearInterval(this.searchPollingInterval);
172-
this.searchTaskId = null;
173-
this.loading = false;
174-
}, 120000);
175-
}
176-
});
196+
await this.runSearch();
177197
},
178198
};
179199
</script>

0 commit comments

Comments
 (0)