fix: combine request throttling logic (#1568)

This commit is contained in:
Nolan Lawson 2019-10-12 21:08:08 -07:00 committed by GitHub
parent 8b3842f15a
commit 89265f709e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 56 additions and 51 deletions

View file

@ -5,10 +5,9 @@ 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' import { RequestThrottler } from '../_utils/RequestThrottler'
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) {
const usernameA = a.acct.toLowerCase() const usernameA = a.acct.toLowerCase()
@ -26,31 +25,24 @@ export function doAccountSearch (searchText) {
let localResults let localResults
let remoteResults let remoteResults
const { currentInstance, accessToken } = store.get() const { currentInstance, accessToken } = store.get()
let controller = typeof AbortController === 'function' && new AbortController() const requestThrottler = new RequestThrottler(searchAccountsRemotely, onNewRemoteResults)
function abortFetch () {
if (controller) {
controller.abort()
controller = null
}
}
async function searchAccountsLocally (searchText) { async function searchAccountsLocally (searchText) {
localResults = await database.searchAccountsByUsername( localResults = await database.searchAccountsByUsername(
currentInstance, searchText.substring(1), DATABASE_SEARCH_RESULTS_LIMIT) currentInstance, searchText.substring(1), DATABASE_SEARCH_RESULTS_LIMIT)
} }
async function searchAccountsRemotely (searchText) { async function searchAccountsRemotely (signal) {
// Throttle our XHRs to be a good citizen and not spam the server with one XHR per keystroke return (await search(
await promiseThrottler.next() currentInstance, accessToken, searchText, false, SEARCH_RESULTS_LIMIT, signal
if (canceled) {
return
}
remoteResults = (await search(
currentInstance, accessToken, searchText, false, SEARCH_RESULTS_LIMIT, controller && controller.signal
)).accounts )).accounts
} }
function onNewRemoteResults (results) {
remoteResults = results
onNewResults()
}
function mergeAndTruncateResults () { function mergeAndTruncateResults () {
// Always include local results; they are more likely to be relevant // Always include local results; they are more likely to be relevant
// because the user has seen their content before. Otherwise, sort by username. // because the user has seen their content before. Otherwise, sort by username.
@ -87,18 +79,14 @@ export function doAccountSearch (searchText) {
return return
} }
// run the two searches in parallel // run the two searches in parallel
searchAccountsLocally(searchText).then(onNewResults).catch(err => { searchAccountsLocally(searchText).then(onNewResults)
console.error('could not search locally', err) requestThrottler.request()
})
searchAccountsRemotely(searchText).then(onNewResults).catch(err => {
console.error('could not search remotely', err)
})
}) })
return { return {
cancel: () => { cancel: () => {
canceled = true canceled = true
abortFetch() requestThrottler.cancel()
} }
} }
} }

View file

@ -2,32 +2,19 @@ import { search } from '../_api/search'
import { store } from '../_store/store' import { store } from '../_store/store'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask' import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
import { SEARCH_RESULTS_LIMIT } from '../_static/autosuggest' import { SEARCH_RESULTS_LIMIT } from '../_static/autosuggest'
import { PromiseThrottler } from '../_utils/PromiseThrottler' import { RequestThrottler } from '../_utils/RequestThrottler'
const promiseThrottler = new PromiseThrottler(200) // Mastodon FE also uses 200ms
export function doHashtagSearch (searchText) { export function doHashtagSearch (searchText) {
let canceled = false
const { currentInstance, accessToken } = store.get() const { currentInstance, accessToken } = store.get()
let controller = typeof AbortController === 'function' && new AbortController() const requestThrottler = new RequestThrottler(searchHashtagsRemotely, onNewResults)
function abortFetch () { async function searchHashtagsRemotely (signal) {
if (controller) { return (await search(
controller.abort() currentInstance, accessToken, searchText, false, SEARCH_RESULTS_LIMIT, signal
controller = null )).hashtags
}
} }
async function searchHashtagsRemotely (searchText) { function onNewResults (results) {
// Throttle our XHRs to be a good citizen and not spam the server with one XHR per keystroke
await promiseThrottler.next()
if (canceled) {
return
}
const searchPromise = search(
currentInstance, accessToken, searchText, false, SEARCH_RESULTS_LIMIT, controller && controller.signal
)
const results = (await searchPromise).hashtags
store.setForCurrentAutosuggest({ store.setForCurrentAutosuggest({
autosuggestType: 'hashtag', autosuggestType: 'hashtag',
autosuggestSelected: 0, autosuggestSelected: 0,
@ -36,16 +23,12 @@ export function doHashtagSearch (searchText) {
} }
scheduleIdleTask(() => { scheduleIdleTask(() => {
if (canceled) { requestThrottler.request()
return
}
/* no await */ searchHashtagsRemotely(searchText)
}) })
return { return {
cancel: () => { cancel: () => {
canceled = true requestThrottler.cancel()
abortFetch()
} }
} }
} }

View file

@ -0,0 +1,34 @@
// Throttle network requests to be a good citizen and not issue an HTTP request on every keystroke
import { PromiseThrottler } from './PromiseThrottler'
const promiseThrottler = new PromiseThrottler(200) // Mastodon FE also uses 200ms
export class RequestThrottler {
constructor (fetcher, onNewResults) {
this._canceled = false
this._controller = typeof AbortController === 'function' && new AbortController()
this._fetcher = fetcher
this._onNewResults = onNewResults
}
async request () {
if (this._canceled) {
return
}
await promiseThrottler.next()
if (this._canceled) {
return
}
const signal = this._controller && this._controller.signal
const results = await this._fetcher(signal)
this._onNewResults(results)
}
cancel () {
this._canceled = true
if (this._controller) {
this._controller.abort()
this._controller = null
}
}
}