Merge pull request #3797 from ornicar/throttle-debounce
split throttle and debounce, improve debouncerpull/3820/head
commit
30385b6c41
|
@ -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({
|
if (!this.embed && !defined(this.node.dests)) this.socket.sendAnaDests({
|
||||||
variant: this.data.game.variant.key,
|
variant: this.data.game.variant.key,
|
||||||
fen: this.node.fen,
|
fen: this.node.fen,
|
||||||
|
@ -286,25 +286,25 @@ export default class AnalyseCtrl {
|
||||||
}
|
}
|
||||||
|
|
||||||
private sound = li.sound ? {
|
private sound = li.sound ? {
|
||||||
move: throttle(50, false, li.sound.move),
|
move: throttle(50, li.sound.move),
|
||||||
capture: throttle(50, false, li.sound.capture),
|
capture: throttle(50, li.sound.capture),
|
||||||
check: throttle(50, false, li.sound.check)
|
check: throttle(50, li.sound.check)
|
||||||
} : {
|
} : {
|
||||||
move: $.noop,
|
move: $.noop,
|
||||||
capture: $.noop,
|
capture: $.noop,
|
||||||
check: $.noop
|
check: $.noop
|
||||||
};
|
};
|
||||||
|
|
||||||
private onChange: () => void = throttle(300, false, () => {
|
private onChange: () => void = throttle(300, () => {
|
||||||
if (this.opts.onChange) {
|
if (this.opts.onChange) {
|
||||||
const mainlinePly = this.onMainline ? this.node.ply : false;
|
const mainlinePly = this.onMainline ? this.node.ply : false;
|
||||||
this.opts.onChange!(this.node.fen, this.path, mainlinePly);
|
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);
|
if (!this.opts.study) window.history.replaceState(null, '', '#' + this.node.ply);
|
||||||
}, false);
|
}, 750);
|
||||||
|
|
||||||
autoScroll(): void {
|
autoScroll(): void {
|
||||||
this.autoScrollRequested = true;
|
this.autoScrollRequested = true;
|
||||||
|
@ -602,7 +602,7 @@ export default class AnalyseCtrl {
|
||||||
return !this.gameOver() && !this.node.threefold;
|
return !this.gameOver() && !this.node.threefold;
|
||||||
}
|
}
|
||||||
|
|
||||||
startCeval = throttle(800, false, () => {
|
startCeval = throttle(800, () => {
|
||||||
if (this.ceval.enabled()) {
|
if (this.ceval.enabled()) {
|
||||||
if (this.canUseCeval()) {
|
if (this.canUseCeval()) {
|
||||||
this.ceval.start(this.path, this.nodeList, this.threatMode(), false);
|
this.ceval.start(this.path, this.nodeList, this.threatMode(), false);
|
||||||
|
|
|
@ -64,7 +64,7 @@ export function make(opts): EvalCache {
|
||||||
return fenFetched.indexOf(node.fen) !== -1;
|
return fenFetched.indexOf(node.fen) !== -1;
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
onCeval: throttle(500, false, function() {
|
onCeval: throttle(500, function() {
|
||||||
const node = opts.getNode(), ev = node.ceval;
|
const node = opts.getNode(), ev = node.ceval;
|
||||||
if (ev && !ev.cloud && hasFetched(node) && qualityCheck(ev) && opts.canPut(node)) {
|
if (ev && !ev.cloud && hasFetched(node) && qualityCheck(ev) && opts.canPut(node)) {
|
||||||
opts.send("evalPut", toPutData(opts.variant, ev));
|
opts.send("evalPut", toPutData(opts.variant, ev));
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { throttle, prop, storedProp } from 'common';
|
import { prop, storedProp } from 'common';
|
||||||
import { controller as configCtrl } from './explorerConfig';
|
import { controller as configCtrl } from './explorerConfig';
|
||||||
import xhr = require('./openingXhr');
|
import xhr = require('./openingXhr');
|
||||||
import { synthetic } from '../util';
|
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 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 fen = root.node.fen;
|
||||||
const request: JQueryPromise<ExplorerData> = (withGames && tablebaseRelevant(effectiveVariant, fen)) ?
|
const request: JQueryPromise<ExplorerData> = (withGames && tablebaseRelevant(effectiveVariant, fen)) ?
|
||||||
xhr.tablebase(opts.tablebaseEndpoint, effectiveVariant, fen) :
|
xhr.tablebase(opts.tablebaseEndpoint, effectiveVariant, fen) :
|
||||||
|
@ -55,7 +55,7 @@ export default function(root: AnalyseCtrl, opts, allow: boolean): ExplorerCtrl {
|
||||||
failing(true);
|
failing(true);
|
||||||
root.redraw();
|
root.redraw();
|
||||||
});
|
});
|
||||||
}, false);
|
}, 250, true);
|
||||||
|
|
||||||
const empty = {
|
const empty = {
|
||||||
opening: true,
|
opening: true,
|
||||||
|
|
|
@ -35,7 +35,7 @@ export function ctrl(root: AnalyseCtrl): CommentForm {
|
||||||
doSubmit(text);
|
doSubmit(text);
|
||||||
};
|
};
|
||||||
|
|
||||||
const doSubmit = throttle(500, false, (text: string) => {
|
const doSubmit = throttle(500, (text: string) => {
|
||||||
const cur = current();
|
const cur = current();
|
||||||
if (cur) root.study!.makeChange('setComment', {
|
if (cur) root.study!.makeChange('setComment', {
|
||||||
ch: cur.chapterId,
|
ch: cur.chapterId,
|
||||||
|
|
|
@ -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', {
|
ctrl.socket.send('setGamebook', {
|
||||||
path: ctrl.path,
|
path: ctrl.path,
|
||||||
ch: ctrl.study!.vm.chapterId,
|
ch: ctrl.study!.vm.chapterId,
|
||||||
|
|
|
@ -207,7 +207,7 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
|
||||||
).then(onReload, li.reload);
|
).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({
|
if (vm.mode.sticky && path !== data.position.path) makeChange("setPath", addChapterId({
|
||||||
path
|
path
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -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({
|
root.study!.makeChange('toggleGlyph', root.study!.withPosition({
|
||||||
id
|
id
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -86,7 +86,7 @@ function renderPgnTags(chapter: StudyChapter, submit, types: string[]): VNode {
|
||||||
|
|
||||||
export function ctrl(root: AnalyseCtrl, getChapter: () => StudyChapter, types) {
|
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', {
|
root.study!.makeChange('setTag', {
|
||||||
chapterId: getChapter().id,
|
chapterId: getChapter().id,
|
||||||
name,
|
name,
|
||||||
|
|
|
@ -72,7 +72,7 @@ export default function(opts: CevalOpts): CevalCtrl {
|
||||||
|
|
||||||
let lastEmitFen: string | null = null;
|
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');
|
sortPvsInPlace(ev.pvs, (work.ply % 2 === (work.threatMode ? 1 : 0)) ? 'white' : 'black');
|
||||||
npsRecorder(ev);
|
npsRecorder(ev);
|
||||||
curEval = ev;
|
curEval = ev;
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
/// <reference types="types/lichess" />
|
/// <reference types="types/lichess" />
|
||||||
|
|
||||||
import throttle from './throttle';
|
|
||||||
|
|
||||||
export function defined<A>(v: A | undefined): v is A {
|
export function defined<A>(v: A | undefined): v is A {
|
||||||
return typeof v !== 'undefined';
|
return typeof v !== 'undefined';
|
||||||
}
|
}
|
||||||
|
@ -87,7 +85,28 @@ export function sync<T>(promise: Promise<T>): Sync<T> {
|
||||||
return 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;
|
export type F = () => void;
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -46,7 +46,7 @@ module.exports = function(env, domElement) {
|
||||||
this.vm.filters = {};
|
this.vm.filters = {};
|
||||||
}.bind(this);
|
}.bind(this);
|
||||||
|
|
||||||
var askQuestion = throttle(1000, false, function() {
|
var askQuestion = throttle(1000, function() {
|
||||||
if (!this.validCombinationCurrent()) reset();
|
if (!this.validCombinationCurrent()) reset();
|
||||||
this.pushState();
|
this.pushState();
|
||||||
this.vm.loading = true;
|
this.vm.loading = true;
|
||||||
|
|
|
@ -139,7 +139,7 @@ export default function(opts, redraw: () => void): Controller {
|
||||||
socket.sendAnaMove(move);
|
socket.sendAnaMove(move);
|
||||||
};
|
};
|
||||||
|
|
||||||
var getDests = throttle(800, false, function() {
|
var getDests = throttle(800, function() {
|
||||||
if (!vm.node.dests && treePath.contains(vm.path, vm.initialPath))
|
if (!vm.node.dests && treePath.contains(vm.path, vm.initialPath))
|
||||||
socket.sendAnaDests({
|
socket.sendAnaDests({
|
||||||
fen: vm.node.fen,
|
fen: vm.node.fen,
|
||||||
|
@ -304,7 +304,7 @@ export default function(opts, redraw: () => void): Controller {
|
||||||
if (ceval.enabled() && canUseCeval()) doStartCeval();
|
if (ceval.enabled() && canUseCeval()) doStartCeval();
|
||||||
};
|
};
|
||||||
|
|
||||||
const doStartCeval = throttle(800, false, function() {
|
const doStartCeval = throttle(800, function() {
|
||||||
ceval.start(vm.path, vm.nodeList, threatMode());
|
ceval.start(vm.path, vm.nodeList, threatMode());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -419,7 +419,7 @@ export default function(opts, redraw: () => void): Controller {
|
||||||
|
|
||||||
const callToVote = () => parseInt(nbToVoteCall()) < 1;
|
const callToVote = () => parseInt(nbToVoteCall()) < 1;
|
||||||
|
|
||||||
const vote = throttle(1000, false, function(v) {
|
const vote = throttle(1000, function(v) {
|
||||||
if (callToVote()) thanksUntil = Date.now() + 2000;
|
if (callToVote()) thanksUntil = Date.now() + 2000;
|
||||||
nbToVoteCall(5);
|
nbToVoteCall(5);
|
||||||
vm.voted = v;
|
vm.voted = v;
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { throttle } from 'common';
|
||||||
const sounds = window.lichess.sound;
|
const sounds = window.lichess.sound;
|
||||||
|
|
||||||
export const sound = {
|
export const sound = {
|
||||||
move: throttle(50, false, sounds.move),
|
move: throttle(50, sounds.move),
|
||||||
capture: throttle(50, false, sounds.capture),
|
capture: throttle(50, sounds.capture),
|
||||||
check: throttle(50, false, sounds.check)
|
check: throttle(50, sounds.check)
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,7 +4,7 @@ import RoundController from './ctrl';
|
||||||
|
|
||||||
let element: HTMLElement;
|
let element: HTMLElement;
|
||||||
|
|
||||||
export const reload = throttle(1000, false, (ctrl: RoundController) => {
|
export const reload = throttle(1000, (ctrl: RoundController) => {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: (ctrl.data.player.spectator ?
|
url: (ctrl.data.player.spectator ?
|
||||||
router.game(ctrl.data, ctrl.data.player.color) :
|
router.game(ctrl.data, ctrl.data.player.color) :
|
||||||
|
|
|
@ -138,9 +138,9 @@ export function make(send: SocketSend, ctrl: RoundController): RoundSocket {
|
||||||
return {
|
return {
|
||||||
send,
|
send,
|
||||||
handlers,
|
handlers,
|
||||||
moreTime: throttle(300, false, () => send('moretime')),
|
moreTime: throttle(300, () => send('moretime')),
|
||||||
outoftime: throttle(500, false, () => send('flag', ctrl.data.game.player)),
|
outoftime: throttle(500, () => send('flag', ctrl.data.game.player)),
|
||||||
berserk: throttle(200, false, () => send('berserk', null, { ackable: true })),
|
berserk: throttle(200, () => send('berserk', null, { ackable: true })),
|
||||||
sendLoading(typ: string, data?: any) {
|
sendLoading(typ: string, data?: any) {
|
||||||
ctrl.setLoading(true);
|
ctrl.setLoading(true);
|
||||||
send(typ, data);
|
send(typ, data);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { throttle } from 'common';
|
import { throttle } from 'common';
|
||||||
|
|
||||||
function throttled(sound: string): () => void {
|
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');
|
export const move = throttled('move');
|
||||||
|
|
|
@ -46,17 +46,19 @@ lichess.topMenuIntent = function() {
|
||||||
};
|
};
|
||||||
lichess.fp.debounce = function(func, wait, immediate) {
|
lichess.fp.debounce = function(func, wait, immediate) {
|
||||||
var timeout;
|
var timeout;
|
||||||
|
var lastBounce = 0;
|
||||||
return function() {
|
return function() {
|
||||||
var context = this,
|
var context = this,
|
||||||
args = arguments;
|
args = arguments,
|
||||||
|
elapsed = Date.now() - lastBounce;
|
||||||
|
lastBounce = Date.now();
|
||||||
var later = function() {
|
var later = function() {
|
||||||
timeout = null;
|
timeout = null;
|
||||||
if (!immediate) func.apply(context, args);
|
func.apply(context, args);
|
||||||
};
|
};
|
||||||
var callNow = immediate && !timeout;
|
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
timeout = setTimeout(later, wait);
|
if (immediate && elapsed > wait) func.apply(context, args);
|
||||||
if (callNow) func.apply(context, args);
|
else timeout = setTimeout(later, wait);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -64,9 +64,9 @@ function playerInfo(ctrl: TournamentController, userId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
join: throttle(1000, false, join),
|
join: throttle(1000, join),
|
||||||
withdraw: throttle(1000, false, withdraw),
|
withdraw: throttle(1000, withdraw),
|
||||||
loadPage: throttle(1000, false, loadPage),
|
loadPage: throttle(1000, loadPage),
|
||||||
reloadTournament: throttle(2000, false, reloadTournament),
|
reloadTournament: throttle(2000, reloadTournament),
|
||||||
playerInfo
|
playerInfo
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,7 +21,10 @@ interface Lichess {
|
||||||
escapeHtml(html: string): string
|
escapeHtml(html: string): string
|
||||||
toYouTubeEmbedUrl(url: 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
|
sound: any
|
||||||
powertip: any
|
powertip: any
|
||||||
userAutocomplete: any
|
userAutocomplete: any
|
||||||
|
|
Loading…
Reference in New Issue