Merge pull request #3797 from ornicar/throttle-debounce

split throttle and debounce, improve debouncer
pull/3820/head
Thibault Duplessis 2017-11-23 18:59:49 -05:00 committed by GitHub
commit 30385b6c41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 67 additions and 131 deletions

View File

@ -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 = li.fp.debounce(() => {
if (!this.opts.study) window.history.replaceState(null, '', '#' + this.node.ply);
}, false);
}, 750);
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);

View File

@ -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));

View File

@ -1,4 +1,4 @@
import { throttle, 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 = throttle(250, function() {
const fetch = window.lichess.fp.debounce(function() {
const fen = root.node.fen;
const request: JQueryPromise<ExplorerData> = (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();
});
}, false);
}, 250, true);
const empty = {
opening: true,

View File

@ -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,

View File

@ -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,

View File

@ -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
}));

View File

@ -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
}));

View File

@ -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,

View File

@ -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;

View File

@ -1,7 +1,5 @@
/// <reference types="types/lichess" />
import throttle from './throttle';
export function defined<A>(v: A | undefined): v is A {
return typeof v !== 'undefined';
}
@ -87,7 +85,28 @@ export function sync<T>(promise: Promise<T>): Sync<T> {
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;

View File

@ -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);
}
};
}

View File

@ -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;

View File

@ -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;

View File

@ -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)
};

View File

@ -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) :

View File

@ -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);

View File

@ -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');

View File

@ -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);
};
};

View File

@ -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
};

View File

@ -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<T>(list: T[], needle: T): boolean;
}
sound: any
powertip: any
userAutocomplete: any