// OpenTween - Client of Twitter // Copyright (c) 2022 kim_upsilon (@kim_upsilon) // All rights reserved. // // This file is part of OpenTween. // // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. // // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License // for more details. // // You should have received a copy of the GNU General public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, // Boston, MA 02110-1301, USA. #nullable enable using System; using System.Threading; using System.Threading.Tasks; namespace OpenTween { /// /// コールバック先の関数を 未満の頻度で呼ばないよう制御するタイマー /// /// /// lodash の _.debounce() に相当する機能となっている /// public class DebounceTimer : IDisposable { private readonly ITimer debouncingTimer; private readonly Func timerCallback; private readonly object lockObject = new(); private DateTimeUtc lastCall; private bool calledSinceLastInvoke; private bool refreshTimerEnabled; public TimeSpan Interval { get; } public bool InvokeLeading { get; } public bool InvokeTrailing { get; } public DebounceTimer(Func timerCallback, TimeSpan interval, bool leading, bool trailing) { this.timerCallback = timerCallback; this.Interval = interval; this.InvokeLeading = leading; this.InvokeTrailing = trailing; this.debouncingTimer = this.CreateTimer(this.Execute); this.lastCall = DateTimeUtc.MinValue; this.calledSinceLastInvoke = false; this.refreshTimerEnabled = false; } protected virtual ITimer CreateTimer(Func callback) => new AsyncTimer(callback); public async Task Call() { bool startTimer, invoke; lock (this.lockObject) { this.lastCall = DateTimeUtc.Now; this.calledSinceLastInvoke = true; if (this.refreshTimerEnabled) { startTimer = false; invoke = false; } else { startTimer = true; invoke = this.InvokeLeading; this.refreshTimerEnabled = true; } } if (startTimer) { if (invoke) await this.Invoke().ConfigureAwait(false); this.debouncingTimer.Change(dueTime: this.Interval, period: Timeout.InfiniteTimeSpan); } } private async Task Execute() { bool startTimer, invoke; TimeSpan wait; lock (this.lockObject) { var sinceLastCall = DateTimeUtc.Now - this.lastCall; if (sinceLastCall < TimeSpan.Zero) { // システムの時計が過去の時刻に変更された場合は無限ループを防ぐために lastCall をリセットする this.lastCall = DateTimeUtc.Now; sinceLastCall = TimeSpan.Zero; } if (sinceLastCall < this.Interval) { startTimer = true; wait = this.Interval - sinceLastCall; invoke = false; } else { startTimer = false; wait = TimeSpan.Zero; invoke = this.InvokeTrailing && this.calledSinceLastInvoke; this.refreshTimerEnabled = false; } } if (invoke) await this.Invoke().ConfigureAwait(false); if (startTimer) this.debouncingTimer.Change(dueTime: wait, period: Timeout.InfiniteTimeSpan); } private async Task Invoke() { await Task.Run(async () => { lock (this.lockObject) this.calledSinceLastInvoke = false; await this.timerCallback().ConfigureAwait(false); }); } public void Dispose() => this.debouncingTimer.Dispose(); public static DebounceTimer Create(Func callback, TimeSpan wait, bool leading = false, bool trailing = true) => new(callback, wait, leading, trailing); } }