From 4c5fac2fce0a5927d9c06305e5c26727c67343b1 Mon Sep 17 00:00:00 2001 From: Niklas Fiekas Date: Wed, 15 Nov 2017 21:35:23 +0100 Subject: [PATCH 1/3] simplify throttle Only ExplorerCtrl.fetch and updateHref were using debounce mode. --- ui/analyse/src/ctrl.ts | 16 ++-- ui/analyse/src/evalCache.ts | 2 +- ui/analyse/src/explorer/explorerCtrl.ts | 2 +- ui/analyse/src/study/commentForm.ts | 2 +- ui/analyse/src/study/gamebook/gamebookEdit.ts | 2 +- ui/analyse/src/study/studyCtrl.ts | 2 +- ui/analyse/src/study/studyGlyph.ts | 2 +- ui/analyse/src/study/studyTags.ts | 2 +- ui/ceval/src/ctrl.ts | 2 +- ui/common/src/main.ts | 25 +++++- ui/common/src/throttle.ts | 88 ------------------- ui/insight/src/ctrl.js | 2 +- ui/puzzle/src/ctrl.ts | 6 +- ui/puzzle/src/sound.ts | 6 +- ui/round/src/blind.ts | 2 +- ui/round/src/socket.ts | 6 +- ui/round/src/sound.ts | 2 +- ui/tournament/src/xhr.ts | 8 +- 18 files changed, 54 insertions(+), 123 deletions(-) delete mode 100644 ui/common/src/throttle.ts diff --git a/ui/analyse/src/ctrl.ts b/ui/analyse/src/ctrl.ts index 5642b91c0d..20cd73b8ee 100644 --- a/ui/analyse/src/ctrl.ts +++ b/ui/analyse/src/ctrl.ts @@ -241,7 +241,7 @@ export default class AnalyseCtrl { }); } - getDests: () => void = throttle(800, false, () => { + getDests: () => void = throttle(800, () => { if (!this.embed && !defined(this.node.dests)) this.socket.sendAnaDests({ variant: this.data.game.variant.key, fen: this.node.fen, @@ -286,25 +286,25 @@ export default class AnalyseCtrl { } private sound = li.sound ? { - move: throttle(50, false, li.sound.move), - capture: throttle(50, false, li.sound.capture), - check: throttle(50, false, li.sound.check) + move: throttle(50, li.sound.move), + capture: throttle(50, li.sound.capture), + check: throttle(50, li.sound.check) } : { move: $.noop, capture: $.noop, check: $.noop }; - private onChange: () => void = throttle(300, false, () => { + private onChange: () => void = throttle(300, () => { if (this.opts.onChange) { const mainlinePly = this.onMainline ? this.node.ply : false; this.opts.onChange!(this.node.fen, this.path, mainlinePly); } }); - private updateHref: () => void = throttle(750, false, () => { + private updateHref: () => void = throttle(750, () => { if (!this.opts.study) window.history.replaceState(null, '', '#' + this.node.ply); - }, false); + }); // TODO: Debounce autoScroll(): void { this.autoScrollRequested = true; @@ -602,7 +602,7 @@ export default class AnalyseCtrl { return !this.gameOver() && !this.node.threefold; } - startCeval = throttle(800, false, () => { + startCeval = throttle(800, () => { if (this.ceval.enabled()) { if (this.canUseCeval()) { this.ceval.start(this.path, this.nodeList, this.threatMode(), false); diff --git a/ui/analyse/src/evalCache.ts b/ui/analyse/src/evalCache.ts index 0b0d7bf943..632f13511d 100644 --- a/ui/analyse/src/evalCache.ts +++ b/ui/analyse/src/evalCache.ts @@ -64,7 +64,7 @@ export function make(opts): EvalCache { return fenFetched.indexOf(node.fen) !== -1; }; return { - onCeval: throttle(500, false, function() { + onCeval: throttle(500, function() { const node = opts.getNode(), ev = node.ceval; if (ev && !ev.cloud && hasFetched(node) && qualityCheck(ev) && opts.canPut(node)) { opts.send("evalPut", toPutData(opts.variant, ev)); diff --git a/ui/analyse/src/explorer/explorerCtrl.ts b/ui/analyse/src/explorer/explorerCtrl.ts index 38131a3888..2dc63a1a02 100644 --- a/ui/analyse/src/explorer/explorerCtrl.ts +++ b/ui/analyse/src/explorer/explorerCtrl.ts @@ -55,7 +55,7 @@ export default function(root: AnalyseCtrl, opts, allow: boolean): ExplorerCtrl { failing(true); root.redraw(); }); - }, false); + }); // TODO: Debounce const empty = { opening: true, diff --git a/ui/analyse/src/study/commentForm.ts b/ui/analyse/src/study/commentForm.ts index 62746e6591..5767b21c14 100644 --- a/ui/analyse/src/study/commentForm.ts +++ b/ui/analyse/src/study/commentForm.ts @@ -35,7 +35,7 @@ export function ctrl(root: AnalyseCtrl): CommentForm { doSubmit(text); }; - const doSubmit = throttle(500, false, (text: string) => { + const doSubmit = throttle(500, (text: string) => { const cur = current(); if (cur) root.study!.makeChange('setComment', { ch: cur.chapterId, diff --git a/ui/analyse/src/study/gamebook/gamebookEdit.ts b/ui/analyse/src/study/gamebook/gamebookEdit.ts index 6f7e3e74ec..4e959ed577 100644 --- a/ui/analyse/src/study/gamebook/gamebookEdit.ts +++ b/ui/analyse/src/study/gamebook/gamebookEdit.ts @@ -125,7 +125,7 @@ function renderHint(ctrl: AnalyseCtrl): VNode { ]); } -const saveNode = throttle(500, false, (ctrl: AnalyseCtrl, gamebook: Tree.Gamebook) => { +const saveNode = throttle(500, (ctrl: AnalyseCtrl, gamebook: Tree.Gamebook) => { ctrl.socket.send('setGamebook', { path: ctrl.path, ch: ctrl.study!.vm.chapterId, diff --git a/ui/analyse/src/study/studyCtrl.ts b/ui/analyse/src/study/studyCtrl.ts index f454d1f35a..d1e8a98877 100644 --- a/ui/analyse/src/study/studyCtrl.ts +++ b/ui/analyse/src/study/studyCtrl.ts @@ -207,7 +207,7 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes, ).then(onReload, li.reload); }; - const onSetPath = throttle(300, false, (path: Tree.Path) => { + const onSetPath = throttle(300, (path: Tree.Path) => { if (vm.mode.sticky && path !== data.position.path) makeChange("setPath", addChapterId({ path })); diff --git a/ui/analyse/src/study/studyGlyph.ts b/ui/analyse/src/study/studyGlyph.ts index bd6ae16541..055efbcf12 100644 --- a/ui/analyse/src/study/studyGlyph.ts +++ b/ui/analyse/src/study/studyGlyph.ts @@ -37,7 +37,7 @@ export function ctrl(root: AnalyseCtrl) { }); }; - const toggleGlyph = throttle(500, false, function(id) { + const toggleGlyph = throttle(500, function(id) { root.study!.makeChange('toggleGlyph', root.study!.withPosition({ id })); diff --git a/ui/analyse/src/study/studyTags.ts b/ui/analyse/src/study/studyTags.ts index 995ece1afd..f9ecf23c5e 100644 --- a/ui/analyse/src/study/studyTags.ts +++ b/ui/analyse/src/study/studyTags.ts @@ -86,7 +86,7 @@ function renderPgnTags(chapter: StudyChapter, submit, types: string[]): VNode { export function ctrl(root: AnalyseCtrl, getChapter: () => StudyChapter, types) { - const submit = throttle(500, false, function(name, value) { + const submit = throttle(500, function(name, value) { root.study!.makeChange('setTag', { chapterId: getChapter().id, name, diff --git a/ui/ceval/src/ctrl.ts b/ui/ceval/src/ctrl.ts index 0aa77f3e3a..068d69dca1 100644 --- a/ui/ceval/src/ctrl.ts +++ b/ui/ceval/src/ctrl.ts @@ -72,7 +72,7 @@ export default function(opts: CevalOpts): CevalCtrl { let lastEmitFen: string | null = null; - const onEmit = throttle(500, false, (ev: Tree.ClientEval, work: Work) => { + const onEmit = throttle(500, (ev: Tree.ClientEval, work: Work) => { sortPvsInPlace(ev.pvs, (work.ply % 2 === (work.threatMode ? 1 : 0)) ? 'white' : 'black'); npsRecorder(ev); curEval = ev; diff --git a/ui/common/src/main.ts b/ui/common/src/main.ts index 3dfdb3c57c..fea0ec115d 100644 --- a/ui/common/src/main.ts +++ b/ui/common/src/main.ts @@ -1,7 +1,5 @@ /// -import throttle from './throttle'; - export function defined(v: A | undefined): v is A { return typeof v !== 'undefined'; } @@ -87,7 +85,28 @@ export function sync(promise: Promise): Sync { return sync; } -export { throttle }; +// Ensures calls to the wrapped function are spaced by the given delay. +// Any extra calls are dropped, except the last one. +export function throttle(delay: number, callback: (...args: any[]) => void): (...args: any[]) => void { + let timer: number | undefined; + let lastExec = 0; + + return function(this: any, ...args: any[]): void { + const self: any = this; + const elapsed = Date.now() - lastExec; + + function exec() { + timer = undefined; + lastExec = Date.now(); + callback.apply(self, args); + } + + if (timer) clearTimeout(timer); + + if (elapsed > delay) exec(); + else timer = setTimeout(exec, delay - elapsed); + }; +} export type F = () => void; diff --git a/ui/common/src/throttle.ts b/ui/common/src/throttle.ts deleted file mode 100644 index 4dd0e7a616..0000000000 --- a/ui/common/src/throttle.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * https://github.com/niksy/throttle-debounce/blob/master/throttle.js - * - * Throttle execution of a function. Especially useful for rate limiting - * execution of handlers on events like resize and scroll. - * - * @param {Number} delay A zero-or-greater delay in milliseconds. For event callbacks, values around 100 or 250 (or even higher) are most useful. - * @param {Boolean} noTrailing Optional, defaults to false. If noTrailing is true, callback will only execute every `delay` milliseconds while the - * throttled-function is being called. If noTrailing is false or unspecified, callback will be executed one final time - * after the last throttled-function call. (After the throttled-function has not been called for `delay` milliseconds, - * the internal counter is reset) - * @param {Function} callback A function to be executed after delay milliseconds. The `this` context and all arguments are passed through, as-is, - * to `callback` when the throttled-function is executed. - * @param {Boolean} debounceMode If `debounceMode` is true (at begin), schedule `clear` to execute after `delay` ms. If `debounceMode` is false (at end), - * schedule `callback` to execute after `delay` ms. - * - * @return {Function} A new, throttled, function. - */ -export default function(delay: number, noTrailing: boolean, callback: (...args: any[]) => void, debounceMode?: boolean): (...args: any[]) => void; -export default function(delay: number, callback: (...args: any[]) => void, debounceMode?: boolean): (...args: any[]) => void; -export default function(delay: number, noTrailing: any, callback: any, debounceMode?: any): (...args: any[]) => void { - - // After wrapper has stopped being called, this timeout ensures that - // `callback` is executed at the proper times in `throttle` and `end` - // debounce modes. - let timeoutID: number | undefined; - - // Keep track of the last time `callback` was executed. - let lastExec = 0; - - // `noTrailing` defaults to falsy. - if (typeof(noTrailing) !== 'boolean') { - debounceMode = callback; - callback = noTrailing; - noTrailing = undefined; - } - - // The `wrapper` function encapsulates all of the throttling / debouncing - // functionality and when executed will limit the rate at which `callback` - // is executed. - return function(this: any, ...args: any[]): void { - - const self: any = this; - const elapsed = Date.now() - lastExec; - - // Execute `callback` and update the `lastExec` timestamp. - function exec() { - lastExec = Date.now(); - callback.apply(self, args); - } - - // If `debounceMode` is true (at begin) this is used to clear the flag - // to allow future `callback` executions. - function clear() { - timeoutID = undefined; - } - - if (debounceMode && !timeoutID) { - // Since `wrapper` is being called for the first time and - // `debounceMode` is true (at begin), execute `callback`. - exec(); - } - - // Clear any existing timeout. - if (timeoutID) { - clearTimeout(timeoutID); - } - - if (debounceMode === undefined && elapsed > delay) { - // In throttle mode, if `delay` time has been exceeded, execute - // `callback`. - exec(); - - } else if (noTrailing !== true) { - // In trailing throttle mode, since `delay` time has not been - // exceeded, schedule `callback` to execute `delay` ms after most - // recent execution. - // - // If `debounceMode` is true (at begin), schedule `clear` to execute - // after `delay` ms. - // - // If `debounceMode` is false (at end), schedule `callback` to - // execute after `delay` ms. - timeoutID = setTimeout(debounceMode ? clear : exec, debounceMode === undefined ? delay - elapsed : delay); - } - - }; -} diff --git a/ui/insight/src/ctrl.js b/ui/insight/src/ctrl.js index 3fe991be21..90a4a1c1d7 100644 --- a/ui/insight/src/ctrl.js +++ b/ui/insight/src/ctrl.js @@ -46,7 +46,7 @@ module.exports = function(env, domElement) { this.vm.filters = {}; }.bind(this); - var askQuestion = throttle(1000, false, function() { + var askQuestion = throttle(1000, function() { if (!this.validCombinationCurrent()) reset(); this.pushState(); this.vm.loading = true; diff --git a/ui/puzzle/src/ctrl.ts b/ui/puzzle/src/ctrl.ts index 1e70a0da1f..ff41564dac 100644 --- a/ui/puzzle/src/ctrl.ts +++ b/ui/puzzle/src/ctrl.ts @@ -139,7 +139,7 @@ export default function(opts, redraw: () => void): Controller { socket.sendAnaMove(move); }; - var getDests = throttle(800, false, function() { + var getDests = throttle(800, function() { if (!vm.node.dests && treePath.contains(vm.path, vm.initialPath)) socket.sendAnaDests({ fen: vm.node.fen, @@ -304,7 +304,7 @@ export default function(opts, redraw: () => void): Controller { if (ceval.enabled() && canUseCeval()) doStartCeval(); }; - const doStartCeval = throttle(800, false, function() { + const doStartCeval = throttle(800, function() { ceval.start(vm.path, vm.nodeList, threatMode()); }); @@ -419,7 +419,7 @@ export default function(opts, redraw: () => void): Controller { const callToVote = () => parseInt(nbToVoteCall()) < 1; - const vote = throttle(1000, false, function(v) { + const vote = throttle(1000, function(v) { if (callToVote()) thanksUntil = Date.now() + 2000; nbToVoteCall(5); vm.voted = v; diff --git a/ui/puzzle/src/sound.ts b/ui/puzzle/src/sound.ts index c0df2a1ed6..b376bdb913 100644 --- a/ui/puzzle/src/sound.ts +++ b/ui/puzzle/src/sound.ts @@ -3,7 +3,7 @@ import { throttle } from 'common'; const sounds = window.lichess.sound; export const sound = { - move: throttle(50, false, sounds.move), - capture: throttle(50, false, sounds.capture), - check: throttle(50, false, sounds.check) + move: throttle(50, sounds.move), + capture: throttle(50, sounds.capture), + check: throttle(50, sounds.check) }; diff --git a/ui/round/src/blind.ts b/ui/round/src/blind.ts index 571ac702fc..de758b1419 100644 --- a/ui/round/src/blind.ts +++ b/ui/round/src/blind.ts @@ -4,7 +4,7 @@ import RoundController from './ctrl'; let element: HTMLElement; -export const reload = throttle(1000, false, (ctrl: RoundController) => { +export const reload = throttle(1000, (ctrl: RoundController) => { $.ajax({ url: (ctrl.data.player.spectator ? router.game(ctrl.data, ctrl.data.player.color) : diff --git a/ui/round/src/socket.ts b/ui/round/src/socket.ts index dfa426d1a5..eb04d3de56 100644 --- a/ui/round/src/socket.ts +++ b/ui/round/src/socket.ts @@ -138,9 +138,9 @@ export function make(send: SocketSend, ctrl: RoundController): RoundSocket { return { send, handlers, - moreTime: throttle(300, false, () => send('moretime')), - outoftime: throttle(500, false, () => send('flag', ctrl.data.game.player)), - berserk: throttle(200, false, () => send('berserk', null, { ackable: true })), + moreTime: throttle(300, () => send('moretime')), + outoftime: throttle(500, () => send('flag', ctrl.data.game.player)), + berserk: throttle(200, () => send('berserk', null, { ackable: true })), sendLoading(typ: string, data?: any) { ctrl.setLoading(true); send(typ, data); diff --git a/ui/round/src/sound.ts b/ui/round/src/sound.ts index 87f505795b..b9f7cb7a6a 100644 --- a/ui/round/src/sound.ts +++ b/ui/round/src/sound.ts @@ -1,7 +1,7 @@ import { throttle } from 'common'; function throttled(sound: string): () => void { - return throttle(100, false, () => window.lichess.sound[sound]()) + return throttle(100, () => window.lichess.sound[sound]()) } export const move = throttled('move'); diff --git a/ui/tournament/src/xhr.ts b/ui/tournament/src/xhr.ts index e64a722553..4cd2087902 100644 --- a/ui/tournament/src/xhr.ts +++ b/ui/tournament/src/xhr.ts @@ -64,9 +64,9 @@ function playerInfo(ctrl: TournamentController, userId: string) { } export default { - join: throttle(1000, false, join), - withdraw: throttle(1000, false, withdraw), - loadPage: throttle(1000, false, loadPage), - reloadTournament: throttle(2000, false, reloadTournament), + join: throttle(1000, join), + withdraw: throttle(1000, withdraw), + loadPage: throttle(1000, loadPage), + reloadTournament: throttle(2000, reloadTournament), playerInfo }; From d454a96dd2dbb70530db29e1b12c0c7048d6277a Mon Sep 17 00:00:00 2001 From: Niklas Fiekas Date: Wed, 15 Nov 2017 22:48:09 +0100 Subject: [PATCH 2/3] implement a debouncer that fires at first and last --- ui/analyse/src/ctrl.ts | 6 +++--- ui/analyse/src/explorer/explorerCtrl.ts | 6 +++--- ui/common/src/main.ts | 25 +++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/ui/analyse/src/ctrl.ts b/ui/analyse/src/ctrl.ts index 20cd73b8ee..e0bc7c8d42 100644 --- a/ui/analyse/src/ctrl.ts +++ b/ui/analyse/src/ctrl.ts @@ -10,7 +10,7 @@ import { Autoplay, AutoplayDelay } from './autoplay'; import * as promotion from './promotion'; import * as util from './util'; import * as chessUtil from 'chess'; -import { storedProp, throttle, defined, prop, Prop, StoredBooleanProp } from 'common'; +import { storedProp, throttle, debounce, defined, prop, Prop, StoredBooleanProp } from 'common'; import { make as makeSocket, Socket } from './socket'; import { make as makeForecast, ForecastCtrl } from './forecast/forecastCtrl'; import { ctrl as cevalCtrl, isEvalBetter, CevalCtrl, Work as CevalWork, CevalOpts } from 'ceval'; @@ -302,9 +302,9 @@ export default class AnalyseCtrl { } }); - private updateHref: () => void = throttle(750, () => { + private updateHref: () => void = debounce(750, () => { if (!this.opts.study) window.history.replaceState(null, '', '#' + this.node.ply); - }); // TODO: Debounce + }); autoScroll(): void { this.autoScrollRequested = true; diff --git a/ui/analyse/src/explorer/explorerCtrl.ts b/ui/analyse/src/explorer/explorerCtrl.ts index 2dc63a1a02..694b1dbbb7 100644 --- a/ui/analyse/src/explorer/explorerCtrl.ts +++ b/ui/analyse/src/explorer/explorerCtrl.ts @@ -1,4 +1,4 @@ -import { throttle, prop, storedProp } from 'common'; +import { debounce, prop, storedProp } from 'common'; import { controller as configCtrl } from './explorerConfig'; import xhr = require('./openingXhr'); import { synthetic } from '../util'; @@ -38,7 +38,7 @@ export default function(root: AnalyseCtrl, opts, allow: boolean): ExplorerCtrl { const config = configCtrl(root.data.game, onConfigClose, root.trans, root.redraw); - const fetch = throttle(250, function() { + const fetch = debounce(250, function() { const fen = root.node.fen; const request: JQueryPromise = (withGames && tablebaseRelevant(effectiveVariant, fen)) ? xhr.tablebase(opts.tablebaseEndpoint, effectiveVariant, fen) : @@ -55,7 +55,7 @@ export default function(root: AnalyseCtrl, opts, allow: boolean): ExplorerCtrl { failing(true); root.redraw(); }); - }); // TODO: Debounce + }); const empty = { opening: true, diff --git a/ui/common/src/main.ts b/ui/common/src/main.ts index fea0ec115d..5bc7e86b7d 100644 --- a/ui/common/src/main.ts +++ b/ui/common/src/main.ts @@ -108,6 +108,31 @@ export function throttle(delay: number, callback: (...args: any[]) => void): (.. }; } +// Debounces calls to the wrapped function: Only forwards calls if there has +// been silence for the given delay. Any extra calls are dropped, except the +// last one. +export function debounce(delay: number, callback: (...args: any[]) => void): (...args: any[]) => void { + let timer: number | undefined; + let lastBounce = 0; + + return function(this: any, ...args: any[]): void { + const self: any = this; + + const elapsed = Date.now() - lastBounce; + lastBounce = Date.now(); + + function exec() { + timer = undefined; + callback.apply(self, args); + } + + if (timer) clearTimeout(timer); + + if (elapsed > delay) exec(); + else timer = setTimeout(exec, delay); + } +} + export type F = () => void; export function dropThrottle(delay: number): (f: F) => void { From 602b0e4fd10aef3e760a8d1ca017f7750e835097 Mon Sep 17 00:00:00 2001 From: Niklas Fiekas Date: Fri, 24 Nov 2017 00:21:50 +0100 Subject: [PATCH 3/3] merge debounce into lichess.fp.debounce lichess.fp.debounce was not firing the last event in immediate mode, but we can change that behavior because this mode was unused. --- ui/analyse/src/ctrl.ts | 6 +++--- ui/analyse/src/explorer/explorerCtrl.ts | 6 +++--- ui/common/src/main.ts | 25 ------------------------- ui/site/src/main.js | 12 +++++++----- ui/types/lichess.d.ts | 5 ++++- 5 files changed, 17 insertions(+), 37 deletions(-) diff --git a/ui/analyse/src/ctrl.ts b/ui/analyse/src/ctrl.ts index e0bc7c8d42..f726dbfd78 100644 --- a/ui/analyse/src/ctrl.ts +++ b/ui/analyse/src/ctrl.ts @@ -10,7 +10,7 @@ import { Autoplay, AutoplayDelay } from './autoplay'; import * as promotion from './promotion'; import * as util from './util'; import * as chessUtil from 'chess'; -import { storedProp, throttle, debounce, defined, prop, Prop, StoredBooleanProp } from 'common'; +import { storedProp, throttle, defined, prop, Prop, StoredBooleanProp } from 'common'; import { make as makeSocket, Socket } from './socket'; import { make as makeForecast, ForecastCtrl } from './forecast/forecastCtrl'; import { ctrl as cevalCtrl, isEvalBetter, CevalCtrl, Work as CevalWork, CevalOpts } from 'ceval'; @@ -302,9 +302,9 @@ export default class AnalyseCtrl { } }); - private updateHref: () => void = debounce(750, () => { + private updateHref: () => void = li.fp.debounce(() => { if (!this.opts.study) window.history.replaceState(null, '', '#' + this.node.ply); - }); + }, 750); autoScroll(): void { this.autoScrollRequested = true; diff --git a/ui/analyse/src/explorer/explorerCtrl.ts b/ui/analyse/src/explorer/explorerCtrl.ts index 694b1dbbb7..34259018ef 100644 --- a/ui/analyse/src/explorer/explorerCtrl.ts +++ b/ui/analyse/src/explorer/explorerCtrl.ts @@ -1,4 +1,4 @@ -import { debounce, prop, storedProp } from 'common'; +import { prop, storedProp } from 'common'; import { controller as configCtrl } from './explorerConfig'; import xhr = require('./openingXhr'); import { synthetic } from '../util'; @@ -38,7 +38,7 @@ export default function(root: AnalyseCtrl, opts, allow: boolean): ExplorerCtrl { const config = configCtrl(root.data.game, onConfigClose, root.trans, root.redraw); - const fetch = debounce(250, function() { + const fetch = window.lichess.fp.debounce(function() { const fen = root.node.fen; const request: JQueryPromise = (withGames && tablebaseRelevant(effectiveVariant, fen)) ? xhr.tablebase(opts.tablebaseEndpoint, effectiveVariant, fen) : @@ -55,7 +55,7 @@ export default function(root: AnalyseCtrl, opts, allow: boolean): ExplorerCtrl { failing(true); root.redraw(); }); - }); + }, 250, true); const empty = { opening: true, diff --git a/ui/common/src/main.ts b/ui/common/src/main.ts index 5bc7e86b7d..fea0ec115d 100644 --- a/ui/common/src/main.ts +++ b/ui/common/src/main.ts @@ -108,31 +108,6 @@ export function throttle(delay: number, callback: (...args: any[]) => void): (.. }; } -// Debounces calls to the wrapped function: Only forwards calls if there has -// been silence for the given delay. Any extra calls are dropped, except the -// last one. -export function debounce(delay: number, callback: (...args: any[]) => void): (...args: any[]) => void { - let timer: number | undefined; - let lastBounce = 0; - - return function(this: any, ...args: any[]): void { - const self: any = this; - - const elapsed = Date.now() - lastBounce; - lastBounce = Date.now(); - - function exec() { - timer = undefined; - callback.apply(self, args); - } - - if (timer) clearTimeout(timer); - - if (elapsed > delay) exec(); - else timer = setTimeout(exec, delay); - } -} - export type F = () => void; export function dropThrottle(delay: number): (f: F) => void { diff --git a/ui/site/src/main.js b/ui/site/src/main.js index 116261e12e..4045c86025 100644 --- a/ui/site/src/main.js +++ b/ui/site/src/main.js @@ -46,17 +46,19 @@ lichess.topMenuIntent = function() { }; lichess.fp.debounce = function(func, wait, immediate) { var timeout; + var lastBounce = 0; return function() { var context = this, - args = arguments; + args = arguments, + elapsed = Date.now() - lastBounce; + lastBounce = Date.now(); var later = function() { timeout = null; - if (!immediate) func.apply(context, args); + func.apply(context, args); }; - var callNow = immediate && !timeout; clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) func.apply(context, args); + if (immediate && elapsed > wait) func.apply(context, args); + else timeout = setTimeout(later, wait); }; }; diff --git a/ui/types/lichess.d.ts b/ui/types/lichess.d.ts index 8d27a2e2a1..1284460716 100644 --- a/ui/types/lichess.d.ts +++ b/ui/types/lichess.d.ts @@ -21,7 +21,10 @@ interface Lichess { escapeHtml(html: string): string toYouTubeEmbedUrl(url: string): string - fp: any + fp: { + debounce(func: (...args: any[]) => void, wait: number, immediate?: boolean): (...args: any[]) => void; + contains(list: T[], needle: T): boolean; + } sound: any powertip: any userAutocomplete: any