Mailspring/internal_packages/thread-search/lib/search-store.es6
Mark Hahnenberg c7d8f1700d [thread-search] Background on-keypress search queries
Summary:
When the search index got very big the queries we were running during
after keypress would cause jank in the UI which was very noticeable on
subsequent keypresses. This diff backgrounds these queries and adds a
LIMIT to the fts MATCH clauses to avoid having to send too much stuff
across the IPC bridge.

Test Plan: Run locally, make sure that typing is smooth while searching

Reviewers: juan, evan

Reviewed By: juan, evan

Differential Revision: https://phab.nylas.com/D3757
2017-01-23 11:50:27 -08:00

221 lines
6.3 KiB
JavaScript

import NylasStore from 'nylas-store';
import {
Thread,
Actions,
ContactStore,
AccountStore,
DatabaseStore,
ComponentRegistry,
FocusedPerspectiveStore,
} from 'nylas-exports';
import {parseSearchQuery} from './search-query-parser'
import SearchActions from './search-actions';
import SearchMailboxPerspective from './search-mailbox-perspective';
// Stores should closely match the needs of a particular part of the front end.
// For example, we might create a "MessageStore" that observes this store
// for changes in selectedThread, "DatabaseStore" for changes to the underlying database,
// and vends up the array used for that view.
class SearchStore extends NylasStore {
constructor() {
super();
this._searchQuery = FocusedPerspectiveStore.current().searchQuery || "";
this._searchSuggestionsVersion = 1;
this._isSearching = false;
this._extensionData = []
this._clearResults();
this.listenTo(FocusedPerspectiveStore, this._onPerspectiveChanged);
this.listenTo(SearchActions.querySubmitted, this._onQuerySubmitted);
this.listenTo(SearchActions.queryChanged, this._onQueryChanged);
this.listenTo(SearchActions.searchBlurred, this._onSearchBlurred);
this.listenTo(SearchActions.searchCompleted, this._onSearchCompleted);
}
query() {
return this._searchQuery;
}
queryPopulated() {
return this._searchQuery && this._searchQuery.trim().length > 0;
}
suggestions() {
return this._suggestions;
}
isSearching() {
return this._isSearching;
}
_onSearchCompleted = () => {
this._isSearching = false;
this.trigger();
}
_onPerspectiveChanged = () => {
this._searchQuery = FocusedPerspectiveStore.current().searchQuery || "";
this.trigger();
}
_onQueryChanged = (query) => {
this._searchQuery = query;
if (this._searchQuery.length <= 1) {
this.trigger()
return
}
this._compileResults();
setTimeout(() => this._rebuildResults(), 0);
}
_onQuerySubmitted = (query) => {
this._searchQuery = query;
const current = FocusedPerspectiveStore.current();
if (this.queryPopulated()) {
this._isSearching = true;
if (this._perspectiveBeforeSearch == null) {
this._perspectiveBeforeSearch = current;
}
const next = new SearchMailboxPerspective(current.accountIds, this._searchQuery.trim());
Actions.focusMailboxPerspective(next);
} else if (current instanceof SearchMailboxPerspective) {
if (this._perspectiveBeforeSearch) {
Actions.focusMailboxPerspective(this._perspectiveBeforeSearch);
this._perspectiveBeforeSearch = null;
} else {
Actions.focusDefaultMailboxPerspectiveForAccounts(AccountStore.accounts());
}
}
this._clearResults();
}
_onSearchBlurred = () => {
this._clearResults();
}
_clearResults() {
this._searchSuggestionsVersion = 1;
this._threadResults = [];
this._contactResults = [];
this._suggestions = [];
this.trigger();
}
_rebuildResults() {
if (!this.queryPopulated()) {
this._clearResults();
return;
}
this._searchSuggestionsVersion += 1;
const searchExtensions = ComponentRegistry.findComponentsMatching({
role: "SearchBarResults",
})
Promise.map(searchExtensions, (ext) => {
return Promise.props({
label: ext.searchLabel(),
suggestions: ext.fetchSearchSuggestions(this._searchQuery),
})
}).then((extensionData = []) => {
this._extensionData = extensionData;
this._compileResults();
})
this._fetchThreadResults();
this._fetchContactResults();
}
_fetchContactResults() {
const version = this._searchSuggestionsVersion;
ContactStore.searchContacts(this._searchQuery, {limit: 10}).then(contacts => {
if (version !== this._searchSuggestionsVersion) {
return;
}
this._contactResults = contacts;
this._compileResults();
});
}
_fetchThreadResults() {
if (this._fetchingThreadResultsVersion) { return; }
this._fetchingThreadResultsVersion = this._searchSuggestionsVersion;
const {accountIds} = FocusedPerspectiveStore.current();
let dbQuery = DatabaseStore.findAll(Thread).distinct()
if (Array.isArray(accountIds) && accountIds.length === 1) {
dbQuery = dbQuery.where({accountId: accountIds[0]})
}
try {
const parsedQuery = parseSearchQuery(this._searchQuery);
// console.info('Successfully parsed and codegened search query', parsedQuery);
dbQuery = dbQuery.structuredSearch(parsedQuery);
} catch (e) {
// console.info('Failed to parse local search query, falling back to generic query', e);
dbQuery = dbQuery.search(this._searchQuery);
}
dbQuery = dbQuery
.order(Thread.attributes.lastMessageReceivedTimestamp.descending())
// console.info(dbQuery.sql());
dbQuery.background().then(results => {
// We've fetched the latest thread results - display them!
if (this._searchSuggestionsVersion === this._fetchingThreadResultsVersion) {
this._fetchingThreadResultsVersion = null;
this._threadResults = results;
this._compileResults();
// We're behind and need to re-run the search for the latest results
} else if (this._searchSuggestionsVersion > this._fetchingThreadResultsVersion) {
this._fetchingThreadResultsVersion = null;
this._fetchThreadResults();
} else {
this._fetchingThreadResultsVersion = null;
}
}
);
}
_compileResults() {
this._suggestions = [];
this._suggestions.push({
label: `Message Contains: ${this._searchQuery}`,
value: this._searchQuery,
});
if (this._threadResults.length) {
this._suggestions.push({divider: 'Threads'});
for (const thread of this._threadResults) {
this._suggestions.push({thread});
}
}
if (this._contactResults.length) {
this._suggestions.push({divider: 'People'});
for (const contact of this._contactResults) {
this._suggestions.push({
contact: contact,
value: contact.email,
});
}
}
if (this._extensionData.length) {
for (const {label, suggestions} of this._extensionData) {
this._suggestions.push({divider: label});
this._suggestions = this._suggestions.concat(suggestions)
}
}
this.trigger();
}
}
export default new SearchStore();