fix: combine request throttling logic (#1568)
This commit is contained in:
parent
8b3842f15a
commit
89265f709e
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
34
src/routes/_utils/RequestThrottler.js
Normal file
34
src/routes/_utils/RequestThrottler.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue