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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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