search: ignore searchResults if it isn't the active query

Prior to this, the search is still racy but one tends to notice
this only when the DB is large or network is involved.
The user can initiate a search, get bored, navigate to another chan
issue a different search.

Now however, the results of the first search come back in and
hilarity ensues as we are now confused with the state.

To avoid this, keep track of the last search done and any result
that comes in that isn't equal to the active query is garbage and
can be dropped.
This commit is contained in:
Reto Brunner 2022-11-26 17:14:09 +01:00
parent 8b1a4f72fa
commit 0ebc3a574c
8 changed files with 59 additions and 46 deletions

View file

@ -33,18 +33,19 @@
<button
ref="loadMoreButton"
:disabled="
store.state.messageSearchInProgress || !store.state.isConnected
!!store.state.messageSearchPendingQuery ||
!store.state.isConnected
"
class="btn"
@click="onShowMoreClick"
>
<span v-if="store.state.messageSearchInProgress">Loading</span>
<span v-if="store.state.messageSearchPendingQuery">Loading</span>
<span v-else>Show older messages</span>
</button>
</div>
<div
v-if="store.state.messageSearchInProgress && !offset"
v-if="store.state.messageSearchPendingQuery && !offset"
class="search-status"
>
Searching
@ -105,6 +106,7 @@ import type {ClientMessage} from "../../js/types";
import {useStore} from "../../js/store";
import {useRoute, useRouter} from "vue-router";
import {switchToChannel} from "../../js/router";
import {SearchQuery} from "../../../server/plugins/messageStorage/types";
export default defineComponent({
name: "SearchResults",
@ -187,37 +189,44 @@ export default defineComponent({
const clearSearchState = () => {
offset.value = 0;
store.commit("messageSearchInProgress", false);
store.commit("messageSearchResults", null);
store.commit("messageSearchPendingQuery", null);
};
const doSearch = () => {
if (!network.value || !channel.value) {
return;
}
clearSearchState(); // this is a new search, so we need to clear anything before that
socket.emit("search", {
networkUuid: network.value?.uuid,
channelName: channel.value?.name,
const query: SearchQuery = {
networkUuid: network.value.uuid,
channelName: channel.value.name,
searchTerm: String(route.query.q || ""),
offset: offset.value,
});
};
store.commit("messageSearchPendingQuery", query);
socket.emit("search", query);
};
const onShowMoreClick = () => {
if (!chat.value) {
if (!chat.value || !network.value || !channel.value) {
return;
}
offset.value += 100;
store.commit("messageSearchInProgress", true);
oldScrollTop.value = chat.value.scrollTop;
oldChatHeight.value = chat.value.scrollHeight;
socket.emit("search", {
networkUuid: network.value?.uuid,
channelName: channel.value?.name,
const query: SearchQuery = {
networkUuid: network.value.uuid,
channelName: channel.value.name,
searchTerm: String(route.query.q || ""),
offset: offset.value,
});
};
store.commit("messageSearchPendingQuery", query);
socket.emit("search", query);
};
const jumpToBottom = async () => {

View file

@ -2,12 +2,27 @@ import socket from "../socket";
import {store} from "../store";
socket.on("search:results", (response) => {
store.commit("messageSearchInProgress", false);
const pendingQuery = store.state.messageSearchPendingQuery;
if (
!pendingQuery ||
pendingQuery.channelName !== response.channelName ||
pendingQuery.networkUuid !== response.networkUuid ||
pendingQuery.offset !== response.offset ||
pendingQuery.searchTerm !== response.searchTerm
) {
// This is a response from a search that we are not interested in.
// The user may have entered a different search while one was still in flight.
// We can simply drop it on the floor.
return;
}
store.commit("messageSearchPendingQuery", null);
if (store.state.messageSearchResults) {
store.commit("addMessageSearchResults", response);
return;
}
store.commit("messageSearchResults", response);
store.commit("messageSearchResults", {results: response.results});
});

View file

@ -15,6 +15,7 @@ import type {
import type {InjectionKey} from "vue";
import {SettingsState} from "./settings";
import {SearchQuery} from "../../server/plugins/messageStorage/types";
const appName = document.title;
@ -85,7 +86,7 @@ export type State = {
messageSearchResults: {
results: ClientMessage[];
} | null;
messageSearchInProgress: boolean;
messageSearchPendingQuery: SearchQuery | null;
searchEnabled: boolean;
};
@ -111,7 +112,7 @@ const state = () =>
versionDataExpired: false,
serverHasSettings: false,
messageSearchResults: null,
messageSearchInProgress: false,
messageSearchPendingQuery: null,
searchEnabled: false,
} as State);
@ -260,7 +261,7 @@ type Mutations = {
versionStatus(state: State, payload: State["versionStatus"]): void;
versionDataExpired(state: State, payload: State["versionDataExpired"]): void;
serverHasSettings(state: State, value: State["serverHasSettings"]): void;
messageSearchInProgress(state: State, value: State["messageSearchInProgress"]): void;
messageSearchPendingQuery(state: State, value: State["messageSearchPendingQuery"]): void;
messageSearchResults(state: State, value: State["messageSearchResults"]): void;
addMessageSearchResults(state: State, value: NonNullable<State["messageSearchResults"]>): void;
};
@ -338,8 +339,8 @@ const mutations: Mutations = {
serverHasSettings(state, value) {
state.serverHasSettings = value;
},
messageSearchInProgress(state, value) {
state.messageSearchInProgress = value;
messageSearchPendingQuery(state, value) {
state.messageSearchPendingQuery = value;
},
messageSearchResults(state, value) {
state.messageSearchResults = value;

View file

@ -17,7 +17,7 @@ import SqliteMessageStorage from "./plugins/messageStorage/sqlite";
import TextFileMessageStorage from "./plugins/messageStorage/text";
import Network, {IgnoreListItem, NetworkWithIrcFramework} from "./models/network";
import ClientManager from "./clientManager";
import {MessageStorage, SearchQuery} from "./plugins/messageStorage/types";
import {MessageStorage, SearchQuery, SearchResponse} from "./plugins/messageStorage/types";
type OrderItem = Chan["id"] | Network["uuid"];
type Order = OrderItem[];
@ -618,15 +618,12 @@ class Client {
}
}
search(query: SearchQuery) {
async search(query: SearchQuery): Promise<SearchResponse> {
if (!this.messageProvider?.isEnabled) {
return Promise.resolve({
return {
...query,
results: [],
target: "",
networkUuid: "",
offset: 0,
searchTerm: query?.searchTerm,
});
};
}
return this.messageProvider.search(query);

View file

@ -259,15 +259,10 @@ class SqliteMessageStorage implements ISqliteMessageStorage {
params.push(query.offset);
const rows = await this.serialize_fetchall(select, ...params);
const response: SearchResponse = {
searchTerm: query.searchTerm,
target: query.channelName,
networkUuid: query.networkUuid,
offset: query.offset,
return {
...query,
results: parseSearchRowsToMessages(query.offset, rows).reverse(),
};
return response;
}
canProvideMessages() {

View file

@ -29,12 +29,9 @@ export type SearchQuery = {
offset: number;
};
export type SearchResponse =
| Omit<SearchQuery, "channelName" | "offset"> & {
results: Message[];
target: string;
offset: number;
};
export type SearchResponse = SearchQuery & {
results: Message[];
};
type SearchFunction = (query: SearchQuery) => Promise<SearchResponse>;

View file

@ -760,9 +760,8 @@ function initializeClient(
});
socket.on("search", async (query) => {
await client.search(query).then((results) => {
socket.emit("search:results", results);
});
const results = await client.search(query);
socket.emit("search:results", results);
});
socket.on("mute:change", ({target, setMutedTo}) => {

View file

@ -107,7 +107,7 @@ interface ServerToClientEvents {
token: string;
}) => void;
"search:results": (response: {results: ClientMessage[]}) => void;
"search:results": (response: SearchResponse) => void;
quit: (args: {network: string}) => void;