fix: throttle XHRs from autosuggest (#1190)
* fix: throttle XHRs from autosuggest * throttle and abort properly * add comment * fix xhr bug
This commit is contained in:
parent
cef76e6bba
commit
de220e7262
|
@ -148,7 +148,8 @@
|
|||
"NodeList",
|
||||
"DOMParser",
|
||||
"CSS",
|
||||
"customElements"
|
||||
"customElements",
|
||||
"AbortController"
|
||||
],
|
||||
"ignore": [
|
||||
"dist",
|
||||
|
|
|
@ -5,8 +5,10 @@ import { SEARCH_RESULTS_LIMIT } from '../_static/autosuggest'
|
|||
import { concat } from '../_utils/arrays'
|
||||
import uniqBy from 'lodash-es/uniqBy'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
||||
import { PromiseThrottler } from '../_utils/PromiseThrottler'
|
||||
|
||||
const DATABASE_SEARCH_RESULTS_LIMIT = 30
|
||||
const promiseThrottler = new PromiseThrottler(200) // Mastodon FE also uses 200ms
|
||||
|
||||
function byUsername (a, b) {
|
||||
let usernameA = a.acct.toLowerCase()
|
||||
|
@ -24,6 +26,14 @@ export function doAccountSearch (searchText) {
|
|||
let localResults
|
||||
let remoteResults
|
||||
let { currentInstance, accessToken } = store.get()
|
||||
let controller = typeof AbortController === 'function' && new AbortController()
|
||||
|
||||
function abortFetch () {
|
||||
if (controller) {
|
||||
controller.abort()
|
||||
controller = null
|
||||
}
|
||||
}
|
||||
|
||||
async function searchAccountsLocally (searchText) {
|
||||
localResults = await database.searchAccountsByUsername(
|
||||
|
@ -31,7 +41,14 @@ export function doAccountSearch (searchText) {
|
|||
}
|
||||
|
||||
async function searchAccountsRemotely (searchText) {
|
||||
remoteResults = (await search(currentInstance, accessToken, searchText, false, SEARCH_RESULTS_LIMIT)).accounts
|
||||
// Throttle our XHRs to be a good citizen and not spam the server with one XHR per keystroke
|
||||
await promiseThrottler.next()
|
||||
if (canceled) {
|
||||
return
|
||||
}
|
||||
remoteResults = (await search(
|
||||
currentInstance, accessToken, searchText, false, SEARCH_RESULTS_LIMIT, controller && controller.signal
|
||||
)).accounts
|
||||
}
|
||||
|
||||
function mergeAndTruncateResults () {
|
||||
|
@ -81,6 +98,7 @@ export function doAccountSearch (searchText) {
|
|||
return {
|
||||
cancel: () => {
|
||||
canceled = true
|
||||
abortFetch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import { get, paramsString, DEFAULT_TIMEOUT } from '../_utils/ajax'
|
||||
import { auth, basename } from './utils'
|
||||
|
||||
export function search (instanceName, accessToken, query, resolve = true, limit = 40) {
|
||||
export function search (instanceName, accessToken, query, resolve = true, limit = 40, signal = null) {
|
||||
let url = `${basename(instanceName)}/api/v1/search?` + paramsString({
|
||||
q: query,
|
||||
resolve,
|
||||
limit
|
||||
})
|
||||
return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
|
||||
return get(url, auth(accessToken), {
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
signal
|
||||
})
|
||||
}
|
||||
|
|
|
@ -6,29 +6,21 @@ export function autosuggestObservers () {
|
|||
let lastSearch
|
||||
|
||||
store.observe('autosuggestSearchText', async autosuggestSearchText => {
|
||||
let { composeFocused } = store.get()
|
||||
if (!composeFocused || !autosuggestSearchText) {
|
||||
return
|
||||
}
|
||||
/* autosuggestSelecting indicates that the user has pressed Enter or clicked on an item
|
||||
and the results are being processed. Returning early avoids a flash of searched content.
|
||||
We can also cancel any inflight XHRs here.
|
||||
*/
|
||||
let autosuggestSelecting = store.getForCurrentAutosuggest('autosuggestSelecting')
|
||||
if (autosuggestSelecting) {
|
||||
// cancel any inflight XHRs or other operations
|
||||
if (lastSearch) {
|
||||
lastSearch.cancel()
|
||||
lastSearch = null
|
||||
}
|
||||
// autosuggestSelecting indicates that the user has pressed Enter or clicked on an item
|
||||
// and the results are being processed. Returning early avoids a flash of searched content.
|
||||
let { composeFocused } = store.get()
|
||||
let autosuggestSelecting = store.getForCurrentAutosuggest('autosuggestSelecting')
|
||||
if (!composeFocused || !autosuggestSearchText || autosuggestSelecting) {
|
||||
return
|
||||
}
|
||||
|
||||
let autosuggestType = autosuggestSearchText.startsWith('@') ? 'account' : 'emoji'
|
||||
|
||||
if (lastSearch) {
|
||||
lastSearch.cancel()
|
||||
}
|
||||
|
||||
if (autosuggestType === 'emoji') {
|
||||
lastSearch = doEmojiSearch(autosuggestSearchText)
|
||||
} else {
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
// Utility for throttling in the Lodash style (assuming leading: true and trailing: true) but
|
||||
// creates a promise.
|
||||
export class PromiseThrottler {
|
||||
constructor (timeout) {
|
||||
this._timeout = timeout
|
||||
this._promise = Promise.resolve()
|
||||
}
|
||||
|
||||
next () {
|
||||
let res = this._promise
|
||||
// update afterwards, so we get a "leading" XHR
|
||||
this._promise = this._promise.then(() => new Promise(resolve => setTimeout(resolve, this._timeout)))
|
||||
return res
|
||||
}
|
||||
}
|
|
@ -9,13 +9,17 @@ function fetchWithTimeout (url, fetchOptions, timeout) {
|
|||
})
|
||||
}
|
||||
|
||||
function makeFetchOptions (method, headers) {
|
||||
return {
|
||||
function makeFetchOptions (method, headers, options) {
|
||||
let res = {
|
||||
method,
|
||||
headers: Object.assign(headers || {}, {
|
||||
'Accept': 'application/json'
|
||||
})
|
||||
}
|
||||
if (options && options.signal) {
|
||||
res.signal = options.signal
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
async function throwErrorIfInvalidResponse (response) {
|
||||
|
@ -40,7 +44,7 @@ async function _fetch (url, fetchOptions, options) {
|
|||
}
|
||||
|
||||
async function _putOrPostOrPatch (method, url, body, headers, options) {
|
||||
let fetchOptions = makeFetchOptions(method, headers)
|
||||
let fetchOptions = makeFetchOptions(method, headers, options)
|
||||
if (body) {
|
||||
if (body instanceof FormData) {
|
||||
fetchOptions.body = body
|
||||
|
@ -65,11 +69,11 @@ export async function patch (url, body, headers, options) {
|
|||
}
|
||||
|
||||
export async function get (url, headers, options) {
|
||||
return _fetch(url, makeFetchOptions('GET', headers), options)
|
||||
return _fetch(url, makeFetchOptions('GET', headers, options), options)
|
||||
}
|
||||
|
||||
export async function del (url, headers, options) {
|
||||
return _fetch(url, makeFetchOptions('DELETE', headers), options)
|
||||
return _fetch(url, makeFetchOptions('DELETE', headers, options), options)
|
||||
}
|
||||
|
||||
export function paramsString (paramsObject) {
|
||||
|
|
Loading…
Reference in New Issue