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",
|
"NodeList",
|
||||||
"DOMParser",
|
"DOMParser",
|
||||||
"CSS",
|
"CSS",
|
||||||
"customElements"
|
"customElements",
|
||||||
|
"AbortController"
|
||||||
],
|
],
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|
|
@ -5,8 +5,10 @@ import { SEARCH_RESULTS_LIMIT } from '../_static/autosuggest'
|
||||||
import { concat } from '../_utils/arrays'
|
import { concat } from '../_utils/arrays'
|
||||||
import uniqBy from 'lodash-es/uniqBy'
|
import uniqBy from 'lodash-es/uniqBy'
|
||||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
||||||
|
import { PromiseThrottler } from '../_utils/PromiseThrottler'
|
||||||
|
|
||||||
const DATABASE_SEARCH_RESULTS_LIMIT = 30
|
const DATABASE_SEARCH_RESULTS_LIMIT = 30
|
||||||
|
const promiseThrottler = new PromiseThrottler(200) // Mastodon FE also uses 200ms
|
||||||
|
|
||||||
function byUsername (a, b) {
|
function byUsername (a, b) {
|
||||||
let usernameA = a.acct.toLowerCase()
|
let usernameA = a.acct.toLowerCase()
|
||||||
|
@ -24,6 +26,14 @@ export function doAccountSearch (searchText) {
|
||||||
let localResults
|
let localResults
|
||||||
let remoteResults
|
let remoteResults
|
||||||
let { currentInstance, accessToken } = store.get()
|
let { currentInstance, accessToken } = store.get()
|
||||||
|
let controller = typeof AbortController === 'function' && new AbortController()
|
||||||
|
|
||||||
|
function abortFetch () {
|
||||||
|
if (controller) {
|
||||||
|
controller.abort()
|
||||||
|
controller = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function searchAccountsLocally (searchText) {
|
async function searchAccountsLocally (searchText) {
|
||||||
localResults = await database.searchAccountsByUsername(
|
localResults = await database.searchAccountsByUsername(
|
||||||
|
@ -31,7 +41,14 @@ export function doAccountSearch (searchText) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function searchAccountsRemotely (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 () {
|
function mergeAndTruncateResults () {
|
||||||
|
@ -81,6 +98,7 @@ export function doAccountSearch (searchText) {
|
||||||
return {
|
return {
|
||||||
cancel: () => {
|
cancel: () => {
|
||||||
canceled = true
|
canceled = true
|
||||||
|
abortFetch()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import { get, paramsString, DEFAULT_TIMEOUT } from '../_utils/ajax'
|
import { get, paramsString, DEFAULT_TIMEOUT } from '../_utils/ajax'
|
||||||
import { auth, basename } from './utils'
|
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({
|
let url = `${basename(instanceName)}/api/v1/search?` + paramsString({
|
||||||
q: query,
|
q: query,
|
||||||
resolve,
|
resolve,
|
||||||
limit
|
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
|
let lastSearch
|
||||||
|
|
||||||
store.observe('autosuggestSearchText', async autosuggestSearchText => {
|
store.observe('autosuggestSearchText', async autosuggestSearchText => {
|
||||||
let { composeFocused } = store.get()
|
// cancel any inflight XHRs or other operations
|
||||||
if (!composeFocused || !autosuggestSearchText) {
|
if (lastSearch) {
|
||||||
return
|
lastSearch.cancel()
|
||||||
|
lastSearch = null
|
||||||
}
|
}
|
||||||
/* autosuggestSelecting indicates that the user has pressed Enter or clicked on an item
|
// 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.
|
// and the results are being processed. Returning early avoids a flash of searched content.
|
||||||
We can also cancel any inflight XHRs here.
|
let { composeFocused } = store.get()
|
||||||
*/
|
|
||||||
let autosuggestSelecting = store.getForCurrentAutosuggest('autosuggestSelecting')
|
let autosuggestSelecting = store.getForCurrentAutosuggest('autosuggestSelecting')
|
||||||
if (autosuggestSelecting) {
|
if (!composeFocused || !autosuggestSearchText || autosuggestSelecting) {
|
||||||
if (lastSearch) {
|
|
||||||
lastSearch.cancel()
|
|
||||||
lastSearch = null
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let autosuggestType = autosuggestSearchText.startsWith('@') ? 'account' : 'emoji'
|
let autosuggestType = autosuggestSearchText.startsWith('@') ? 'account' : 'emoji'
|
||||||
|
|
||||||
if (lastSearch) {
|
|
||||||
lastSearch.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (autosuggestType === 'emoji') {
|
if (autosuggestType === 'emoji') {
|
||||||
lastSearch = doEmojiSearch(autosuggestSearchText)
|
lastSearch = doEmojiSearch(autosuggestSearchText)
|
||||||
} else {
|
} 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) {
|
function makeFetchOptions (method, headers, options) {
|
||||||
return {
|
let res = {
|
||||||
method,
|
method,
|
||||||
headers: Object.assign(headers || {}, {
|
headers: Object.assign(headers || {}, {
|
||||||
'Accept': 'application/json'
|
'Accept': 'application/json'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (options && options.signal) {
|
||||||
|
res.signal = options.signal
|
||||||
|
}
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
async function throwErrorIfInvalidResponse (response) {
|
async function throwErrorIfInvalidResponse (response) {
|
||||||
|
@ -40,7 +44,7 @@ async function _fetch (url, fetchOptions, options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _putOrPostOrPatch (method, url, body, headers, options) {
|
async function _putOrPostOrPatch (method, url, body, headers, options) {
|
||||||
let fetchOptions = makeFetchOptions(method, headers)
|
let fetchOptions = makeFetchOptions(method, headers, options)
|
||||||
if (body) {
|
if (body) {
|
||||||
if (body instanceof FormData) {
|
if (body instanceof FormData) {
|
||||||
fetchOptions.body = body
|
fetchOptions.body = body
|
||||||
|
@ -65,11 +69,11 @@ export async function patch (url, body, headers, options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get (url, 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) {
|
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) {
|
export function paramsString (paramsObject) {
|
||||||
|
|
Loading…
Reference in New Issue